pi-gauntlet 8.3.0 → 8.4.1
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 +4 -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/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/phase-tracker.test.ts +6 -3
- package/extensions/phase-tracker.ts +2 -0
- package/extensions/plan-tracker.test.ts +4 -2
- package/extensions/plan-tracker.ts +1 -0
- 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 +1 -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/seed.md +1 -6
- package/skills/shape-ticket/SKILL.md +1 -1
- package/skills/shape-ticket/reference/repo-audit.md +2 -2
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { parse as parseYaml } from "yaml";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
|
-
import { mkdtempSync, mkdirSync, writeFileSync,
|
|
4
|
+
import { mkdtempSync, mkdirSync, writeFileSync, realpathSync, rmSync } from "node:fs";
|
|
5
5
|
import { join, dirname } from "node:path";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { isolatedGitEnv } from "../extensions/test-support/git-env.mjs";
|
|
9
10
|
|
|
10
11
|
const CLI = join(dirname(fileURLToPath(import.meta.url)), "gauntlet-performance.mjs");
|
|
11
12
|
const TDIR = ".pi/gauntlet/telemetry/doc/specs";
|
|
12
13
|
const EMPTY_AGENT = mkdtempSync(join(tmpdir(), "gp-empty-agent-"));
|
|
13
14
|
process.on("exit", () => rmSync(EMPTY_AGENT, { recursive: true, force: true }));
|
|
15
|
+
const GIT_ENV = isolatedGitEnv(EMPTY_AGENT);
|
|
14
16
|
|
|
15
17
|
const write = (root, rel, text) => {
|
|
16
18
|
mkdirSync(join(root, dirname(rel)), { recursive: true });
|
|
@@ -18,13 +20,13 @@ const write = (root, rel, text) => {
|
|
|
18
20
|
};
|
|
19
21
|
const gitRepo = (prefix = "gp-") => {
|
|
20
22
|
const root = mkdtempSync(join(tmpdir(), prefix));
|
|
21
|
-
spawnSync("git", ["init", "-q"], { cwd: root });
|
|
23
|
+
spawnSync("git", ["init", "-q"], { cwd: root, env: { ...process.env, ...GIT_ENV } });
|
|
22
24
|
return root;
|
|
23
25
|
};
|
|
24
26
|
const SOURCE_CLI = join(dirname(CLI), "../src/bins/gauntlet-performance.mjs");
|
|
25
27
|
const invoke = (cwd, args = [], env = {}, cli = CLI) => {
|
|
26
|
-
const r = spawnSync(process.execPath, [cli, ...args], {
|
|
27
|
-
cwd, encoding: "utf8", env: { ...process.env, PI_CODING_AGENT_DIR: EMPTY_AGENT, ...env },
|
|
28
|
+
const r = spawnSync(process.execPath, [cli, ...(cli.endsWith("gauntlet-performance.mjs") ? ["--dir", cwd] : []), ...args], {
|
|
29
|
+
cwd, encoding: "utf8", env: { ...process.env, ...GIT_ENV, PI_CODING_AGENT_DIR: EMPTY_AGENT, ...env },
|
|
28
30
|
});
|
|
29
31
|
return { status: r.status, stdout: r.stdout, stderr: r.stderr.trim(), lines: r.stdout.split("\n").filter(Boolean) };
|
|
30
32
|
};
|
|
@@ -159,7 +161,7 @@ test("per-run fields: typed reads, unphased excluded, truncated wall null, findi
|
|
|
159
161
|
assert.deepEqual(d.corpus, { [root.split("/").pop()]: 8 });
|
|
160
162
|
assert.equal(d.since, null);
|
|
161
163
|
const a = byId(d.runs, "aaaaaaaa");
|
|
162
|
-
assert.equal(a.repo,
|
|
164
|
+
assert.equal(a.repo, "unknown repo", "legacy schema-1 records need no repo field");
|
|
163
165
|
assert.equal(a.spec, "a");
|
|
164
166
|
assert.equal(a.version, "5.9.1");
|
|
165
167
|
assert.equal(a.status, "shipped");
|
|
@@ -329,8 +331,8 @@ test("text output: header, shipped* marker, - cells, by version block, skipped l
|
|
|
329
331
|
const aRow = r.lines.find((l) => l.startsWith("aaaaaaaa"));
|
|
330
332
|
assert.match(aRow, /\sshipped\s+waves\s+60m\s+10\/10\/10\/10\/10\s+6\.6k\s+2\.25\s+m\/alpha,m\/beta\s+7\s+n\/a\s+no\s+1\s+0\s+2\s+1\/2\/3\s+4\s+-$/);
|
|
331
333
|
assert.ok(r.lines.includes("by version"));
|
|
332
|
-
const v = r.lines.find((l) =>
|
|
333
|
-
assert.match(v, /^5\.9\.1\s+4\s+3\s+1\s+60m\/120m\s/);
|
|
334
|
+
const v = r.lines.find((l) => /^unknown repo\s+5\.9\.1\s/.test(l));
|
|
335
|
+
assert.match(v, /^unknown repo\s+5\.9\.1\s+4\s+3\s+1\s+60m\/120m\s/);
|
|
334
336
|
assert.ok(r.lines.some((l) => /^skipped: .*broken\.yaml: unparseable$/.test(l)));
|
|
335
337
|
});
|
|
336
338
|
|
|
@@ -349,30 +351,29 @@ test("--since numeric compare: 5.10.0 > 5.9.1, unknown and malformed versions dr
|
|
|
349
351
|
const r = invoke(root, ["--since", "9.9.9"]);
|
|
350
352
|
assert.equal(r.status, 0);
|
|
351
353
|
assert.equal(r.lines[0], `corpus: ${root.split("/").pop()}=0 since: 9.9.9`);
|
|
352
|
-
assert.
|
|
354
|
+
assert.equal(r.lines[1], `no records found in ${realpathSync(root)}`);
|
|
353
355
|
assert.equal(r.lines.length, 2);
|
|
354
356
|
});
|
|
355
357
|
|
|
356
|
-
test("--dir
|
|
358
|
+
test("--dir reads bare directories without fetching and collapses duplicate run IDs", (t) => {
|
|
357
359
|
const root = corpus(gitRepo());
|
|
358
360
|
const other = gitRepo("gp-other-");
|
|
359
361
|
write(other, `${TDIR}/dup.yaml`, COMPLETE);
|
|
360
362
|
write(other, `${TDIR}/h.yaml`, COMPLETE.replace("run_id: aaaaaaaa-0000-0000-0000-000000000001", "run_id: 88888888-0000-0000-0000-000000000008"));
|
|
361
363
|
const empty = gitRepo("gp-empty-");
|
|
362
|
-
write(empty, "
|
|
364
|
+
write(empty, "README.md", "not telemetry\n");
|
|
363
365
|
const bare = mkdtempSync(join(tmpdir(), "gp-bare-"));
|
|
364
366
|
write(bare, "x.yaml", COMPLETE.replace("run_id: aaaaaaaa-0000-0000-0000-000000000001", "run_id: 66666666-0000-0000-0000-000000000006"));
|
|
365
367
|
cleanup(t, root, other, empty, bare);
|
|
366
368
|
const d = json(root, ["--dir", other, "--dir", empty, "--dir", bare, "--dir", join(bare, "missing")]);
|
|
367
369
|
assert.equal(d.corpus[other.split("/").pop()], 1);
|
|
368
370
|
assert.equal(d.corpus[bare.split("/").pop()], 1);
|
|
369
|
-
assert.equal(d.corpus[empty.split("/").pop()],
|
|
371
|
+
assert.equal(d.corpus[empty.split("/").pop()], 0);
|
|
370
372
|
assert.ok(d.runs.some((r) => r.run_id.startsWith("88888888")));
|
|
371
373
|
assert.ok(d.runs.some((r) => r.run_id.startsWith("66666666")));
|
|
372
|
-
assert.ok(!d.runs.some((r) => r.run_id.startsWith("77777777")));
|
|
373
374
|
assert.equal(d.runs.filter((r) => r.run_id.startsWith("aaaaaaaa")).length, 1);
|
|
374
375
|
assert.ok(d.skipped.some((s) => s.file.endsWith("dup.yaml") && s.reason === "duplicate run_id"));
|
|
375
|
-
assert.
|
|
376
|
+
assert.equal(invoke(root, ["--dir", bare]).stderr, "", "explicit directories do not fetch, even without origin");
|
|
376
377
|
assert.ok(d.skipped.some((s) => s.file === join(bare, "missing") && s.reason === "not found"));
|
|
377
378
|
});
|
|
378
379
|
|
|
@@ -387,14 +388,6 @@ test("--dir skips an existing regular file as not a directory", (t) => {
|
|
|
387
388
|
assert.ok(d.skipped.some((s) => s.file === file && s.reason === "not a directory"));
|
|
388
389
|
});
|
|
389
390
|
|
|
390
|
-
test("repo .pi/settings.json telemetry.dir is honoured for the default corpus", (t) => {
|
|
391
|
-
const root = gitRepo(); cleanup(t, root);
|
|
392
|
-
write(root, ".pi/settings.json", JSON.stringify({ piGauntlet: { telemetry: { dir: "custom/dir" } } }));
|
|
393
|
-
write(root, "custom/dir/doc/specs/a.yaml", COMPLETE);
|
|
394
|
-
const d = json(root);
|
|
395
|
-
assert.equal(d.runs.length, 1);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
391
|
test("usage error exits 1", (t) => {
|
|
399
392
|
const root = gitRepo(); cleanup(t, root);
|
|
400
393
|
assert.equal(invoke(root, ["--bogus"]).status, 1);
|
|
@@ -474,8 +467,9 @@ test("council section: rosters, sums, chairs and flags", (t) => {
|
|
|
474
467
|
const at = r.lines.indexOf("council");
|
|
475
468
|
assert.ok(at > 0);
|
|
476
469
|
assert.equal(r.lines[at + 1], "severity is the chair's consolidated severity (highest any co-raiser assigned), not the member's own");
|
|
477
|
-
assert.equal(r.lines[at + 2], "
|
|
478
|
-
assert.
|
|
470
|
+
assert.equal(r.lines[at + 2], "repo: unknown repo");
|
|
471
|
+
assert.equal(r.lines[at + 3], "roster (2 runs): p/alpha:xhigh, p/delta:high");
|
|
472
|
+
assert.match(r.lines[at + 4], /^ member\s+total\s+unique\s+applied\s+uniq_appl\s+deferred\s+rejected\s+uniq_appl_nonminor\/run$/);
|
|
479
473
|
assert.ok(r.lines.includes(" not enough runs to assess (2 of 5)"));
|
|
480
474
|
assert.ok(r.lines.includes("roster (6 runs): p/alpha:xhigh, p/beta:high, p/gamma:high, p/zero:high"));
|
|
481
475
|
assert.match(r.lines.find((l) => /^ p\/alpha:xhigh\s+6\/18\/6\s/.test(l)), /^ p\/alpha:xhigh\s+6\/18\/6\s+0\/6\/6\s+6\/18\/0\s+0\/6\/0\s+0\/0\/0\s+0\/0\/6\s+1\.00$/);
|
|
@@ -568,7 +562,7 @@ test("shipped seal preserves verification runs and plan totals for performance",
|
|
|
568
562
|
const root = gitRepo(); cleanup(t, root);
|
|
569
563
|
const git = (args) => {
|
|
570
564
|
const result = spawnSync("git", ["-c", "user.name=t", "-c", "user.email=t@t",
|
|
571
|
-
"-c", "commit.gpgsign=false", ...args], { cwd: root, encoding: "utf8" });
|
|
565
|
+
"-c", "commit.gpgsign=false", ...args], { cwd: root, encoding: "utf8", env: { ...process.env, ...GIT_ENV } });
|
|
572
566
|
assert.equal(result.status, 0, result.stderr);
|
|
573
567
|
return result.stdout.trim();
|
|
574
568
|
};
|
|
@@ -577,7 +571,9 @@ test("shipped seal preserves verification runs and plan totals for performance",
|
|
|
577
571
|
const base = git(["rev-parse", "HEAD"]);
|
|
578
572
|
write(root, "doc/specs/seal.md", "# Seal\n");
|
|
579
573
|
git(["add", "doc/specs/seal.md"]); git(["commit", "-qm", "Spec"]);
|
|
580
|
-
|
|
574
|
+
git(["remote", "add", "origin", "https://example.test/team/product.git"]);
|
|
575
|
+
git(["config", `url.${root}.insteadOf`, "https://example.test/team/product.git"]);
|
|
576
|
+
const path = ".git/gauntlet/telemetry/doc/specs/seal.yaml";
|
|
581
577
|
write(root, path, JSON.stringify({
|
|
582
578
|
schema: 1, spec: "doc/specs/seal.md", run_id: "seal", status: "in_progress",
|
|
583
579
|
created_at: "2026-09-01T00:00:00Z", sessions: ["s"], accumulators: {},
|
|
@@ -589,9 +585,13 @@ test("shipped seal preserves verification runs and plan totals for performance",
|
|
|
589
585
|
GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t",
|
|
590
586
|
}, join(dirname(CLI), "gauntlet-telemetry-seal.mjs"));
|
|
591
587
|
assert.equal(result.status, 0, result.stderr);
|
|
592
|
-
const sealed = parseYaml(
|
|
588
|
+
const sealed = parseYaml(git(["show", "gauntlet/telemetry:team/product/brainstorming/doc/specs/seal.yaml"]));
|
|
593
589
|
assert.equal(sealed.status, "shipped");
|
|
594
590
|
assert.equal(sealed.derived.verification_runs, 2);
|
|
595
591
|
assert.deepEqual(sealed.derived.plan, { tasks: 2, complete: 1, skipped: 1, failed: 0 });
|
|
596
|
-
|
|
592
|
+
const output = spawnSync(process.execPath, [CLI, "--json"], {
|
|
593
|
+
cwd: root, encoding: "utf8", env: { ...process.env, ...GIT_ENV, PI_CODING_AGENT_DIR: EMPTY_AGENT },
|
|
594
|
+
});
|
|
595
|
+
assert.equal(output.status, 0, output.stderr);
|
|
596
|
+
assert.equal(JSON.parse(output.stdout).runs[0].dispatches_per_task, 1);
|
|
597
597
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/bins/gauntlet-spec-index.mjs
|
|
4
|
-
import { readFileSync, appendFileSync, existsSync, statSync, lstatSync, readdirSync, mkdirSync, rmSync, realpathSync, openSync, readSync, closeSync } from "node:fs";
|
|
5
|
-
import { join, dirname, basename, isAbsolute, matchesGlob } from "node:path";
|
|
4
|
+
import { readFileSync as readFileSync2, appendFileSync, existsSync as existsSync3, statSync as statSync2, lstatSync, readdirSync, mkdirSync, rmSync as rmSync2, realpathSync, openSync, readSync, closeSync } from "node:fs";
|
|
5
|
+
import { join as join3, dirname as dirname2, basename as basename2, isAbsolute, matchesGlob } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
|
-
import
|
|
7
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
8
|
+
import process2 from "node:process";
|
|
9
9
|
import { parse as parseYaml } from "yaml";
|
|
10
10
|
|
|
11
11
|
// extensions/lib/gauntlet-settings.ts
|
|
@@ -13,6 +13,7 @@ function mergeGauntlet(preset, repo) {
|
|
|
13
13
|
return { ...preset ?? {}, ...repo ?? {} };
|
|
14
14
|
}
|
|
15
15
|
var nonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
|
|
16
|
+
var joinWarn = (ws) => ws.length ? ws.join("; ") : void 0;
|
|
16
17
|
var DEFAULT_SPEC_DIRS = ["doc/specs", "docs/specs"];
|
|
17
18
|
var normalizeDir = (d) => d.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
18
19
|
function resolveFlowGuards(g) {
|
|
@@ -31,6 +32,232 @@ function planDirsFor(specDirs) {
|
|
|
31
32
|
}
|
|
32
33
|
return [...out];
|
|
33
34
|
}
|
|
35
|
+
var DEFAULT_TELEMETRY_BUCKETS = [
|
|
36
|
+
["test", ["**/test/**", "**/tests/**", "**/__tests__/**", "**/*.test.*", "**/*.spec.*", "**/*_test.*"]],
|
|
37
|
+
["docs", ["**/*.md"]],
|
|
38
|
+
["config", ["**/*.json", "**/*.yaml", "**/*.yml", "**/*.toml", "**/*.lock", "**/*-lock.*"]]
|
|
39
|
+
];
|
|
40
|
+
function resolveTelemetry(g) {
|
|
41
|
+
const t = g.telemetry;
|
|
42
|
+
const warnings = [];
|
|
43
|
+
const enabled = t?.enabled !== false;
|
|
44
|
+
if (t && Object.hasOwn(t, "dir")) {
|
|
45
|
+
warnings.push("piGauntlet.telemetry.dir is retired; records live on the gauntlet/telemetry branch - set piGauntlet.telemetry.remote to redirect them");
|
|
46
|
+
}
|
|
47
|
+
let buckets = DEFAULT_TELEMETRY_BUCKETS;
|
|
48
|
+
if (t?.buckets !== void 0) {
|
|
49
|
+
const b = t.buckets;
|
|
50
|
+
const valid = b !== null && typeof b === "object" && !Array.isArray(b) && Object.keys(b).length > 0 && Object.values(b).every((v) => Array.isArray(v) && v.length > 0 && v.every(nonEmptyString));
|
|
51
|
+
if (valid) buckets = Object.entries(b).map(([name, globs]) => [name, [...globs]]);
|
|
52
|
+
else warnings.push("telemetry.buckets is not an object of non-empty glob arrays; using the defaults");
|
|
53
|
+
}
|
|
54
|
+
const remote = nonEmptyString(t?.remote) ? t.remote.trim() : void 0;
|
|
55
|
+
if (t?.remote !== void 0 && !remote) warnings.push("telemetry.remote must be a non-empty URL; using origin");
|
|
56
|
+
return { enabled, ...remote ? { remote } : {}, buckets, warning: joinWarn(warnings) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// extensions/lib/telemetry-branch.ts
|
|
60
|
+
import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
61
|
+
import { tmpdir } from "node:os";
|
|
62
|
+
import { basename, join as join2 } from "node:path";
|
|
63
|
+
|
|
64
|
+
// extensions/lib/checkout.ts
|
|
65
|
+
import { execFileSync } from "node:child_process";
|
|
66
|
+
import { existsSync, statSync } from "node:fs";
|
|
67
|
+
import { dirname, join } from "node:path";
|
|
68
|
+
var CHECKOUT_ARGS = ["rev-parse", "--path-format=absolute", "--show-toplevel", "--git-dir", "--git-common-dir"];
|
|
69
|
+
function parseCheckout(r) {
|
|
70
|
+
if (r.code !== 0) return void 0;
|
|
71
|
+
const lines = r.stdout.trim().split("\n").map((l) => l.trim());
|
|
72
|
+
if (lines.length < 3 || !lines[0]) return void 0;
|
|
73
|
+
return { toplevel: lines[0], isPrimary: lines[1] === lines[2] };
|
|
74
|
+
}
|
|
75
|
+
var isDir = (p) => {
|
|
76
|
+
try {
|
|
77
|
+
return statSync(p).isDirectory();
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
function nearestExistingDir(absPath) {
|
|
83
|
+
let p = absPath;
|
|
84
|
+
while (!(existsSync(p) && isDir(p))) {
|
|
85
|
+
const parent = dirname(p);
|
|
86
|
+
if (parent === p) return p;
|
|
87
|
+
p = parent;
|
|
88
|
+
}
|
|
89
|
+
return p;
|
|
90
|
+
}
|
|
91
|
+
var JJ_ROOT_ARGS = ["root"];
|
|
92
|
+
function parseJjRoot(r) {
|
|
93
|
+
if (r.code !== 0) return void 0;
|
|
94
|
+
const toplevel = r.stdout.trim();
|
|
95
|
+
if (!toplevel) return void 0;
|
|
96
|
+
return { toplevel, isPrimary: isDir(join(toplevel, ".jj", "repo")) };
|
|
97
|
+
}
|
|
98
|
+
function checkoutOfSync(absPath, git2 = gitSync, jj = jjSync) {
|
|
99
|
+
const cwd = nearestExistingDir(absPath);
|
|
100
|
+
const g = parseCheckout(git2(CHECKOUT_ARGS, cwd));
|
|
101
|
+
if (g) return { ...g, via: "git" };
|
|
102
|
+
const j = parseJjRoot(jj(JJ_ROOT_ARGS, cwd));
|
|
103
|
+
return j ? { ...j, via: "jj" } : void 0;
|
|
104
|
+
}
|
|
105
|
+
var commandSync = (bin) => (args, cwd) => {
|
|
106
|
+
try {
|
|
107
|
+
const stdout = execFileSync(bin, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
108
|
+
return { code: 0, stdout };
|
|
109
|
+
} catch (e) {
|
|
110
|
+
const status = e.status;
|
|
111
|
+
return { code: typeof status === "number" ? status : 1, stdout: "" };
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var gitSync = commandSync("git");
|
|
115
|
+
var jjSync = commandSync("jj");
|
|
116
|
+
|
|
117
|
+
// extensions/lib/vcs.ts
|
|
118
|
+
import { spawnSync } from "node:child_process";
|
|
119
|
+
|
|
120
|
+
// extensions/lib/phase-tracker-helpers.ts
|
|
121
|
+
var STMT_START = "(?:^|[\\n;&|(])\\s*";
|
|
122
|
+
|
|
123
|
+
// extensions/lib/telemetry-paths.ts
|
|
124
|
+
var GIT_FLAGS = "git\\s+(?:-\\S+(?:\\s+\\S+)?\\s+)*";
|
|
125
|
+
var SHIP_RE = new RegExp(STMT_START + "(" + GIT_FLAGS + "(?:merge\\s+--squash|push)(?=\\s|$)|gh\\s+pr\\s+create)");
|
|
126
|
+
var SQUASH_RE = new RegExp("^" + GIT_FLAGS + "merge\\s+--squash");
|
|
127
|
+
|
|
128
|
+
// extensions/lib/telemetry-diff.ts
|
|
129
|
+
var JJ_MAINLINE = "coalesce(trunk() ~ root(), present(main), present(master))";
|
|
130
|
+
var JJ_FORK_POINT = `fork_point(${JJ_MAINLINE} | @) ~ root() & ::${JJ_MAINLINE}`;
|
|
131
|
+
|
|
132
|
+
// extensions/lib/vcs.ts
|
|
133
|
+
var NO_PROMPT_ENV = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_EDITOR: "true" };
|
|
134
|
+
var runBinary = (bin) => (args, cwd, timeoutMs = 1e4, options = {}) => {
|
|
135
|
+
const r = spawnSync(bin, args, { cwd, encoding: "utf8", input: options.input, env: { ...NO_PROMPT_ENV, ...options.env }, timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024 });
|
|
136
|
+
const err = r.error;
|
|
137
|
+
const timedOut = err?.code === "ETIMEDOUT";
|
|
138
|
+
const stderr = timedOut ? "timed out" : (r.stderr ?? "").trim().split("\n")[0] || err?.message || `${bin} exited ${r.status}`;
|
|
139
|
+
return { ok: !timedOut && r.status === 0, code: timedOut ? 1 : r.status ?? 1, stdout: options.raw ? r.stdout ?? "" : (r.stdout ?? "").trim(), stderr };
|
|
140
|
+
};
|
|
141
|
+
var gitRun = runBinary("git");
|
|
142
|
+
|
|
143
|
+
// extensions/lib/telemetry-branch.ts
|
|
144
|
+
var TELEMETRY_REF = "refs/heads/gauntlet/telemetry";
|
|
145
|
+
function telemetryRepo(root) {
|
|
146
|
+
const result = gitRun(["config", "--get", "remote.origin.url"], root);
|
|
147
|
+
const match = result.stdout.match(/^(?:https?:\/\/[^/]+\/|ssh:\/\/(?:[^@/]+@)?[^/]+\/|(?:[^@\s]+@)?[^:\s/]+:)(.+)$/);
|
|
148
|
+
const path = match?.[1].replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
149
|
+
if (result.ok && path) return path;
|
|
150
|
+
const top = checkoutOfSync(root)?.toplevel ?? root;
|
|
151
|
+
for (const file of [".pi/gauntlet-overrides.md", "gauntlet-overrides.md", "doc/gauntlet-overrides.md"]) {
|
|
152
|
+
const path2 = join2(top, file);
|
|
153
|
+
if (!existsSync2(path2)) continue;
|
|
154
|
+
const section = readFileSync(path2, "utf8").split(/^##\s+Issue tracker\s*$/m)[1]?.split(/^##\s/m)[0] ?? "";
|
|
155
|
+
const repo = /^-[\t ]*repo:[\t ]*(\S[^\r\n]*)$/m.exec(section)?.[1].trim();
|
|
156
|
+
if (repo) return repo;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
const worktrees = gitRun(["worktree", "list", "--porcelain", "-z"], root);
|
|
160
|
+
const primary = worktrees.ok ? /^worktree ([^\0]+)/.exec(worktrees.stdout)?.[1] : void 0;
|
|
161
|
+
return `local/${basename(primary ?? top)}`;
|
|
162
|
+
}
|
|
163
|
+
var trackingRef = (remote) => `refs/remotes/${remote === "origin" ? "origin" : "telemetry"}/gauntlet/telemetry`;
|
|
164
|
+
var fetchBranch = (root, remote) => gitRun(["fetch", "--no-tags", remote, `+${TELEMETRY_REF}:${trackingRef(remote)}`], root);
|
|
165
|
+
function syncTelemetryBranch(root, records, remote = "origin") {
|
|
166
|
+
const scratch = mkdtempSync(join2(tmpdir(), "gauntlet-telemetry-index-"));
|
|
167
|
+
const env = { GIT_INDEX_FILE: join2(scratch, "index") };
|
|
168
|
+
const git2 = (args, input) => {
|
|
169
|
+
const result = gitRun(args, root, void 0, { env, input });
|
|
170
|
+
if (!result.ok) throw new Error(result.stderr);
|
|
171
|
+
return result.stdout;
|
|
172
|
+
};
|
|
173
|
+
const tracking = trackingRef(remote);
|
|
174
|
+
let fetchWarning;
|
|
175
|
+
const fetch = () => {
|
|
176
|
+
const result = fetchBranch(root, remote);
|
|
177
|
+
fetchWarning = result.ok ? void 0 : `fetch failed: ${result.stderr}`;
|
|
178
|
+
return result;
|
|
179
|
+
};
|
|
180
|
+
const resolve = (ref) => {
|
|
181
|
+
const result = gitRun(["rev-parse", "--verify", ref], root);
|
|
182
|
+
return result.ok ? result.stdout : void 0;
|
|
183
|
+
};
|
|
184
|
+
const commit = (tree, parent, message) => git2(["-c", "commit.gpgsign=false", "commit-tree", tree, ...parent ? ["-p", parent] : [], "-m", message]);
|
|
185
|
+
let committed = false;
|
|
186
|
+
try {
|
|
187
|
+
let tip = resolve(TELEMETRY_REF);
|
|
188
|
+
committed = records.size === 0 && !!tip;
|
|
189
|
+
fetch();
|
|
190
|
+
let upstream = resolve(tracking);
|
|
191
|
+
if (records.size) {
|
|
192
|
+
const parent = tip ?? upstream;
|
|
193
|
+
git2(parent ? ["read-tree", parent] : ["read-tree", "--empty"]);
|
|
194
|
+
for (const [path, content] of records) {
|
|
195
|
+
const blob = git2(["hash-object", "-w", "--stdin"], content);
|
|
196
|
+
git2(["update-index", "--add", "--cacheinfo", "100644", blob, path]);
|
|
197
|
+
}
|
|
198
|
+
const tree = git2(["write-tree"]);
|
|
199
|
+
const next = parent && git2(["rev-parse", `${parent}^{tree}`]) === tree ? parent : commit(tree, parent, "telemetry: update records");
|
|
200
|
+
git2(["update-ref", TELEMETRY_REF, next, tip ?? ""]);
|
|
201
|
+
tip = next;
|
|
202
|
+
committed = true;
|
|
203
|
+
}
|
|
204
|
+
const pending = () => tip ? git2(["rev-list", "--reverse", "--topo-order", tip, ...upstream ? [`^${upstream}`] : []]).split("\n").filter(Boolean) : [];
|
|
205
|
+
if (!pending().length) return { ok: true, committed, pushed: false, warning: fetchWarning };
|
|
206
|
+
const push = () => gitRun(["push", remote, `${TELEMETRY_REF}:${TELEMETRY_REF}`], root);
|
|
207
|
+
const first = push();
|
|
208
|
+
if (first.ok) return { ok: true, committed, pushed: true, warning: fetchWarning };
|
|
209
|
+
const fetched = fetch();
|
|
210
|
+
if (!fetched.ok) throw new Error(`${first.stderr}; refresh failed: ${fetched.stderr}`);
|
|
211
|
+
upstream = resolve(tracking);
|
|
212
|
+
let replayed = upstream;
|
|
213
|
+
for (const sha of pending()) {
|
|
214
|
+
git2(replayed ? ["read-tree", replayed] : ["read-tree", "--empty"]);
|
|
215
|
+
const changes = git2(["diff-tree", "--root", "--no-commit-id", "--raw", "--no-abbrev", "--no-renames", "-r", "-z", sha]).split("\0");
|
|
216
|
+
for (let i = 0; i + 1 < changes.length; i += 2) {
|
|
217
|
+
const [, mode, , blob] = changes[i].split(" ");
|
|
218
|
+
const path = changes[i + 1];
|
|
219
|
+
git2(mode === "000000" ? ["update-index", "--force-remove", "--", path] : ["update-index", "--add", "--cacheinfo", mode, blob, path]);
|
|
220
|
+
}
|
|
221
|
+
replayed = commit(git2(["write-tree"]), replayed, git2(["log", "-1", "--format=%B", sha]));
|
|
222
|
+
}
|
|
223
|
+
if (replayed) git2(["update-ref", TELEMETRY_REF, replayed, tip]);
|
|
224
|
+
const second = push();
|
|
225
|
+
if (!second.ok) throw new Error(second.stderr);
|
|
226
|
+
return { ok: true, committed, pushed: true };
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const reason = error.message;
|
|
229
|
+
return { ok: false, committed, pushed: false, reason, warning: [fetchWarning, `telemetry: not pushed (${reason})`].filter(Boolean).join("; ") };
|
|
230
|
+
} finally {
|
|
231
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function readTelemetryBranch(root, { remote = "origin", prefix = "" } = {}) {
|
|
235
|
+
const records = [];
|
|
236
|
+
let warning;
|
|
237
|
+
try {
|
|
238
|
+
const synced = syncTelemetryBranch(root, /* @__PURE__ */ new Map(), remote);
|
|
239
|
+
warning = synced.warning;
|
|
240
|
+
if (synced.pushed) {
|
|
241
|
+
const fetched = fetchBranch(root, remote);
|
|
242
|
+
warning = fetched.ok ? void 0 : `fetch failed: ${fetched.stderr}`;
|
|
243
|
+
}
|
|
244
|
+
const tip = gitRun(["rev-parse", "--verify", trackingRef(remote)], root);
|
|
245
|
+
if (!tip.ok) return { records, warning: [warning, "no telemetry ref fetched yet"].filter(Boolean).join("; ") };
|
|
246
|
+
const tree = gitRun(["ls-tree", "-r", "-z", tip.stdout, ...prefix ? ["--", prefix] : []], root);
|
|
247
|
+
if (!tree.ok) throw new Error(tree.stderr);
|
|
248
|
+
for (const entry of tree.stdout.split("\0").filter(Boolean)) {
|
|
249
|
+
const [meta, path] = entry.split(" ");
|
|
250
|
+
const [, type, sha] = meta.split(" ");
|
|
251
|
+
if (type !== "blob" || !path.startsWith(prefix) || !path.endsWith(".yaml")) continue;
|
|
252
|
+
const blob = gitRun(["cat-file", "blob", sha], root, void 0, { raw: true });
|
|
253
|
+
if (!blob.ok) throw new Error(blob.stderr);
|
|
254
|
+
records.push({ path, bytes: blob.stdout });
|
|
255
|
+
}
|
|
256
|
+
return { records, warning };
|
|
257
|
+
} catch (error) {
|
|
258
|
+
return { records, warning: [warning, `read failed: ${error.message}`].filter(Boolean).join("; ") };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
34
261
|
|
|
35
262
|
// src/bins/gauntlet-spec-index.mjs
|
|
36
263
|
var SCHEMA_VERSION = 3;
|
|
@@ -58,13 +285,13 @@ var SCHEMA = `
|
|
|
58
285
|
tokenize = 'porter unicode61');
|
|
59
286
|
`;
|
|
60
287
|
var usage = () => {
|
|
61
|
-
|
|
62
|
-
|
|
288
|
+
process2.stderr.write('usage: gauntlet-spec-index --query "<text>" [--corpus specs|docs] [--limit N] [--exclude <repo-relative path>]...\n');
|
|
289
|
+
process2.exit(1);
|
|
63
290
|
};
|
|
64
291
|
var die = (msg) => {
|
|
65
|
-
|
|
292
|
+
process2.stderr.write(`${msg}
|
|
66
293
|
`);
|
|
67
|
-
|
|
294
|
+
process2.exit(2);
|
|
68
295
|
};
|
|
69
296
|
function parseArgs(argv) {
|
|
70
297
|
let query2;
|
|
@@ -82,43 +309,45 @@ function parseArgs(argv) {
|
|
|
82
309
|
return { query: query2, corpus, limit, exclude };
|
|
83
310
|
}
|
|
84
311
|
function nodeOk() {
|
|
85
|
-
const cur =
|
|
312
|
+
const cur = process2.versions.node.split(".").map(Number);
|
|
86
313
|
for (let i = 0; i < 3; i++) if (cur[i] !== MIN_NODE[i]) return cur[i] > MIN_NODE[i];
|
|
87
314
|
return true;
|
|
88
315
|
}
|
|
89
|
-
var git = (cwd, args) =>
|
|
316
|
+
var git = (cwd, args) => execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
90
317
|
function repoRoot() {
|
|
91
318
|
try {
|
|
92
|
-
return git(
|
|
319
|
+
return git(process2.cwd(), ["rev-parse", "--show-toplevel"]);
|
|
93
320
|
} catch {
|
|
94
321
|
return die("gauntlet-spec-index: not inside a git repository");
|
|
95
322
|
}
|
|
96
323
|
}
|
|
97
324
|
function readLayer(file) {
|
|
98
|
-
if (!
|
|
325
|
+
if (!existsSync3(file)) return {};
|
|
99
326
|
try {
|
|
100
|
-
return JSON.parse(
|
|
327
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
101
328
|
} catch (e) {
|
|
102
|
-
|
|
329
|
+
process2.stderr.write(`warning: ${file}: ${e.message}; using {} for this layer
|
|
103
330
|
`);
|
|
104
331
|
return {};
|
|
105
332
|
}
|
|
106
333
|
}
|
|
334
|
+
function settingsFor(root) {
|
|
335
|
+
const agentDir = process2.env.PI_CODING_AGENT_DIR || join3(homedir(), ".pi", "agent");
|
|
336
|
+
return mergeGauntlet(readLayer(join3(agentDir, "settings.json"))?.piGauntlet, readLayer(join3(root, ".pi", "settings.json"))?.piGauntlet);
|
|
337
|
+
}
|
|
107
338
|
function specDirsFor(root) {
|
|
108
|
-
const
|
|
109
|
-
const merged = mergeGauntlet(readLayer(join(agentDir, "settings.json"))?.piGauntlet, readLayer(join(root, ".pi", "settings.json"))?.piGauntlet);
|
|
110
|
-
const specDirs = resolveFlowGuards(merged).specDirs;
|
|
339
|
+
const specDirs = resolveFlowGuards(settingsFor(root)).specDirs;
|
|
111
340
|
return { specDirs, planDirs: planDirsFor(specDirs) };
|
|
112
341
|
}
|
|
113
342
|
function discover(root) {
|
|
114
343
|
const out = [];
|
|
115
344
|
const seen = /* @__PURE__ */ new Set();
|
|
116
345
|
const collect = (relDir, service) => {
|
|
117
|
-
const abs =
|
|
346
|
+
const abs = join3(root, relDir);
|
|
118
347
|
let real;
|
|
119
348
|
try {
|
|
120
349
|
real = realpathSync(abs);
|
|
121
|
-
if (!
|
|
350
|
+
if (!statSync2(real).isDirectory()) return;
|
|
122
351
|
} catch {
|
|
123
352
|
return;
|
|
124
353
|
}
|
|
@@ -127,7 +356,7 @@ function discover(root) {
|
|
|
127
356
|
for (const name of readdirSync(abs).sort()) {
|
|
128
357
|
if (!name.endsWith(".md")) continue;
|
|
129
358
|
const rel = `${relDir}/${name}`;
|
|
130
|
-
const st =
|
|
359
|
+
const st = statSync2(join3(root, rel));
|
|
131
360
|
if (st.isFile()) out.push({ path: rel, service, mtime_ms: Math.trunc(st.mtimeMs), size: st.size });
|
|
132
361
|
}
|
|
133
362
|
};
|
|
@@ -143,13 +372,13 @@ function services(root) {
|
|
|
143
372
|
return readdirSync(root, { withFileTypes: true }).filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name)).map((e) => e.name);
|
|
144
373
|
}
|
|
145
374
|
function includes(root) {
|
|
146
|
-
const file = OVERRIDES_FILES.map((rel) =>
|
|
375
|
+
const file = OVERRIDES_FILES.map((rel) => join3(root, rel)).find((abs) => existsSync3(abs));
|
|
147
376
|
if (!file) return DOC_INCLUDES;
|
|
148
377
|
const globs = [];
|
|
149
378
|
let inSection = false;
|
|
150
379
|
let text;
|
|
151
380
|
try {
|
|
152
|
-
text =
|
|
381
|
+
text = readFileSync2(file, "utf8");
|
|
153
382
|
} catch (e) {
|
|
154
383
|
die(`gauntlet-spec-index: unreadable overrides file ${file}: ${e.message}`);
|
|
155
384
|
}
|
|
@@ -187,15 +416,15 @@ function discoverDocs(root, globs) {
|
|
|
187
416
|
const skip = [...specDirs, ...planDirs];
|
|
188
417
|
const svc = services(root);
|
|
189
418
|
const patterns = globs.flatMap((g) => g.includes("/") ? [g] : [g, ...svc.map((s) => `${s}/${g}`)]);
|
|
190
|
-
const listed =
|
|
419
|
+
const listed = execFileSync2("git", ["-C", root, "ls-files", "-co", "--exclude-standard", "-z", "--", "*.md"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\0").filter(Boolean);
|
|
191
420
|
const out = [];
|
|
192
421
|
for (const rel of listed) {
|
|
193
422
|
if (!patterns.some((p) => matchesGlob(rel, p))) continue;
|
|
194
423
|
if (DOC_EXCLUDE_GLOBS.some((p) => matchesGlob(rel, p)) || underDirs(rel, skip)) continue;
|
|
195
424
|
let st;
|
|
196
425
|
try {
|
|
197
|
-
st = lstatSync(
|
|
198
|
-
if (!st.isFile() || firstLine(
|
|
426
|
+
st = lstatSync(join3(root, rel));
|
|
427
|
+
if (!st.isFile() || firstLine(join3(root, rel)) === DRAFT_MARKER) continue;
|
|
199
428
|
} catch {
|
|
200
429
|
continue;
|
|
201
430
|
}
|
|
@@ -205,18 +434,18 @@ function discoverDocs(root, globs) {
|
|
|
205
434
|
}
|
|
206
435
|
function ensureExclude(root) {
|
|
207
436
|
const rel = git(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
208
|
-
const abs = isAbsolute(rel) ? rel :
|
|
209
|
-
const cur =
|
|
437
|
+
const abs = isAbsolute(rel) ? rel : join3(root, rel);
|
|
438
|
+
const cur = existsSync3(abs) ? readFileSync2(abs, "utf8") : "";
|
|
210
439
|
if (cur.split("\n").includes(EXCLUDE_LINE)) return;
|
|
211
|
-
mkdirSync(
|
|
440
|
+
mkdirSync(dirname2(abs), { recursive: true });
|
|
212
441
|
appendFileSync(abs, `${cur.length && !cur.endsWith("\n") ? "\n" : ""}${EXCLUDE_LINE}
|
|
213
442
|
`);
|
|
214
443
|
}
|
|
215
444
|
async function openDb(root) {
|
|
216
445
|
const { DatabaseSync } = await import("node:sqlite");
|
|
217
|
-
const dbPath =
|
|
218
|
-
mkdirSync(
|
|
219
|
-
const fresh = !
|
|
446
|
+
const dbPath = join3(root, ".pi/gauntlet/index.sqlite");
|
|
447
|
+
mkdirSync(dirname2(dbPath), { recursive: true });
|
|
448
|
+
const fresh = !existsSync3(dbPath);
|
|
220
449
|
const create = () => {
|
|
221
450
|
const db2 = new DatabaseSync(dbPath);
|
|
222
451
|
db2.exec("PRAGMA busy_timeout = 2000");
|
|
@@ -234,7 +463,7 @@ async function openDb(root) {
|
|
|
234
463
|
db2?.close();
|
|
235
464
|
} catch {
|
|
236
465
|
}
|
|
237
|
-
for (const suffix of ["", "-journal", "-wal", "-shm"])
|
|
466
|
+
for (const suffix of ["", "-journal", "-wal", "-shm"]) rmSync2(dbPath + suffix, { force: true });
|
|
238
467
|
return create();
|
|
239
468
|
};
|
|
240
469
|
if (fresh) ensureExclude(root);
|
|
@@ -275,7 +504,7 @@ function extract(text, path) {
|
|
|
275
504
|
const lines = text.split(/\r?\n/);
|
|
276
505
|
const fenced = fenceMask(lines);
|
|
277
506
|
const h1 = lines.findIndex((l, i) => !fenced[i] && l.startsWith("# "));
|
|
278
|
-
const title = (h1 >= 0 ? lines[h1].slice(2).trim() : "") ||
|
|
507
|
+
const title = (h1 >= 0 ? lines[h1].slice(2).trim() : "") || basename2(path, ".md");
|
|
279
508
|
const goal = lines.find((l) => l.startsWith("**Goal:**"))?.slice("**Goal:**".length).trim() ?? "";
|
|
280
509
|
const headings = lines.filter((l, i) => !fenced[i] && /^##{1,2} /.test(l)).map((l) => l.replace(/^#+ /, "")).join("\n");
|
|
281
510
|
let state = "live";
|
|
@@ -308,7 +537,7 @@ function refresh(db, root, corpus) {
|
|
|
308
537
|
for (const f of found) {
|
|
309
538
|
const k = known.get(f.path);
|
|
310
539
|
if (k && k.mtime_ms === f.mtime_ms && k.size === f.size) continue;
|
|
311
|
-
const text =
|
|
540
|
+
const text = readFileSync2(join3(root, f.path), "utf8");
|
|
312
541
|
delRow.run(f.path);
|
|
313
542
|
if (text.split(/\r?\n/, 1)[0] === DRAFT_MARKER) {
|
|
314
543
|
delFile.run(table, f.path);
|
|
@@ -370,21 +599,34 @@ function query(db, corpus, toks, { limit, exclude }) {
|
|
|
370
599
|
const cut = best ? kept.filter((r) => Math.abs(r.score) >= SCORE_RATIO * Math.abs(best.score)) : kept;
|
|
371
600
|
return { rows: cut.slice(0, limit), n };
|
|
372
601
|
}
|
|
373
|
-
function
|
|
602
|
+
function telemetryCorpus(root) {
|
|
603
|
+
const settings = resolveTelemetry(settingsFor(root));
|
|
604
|
+
if (settings.warning) process2.stderr.write(`warning: ${settings.warning}
|
|
605
|
+
`);
|
|
606
|
+
if (!settings.enabled) return /* @__PURE__ */ new Map();
|
|
607
|
+
const repo = telemetryRepo(root);
|
|
608
|
+
const prefix = `${repo}/brainstorming/`;
|
|
609
|
+
const result = readTelemetryBranch(root, { remote: settings.remote, prefix });
|
|
610
|
+
if (result.warning) process2.stderr.write(`warning: ${result.warning}
|
|
611
|
+
`);
|
|
612
|
+
return new Map(result.records.map(({ path, bytes }) => [path.slice(prefix.length), bytes]));
|
|
613
|
+
}
|
|
614
|
+
function telemetry(root, specPath, records) {
|
|
374
615
|
const blank = { status: null, shipped_at: null, files: "" };
|
|
375
|
-
const p =
|
|
376
|
-
|
|
616
|
+
const p = specPath.replace(/\.md$/, ".yaml");
|
|
617
|
+
const bytes = records.get(p);
|
|
618
|
+
if (bytes === void 0) return blank;
|
|
377
619
|
let rec;
|
|
378
620
|
try {
|
|
379
|
-
rec = parseYaml(
|
|
621
|
+
rec = parseYaml(bytes);
|
|
380
622
|
} catch {
|
|
381
|
-
|
|
623
|
+
process2.stderr.write(`gauntlet-spec-index: warning: unreadable telemetry ${p}
|
|
382
624
|
`);
|
|
383
625
|
return blank;
|
|
384
626
|
}
|
|
385
627
|
if (!rec || typeof rec !== "object" || Array.isArray(rec)) return blank;
|
|
386
628
|
const mf = rec.derived?.modified_files;
|
|
387
|
-
const files = Array.isArray(mf) ? mf.filter((f) => typeof f === "string" &&
|
|
629
|
+
const files = Array.isArray(mf) ? mf.filter((f) => typeof f === "string" && existsSync3(join3(root, f))).join(";") : "missing";
|
|
388
630
|
return { status: rec.status ?? null, shipped_at: rec.shipped_at ?? null, files };
|
|
389
631
|
}
|
|
390
632
|
var cell = (v) => v === null || v === void 0 ? "" : String(v).replace(/\s+/g, " ").trim();
|
|
@@ -408,8 +650,8 @@ var CORPORA = {
|
|
|
408
650
|
const x = extract(text, f.path);
|
|
409
651
|
return [f.path, f.service, x.title, x.goal, x.headings, text, x.state];
|
|
410
652
|
},
|
|
411
|
-
format: (root, r) => {
|
|
412
|
-
const t = telemetry(root, r.path);
|
|
653
|
+
format: (root, r, records) => {
|
|
654
|
+
const t = telemetry(root, r.path, records);
|
|
413
655
|
return [...[r.score.toFixed(3), r.path, r.service, r.title, t.status, t.shipped_at, r.state].map(cell), filesCell(t.files), cell(r.snippet)];
|
|
414
656
|
}
|
|
415
657
|
},
|
|
@@ -430,8 +672,8 @@ var CORPORA = {
|
|
|
430
672
|
}
|
|
431
673
|
};
|
|
432
674
|
async function main() {
|
|
433
|
-
if (!nodeOk()) die(`gauntlet-spec-index needs Node >=24.15.0 (found ${
|
|
434
|
-
const { query: text, corpus: name, limit, exclude } = parseArgs(
|
|
675
|
+
if (!nodeOk()) die(`gauntlet-spec-index needs Node >=24.15.0 (found ${process2.versions.node})`);
|
|
676
|
+
const { query: text, corpus: name, limit, exclude } = parseArgs(process2.argv.slice(2));
|
|
435
677
|
const toks = tokens(text);
|
|
436
678
|
if (!toks.length) usage();
|
|
437
679
|
const root = repoRoot();
|
|
@@ -439,11 +681,12 @@ async function main() {
|
|
|
439
681
|
const db = await openDb(root);
|
|
440
682
|
refresh(db, root, corpus);
|
|
441
683
|
const { rows, n } = query(db, corpus, toks, { limit, exclude });
|
|
684
|
+
const records = name === "specs" ? telemetryCorpus(root) : /* @__PURE__ */ new Map();
|
|
442
685
|
const out = [corpus.header.join(" ")];
|
|
443
|
-
for (const r of rows) out.push(corpus.format(root, r).join(" "));
|
|
444
|
-
|
|
686
|
+
for (const r of rows) out.push(corpus.format(root, r, records).join(" "));
|
|
687
|
+
process2.stdout.write(out.join("\n") + "\n");
|
|
445
688
|
if (rows.length === 0 && n < MIN_CORPUS) {
|
|
446
|
-
|
|
689
|
+
process2.stderr.write(`gauntlet-spec-index: ${name} corpus has ${n} documents - too small for the confidence rule; no rows returned
|
|
447
690
|
`);
|
|
448
691
|
}
|
|
449
692
|
db.close();
|