pi-gauntlet 8.2.1 → 8.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +5 -5
- package/agents/loop-implementer.md +19 -4
- package/bin/gauntlet-performance.mjs +477 -64
- package/bin/gauntlet-performance.test.mjs +27 -27
- package/bin/gauntlet-spec-index.mjs +290 -47
- package/bin/gauntlet-spec-index.test.mjs +42 -18
- package/bin/gauntlet-telemetry-seal.mjs +220 -77
- package/bin/gauntlet-telemetry-seal.test.mjs +222 -457
- package/extensions/lib/gauntlet-settings.test.ts +12 -14
- package/extensions/lib/gauntlet-settings.ts +7 -16
- package/extensions/lib/plan-check.test.ts +10 -0
- package/extensions/lib/plan-check.ts +2 -2
- package/extensions/lib/telemetry-branch.test.ts +102 -0
- package/extensions/lib/telemetry-branch.ts +145 -0
- package/extensions/lib/telemetry-diff.test.ts +2 -2
- package/extensions/lib/telemetry-diff.ts +1 -1
- package/extensions/lib/telemetry-leg.ts +163 -0
- package/extensions/lib/telemetry-record.ts +2 -0
- package/extensions/lib/vcs.ts +6 -23
- package/extensions/telemetry-leg-failures.test.ts +138 -0
- package/extensions/telemetry-leg-fields.test.ts +130 -0
- package/extensions/telemetry-leg-no-write.test.ts +90 -0
- package/extensions/telemetry-leg-rounds.test.ts +157 -0
- package/extensions/telemetry-leg-trackers.test.ts +114 -0
- package/extensions/telemetry-leg.test.ts +151 -0
- package/extensions/telemetry-settlement-contract.test.ts +67 -0
- package/extensions/telemetry.test.ts +44 -27
- package/extensions/telemetry.ts +68 -48
- package/extensions/test-support/git-env.mjs +13 -0
- package/package.json +1 -1
- package/skills/brainstorming/SKILL.md +3 -1
- package/skills/check-delivery/SKILL.md +2 -0
- package/skills/finishing-a-development-branch/SKILL.md +13 -13
- package/skills/finishing-a-development-branch/reference/verification-set.md +2 -2
- package/skills/gatekeep-pr/reference/batch.md +3 -1
- package/skills/gatekeep-pr/reference/cleanup.md +1 -1
- package/skills/gatekeep-pr/reference/post-selection-loop.md +7 -1
- package/skills/gauntlet-resume/reference/reconstruction.md +6 -6
- package/skills/gauntlet-resume/reference/seed.md +1 -6
- package/skills/loop-implementation/SKILL.md +40 -21
- package/skills/loop-implementation/reference/action-spend.md +10 -0
- package/skills/loop-implementation/reference/coordinated-legs.md +5 -5
- package/skills/loop-implementation/reference/interrupted-recovery.md +10 -0
- package/skills/loop-implementation/reference/unreachable-returns.md +13 -0
- package/skills/shape-ticket/SKILL.md +1 -1
- package/skills/shape-ticket/reference/repo-audit.md +2 -2
- package/skills/verification-before-completion/reference/conformance-check.md +1 -1
|
@@ -6,21 +6,19 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/bins/gauntlet-performance.mjs
|
|
9
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, realpathSync, statSync as statSync2 } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
|
-
import { basename, join, resolve } from "node:path";
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
13
|
-
import
|
|
11
|
+
import { basename as basename2, join as join3, resolve } from "node:path";
|
|
12
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
13
|
+
import process2 from "node:process";
|
|
14
14
|
import { parse as parseYaml } from "yaml";
|
|
15
15
|
|
|
16
16
|
// extensions/lib/gauntlet-settings.ts
|
|
17
|
-
import path from "node:path";
|
|
18
17
|
function mergeGauntlet(preset, repo) {
|
|
19
18
|
return { ...preset ?? {}, ...repo ?? {} };
|
|
20
19
|
}
|
|
21
20
|
var nonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
|
|
22
21
|
var joinWarn = (ws) => ws.length ? ws.join("; ") : void 0;
|
|
23
|
-
var DEFAULT_TELEMETRY_DIR = ".pi/gauntlet/telemetry";
|
|
24
22
|
var DEFAULT_TELEMETRY_BUCKETS = [
|
|
25
23
|
["test", ["**/test/**", "**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/*_test.*"]],
|
|
26
24
|
["docs", ["**/*.md"]],
|
|
@@ -30,13 +28,8 @@ function resolveTelemetry(g) {
|
|
|
30
28
|
const t = g.telemetry;
|
|
31
29
|
const warnings = [];
|
|
32
30
|
const enabled = t?.enabled !== false;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const value = nonEmptyString(t.dir) ? t.dir.trim().replace(/\/+$/, "") : "";
|
|
36
|
-
const canonical = value.replace(/\\/g, "/");
|
|
37
|
-
const normalized = path.posix.normalize(canonical);
|
|
38
|
-
if (value && path.win32.parse(canonical).root === "" && normalized !== ".." && !normalized.startsWith("../")) dir = normalized;
|
|
39
|
-
else warnings.push("telemetry.dir must be a non-empty path relative to the git toplevel; using the default");
|
|
31
|
+
if (t && Object.hasOwn(t, "dir")) {
|
|
32
|
+
warnings.push("piGauntlet.telemetry.dir is retired; records live on the gauntlet/telemetry branch - set piGauntlet.telemetry.remote to redirect them");
|
|
40
33
|
}
|
|
41
34
|
let buckets = DEFAULT_TELEMETRY_BUCKETS;
|
|
42
35
|
if (t?.buckets !== void 0) {
|
|
@@ -45,7 +38,212 @@ function resolveTelemetry(g) {
|
|
|
45
38
|
if (valid) buckets = Object.entries(b).map(([name, globs]) => [name, [...globs]]);
|
|
46
39
|
else warnings.push("telemetry.buckets is not an object of non-empty glob arrays; using the defaults");
|
|
47
40
|
}
|
|
48
|
-
|
|
41
|
+
const remote = nonEmptyString(t?.remote) ? t.remote.trim() : void 0;
|
|
42
|
+
if (t?.remote !== void 0 && !remote) warnings.push("telemetry.remote must be a non-empty URL; using origin");
|
|
43
|
+
return { enabled, ...remote ? { remote } : {}, buckets, warning: joinWarn(warnings) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// extensions/lib/telemetry-branch.ts
|
|
47
|
+
import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
48
|
+
import { tmpdir } from "node:os";
|
|
49
|
+
import { basename, join as join2 } from "node:path";
|
|
50
|
+
|
|
51
|
+
// extensions/lib/checkout.ts
|
|
52
|
+
import { execFileSync } from "node:child_process";
|
|
53
|
+
import { existsSync, statSync } from "node:fs";
|
|
54
|
+
import { dirname, join } from "node:path";
|
|
55
|
+
var CHECKOUT_ARGS = ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-dir", "--git-common-dir"];
|
|
56
|
+
function parseCheckout(r) {
|
|
57
|
+
if (r.code !== 0) return void 0;
|
|
58
|
+
const lines = r.stdout.trim().split("\n").map((l) => l.trim());
|
|
59
|
+
if (lines.length < 3 || !lines[0]) return void 0;
|
|
60
|
+
return { toplevel: lines[0], isPrimary: lines[1] === lines[2] };
|
|
61
|
+
}
|
|
62
|
+
var isDir = (p) => {
|
|
63
|
+
try {
|
|
64
|
+
return statSync(p).isDirectory();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
function nearestExistingDir(absPath) {
|
|
70
|
+
let p = absPath;
|
|
71
|
+
while (!(existsSync(p) && isDir(p))) {
|
|
72
|
+
const parent = dirname(p);
|
|
73
|
+
if (parent === p) return p;
|
|
74
|
+
p = parent;
|
|
75
|
+
}
|
|
76
|
+
return p;
|
|
77
|
+
}
|
|
78
|
+
var JJ_ROOT_ARGS = ["root"];
|
|
79
|
+
function parseJjRoot(r) {
|
|
80
|
+
if (r.code !== 0) return void 0;
|
|
81
|
+
const toplevel = r.stdout.trim();
|
|
82
|
+
if (!toplevel) return void 0;
|
|
83
|
+
return { toplevel, isPrimary: isDir(join(toplevel, ".jj", "repo")) };
|
|
84
|
+
}
|
|
85
|
+
function checkoutOfSync(absPath, git = gitSync, jj = jjSync) {
|
|
86
|
+
const cwd = nearestExistingDir(absPath);
|
|
87
|
+
const g = parseCheckout(git(CHECKOUT_ARGS, cwd));
|
|
88
|
+
if (g) return { ...g, via: "git" };
|
|
89
|
+
const j = parseJjRoot(jj(JJ_ROOT_ARGS, cwd));
|
|
90
|
+
return j ? { ...j, via: "jj" } : void 0;
|
|
91
|
+
}
|
|
92
|
+
var commandSync = (bin) => (args, cwd) => {
|
|
93
|
+
try {
|
|
94
|
+
const stdout = execFileSync(bin, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
95
|
+
return { code: 0, stdout };
|
|
96
|
+
} catch (e) {
|
|
97
|
+
const status = e.status;
|
|
98
|
+
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var gitSync = commandSync("git");
|
|
102
|
+
var jjSync = commandSync("jj");
|
|
103
|
+
|
|
104
|
+
// extensions/lib/vcs.ts
|
|
105
|
+
import { spawnSync } from "node:child_process";
|
|
106
|
+
|
|
107
|
+
// extensions/lib/phase-tracker-helpers.ts
|
|
108
|
+
var STMT_START = "(?:^|[\\n;&|(])\\s*";
|
|
109
|
+
|
|
110
|
+
// extensions/lib/telemetry-paths.ts
|
|
111
|
+
var GIT_FLAGS = "git\\s+(?:-\\S+(?:\\s+\\S+)?\\s+)*";
|
|
112
|
+
var SHIP_RE = new RegExp(STMT_START + "(" + GIT_FLAGS + "(?:merge\\s+--squash|push)(?=\\s|$)|gh\\s+pr\\s+create)");
|
|
113
|
+
var SQUASH_RE = new RegExp("^" + GIT_FLAGS + "merge\\s+--squash");
|
|
114
|
+
|
|
115
|
+
// extensions/lib/telemetry-diff.ts
|
|
116
|
+
var JJ_MAINLINE = "coalesce(trunk() ~ root(), present(main), present(master))";
|
|
117
|
+
var JJ_FORK_POINT = `fork_point(${JJ_MAINLINE} | @) ~ root() & ::${JJ_MAINLINE}`;
|
|
118
|
+
|
|
119
|
+
// extensions/lib/vcs.ts
|
|
120
|
+
var NO_PROMPT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_EDITOR: "true" };
|
|
121
|
+
var runBinary = (bin) => (args, cwd, timeoutMs = 1e4, options = {}) => {
|
|
122
|
+
const r = spawnSync(bin, args, { cwd, encoding: "utf8", input: options.input, env: { ...NO_PROMPT_ENV, ...options.env }, timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
|
|
123
|
+
const err = r.error;
|
|
124
|
+
const timedOut = err?.code === "ETIMEDOUT";
|
|
125
|
+
const stderr = timedOut ? "timed out" : (r.stderr ?? "").trim().split("\n")[0] || err?.message || `${bin} exited ${r.status}`;
|
|
126
|
+
return { ok: !timedOut && r.status === 0, code: timedOut ? 1 : r.status ?? 1, stdout: options.raw ? r.stdout ?? "" : (r.stdout ?? "").trim(), stderr };
|
|
127
|
+
};
|
|
128
|
+
var gitRun = runBinary("git");
|
|
129
|
+
|
|
130
|
+
// extensions/lib/telemetry-branch.ts
|
|
131
|
+
var TELEMETRY_REF = "refs/heads/gauntlet/telemetry";
|
|
132
|
+
function telemetryRepo(root) {
|
|
133
|
+
const result = gitRun(["config", "--get", "remote.origin.url"], root);
|
|
134
|
+
const match = result.stdout.match(/^(?:https?:\/\/[^/]+\/|ssh:\/\/(?:[^@/]+@)?[^/]+\/|(?:[^@\s]+@)?[^:\s/]+:)(.+)$/);
|
|
135
|
+
const path = match?.[1].replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
136
|
+
if (result.ok && path) return path;
|
|
137
|
+
const top = checkoutOfSync(root)?.toplevel ?? root;
|
|
138
|
+
for (const file of [".pi/gauntlet-overrides.md", "gauntlet-overrides.md", "doc/gauntlet-overrides.md"]) {
|
|
139
|
+
const path2 = join2(top, file);
|
|
140
|
+
if (!existsSync2(path2)) continue;
|
|
141
|
+
const section = readFileSync(path2, "utf8").split(/^##\s+Issue tracker\s*$/m)[1]?.split(/^##\s/m)[0] ?? "";
|
|
142
|
+
const repo = /^-[\t ]*repo:[\t ]*(\S[^\r\n]*)$/m.exec(section)?.[1].trim();
|
|
143
|
+
if (repo) return repo;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
const worktrees = gitRun(["worktree", "list", "--porcelain", "-z"], root);
|
|
147
|
+
const primary = worktrees.ok ? /^worktree ([^\0]+)/.exec(worktrees.stdout)?.[1] : void 0;
|
|
148
|
+
return `local/${basename(primary ?? top)}`;
|
|
149
|
+
}
|
|
150
|
+
var trackingRef = (remote) => `refs/remotes/${remote === "origin" ? "origin" : "telemetry"}/gauntlet/telemetry`;
|
|
151
|
+
var fetchBranch = (root, remote) => gitRun(["fetch", "--no-tags", remote, `+${TELEMETRY_REF}:${trackingRef(remote)}`], root);
|
|
152
|
+
function syncTelemetryBranch(root, records, remote = "origin") {
|
|
153
|
+
const scratch = mkdtempSync(join2(tmpdir(), "gauntlet-telemetry-index-"));
|
|
154
|
+
const env = { GIT_INDEX_FILE: join2(scratch, "index") };
|
|
155
|
+
const git = (args, input) => {
|
|
156
|
+
const result = gitRun(args, root, void 0, { env, input });
|
|
157
|
+
if (!result.ok) throw new Error(result.stderr);
|
|
158
|
+
return result.stdout;
|
|
159
|
+
};
|
|
160
|
+
const tracking = trackingRef(remote);
|
|
161
|
+
let fetchWarning;
|
|
162
|
+
const fetch = () => {
|
|
163
|
+
const result = fetchBranch(root, remote);
|
|
164
|
+
fetchWarning = result.ok ? void 0 : `fetch failed: ${result.stderr}`;
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
const resolve2 = (ref) => {
|
|
168
|
+
const result = gitRun(["rev-parse", "--verify", ref], root);
|
|
169
|
+
return result.ok ? result.stdout : void 0;
|
|
170
|
+
};
|
|
171
|
+
const commit = (tree, parent, message) => git(["-c", "commit.gpgsign=false", "commit-tree", tree, ...parent ? ["-p", parent] : [], "-m", message]);
|
|
172
|
+
let committed = false;
|
|
173
|
+
try {
|
|
174
|
+
let tip = resolve2(TELEMETRY_REF);
|
|
175
|
+
committed = records.size === 0 && !!tip;
|
|
176
|
+
fetch();
|
|
177
|
+
let upstream = resolve2(tracking);
|
|
178
|
+
if (records.size) {
|
|
179
|
+
const parent = tip ?? upstream;
|
|
180
|
+
git(parent ? ["read-tree", parent] : ["read-tree", "--empty"]);
|
|
181
|
+
for (const [path, content] of records) {
|
|
182
|
+
const blob = git(["hash-object", "-w", "--stdin"], content);
|
|
183
|
+
git(["update-index", "--add", "--cacheinfo", "100644", blob, path]);
|
|
184
|
+
}
|
|
185
|
+
const tree = git(["write-tree"]);
|
|
186
|
+
const next = parent && git(["rev-parse", `${parent}^{tree}`]) === tree ? parent : commit(tree, parent, "telemetry: update records");
|
|
187
|
+
git(["update-ref", TELEMETRY_REF, next, tip ?? ""]);
|
|
188
|
+
tip = next;
|
|
189
|
+
committed = true;
|
|
190
|
+
}
|
|
191
|
+
const pending = () => tip ? git(["rev-list", "--reverse", "--topo-order", tip, ...upstream ? [`^${upstream}`] : []]).split("\n").filter(Boolean) : [];
|
|
192
|
+
if (!pending().length) return { ok: true, committed, pushed: false, warning: fetchWarning };
|
|
193
|
+
const push = () => gitRun(["push", remote, `${TELEMETRY_REF}:${TELEMETRY_REF}`], root);
|
|
194
|
+
const first = push();
|
|
195
|
+
if (first.ok) return { ok: true, committed, pushed: true, warning: fetchWarning };
|
|
196
|
+
const fetched = fetch();
|
|
197
|
+
if (!fetched.ok) throw new Error(`${first.stderr}; refresh failed: ${fetched.stderr}`);
|
|
198
|
+
upstream = resolve2(tracking);
|
|
199
|
+
let replayed = upstream;
|
|
200
|
+
for (const sha of pending()) {
|
|
201
|
+
git(replayed ? ["read-tree", replayed] : ["read-tree", "--empty"]);
|
|
202
|
+
const changes = git(["diff-tree", "--root", "--no-commit-id", "--raw", "--no-abbrev", "--no-renames", "-r", "-z", sha]).split("\0");
|
|
203
|
+
for (let i = 0; i + 1 < changes.length; i += 2) {
|
|
204
|
+
const [, mode, , blob] = changes[i].split(" ");
|
|
205
|
+
const path = changes[i + 1];
|
|
206
|
+
git(mode === "000000" ? ["update-index", "--force-remove", "--", path] : ["update-index", "--add", "--cacheinfo", mode, blob, path]);
|
|
207
|
+
}
|
|
208
|
+
replayed = commit(git(["write-tree"]), replayed, git(["log", "-1", "--format=%B", sha]));
|
|
209
|
+
}
|
|
210
|
+
if (replayed) git(["update-ref", TELEMETRY_REF, replayed, tip]);
|
|
211
|
+
const second = push();
|
|
212
|
+
if (!second.ok) throw new Error(second.stderr);
|
|
213
|
+
return { ok: true, committed, pushed: true };
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const reason = error.message;
|
|
216
|
+
return { ok: false, committed, pushed: false, reason, warning: [fetchWarning, `telemetry: not pushed (${reason})`].filter(Boolean).join("; ") };
|
|
217
|
+
} finally {
|
|
218
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function readTelemetryBranch(root, { remote = "origin", prefix = "" } = {}) {
|
|
222
|
+
const records = [];
|
|
223
|
+
let warning;
|
|
224
|
+
try {
|
|
225
|
+
const synced = syncTelemetryBranch(root, /* @__PURE__ */ new Map(), remote);
|
|
226
|
+
warning = synced.warning;
|
|
227
|
+
if (synced.pushed) {
|
|
228
|
+
const fetched = fetchBranch(root, remote);
|
|
229
|
+
warning = fetched.ok ? void 0 : `fetch failed: ${fetched.stderr}`;
|
|
230
|
+
}
|
|
231
|
+
const tip = gitRun(["rev-parse", "--verify", trackingRef(remote)], root);
|
|
232
|
+
if (!tip.ok) return { records, warning: [warning, "no telemetry ref fetched yet"].filter(Boolean).join("; ") };
|
|
233
|
+
const tree = gitRun(["ls-tree", "-r", "-z", tip.stdout, ...prefix ? ["--", prefix] : []], root);
|
|
234
|
+
if (!tree.ok) throw new Error(tree.stderr);
|
|
235
|
+
for (const entry of tree.stdout.split("\0").filter(Boolean)) {
|
|
236
|
+
const [meta, path] = entry.split(" ");
|
|
237
|
+
const [, type, sha] = meta.split(" ");
|
|
238
|
+
if (type !== "blob" || !path.startsWith(prefix) || !path.endsWith(".yaml")) continue;
|
|
239
|
+
const blob = gitRun(["cat-file", "blob", sha], root, void 0, { raw: true });
|
|
240
|
+
if (!blob.ok) throw new Error(blob.stderr);
|
|
241
|
+
records.push({ path, bytes: blob.stdout });
|
|
242
|
+
}
|
|
243
|
+
return { records, warning };
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return { records, warning: [warning, `read failed: ${error.message}`].filter(Boolean).join("; ") };
|
|
246
|
+
}
|
|
49
247
|
}
|
|
50
248
|
|
|
51
249
|
// src/bins/performance/shared.mjs
|
|
@@ -127,7 +325,7 @@ __export(versions_exports, {
|
|
|
127
325
|
renderText: () => renderText2
|
|
128
326
|
});
|
|
129
327
|
var key2 = "by_version";
|
|
130
|
-
var HEADER2 = ["version", "n", "shipped", "truncated", "wall p50/max", "tokens p50/max", "cost p50/max", "disp p50", "grants p50/max", "reopens p50/max", "loops p50/max", "findings p50 b/M/m", "models"];
|
|
328
|
+
var HEADER2 = ["repo", "version", "n", "shipped", "truncated", "wall p50/max", "tokens p50/max", "cost p50/max", "disp p50", "grants p50/max", "reopens p50/max", "loops p50/max", "findings p50 b/M/m", "models"];
|
|
131
329
|
var versionOrder = (a, b) => {
|
|
132
330
|
const x = semver(a), y = semver(b);
|
|
133
331
|
return x && y ? cmpSemver(x, y) : x ? -1 : y ? 1 : 0;
|
|
@@ -136,15 +334,17 @@ function aggregate2(entries) {
|
|
|
136
334
|
const rows = entries.map((e) => e.row);
|
|
137
335
|
const groups = /* @__PURE__ */ new Map();
|
|
138
336
|
for (const r of rows) {
|
|
139
|
-
|
|
140
|
-
groups.
|
|
337
|
+
const key4 = JSON.stringify([r.repo, r.version]);
|
|
338
|
+
if (!groups.has(key4)) groups.set(key4, []);
|
|
339
|
+
groups.get(key4).push(r);
|
|
141
340
|
}
|
|
142
|
-
return [...groups.
|
|
341
|
+
return [...groups.values()].sort(([a], [b]) => a.repo.localeCompare(b.repo) || versionOrder(a.version, b.version)).map((all) => {
|
|
143
342
|
const shipped = all.filter((r) => r.status === "shipped" && !r.truncated);
|
|
144
343
|
const models = {};
|
|
145
344
|
for (const r of shipped) for (const m of r.models) models[m] = (models[m] ?? 0) + 1;
|
|
146
345
|
return {
|
|
147
|
-
|
|
346
|
+
repo: all[0].repo,
|
|
347
|
+
version: all[0].version,
|
|
148
348
|
n: all.length,
|
|
149
349
|
shipped: shipped.length,
|
|
150
350
|
truncated: all.filter((r) => r.truncated).length,
|
|
@@ -165,6 +365,7 @@ function aggregate2(entries) {
|
|
|
165
365
|
});
|
|
166
366
|
}
|
|
167
367
|
var versionRow = (g) => [
|
|
368
|
+
g.repo,
|
|
168
369
|
g.version,
|
|
169
370
|
String(g.n),
|
|
170
371
|
String(g.shipped),
|
|
@@ -222,8 +423,8 @@ function aggregate3(entries) {
|
|
|
222
423
|
const members = obj(obj(e.council)?.members);
|
|
223
424
|
if (!members) continue;
|
|
224
425
|
const roster = Object.keys(members).sort();
|
|
225
|
-
const k =
|
|
226
|
-
if (!groups.has(k)) groups.set(k, { roster, entries: [] });
|
|
426
|
+
const k = JSON.stringify([e.row.repo, roster]);
|
|
427
|
+
if (!groups.has(k)) groups.set(k, { repo: e.row.repo, roster, entries: [] });
|
|
227
428
|
groups.get(k).entries.push(e);
|
|
228
429
|
groups.get(k).last = i;
|
|
229
430
|
}
|
|
@@ -249,14 +450,14 @@ function aggregate3(entries) {
|
|
|
249
450
|
}
|
|
250
451
|
for (const m of g.roster) members[m].unique_applied_nonminor_per_run = (members[m].unique_applied.blocker + members[m].unique_applied.major) / runs;
|
|
251
452
|
const chairList = [...chairs.values()].sort((a, b) => b.runs - a.runs || a.model.localeCompare(b.model)).map((c) => ({ model: c.model, runs: c.runs, avg_clusters: c.clusters / c.runs, avg_members_reported: c.reported / c.runs, retried_runs: c.retried_runs }));
|
|
252
|
-
return { roster: g.roster, runs, members, chairs: chairList, flags: runs >= MIN_RUNS ? flagsFor(members, runs) : [] };
|
|
453
|
+
return { repo: g.repo, roster: g.roster, runs, members, chairs: chairList, flags: runs >= MIN_RUNS ? flagsFor(members, runs) : [] };
|
|
253
454
|
});
|
|
254
455
|
}
|
|
255
456
|
function renderText3(agg) {
|
|
256
457
|
if (!agg.length) return "council\nno council data in selected records";
|
|
257
458
|
const out = ["council", "severity is the chair's consolidated severity (highest any co-raiser assigned), not the member's own"];
|
|
258
459
|
for (const g of agg) {
|
|
259
|
-
out.push(`roster (${g.runs} runs): ${g.roster.join(", ")}`);
|
|
460
|
+
out.push(`repo: ${g.repo}`, `roster (${g.runs} runs): ${g.roster.join(", ")}`);
|
|
260
461
|
const rows = g.roster.map((m) => {
|
|
261
462
|
const x = g.members[m];
|
|
262
463
|
return [m, cell(x.total), cell(x.unique), cell(x.applied), cell(x.unique_applied), cell(x.deferred), cell(x.rejected), x.unique_applied_nonminor_per_run.toFixed(2)];
|
|
@@ -269,11 +470,196 @@ function renderText3(agg) {
|
|
|
269
470
|
return out.join("\n");
|
|
270
471
|
}
|
|
271
472
|
|
|
473
|
+
// src/bins/performance/leg-rounds.mjs
|
|
474
|
+
var LEG_SKILLS = ["shape-ticket", "gatekeep-pr", "check-delivery"];
|
|
475
|
+
function loadLeg(doc) {
|
|
476
|
+
if (![doc.session, doc.ts].every((v) => typeof v === "string" && v.length) || !Array.isArray(doc.items) || !doc.items.every((item) => obj(item) && ["ticket", "pr"].includes(item.kind) && str(item.outcome) !== null && (item.key === void 0 || str(item.key) !== null))) {
|
|
477
|
+
return { skip: "not a leg record" };
|
|
478
|
+
}
|
|
479
|
+
return { leg: { ...doc, repo: str(doc.repo) ?? "unknown repo" } };
|
|
480
|
+
}
|
|
481
|
+
var legId = (leg) => JSON.stringify([leg.repo, leg.skill, leg.session, leg.ts]);
|
|
482
|
+
function itemKey(key4, repo) {
|
|
483
|
+
if (!key4) return null;
|
|
484
|
+
const ref = key4.replace(/@[a-f\d]+$/i, "");
|
|
485
|
+
if (/^#\d+$/.test(ref)) return `${repo}${ref}`;
|
|
486
|
+
const github = /^https?:\/\/[^/]+\/([^/]+\/[^/]+)\/(?:issues|pull)\/(\d+)\/?$/.exec(ref);
|
|
487
|
+
if (github) return `${github[1]}#${github[2]}`;
|
|
488
|
+
const linear = /^https?:\/\/[^/]+\/[^/]+\/issue\/([A-Z][A-Z0-9]*-\d+)(?:\/.*)?$/.exec(ref);
|
|
489
|
+
return linear ? linear[1] : ref;
|
|
490
|
+
}
|
|
491
|
+
function groupRounds(legs, skill) {
|
|
492
|
+
const groups = /* @__PURE__ */ new Map();
|
|
493
|
+
for (const leg of legs.filter((leg2) => leg2.skill === skill).sort((a, b) => a.ts.localeCompare(b.ts) || a.session.localeCompare(b.session))) {
|
|
494
|
+
const keys = new Set(leg.items.map((item) => itemKey(item.key, leg.repo)).filter(Boolean));
|
|
495
|
+
for (const key4 of keys) {
|
|
496
|
+
const id2 = JSON.stringify([leg.repo, key4]);
|
|
497
|
+
if (!groups.has(id2)) groups.set(id2, { repo: leg.repo, key: key4, rounds: [] });
|
|
498
|
+
groups.get(id2).rounds.push(leg);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return [...groups.values()].sort((a, b) => a.repo.localeCompare(b.repo) || a.key.localeCompare(b.key));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/bins/performance/costs.mjs
|
|
505
|
+
var allocationNote = "costs: batch and multi-ticket records are split equally across their items; the rounding remainder lands on the last row";
|
|
506
|
+
var columns = ["shape", "run", "gatekeep", "delivery"];
|
|
507
|
+
var skillColumn = { "shape-ticket": "shape", "gatekeep-pr": "gatekeep", "check-delivery": "delivery" };
|
|
508
|
+
var id = (repo, key4) => JSON.stringify([repo, key4]);
|
|
509
|
+
var ordered = (map) => [...map.values()].sort((a, b) => a.repo.localeCompare(b.repo) || a.key.localeCompare(b.key));
|
|
510
|
+
var add = (row, column, cost) => {
|
|
511
|
+
if (cost !== null) row[column] = (row[column] ?? 0) + cost;
|
|
512
|
+
};
|
|
513
|
+
var allocations = /* @__PURE__ */ new WeakMap();
|
|
514
|
+
var split = (cost, count) => Array(count).fill(cost === null ? null : cost / count);
|
|
515
|
+
function aggregateCosts(entries, legs) {
|
|
516
|
+
const tickets = /* @__PURE__ */ new Map(), prs = /* @__PURE__ */ new Map(), unattributed = /* @__PURE__ */ new Map();
|
|
517
|
+
const splits = [], links = [], cells = /* @__PURE__ */ new Map();
|
|
518
|
+
const cell2 = (row, column) => {
|
|
519
|
+
if (!cells.has(row)) cells.set(row, /* @__PURE__ */ new Map());
|
|
520
|
+
const columns2 = cells.get(row);
|
|
521
|
+
if (!columns2.has(column)) columns2.set(column, { row, column });
|
|
522
|
+
return columns2.get(column);
|
|
523
|
+
};
|
|
524
|
+
const ensure = (map, repo, key4) => {
|
|
525
|
+
const identity = id(repo, key4);
|
|
526
|
+
if (!map.has(identity)) map.set(identity, { repo, key: key4, shape: null, run: null, gatekeep: null, delivery: null, rounds: [] });
|
|
527
|
+
return map.get(identity);
|
|
528
|
+
};
|
|
529
|
+
for (const { row } of entries) {
|
|
530
|
+
const keys = [...new Set(row.tickets.map((ref) => itemKey(ref, row.repo)))].sort();
|
|
531
|
+
const shares = split(row.cost, keys.length || 1);
|
|
532
|
+
if (!keys.length) add(ensure(unattributed, row.repo, row.session), "run", shares[0]);
|
|
533
|
+
const recipients = keys.map((key4) => ensure(tickets, row.repo, key4));
|
|
534
|
+
recipients.forEach((recipient, i) => add(recipient, "run", shares[i]));
|
|
535
|
+
splits.push(recipients.map((row2) => cell2(row2, "run")));
|
|
536
|
+
}
|
|
537
|
+
const ticketPrs = /* @__PURE__ */ new Map();
|
|
538
|
+
for (const leg of [...legs].sort((a, b) => a.ts.localeCompare(b.ts) || a.session.localeCompare(b.session))) {
|
|
539
|
+
const items = [...leg.items].sort((a, b) => (itemKey(a.key, leg.repo) ?? "\uFFFF").localeCompare(itemKey(b.key, leg.repo) ?? "\uFFFF"));
|
|
540
|
+
const shares = split(num(leg.cost), items.length || 1);
|
|
541
|
+
const recipients = [];
|
|
542
|
+
if (!items.length) add(ensure(unattributed, leg.repo, leg.session), skillColumn[leg.skill], shares[0]);
|
|
543
|
+
const roundShares = /* @__PURE__ */ new Map();
|
|
544
|
+
items.forEach((item, i) => {
|
|
545
|
+
const key4 = itemKey(item.key, leg.repo);
|
|
546
|
+
const ticket = item.kind === "ticket" ? key4 : itemKey(item.ticket, leg.repo);
|
|
547
|
+
const row = ticket ? ensure(tickets, leg.repo, ticket) : ensure(unattributed, leg.repo, item.kind === "pr" && key4 ? key4 : leg.session);
|
|
548
|
+
add(row, skillColumn[leg.skill], shares[i]);
|
|
549
|
+
recipients.push(cell2(row, skillColumn[leg.skill]));
|
|
550
|
+
if (item.kind !== "pr" || !key4 || leg.skill !== "gatekeep-pr") return;
|
|
551
|
+
const pr = ensure(prs, leg.repo, key4);
|
|
552
|
+
if (!roundShares.has(pr)) roundShares.set(pr, null);
|
|
553
|
+
roundShares.set(pr, sumOrNull([roundShares.get(pr), shares[i]]));
|
|
554
|
+
if (ticket) {
|
|
555
|
+
const identity = id(leg.repo, ticket);
|
|
556
|
+
if (!ticketPrs.has(identity)) ticketPrs.set(identity, /* @__PURE__ */ new Set());
|
|
557
|
+
ticketPrs.get(identity).add(pr);
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
splits.push(recipients);
|
|
561
|
+
const roundCells = [];
|
|
562
|
+
for (const [pr, cost] of roundShares) {
|
|
563
|
+
pr.rounds.push(leg);
|
|
564
|
+
(pr.round_costs ??= []).push(cost);
|
|
565
|
+
roundCells.push(cell2(pr.round_costs, pr.round_costs.length - 1));
|
|
566
|
+
}
|
|
567
|
+
splits.push(roundCells);
|
|
568
|
+
}
|
|
569
|
+
for (const [identity, linked] of ticketPrs) {
|
|
570
|
+
const cost = tickets.get(identity).run;
|
|
571
|
+
const rows = [...linked].sort((a, b) => a.key.localeCompare(b.key));
|
|
572
|
+
const shares = split(cost, rows.length);
|
|
573
|
+
rows.forEach((row, i) => add(row, "run", shares[i]));
|
|
574
|
+
links.push({ source: cell2(tickets.get(identity), "run"), targets: rows.map((row) => cell2(row, "run")) });
|
|
575
|
+
}
|
|
576
|
+
for (const group of groupRounds(legs, "shape-ticket")) {
|
|
577
|
+
const row = tickets.get(id(group.repo, group.key));
|
|
578
|
+
if (row) row.rounds = group.rounds;
|
|
579
|
+
}
|
|
580
|
+
for (const row of [...tickets.values(), ...unattributed.values()]) row.total = sumOrNull(columns.map((column) => row[column]));
|
|
581
|
+
const prRows = ordered(prs);
|
|
582
|
+
for (const row of prRows) {
|
|
583
|
+
for (const column of ["shape", "gatekeep", "delivery"]) delete row[column];
|
|
584
|
+
row.total = sumOrNull([row.run, ...row.round_costs]);
|
|
585
|
+
}
|
|
586
|
+
const result = { tickets: ordered(tickets), prs: prRows, unattributed: ordered(unattributed) };
|
|
587
|
+
allocations.set(result, { splits, links });
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
function displayCosts(costs) {
|
|
591
|
+
const copies = /* @__PURE__ */ new Map();
|
|
592
|
+
const result = Object.fromEntries(Object.entries(costs).map(([table2, rows]) => [table2, rows.map((row) => {
|
|
593
|
+
const copy = { ...row };
|
|
594
|
+
for (const column of columns) if (typeof copy[column] === "number") copy[column] = Math.round(copy[column] * 100) / 100;
|
|
595
|
+
copies.set(row, copy);
|
|
596
|
+
if (row.round_costs) {
|
|
597
|
+
copy.round_costs = row.round_costs.map((cost) => cost === null ? null : Math.round(cost * 100) / 100);
|
|
598
|
+
copies.set(row.round_costs, copy.round_costs);
|
|
599
|
+
}
|
|
600
|
+
return copy;
|
|
601
|
+
})]));
|
|
602
|
+
const { splits, links } = allocations.get(costs);
|
|
603
|
+
const order = new Map([...copies.keys()].map((row, i) => [row, i]));
|
|
604
|
+
const round = (groups, linked = false) => {
|
|
605
|
+
const components = [];
|
|
606
|
+
for (const group of groups) {
|
|
607
|
+
const cells = new Set(linked ? group.targets : group);
|
|
608
|
+
const sources = new Set(linked ? [group.source] : []);
|
|
609
|
+
for (let i = components.length - 1; i >= 0; i--) {
|
|
610
|
+
if (![...components[i].cells].some((cell2) => cells.has(cell2))) continue;
|
|
611
|
+
for (const cell2 of components[i].cells) cells.add(cell2);
|
|
612
|
+
for (const source of components[i].sources) sources.add(source);
|
|
613
|
+
components.splice(i, 1);
|
|
614
|
+
}
|
|
615
|
+
if (cells.size) components.push({ cells, sources });
|
|
616
|
+
}
|
|
617
|
+
for (const { cells, sources } of components) {
|
|
618
|
+
const active = [...cells].filter(({ row, column }) => row[column] !== null).sort((a, b) => order.get(a.row) - order.get(b.row));
|
|
619
|
+
const total = sumOrNull([...linked ? sources : active].map(({ row, column }) => (linked ? copies.get(row) : row)[column]));
|
|
620
|
+
const shares = active.map(({ row, column }, index) => {
|
|
621
|
+
const value = row[column] * 100;
|
|
622
|
+
const cents = Math.trunc(value);
|
|
623
|
+
return { row, column, index, cents, fraction: value - cents };
|
|
624
|
+
});
|
|
625
|
+
const residual = Math.round(total * 100) - shares.reduce((sum2, share) => sum2 + share.cents, 0);
|
|
626
|
+
const direction = Math.sign(residual);
|
|
627
|
+
const ranked = shares.filter((share) => share.fraction * direction > 0).sort((a, b) => direction * (b.fraction - a.fraction) || (Math.abs(a.fraction) < 0.5 ? b.index - a.index : a.index - b.index));
|
|
628
|
+
const distributed = Math.min(Math.abs(residual), ranked.length);
|
|
629
|
+
for (let i = 0; i < distributed; i++) ranked[i].cents += direction;
|
|
630
|
+
if (distributed < Math.abs(residual)) shares.at(-1).cents += residual - distributed * direction;
|
|
631
|
+
for (const { row, column, cents } of shares) copies.get(row)[column] = cents / 100;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
round(splits);
|
|
635
|
+
round(links, true);
|
|
636
|
+
for (const rows of Object.values(costs)) for (const row of rows) {
|
|
637
|
+
const copy = copies.get(row);
|
|
638
|
+
const displayed = copy.round_costs ? [copy.run, ...copy.round_costs] : columns.map((column) => copy[column]);
|
|
639
|
+
copy.total = sumOrNull(displayed);
|
|
640
|
+
}
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
function renderCosts(aggregate4) {
|
|
644
|
+
const { tickets, prs, unattributed } = displayCosts(aggregate4);
|
|
645
|
+
const roundCount = Math.max(0, ...prs.map((row) => row.round_costs.length));
|
|
646
|
+
const costs = (rows) => rows.map((row) => [row.repo, row.key, ...columns.map((column) => fmtCost(row[column])), fmtCost(row.total)]);
|
|
647
|
+
return [
|
|
648
|
+
`tickets
|
|
649
|
+
${table(["repo", "ticket", "rounds", ...columns, "total"], costs(tickets).map((cells, i) => [...cells.slice(0, 2), String(tickets[i].rounds.length), ...cells.slice(2)]))}`,
|
|
650
|
+
`prs
|
|
651
|
+
${table(["repo", "PR", "rounds", "run", ...Array.from({ length: roundCount }, (_, i) => `round ${i + 1}`), "total"], prs.map((row) => [row.repo, row.key, String(row.rounds.length), fmtCost(row.run), ...Array.from({ length: roundCount }, (_, i) => fmtCost(row.round_costs[i] ?? null)), fmtCost(row.total)]))}`,
|
|
652
|
+
`unattributed
|
|
653
|
+
${table(["repo", "PR/session", ...columns, "total"], costs(unattributed))}`,
|
|
654
|
+
allocationNote
|
|
655
|
+
].join("\n\n");
|
|
656
|
+
}
|
|
657
|
+
|
|
272
658
|
// src/bins/gauntlet-performance.mjs
|
|
273
659
|
var SECTIONS = [runs_exports, versions_exports, council_exports];
|
|
274
660
|
var usage = () => {
|
|
275
|
-
|
|
276
|
-
|
|
661
|
+
process2.stderr.write("usage: gauntlet-performance [--dir <telemetry dir>]... [--since <version>] [--json]\n");
|
|
662
|
+
process2.exit(1);
|
|
277
663
|
};
|
|
278
664
|
function parseArgs(argv) {
|
|
279
665
|
const opts = { dirs: [], since: void 0, json: false };
|
|
@@ -288,57 +674,61 @@ function parseArgs(argv) {
|
|
|
288
674
|
return opts;
|
|
289
675
|
}
|
|
290
676
|
function readLayer(file) {
|
|
291
|
-
if (!
|
|
677
|
+
if (!existsSync3(file)) return {};
|
|
292
678
|
try {
|
|
293
|
-
return JSON.parse(
|
|
679
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
294
680
|
} catch (e) {
|
|
295
|
-
|
|
681
|
+
process2.stderr.write(`warning: ${file}: ${e.message}; using {} for this layer
|
|
296
682
|
`);
|
|
297
683
|
return {};
|
|
298
684
|
}
|
|
299
685
|
}
|
|
300
|
-
function
|
|
301
|
-
const agentDir =
|
|
302
|
-
const preset = readLayer(
|
|
303
|
-
const repo = readLayer(
|
|
686
|
+
function branchCorpus(root) {
|
|
687
|
+
const agentDir = process2.env.PI_CODING_AGENT_DIR || join3(homedir(), ".pi", "agent");
|
|
688
|
+
const preset = readLayer(join3(agentDir, "settings.json"));
|
|
689
|
+
const repo = readLayer(join3(root, ".pi", "settings.json"));
|
|
304
690
|
const t = resolveTelemetry(mergeGauntlet(preset?.piGauntlet, repo?.piGauntlet));
|
|
305
|
-
if (t.warning)
|
|
691
|
+
if (t.warning) process2.stderr.write(`warning: ${t.warning}
|
|
692
|
+
`);
|
|
693
|
+
const label = telemetryRepo(root);
|
|
694
|
+
if (!t.enabled) {
|
|
695
|
+
process2.stderr.write("telemetry disabled\n");
|
|
696
|
+
return { label, records: [] };
|
|
697
|
+
}
|
|
698
|
+
const result = readTelemetryBranch(root, { remote: t.remote, prefix: `${label}/` });
|
|
699
|
+
if (result.warning) process2.stderr.write(`warning: ${result.warning}
|
|
306
700
|
`);
|
|
307
|
-
return
|
|
701
|
+
return { label, records: result.records };
|
|
308
702
|
}
|
|
309
703
|
var gitToplevel = (cwd) => {
|
|
310
|
-
const r =
|
|
704
|
+
const r = spawnSync2("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { encoding: "utf8", timeout: 1e4 });
|
|
311
705
|
return r.status === 0 ? r.stdout.trim() : void 0;
|
|
312
706
|
};
|
|
313
|
-
function corpusFor(
|
|
314
|
-
const label =
|
|
315
|
-
if (!
|
|
316
|
-
const abs = realpathSync(
|
|
317
|
-
|
|
318
|
-
if (top !== void 0 && realpathSync(top) === abs) {
|
|
319
|
-
const dir = telemetryDirOf(abs);
|
|
320
|
-
return existsSync(dir) ? { label, dir } : { label, skip: "no telemetry dir" };
|
|
321
|
-
}
|
|
322
|
-
if (!statSync(abs).isDirectory()) return { label, skip: "not a directory" };
|
|
707
|
+
function corpusFor(path) {
|
|
708
|
+
const label = basename2(resolve(path));
|
|
709
|
+
if (!existsSync3(path)) return { label, skip: "not found" };
|
|
710
|
+
const abs = realpathSync(path);
|
|
711
|
+
if (!statSync2(abs).isDirectory()) return { label, skip: "not a directory" };
|
|
323
712
|
return { label, dir: abs };
|
|
324
713
|
}
|
|
325
714
|
function* yamlFiles(dir) {
|
|
326
|
-
if (!
|
|
715
|
+
if (!existsSync3(dir)) return;
|
|
327
716
|
for (const e of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
328
|
-
const p =
|
|
717
|
+
const p = join3(dir, e.name);
|
|
329
718
|
if (e.isDirectory()) yield* yamlFiles(p);
|
|
330
719
|
else if (e.isFile() && e.name.endsWith(".yaml")) yield p;
|
|
331
720
|
}
|
|
332
721
|
}
|
|
333
|
-
function loadRecord(file,
|
|
722
|
+
function loadRecord(file, bytes) {
|
|
334
723
|
let doc;
|
|
335
724
|
try {
|
|
336
|
-
doc = parseYaml(
|
|
725
|
+
doc = parseYaml(bytes);
|
|
337
726
|
} catch {
|
|
338
727
|
return { skip: "unparseable" };
|
|
339
728
|
}
|
|
340
729
|
const d = obj(doc);
|
|
341
730
|
if (!d) return { skip: "unparseable" };
|
|
731
|
+
if (LEG_SKILLS.includes(d.skill)) return loadLeg(d);
|
|
342
732
|
if (d.schema !== 1) return { skip: `schema ${d.schema === void 0 ? "missing" : String(d.schema)}` };
|
|
343
733
|
if (typeof d.spec !== "string" || typeof d.run_id !== "string") return { skip: "not a record" };
|
|
344
734
|
const derived = obj(d.derived) ?? {};
|
|
@@ -386,10 +776,12 @@ function loadRecord(file, label) {
|
|
|
386
776
|
return {
|
|
387
777
|
row: {
|
|
388
778
|
run_id: d.run_id,
|
|
779
|
+
session: Array.isArray(d.sessions) && typeof d.sessions[0] === "string" ? d.sessions[0] : d.run_id,
|
|
780
|
+
tickets: Array.isArray(d.tickets) ? d.tickets.filter((ref) => typeof ref === "string" && ref.length) : [],
|
|
389
781
|
engine: str(derived.engine) ?? "waves",
|
|
390
782
|
iterations: num(derived.iterations),
|
|
391
|
-
repo:
|
|
392
|
-
spec:
|
|
783
|
+
repo: str(d.repo) ?? "unknown repo",
|
|
784
|
+
spec: basename2(file, ".yaml"),
|
|
393
785
|
version: semver(version) ? version.trim() : "unknown",
|
|
394
786
|
status: str(d.status),
|
|
395
787
|
created_at: str(d.created_at),
|
|
@@ -423,13 +815,15 @@ function loadRecord(file, label) {
|
|
|
423
815
|
};
|
|
424
816
|
}
|
|
425
817
|
function main() {
|
|
426
|
-
const opts = parseArgs(
|
|
818
|
+
const opts = parseArgs(process2.argv.slice(2));
|
|
427
819
|
const corpora = [];
|
|
428
820
|
const skipped = [];
|
|
429
|
-
|
|
430
|
-
|
|
821
|
+
if (!opts.dirs.length) {
|
|
822
|
+
const top = gitToplevel(process2.cwd());
|
|
823
|
+
if (top === void 0) process2.stderr.write(`not a git checkout: ${process2.cwd()}
|
|
431
824
|
`);
|
|
432
|
-
|
|
825
|
+
else corpora.push(branchCorpus(top));
|
|
826
|
+
}
|
|
433
827
|
for (const p of opts.dirs) {
|
|
434
828
|
const c = corpusFor(p);
|
|
435
829
|
if (c.skip) skipped.push({ file: p, reason: c.skip });
|
|
@@ -437,21 +831,38 @@ function main() {
|
|
|
437
831
|
}
|
|
438
832
|
const since = opts.since === void 0 ? void 0 : semver(opts.since);
|
|
439
833
|
const seen = /* @__PURE__ */ new Set();
|
|
834
|
+
const seenLegs = /* @__PURE__ */ new Set();
|
|
440
835
|
const corpus = {};
|
|
441
836
|
const entries = [];
|
|
837
|
+
const legs = [];
|
|
442
838
|
for (const c of corpora) {
|
|
443
839
|
corpus[c.label] = corpus[c.label] ?? 0;
|
|
444
|
-
|
|
445
|
-
|
|
840
|
+
const records = c.records ?? [...yamlFiles(c.dir)].map((path) => ({ path, bytes: readFileSync2(path, "utf8") }));
|
|
841
|
+
for (const { path: file, bytes } of records) {
|
|
842
|
+
const res = loadRecord(file, bytes);
|
|
446
843
|
if (res.skip) {
|
|
447
844
|
skipped.push({ file, reason: res.skip });
|
|
448
845
|
continue;
|
|
449
846
|
}
|
|
450
|
-
if (
|
|
847
|
+
if (res.leg) {
|
|
848
|
+
const id2 = legId(res.leg);
|
|
849
|
+
if (seenLegs.has(id2)) {
|
|
850
|
+
skipped.push({ file, reason: "duplicate leg" });
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
seenLegs.add(id2);
|
|
854
|
+
const v2 = semver(res.leg.gauntlet_version);
|
|
855
|
+
if (since && (!v2 || cmpSemver(v2, since) < 0)) continue;
|
|
856
|
+
corpus[c.label]++;
|
|
857
|
+
legs.push(res.leg);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const runId = JSON.stringify([res.row.repo, res.row.run_id]);
|
|
861
|
+
if (seen.has(runId)) {
|
|
451
862
|
skipped.push({ file, reason: "duplicate run_id" });
|
|
452
863
|
continue;
|
|
453
864
|
}
|
|
454
|
-
seen.add(
|
|
865
|
+
seen.add(runId);
|
|
455
866
|
const v = semver(res.row.version);
|
|
456
867
|
if (since && (!v || cmpSemver(v, since) < 0)) continue;
|
|
457
868
|
corpus[c.label]++;
|
|
@@ -460,20 +871,22 @@ function main() {
|
|
|
460
871
|
}
|
|
461
872
|
entries.sort((a, b) => (a.row.created_at ?? "").localeCompare(b.row.created_at ?? "") || a.row.run_id.localeCompare(b.row.run_id));
|
|
462
873
|
const aggregates = SECTIONS.map((s) => [s, s.aggregate(entries)]);
|
|
874
|
+
const hasRecords = entries.length + legs.length > 0;
|
|
463
875
|
if (opts.json) {
|
|
464
876
|
const out = { corpus, since: opts.since ?? null };
|
|
465
877
|
for (const [s, agg] of aggregates) out[s.key] = agg;
|
|
878
|
+
Object.assign(out, aggregateCosts(entries, legs));
|
|
466
879
|
out.skipped = skipped;
|
|
467
880
|
console.log(JSON.stringify(out, null, 2));
|
|
468
881
|
return;
|
|
469
882
|
}
|
|
470
883
|
console.log(`corpus: ${Object.entries(corpus).map(([k, v]) => `${k}=${v}`).join(", ")}${opts.since === void 0 ? "" : ` since: ${opts.since}`}`);
|
|
471
|
-
if (
|
|
472
|
-
console.log(`no records found in ${corpora.map((c) => c.dir).join(", ") ||
|
|
884
|
+
if (!hasRecords) {
|
|
885
|
+
console.log(`no records found in ${corpora.map((c) => c.dir ?? c.label).join(", ") || process2.cwd()}`);
|
|
473
886
|
} else {
|
|
474
|
-
console.log(aggregates.map(([s, agg]) => s.renderText(agg)).join("\n\n"));
|
|
887
|
+
console.log([...aggregates.map(([s, agg]) => s.renderText(agg)), renderCosts(aggregateCosts(entries, legs, true))].join("\n\n"));
|
|
475
888
|
}
|
|
476
|
-
if (
|
|
477
|
-
if (
|
|
889
|
+
if (hasRecords && skipped.length) console.log("");
|
|
890
|
+
if (hasRecords) for (const s of skipped) console.log(`skipped: ${s.file}: ${s.reason}`);
|
|
478
891
|
}
|
|
479
892
|
main();
|