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

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
@@ -2334,7 +2334,7 @@ function withLaunchNames(windows) {
2334
2334
  }
2335
2335
 
2336
2336
  // packages/derive/dist/src/model.js
2337
- var DERIVATION_VERSION = 28;
2337
+ var DERIVATION_VERSION = 29;
2338
2338
  // packages/stats/dist/src/core/rng.js
2339
2339
  function splitmix32(seed) {
2340
2340
  let s = seed >>> 0;
@@ -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
  }
@@ -5003,7 +5006,7 @@ function keyFirstOpens(windows) {
5003
5006
  }
5004
5007
 
5005
5008
  // packages/derive/dist/src/derive.js
5006
- var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28];
5009
+ var DERIVATION_VERSIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28, 29];
5007
5010
  var windowsForVersion = (windows, version = DERIVATION_VERSION) => version >= 22 ? keyFirstOpens(windows) : windows;
5008
5011
  function stepWindowsFor(windows, version = DERIVATION_VERSION) {
5009
5012
  return windowsForVersion(version >= 16 ? withLaunchNames(windows).filter((w) => !isBackgroundWindow(w)) : windows, version);
@@ -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";
@@ -5307,6 +5332,17 @@ function valueText(measure, v) {
5307
5332
  const def = defOf(measure);
5308
5333
  return format(def.unit, def.decimals, v);
5309
5334
  }
5335
+ function rangeText(measure, lo, hi) {
5336
+ const a = valueText(measure, lo);
5337
+ const b = valueText(measure, hi);
5338
+ if (a === b)
5339
+ return a;
5340
+ const unitOf = (text) => text.slice(text.lastIndexOf(" ") + 1);
5341
+ const shared = a.includes(" ") && b.includes(" ") && unitOf(a) === unitOf(b);
5342
+ if (shared)
5343
+ return `${a.slice(0, a.lastIndexOf(" "))}–${b}`;
5344
+ return a.includes(" ") || b.includes(" ") ? `${a} – ${b}` : `${a}–${b}`;
5345
+ }
5310
5346
  var KEY_SEPARATOR = ":";
5311
5347
  function breakdownMeasure(field) {
5312
5348
  const at = field.indexOf(KEY_SEPARATOR);
@@ -5384,12 +5420,18 @@ var screenLeadText = (tier, subject, measure, main, pr) => `${STATE_WORDS[TIER_S
5384
5420
  var FOR_INFORMATION = "For information";
5385
5421
  var informationLeadText = (subject, measure, main, pr) => `${FOR_INFORMATION}: ${leadBody(subject, measure, main, pr)}`;
5386
5422
  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;
5423
+ return namePartsText(placeShownNameParts(p, o));
5424
+ }
5425
+ function placeShownNameParts(p, o = {}) {
5426
+ const target = p.target ? componentNameParts(p.target.name, p.target.file) : plainParts(libraryViewName(p.kind, p.host ?? ""));
5427
+ const named = p.target && p.parent ? [...target, textPart(" in "), ...componentNameParts(p.parent.name, p.parent.file), textPart(p.k ? `, ${ordinal(p.k)}` : "")] : target;
5428
+ const screen = typeof o.onScreen === "string" ? plainParts(o.onScreen) : o.onScreen;
5429
+ return nameParts(screen?.length && namePartsText(screen) ? [...named, textPart(" on "), ...screen] : named);
5390
5430
  }
5391
5431
  var libraryViewName = (kind, hostType) => `${kind === "drag" ? "Scroll" : "Tap"} outside app components (${hostType})`;
5392
- function componentShownName(name, file) {
5432
+ var componentShownName = (name, file) => componentFileName(name, file);
5433
+ var componentNameParts = (name, file) => [componentPart(componentFileName(name, file))];
5434
+ function componentFileName(name, file) {
5393
5435
  if (name !== "default" || !file)
5394
5436
  return name;
5395
5437
  const path = file.replace(/\.[^.\/]+$/, "");
@@ -5456,21 +5498,36 @@ function foldTwins(rows, placeOf, also = () => "") {
5456
5498
  return { rows: out, folded: rows.length - out.length };
5457
5499
  }
5458
5500
  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 = {}) {
5501
+ function placeIdShownNameParts(id, o = {}) {
5460
5502
  const p = placeParts(id);
5461
5503
  if (!p)
5462
5504
  return null;
5463
5505
  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 ?? "");
5506
+ 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
5507
  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 });
5508
+ return nameParts([...screen, textPart(p.kind === "screen" && typeof p.visit === "number" && p.visit > 1 ? ` (${ordinal(p.visit)} visit)` : "")]);
5509
+ 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
5510
  }
5469
5511
  var isLibraryPlace = (id) => placeParts(id)?.outsideHost != null;
5470
- function stemName(stem, kind) {
5471
- const shown = placeIdShownName(stem);
5512
+ function dropTail(parts, n) {
5513
+ const out = [];
5514
+ for (const p of [...parts].reverse()) {
5515
+ if (n >= p.text.length) {
5516
+ n -= p.text.length;
5517
+ continue;
5518
+ }
5519
+ out.unshift({ ...p, text: p.text.slice(0, p.text.length - n) });
5520
+ n = 0;
5521
+ }
5522
+ return out;
5523
+ }
5524
+ function stemNameParts(stem, kind) {
5525
+ const shown = placeIdShownNameParts(stem);
5472
5526
  if (shown !== null)
5473
- return isLibraryPlace(stem) ? shown : `${KIND_WORD[kind]}: ${shown}`;
5527
+ return nameParts(isLibraryPlace(stem) ? shown : [textPart(`${KIND_WORD[kind]}: `), ...shown]);
5528
+ return plainParts(stemText(stem, kind));
5529
+ }
5530
+ function stemText(stem, kind) {
5474
5531
  if (kind === "journey")
5475
5532
  return PLACE_KIND_WORDS.journey;
5476
5533
  if (kind === "other")
@@ -5486,12 +5543,13 @@ function stemName(stem, kind) {
5486
5543
  const visit = visitMark ? Number(visitMark[2]) : 1;
5487
5544
  return `${PLACE_KIND_WORDS.screen}: ${screenName(visitMark ? visitMark[1] : rest)}${visit > 1 ? ` (${ordinal(visit)} visit)` : ""}`;
5488
5545
  }
5489
- function windowName(id) {
5546
+ var windowName = (id) => namePartsText(windowNameParts(id));
5547
+ function windowNameParts(id) {
5490
5548
  if (id === "flow")
5491
- return PLACE_KIND_WORDS.journey;
5549
+ return plainParts(PLACE_KIND_WORDS.journey);
5492
5550
  const ref = placeRef(id);
5493
- const name = stemName(ref.stem, ref.kind);
5494
- return ref.segment === "after3s" ? `${name} · ${PLACE_KIND_WORDS.after3s}` : name;
5551
+ const name = stemNameParts(ref.stem, ref.kind);
5552
+ return ref.segment === "after3s" ? nameParts([...name, textPart(` · ${PLACE_KIND_WORDS.after3s}`)]) : name;
5495
5553
  }
5496
5554
  function placeKindWord(id) {
5497
5555
  if (id === "flow")
@@ -5499,31 +5557,33 @@ function placeKindWord(id) {
5499
5557
  const { kind } = placeRef(id);
5500
5558
  return kind === "other" ? "Window" : KIND_WORD[kind];
5501
5559
  }
5502
- function windowQualified(id) {
5560
+ var windowQualified = (id) => namePartsText(windowQualifiedParts(id));
5561
+ function windowQualifiedParts(id) {
5503
5562
  const word = placeOpenWord(id);
5504
- const label = placeLabel(id);
5563
+ const label = placeLabelParts(id);
5505
5564
  if (!word)
5506
5565
  return label;
5507
5566
  const after = placeRef(id).segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
5508
- return `${after ? label.slice(0, -after.length) : label} (${word})${after}`;
5567
+ return nameParts([...after ? dropTail(label, after.length) : label, textPart(` (${word})${after}`)]);
5509
5568
  }
5510
5569
  var ELEMENT_SEPARATORS = /(?<!\d)\.|\.(?!\d)|[_-]+/g;
5511
5570
  var elementWords = (testId) => testId.replace(ELEMENT_SEPARATORS, " ").replace(/\s+/g, " ").trim();
5512
- function placeLabel(id, screen) {
5571
+ var placeLabel = (id, screen) => namePartsText(placeLabelParts(id, screen));
5572
+ function placeLabelParts(id, screen) {
5513
5573
  if (id === "flow")
5514
- return PLACE_KIND_WORDS.journey;
5574
+ return plainParts(PLACE_KIND_WORDS.journey);
5515
5575
  const ref = placeRef(id);
5516
5576
  const after = ref.segment === "after3s" ? ` · ${PLACE_KIND_WORDS.after3s}` : "";
5517
- const shown = placeIdShownName(ref.stem);
5577
+ const shown = placeIdShownNameParts(ref.stem);
5518
5578
  if (shown !== null)
5519
- return isLibraryPlace(ref.stem) ? `${shown}${after}` : `${KIND_WORD[ref.kind]} · ${shown}${after}`;
5579
+ return nameParts([...isLibraryPlace(ref.stem) ? [] : [textPart(`${KIND_WORD[ref.kind]} · `)], ...shown, textPart(after)]);
5520
5580
  if (ref.kind === "press" || ref.kind === "drag" || ref.kind === "step") {
5521
5581
  const element = elementWords(ref.stem.slice(ref.stem.indexOf(":") + 1).replace(OPEN_MARK, ""));
5522
- return `${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`;
5582
+ return plainParts(`${KIND_WORD[ref.kind]} · “${element}”${screen ? ` · ${screen}` : ""}${after}`);
5523
5583
  }
5524
5584
  const name = windowName(ref.stem);
5525
5585
  const colon = name.indexOf(": ");
5526
- return `${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`;
5586
+ return plainParts(`${colon > 0 && ref.kind !== "other" ? `${name.slice(0, colon)} · ${name.slice(colon + 2)}` : name}${after}`);
5527
5587
  }
5528
5588
  // packages/present/dist/src/placeNames.js
5529
5589
  function shortestPaths(files) {
@@ -5542,42 +5602,34 @@ function shortestPaths(files) {
5542
5602
  }
5543
5603
  return out;
5544
5604
  }
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
- }
5605
+ // packages/present/dist/src/words.js
5606
+ var REASON_WORDS = {
5607
+ "no-baseline": "there are no main runs on this phone and journey yet",
5608
+ "too-few-runs": "there are too few valid runs on a side",
5609
+ "judged-measures-missing": "the judged measures are missing on a side",
5610
+ "measured-differently": "the sides were measured differently",
5611
+ "runs-unknown": "a change that would count has no value in some runs, so it cannot be confirmed in every run",
5612
+ "facts-version-mismatch": "the sides were derived with different versions",
5613
+ "not-judged-on-simulators": "timing is not judged on simulators",
5614
+ "platforms-differ": "the sides ran on different platforms (iOS and Android are never compared)",
5615
+ "not-enough-main": "main does not have enough sessions on this phone and journey yet"
5580
5616
  };
5617
+ var waitingWords = (runs) => `${runs} main ${runs === 1 ? "run waits" : "runs wait"} for a reprocess`;
5618
+ var AT_MOST = "≤ ";
5619
+ var atMost = (text, isBound) => isBound ? `${AT_MOST}${text}` : text;
5620
+ var AT_LEAST = "≥ ";
5621
+ var atLeast = (text, isBound) => isBound ? `${AT_LEAST}${text}` : text;
5622
+
5623
+ // packages/present/dist/src/place-tier.js
5624
+ var slowerCountsText = (n, sep = " · ") => [n.worse ? `${n.worse} ${STATE_WORDS.worse.toLowerCase()}` : "", n.slight ? `${n.slight} ${STATE_WORDS.warning.toLowerCase()}` : ""].filter(Boolean).join(sep);
5625
+
5626
+ // packages/present/dist/src/shown.js
5627
+ function feltRangeText(ms, byMs, direction) {
5628
+ if (!ms || !(ms > 0))
5629
+ return null;
5630
+ return `${rangeText("jank_ms", ms, byMs !== null && byMs !== undefined && byMs - ms >= 1 ? byMs : ms)} ${direction}`;
5631
+ }
5632
+
5581
5633
  // packages/present/dist/src/stability.js
5582
5634
  var UNSTABLE_WORDS = {
5583
5635
  short: "main varies too much here",
@@ -5724,7 +5776,7 @@ var READ_USAGE = {
5724
5776
  page: "--page must be a whole number from 1; use it only with places or compare.",
5725
5777
  all: "--all is for places or compare; do not combine it with --page, --offset or --short.",
5726
5778
  detailFlag: " --detail (places: include recorded run detail; also included by --verbose or component paging)",
5727
- detail: "Include recorded per-run component and window detail.",
5779
+ 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
5780
  compareTop: "--top-components must be a whole number from 1 to 200 for compare.",
5729
5781
  top: "--top-components must be a whole number from 0 to 20, or from 1 to 200 with --page or --all.",
5730
5782
  paging: "The API did not advance the component page; stopped without claiming the list is complete.",
@@ -5734,23 +5786,123 @@ var READ_USAGE = {
5734
5786
  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
5787
  var notMeasuredText = (side) => `${READ_WORDS.notMeasured} in run ${side.toUpperCase()}`;
5736
5788
 
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"
5789
+ // packages/present/dist/src/problems.js
5790
+ var FAR_OVER_BUDGET_WORD = "Far over budget";
5791
+ var heaviestWords = (name, value) => [textPart(`${HEAVIEST_WORD}: `), componentPart(name), ...value ? [textPart(` ${value}`)] : []];
5792
+ var HEAVIEST_WORD = "heaviest";
5793
+ var PROBLEM_TAG_WORDS = { new: NEW_IN_PR_CHIP, main: "Also on main", none: NOT_COMPARED_WORD };
5794
+ var PROBLEM_TAG_LINE_WORDS = { new: "new in this PR", main: "also on main", none: "not compared with main" };
5795
+ var problems = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "problem" : "problems"}`;
5796
+ var places = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
5797
+ var problemsAtPlaces = (n, at) => `${problems(n)} at ${places(at)}`;
5798
+ function problemCountsText(c, own) {
5799
+ const tags = c.tags;
5800
+ 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(", ")}` : "";
5801
+ return `${problemsAtPlaces(c.count, c.places)} in ${own} own runs${split}${c.far_count ? `; ${c.far_count} of them ${FAR_OVER_BUDGET_WORD.toLowerCase()}` : ""}`;
5802
+ }
5803
+ var newProblems = (n) => `${n.toLocaleString("en-US")} new ${n === 1 ? "problem" : "problems"}`;
5804
+ var PROBLEMS_BLOCK_WORDS = {
5805
+ branch: {
5806
+ heading: "New problems in this branch",
5807
+ sub: "too slow on their own, whatever main does; they don’t block the merge",
5808
+ seeAll: (n) => `See all ${newProblems(n)}`,
5809
+ alsoLine: (n, at) => `${PROBLEM_TAG_WORDS.main}: ${problemsAtPlaces(n, at)}`,
5810
+ notComparedLine: (n, at) => `${PROBLEM_TAG_WORDS.none}: ${problemsAtPlaces(n, at)}`,
5811
+ choices: { new: PROBLEM_TAG_WORDS.new, also: PROBLEM_TAG_WORDS.main, not_compared: PROBLEM_TAG_WORDS.none },
5812
+ panelTitle: "Problems in this branch",
5813
+ panelNote: "Each one is too slow in this branch’s own runs, whatever main does. Problems never block.",
5814
+ filter: "Which problems"
5815
+ },
5816
+ main: {
5817
+ heading: "New problems in this session",
5818
+ sub: "too slow on their own, whatever earlier sessions did",
5819
+ seeAll: (n) => `See all ${newProblems(n)}`,
5820
+ alsoLine: (n, at) => `Also in earlier sessions: ${problemsAtPlaces(n, at)}`,
5821
+ notComparedLine: (n, at) => `Not compared with earlier sessions: ${problemsAtPlaces(n, at)}`,
5822
+ choices: { new: "New in this session", also: "Also in earlier sessions", not_compared: "Not compared with earlier sessions" },
5823
+ panelTitle: "Problems in this session",
5824
+ panelNote: "Each one is too slow in this session’s own runs, whatever earlier sessions did. Problems never block.",
5825
+ filter: "Which problems"
5826
+ }
5748
5827
  };
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;
5828
+ // packages/present/dist/src/tip.js
5829
+ function tip(headline, rows = [], foot) {
5830
+ const made = rows.flatMap((r) => r ? [Array.isArray(r) ? { label: r[0], value: r[1] } : r] : []);
5831
+ return foot ? { headline, rows: made, foot } : { headline, rows: made };
5832
+ }
5833
+
5834
+ // packages/present/dist/src/verdict-section.js
5835
+ var places2 = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
5836
+ var of = (n, total) => `${n.toLocaleString("en-US")} of ${places2(total)}`;
5837
+ var runs = (n) => `${n} valid ${n === 1 ? "run" : "runs"}`;
5838
+ var VERDICT_SECTION_WORDS = {
5839
+ slower: (n, total, slight) => `${slight ? "A little slower" : "Slower"} at ${of(n, total)}`,
5840
+ blocksMerge: "blocks the merge",
5841
+ faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
5842
+ mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
5843
+ unpublishedSdk: "ran an unpublished SDK build",
5844
+ 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)})`,
5845
+ 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`,
5846
+ tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
5847
+ tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
5848
+ tooFewBefore: (valid) => `Not compared: the sessions before have only ${runs(valid)}`,
5849
+ noBaseline: { branch: "Nothing to compare with: no main runs on this phone yet", main: "Nothing to compare with: no earlier sessions on this phone" },
5850
+ noisy: "Not judged: main’s own runs vary too much at every place here",
5851
+ versionMismatch: "Not compared: the sides were computed with different analysis versions",
5852
+ unconfirmed: (n) => `Can’t confirm ${places2(n)}: a run has no value there`,
5853
+ notCompared: (reason) => `Not compared: ${reason}`,
5854
+ notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
5855
+ 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`,
5856
+ doesNotBlock: "doesn’t block",
5857
+ fixes: {
5858
+ branch: {
5859
+ "no-baseline": "Add this journey on this phone to main’s nightly runs.",
5860
+ "too-few-runs": "Run this branch again on this phone.",
5861
+ "judged-measures-missing": "Record the judged measures, then run again.",
5862
+ "runs-unknown": "Run this branch again; a full set of runs decides it.",
5863
+ "facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed.",
5864
+ "platforms-differ": "Add this phone’s platform to main’s nightly runs."
5865
+ },
5866
+ main: {
5867
+ "no-baseline": "Nothing to do: main’s next 3 nightly sessions make a result possible.",
5868
+ "too-few-runs": "Main’s next nightly session decides it.",
5869
+ "runs-unknown": "Main’s next nightly session decides it.",
5870
+ "judged-measures-missing": "Record the judged measures on main; its next nightly session is compared.",
5871
+ "facts-version-mismatch": "Recompute both sides in Setup; no new runs are needed."
5872
+ }
5873
+ },
5874
+ waitForMain: { branch: "Nothing to do on this branch; main’s next nightly session decides it.", main: "Main’s next nightly session decides it." },
5875
+ sides: {
5876
+ branch: { left: "Main", right: "This branch", rightShort: "Branch" },
5877
+ main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
5878
+ runSet: { left: "Main", right: "This run set", rightShort: "Run set" }
5879
+ },
5880
+ branchSides: (name) => ({ left: "Main", right: name || "This branch", rightShort: "Branch" }),
5881
+ inProgress: (arrived, expected) => `${arrived} of ${expected} runs arrived; the result comes with the last one`,
5882
+ whatChangedLabel: "What changed",
5883
+ moreSlower: (n) => `+${n.toLocaleString("en-US")} more slower`,
5884
+ slowerPanel: (n) => `${places2(n)} slower`,
5885
+ forInformation: "for information",
5886
+ allFilter: "All",
5887
+ filter: {
5888
+ label: "Show places",
5889
+ choice: (tierWord) => upperFirst(tierWord),
5890
+ showing: (choice, n) => `Showing ${choice.toLowerCase()}: ${n}`
5891
+ },
5892
+ countsLabel: "Places by result",
5893
+ links: {
5894
+ openChart: "Open chart",
5895
+ seeAll: (n) => `See all ${places2(n)}`,
5896
+ whichPlaces: "Which places",
5897
+ whichChanges: "Which changes",
5898
+ howToFix: "How to fix",
5899
+ whatDiffers: "What differs",
5900
+ whatChanged: "What changed",
5901
+ openSetup: "Open setup",
5902
+ openScreen: "Open screen"
5903
+ }
5904
+ };
5905
+
5754
5906
  // packages/present/dist/src/main-history.js
5755
5907
  var MAIN_TIER_WORDS = {
5756
5908
  same: "In norm",
@@ -5832,75 +5984,41 @@ function mainCutWords(v) {
5832
5984
  return parts.length ? parts.join(" ") : "Measures alike; no baseline cut.";
5833
5985
  }
5834
5986
  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
- }
5987
+ var MEMORY_HELD_WORDS = { head: "Memory", rest: "still held after the journey", text: "Memory still held after the journey" };
5988
+ var MAIN_CHART_TITLES = {
5989
+ renders: "Places vs their normal range",
5990
+ rendersTip: (baseline) => tip("Places vs their normal range", [
5991
+ ["Bar", "The journey’s places in one session"],
5992
+ ["Colour", `Result against main’s ${baseline} sessions before it`],
5993
+ ["Dashed", `Not judged yet: main is collecting ${baseline} sessions on this setup`]
5994
+ ], "Main collects them after its start or a change"),
5995
+ memory: `${MEMORY_HELD_WORDS.text} · MB`,
5996
+ memoryTip: tip(MEASURES["mem.retained_growth_mb"].label, [
5997
+ ["Shows", "What it still holds after the clean-up at its end"],
5998
+ ["Line", "The median of a session"],
5999
+ ["Band", "The spread inside the session"]
6000
+ ])
6001
+ };
6002
+
6003
+ // packages/present/dist/src/tips.js
6004
+ var LEAD_TIP = tip("A lead, not a confirmed cause", [], "Read from what the app did");
6005
+ // packages/present/dist/src/rules-lines.js
6006
+ var RULES_WORDS = {
6007
+ action: "Recompute",
6008
+ older: "Computed with older rules",
6009
+ marker: "new rules",
6010
+ blocksUnderPrevious: "blocks under the previous rules",
6011
+ bannerText: "New rules: pull requests wait for a recompute",
6012
+ bannerAction: "Recompute",
6013
+ engine: "engine",
6014
+ engineUpdate: "engine update"
5903
6015
  };
6016
+ var sameRulesRef = (a, b) => a.configId === b.configId && a.coreVersion === b.coreVersion && a.derivation === b.derivation && (a.configFingerprint === undefined || b.configFingerprint === undefined || a.configFingerprint === b.configFingerprint);
6017
+ var rulesChangeText = (computed, live) => sameRulesRef(computed, live) ? null : RULES_WORDS.older;
6018
+ var staleRulesText = (rules) => rules ? rulesChangeText(rules.computed, rules.live) : null;
6019
+ var blocksUnderPreviousRulesText = (r) => r.blocking && r.stale ? RULES_WORDS.blocksUnderPrevious : null;
6020
+ var waitingForRecomputeBanner = (r) => r.waiting > 0 ? `${RULES_WORDS.bannerText} · ${RULES_WORDS.bannerAction}` : null;
6021
+
5904
6022
  // packages/present/dist/src/summary-message.js
5905
6023
  var SUMMARY_WORDS = {
5906
6024
  notComputed: "Not judged yet",
@@ -5916,6 +6034,9 @@ var SUMMARY_WORDS = {
5916
6034
  measuredDifferently: "measured differently",
5917
6035
  namesChanged: (sdk, mainSessions, needed) => `Main: ${mainSessions} of ${needed} sessions on SDK ${sdkTag(sdk)}`,
5918
6036
  morePlaces: (n) => `+${n} more ${n === 1 ? "place" : "places"} changed`,
6037
+ slowerPlaces: (n, split) => `${n} ${n === 1 ? "place" : "places"} slower${split && slowerCountsText(split) ? `: ${slowerCountsText(split, ", ")}` : ""}`,
6038
+ noPlaceSlower: "no place slower",
6039
+ toCheck: (n) => `${n} ${n === 1 ? "component" : "components"} to check`,
5919
6040
  compared: {
5920
6041
  branch: (sessions, days) => `vs main’s last ${sessions}${days}`,
5921
6042
  main: (sessions, days) => `newest session vs main’s ${sessions} before${days}`,
@@ -5928,11 +6049,13 @@ var SUMMARY_WORDS = {
5928
6049
  link: {
5929
6050
  comparison: "See the comparison",
5930
6051
  session: "See the session",
6052
+ mainOverTime: "See main over time",
5931
6053
  branch: (branch) => `Open ${branch}`,
5932
6054
  runSet: "Open the run set"
5933
6055
  }
5934
6056
  };
5935
- var ORDER = ["worse", "warning", "cant-tell", "not-computed", "better", "no-change"];
6057
+ var feltOf = (l) => l.state === "worse" || l.state === "warning" ? feltRangeText(l.feltCostMs, l.slowerByMs, "slower") : l.state === "better" ? feltRangeText(l.feltGainMs, l.fasterByMs, "faster") : null;
6058
+ var ORDER = [...STATE_ORDER.slice(0, 3), "not-computed", ...STATE_ORDER.slice(3)];
5936
6059
  var rank = (l) => ORDER.indexOf(l.state);
5937
6060
  var WORD = { ...STATE_WORDS, "not-computed": SUMMARY_WORDS.notComputed };
5938
6061
  var shownLane = (lanes) => [...lanes].sort((a, b) => rank(a) - rank(b) || b.places - a.places)[0];
@@ -5957,6 +6080,17 @@ var changeLine = (c, tier) => {
5957
6080
  const values = c.main !== null && c.now !== null ? `${fromTo(c.main, c.now)}${c.change ? ` (${c.change})` : ""}` : c.change;
5958
6081
  return [c.place ?? upper(c.measure), ...c.place ? [c.measure] : [], values, WORD[c.tier ?? tier]].filter(Boolean).join(" · ");
5959
6082
  };
6083
+ function tierSplit(l) {
6084
+ const worse = new Set, all = new Set;
6085
+ for (const c of l.changes) {
6086
+ const key = c.key ?? c.place ?? c.measure;
6087
+ all.add(key);
6088
+ if ((c.tier ?? l.state) === "worse")
6089
+ worse.add(key);
6090
+ }
6091
+ const split = { worse: worse.size, slight: all.size - worse.size };
6092
+ return l.state === "worse" && !split.slight || l.state === "warning" && !split.worse ? undefined : split;
6093
+ }
5960
6094
  var wordReason = (word) => word?.includes(": ") ? word.slice(word.indexOf(": ") + 2) : null;
5961
6095
  var notJudgedWhy = (l) => l.state === "not-computed" && l.word?.startsWith(`${NOT_JUDGED_WORD}: `) ? upper(l.word.slice(NOT_JUDGED_WORD.length + 2)) : null;
5962
6096
  function why(l) {
@@ -5984,18 +6118,21 @@ var noneMoved = VERDICT_SECTION_WORDS.noneMoved(1).replace(/ \(.*\)$/, "");
5984
6118
  function conditions(l, pr) {
5985
6119
  const moved = l.state === "worse" || l.state === "warning";
5986
6120
  return [
5987
- pr && l.state === "worse" ? l.blocking ? VERDICT_SECTION_WORDS.blocksMerge : NOT_BLOCKING : "",
6121
+ pr && l.state === "worse" ? l.blocking ? blocksUnderPreviousRulesText({ blocking: true, stale: !!l.rules }) ?? VERDICT_SECTION_WORDS.blocksMerge : NOT_BLOCKING : "",
5988
6122
  moved && l.measuredDifferently ? SUMMARY_WORDS.measuredDifferently : "",
5989
6123
  (moved || l.state === "better") && l.fewRuns ? SUMMARY_WORDS.fewRuns : ""
5990
6124
  ].filter(Boolean);
5991
6125
  }
6126
+ var rulesLines = (l) => [staleRulesText(l.rules), waitingForRecomputeBanner({ waiting: l.waiting ?? 0 })].filter((x) => x !== null);
6127
+ function comparedText(main, kind) {
6128
+ const args = [SUMMARY_WORDS.sessions(main.sessions), SUMMARY_WORDS.days(days(main.labels))];
6129
+ return `${SUMMARY_WORDS.compared[kind](...args)} · ${SUMMARY_WORDS.runs(main.runs, main.each)}`;
6130
+ }
5992
6131
  function compared(l, pr, slower) {
5993
6132
  const judged = l.state === "worse" || l.state === "warning" || l.state === "better" || l.state === "no-change";
5994
6133
  if (!judged || !l.main?.sessions)
5995
6134
  return null;
5996
- const args = [SUMMARY_WORDS.sessions(l.main.sessions), SUMMARY_WORDS.days(days(l.main.labels))];
5997
- const what = pr ? SUMMARY_WORDS.compared.branch(...args) : slower ? SUMMARY_WORDS.compared.mainSlower(...args) : SUMMARY_WORDS.compared.main(...args);
5998
- return `${what} · ${SUMMARY_WORDS.runs(l.main.runs, l.main.each)}`;
6135
+ return comparedText(l.main, pr ? "branch" : slower ? "mainSlower" : "main");
5999
6136
  }
6000
6137
  function summaryMessage(input) {
6001
6138
  const l = shownLane(input.lanes);
@@ -6019,11 +6156,42 @@ function summaryMessage(input) {
6019
6156
  input.pr && input.prNumber ? `${input.branch} (#${input.prNumber})` : input.branch,
6020
6157
  phones === 1 ? l.phone ?? "unknown phone" : SUMMARY_WORDS.phones(phones)
6021
6158
  ].join(" · ");
6159
+ const label = input.pr ? SUMMARY_WORDS.link.comparison : SUMMARY_WORDS.link.session;
6160
+ const link = { label, url: (single ? l.url : null) ?? input.url };
6161
+ if (!input.slack)
6162
+ return { state: l.state, newestSlower, lead, ...withLines(changeLines(l, input.pr, newestSlower)), link };
6022
6163
  const lines = [];
6023
- const basis = compared(l, input.pr, newestSlower);
6164
+ if (l.state === "worse" || l.state === "warning") {
6165
+ const n = l.places || l.changes.length;
6166
+ lines.push(upper([...n ? [SUMMARY_WORDS.slowerPlaces(n, tierSplit(l))] : [], feltOf(l)].filter(Boolean).join(" · ") || why(l)));
6167
+ } else if (l.state === "no-change")
6168
+ lines.push(upper(SUMMARY_WORDS.noPlaceSlower));
6169
+ else if (l.state === "cant-tell") {
6170
+ const fix = unblock(l, input.pr);
6171
+ lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
6172
+ } else if (l.state === "better")
6173
+ lines.push([why(l), feltOf(l)].filter(Boolean).join(" · "));
6174
+ else
6175
+ lines.push(why(l));
6176
+ if (l.toCheckCount)
6177
+ lines.push(upper(SUMMARY_WORDS.toCheck(l.toCheckCount)));
6178
+ const basis = compared(l, input.pr, false);
6024
6179
  const extra = conditions(l, input.pr).join(", ");
6180
+ const heat = l.heatedRuns ? SUMMARY_WORDS.heated(l.heatedRuns, l.runs) : null;
6181
+ const context = [basis ? `${basis}${extra ? `; ${extra}` : ""}` : extra, heat].filter(Boolean).join(" · ");
6182
+ if (context)
6183
+ lines.push(basis ? context : upper(context));
6184
+ lines.push(...rulesLines(l));
6185
+ return { state: l.state, newestSlower, lead, ...withLines(lines), link };
6186
+ }
6187
+ function changeLines(l, pr, newestSlower) {
6188
+ const lines = [];
6189
+ const basis = compared(l, pr, newestSlower);
6190
+ const extra = conditions(l, pr).join(", ");
6025
6191
  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] : []];
6192
+ const felt = feltOf(l);
6193
+ const sums = upper([felt, l.toCheckCount ? SUMMARY_WORDS.toCheck(l.toCheckCount) : null].filter(Boolean).join(" · "));
6194
+ const tail = [...sums ? [sums] : [], ...basis || extra ? [basis ? `${basis}${extra ? `; ${extra}` : ""}` : upper(extra)] : [], ...heat ? [heat] : [], ...rulesLines(l)];
6027
6195
  if (l.state === "worse" || l.state === "warning" || l.state === "better") {
6028
6196
  const seen = new Set;
6029
6197
  const places = l.changes.filter((c) => {
@@ -6039,12 +6207,11 @@ function summaryMessage(input) {
6039
6207
  if (top.length && more > 0)
6040
6208
  lines.push(SUMMARY_WORDS.morePlaces(more));
6041
6209
  } else {
6042
- const fix = l.state === "cant-tell" ? unblock(l, input.pr) : null;
6210
+ const fix = l.state === "cant-tell" ? unblock(l, pr) : null;
6043
6211
  lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
6044
6212
  }
6045
6213
  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 } };
6214
+ return lines;
6048
6215
  }
6049
6216
  var withLines = (lines) => ({ lines, detail: lines.join(`
6050
6217
  `) });
@@ -6054,6 +6221,7 @@ function pendingMessage(branch, url) {
6054
6221
  function noResultMessage(branch, reason, url) {
6055
6222
  return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.noResult} · ${clip(branch, NAME.name)}`, ...withLines([reason.replace(/\.$/, "")]), link: url ? { label: SUMMARY_WORDS.link.runSet, url } : null };
6056
6223
  }
6224
+
6057
6225
  // packages/present/dist/src/run-set-message.js
6058
6226
  var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
6059
6227
  function movedItems(v, sc) {
@@ -6142,13 +6310,20 @@ function laneOf(v, sc, set) {
6142
6310
  namesChanged: sc.context?.namesChanged ?? null,
6143
6311
  url: linkOf(sc, set),
6144
6312
  main: sc.main ?? null,
6145
- reason: sc.headline?.reason ?? sc.headline?.reasons?.[0] ?? null
6313
+ reason: sc.headline?.reason ?? sc.headline?.reasons?.[0] ?? null,
6314
+ feltCostMs: sc.headline?.felt_cost_ms ?? null,
6315
+ feltGainMs: sc.headline?.felt_gain_ms ?? null,
6316
+ slowerByMs: sc.headline?.slower_by_ms ?? null,
6317
+ fasterByMs: sc.headline?.faster_by_ms ?? null,
6318
+ toCheckCount: sc.headline?.to_check_count ?? null,
6319
+ rules: sc.headline?.rules ?? null,
6320
+ waiting: sc.headline?.waiting_for_reprocess ?? 0
6146
6321
  };
6147
6322
  }
6148
6323
  var isPending = (s) => lanesOf(s).every((l) => l.state === "not-computed" && !l.word?.startsWith(`${NOT_JUDGED_WORD}: `));
6149
6324
  var lanesOf = (s) => s.verdicts ? s.verdicts.scenarios.map((sc) => laneOf(s.verdicts, sc, s)) : [];
6150
6325
  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 });
6326
+ 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
6327
  var MESSAGE_COLORS = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
6153
6328
  function messageTone(s) {
6154
6329
  const lanes = s ? lanesOf(s) : [];
@@ -6181,16 +6356,10 @@ ${esc(m.detail)}` } },
6181
6356
  }]
6182
6357
  };
6183
6358
  }
6184
- function resultSlackPayload(s, o = {}) {
6185
- if (!s.verdicts)
6359
+ function resultSlackPayload(s) {
6360
+ if (!s.verdicts || !isPr(s))
6186
6361
  return null;
6187
- const lanes = lanesOf(s);
6188
- const moved = lanes.some((l) => l.state === "worse" || l.state === "warning");
6189
- const heat = lanes.some((l) => l.heatedRuns > 0);
6190
- const improved = lanes.some((l) => l.state === "better") || s.verdicts.state === "better";
6191
- if (isPr(s) ? !moved && !heat : !moved && !improved && !heat && !o.always)
6192
- return null;
6193
- return slackPayload(runSetMessage(s), messageTone(s));
6362
+ return slackPayload(runSetMessage(s, { slack: true }), messageTone(s));
6194
6363
  }
6195
6364
  var noResultSlackPayload = (branch, reason, url) => slackPayload(noResultMessage(branch, reason, url), { mark: "⚪", color: MESSAGE_COLORS.none });
6196
6365
  var markdown = (m, mark) => `### ${mark} ${m.lead}
@@ -6203,6 +6372,7 @@ ${m.link ? `
6203
6372
  var resultMarkdown = (s) => markdown(runSetMessage(s), messageTone(s).mark);
6204
6373
  var noResultMarkdown = (branch, reason, url) => markdown(noResultMessage(branch, reason, url), "⚪");
6205
6374
  var pendingMarkdown = (branch, url) => markdown(pendingMessage(branch, url), "⚪");
6375
+
6206
6376
  // packages/present/dist/src/stale.js
6207
6377
  function staleNoteText(why) {
6208
6378
  const newer = why.includes("newer-run-set");
@@ -6220,6 +6390,15 @@ var staleFlagText = (why) => {
6220
6390
  return `${note.charAt(0).toLowerCase()}${note.slice(1)}`;
6221
6391
  };
6222
6392
  var STALE_MARK = "stale result";
6393
+
6394
+ // packages/present/dist/src/main-session-message.js
6395
+ var TONE = {
6396
+ worse: { mark: "\uD83D\uDD34", color: MESSAGE_COLORS.error },
6397
+ slight: { mark: "\uD83D\uDFE1", color: MESSAGE_COLORS.warn },
6398
+ better: { mark: "\uD83D\uDFE2", color: MESSAGE_COLORS.better },
6399
+ cant: { mark: "❔", color: MESSAGE_COLORS.none },
6400
+ same: { mark: "⚪", color: MESSAGE_COLORS.none }
6401
+ };
6223
6402
  // apps/agent/src/upload.ts
6224
6403
  import { join as join4 } from "node:path";
6225
6404
  var MAX_RATE_RETRIES = 5;
@@ -6379,7 +6558,8 @@ var addCounts = (xs) => xs.reduce((a, c) => ({ worse: a.worse + c.worse, warning
6379
6558
  var setupLine = (h) => {
6380
6559
  const parts = [
6381
6560
  h?.measured_differently?.length ? `Measured differently (${h.measured_differently.join(", ")}) — ${NOT_BLOCKING}` : "",
6382
- h?.waiting_for_reprocess ? waitingWords(h.waiting_for_reprocess) : ""
6561
+ h?.waiting_for_reprocess ? waitingWords(h.waiting_for_reprocess) : "",
6562
+ staleRulesText(h?.rules) ?? ""
6383
6563
  ].filter(Boolean);
6384
6564
  return parts.length ? parts.join("; ") : null;
6385
6565
  };
@@ -10589,7 +10769,7 @@ async function aa(deps, outDir, opts = {}) {
10589
10769
  }
10590
10770
 
10591
10771
  // apps/agent/src/farm.ts
10592
- import { dirname as dirname3, join as join13, resolve as resolve5 } from "node:path";
10772
+ import { dirname as dirname4, join as join13, resolve as resolve6 } from "node:path";
10593
10773
 
10594
10774
  // packages/appium-driver/dist/src/links.js
10595
10775
  var ALERT_POLL_MS = 250;
@@ -10893,7 +11073,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
10893
11073
  import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
10894
11074
  import { fileURLToPath } from "node:url";
10895
11075
  function driverVersion() {
10896
- const built = "0.1.0-alpha.19";
11076
+ const built = "0.1.0-alpha.21";
10897
11077
  if (built)
10898
11078
  return built;
10899
11079
  let dir = dirname2(fileURLToPath(import.meta.url));
@@ -10948,9 +11128,15 @@ var SDK_FLUSH_WORST_CASE_MS = 315000;
10948
11128
  var FLUSH_MARGIN_MS = 30000;
10949
11129
  var FLUSH_WAIT_MS = SDK_FLUSH_WORST_CASE_MS + FLUSH_MARGIN_MS;
10950
11130
  var FLUSH_STATUSES = new Set(["ok", "error", "off"]);
11131
+ // packages/verdict-core/dist/src/version.generated.js
11132
+ var CORE_FINGERPRINT = "1aef48c7adfd5630";
11133
+
11134
+ // packages/verdict-core/dist/src/version.js
11135
+ var CORE_VERSION = parseInt(CORE_FINGERPRINT.slice(0, 7), 16);
10951
11136
  // packages/verdict-core/dist/src/window-count.js
10952
11137
  var WINDOW_DEPENDENT_JOURNEY_MEASURES = ["sampler.cpu_ms", "sampler.frames.longest_gap_ms"];
10953
11138
  var OFF = new WeakSet;
11139
+
10954
11140
  // packages/verdict-core/dist/src/clauses.js
10955
11141
  var row = (id, what, unit, cmp, ruling, uncalibrated, value) => ({ id, what, unit, cmp, ruling, uncalibrated, value });
10956
11142
  var NO_RULING = "no ruling: a starting value of the deciding config (L2 audit F-13)";
@@ -11004,6 +11190,12 @@ var CLAUSES = {
11004
11190
  "U-NOISY": row("U-NOISY", "A place is unstable on main above this ratio of spread to allowed, and stays so down to `below`; from this many main runs in this many sessions.", "ratio", ">", "REQ-MET-29, Arek 2026-10-18 ~12:00-13:20; thresholds provisional", false, { above: 1, below: 0.75, minMainRuns: 6, minSessions: 3 })
11005
11191
  };
11006
11192
  var CLAUSE_LIST = Object.values(CLAUSES);
11193
+
11194
+ // packages/verdict-core/dist/src/comp-confirm.js
11195
+ var COMPONENT_K99 = (() => {
11196
+ const f = CLAUSES["T-WARN-NOISE"].value.component;
11197
+ return { a: 0, b: f.c, power: 0.5, lift: f.k99 };
11198
+ })();
11007
11199
  // packages/verdict-core/dist/src/config.js
11008
11200
  var WINDOWS = ["screen", "press", "drag", "step", "other"];
11009
11201
  var STEP = ["step"];
@@ -11225,7 +11417,7 @@ var DRIVER_RELEASES = [
11225
11417
  { affects: LOADED_MEMORY }
11226
11418
  ],
11227
11419
  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)."
11420
+ 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
11421
  },
11230
11422
  {
11231
11423
  version: "0.1.0-alpha.18",
@@ -11243,8 +11435,32 @@ var DRIVER_RELEASES = [
11243
11435
  ],
11244
11436
  uncalibrated: false,
11245
11437
  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)."
11438
+ },
11439
+ {
11440
+ version: "0.1.0-alpha.20",
11441
+ previous: "0.1.0-alpha.19",
11442
+ declares: [
11443
+ { affects: MEASURES_AT("drag"), places: ["drag"] },
11444
+ { affects: MEASURES_AT("step"), places: ["step"] },
11445
+ { affects: MEASURES_AT("journey"), places: ["journey"] }
11446
+ ],
11447
+ uncalibrated: false,
11448
+ 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.'
11449
+ },
11450
+ {
11451
+ version: "0.1.0-alpha.21",
11452
+ previous: "0.1.0-alpha.20",
11453
+ declares: [
11454
+ { affects: MEASURES_AT("drag"), places: ["drag"] },
11455
+ { affects: MEASURES_AT("step"), places: ["step"] },
11456
+ { affects: MEASURES_AT("journey"), places: ["journey"] }
11457
+ ],
11458
+ uncalibrated: false,
11459
+ source: `#491 (appium-driver CHANGELOG "0.1.0-alpha.21", 2026-10-29): in a paged scrollToEnd pass, when a page has not landed by the end of its 3 s wait and the before row is cut by (Android: touches) the list's bottom edge, the pass spends its one extra end gesture and waits again, where alpha.20 failed the run. Such a run sends one more end gesture, one row lookup and one frame read; every other request, body and wait is unchanged. That drag window, the step whose scroll steps contain it and the whole run (one more gesture and coast) can move, on both platforms, so drag, step and journey places are declared (REQ-FARM-35).`
11246
11460
  }
11247
11461
  ];
11462
+ var JOURNEY_RELEASES = {};
11463
+ var CAPTURE_MEASURED_WITH = { aspect: "capture", affects: LOADED_MEMORY, marks: true };
11248
11464
  var UNSTABLE_NOISY = {
11249
11465
  reason: "noisy",
11250
11466
  spread: "sessions",
@@ -11258,7 +11474,6 @@ var UNSTABLE_NOISY = {
11258
11474
  uncalibrated: false,
11259
11475
  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
11476
  };
11261
-
11262
11477
  class ConfigError2 extends Error {
11263
11478
  problems;
11264
11479
  constructor(problems) {
@@ -11322,7 +11537,13 @@ function withClauseValues(base) {
11322
11537
  const valued = (r) => {
11323
11538
  if (r.id === "memory_retained")
11324
11539
  return { ...r, threshold: [part(memory.value.warn)], uncalibrated: memory.uncalibrated };
11325
- return r.role === "judged" ? judgedByNoise(r) : evidence(r);
11540
+ if (r.role === "judged")
11541
+ return judgedByNoise(r);
11542
+ if (r.id in NOISE_FITS) {
11543
+ const { errorNoise: _error, ...shown } = judgedByNoise(r);
11544
+ return shown;
11545
+ }
11546
+ return evidence(r);
11326
11547
  };
11327
11548
  const rules = base.rules.map(valued);
11328
11549
  const frames = (id) => ({ above: { frames: CLAUSES[id].value.frames }, uncalibrated: CLAUSES[id].uncalibrated });
@@ -11384,9 +11605,32 @@ var MEMORY_RETAINED = {
11384
11605
  uncalibrated: true,
11385
11606
  source: MEMORY_PAIR_SOURCE
11386
11607
  };
11608
+ 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
11609
  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)`;
11610
+ var NOISY_WIDEN = {
11611
+ noisyFrom: 1,
11612
+ lineMultiplier: 1,
11613
+ sd: "aa-loo",
11614
+ excludeAbove: null,
11615
+ excludeReturnBelow: null,
11616
+ excludeAboveMs: null,
11617
+ uncalibrated: false,
11618
+ 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`
11619
+ };
11620
+ var COMP_CONFIRM = {
11621
+ maxComponents: 3,
11622
+ share: 0.6,
11623
+ uncalibrated: false,
11624
+ source: `Arek 2026-10-28 ~08:00 idea, ctl 08:10 "Zleć jako pomiar", k-sweep ctl 08:50, deciding ~11:00 "tak" [ctl-arek-comp-confirm]: a place's render sums many components, the component that caused a slowdown carries far less noise; form B (the confirmed components' summed excess past their own lines is the place's excess); k 3 and share 60 % from the k-sweep (docs/stats/2026-10-28-comp-confirm.md); gate: A/A vs simple-4.28, added false Worse 0 and added false Slightly worse at most 1 per 500 splits`
11625
+ };
11626
+ var RELIABLE = {
11627
+ minValidRuns: 3,
11628
+ warmStart: { androidRawFrom: 3, iosFrom: 2 },
11629
+ uncalibrated: false,
11630
+ source: 'Arek via ctl 2026-10-28 ~11:40, corrected ~11:55 ("fair and moderate are not an overheated phone") [ctl-arek-thermal-unreliable]: a run that started at Android SEVERE (3) or above, or iOS serious (2) or above, makes its session unreliable; the SDK maps SEVERE to serious, so both scales read alike. NONE / LIGHT / MODERATE and nominal / fair are normal: listed as started warm, never unreliable. [ctl-arek-three-runs]: a session counts only with 3 valid runs, on main and on the PR (no earlier ruling on partial PR sessions: G-MINRUNS had none). Background: giga2-scratch/noise-p9p-g71/findings.md (P9P false Worse from LIGHT starts stay; the G71 1-run main session no longer counts)'
11631
+ };
11388
11632
  var SHAPE = {
11389
- id: "simple-4.26+noise95-99",
11633
+ id: "simple-4.29+noise95-99",
11390
11634
  minRuns: 2,
11391
11635
  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
11636
  repeats: { kind: "every-pr-beyond-every-main" },
@@ -11408,6 +11652,7 @@ var SHAPE = {
11408
11652
  uncalibrated: false,
11409
11653
  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
11654
  driverReleases: DRIVER_RELEASES,
11655
+ ...Object.keys(JOURNEY_RELEASES).length ? { journeyReleases: JOURNEY_RELEASES } : {},
11411
11656
  unknownDiffers: ["journeyVersion"]
11412
11657
  },
11413
11658
  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 +11660,7 @@ var SHAPE = {
11415
11660
  { ...COMMIT_MAX_JUDGED, id: "jank", on: { measure: "jank_ms" }, source: JANK_SOURCE },
11416
11661
  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
11662
  RENDER_JUDGED,
11418
- COMPONENT_JUDGED,
11663
+ { ...COMPONENT_JUDGED, role: "evidence", source: FELT_SOURCE },
11419
11664
  { ...evidenceRule("commits", { measure: "commits" }, REACT_PLACES, [{ rel: 0.5, abs: 2 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
11420
11665
  { ...evidenceRule("renders", { measure: "renders" }, REACT_PLACES, [{ rel: 0.5, abs: 10 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
11421
11666
  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 +11728,11 @@ var SHAPE = {
11483
11728
  uncalibrated: true,
11484
11729
  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
11730
  },
11731
+ feltCost: {
11732
+ reads: ["jank", "render"],
11733
+ uncalibrated: false,
11734
+ 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'
11735
+ },
11486
11736
  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
11737
  notJudgedLabels: ["launch"],
11488
11738
  unstable: [{
@@ -11491,6 +11741,9 @@ var SHAPE = {
11491
11741
  reads: [COMMIT_MAX_JUDGED, RENDER_JUDGED, COMPONENT_JUDGED, MEMORY_RETAINED],
11492
11742
  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
11743
  }],
11744
+ noisy: NOISY_WIDEN,
11745
+ compConfirm: COMP_CONFIRM,
11746
+ reliable: RELIABLE,
11494
11747
  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
11748
  measuredWith: [
11496
11749
  { aspect: "renderer", affects: REACT_MEASURES },
@@ -11499,7 +11752,8 @@ var SHAPE = {
11499
11752
  { aspect: "screenWindowModel", affects: WINDOW_MODEL_AFFECTS, places: ["screen"], marks: true },
11500
11753
  { aspect: "cpuSource", affects: ["sampler.cpu_ms"] },
11501
11754
  { aspect: "causeSplit", affects: REACT_MEASURES, places: REACT_PLACES, marks: true },
11502
- { aspect: "endGc", affects: MEMORY_MEASURES }
11755
+ { aspect: "endGc", affects: MEMORY_MEASURES },
11756
+ CAPTURE_MEASURED_WITH
11503
11757
  ],
11504
11758
  tiers: {
11505
11759
  rel: TIER_REL,
@@ -11523,15 +11777,6 @@ var SHAPE = {
11523
11777
  }
11524
11778
  };
11525
11779
  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
11780
  var P4_EVEN = {
11536
11781
  ...CONFIG,
11537
11782
  id: `${CONFIG.id}+p4-even`,
@@ -11542,6 +11787,51 @@ var P3_COMMIT = withClauseValues({
11542
11787
  id: `${CONFIG.id}+p3-commit`,
11543
11788
  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
11789
  });
11790
+ // apps/agent/src/flow-version.ts
11791
+ import { createHash as createHash2 } from "node:crypto";
11792
+ import { dirname as dirname3, relative as relative3, resolve as resolve5, sep as sep2 } from "node:path";
11793
+ var REFERENCE = /^\s*(?:-\s*)?(?:runFlow|runScript|file)\s*:\s*['"]?([^'"\s#{}$]+\.(?:ya?ml|js))['"]?\s*$/gm;
11794
+ var commonDir = (paths) => {
11795
+ let dir = dirname3(paths[0]);
11796
+ while (!paths.every((p) => p === dir || p.startsWith(dir.endsWith(sep2) ? dir : dir + sep2)))
11797
+ dir = dirname3(dir);
11798
+ return dir;
11799
+ };
11800
+ async function flowsVersion(files, read, skip = []) {
11801
+ if (files.length === 0)
11802
+ return null;
11803
+ const roots = files.map((f) => resolve5(f));
11804
+ const skipped = new Set(skip.map((s) => resolve5(s)));
11805
+ const seen = new Map;
11806
+ const visit = async (file, root) => {
11807
+ if (seen.has(file) || skipped.has(file))
11808
+ return true;
11809
+ const text = await read(file).then((t) => t.replace(/\r\n?/g, `
11810
+ `), () => null);
11811
+ if (text === null)
11812
+ return !root;
11813
+ seen.set(file, text);
11814
+ for (const m of text.matchAll(REFERENCE))
11815
+ await visit(resolve5(dirname3(file), m[1]), false);
11816
+ return true;
11817
+ };
11818
+ for (const r of roots)
11819
+ if (!await visit(r, true))
11820
+ return null;
11821
+ const base = commonDir(roots);
11822
+ const keyed = [...seen].map(([file, text]) => [relative3(base, file).split(sep2).join("/"), text]);
11823
+ const hash = createHash2("sha256");
11824
+ for (const [key, text] of keyed.sort((a, b) => a[0] < b[0] ? -1 : 1))
11825
+ hash.update(key).update("\x00").update(text).update("\x00");
11826
+ return hash.digest("hex").slice(0, 12);
11827
+ }
11828
+ async function maestroCliVersion(deps) {
11829
+ const r = await deps.spawner.run("maestro", ["--version"], { timeoutMs: 30000, killAfterMs: 5000 });
11830
+ if (r.code !== 0)
11831
+ return;
11832
+ return /^\s*v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*$/m.exec(r.stdout)?.[1];
11833
+ }
11834
+
11545
11835
  // apps/agent/src/device-model.ts
11546
11836
  import { tmpdir } from "node:os";
11547
11837
  import { join as join12 } from "node:path";
@@ -11623,7 +11913,7 @@ async function detectSimulator(deps, udid) {
11623
11913
  function slackMessage(s, ctx) {
11624
11914
  if (ctx.blocked)
11625
11915
  return noResultSlackPayload(ctx.branch, ctx.blocked.reason, s ? linkOf(s) : null);
11626
- return s ? resultSlackPayload(s, { always: ctx.always }) : null;
11916
+ return s ? resultSlackPayload(s) : null;
11627
11917
  }
11628
11918
  async function postToSlack(url, payload, fetchImpl = fetch) {
11629
11919
  try {
@@ -11684,6 +11974,19 @@ var maestroLinkFields = (sdkArgs = "") => ({ keepsnappyLinkArgs: linkArgs(sdkArg
11684
11974
 
11685
11975
  // apps/agent/src/farm.ts
11686
11976
  var STATE_VERSION = 1;
11977
+ function plantOf(raw) {
11978
+ let v;
11979
+ try {
11980
+ v = JSON.parse(raw);
11981
+ } catch {
11982
+ return null;
11983
+ }
11984
+ const ok = (p) => {
11985
+ const x = p;
11986
+ 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");
11987
+ };
11988
+ return Array.isArray(v) && v.length > 0 && v.length <= 200 && v.every(ok) ? v : null;
11989
+ }
11687
11990
  var COMPLETION_MARKER = "run:complete";
11688
11991
  var SETUP_FLAGS = [
11689
11992
  ["driver-version", "driver_version"],
@@ -11692,19 +11995,26 @@ var SETUP_FLAGS = [
11692
11995
  ["journey-version", "journey_version"],
11693
11996
  ["flag-set", "flag_set"]
11694
11997
  ];
11695
- function instrumentFlags(f) {
11696
- if (!f["journey-module"] && f.driver !== "appium")
11697
- return {};
11998
+ var LOCAL_PROVIDERS = ["local", "usb", "customer", "simulator", "emulator"];
11999
+ var COMMIT_ID = /^[0-9a-f]{40}$/i;
12000
+ function instrumentFlags(f, maestro = {}) {
12001
+ const given = f["journey-version"]?.trim();
12002
+ const named = given && !COMMIT_ID.test(given) ? given : undefined;
12003
+ if (!f["journey-module"] && f.driver !== "appium") {
12004
+ const driver = f["driver-version"] ?? maestro.maestroVersion;
12005
+ const journey = maestro.flowsVersion ?? named;
12006
+ return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
12007
+ }
11698
12008
  let hash;
11699
12009
  if (f["journey-module"]) {
11700
12010
  try {
11701
- hash = journeyVersion(resolve5(f["journey-module"]));
12011
+ hash = journeyVersion(resolve6(f["journey-module"]));
11702
12012
  } catch {
11703
12013
  hash = undefined;
11704
12014
  }
11705
12015
  }
11706
12016
  const driver = f["driver-version"] ?? driverVersion() ?? undefined;
11707
- const journey = f["journey-version"] ?? hash;
12017
+ const journey = hash ?? named;
11708
12018
  return { ...driver ? { "driver-version": driver } : {}, ...journey ? { "journey-version": journey } : {} };
11709
12019
  }
11710
12020
  var TOKENS_STUB = "// Replaced by `keepsnappy farm open` for one upload. Never commit real tokens.\noutput.keepsnappyTokens = {};\n";
@@ -11765,7 +12075,8 @@ async function farmJourneys(deps, f, rt, journeys) {
11765
12075
  if (!byName.has(j))
11766
12076
  rt.print(`journey ${j}: no flow file named ${j}.yaml in --config/--flows; sent without a title`);
11767
12077
  }
11768
- return journeysFor(journeys.map((j) => ({ name: j, ...byName.get(j) ?? {} })), { readFile: (p) => deps.fs.readFile(p), root: files.length ? await repoRoot(deps) : null });
12078
+ 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 });
12079
+ return { journeys: info, files: files.map((x) => x.file) };
11769
12080
  }
11770
12081
  async function farmOpen(deps, f, rt) {
11771
12082
  const problems = [];
@@ -11782,7 +12093,7 @@ async function farmOpen(deps, f, rt) {
11782
12093
  provider = "simulator";
11783
12094
  model = model ? withSimulatorWord(model) : simulator;
11784
12095
  }
11785
- if (!model && ["local", "usb", "customer", "simulator", "emulator"].includes(provider)) {
12096
+ if (!model && LOCAL_PROVIDERS.includes(provider)) {
11786
12097
  model = await detectDeviceModel(deps, { platform: f.platform, deviceId: f["device-id"]?.trim() }) ?? undefined;
11787
12098
  if (model)
11788
12099
  rt.print(`device model read from the device: ${model}`);
@@ -11809,18 +12120,29 @@ async function farmOpen(deps, f, rt) {
11809
12120
  problems.push("--tokens-js <path> is required (the file the suite overlay puts at flows/keepsnappy/tokens.js)");
11810
12121
  if (!f.state)
11811
12122
  problems.push("--state <path> is required (read by keepsnappy farm close)");
12123
+ const plant = f.plant === undefined ? undefined : plantOf(f.plant);
12124
+ if (plant === null)
12125
+ problems.push('--plant must be a JSON list of { place, kind, factor > 0, unit: "budget" | "MB" }');
11812
12126
  if (problems.length)
11813
12127
  throw new ConfigError(problems);
11814
- const statePath = resolve5(f.state);
11815
- const tokensPath = resolve5(f["tokens-js"]);
12128
+ const statePath = resolve6(f.state);
12129
+ const tokensPath = resolve6(f["tokens-js"]);
11816
12130
  if (await deps.fs.exists(statePath)) {
11817
12131
  throw new ConfigError([`${statePath} already exists: close that run set first (keepsnappy farm close --state ${f.state}) or remove the file`]);
11818
12132
  }
11819
12133
  const api = apiTarget(url, f, rt);
11820
12134
  const now = rt.now ?? Date.now;
11821
12135
  const device = { model, os: f["device-os"] ?? null, provider };
11822
- const journeyInfo = await farmJourneys(deps, f, rt, journeys);
11823
- const withInstrument = { ...f, ...instrumentFlags(f) };
12136
+ const { journeys: journeyInfo, files: flowFiles } = await farmJourneys(deps, f, rt, journeys);
12137
+ const { "driver-version": _driver, "journey-version": given, ...rest } = f;
12138
+ const maestro = !f["journey-module"] && f.driver !== "appium";
12139
+ const flows = maestro ? await flowsVersion(flowFiles, (p) => deps.fs.readFile(p), [tokensPath]) : null;
12140
+ const asked = maestro && !f["driver-version"] && LOCAL_PROVIDERS.includes(provider) ? await maestroCliVersion(deps) : undefined;
12141
+ const instrument = instrumentFlags(f, { ...flows ? { flowsVersion: flows } : {}, ...asked ? { maestroVersion: asked } : {} });
12142
+ if (given?.trim() && given.trim() !== instrument["journey-version"]) {
12143
+ 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"}`);
12144
+ }
12145
+ const withInstrument = { ...rest, ...instrument };
11824
12146
  const setup = Object.fromEntries(SETUP_FLAGS.flatMap(([flag, key]) => withInstrument[flag]?.trim() ? [[key, withInstrument[flag].trim().slice(0, 200)]] : []));
11825
12147
  const base = await baseCommitFor(deps, rt.env, { explicit: f["base-commit"], branch: git.branch, commit: git.commit }, rt.print);
11826
12148
  const main = await mainMerges(deps, rt.env, { branch: git.branch, commit: git.commit });
@@ -11837,7 +12159,13 @@ async function farmOpen(deps, f, rt) {
11837
12159
  journeys: journeyInfo,
11838
12160
  repetitions: repeat,
11839
12161
  source: "farm",
11840
- ...f["completion-marker"] || Object.keys(setup).length ? { meta: { ...f["completion-marker"] ? { completion_marker: COMPLETION_MARKER } : {}, ...Object.keys(setup).length ? { setup } : {} } } : {}
12162
+ ...f["completion-marker"] || Object.keys(setup).length || plant ? {
12163
+ meta: {
12164
+ ...f["completion-marker"] ? { completion_marker: COMPLETION_MARKER } : {},
12165
+ ...Object.keys(setup).length ? { setup } : {},
12166
+ ...plant ? { plant } : {}
12167
+ }
12168
+ } : {}
11841
12169
  }).catch(infra2(`cannot open a run set at ${api.url}`));
11842
12170
  const prefix = `${git.commit ? git.commit.slice(0, 12) : "farm"}-${rs.run_set_id.slice(0, 8)}`;
11843
12171
  const runs = [];
@@ -11868,7 +12196,7 @@ async function farmOpen(deps, f, rt) {
11868
12196
  if (rt.env.GITHUB_ACTIONS === "true")
11869
12197
  for (const t of Object.values(tokens).flat())
11870
12198
  rt.print(`::add-mask::${t}`);
11871
- await deps.fs.mkdir(dirname3(tokensPath));
12199
+ await deps.fs.mkdir(dirname4(tokensPath));
11872
12200
  await deps.fs.writeFile(tokensPath, tokensJs(api.url, rs.run_set_id, tokens, maestroLinkFields(f["sdk-args"] ?? "")));
11873
12201
  const state = {
11874
12202
  version: STATE_VERSION,
@@ -11888,7 +12216,7 @@ async function farmOpen(deps, f, rt) {
11888
12216
  closed_at: null,
11889
12217
  result: null
11890
12218
  };
11891
- await deps.fs.mkdir(dirname3(statePath));
12219
+ await deps.fs.mkdir(dirname4(statePath));
11892
12220
  await deps.fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}
11893
12221
  `);
11894
12222
  await githubOutputs(deps, rt, {
@@ -11994,7 +12322,7 @@ async function farmClose(deps, f, rt) {
11994
12322
  throw new ConfigError(["--state <path> is required (written by keepsnappy farm open)"]);
11995
12323
  if (f["fail-on-worse"] && !f.wait)
11996
12324
  throw new ConfigError(["--fail-on-worse needs --wait"]);
11997
- const statePath = resolve5(f.state);
12325
+ const statePath = resolve6(f.state);
11998
12326
  let state;
11999
12327
  try {
12000
12328
  state = JSON.parse(await deps.fs.readFile(statePath));
@@ -12021,10 +12349,10 @@ async function closeRunSet(deps, f, rt, state, statePath) {
12021
12349
  const api = apiTarget(f.api ?? f["ingest-url"] ?? state.api, f, rt);
12022
12350
  const now = rt.now ?? Date.now;
12023
12351
  const sleep = rt.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
12024
- const out = resolve5(f.out ?? dirname3(statePath));
12352
+ const out = resolve6(f.out ?? dirname4(statePath));
12025
12353
  let bsText = null;
12026
12354
  if (f["bs-build"])
12027
- bsText = await deps.fs.readFile(resolve5(f["bs-build"])).catch(() => null);
12355
+ bsText = await deps.fs.readFile(resolve6(f["bs-build"])).catch(() => null);
12028
12356
  const session = sessionOutcome(bsText);
12029
12357
  const where = `${state.branch}${state.commit ? ` @ ${state.commit.slice(0, 7)}` : ""}`;
12030
12358
  rt.print(`KeepSnappy run set ${state.run_set_id} (${where}): closing`);
@@ -12109,7 +12437,7 @@ async function closeRunSet(deps, f, rt, state, statePath) {
12109
12437
  // apps/agent/src/farm-run.ts
12110
12438
  import { existsSync as existsSync2 } from "node:fs";
12111
12439
  import { createRequire } from "node:module";
12112
- import { dirname as dirname4, join as join16, resolve as resolve6 } from "node:path";
12440
+ import { dirname as dirname5, join as join16, resolve as resolve7 } from "node:path";
12113
12441
  import { fileURLToPath as fileURLToPath2 } from "node:url";
12114
12442
 
12115
12443
  // apps/agent/src/app-source.ts
@@ -12382,10 +12710,10 @@ ${r.stderr}`;
12382
12710
  // apps/agent/src/farm-run.ts
12383
12711
  function appiumDriverEntry(o = {}) {
12384
12712
  const exists = o.exists ?? existsSync2;
12385
- const bundled = join16(o.selfDir ?? dirname4(fileURLToPath2(import.meta.url)), "appium-driver.js");
12713
+ const bundled = join16(o.selfDir ?? dirname5(fileURLToPath2(import.meta.url)), "appium-driver.js");
12386
12714
  if (exists(bundled))
12387
12715
  return bundled;
12388
- const pkgDir = dirname4(o.packageJson ? o.packageJson() : createRequire(import.meta.url).resolve("@keepsnappy/appium-driver/package.json"));
12716
+ const pkgDir = dirname5(o.packageJson ? o.packageJson() : createRequire(import.meta.url).resolve("@keepsnappy/appium-driver/package.json"));
12389
12717
  const built = join16(pkgDir, "dist", "src", "cli.js");
12390
12718
  return exists(built) ? built : join16(pkgDir, "src", "cli.ts");
12391
12719
  }
@@ -12484,7 +12812,7 @@ function planFarmRun(f) {
12484
12812
  ports,
12485
12813
  env,
12486
12814
  driverArgs: f["driver-arg"] ?? [],
12487
- out: resolve6(f.out ?? "farm-run"),
12815
+ out: resolve7(f.out ?? "farm-run"),
12488
12816
  timeoutMinutes,
12489
12817
  killAfterSeconds,
12490
12818
  pollSeconds
@@ -12533,7 +12861,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
12533
12861
  device: p.device,
12534
12862
  app: ctx.app,
12535
12863
  ...f["custom-id"] ? { customId: f["custom-id"] } : {},
12536
- suiteDir: resolve6(f["suite-dir"]),
12864
+ suiteDir: resolve7(f["suite-dir"]),
12537
12865
  tokensPath: f["tokens-path"] ?? "flows/keepsnappy/tokens.js",
12538
12866
  tokensJs: ctx.tokensJs,
12539
12867
  execute: f.execute,
@@ -12559,7 +12887,7 @@ async function runDriver2(deps, rt, f, p, ports, ctx) {
12559
12887
  throw new Error("no Appium driver entry: pass --driver-script");
12560
12888
  const args = [
12561
12889
  entry,
12562
- ...f["journey-module"] ? ["--journey-module", resolve6(f["journey-module"])] : [],
12890
+ ...f["journey-module"] ? ["--journey-module", resolve7(f["journey-module"])] : [],
12563
12891
  "--platform",
12564
12892
  p.platform,
12565
12893
  "--device",
@@ -12636,7 +12964,8 @@ async function farmRun(deps, f, rt, ports) {
12636
12964
  driver: p.driver,
12637
12965
  "tokens-js": tokensJs,
12638
12966
  state: statePath,
12639
- ...commit ? { commit } : {}
12967
+ ...commit ? { commit } : {},
12968
+ ...p.driver !== "appium" && !f["driver-version"] && f["maestro-version"] ? { "driver-version": f["maestro-version"] } : {}
12640
12969
  }, rt);
12641
12970
  if (openCode !== EXIT.ok)
12642
12971
  return openCode;
@@ -12679,15 +13008,15 @@ async function farmRun(deps, f, rt, ports) {
12679
13008
  }
12680
13009
 
12681
13010
  // apps/agent/src/sourcemaps.ts
12682
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
13011
+ import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
12683
13012
  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";
13013
+ import { basename as basename3, join as join17, resolve as resolve8 } from "node:path";
12685
13014
  import { parseArgs as parseArgs2 } from "node:util";
12686
13015
  import { gzipSync } from "node:zlib";
12687
13016
  var MAX_COMPLETE_ROUNDS = 20;
12688
13017
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
12689
13018
  async function prepareComponents(projectRoot, options = {}) {
12690
- const root = resolve7(projectRoot);
13019
+ const root = resolve8(projectRoot);
12691
13020
  const directory = join17(root, ".keepsnappy", "components");
12692
13021
  const configurations = await readdir2(directory, { withFileTypes: true }).catch((error) => {
12693
13022
  if (error.code === "ENOENT")
@@ -12728,7 +13057,7 @@ async function prepareComponents(projectRoot, options = {}) {
12728
13057
  throw new ConfigError([`invalid KeepSnappy component fragment ${file}`]);
12729
13058
  if (!hashes.has(fragment.source)) {
12730
13059
  try {
12731
- hashes.set(fragment.source, createHash2("sha256").update(await readFile4(resolve7(root, fragment.source))).digest("hex"));
13060
+ hashes.set(fragment.source, createHash3("sha256").update(await readFile4(resolve8(root, fragment.source))).digest("hex"));
12732
13061
  } catch (error) {
12733
13062
  if (error.code !== "ENOENT")
12734
13063
  throw error;
@@ -12753,7 +13082,7 @@ async function prepareComponents(projectRoot, options = {}) {
12753
13082
  await rm(temporary, { force: true });
12754
13083
  }
12755
13084
  const gzip = gzipSync(text);
12756
- return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256: createHash2("sha256").update(gzip).digest("hex") };
13085
+ return { file, count: components.length, bytes: Buffer.byteLength(text), gzip, sha256: createHash3("sha256").update(gzip).digest("hex") };
12757
13086
  }
12758
13087
  function stripSourcesContent(map) {
12759
13088
  if (!isObject(map))
@@ -12807,7 +13136,7 @@ async function uploadSourcemaps(opts) {
12807
13136
  const { map, stripped } = stripSourcesContent(parsed);
12808
13137
  const text = Buffer.from(JSON.stringify(map));
12809
13138
  const gz = gzipSync(text);
12810
- const sha256 = createHash2("sha256").update(gz).digest("hex");
13139
+ const sha256 = createHash3("sha256").update(gz).digest("hex");
12811
13140
  const name = basename3(file);
12812
13141
  const announced = await call(f, opts.url, `/v1/builds/${encodeURIComponent(opts.build)}/sourcemaps`, headers, {
12813
13142
  platform: opts.platform,
@@ -12957,7 +13286,8 @@ var SHORT_FORM_NOTES = {
12957
13286
  window_history: `the runs older than the newest ${SHORT_POINTS}`,
12958
13287
  pr_status: "the per-journey dashboard links and the heat note",
12959
13288
  list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}`,
12960
- compare_runs: `the component lines and the measures beyond each place's ${SHORT_COMPARE_MEASURES} times that moved most`
13289
+ compare_runs: `the component lines and the measures beyond each place's ${SHORT_COMPARE_MEASURES} times that moved most`,
13290
+ get_investigation: "the lines of the worse places, the components to check, the problems and the commits"
12961
13291
  };
12962
13292
  var andList = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
12963
13293
 
@@ -13060,7 +13390,7 @@ function measuredLines(l, r, opts) {
13060
13390
  }
13061
13391
  const lane = (x) => {
13062
13392
  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)}` : ""}`;
13393
+ 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
13394
  };
13065
13395
  if (m.lanes.length === 1) {
13066
13396
  r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
@@ -13086,6 +13416,7 @@ function measuredLines(l, r, opts) {
13086
13416
  ...m.lanes.length > shown.length ? [plural2(m.lanes.length - shown.length, "lane") + " beyond the eighth"] : []
13087
13417
  ];
13088
13418
  }
13419
+ var feltWords = (x) => `${x.felt_cost ? ` · ${x.felt_cost}` : ""}${x.to_check ? `; ${SUMMARY_WORDS.toCheck(x.to_check)}` : ""}`;
13089
13420
  function countsLine2(l) {
13090
13421
  const c = l.counts;
13091
13422
  const parts = [
@@ -13139,10 +13470,14 @@ function formatBranchStatus(s, opts = {}) {
13139
13470
  if (s.lanes.length)
13140
13471
  r.both("");
13141
13472
  for (const l of s.lanes) {
13142
- const shown = `${l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)}${l.stale ? ` (${STALE_FLAG})` : ""}`;
13473
+ const blocks = blocksUnderPreviousRulesText({ blocking: l.blocking, stale: !!l.rules }) ?? "blocks";
13474
+ const shown = `${l.result === "blocks" ? `${l.word} (${blocks})` : badgedWord(l.word, l.blocking)}${l.stale && !l.rules ? ` (${STALE_FLAG})` : ""}`;
13143
13475
  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)}` : ""}`);
13476
+ 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
13477
  r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
13478
+ const rulesLine = staleRulesText(l.rules);
13479
+ if (rulesLine)
13480
+ r.both(` ${rulesLine}`);
13146
13481
  r.both(...(l.comparison_notes ?? []).map((line) => ` ${line}`));
13147
13482
  r.full(...(l.session_speed ?? []).map((line) => ` ${line}`));
13148
13483
  for (const f of l.worst)
@@ -13211,11 +13546,11 @@ function formatProblemList(l, opts = {}) {
13211
13546
  r.both(`${staleWords(c.stale_lanes)}: ${c.stale_lanes === 1 ? "its" : "their"} problems are listed, each marked “${STALE_MARK}”.`);
13212
13547
  l.problems.forEach((p, i) => {
13213
13548
  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` : ""}`);
13549
+ r.add(keep, "", `${l.offset + i + 1}. ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}: ${p.text}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
13215
13550
  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
13551
  const top = p.top_components[0];
13217
13552
  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);
13553
+ 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
13554
  if (detail.length)
13220
13555
  r.full(` ${detail.join("; ")}`);
13221
13556
  if (p.url)
@@ -13571,7 +13906,7 @@ class ApiError extends Error {
13571
13906
  this.name = "ApiError";
13572
13907
  }
13573
13908
  }
13574
- async function get(t, path, query = {}) {
13909
+ async function get(t, path, query = {}, send = {}) {
13575
13910
  const u = new URL(path, t.url.endsWith("/") ? t.url : `${t.url}/`);
13576
13911
  for (const [k, v] of Object.entries(query)) {
13577
13912
  if (Array.isArray(v))
@@ -13580,7 +13915,10 @@ async function get(t, path, query = {}) {
13580
13915
  else if (v !== undefined && v !== "")
13581
13916
  u.searchParams.set(k, String(v));
13582
13917
  }
13583
- const r = await (t.fetch ?? fetch)(u, { headers: { "x-keepsnappy-key": t.key, accept: "application/json" } });
13918
+ const headers = { "x-keepsnappy-key": t.key, accept: "application/json" };
13919
+ if (send.body !== undefined)
13920
+ headers["content-type"] = "application/json";
13921
+ const r = await (t.fetch ?? fetch)(u, { method: send.method ?? "GET", headers, ...send.body !== undefined ? { body: JSON.stringify(send.body) } : {} });
13584
13922
  const text = await r.text();
13585
13923
  let body;
13586
13924
  try {
@@ -13592,7 +13930,7 @@ async function get(t, path, query = {}) {
13592
13930
  if (!r.ok) {
13593
13931
  const code = typeof body.error === "string" ? body.error : String(r.status);
13594
13932
  const message = typeof body.message === "string" ? body.message : code;
13595
- const hint = r.status === 401 ? " (check KEEPSNAPPY_KEY)" : r.status === 403 ? " (the key needs the read or run scope)" : "";
13933
+ const hint = r.status === 401 ? " (check KEEPSNAPPY_KEY)" : r.status === 403 ? ` (the key needs the ${send.scope ?? "read or run"} scope)` : "";
13596
13934
  throw new ApiError(r.status, code, `${message}${hint}`);
13597
13935
  }
13598
13936
  return body;
@@ -13958,6 +14296,7 @@ var USAGE = `keepsnappy <command>
13958
14296
  [--config keepsnappy.yml] [--flows <files|dirs|globs,...>] (journey titles: keepsnappy.yml flows.paths, else the flow's name: / top comment)
13959
14297
  [--completion-marker] (the flow posts step run:complete at the end of each iteration; runs without it are invalid)
13960
14298
  [--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)
14299
+ [--plant JSON] (what the build plants against main, [{ place, kind, factor, unit: budget|MB }]: the run set's meta.plant)
13961
14300
  [--sdk-args k=v&...] (over the run defaults profile=1 diag=1; tokens.js carries them to the flows' activation link)
13962
14301
  farm close --state <file> [--wait [--fail-on-worse]] [--bs-build browserstack.ios.json] [--settle-seconds 60] [--max-wait-seconds 300] [--out DIR]
13963
14302
  (--fail-on-worse as for run: with no verdict computed on the run set it checks nothing)
@@ -14053,6 +14392,7 @@ async function main2(argv) {
14053
14392
  "harness-version": { type: "string" },
14054
14393
  "journey-version": { type: "string" },
14055
14394
  "flag-set": { type: "string" },
14395
+ plant: { type: "string" },
14056
14396
  app: { type: "string" },
14057
14397
  driver: { type: "string" },
14058
14398
  "tunnel-for": { type: "string", multiple: true },
@@ -14103,9 +14443,9 @@ async function main2(argv) {
14103
14443
  if (platform === "ios-device" && !o.device)
14104
14444
  throw new ConfigError(["--device <udid> is required for ios-device"]);
14105
14445
  const arms = parseArmEnv(o["arm-env"], parseArms(o.arms));
14106
- const out = resolve8(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
14446
+ const out = resolve9(o.out ?? `keepsnappy-${new Date().toISOString().replace(/[:.]/g, "-")}`);
14107
14447
  const udid = o.device ?? "booted";
14108
- const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve8(out, ".scratch"));
14448
+ const source = platform === "ios-sim" ? new IosSimulatorDataSource(deps, udid) : new IosDeviceDataSource(deps, udid, resolve9(out, ".scratch"));
14109
14449
  const installer = platform === "ios-sim" ? new IosSimulatorInstaller(deps, udid) : new IosDeviceInstaller(deps, udid);
14110
14450
  const bundleId = await bundleIdOf(deps, arms[0]?.target, o["bundle-id"]);
14111
14451
  let markers;
@@ -14124,7 +14464,7 @@ async function main2(argv) {
14124
14464
  outDir: out,
14125
14465
  testCommand: positionals,
14126
14466
  bundleId,
14127
- ...o["ri-capture"] ? { ri: new FileRiSource(deps, resolve8(o["ri-capture"])) } : {},
14467
+ ...o["ri-capture"] ? { ri: new FileRiSource(deps, resolve9(o["ri-capture"])) } : {},
14128
14468
  ...o["mem-window"] ? { memWindow: o["mem-window"] } : {},
14129
14469
  ...o.timeout ? { testTimeoutMs: Number(o.timeout) * 1000 } : {}
14130
14470
  });
@@ -14136,9 +14476,9 @@ async function main2(argv) {
14136
14476
  const dir = positionals[0];
14137
14477
  if (!dir)
14138
14478
  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")}`);
14479
+ const ri = o["ri-capture"] ? new FileRiSource(deps, resolve9(o["ri-capture"])) : undefined;
14480
+ const rows = await extract(deps, resolve9(dir), ri, o["mem-window"], o["clock-offset-ms"] ? Number(o["clock-offset-ms"]) : undefined);
14481
+ console.log(`${rows.length} rows -> ${resolve9(dir, "metrics.csv")}`);
14142
14482
  return 0;
14143
14483
  }
14144
14484
  if (cmd === "aa") {
@@ -14146,7 +14486,7 @@ async function main2(argv) {
14146
14486
  if (!dir)
14147
14487
  throw new ConfigError(["aa needs <out-dir>"]);
14148
14488
  const config = { ...o.reps ? { reps: Number(o.reps) } : {}, ...o.boot ? { nBoot: Number(o.boot) } : {} };
14149
- console.log((await aa(deps, resolve8(dir), { config })).table);
14489
+ console.log((await aa(deps, resolve9(dir), { config })).table);
14150
14490
  return 0;
14151
14491
  }
14152
14492
  console.error(USAGE);