@keepsnappy/agent 0.1.0-alpha.18 → 0.1.0-alpha.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/appium-driver.js +23 -17
- package/dist/keepsnappy.js +363 -133
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Annotated example: `examples/keepsnappy.yml`. Every setting:
|
|
|
70
70
|
|
|
71
71
|
Unknown settings and wrong types are errors, all listed together.
|
|
72
72
|
|
|
73
|
-
Results and exit codes: results never change the exit code by default (ruling D9, no blocking PR checks): `0` also for "More work". `--fail-on-worse` (needs `--wait`) fails the job (exit `3`) only when the verdict computed on that run set is Worse and blocking. Closing a run set never computes its verdict (decision 0031: the operator computes verdicts on demand), so with no computed verdict `--fail-on-worse` checks nothing: the CLI prints `Verdict not computed yet (computed on demand): --fail-on-worse checked nothing` and exits `0`. The CLI exits `2` for a configuration problem (bad or missing `keepsnappy.yml`, unknown or misspelt flags, a glob matching nothing, `--wait` without a branch or key) and `1` for an infrastructure problem (cannot read the device log, an API request failing, an API URL that does not answer as the KeepSnappy API, `--upload` that sent no run: "Blocked"; results stay in the out dir). Failed runs are marked invalid in `runs.ndjson` / `metrics.csv` and ignored by the statistics. The ending summary reads like:
|
|
73
|
+
Results and exit codes: results never change the exit code by default (ruling D9, no blocking PR checks): `0` also for "More work". `--fail-on-worse` (needs `--wait`) fails the job (exit `3`) only when the verdict computed on that run set is Worse and blocking. Closing a run set never computes its verdict (decision 0031: the operator computes verdicts on demand), so with no computed verdict `--fail-on-worse` checks nothing: the CLI prints `Verdict not computed yet (computed on demand): --fail-on-worse checked nothing` and exits `0`. The CLI exits `2` for a configuration problem (bad or missing `keepsnappy.yml`, unknown or misspelt flags, a glob matching nothing, `--wait` without a branch or key) and `1` for an infrastructure problem (cannot read the device log, an API request failing, an API URL that does not answer as the KeepSnappy API, `--upload` that sent no run: "Blocked", or a local run with no valid run; results stay in the out dir). For a local run with no valid run, it prints `No valid run was recorded` with the invalid reasons when known. Failed runs are marked invalid in `runs.ndjson` / `metrics.csv` and ignored by the statistics. The ending summary reads like:
|
|
74
74
|
|
|
75
75
|
```
|
|
76
76
|
7/8 runs valid (dropped: 1 test-failed: exit 1)
|
package/dist/appium-driver.js
CHANGED
|
@@ -507,23 +507,29 @@ class Driver {
|
|
|
507
507
|
});
|
|
508
508
|
await this.cmd("DELETE", "/actions").catch(() => null);
|
|
509
509
|
}
|
|
510
|
-
async drag(from, to, durationMs) {
|
|
510
|
+
async drag(from, to, durationMs, note) {
|
|
511
511
|
to = { x: to.x, y: Math.min(to.y, await this.safeBottomY()) };
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
{
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
512
|
+
const logged = this.log.length;
|
|
513
|
+
try {
|
|
514
|
+
await this.cmd("POST", "/actions", {
|
|
515
|
+
actions: [
|
|
516
|
+
{
|
|
517
|
+
type: "pointer",
|
|
518
|
+
id: "finger1",
|
|
519
|
+
parameters: { pointerType: "touch" },
|
|
520
|
+
actions: [
|
|
521
|
+
{ type: "pointerMove", duration: 0, x: from.x, y: from.y },
|
|
522
|
+
{ type: "pointerDown", button: 0 },
|
|
523
|
+
{ type: "pointerMove", duration: durationMs, x: to.x, y: to.y },
|
|
524
|
+
{ type: "pointerUp", button: 0 }
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
]
|
|
528
|
+
});
|
|
529
|
+
} finally {
|
|
530
|
+
if (note && this.log[logged])
|
|
531
|
+
Object.assign(this.log[logged], note);
|
|
532
|
+
}
|
|
527
533
|
await this.cmd("DELETE", "/actions").catch(() => null);
|
|
528
534
|
}
|
|
529
535
|
async relaunch(bundleId = this.bundleId) {
|
|
@@ -726,7 +732,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
726
732
|
import { fileURLToPath } from "node:url";
|
|
727
733
|
var DRIVER_NAME = "appium";
|
|
728
734
|
function driverVersion() {
|
|
729
|
-
const built = "0.1.0-alpha.
|
|
735
|
+
const built = "0.1.0-alpha.19";
|
|
730
736
|
if (built)
|
|
731
737
|
return built;
|
|
732
738
|
let dir = dirname(fileURLToPath(import.meta.url));
|
package/dist/keepsnappy.js
CHANGED
|
@@ -1953,6 +1953,16 @@ var MEASURES = {
|
|
|
1953
1953
|
decimals: 1,
|
|
1954
1954
|
what: "The part of the slowest screen update that React spent outside every component's own render, in its own bookkeeping"
|
|
1955
1955
|
},
|
|
1956
|
+
jank_ms: {
|
|
1957
|
+
unit: "ms",
|
|
1958
|
+
worse: "higher",
|
|
1959
|
+
at: ["screen", "launch", "press", "drag", "step", "other", "background"],
|
|
1960
|
+
producer: "tracesift",
|
|
1961
|
+
label: "Jank time",
|
|
1962
|
+
short: "jank time",
|
|
1963
|
+
decimals: 1,
|
|
1964
|
+
what: "The time the window's screen updates spent past one 60 Hz frame, added up, read from the full React export"
|
|
1965
|
+
},
|
|
1956
1966
|
commits_over_2_frames: {
|
|
1957
1967
|
unit: "count",
|
|
1958
1968
|
worse: "higher",
|
|
@@ -5203,6 +5213,7 @@ var markerEnv = (url, runId) => ({
|
|
|
5203
5213
|
|
|
5204
5214
|
// packages/present/dist/src/capital.js
|
|
5205
5215
|
var upperFirst = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
5216
|
+
var lowerFirst = (s) => s ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
5206
5217
|
|
|
5207
5218
|
// packages/present/dist/src/placeWords.js
|
|
5208
5219
|
var GENERIC_GROUPS = new Set(["app", "tabs", "root"]);
|
|
@@ -5247,6 +5258,7 @@ var notBlockingBadge = (word, blocking) => word === STATE_WORDS.worse && blockin
|
|
|
5247
5258
|
var BLOCKED_WORD = "Blocked";
|
|
5248
5259
|
var NOT_COMPUTED_WORD = "Verdict not computed yet (computed on demand)";
|
|
5249
5260
|
var NO_BASELINE_WORD = "Waiting for main";
|
|
5261
|
+
var NOT_COMPARED_WORD = "Not compared with main";
|
|
5250
5262
|
var NOT_JUDGED_WORD = "Not judged";
|
|
5251
5263
|
|
|
5252
5264
|
// packages/present/dist/src/values.js
|
|
@@ -5310,6 +5322,13 @@ function fieldValueText(field, v) {
|
|
|
5310
5322
|
const def = fieldDef(field.split(KEY_SEPARATOR)[0]);
|
|
5311
5323
|
return format(def?.unit, def?.decimals ?? FALLBACK_DECIMALS, v);
|
|
5312
5324
|
}
|
|
5325
|
+
function timeMove(field, a, b) {
|
|
5326
|
+
if (a === null || b === null || !(a > 0))
|
|
5327
|
+
return null;
|
|
5328
|
+
const breakdown = breakdownMeasure(field);
|
|
5329
|
+
const unit = breakdown ? BREAKDOWN_MEASURES[breakdown].unit : fieldDef(field.split(KEY_SEPARATOR)[0])?.unit;
|
|
5330
|
+
return unit === "ms" ? Math.abs(b - a) / a : null;
|
|
5331
|
+
}
|
|
5313
5332
|
|
|
5314
5333
|
// packages/present/dist/src/rowWords.js
|
|
5315
5334
|
var GONE_TAILS = {
|
|
@@ -5364,9 +5383,6 @@ function leadBody(subject, measure, main, pr) {
|
|
|
5364
5383
|
var screenLeadText = (tier, subject, measure, main, pr) => `${STATE_WORDS[TIER_STATE[tier]]}: ${leadBody(subject, measure, main, pr)}`;
|
|
5365
5384
|
var FOR_INFORMATION = "For information";
|
|
5366
5385
|
var informationLeadText = (subject, measure, main, pr) => `${FOR_INFORMATION}: ${leadBody(subject, measure, main, pr)}`;
|
|
5367
|
-
function foldedText(n, rows, page) {
|
|
5368
|
-
return `${n} ${GONE_TAILS[page][rows === "components" ? "removed" : "notInRun"]}`;
|
|
5369
|
-
}
|
|
5370
5386
|
function placeShownName(p, o = {}) {
|
|
5371
5387
|
const target = p.target ? componentShownName(p.target.name, p.target.file) : libraryViewName(p.kind, p.host ?? "");
|
|
5372
5388
|
const named = p.target && p.parent ? `${target} in ${componentShownName(p.parent.name, p.parent.file)}${p.k ? `, ${ordinal(p.k)}` : ""}` : target;
|
|
@@ -5427,6 +5443,19 @@ var placeOpenWord = (id) => {
|
|
|
5427
5443
|
const open = placeOpen(id);
|
|
5428
5444
|
return open ? OPEN_WORDS[open] : null;
|
|
5429
5445
|
};
|
|
5446
|
+
var TWIN2 = /\+3s$/;
|
|
5447
|
+
function foldTwins(rows, placeOf, also = () => "") {
|
|
5448
|
+
const seen = new Set;
|
|
5449
|
+
const out = rows.filter((r) => {
|
|
5450
|
+
const key = `${placeOf(r).replace(TWIN2, "")}\x00${also(r)}`;
|
|
5451
|
+
if (seen.has(key))
|
|
5452
|
+
return false;
|
|
5453
|
+
seen.add(key);
|
|
5454
|
+
return true;
|
|
5455
|
+
});
|
|
5456
|
+
return { rows: out, folded: rows.length - out.length };
|
|
5457
|
+
}
|
|
5458
|
+
var foldedTwinsText = (n) => `${n} “${PLACE_KIND_WORDS.after3s}” ${n === 1 ? "twin" : "twins"} folded into ${n === 1 ? "its place" : "their places"}`;
|
|
5430
5459
|
function placeIdShownName(id, o = {}) {
|
|
5431
5460
|
const p = placeParts(id);
|
|
5432
5461
|
if (!p)
|
|
@@ -5513,6 +5542,42 @@ function shortestPaths(files) {
|
|
|
5513
5542
|
}
|
|
5514
5543
|
return out;
|
|
5515
5544
|
}
|
|
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
|
+
}
|
|
5580
|
+
};
|
|
5516
5581
|
// packages/present/dist/src/stability.js
|
|
5517
5582
|
var UNSTABLE_WORDS = {
|
|
5518
5583
|
short: "main varies too much here",
|
|
@@ -5619,8 +5684,6 @@ function resultContextLines(c, verbose = false, short = false) {
|
|
|
5619
5684
|
for (const [screen, rows] of screens) {
|
|
5620
5685
|
const line = notInRunRowsText({ screens: rows.filter((p) => p.kind === "screen").length, places: rows.filter((p) => p.kind !== "screen").length }, c.page);
|
|
5621
5686
|
out.push(`${screen ? `${screen}: ` : ""}${line.lead}${line.rest}`);
|
|
5622
|
-
if (rows.length > 3)
|
|
5623
|
-
out.push(` ${foldedText(rows.length, "places", c.page)}`);
|
|
5624
5687
|
if (!short || rows.length <= 3)
|
|
5625
5688
|
out.push(...rows.map((p) => ` ${windowQualified(p.place)}`));
|
|
5626
5689
|
}
|
|
@@ -5630,8 +5693,6 @@ function resultContextLines(c, verbose = false, short = false) {
|
|
|
5630
5693
|
for (const [place, rows] of removed) {
|
|
5631
5694
|
const line = removedRowsText(rows.length, c.page);
|
|
5632
5695
|
out.push(`${windowQualified(place)}: ${line.lead}${line.rest}`);
|
|
5633
|
-
if (rows.length > 3)
|
|
5634
|
-
out.push(` ${foldedText(rows.length, "components", c.page)}`);
|
|
5635
5696
|
if (!short || rows.length <= 3)
|
|
5636
5697
|
out.push(...rows.map((r) => ` ${r.ref.kind === "breakdown" ? componentReadName({ key: r.ref.key, name: r.ref.name }) : measureWords(r.ref.measure)}`));
|
|
5637
5698
|
}
|
|
@@ -5706,11 +5767,13 @@ var MAIN_TIER_RESULT_WORDS = {
|
|
|
5706
5767
|
worse: STATE_WORDS.worse
|
|
5707
5768
|
};
|
|
5708
5769
|
var MAIN_TIER_RANK = { worse: 0, slight: 1, cant: 2, better: 3, same: 4 };
|
|
5770
|
+
var earlierWords = (sessions, since, config) => `main has ${sessions}${config.baseline.sessions === undefined ? "" : ` of ${config.baseline.sessions}`} earlier sessions${since ? ` on ${since}` : ""}`;
|
|
5709
5771
|
function mainCantWords(reason, config) {
|
|
5710
5772
|
if (!reason)
|
|
5711
5773
|
return "";
|
|
5712
|
-
return reason.kind === "noisy_main" ? "main’s runs vary too much there" :
|
|
5774
|
+
return reason.kind === "noisy_main" ? "main’s runs vary too much there" : earlierWords(String(reason.earlier_sessions), reason.since_label, config);
|
|
5713
5775
|
}
|
|
5776
|
+
var mainCantRangeWords = (r, config) => earlierWords(`${r.min}–${r.max}`, r.since_label, config);
|
|
5714
5777
|
var waitingSessionsWords = (sessions) => `${sessions} older main ${sessions === 1 ? "session waits" : "sessions wait"} for a reprocess`;
|
|
5715
5778
|
function mainPlaceReasonWords(reason, config) {
|
|
5716
5779
|
switch (reason.kind) {
|
|
@@ -5740,9 +5803,11 @@ var SHORT_ASPECT_WORDS = {
|
|
|
5740
5803
|
driverVersion: "Driver",
|
|
5741
5804
|
journeyVersion: "Journey"
|
|
5742
5805
|
};
|
|
5806
|
+
var cantAt = (places) => `${STATE_WORDS["cant-tell"]} at ${places} place${places === 1 ? "" : "s"}`;
|
|
5743
5807
|
function mainSessionCantWords(places, reason, config) {
|
|
5744
|
-
return `${
|
|
5808
|
+
return `${cantAt(places)}: ${mainCantWords(reason, config)}`;
|
|
5745
5809
|
}
|
|
5810
|
+
var mainSessionCantRangeWords = (places, r, config) => `${cantAt(places)}: ${mainCantRangeWords(r, config)}`;
|
|
5746
5811
|
var KIND_PLURAL = {
|
|
5747
5812
|
screen: "screens",
|
|
5748
5813
|
press: "taps",
|
|
@@ -5768,8 +5833,8 @@ function mainCutWords(v) {
|
|
|
5768
5833
|
}
|
|
5769
5834
|
var BY_RANK = Object.keys(MAIN_TIER_RANK).sort((a, b) => MAIN_TIER_RANK[a] - MAIN_TIER_RANK[b]);
|
|
5770
5835
|
// packages/present/dist/src/verdict-section.js
|
|
5771
|
-
var
|
|
5772
|
-
var of = (n, total) => `${n.toLocaleString("en-US")} of ${
|
|
5836
|
+
var places2 = (n) => `${n.toLocaleString("en-US")} ${n === 1 ? "place" : "places"}`;
|
|
5837
|
+
var of = (n, total) => `${n.toLocaleString("en-US")} of ${places2(total)}`;
|
|
5773
5838
|
var runs = (n) => `${n} valid ${n === 1 ? "run" : "runs"}`;
|
|
5774
5839
|
var VERDICT_SECTION_WORDS = {
|
|
5775
5840
|
slower: (n, total, slight) => `${slight ? "A little slower" : "Slower"} at ${of(n, total)}`,
|
|
@@ -5777,7 +5842,7 @@ var VERDICT_SECTION_WORDS = {
|
|
|
5777
5842
|
faster: (n, total) => `Faster at ${of(n, total)}, slower at none`,
|
|
5778
5843
|
mixed: (slower, faster, total) => `Slower at ${of(slower, total)}, faster at ${faster.toLocaleString("en-US")}`,
|
|
5779
5844
|
unpublishedSdk: "ran an unpublished SDK build",
|
|
5780
|
-
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 (${
|
|
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)})`,
|
|
5781
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`,
|
|
5782
5847
|
tooFewRuns: (valid, needed) => `Not compared: ${runs(valid)} of ${needed} needed`,
|
|
5783
5848
|
tooFewMainRuns: (valid, needed) => `Not compared: main has ${runs(valid)} of ${needed} needed`,
|
|
@@ -5785,11 +5850,29 @@ var VERDICT_SECTION_WORDS = {
|
|
|
5785
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" },
|
|
5786
5851
|
noisy: "Not judged: main’s own runs vary too much at every place here",
|
|
5787
5852
|
versionMismatch: "Not compared: the sides were computed with different analysis versions",
|
|
5788
|
-
unconfirmed: (n) => `Can’t confirm ${
|
|
5853
|
+
unconfirmed: (n) => `Can’t confirm ${places2(n)}: a run has no value there`,
|
|
5789
5854
|
notCompared: (reason) => `Not compared: ${reason}`,
|
|
5790
5855
|
notJudgedYet: (why) => `Not judged yet: ${why} on this phone`,
|
|
5791
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`,
|
|
5792
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." },
|
|
5793
5876
|
sides: {
|
|
5794
5877
|
branch: { left: "Main", right: "This branch", rightShort: "Branch" },
|
|
5795
5878
|
main: { left: "Sessions before", right: "Newest session", rightShort: "Newest" },
|
|
@@ -5808,7 +5891,7 @@ var VERDICT_SECTION_WORDS = {
|
|
|
5808
5891
|
countsLabel: "Places by result",
|
|
5809
5892
|
links: {
|
|
5810
5893
|
openChart: "Open chart",
|
|
5811
|
-
seeAll: (n) => `See all ${
|
|
5894
|
+
seeAll: (n) => `See all ${places2(n)}`,
|
|
5812
5895
|
whichPlaces: "Which places",
|
|
5813
5896
|
whichChanges: "Which changes",
|
|
5814
5897
|
howToFix: "How to fix",
|
|
@@ -5832,9 +5915,19 @@ var SUMMARY_WORDS = {
|
|
|
5832
5915
|
fewRuns: "few runs",
|
|
5833
5916
|
measuredDifferently: "measured differently",
|
|
5834
5917
|
namesChanged: (sdk, mainSessions, needed) => `Main: ${mainSessions} of ${needed} sessions on SDK ${sdkTag(sdk)}`,
|
|
5918
|
+
morePlaces: (n) => `+${n} more ${n === 1 ? "place" : "places"} changed`,
|
|
5919
|
+
compared: {
|
|
5920
|
+
branch: (sessions, days) => `vs main’s last ${sessions}${days}`,
|
|
5921
|
+
main: (sessions, days) => `newest session vs main’s ${sessions} before${days}`,
|
|
5922
|
+
mainSlower: (sessions, days) => `newest session slower than main’s ${sessions} before${days}`
|
|
5923
|
+
},
|
|
5924
|
+
sessions: (n) => `${n} ${n === 1 ? "session" : "sessions"}`,
|
|
5925
|
+
days: (days) => days ? ` (${days})` : "",
|
|
5926
|
+
runs: (runs, each) => each ? `${each} ${each === 1 ? "run" : "runs"} each` : `${runs} ${runs === 1 ? "run" : "runs"}`,
|
|
5927
|
+
unblock: (why, fix) => `${why} → ${fix}`,
|
|
5835
5928
|
link: {
|
|
5836
|
-
|
|
5837
|
-
|
|
5929
|
+
comparison: "See the comparison",
|
|
5930
|
+
session: "See the session",
|
|
5838
5931
|
branch: (branch) => `Open ${branch}`,
|
|
5839
5932
|
runSet: "Open the run set"
|
|
5840
5933
|
}
|
|
@@ -5844,26 +5937,25 @@ var rank = (l) => ORDER.indexOf(l.state);
|
|
|
5844
5937
|
var WORD = { ...STATE_WORDS, "not-computed": SUMMARY_WORDS.notComputed };
|
|
5845
5938
|
var shownLane = (lanes) => [...lanes].sort((a, b) => rank(a) - rank(b) || b.places - a.places)[0];
|
|
5846
5939
|
var clip = (s, n) => s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
5847
|
-
var NAME = { name: 32,
|
|
5848
|
-
var
|
|
5940
|
+
var NAME = { name: 32, button: 30 };
|
|
5941
|
+
var TOP2 = 3;
|
|
5942
|
+
var MAX_LINES = 5;
|
|
5849
5943
|
var upper = (s) => s ? `${s[0].toUpperCase()}${s.slice(1)}` : s;
|
|
5850
5944
|
var fromTo = (a, b) => {
|
|
5851
5945
|
const unit = (v) => / [^\d\s]+$/.exec(v)?.[0];
|
|
5852
5946
|
return unit(a) && unit(a) === unit(b) ? `${a.slice(0, -unit(a).length)} → ${b}` : `${a} → ${b}`;
|
|
5853
5947
|
};
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
}
|
|
5864
|
-
|
|
5865
|
-
const what = `${c.measure} ${c.main !== null && c.now !== null ? fromTo(c.main, c.now) : c.change}`;
|
|
5866
|
-
return c.place ? `${clipPlace(c.place, Math.max(NAME.place, room - what.length - 3))} · ${what}` : upper(what);
|
|
5948
|
+
var days = (labels) => {
|
|
5949
|
+
const a = labels[0], b = labels.at(-1);
|
|
5950
|
+
if (!a || !b || a === b)
|
|
5951
|
+
return a ?? "";
|
|
5952
|
+
const [ma] = a.split(" ");
|
|
5953
|
+
const [mb, db] = b.split(" ");
|
|
5954
|
+
return ma === mb ? `${a}–${db}` : `${a}–${b}`;
|
|
5955
|
+
};
|
|
5956
|
+
var changeLine = (c, tier) => {
|
|
5957
|
+
const values = c.main !== null && c.now !== null ? `${fromTo(c.main, c.now)}${c.change ? ` (${c.change})` : ""}` : c.change;
|
|
5958
|
+
return [c.place ?? upper(c.measure), ...c.place ? [c.measure] : [], values, WORD[c.tier ?? tier]].filter(Boolean).join(" · ");
|
|
5867
5959
|
};
|
|
5868
5960
|
var wordReason = (word) => word?.includes(": ") ? word.slice(word.indexOf(": ") + 2) : null;
|
|
5869
5961
|
var notJudgedWhy = (l) => l.state === "not-computed" && l.word?.startsWith(`${NOT_JUDGED_WORD}: `) ? upper(l.word.slice(NOT_JUDGED_WORD.length + 2)) : null;
|
|
@@ -5880,19 +5972,30 @@ function why(l) {
|
|
|
5880
5972
|
return upper(SUMMARY_WORDS.placesOnPage);
|
|
5881
5973
|
return upper(wordReason(l.word) ?? (l.word === NO_BASELINE_WORD ? l.word : REASON_WORDS["judged-measures-missing"]));
|
|
5882
5974
|
}
|
|
5975
|
+
function unblock(l, pr) {
|
|
5976
|
+
const page = pr ? "branch" : "main";
|
|
5977
|
+
const reason = l.namesChanged ? "not-enough-main" : l.reason;
|
|
5978
|
+
if (!reason)
|
|
5979
|
+
return null;
|
|
5980
|
+
const fix = reason === "not-enough-main" ? VERDICT_SECTION_WORDS.waitForMain[page] : VERDICT_SECTION_WORDS.fixes[page][reason];
|
|
5981
|
+
return fix ? lowerFirst(fix.replace(/\.$/, "")) : null;
|
|
5982
|
+
}
|
|
5883
5983
|
var noneMoved = VERDICT_SECTION_WORDS.noneMoved(1).replace(/ \(.*\)$/, "");
|
|
5884
5984
|
function conditions(l, pr) {
|
|
5885
5985
|
const moved = l.state === "worse" || l.state === "warning";
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5986
|
+
return [
|
|
5987
|
+
pr && l.state === "worse" ? l.blocking ? VERDICT_SECTION_WORDS.blocksMerge : NOT_BLOCKING : "",
|
|
5988
|
+
moved && l.measuredDifferently ? SUMMARY_WORDS.measuredDifferently : "",
|
|
5989
|
+
(moved || l.state === "better") && l.fewRuns ? SUMMARY_WORDS.fewRuns : ""
|
|
5990
|
+
].filter(Boolean);
|
|
5991
|
+
}
|
|
5992
|
+
function compared(l, pr, slower) {
|
|
5993
|
+
const judged = l.state === "worse" || l.state === "warning" || l.state === "better" || l.state === "no-change";
|
|
5994
|
+
if (!judged || !l.main?.sessions)
|
|
5995
|
+
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)}`;
|
|
5896
5999
|
}
|
|
5897
6000
|
function summaryMessage(input) {
|
|
5898
6001
|
const l = shownLane(input.lanes);
|
|
@@ -5901,37 +6004,55 @@ function summaryMessage(input) {
|
|
|
5901
6004
|
state: "not-computed",
|
|
5902
6005
|
newestSlower: false,
|
|
5903
6006
|
lead: `${SUMMARY_WORDS.notComputed} · ${clip(input.branch, NAME.name)}`,
|
|
5904
|
-
|
|
6007
|
+
...withLines([upper(SUMMARY_WORDS.notComputedWhy)]),
|
|
5905
6008
|
link: { label: SUMMARY_WORDS.link.branch(clip(input.branch, NAME.button - 5)), url: input.url }
|
|
5906
6009
|
};
|
|
5907
6010
|
}
|
|
5908
6011
|
const journeys = new Set(input.lanes.map((x) => x.journey ?? "-")).size;
|
|
6012
|
+
const phones = new Set(input.lanes.map((x) => x.phone ?? "-")).size;
|
|
5909
6013
|
const single = input.lanes.length === 1;
|
|
5910
|
-
const phone = l.phone ?? "unknown phone";
|
|
5911
|
-
const several = journeys === 1 ? SUMMARY_WORDS.phones(new Set(input.lanes.map((x) => x.phone ?? "-")).size) : SUMMARY_WORDS.journeys(journeys);
|
|
5912
6014
|
const newestSlower = !input.pr && (l.state === "worse" || l.state === "warning");
|
|
5913
|
-
const
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
const
|
|
5924
|
-
const
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
6015
|
+
const lead = [
|
|
6016
|
+
notJudgedWhy(l) ? NOT_JUDGED_WORD : WORD[l.state],
|
|
6017
|
+
...input.app ? [input.app] : [],
|
|
6018
|
+
journeys === 1 ? l.journey ?? "journey" : SUMMARY_WORDS.journeys(journeys),
|
|
6019
|
+
input.pr && input.prNumber ? `${input.branch} (#${input.prNumber})` : input.branch,
|
|
6020
|
+
phones === 1 ? l.phone ?? "unknown phone" : SUMMARY_WORDS.phones(phones)
|
|
6021
|
+
].join(" · ");
|
|
6022
|
+
const lines = [];
|
|
6023
|
+
const basis = compared(l, input.pr, newestSlower);
|
|
6024
|
+
const extra = conditions(l, input.pr).join(", ");
|
|
6025
|
+
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] : []];
|
|
6027
|
+
if (l.state === "worse" || l.state === "warning" || l.state === "better") {
|
|
6028
|
+
const seen = new Set;
|
|
6029
|
+
const places = l.changes.filter((c) => {
|
|
6030
|
+
const key = c.key ?? c.place ?? c.measure;
|
|
6031
|
+
return seen.has(key) ? false : (seen.add(key), true);
|
|
6032
|
+
});
|
|
6033
|
+
const room = MAX_LINES - tail.length;
|
|
6034
|
+
const top = places.length > room ? places.slice(0, Math.min(TOP2, room - 1)) : places.slice(0, TOP2);
|
|
6035
|
+
lines.push(...top.map((c) => changeLine(c, l.state)));
|
|
6036
|
+
if (!top.length)
|
|
6037
|
+
lines.push(why(l));
|
|
6038
|
+
const more = l.places - top.length;
|
|
6039
|
+
if (top.length && more > 0)
|
|
6040
|
+
lines.push(SUMMARY_WORDS.morePlaces(more));
|
|
6041
|
+
} else {
|
|
6042
|
+
const fix = l.state === "cant-tell" ? unblock(l, input.pr) : null;
|
|
6043
|
+
lines.push(fix ? SUMMARY_WORDS.unblock(why(l), fix) : why(l));
|
|
6044
|
+
}
|
|
6045
|
+
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 } };
|
|
5929
6048
|
}
|
|
6049
|
+
var withLines = (lines) => ({ lines, detail: lines.join(`
|
|
6050
|
+
`) });
|
|
5930
6051
|
function pendingMessage(branch, url) {
|
|
5931
|
-
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.notComputed} · ${clip(branch, NAME.name)}`,
|
|
6052
|
+
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.notComputed} · ${clip(branch, NAME.name)}`, ...withLines([upper(SUMMARY_WORDS.notComputedWhy)]), link: { label: SUMMARY_WORDS.link.runSet, url } };
|
|
5932
6053
|
}
|
|
5933
6054
|
function noResultMessage(branch, reason, url) {
|
|
5934
|
-
return { state: "not-computed", newestSlower: false, lead: `${SUMMARY_WORDS.noResult} · ${clip(branch, NAME.name)}`,
|
|
6055
|
+
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 };
|
|
5935
6056
|
}
|
|
5936
6057
|
// packages/present/dist/src/run-set-message.js
|
|
5937
6058
|
var midLine = (label, metric) => !metric.startsWith("component:") && /^[A-Z][a-z]*(\s|$)/.test(label) ? `${label[0].toLowerCase()}${label.slice(1)}` : label;
|
|
@@ -6006,6 +6127,8 @@ function laneOf(v, sc, set) {
|
|
|
6006
6127
|
changes: shown.map((i) => ({
|
|
6007
6128
|
place: placeOf(i.step),
|
|
6008
6129
|
measure: measureWord(i),
|
|
6130
|
+
key: i.finding ?? placeRef(i.step ?? "*").stem,
|
|
6131
|
+
tier: i.state === "better" ? "better" : i.grade === "warn" ? "warning" : "worse",
|
|
6009
6132
|
main: i.baseline === null ? null : atMost(fieldValueText(i.metric, i.baseline), i.baseline_is_bound),
|
|
6010
6133
|
now: i.candidate === null ? null : fieldValueText(i.metric, i.candidate),
|
|
6011
6134
|
change: changeWords(i)
|
|
@@ -6017,17 +6140,20 @@ function laneOf(v, sc, set) {
|
|
|
6017
6140
|
heatedRuns: sc.heated_runs ?? 0,
|
|
6018
6141
|
runs: sc.runs.valid + sc.runs.invalid,
|
|
6019
6142
|
namesChanged: sc.context?.namesChanged ?? null,
|
|
6020
|
-
url: linkOf(sc, set)
|
|
6143
|
+
url: linkOf(sc, set),
|
|
6144
|
+
main: sc.main ?? null,
|
|
6145
|
+
reason: sc.headline?.reason ?? sc.headline?.reasons?.[0] ?? null
|
|
6021
6146
|
};
|
|
6022
6147
|
}
|
|
6023
6148
|
var isPending = (s) => lanesOf(s).every((l) => l.state === "not-computed" && !l.word?.startsWith(`${NOT_JUDGED_WORD}: `));
|
|
6024
6149
|
var lanesOf = (s) => s.verdicts ? s.verdicts.scenarios.map((sc) => laneOf(s.verdicts, sc, s)) : [];
|
|
6025
6150
|
var isPr = (s) => !!s.verdicts?.scenarios.some((sc) => sc.role === "comparison");
|
|
6026
|
-
var runSetMessage = (s) => summaryMessage({ branch: s.branch, pr: isPr(s), lanes: lanesOf(s), url: linkOf(s) ?? s.dashboard_url });
|
|
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 });
|
|
6027
6152
|
var MESSAGE_COLORS = { error: "#D6334A", warn: "#C47A00", better: "#238359", none: "#7C8492" };
|
|
6028
6153
|
function messageTone(s) {
|
|
6029
6154
|
const lanes = s ? lanesOf(s) : [];
|
|
6030
|
-
|
|
6155
|
+
const pr = !!s && isPr(s);
|
|
6156
|
+
if (lanes.some((l) => l.state === "worse" && (l.blocking || !pr)))
|
|
6031
6157
|
return { mark: "\uD83D\uDD34", color: MESSAGE_COLORS.error };
|
|
6032
6158
|
if (lanes.some((l) => l.state === "warning"))
|
|
6033
6159
|
return { mark: "\uD83D\uDFE1", color: MESSAGE_COLORS.warn };
|
|
@@ -6069,18 +6195,43 @@ function resultSlackPayload(s, o = {}) {
|
|
|
6069
6195
|
var noResultSlackPayload = (branch, reason, url) => slackPayload(noResultMessage(branch, reason, url), { mark: "⚪", color: MESSAGE_COLORS.none });
|
|
6070
6196
|
var markdown = (m, mark) => `### ${mark} ${m.lead}
|
|
6071
6197
|
|
|
6072
|
-
${m.
|
|
6198
|
+
${m.lines.join(`
|
|
6199
|
+
`)}
|
|
6073
6200
|
${m.link ? `
|
|
6074
6201
|
[${m.link.label} ›](${m.link.url})
|
|
6075
6202
|
` : ""}`;
|
|
6076
6203
|
var resultMarkdown = (s) => markdown(runSetMessage(s), messageTone(s).mark);
|
|
6077
6204
|
var noResultMarkdown = (branch, reason, url) => markdown(noResultMessage(branch, reason, url), "⚪");
|
|
6078
6205
|
var pendingMarkdown = (branch, url) => markdown(pendingMessage(branch, url), "⚪");
|
|
6206
|
+
// packages/present/dist/src/stale.js
|
|
6207
|
+
function staleNoteText(why) {
|
|
6208
|
+
const newer = why.includes("newer-run-set");
|
|
6209
|
+
const rules = why.some((w) => w === "core-version" || w === "config");
|
|
6210
|
+
if (newer && rules)
|
|
6211
|
+
return "Result from an older run set and older rules.";
|
|
6212
|
+
if (newer)
|
|
6213
|
+
return "Result from an older run set; a newer one has arrived.";
|
|
6214
|
+
if (rules)
|
|
6215
|
+
return "Result from older rules; it is not recomputed yet.";
|
|
6216
|
+
return "Result from older inputs.";
|
|
6217
|
+
}
|
|
6218
|
+
var staleFlagText = (why) => {
|
|
6219
|
+
const note = staleNoteText(why).replace(/\.$/, "");
|
|
6220
|
+
return `${note.charAt(0).toLowerCase()}${note.slice(1)}`;
|
|
6221
|
+
};
|
|
6222
|
+
var STALE_MARK = "stale result";
|
|
6079
6223
|
// apps/agent/src/upload.ts
|
|
6080
6224
|
import { join as join4 } from "node:path";
|
|
6081
6225
|
var MAX_RATE_RETRIES = 5;
|
|
6082
6226
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6227
|
+
var pause = (ms) => sleep(ms);
|
|
6228
|
+
|
|
6083
6229
|
class UploadError extends Error {
|
|
6230
|
+
status;
|
|
6231
|
+
constructor(message, status) {
|
|
6232
|
+
super(message);
|
|
6233
|
+
this.status = status;
|
|
6234
|
+
}
|
|
6084
6235
|
}
|
|
6085
6236
|
async function call(f, base, path, headers, body, method = "POST") {
|
|
6086
6237
|
let res;
|
|
@@ -6092,7 +6243,7 @@ async function call(f, base, path, headers, body, method = "POST") {
|
|
|
6092
6243
|
...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}
|
|
6093
6244
|
});
|
|
6094
6245
|
} catch (e) {
|
|
6095
|
-
throw new UploadError(`${method} ${path}: ${e.message}
|
|
6246
|
+
throw new UploadError(`${method} ${path}: ${e.message}`, 0);
|
|
6096
6247
|
}
|
|
6097
6248
|
if (res.status !== 429 || attempt >= MAX_RATE_RETRIES)
|
|
6098
6249
|
break;
|
|
@@ -6102,7 +6253,7 @@ async function call(f, base, path, headers, body, method = "POST") {
|
|
|
6102
6253
|
}
|
|
6103
6254
|
const text = await res.text();
|
|
6104
6255
|
if (res.status < 200 || res.status >= 300)
|
|
6105
|
-
throw new UploadError(`${method} ${path} -> ${res.status} ${text.slice(0, 200)}
|
|
6256
|
+
throw new UploadError(`${method} ${path} -> ${res.status} ${text.slice(0, 200)}`, res.status);
|
|
6106
6257
|
try {
|
|
6107
6258
|
return text ? JSON.parse(text) : {};
|
|
6108
6259
|
} catch {
|
|
@@ -6239,13 +6390,35 @@ async function createRunSet(t, body) {
|
|
|
6239
6390
|
throw new UploadError("POST /v1/run-sets: response has no run_set_id (is the URL the KeepSnappy API?)");
|
|
6240
6391
|
return { run_set_id: r.run_set_id, dashboard_url: String(r.dashboard_url ?? "") };
|
|
6241
6392
|
}
|
|
6393
|
+
var UNKNOWN_OUTCOME = new Set([0, 502, 503, 504]);
|
|
6394
|
+
var CONFIRM_READS = 6;
|
|
6395
|
+
var CONFIRM_WAIT_MS = 1e4;
|
|
6242
6396
|
async function completeRunSet(t, id, verdicts) {
|
|
6243
6397
|
const path = `/v1/run-sets/${encodeURIComponent(id)}/complete`;
|
|
6244
|
-
|
|
6398
|
+
let r;
|
|
6399
|
+
try {
|
|
6400
|
+
r = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), { verdicts });
|
|
6401
|
+
} catch (e) {
|
|
6402
|
+
const closed = e instanceof UploadError && UNKNOWN_OUTCOME.has(e.status ?? -1) ? await confirmedClose(t, id) : null;
|
|
6403
|
+
if (closed)
|
|
6404
|
+
return closed;
|
|
6405
|
+
throw e;
|
|
6406
|
+
}
|
|
6245
6407
|
if (!r.run_set_id)
|
|
6246
6408
|
throw new UploadError(`POST ${path}: response has no run_set_id`);
|
|
6247
6409
|
return r;
|
|
6248
6410
|
}
|
|
6411
|
+
async function confirmedClose(t, id) {
|
|
6412
|
+
const path = `/v1/run-sets/${encodeURIComponent(id)}`;
|
|
6413
|
+
for (let i = 0;i < CONFIRM_READS; i++) {
|
|
6414
|
+
if (i)
|
|
6415
|
+
await pause(CONFIRM_WAIT_MS);
|
|
6416
|
+
const s = await call(t.fetch ?? fetch, t.url, path, keyHeader(t), undefined, "GET").catch(() => null);
|
|
6417
|
+
if (s?.run_set_id && (s.status === "processed" || s.status === "failed"))
|
|
6418
|
+
return s;
|
|
6419
|
+
}
|
|
6420
|
+
return null;
|
|
6421
|
+
}
|
|
6249
6422
|
async function createIngestRun(t, body) {
|
|
6250
6423
|
const r = await call(t.fetch ?? fetch, t.url, "/v1/runs", keyHeader(t), body);
|
|
6251
6424
|
if (!r.run_id || !r.run_token)
|
|
@@ -10188,8 +10361,14 @@ ${outcome.summary}`);
|
|
|
10188
10361
|
const info = { device, ...git.branch ? { branch: git.branch } : {}, ...git.commit ? { commit: git.commit } : {} };
|
|
10189
10362
|
await deps.fs.writeFile(join9(out, RUN_INFO), `${JSON.stringify({ ...info, ...runSet ? { run_set_id: runSet.run_set_id } : {} }, null, 2)}
|
|
10190
10363
|
`);
|
|
10191
|
-
if (!api)
|
|
10364
|
+
if (!api) {
|
|
10365
|
+
if (!runs.some((run) => run.valid)) {
|
|
10366
|
+
const reasons = [...new Set(runs.map((run) => run.invalid_reason).filter(Boolean))];
|
|
10367
|
+
rt.print(`No valid run was recorded: ${runs.length === 0 ? "no runs were attempted" : `${runs.length} ${runs.length === 1 ? "run" : "runs"} invalid${reasons.length ? ` (${reasons.join(", ")})` : ""}`}.`);
|
|
10368
|
+
return EXIT.infra;
|
|
10369
|
+
}
|
|
10192
10370
|
return EXIT.ok;
|
|
10371
|
+
}
|
|
10193
10372
|
const up = await uploadRuns(deps, out, runs, {
|
|
10194
10373
|
ingestUrl: api.url,
|
|
10195
10374
|
key: api.key,
|
|
@@ -10714,7 +10893,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
10714
10893
|
import { dirname as dirname2, join as join11, relative as relative2, resolve as resolve4 } from "node:path";
|
|
10715
10894
|
import { fileURLToPath } from "node:url";
|
|
10716
10895
|
function driverVersion() {
|
|
10717
|
-
const built = "0.1.0-alpha.
|
|
10896
|
+
const built = "0.1.0-alpha.19";
|
|
10718
10897
|
if (built)
|
|
10719
10898
|
return built;
|
|
10720
10899
|
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
@@ -10795,6 +10974,9 @@ var CLAUSES = {
|
|
|
10795
10974
|
commit: { c: 0.8326, k95: 1.7675, k99: 2.8108 },
|
|
10796
10975
|
component: { c: 0.6481, k95: 1.8075, k99: 2.6648 }
|
|
10797
10976
|
}),
|
|
10977
|
+
"T-NOISE-JANK": row("T-NOISE-JANK", "The judged `jank` rule: the no-change noise curve of jank time, as T-WARN-NOISE; at a main median of 0, Slightly worse past zeroWarn and Worse past zeroError.", "ms", ">", "W8 option A, Arek 2026-10-26 [ctl-arek-jank-time]; fitted by the 2026-10-16 method (dedup-nomi, PR 3x3 vs main 3x3, every split) on pack v3 aa-ni (S23 TapIn, 10 sessions, 4200 splits, 19 places with a median above 0, R² -0.27; the 10-16 S23 and iPhone sessions are no longer stored); zeroWarn = the largest d99 at a median of 0 over the A/A pools with an export (farm16 Pixel 7), zeroError = zeroWarn x k99 / k95", true, {
|
|
10978
|
+
jank: { c: 0.9159, k95: 2.5757, k99: 3.2072, zeroWarn: 10.38, zeroError: 12.92 }
|
|
10979
|
+
}),
|
|
10798
10980
|
"T-REL": row("T-REL", "The relative part of both tiers when a rule has no noise curve; the deciding config's tiers cap it at the error ms, so no input reaches it.", "share", ">", "REQ-MET-26, controller 2026-10-16", false, { rel: 0.3 }),
|
|
10799
10981
|
"J-MEMORY-RETAINED": row("J-MEMORY-RETAINED", "Memory the journey kept after the end GC: Slightly worse past max(warn MB, warn % of main), Worse past max(error MB, error % of main), on every device.", "MB", ">", "REQ-MET-4, Arek 2026-10-20 ~08:00, pair B (standing-rulings.md); provisional", true, { warn: { abs: 20, rel: 0.1 }, error: { abs: 150, rel: 0.3 } }),
|
|
10800
10982
|
"E-REACT-COUNTS": row("E-REACT-COUNTS", "Commit and render counts are evidence past max(floor, +50 % of main).", "count", ">", "REQ-MET-13, Arek 2026-10-20 (morning rulings, Layer 2); provisional", true, { commits: { rel: 0.5, abs: 2 }, renders: { rel: 0.5, abs: 10 } }),
|
|
@@ -10802,6 +10984,7 @@ var CLAUSES = {
|
|
|
10802
10984
|
"E-BACKGROUND": row("E-BACKGROUND", "Background places are evidence past max(floor, % of main).", "ms", ">", "REQ-MEAS-32; the number has no ruling", true, { background_commit: FLOOR, background_render: FLOOR, background_component: FLOOR }),
|
|
10803
10985
|
"E-LAUNCH": row("E-LAUNCH", "The launch screen is evidence past max(5 ms, +30 % of main).", "ms", ">", "REQ-VER-15; floor adopted 2026-10-21 from config.ts F-13, [ctl-stab-noold-1625]", true, { launch_commit: FLOOR, launch_render: FLOOR, launch_component: FLOOR }),
|
|
10804
10986
|
"E-STEP": row("E-STEP", "A declared step is evidence past max(5 ms, +30 % of main).", "ms", ">", "REQ-MET-14; floor adopted 2026-10-21 from config.ts F-13, [ctl-stab-noold-1625]", true, { step_commit: FLOOR, step_render: FLOOR, step_component: FLOOR }),
|
|
10987
|
+
"E-COMMIT-MAX": row("E-COMMIT-MAX", "The slowest commit at a judged window is evidence past max(5 ms, +30 % of main) while jank time is judged.", "ms", ">", "W8 option A, Arek 2026-10-26 [ctl-arek-jank-time]: commit_max_ms stays shown as evidence", true, { commit_max: FLOOR }),
|
|
10805
10988
|
"E-TOUCH": row("E-TOUCH", "Touch response is evidence past max(10 ms, +20 % of main).", "ms", ">", 'ruling "touch response is not judged" (REQ-MET-24)', true, { touch: { rel: 0.2, abs: 10 } }),
|
|
10806
10989
|
"E-FREEZE": row("E-FREEZE", "The longest freeze (window, journey) is evidence past max(17 ms, +50 % of main).", "ms", ">", NO_RULING, true, { freeze: { rel: 0.5, abs: 17 }, freeze_journey: { rel: 0.5, abs: 17 } }),
|
|
10807
10990
|
"E-MEMORY-PLACE": row("E-MEMORY-PLACE", "Memory added at a place is evidence past this many MB (screens have their own), on every device.", "MB", ">", 'REQ-MET-4 amendment 2026-10-18 ~21:10 and ~22:20 ("abs 2 MB, screens 10 MB")', true, { memory_added: { abs: 2 }, memory_added_screen: { abs: 10 } }),
|
|
@@ -10857,7 +11040,8 @@ var REACT_MEASURES = [
|
|
|
10857
11040
|
"self_ms_top",
|
|
10858
11041
|
"component_ms",
|
|
10859
11042
|
"dimension:component",
|
|
10860
|
-
"dimension:updater"
|
|
11043
|
+
"dimension:updater",
|
|
11044
|
+
"jank_ms"
|
|
10861
11045
|
];
|
|
10862
11046
|
var WINDOW_MODEL_AFFECTS = [
|
|
10863
11047
|
"commits",
|
|
@@ -10882,7 +11066,8 @@ var WINDOW_MODEL_AFFECTS = [
|
|
|
10882
11066
|
"memory_left_mb",
|
|
10883
11067
|
"retained_mb_per_cycle",
|
|
10884
11068
|
"dimension:component",
|
|
10885
|
-
"dimension:updater"
|
|
11069
|
+
"dimension:updater",
|
|
11070
|
+
"jank_ms"
|
|
10886
11071
|
];
|
|
10887
11072
|
var MEASURES_AT = (kind) => [...Object.keys(MEASURES).filter((id) => MEASURES[id].at.includes(kind)), "dimension:component", "dimension:updater"];
|
|
10888
11073
|
var SPLIT_AFFECTS = [
|
|
@@ -11048,6 +11233,16 @@ var DRIVER_RELEASES = [
|
|
|
11048
11233
|
declares: [{ affects: DRAG_TIME_AND_CPU, places: ["drag"], platforms: ["ios"] }],
|
|
11049
11234
|
uncalibrated: false,
|
|
11050
11235
|
source: "R-SCROLL-BUILD (#355 one scroll primitive and the counted gesture retry, #356 the scroll fault matrix). Android declares nothing: on a phone that takes every gesture the driver sends the same requests, bodies and waits as alpha.17 (the whole command stream is pinned). iOS: each checked drag (every to-top drag of flingUntilShown/passChecked and the first drag of each scrollToEnd pass) gains two frame reads of the held row, one right before and one right after it, so a drag window's time and CPU can change; its React render, commits and memory stay compared. A phone that drops a gesture (BrowserStack Galaxy S23, S25 Ultra) gets that gesture once more, as a counted harness defect (REQ-FARM-35)."
|
|
11236
|
+
},
|
|
11237
|
+
{
|
|
11238
|
+
version: "0.1.0-alpha.19",
|
|
11239
|
+
previous: "0.1.0-alpha.18",
|
|
11240
|
+
declares: [
|
|
11241
|
+
{ affects: MEASURES_AT("drag"), places: ["drag"], platforms: ["ios"] },
|
|
11242
|
+
{ affects: MEASURES_AT("step"), places: ["step"], platforms: ["ios"] }
|
|
11243
|
+
],
|
|
11244
|
+
uncalibrated: false,
|
|
11245
|
+
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)."
|
|
11051
11246
|
}
|
|
11052
11247
|
];
|
|
11053
11248
|
var UNSTABLE_NOISY = {
|
|
@@ -11075,7 +11270,7 @@ class ConfigError2 extends Error {
|
|
|
11075
11270
|
var noRow = (id) => {
|
|
11076
11271
|
throw new ConfigError2([`${id}: no row in the clause table`]);
|
|
11077
11272
|
};
|
|
11078
|
-
var NOISE_FITS = CLAUSES["T-WARN-NOISE"].value;
|
|
11273
|
+
var NOISE_FITS = { ...CLAUSES["T-WARN-NOISE"].value, ...CLAUSES["T-NOISE-JANK"].value };
|
|
11079
11274
|
var TIER_REL = 0.3;
|
|
11080
11275
|
var TIER_UPDATE_MS = { warnMs: 5, errorMs: 15 };
|
|
11081
11276
|
var TIER_WORK_MS = { warnMs: 10, errorMs: 30 };
|
|
@@ -11107,17 +11302,16 @@ function withClauseValues(base) {
|
|
|
11107
11302
|
throw new ConfigError2(["better: missing from base config"]);
|
|
11108
11303
|
const warnMs = CLAUSES["T-WARN-MS"].value;
|
|
11109
11304
|
const errorMs = CLAUSES["TIER-ERROR"].value;
|
|
11110
|
-
const noise = CLAUSES["T-WARN-NOISE"];
|
|
11111
11305
|
const memory = CLAUSES["J-MEMORY-RETAINED"];
|
|
11112
11306
|
const judgedByNoise = (r) => {
|
|
11113
11307
|
const fit = NOISE_FITS[r.id] ?? noRow(r.id);
|
|
11114
|
-
const n = { a: 0, b: fit.c, power: 0.5, lift: fit.k95 };
|
|
11308
|
+
const n = { a: 0, b: fit.c, power: 0.5, lift: fit.k95, ...fit.zeroWarn !== undefined ? { atZero: fit.zeroWarn } : {} };
|
|
11115
11309
|
return {
|
|
11116
11310
|
...r,
|
|
11117
11311
|
threshold: [{ abs: warnMs.update, noise: n }],
|
|
11118
11312
|
thresholdByWork: { entry: [{ abs: warnMs.work, noise: n }], mount: [{ abs: warnMs.work, noise: n }] },
|
|
11119
|
-
errorNoise: {
|
|
11120
|
-
uncalibrated:
|
|
11313
|
+
errorNoise: { a: 0, b: fit.c, power: 0.5, lift: fit.k99, ...fit.zeroError !== undefined ? { atZero: fit.zeroError } : {} },
|
|
11314
|
+
uncalibrated: (r.id in CLAUSES["T-NOISE-JANK"].value ? CLAUSES["T-NOISE-JANK"] : CLAUSES["T-WARN-NOISE"]).uncalibrated
|
|
11121
11315
|
};
|
|
11122
11316
|
};
|
|
11123
11317
|
const evidence = (r) => {
|
|
@@ -11125,11 +11319,12 @@ function withClauseValues(base) {
|
|
|
11125
11319
|
const at = r.thresholdAt && Object.fromEntries(Object.keys(r.thresholdAt).map((kind) => [kind, [part((EVIDENCE_BARS.get(`${r.id}_${kind}`) ?? noRow(`${r.id}_${kind}`)).bar)]]));
|
|
11126
11320
|
return { ...r, threshold: [part(e.bar)], ...at ? { thresholdAt: at } : {}, uncalibrated: e.uncalibrated };
|
|
11127
11321
|
};
|
|
11128
|
-
const
|
|
11322
|
+
const valued = (r) => {
|
|
11129
11323
|
if (r.id === "memory_retained")
|
|
11130
11324
|
return { ...r, threshold: [part(memory.value.warn)], uncalibrated: memory.uncalibrated };
|
|
11131
11325
|
return r.role === "judged" ? judgedByNoise(r) : evidence(r);
|
|
11132
|
-
}
|
|
11326
|
+
};
|
|
11327
|
+
const rules = base.rules.map(valued);
|
|
11133
11328
|
const frames = (id) => ({ above: { frames: CLAUSES[id].value.frames }, uncalibrated: CLAUSES[id].uncalibrated });
|
|
11134
11329
|
const entry = CLAUSES["P-HEAVY-ENTRY"];
|
|
11135
11330
|
const unstable = CLAUSES["U-NOISY"];
|
|
@@ -11171,12 +11366,27 @@ function withClauseValues(base) {
|
|
|
11171
11366
|
minMainRuns: unstable.value.minMainRuns,
|
|
11172
11367
|
minSessions: unstable.value.minSessions,
|
|
11173
11368
|
uncalibrated: unstable.uncalibrated,
|
|
11174
|
-
curve: Object.fromEntries(Object.keys(u.curve).map((id) => [id, (NOISE_FITS[id] ?? noRow(`unstable curve ${id}`)).c]))
|
|
11369
|
+
curve: Object.fromEntries(Object.keys(u.curve).map((id) => [id, (NOISE_FITS[id] ?? noRow(`unstable curve ${id}`)).c])),
|
|
11370
|
+
...u.reads ? { reads: u.reads.map(valued) } : {}
|
|
11175
11371
|
}))
|
|
11176
11372
|
};
|
|
11177
11373
|
}
|
|
11374
|
+
var COMMIT_MAX_JUDGED = noiseJudged("commit", { measure: "commit_max_ms" });
|
|
11375
|
+
var RENDER_JUDGED = noiseJudged("render", { measure: "render_ms" });
|
|
11376
|
+
var COMPONENT_JUDGED = noiseJudged("component", { dimension: "component", measure: "self_ms" });
|
|
11377
|
+
var MEMORY_RETAINED = {
|
|
11378
|
+
id: "memory_retained",
|
|
11379
|
+
role: "judged",
|
|
11380
|
+
on: { measure: "mem.retained_growth_mb" },
|
|
11381
|
+
places: JOURNEY,
|
|
11382
|
+
devices: ANY_DEVICE,
|
|
11383
|
+
threshold: [{ rel: 0.1, abs: 20 }],
|
|
11384
|
+
uncalibrated: true,
|
|
11385
|
+
source: MEMORY_PAIR_SOURCE
|
|
11386
|
+
};
|
|
11387
|
+
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)`;
|
|
11178
11388
|
var SHAPE = {
|
|
11179
|
-
id: "simple-4.
|
|
11389
|
+
id: "simple-4.26+noise95-99",
|
|
11180
11390
|
minRuns: 2,
|
|
11181
11391
|
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)") },
|
|
11182
11392
|
repeats: { kind: "every-pr-beyond-every-main" },
|
|
@@ -11202,9 +11412,10 @@ var SHAPE = {
|
|
|
11202
11412
|
},
|
|
11203
11413
|
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)' },
|
|
11204
11414
|
rules: [
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11415
|
+
{ ...COMMIT_MAX_JUDGED, id: "jank", on: { measure: "jank_ms" }, source: JANK_SOURCE },
|
|
11416
|
+
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
|
+
RENDER_JUDGED,
|
|
11418
|
+
COMPONENT_JUDGED,
|
|
11208
11419
|
{ ...evidenceRule("commits", { measure: "commits" }, REACT_PLACES, [{ rel: 0.5, abs: 2 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11209
11420
|
{ ...evidenceRule("renders", { measure: "renders" }, REACT_PLACES, [{ rel: 0.5, abs: 10 }], "render and commit counts are evidence only (ruling a)"), source: COUNT_SOURCE },
|
|
11210
11421
|
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"),
|
|
@@ -11230,16 +11441,7 @@ var SHAPE = {
|
|
|
11230
11441
|
evidenceRule("gc", { measure: "gc_ms" }, SHOWN2, [{ rel: 0.5, abs: 2 }], "v5.3 warn level"),
|
|
11231
11442
|
evidenceRule("requests", { measure: "net_requests" }, SHOWN2, [{ rel: 0.5, abs: 1 }], "v5.3 warn level"),
|
|
11232
11443
|
evidenceRule("js_cpu", { measure: "js_cpu_ms" }, SHOWN2, [{ rel: 0.5, abs: 5 }], "v5.3 warn level"),
|
|
11233
|
-
|
|
11234
|
-
id: "memory_retained",
|
|
11235
|
-
role: "judged",
|
|
11236
|
-
on: { measure: "mem.retained_growth_mb" },
|
|
11237
|
-
places: JOURNEY,
|
|
11238
|
-
devices: ANY_DEVICE,
|
|
11239
|
-
threshold: [{ rel: 0.1, abs: 20 }],
|
|
11240
|
-
uncalibrated: true,
|
|
11241
|
-
source: MEMORY_PAIR_SOURCE
|
|
11242
|
-
}
|
|
11444
|
+
MEMORY_RETAINED
|
|
11243
11445
|
],
|
|
11244
11446
|
problems: [
|
|
11245
11447
|
{
|
|
@@ -11286,7 +11488,8 @@ var SHAPE = {
|
|
|
11286
11488
|
unstable: [{
|
|
11287
11489
|
...UNSTABLE_NOISY,
|
|
11288
11490
|
step: "own",
|
|
11289
|
-
|
|
11491
|
+
reads: [COMMIT_MAX_JUDGED, RENDER_JUDGED, COMPONENT_JUDGED, MEMORY_RETAINED],
|
|
11492
|
+
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`
|
|
11290
11493
|
}],
|
|
11291
11494
|
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' },
|
|
11292
11495
|
measuredWith: [
|
|
@@ -12743,6 +12946,7 @@ import { parseArgs as parseArgs3 } from "node:util";
|
|
|
12743
12946
|
var SHORT_SESSIONS = 3;
|
|
12744
12947
|
var SHORT_POINTS = 5;
|
|
12745
12948
|
var SHORT_PLACES = 5;
|
|
12949
|
+
var SHORT_COMPARE_MEASURES = 3;
|
|
12746
12950
|
var SHORT_FORM_NOTES = {
|
|
12747
12951
|
list_findings: "the how-sure lines, the place lines of findings that are not Worse and the lanes that were judged",
|
|
12748
12952
|
get_finding: "the why, seen and layer lines, the component lines, the memory evidence lines other than the judged measure and the prompt",
|
|
@@ -12752,7 +12956,8 @@ var SHORT_FORM_NOTES = {
|
|
|
12752
12956
|
place_trend: `the sessions older than the newest ${SHORT_SESSIONS} and the history restarts among them (unknown restarts always stay)`,
|
|
12753
12957
|
window_history: `the runs older than the newest ${SHORT_POINTS}`,
|
|
12754
12958
|
pr_status: "the per-journey dashboard links and the heat note",
|
|
12755
|
-
list_problems: `the work and component lines, the dashboard links and the problems after the first ${SHORT_PLACES}
|
|
12959
|
+
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`
|
|
12756
12961
|
};
|
|
12757
12962
|
var andList = (xs) => xs.length < 2 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} and ${xs[xs.length - 1]}`;
|
|
12758
12963
|
|
|
@@ -12859,6 +13064,7 @@ function measuredLines(l, r, opts) {
|
|
|
12859
13064
|
};
|
|
12860
13065
|
if (m.lanes.length === 1) {
|
|
12861
13066
|
r.both(`${scope}: newest measured ${f.branch ? "" : `on ${n.branch}, `}${lane(n)}; ${plural2(m.run_sets, "run set")} measured.`);
|
|
13067
|
+
r.both(...(n.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
12862
13068
|
r.full(...(n.session_speed ?? []).map((line) => ` ${line}`));
|
|
12863
13069
|
r.both(...laneContextLines(n.context, n.journey, n.phone_name, opts.verbose, opts.form === "short"));
|
|
12864
13070
|
return [];
|
|
@@ -12866,9 +13072,10 @@ function measuredLines(l, r, opts) {
|
|
|
12866
13072
|
const shown = m.lanes.slice(0, LANES_SHOWN);
|
|
12867
13073
|
r.add(shown.some((x) => !x.judged || !!x.context), `${scope}: ${plural2(m.run_sets, "run set")} measured; the newest of each journey × phone:`);
|
|
12868
13074
|
for (const x of shown) {
|
|
12869
|
-
const keep = !x.judged || !!x.context;
|
|
13075
|
+
const keep = !x.judged || !!x.context || !!x.comparison_notes;
|
|
12870
13076
|
r.add(keep, ` ${x.journey} on ${x.phone_name}${f.branch ? "" : ` (${x.branch})`}: ${lane(x)}`);
|
|
12871
13077
|
r.add(keep, ...resultContextLines(x.context, opts.verbose, opts.form === "short").map((line) => ` ${line}`));
|
|
13078
|
+
r.add(keep, ...(x.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
12872
13079
|
r.full(...(x.session_speed ?? []).map((line) => ` ${line}`));
|
|
12873
13080
|
}
|
|
12874
13081
|
if (m.lanes.length > shown.length)
|
|
@@ -12932,10 +13139,11 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12932
13139
|
if (s.lanes.length)
|
|
12933
13140
|
r.both("");
|
|
12934
13141
|
for (const l of s.lanes) {
|
|
12935
|
-
const shown = l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)
|
|
13142
|
+
const shown = `${l.result === "blocks" ? `${l.word} (blocks)` : badgedWord(l.word, l.blocking)}${l.stale ? ` (${STALE_FLAG})` : ""}`;
|
|
12936
13143
|
const sha = l.commit?.sha?.slice(0, 7);
|
|
12937
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)}` : ""}`);
|
|
12938
13145
|
r.both(...resultContextLines(l.context, opts.verbose, opts.form === "short"));
|
|
13146
|
+
r.both(...(l.comparison_notes ?? []).map((line) => ` ${line}`));
|
|
12939
13147
|
r.full(...(l.session_speed ?? []).map((line) => ` ${line}`));
|
|
12940
13148
|
for (const f of l.worst)
|
|
12941
13149
|
r.both(` ${f.id} ${f.headline}`);
|
|
@@ -12945,6 +13153,7 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12945
13153
|
r.full(` ${l.link}`);
|
|
12946
13154
|
}
|
|
12947
13155
|
const links = s.lanes.filter((l) => l.link).length;
|
|
13156
|
+
const staleLanes = s.problems?.stale_lanes ?? 0;
|
|
12948
13157
|
const heated = s.counts.heat_runs ?? 0;
|
|
12949
13158
|
const more = [
|
|
12950
13159
|
[false, heated ? `${INFO_WORDS.heat.toLowerCase()} in ${plural2(heated, "run")} (information, never blocks)` : ""],
|
|
@@ -12953,6 +13162,7 @@ function formatBranchStatus(s, opts = {}) {
|
|
|
12953
13162
|
s.not_judged_findings ? `${plural2(s.not_judged_findings, "finding")} not judged in the newest run set; they may still be there: keepsnappy findings --branch ${s.branch} --state not-judged` : ""
|
|
12954
13163
|
],
|
|
12955
13164
|
[true, statusProblemsLine(s)],
|
|
13165
|
+
[true, staleLanes ? `${staleWords(staleLanes)}: ${staleLanes === 1 ? "its word and problems are" : "their words and problems are"} of older rules` : ""],
|
|
12956
13166
|
[true, s.problems?.unread_lanes ? `${unreadWords(s.problems.unread_lanes)} for problems: ${staleResult(s.problems.unread_lanes)}` : ""],
|
|
12957
13167
|
[true, s.links.overview ? `Dashboard: ${s.links.overview}` : ""],
|
|
12958
13168
|
[true, s.worst.length ? "One finding with its evidence and a prompt: keepsnappy finding <id> --prompt" : ""]
|
|
@@ -12968,9 +13178,11 @@ function statusProblemsLine(s) {
|
|
|
12968
13178
|
const p = s.problems;
|
|
12969
13179
|
if (!p?.count)
|
|
12970
13180
|
return "";
|
|
12971
|
-
return `${
|
|
13181
|
+
return `${problemCountsText(p, "this branch’s")}. Not a regression, never blocks: list_problems, or keepsnappy problems --branch ${s.branch}`;
|
|
12972
13182
|
}
|
|
12973
13183
|
var unreadWords = (n) => `${n} journey × phone not read`;
|
|
13184
|
+
var STALE_FLAG = staleFlagText(["config"]);
|
|
13185
|
+
var staleWords = (n) => `${n} journey × phone from a stale result (${STALE_FLAG})`;
|
|
12974
13186
|
var staleResult = (n) => `${n === 1 ? "its stored result is" : "their stored results are"} not current`;
|
|
12975
13187
|
function formatProblemList(l, opts = {}) {
|
|
12976
13188
|
const r = new Rows;
|
|
@@ -12987,7 +13199,7 @@ function formatProblemList(l, opts = {}) {
|
|
|
12987
13199
|
return r.render(opts.form);
|
|
12988
13200
|
}
|
|
12989
13201
|
if (c.count)
|
|
12990
|
-
r.both(`${b}: ${
|
|
13202
|
+
r.both(`${b}: ${problemCountsText(c, b === l.baseline_branch ? "main’s" : "this branch’s")}`);
|
|
12991
13203
|
else if (read)
|
|
12992
13204
|
r.both(`${b}: no problem over budget in the newest run set (${read} journey × phone read)`);
|
|
12993
13205
|
else
|
|
@@ -12995,10 +13207,12 @@ function formatProblemList(l, opts = {}) {
|
|
|
12995
13207
|
r.both(l.note);
|
|
12996
13208
|
if (read && unread)
|
|
12997
13209
|
r.both(`${unreadWords(unread)}: ${staleResult(unread)}, so nothing is known about ${unread === 1 ? "its" : "their"} problems.`);
|
|
13210
|
+
if (c.stale_lanes)
|
|
13211
|
+
r.both(`${staleWords(c.stale_lanes)}: ${c.stale_lanes === 1 ? "its" : "their"} problems are listed, each marked “${STALE_MARK}”.`);
|
|
12998
13212
|
l.problems.forEach((p, i) => {
|
|
12999
13213
|
const keep = i < SHORT_PLACES;
|
|
13000
13214
|
r.add(keep, "", `${l.offset + i + 1}. ${p.text}: ${p.place_name}${opts.verbose ? ` (${p.place})` : ""}${p.intermittent ? ` — ${p.note}, not counted` : ""}`);
|
|
13001
|
-
r.add(keep, ` ${p.journey_title ?? p.journey} on ${p.phone_name}, ${p.runs_text}${p.tag_word ? `, ${p.tag_word}` : ""}`);
|
|
13215
|
+
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}` : ""}`);
|
|
13002
13216
|
const top = p.top_components[0];
|
|
13003
13217
|
const topName = componentReadNames(p.top_components)[0];
|
|
13004
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);
|
|
@@ -13021,56 +13235,59 @@ function formatProblemList(l, opts = {}) {
|
|
|
13021
13235
|
}
|
|
13022
13236
|
var commitText = (c) => !c ? "commit not recorded" : c.title ? `“${c.title}”${c.author ? ` by ${c.author}` : ""}${c.sha ? ` (${c.sha.slice(0, 7)})` : ""}` : c.sha ? c.sha.slice(0, 7) : "commit not recorded";
|
|
13023
13237
|
function formatCompare(r, opts = {}) {
|
|
13024
|
-
const
|
|
13025
|
-
|
|
13026
|
-
`Run B: ${r.b.external_id} on ${r.b.branch ?? "?"}, ${commitText(r.b.commit ? { sha: r.b.commit_sha, ...r.b.commit } : { sha: r.b.commit_sha, title: null, author: null })}`,
|
|
13027
|
-
`${r.same_journey ? "Same journey" : "Different journeys"}, ${r.same_phone ? "same phone" : "different phones"}. ${r.note}`
|
|
13028
|
-
];
|
|
13238
|
+
const rows = new Rows;
|
|
13239
|
+
rows.both(`Run A: ${r.a.external_id} on ${r.a.branch ?? "?"}, ${commitText(r.a.commit ? { sha: r.a.commit_sha, ...r.a.commit } : { sha: r.a.commit_sha, title: null, author: null })}`, `Run B: ${r.b.external_id} on ${r.b.branch ?? "?"}, ${commitText(r.b.commit ? { sha: r.b.commit_sha, ...r.b.commit } : { sha: r.b.commit_sha, title: null, author: null })}`, `${r.same_journey ? "Same journey" : "Different journeys"}, ${r.same_phone ? "same phone" : "different phones"}. ${r.note}`);
|
|
13029
13240
|
if (r.setup_differs.length)
|
|
13030
|
-
|
|
13241
|
+
rows.both(`Measured differently: ${r.setup_differs.map((d) => `${d.field} ${JSON.stringify(d.a)} → ${JSON.stringify(d.b)}`).join(", ")}`);
|
|
13031
13242
|
for (const w of r.windows) {
|
|
13032
13243
|
const ms = w.metrics.filter((m) => m.a !== null || m.b !== null);
|
|
13033
13244
|
const c = w.component_page ?? w.components;
|
|
13034
13245
|
if (!ms.length && !c)
|
|
13035
13246
|
continue;
|
|
13036
|
-
|
|
13247
|
+
rows.both("", `${w.window_name}${opts.verbose ? ` (${w.window})` : ""}`);
|
|
13037
13248
|
const singles = [...c?.added ?? [], ...c?.removed ?? [], ...w.component_page?.uncompared?.a ?? [], ...w.component_page?.uncompared?.b ?? []];
|
|
13038
13249
|
const components = [...c?.rows ?? [], ...singles];
|
|
13039
13250
|
const names = componentReadNames(components.map((row) => ({ key: row.key, name: row.component })));
|
|
13040
13251
|
const shown = new Map(components.map((row, i) => [row.key, names[i]]));
|
|
13041
13252
|
for (const row of c?.rows ?? [])
|
|
13042
|
-
|
|
13043
|
-
for (const [label,
|
|
13253
|
+
rows.full(` ${shown.get(row.key)}: ${row.self_ms_a === null ? "–" : exactComponentMs(row.self_ms_a)} → ${row.self_ms_b === null ? "–" : exactComponentMs(row.self_ms_b)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
13254
|
+
for (const [label, list] of [
|
|
13044
13255
|
[READ_WORDS.onlyB, c?.added ?? []],
|
|
13045
13256
|
[READ_WORDS.onlyA, c?.removed ?? []]
|
|
13046
13257
|
]) {
|
|
13047
|
-
for (const row of
|
|
13048
|
-
|
|
13258
|
+
for (const row of list)
|
|
13259
|
+
rows.full(` ${shown.get(row.key)}: ${label}; ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
13049
13260
|
}
|
|
13050
13261
|
const page = w.component_page;
|
|
13051
13262
|
if (page) {
|
|
13052
13263
|
for (const side of ["a", "b"]) {
|
|
13053
13264
|
if (!page.measured[side])
|
|
13054
|
-
|
|
13265
|
+
rows.full(` ${notMeasuredText(side)}`);
|
|
13055
13266
|
for (const row of page.uncompared?.[side] ?? [])
|
|
13056
|
-
|
|
13267
|
+
rows.full(` ${shown.get(row.key)}: ${side === "a" ? READ_WORDS.runA : READ_WORDS.runB} ${row.self_ms === null ? "–" : exactComponentMs(row.self_ms)}${opts.verbose ? ` (${row.key})` : ""}`);
|
|
13057
13268
|
}
|
|
13058
|
-
|
|
13269
|
+
rows.full(` ${componentPageText(page.page, components.length)}`);
|
|
13059
13270
|
} else if (c?.totals)
|
|
13060
|
-
|
|
13271
|
+
rows.full(` ${componentListsText(c.totals, { rows: c.rows.length, added: c.added?.length ?? 0, removed: c.removed?.length ?? 0 })}`);
|
|
13061
13272
|
if (c?.omissions)
|
|
13062
13273
|
for (const side of ["a", "b"]) {
|
|
13063
13274
|
if (page && !page.measured[side])
|
|
13064
13275
|
continue;
|
|
13065
13276
|
const o = c.omissions[side];
|
|
13066
13277
|
if (o.sdkOmitted === null || o.omitted === null || o.sdkOmitted > 0 || o.omitted > 0)
|
|
13067
|
-
|
|
13278
|
+
rows.full(` ${omissionText({ run_id: side === "a" ? READ_WORDS.runA : READ_WORDS.runB, ...o })}`);
|
|
13068
13279
|
}
|
|
13280
|
+
const top = new Set(ms.map((m) => ({ m, by: timeMove(m.metric, m.a, m.b) })).filter((x) => x.by !== null && x.by > 0).sort((x, y) => y.by - x.by).slice(0, SHORT_COMPARE_MEASURES).map((x) => x.m));
|
|
13069
13281
|
for (const m of ms)
|
|
13070
|
-
|
|
13282
|
+
rows.add(top.has(m), ` ${m.label.padEnd(28)}${m.a_text.padStart(10)} → ${m.b_text}`);
|
|
13071
13283
|
}
|
|
13072
|
-
|
|
13073
|
-
|
|
13284
|
+
const p = r.windows_page;
|
|
13285
|
+
if (p) {
|
|
13286
|
+
const order = p.order === "moved" ? ", the ones whose time moved most first" : "";
|
|
13287
|
+
const shownText = r.windows.length ? `Showing places ${p.offset + 1}–${p.offset + r.windows.length} of ${p.total}${order}` : `Showing none of ${p.total} places`;
|
|
13288
|
+
rows.both("", p.next_offset !== null ? `${shownText}; the rest from offset ${p.next_offset} (--offset ${p.next_offset}).` : `${shownText}.`);
|
|
13289
|
+
}
|
|
13290
|
+
return rows.render(opts.form, ["the component lines", `the measures beyond each place's ${SHORT_COMPARE_MEASURES} times that moved most`]);
|
|
13074
13291
|
}
|
|
13075
13292
|
function formatHistory(h, opts = {}) {
|
|
13076
13293
|
const r = new Rows;
|
|
@@ -13102,8 +13319,8 @@ function formatHistory(h, opts = {}) {
|
|
|
13102
13319
|
}
|
|
13103
13320
|
function formatCommits(c) {
|
|
13104
13321
|
return [
|
|
13105
|
-
`${plural2(c.commits.length, "measured commit")} (${plural2(c.sessions, "session")}) on ${c.branch}, oldest first:`,
|
|
13106
|
-
...c.commits.map((x) => ` ${when(x.first_measured_at)} ${commitText(x)}${x.pull_request?.number ? `, PR #${x.pull_request.number}` : ""}${x.sessions > 1 ? `; ${x.sessions} sessions, the last ${when(x.last_measured_at)}` : ""}`),
|
|
13322
|
+
`${plural2(c.commits.length, "measured commit")} (${plural2(c.sessions, "session")}) ${c.from_branch ? `from ${c.from_branch} to ${c.branch}` : `on ${c.branch}`}, oldest first:`,
|
|
13323
|
+
...c.commits.map((x) => ` ${when(x.first_measured_at)} ${commitText(x)}${c.from_branch && x.branch ? ` on ${x.branch}` : ""}${x.pull_request?.number ? `, PR #${x.pull_request.number}` : ""}${x.sessions > 1 ? `; ${x.sessions} sessions, the last ${when(x.last_measured_at)}` : ""}`),
|
|
13107
13324
|
c.note
|
|
13108
13325
|
].join(`
|
|
13109
13326
|
`);
|
|
@@ -13139,7 +13356,7 @@ function formatSessionPlaces(p, opts = {}) {
|
|
|
13139
13356
|
return r.both("Stale: its statistics are of another derivation or stats version and are not served until the next recompute rewrites them; nothing is known about its places (not zero).").render(opts.form);
|
|
13140
13357
|
const by = p.filters.measure;
|
|
13141
13358
|
const byLabel = p.places.flatMap((x) => x.measures).find((m) => m.measure === by)?.label ?? by;
|
|
13142
|
-
r.both(`Stored per-session statistics: no verdict, no comparison. Places by ${byLabel} (${p.filters.sort}), highest first${p.filters.window ? `, matching “${p.filters.window}”` : ""}${p.without_measure ? `; ${p.without_measure} of ${p.total} places ${p.without_measure === 1 ? "has" : "have"} no value for it` : ""}.`, "");
|
|
13359
|
+
r.both(`Stored per-session statistics: no verdict, no comparison. Places by ${byLabel} (${p.filters.sort}), highest first${p.filters.window ? `, matching “${p.filters.window}”` : ""}${p.without_measure ? `; ${p.without_measure} of ${p.total} places ${p.without_measure === 1 ? "has" : "have"} no value for it` : ""}${p.folded_twins ? `; ${foldedTwinsText(p.folded_twins)}` : ""}.`, "");
|
|
13143
13360
|
let componentLines = 0;
|
|
13144
13361
|
p.places.forEach((place, i) => {
|
|
13145
13362
|
const m = place.measures.find((x) => x.measure === by);
|
|
@@ -13212,7 +13429,7 @@ function formatMainSessions(h, opts = {}) {
|
|
|
13212
13429
|
if (!s.places)
|
|
13213
13430
|
r.add(k, ` ${MAIN_SUMMARY_PENDING}${s.summary_reason ? ` (${s.summary_reason})` : ""}`);
|
|
13214
13431
|
else
|
|
13215
|
-
r.add(k, ` ${MAIN_TIERS_WORST_FIRST.map((tier) => `${MAIN_TIER_WORDS[tier]}: ${s.places[tier]}`).join("; ")}${s.cant_reason ? `; ${mainSessionCantWords(s.places.cant, s.cant_reason, baseline)}` : ""}`);
|
|
13432
|
+
r.add(k, ` ${MAIN_TIERS_WORST_FIRST.map((tier) => `${MAIN_TIER_WORDS[tier]}: ${s.places[tier]}`).join("; ")}${s.cant_reason ? `; ${mainSessionCantWords(s.places.cant, s.cant_reason, baseline)}` : s.cant_range ? `; ${mainSessionCantRangeWords(s.places.cant, s.cant_range, baseline)}` : ""}`);
|
|
13216
13433
|
if (s.waiting_sessions)
|
|
13217
13434
|
r.add(k, ` ${waitingSessionsWords(s.waiting_sessions)}`);
|
|
13218
13435
|
if (s.memory)
|
|
@@ -13254,9 +13471,13 @@ function formatMainSessionPlaces(p, opts = {}) {
|
|
|
13254
13471
|
return r.both(`${MAIN_SUMMARY_PENDING}${p.summary_reason ? ` (${p.summary_reason})` : ""}`).render(opts.form);
|
|
13255
13472
|
const baselineSessions = p.baseline_sessions;
|
|
13256
13473
|
r.both(`Main session places: tiers are verdicts vs ${p.baseline_sessions === undefined ? "earlier sessions" : `the ${p.baseline_sessions} sessions before`}.`);
|
|
13257
|
-
|
|
13258
|
-
|
|
13259
|
-
|
|
13474
|
+
const kept = new Set(foldTwins([...p.places].sort((a, b) => MAIN_TIER_RANK[a.tier] - MAIN_TIER_RANK[b.tier]), (x) => x.place, (x) => x.ref).rows);
|
|
13475
|
+
const places = p.places.filter((x) => kept.has(x));
|
|
13476
|
+
for (const x of places)
|
|
13477
|
+
r.add(x.tier !== "same", ` ${windowQualified(x.place)}${opts.verbose ? ` (${x.place}; ${x.ref})` : ""} ${storedRefWords(x.ref, places.filter((row) => row.place === x.place).map((row) => row.ref))}: ${MAIN_TIER_WORDS[x.tier]}${x.reason ? `; ${mainPlaceReasonWords(x.reason, { baseline: { sessions: baselineSessions } })}` : ""}${x.reason && mainUnstableWords(x.reason) ? `. ${mainUnstableWords(x.reason)}` : ""}`);
|
|
13478
|
+
if (places.length < p.places.length)
|
|
13479
|
+
r.both(` ${foldedTwinsText(p.places.length - places.length)}`);
|
|
13480
|
+
const inNorm = places.filter((x) => x.tier === "same").length;
|
|
13260
13481
|
return r.render(opts.form, inNorm ? [plural2(inNorm, "place") + " in norm"] : []);
|
|
13261
13482
|
}
|
|
13262
13483
|
var signed = (n) => `${n > 0 ? "+" : ""}${Number(n.toFixed(3))}`;
|
|
@@ -13295,6 +13516,12 @@ function renamedLines(renamed, places) {
|
|
|
13295
13516
|
...component ? [` ${componentNotJoinedText(renamed.sdk)}`] : []
|
|
13296
13517
|
];
|
|
13297
13518
|
}
|
|
13519
|
+
function storedValueText(ref, v, unit) {
|
|
13520
|
+
const rest = ref.slice(2);
|
|
13521
|
+
const field = ref.startsWith("b:") ? `${rest.slice(0, rest.lastIndexOf(":"))}.${rest.slice(rest.lastIndexOf(":") + 1)}` : rest;
|
|
13522
|
+
const text = fieldValueText(field, v);
|
|
13523
|
+
return unit && !/[^\d.,\s-]/.test(text) ? `${text} ${unit}` : text;
|
|
13524
|
+
}
|
|
13298
13525
|
function formatPlaceTrend(t, opts = {}) {
|
|
13299
13526
|
const r = new Rows().both(`${t.journey} · ${t.phone.model} · ${t.baseline_branch}: stored per-session statistics`);
|
|
13300
13527
|
const newest = new Set(t.sessions.slice(-SHORT_SESSIONS).map((s) => s.run_set_id));
|
|
@@ -13315,10 +13542,10 @@ function formatPlaceTrend(t, opts = {}) {
|
|
|
13315
13542
|
const value = place.sessions.find((s) => s.run_set_id === session.run_set_id);
|
|
13316
13543
|
if (!value)
|
|
13317
13544
|
continue;
|
|
13318
|
-
const
|
|
13545
|
+
const value_ = (v) => storedValueText(place.ref, v, place.unit);
|
|
13319
13546
|
if (!k)
|
|
13320
13547
|
olderSessions.add(session.run_set_id);
|
|
13321
|
-
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${value.window ?? place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value.median}
|
|
13548
|
+
r.add(k, `${session.completed_at?.slice(0, 10) ?? "date unknown"} ${session.commit.sha?.slice(0, 7) ?? "commit unknown"} · ${windowQualified(place.window)}${opts.verbose ? ` (${value.window ?? place.window}; ${place.ref})` : ""} · ${place.label} (${storedRefWords(place.ref, t.places.filter((row) => row.window === place.window).map((row) => row.ref))}): median ${value_(value.median)}, MAD ${value_(value.mad)}, n=${value.n}`);
|
|
13322
13549
|
}
|
|
13323
13550
|
}
|
|
13324
13551
|
if (!t.sessions.length)
|
|
@@ -13375,6 +13602,8 @@ var branchStatus = (t, branch) => get(t, "v1/branches/status", { branch });
|
|
|
13375
13602
|
var listProblems = (t, f = {}) => get(t, "v1/problems", { branch: f.branch, main: f.main ? "true" : undefined, journey: f.journey, phone: f.phone, limit: f.limit, offset: f.offset || undefined });
|
|
13376
13603
|
var getFinding = (t, id) => get(t, `v1/findings/${encodeURIComponent(id)}`);
|
|
13377
13604
|
var compareRuns = (t, a, b, page = {}) => get(t, "v1/compare", { a, b, ...page });
|
|
13605
|
+
var SHORT_COMPARE_PLACES = 10;
|
|
13606
|
+
var compareQuery = (short, page) => short ? { order: "moved", limit: SHORT_COMPARE_PLACES, ...page } : page;
|
|
13378
13607
|
var windowHistory = (t, q) => get(t, "v1/history", q);
|
|
13379
13608
|
var commitsBetween = (t, q) => get(t, "v1/commits", q);
|
|
13380
13609
|
var layers = (t) => get(t, "v1/layers");
|
|
@@ -13409,7 +13638,7 @@ var FINDINGS_USAGE = `keepsnappy findings [--branch NAME | --main] [--journey KE
|
|
|
13409
13638
|
problems --branch NAME | --main [--journey KEY] [--phone MODEL] [--limit N] [--offset N] [--short] [--json]
|
|
13410
13639
|
(the single problems "Over budget" / "Far over budget" in the branch's own runs, whatever main does: not a regression, never blocking)
|
|
13411
13640
|
finding <id> [--prompt] [--short] [--json] (--prompt: only the hand-off for a coding agent; with --json the whole finding)
|
|
13412
|
-
compare <runA> <runB> [--json]
|
|
13641
|
+
compare <runA> <runB> [--short] [--limit N] [--offset N] [--json] (run ids or external ids; one run against one, no statistics; --short: the places whose time moved most)
|
|
13413
13642
|
history <journey> <window> [--phone MODEL] [--short] [--json] (exact window id, e.g. "press:live.chat.send")
|
|
13414
13643
|
commits <from> [<to>] [--branch NAME] [--json] (measured commits between two run sets or commit hashes)
|
|
13415
13644
|
places [--run-set ID | --journey KEY [--phone MODEL] [--branch NAME]] [--measure ID] [--sort median|p90|mad] [--window TEXT] [--limit N] [--offset N] [--top-components N] [--short] [--json]
|
|
@@ -13425,7 +13654,7 @@ ${READ_USAGE.detailFlag}
|
|
|
13425
13654
|
--short (findings, status, problems, finding, history, places, main, trend: what decides, without the evidence detail; the same as the MCP tools' form short; --json is unchanged)
|
|
13426
13655
|
(key: KEEPSNAPPY_KEY or --key-env NAME; API: --api URL or KEEPSNAPPY_API)`;
|
|
13427
13656
|
var STATES = ["present", "not-judged", "resolved", "all"];
|
|
13428
|
-
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "history", "places", "main", "trend"]);
|
|
13657
|
+
var SHORT_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "places", "main", "trend"]);
|
|
13429
13658
|
var FINDINGS_COMMANDS = new Set(["findings", "status", "problems", "finding", "compare", "history", "commits", "places", "layers", "main", "trend"]);
|
|
13430
13659
|
async function findingsCommand(cmd, argv, rt) {
|
|
13431
13660
|
const args = (() => {
|
|
@@ -13564,7 +13793,8 @@ async function findingsCommand(cmd, argv, rt) {
|
|
|
13564
13793
|
const size = o["top-components"] === undefined ? 12 : Number(o["top-components"]);
|
|
13565
13794
|
if (!Number.isInteger(size) || size < 1 || size > 200)
|
|
13566
13795
|
throw new UsageError([READ_USAGE.compareTop]);
|
|
13567
|
-
const
|
|
13796
|
+
const components = page || o.all || o["top-components"] !== undefined ? { component_offset: ((page ?? 1) - 1) * size, component_limit: size } : {};
|
|
13797
|
+
const query = compareQuery(o.short === true, { ...components, ...limit ? { limit } : {}, ...offset !== undefined ? { offset } : {} });
|
|
13568
13798
|
const compared = await compareRuns(t, p[0], p[1], query);
|
|
13569
13799
|
return out(o.all ? await allCompare(t, compared, size) : compared, formatCompare);
|
|
13570
13800
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keepsnappy/agent",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"description": "KeepSnappy CLI: runs your app journeys, measures from inside the app and reports More work / Less work / No change per step.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|