@twarc_net/groundtruth 0.1.0 → 0.2.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/README.md +123 -2
- package/dist/cli.js +890 -177
- package/dist/index.d.ts +109 -3
- package/dist/index.js +625 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -75,21 +75,34 @@ function extractClaims(summary) {
|
|
|
75
75
|
seen.add(key);
|
|
76
76
|
claims.push({ kind, target, polarity, source: source.trim() });
|
|
77
77
|
};
|
|
78
|
-
for (const clause of splitClauses(summary)) {
|
|
78
|
+
for (const clause of splitClauses(stripCodeFences(summary))) {
|
|
79
79
|
if (isIntent(clause)) continue;
|
|
80
80
|
const polarity = detectPolarity(clause);
|
|
81
81
|
const hasVerb = ADD_VERBS.test(clause) || REMOVE_VERBS.test(clause) || MODIFY_VERBS.test(clause);
|
|
82
82
|
let concrete = false;
|
|
83
|
+
const renamed = matchRename(clause);
|
|
84
|
+
if (renamed) {
|
|
85
|
+
add("symbol", renamed.from, "remove", clause);
|
|
86
|
+
add("symbol", renamed.to, "add", clause);
|
|
87
|
+
concrete = true;
|
|
88
|
+
}
|
|
83
89
|
for (const tok of backtickTokens(clause)) {
|
|
84
|
-
|
|
85
|
-
if (kind === "file") {
|
|
90
|
+
if (looksLikePath(tok)) {
|
|
86
91
|
add("file", tok, polarity, clause);
|
|
87
92
|
concrete = true;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (tok.includes(" ")) {
|
|
96
|
+
const first = (tok.split(/\s+/)[0] ?? "").toLowerCase();
|
|
97
|
+
if (COMMAND_WORDS.has(first)) {
|
|
98
|
+
add("command", tok, polarity, clause);
|
|
99
|
+
concrete = true;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const sym = symbolName(tok);
|
|
104
|
+
if (sym) {
|
|
105
|
+
add("symbol", sym, polarity, clause);
|
|
93
106
|
concrete = true;
|
|
94
107
|
}
|
|
95
108
|
}
|
|
@@ -248,22 +261,31 @@ var SPECIAL_FILES = /* @__PURE__ */ new Set([
|
|
|
248
261
|
".dockerignore",
|
|
249
262
|
".editorconfig"
|
|
250
263
|
]);
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
const first = tok.split(/\s+/)[0] ?? "";
|
|
255
|
-
return COMMAND_WORDS.has(first.toLowerCase()) ? "command" : null;
|
|
256
|
-
}
|
|
264
|
+
function symbolName(tok) {
|
|
265
|
+
const jsx = /^<\/?\s*([A-Za-z][\w.]*)\b[^>]*>$/.exec(tok);
|
|
266
|
+
if (jsx?.[1]) return jsx[1];
|
|
257
267
|
const id = stripCall(tok);
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(id) ? id : null;
|
|
269
|
+
}
|
|
270
|
+
function matchRename(clause) {
|
|
271
|
+
if (!/\brenam(?:e|ed|es|ing)\b/i.test(clause)) return null;
|
|
272
|
+
const m = /`([^`]+)`\s*(?:to|into|->|→|=>)\s*`([^`]+)`/i.exec(clause);
|
|
273
|
+
if (!m?.[1] || !m[2]) return null;
|
|
274
|
+
const from = symbolName(m[1]);
|
|
275
|
+
const to = symbolName(m[2]);
|
|
276
|
+
return from && to ? { from, to } : null;
|
|
277
|
+
}
|
|
278
|
+
function stripCodeFences(text) {
|
|
279
|
+
return text.replace(/```[\s\S]*?```/g, " ").replace(/~~~[\s\S]*?~~~/g, " ");
|
|
260
280
|
}
|
|
261
281
|
function looksLikePath(tok) {
|
|
262
282
|
if (tok.includes(" ")) return false;
|
|
283
|
+
if (/[<>]/.test(tok)) return false;
|
|
263
284
|
if (SPECIAL_FILES.has(tok)) return true;
|
|
264
285
|
if (/^https?:\/\//i.test(tok)) return false;
|
|
265
|
-
if (tok.includes("/")) return true;
|
|
266
286
|
const ext = extOf(tok);
|
|
287
|
+
if (tok.startsWith("/")) return ext !== null && CODE_EXTENSIONS.has(ext);
|
|
288
|
+
if (tok.includes("/")) return true;
|
|
267
289
|
return ext !== null && CODE_EXTENSIONS.has(ext);
|
|
268
290
|
}
|
|
269
291
|
function extOf(tok) {
|
|
@@ -316,10 +338,97 @@ function escapeRe(s) {
|
|
|
316
338
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
317
339
|
}
|
|
318
340
|
|
|
341
|
+
// src/config.ts
|
|
342
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
343
|
+
import { join } from "path";
|
|
344
|
+
var RC_FILE = ".groundtruthrc.json";
|
|
345
|
+
var VALID_FAIL_LEVELS = /* @__PURE__ */ new Set(["unsupported", "unverifiable"]);
|
|
346
|
+
var VALID_KINDS = /* @__PURE__ */ new Set([
|
|
347
|
+
"file",
|
|
348
|
+
"symbol",
|
|
349
|
+
"test",
|
|
350
|
+
"dependency",
|
|
351
|
+
"command",
|
|
352
|
+
"action"
|
|
353
|
+
]);
|
|
354
|
+
var VALID_OUTPUTS = /* @__PURE__ */ new Set(["terminal", "json", "markdown"]);
|
|
355
|
+
function loadConfig(cwd) {
|
|
356
|
+
const pkg = readJson(join(cwd, "package.json"));
|
|
357
|
+
const fromPkg = pkg && isRecord2(pkg.groundtruth) ? pkg.groundtruth : void 0;
|
|
358
|
+
const fromRc = readJson(join(cwd, RC_FILE));
|
|
359
|
+
return { ...sanitize(fromPkg), ...sanitize(fromRc) };
|
|
360
|
+
}
|
|
361
|
+
function applyConfig(claims, config) {
|
|
362
|
+
const ignoreKinds = new Set(config.ignoreKinds ?? []);
|
|
363
|
+
const matchers = (config.ignore ?? []).map(toMatcher);
|
|
364
|
+
return claims.filter((claim) => {
|
|
365
|
+
if (ignoreKinds.has(claim.kind)) return false;
|
|
366
|
+
return !matchers.some((match) => match(claim.target));
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function toMatcher(pattern) {
|
|
370
|
+
const p = pattern.toLowerCase();
|
|
371
|
+
if (p.includes("*")) {
|
|
372
|
+
const re = new RegExp(`^${p.split("*").map(escapeRe2).join(".*")}$`);
|
|
373
|
+
return (value) => re.test(value.toLowerCase());
|
|
374
|
+
}
|
|
375
|
+
return (value) => value.toLowerCase().includes(p);
|
|
376
|
+
}
|
|
377
|
+
function failingCount(report, config) {
|
|
378
|
+
const levels = config.failOn ?? ["unsupported"];
|
|
379
|
+
let n = 0;
|
|
380
|
+
if (levels.includes("unsupported")) n += report.summary.unsupported;
|
|
381
|
+
if (levels.includes("unverifiable")) n += report.summary.unverifiable;
|
|
382
|
+
return n;
|
|
383
|
+
}
|
|
384
|
+
function sanitize(input) {
|
|
385
|
+
if (!isRecord2(input)) return {};
|
|
386
|
+
const out = {};
|
|
387
|
+
if (typeof input.strict === "boolean") out.strict = input.strict;
|
|
388
|
+
if (typeof input.shadow === "boolean") out.shadow = input.shadow;
|
|
389
|
+
if (isStringArray(input.failOn)) {
|
|
390
|
+
out.failOn = input.failOn.filter((l) => VALID_FAIL_LEVELS.has(l));
|
|
391
|
+
}
|
|
392
|
+
if (isStringArray(input.ignore)) out.ignore = input.ignore;
|
|
393
|
+
if (isStringArray(input.ignoreKinds)) {
|
|
394
|
+
out.ignoreKinds = input.ignoreKinds.filter((k) => VALID_KINDS.has(k));
|
|
395
|
+
}
|
|
396
|
+
if (typeof input.output === "string" && VALID_OUTPUTS.has(input.output)) {
|
|
397
|
+
out.output = input.output;
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
function readJson(path) {
|
|
402
|
+
if (!existsSync(path)) return void 0;
|
|
403
|
+
try {
|
|
404
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
405
|
+
return isRecord2(parsed) ? parsed : void 0;
|
|
406
|
+
} catch {
|
|
407
|
+
return void 0;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function isRecord2(v) {
|
|
411
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
412
|
+
}
|
|
413
|
+
function isStringArray(v) {
|
|
414
|
+
return Array.isArray(v) && v.every((item) => typeof item === "string");
|
|
415
|
+
}
|
|
416
|
+
function escapeRe2(s) {
|
|
417
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
418
|
+
}
|
|
419
|
+
|
|
319
420
|
// src/git.ts
|
|
320
421
|
import { execFileSync } from "child_process";
|
|
321
|
-
function collectGitEvidence(cwd) {
|
|
422
|
+
function collectGitEvidence(cwd, base2) {
|
|
322
423
|
const ev = emptyEvidence();
|
|
424
|
+
if (base2) {
|
|
425
|
+
const range = `${base2}...HEAD`;
|
|
426
|
+
const diff2 = git(["diff", range, "--no-color", "--unified=0"], cwd);
|
|
427
|
+
if (diff2 !== null) parseDiff(diff2, ev);
|
|
428
|
+
const names = git(["diff", "--name-status", range], cwd);
|
|
429
|
+
if (names !== null) parseNameStatus(names, ev);
|
|
430
|
+
return ev;
|
|
431
|
+
}
|
|
323
432
|
const diff = git(["diff", "HEAD", "--no-color", "--unified=0"], cwd);
|
|
324
433
|
if (diff !== null) parseDiff(diff, ev);
|
|
325
434
|
const status = git(["status", "--porcelain"], cwd);
|
|
@@ -357,6 +466,17 @@ function stripDiffPath(raw) {
|
|
|
357
466
|
if (t === "/dev/null") return "";
|
|
358
467
|
return t.replace(/^[ab]\//, "");
|
|
359
468
|
}
|
|
469
|
+
function parseNameStatus(out, ev) {
|
|
470
|
+
for (const line of out.split("\n")) {
|
|
471
|
+
if (!line.trim()) continue;
|
|
472
|
+
const parts = line.split(" ");
|
|
473
|
+
const code = parts[0] ?? "";
|
|
474
|
+
const path = (parts.length > 2 ? parts[parts.length - 1] : parts[1]) ?? "";
|
|
475
|
+
if (!path) continue;
|
|
476
|
+
pushUnique(ev.touchedFiles, path);
|
|
477
|
+
if (code.startsWith("A")) pushUnique(ev.createdFiles, path);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
360
480
|
function parseStatus(status, ev) {
|
|
361
481
|
for (const line of status.split("\n")) {
|
|
362
482
|
if (!line.trim()) continue;
|
|
@@ -375,10 +495,10 @@ function pushUnique(arr, value) {
|
|
|
375
495
|
}
|
|
376
496
|
|
|
377
497
|
// src/evidence.ts
|
|
378
|
-
function buildEvidence(toolUses, cwd) {
|
|
498
|
+
function buildEvidence(toolUses, cwd, base2) {
|
|
379
499
|
const ev = emptyEvidence();
|
|
380
500
|
collectToolEvidence(toolUses, ev);
|
|
381
|
-
if (cwd) mergeEvidence(ev, collectGitEvidence(cwd));
|
|
501
|
+
if (cwd) mergeEvidence(ev, collectGitEvidence(cwd, base2));
|
|
382
502
|
return ev;
|
|
383
503
|
}
|
|
384
504
|
function emptyEvidence() {
|
|
@@ -410,7 +530,7 @@ ${str(input.old_string)}`;
|
|
|
410
530
|
if (file) addFile(ev, file, false);
|
|
411
531
|
const edits = Array.isArray(input.edits) ? input.edits : [];
|
|
412
532
|
for (const edit of edits) {
|
|
413
|
-
if (!
|
|
533
|
+
if (!isRecord3(edit)) continue;
|
|
414
534
|
ev.addedText += `
|
|
415
535
|
${str(edit.new_string)}`;
|
|
416
536
|
ev.removedText += `
|
|
@@ -458,7 +578,7 @@ function pushUnique2(arr, value) {
|
|
|
458
578
|
function str(v) {
|
|
459
579
|
return typeof v === "string" ? v : "";
|
|
460
580
|
}
|
|
461
|
-
function
|
|
581
|
+
function isRecord3(v) {
|
|
462
582
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
463
583
|
}
|
|
464
584
|
|
|
@@ -556,8 +676,20 @@ function fileMatches(claimPath, actual) {
|
|
|
556
676
|
if (b.endsWith(`/${a}`) || a.endsWith(`/${b}`)) return true;
|
|
557
677
|
if (!a.includes("/") && base(b) === a) return true;
|
|
558
678
|
if (!b.includes("/") && base(a) === b) return true;
|
|
679
|
+
if (!hasExt(a)) {
|
|
680
|
+
const bNoExt = stripExt(b);
|
|
681
|
+
if (a === bNoExt) return true;
|
|
682
|
+
if (bNoExt.endsWith(`/${a}`)) return true;
|
|
683
|
+
if (!a.includes("/") && base(bNoExt) === a) return true;
|
|
684
|
+
}
|
|
559
685
|
return false;
|
|
560
686
|
}
|
|
687
|
+
function hasExt(path) {
|
|
688
|
+
return /\.[A-Za-z0-9]+$/.test(base(path));
|
|
689
|
+
}
|
|
690
|
+
function stripExt(path) {
|
|
691
|
+
return path.replace(/\.[A-Za-z0-9]+$/, "");
|
|
692
|
+
}
|
|
561
693
|
function identifierPresent(haystack, id) {
|
|
562
694
|
if (!haystack) return false;
|
|
563
695
|
const esc = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -807,23 +939,492 @@ function truncate(s, max) {
|
|
|
807
939
|
// src/pipeline.ts
|
|
808
940
|
function runPipeline(input) {
|
|
809
941
|
const turn = input.turn ?? (input.transcriptPath ? parseTranscriptFile(input.transcriptPath) : { summary: "", toolUses: [] });
|
|
810
|
-
const
|
|
811
|
-
const
|
|
942
|
+
const config = input.config ?? (input.cwd ? loadConfig(input.cwd) : {});
|
|
943
|
+
const evidence = buildEvidence(turn.toolUses, input.cwd, input.base);
|
|
944
|
+
const claims = applyConfig(extractClaims(turn.summary), config);
|
|
812
945
|
const verdicts = verifyClaims(claims, evidence);
|
|
813
946
|
return buildReport(verdicts);
|
|
814
947
|
}
|
|
948
|
+
|
|
949
|
+
// src/adapters/index.ts
|
|
950
|
+
import { createHash } from "crypto";
|
|
951
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
952
|
+
import { homedir as homedir2 } from "os";
|
|
953
|
+
import { join as join3 } from "path";
|
|
954
|
+
|
|
955
|
+
// src/locate.ts
|
|
956
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
957
|
+
import { homedir } from "os";
|
|
958
|
+
import { join as join2, resolve } from "path";
|
|
959
|
+
function findLatestTranscript(cwd) {
|
|
960
|
+
const dir = projectDir(cwd);
|
|
961
|
+
if (!existsSync2(dir)) return null;
|
|
962
|
+
let newest = null;
|
|
963
|
+
for (const name of readdirSync(dir)) {
|
|
964
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
965
|
+
const path = join2(dir, name);
|
|
966
|
+
try {
|
|
967
|
+
const mtime = statSync(path).mtimeMs;
|
|
968
|
+
if (!newest || mtime > newest.mtime) newest = { path, mtime };
|
|
969
|
+
} catch {
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return newest?.path ?? null;
|
|
973
|
+
}
|
|
974
|
+
function projectDir(cwd) {
|
|
975
|
+
const encoded = resolve(cwd).replace(/[^A-Za-z0-9]/g, "-");
|
|
976
|
+
return join2(homedir(), ".claude", "projects", encoded);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/adapters/turn.ts
|
|
980
|
+
function assembleTurn(events) {
|
|
981
|
+
let start = -1;
|
|
982
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
983
|
+
if (events[i]?.role === "user") {
|
|
984
|
+
start = i;
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
let summary = "";
|
|
989
|
+
const toolUses = [];
|
|
990
|
+
for (const e of events.slice(start + 1)) {
|
|
991
|
+
if (e.role === "assistant" && e.text && e.text.trim()) summary = e.text.trim();
|
|
992
|
+
if (e.tool) toolUses.push(e.tool);
|
|
993
|
+
}
|
|
994
|
+
return { summary, toolUses };
|
|
995
|
+
}
|
|
996
|
+
function parseJsonlLines(raw) {
|
|
997
|
+
const out = [];
|
|
998
|
+
for (const line of raw.split("\n")) {
|
|
999
|
+
const trimmed = line.trim();
|
|
1000
|
+
if (!trimmed) continue;
|
|
1001
|
+
try {
|
|
1002
|
+
out.push(JSON.parse(trimmed));
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return out;
|
|
1007
|
+
}
|
|
1008
|
+
function parseApplyPatch(patch) {
|
|
1009
|
+
const tools = [];
|
|
1010
|
+
let file = null;
|
|
1011
|
+
let op = null;
|
|
1012
|
+
let added = [];
|
|
1013
|
+
let removed = [];
|
|
1014
|
+
const flush = () => {
|
|
1015
|
+
if (!file || !op) return;
|
|
1016
|
+
if (op === "add") {
|
|
1017
|
+
tools.push({ name: "Write", input: { file_path: file, content: added.join("\n") } });
|
|
1018
|
+
} else {
|
|
1019
|
+
tools.push({
|
|
1020
|
+
name: "Edit",
|
|
1021
|
+
input: { file_path: file, new_string: added.join("\n"), old_string: removed.join("\n") }
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
added = [];
|
|
1025
|
+
removed = [];
|
|
1026
|
+
};
|
|
1027
|
+
for (const line of patch.split("\n")) {
|
|
1028
|
+
const add = /^\*\*\* Add File: (.+)$/.exec(line);
|
|
1029
|
+
const upd = /^\*\*\* Update File: (.+)$/.exec(line);
|
|
1030
|
+
if (add?.[1]) {
|
|
1031
|
+
flush();
|
|
1032
|
+
file = add[1].trim();
|
|
1033
|
+
op = "add";
|
|
1034
|
+
} else if (upd?.[1]) {
|
|
1035
|
+
flush();
|
|
1036
|
+
file = upd[1].trim();
|
|
1037
|
+
op = "update";
|
|
1038
|
+
} else if (/^\*\*\* (End Patch|Begin Patch|Delete File:)/.test(line)) {
|
|
1039
|
+
flush();
|
|
1040
|
+
if (line.startsWith("*** End Patch")) {
|
|
1041
|
+
file = null;
|
|
1042
|
+
op = null;
|
|
1043
|
+
}
|
|
1044
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
1045
|
+
added.push(line.slice(1));
|
|
1046
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1047
|
+
removed.push(line.slice(1));
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
flush();
|
|
1051
|
+
return tools;
|
|
1052
|
+
}
|
|
1053
|
+
function isRecord4(v) {
|
|
1054
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1055
|
+
}
|
|
1056
|
+
function str2(v) {
|
|
1057
|
+
return typeof v === "string" ? v : "";
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/adapters/codex.ts
|
|
1061
|
+
function parseCodex(raw) {
|
|
1062
|
+
const events = [];
|
|
1063
|
+
for (const line of parseJsonlLines(raw)) {
|
|
1064
|
+
if (!isRecord4(line) || line.type !== "response_item" || !isRecord4(line.payload)) continue;
|
|
1065
|
+
const p = line.payload;
|
|
1066
|
+
switch (p.type) {
|
|
1067
|
+
case "message": {
|
|
1068
|
+
events.push({ role: p.role === "user" ? "user" : "assistant", text: textOf(p.content) });
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
case "function_call": {
|
|
1072
|
+
for (const tool of fromFunctionCall(str2(p.name), str2(p.arguments))) {
|
|
1073
|
+
events.push({ role: "assistant", tool });
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
case "custom_tool_call": {
|
|
1078
|
+
if (str2(p.name) === "apply_patch") {
|
|
1079
|
+
for (const tool of parseApplyPatch(str2(p.input)))
|
|
1080
|
+
events.push({ role: "assistant", tool });
|
|
1081
|
+
}
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
case "local_shell_call": {
|
|
1085
|
+
const cmd = isRecord4(p.action) ? commandToString(p.action.command) : "";
|
|
1086
|
+
if (cmd)
|
|
1087
|
+
events.push({ role: "assistant", tool: { name: "Bash", input: { command: cmd } } });
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
default:
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return assembleTurn(events);
|
|
1095
|
+
}
|
|
1096
|
+
function textOf(content) {
|
|
1097
|
+
if (typeof content === "string") return content;
|
|
1098
|
+
if (!Array.isArray(content)) return "";
|
|
1099
|
+
return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
|
|
1100
|
+
}
|
|
1101
|
+
function fromFunctionCall(name, argsJson) {
|
|
1102
|
+
if (name === "apply_patch") {
|
|
1103
|
+
const parsed = safeJson(argsJson);
|
|
1104
|
+
const patch = isRecord4(parsed) && typeof parsed.input === "string" ? parsed.input : argsJson;
|
|
1105
|
+
return parseApplyPatch(patch);
|
|
1106
|
+
}
|
|
1107
|
+
if (name === "shell" || name === "shell_command" || name === "bash") {
|
|
1108
|
+
const parsed = safeJson(argsJson);
|
|
1109
|
+
const cmd = isRecord4(parsed) ? commandToString(parsed.command) : "";
|
|
1110
|
+
if (!cmd) return [];
|
|
1111
|
+
if (cmd.includes("apply_patch") && cmd.includes("*** Begin Patch")) {
|
|
1112
|
+
const m = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(cmd);
|
|
1113
|
+
if (m) return parseApplyPatch(m[0]);
|
|
1114
|
+
}
|
|
1115
|
+
return [{ name: "Bash", input: { command: cmd } }];
|
|
1116
|
+
}
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
function commandToString(command) {
|
|
1120
|
+
if (typeof command === "string") return command;
|
|
1121
|
+
if (Array.isArray(command)) return command.filter((c2) => typeof c2 === "string").join(" ");
|
|
1122
|
+
return "";
|
|
1123
|
+
}
|
|
1124
|
+
function safeJson(s) {
|
|
1125
|
+
try {
|
|
1126
|
+
return JSON.parse(s);
|
|
1127
|
+
} catch {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/adapters/cursor.ts
|
|
1133
|
+
function parseCursor(raw) {
|
|
1134
|
+
const events = [];
|
|
1135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1136
|
+
let resultText = "";
|
|
1137
|
+
for (const line of parseJsonlLines(raw)) {
|
|
1138
|
+
if (!isRecord4(line)) continue;
|
|
1139
|
+
switch (line.type) {
|
|
1140
|
+
case "user":
|
|
1141
|
+
events.push({ role: "user", text: messageText(line.message) });
|
|
1142
|
+
break;
|
|
1143
|
+
case "assistant":
|
|
1144
|
+
events.push({ role: "assistant", text: messageText(line.message) });
|
|
1145
|
+
break;
|
|
1146
|
+
case "tool_call": {
|
|
1147
|
+
const id = str2(line.call_id);
|
|
1148
|
+
if (id && seen.has(id)) break;
|
|
1149
|
+
if (id) seen.add(id);
|
|
1150
|
+
const tool = fromToolCall(line.tool_call);
|
|
1151
|
+
if (tool) events.push({ role: "assistant", tool });
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
case "result":
|
|
1155
|
+
resultText = str2(line.result);
|
|
1156
|
+
break;
|
|
1157
|
+
default:
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (resultText.trim()) events.push({ role: "assistant", text: resultText });
|
|
1162
|
+
return assembleTurn(events);
|
|
1163
|
+
}
|
|
1164
|
+
function messageText(message) {
|
|
1165
|
+
if (!isRecord4(message)) return "";
|
|
1166
|
+
const content = message.content;
|
|
1167
|
+
if (typeof content === "string") return content;
|
|
1168
|
+
if (!Array.isArray(content)) return "";
|
|
1169
|
+
return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
|
|
1170
|
+
}
|
|
1171
|
+
function fromToolCall(tc) {
|
|
1172
|
+
if (!isRecord4(tc)) return null;
|
|
1173
|
+
if (isRecord4(tc.writeToolCall)) {
|
|
1174
|
+
const a = argsOf(tc.writeToolCall);
|
|
1175
|
+
return { name: "Write", input: { file_path: str2(a.path), content: str2(a.fileText) } };
|
|
1176
|
+
}
|
|
1177
|
+
if (isRecord4(tc.editToolCall)) {
|
|
1178
|
+
const a = argsOf(tc.editToolCall);
|
|
1179
|
+
return {
|
|
1180
|
+
name: "Edit",
|
|
1181
|
+
input: {
|
|
1182
|
+
file_path: str2(a.path),
|
|
1183
|
+
new_string: str2(a.fileText ?? a.newString),
|
|
1184
|
+
old_string: str2(a.oldString)
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
if (isRecord4(tc.shellToolCall)) {
|
|
1189
|
+
const a = argsOf(tc.shellToolCall);
|
|
1190
|
+
return { name: "Bash", input: { command: str2(a.command) } };
|
|
1191
|
+
}
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
function argsOf(x) {
|
|
1195
|
+
return isRecord4(x.args) ? x.args : {};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/adapters/gemini.ts
|
|
1199
|
+
function parseGemini(raw) {
|
|
1200
|
+
const events = [];
|
|
1201
|
+
for (const rec of records(raw)) {
|
|
1202
|
+
if (!isRecord4(rec)) continue;
|
|
1203
|
+
if (rec.type === "user") {
|
|
1204
|
+
events.push({ role: "user", text: textOf2(rec.content) });
|
|
1205
|
+
} else if (rec.type === "gemini") {
|
|
1206
|
+
events.push({ role: "assistant", text: textOf2(rec.content) });
|
|
1207
|
+
const calls = Array.isArray(rec.toolCalls) ? rec.toolCalls : [];
|
|
1208
|
+
for (const call of calls) {
|
|
1209
|
+
const tool = fromToolCall2(call);
|
|
1210
|
+
if (tool) events.push({ role: "assistant", tool });
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return assembleTurn(events);
|
|
1215
|
+
}
|
|
1216
|
+
function records(raw) {
|
|
1217
|
+
const trimmed = raw.trim();
|
|
1218
|
+
if (trimmed.startsWith("{")) {
|
|
1219
|
+
try {
|
|
1220
|
+
const obj = JSON.parse(trimmed);
|
|
1221
|
+
if (isRecord4(obj) && Array.isArray(obj.messages)) return obj.messages;
|
|
1222
|
+
} catch {
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return parseJsonlLines(raw);
|
|
1226
|
+
}
|
|
1227
|
+
function textOf2(content) {
|
|
1228
|
+
if (typeof content === "string") return content;
|
|
1229
|
+
if (!Array.isArray(content)) return "";
|
|
1230
|
+
return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
|
|
1231
|
+
}
|
|
1232
|
+
function fromToolCall2(call) {
|
|
1233
|
+
if (!isRecord4(call)) return null;
|
|
1234
|
+
const name = str2(call.name);
|
|
1235
|
+
const args = isRecord4(call.args) ? call.args : {};
|
|
1236
|
+
if (name === "write_file") {
|
|
1237
|
+
return { name: "Write", input: { file_path: str2(args.file_path), content: str2(args.content) } };
|
|
1238
|
+
}
|
|
1239
|
+
if (name === "replace") {
|
|
1240
|
+
return {
|
|
1241
|
+
name: "Edit",
|
|
1242
|
+
input: {
|
|
1243
|
+
file_path: str2(args.file_path),
|
|
1244
|
+
new_string: str2(args.new_string),
|
|
1245
|
+
old_string: str2(args.old_string)
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
if (name === "run_shell_command") {
|
|
1250
|
+
return { name: "Bash", input: { command: str2(args.command) } };
|
|
1251
|
+
}
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// src/adapters/index.ts
|
|
1256
|
+
var claude = {
|
|
1257
|
+
name: "claude",
|
|
1258
|
+
locate: (cwd) => findLatestTranscript(cwd),
|
|
1259
|
+
parse: (path) => parseTranscriptFile(path)
|
|
1260
|
+
};
|
|
1261
|
+
var codex = {
|
|
1262
|
+
name: "codex",
|
|
1263
|
+
locate() {
|
|
1264
|
+
const home = process.env.CODEX_HOME ?? join3(homedir2(), ".codex");
|
|
1265
|
+
return newestMatch(
|
|
1266
|
+
join3(home, "sessions"),
|
|
1267
|
+
(n) => n.startsWith("rollout-") && n.endsWith(".jsonl")
|
|
1268
|
+
);
|
|
1269
|
+
},
|
|
1270
|
+
parse: (path) => parseCodex(readFileSync3(path, "utf8"))
|
|
1271
|
+
};
|
|
1272
|
+
var gemini = {
|
|
1273
|
+
name: "gemini",
|
|
1274
|
+
locate(cwd) {
|
|
1275
|
+
const home = process.env.GEMINI_DIR ?? join3(homedir2(), ".gemini");
|
|
1276
|
+
const hash = createHash("sha256").update(cwd).digest("hex");
|
|
1277
|
+
const scoped = newestMatch(
|
|
1278
|
+
join3(home, "tmp", hash, "chats"),
|
|
1279
|
+
(n) => n.endsWith(".jsonl") || n.endsWith(".json")
|
|
1280
|
+
);
|
|
1281
|
+
if (scoped) return scoped;
|
|
1282
|
+
return newestMatch(join3(home, "tmp"), (n) => n.startsWith("session") && /\.jsonl?$/.test(n));
|
|
1283
|
+
},
|
|
1284
|
+
parse: (path) => parseGemini(readFileSync3(path, "utf8"))
|
|
1285
|
+
};
|
|
1286
|
+
var cursor = {
|
|
1287
|
+
name: "cursor",
|
|
1288
|
+
locate() {
|
|
1289
|
+
return newestMatch(join3(homedir2(), ".cursor", "projects"), (n) => n.endsWith(".jsonl"));
|
|
1290
|
+
},
|
|
1291
|
+
parse: (path) => parseCursor(readFileSync3(path, "utf8"))
|
|
1292
|
+
};
|
|
1293
|
+
var ADAPTERS = { claude, codex, gemini, cursor };
|
|
1294
|
+
var AGENT_NAMES = Object.keys(ADAPTERS);
|
|
1295
|
+
function getAdapter(name) {
|
|
1296
|
+
return ADAPTERS[name] ?? null;
|
|
1297
|
+
}
|
|
1298
|
+
function autoDetect(cwd) {
|
|
1299
|
+
let best = null;
|
|
1300
|
+
for (const adapter of Object.values(ADAPTERS)) {
|
|
1301
|
+
const path = adapter.locate(cwd);
|
|
1302
|
+
if (!path) continue;
|
|
1303
|
+
try {
|
|
1304
|
+
const mtime = statSync2(path).mtimeMs;
|
|
1305
|
+
if (!best || mtime > best.mtime) best = { adapter, path, mtime };
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return best ? { adapter: best.adapter, path: best.path } : null;
|
|
1310
|
+
}
|
|
1311
|
+
function newestMatch(dir, test, depth = 6) {
|
|
1312
|
+
const found = walk(dir, test, depth);
|
|
1313
|
+
return found?.path ?? null;
|
|
1314
|
+
}
|
|
1315
|
+
function walk(dir, test, depth) {
|
|
1316
|
+
if (depth < 0 || !existsSync3(dir)) return null;
|
|
1317
|
+
let entries;
|
|
1318
|
+
try {
|
|
1319
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1320
|
+
} catch {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
let best = null;
|
|
1324
|
+
for (const entry of entries) {
|
|
1325
|
+
const full = join3(dir, entry.name);
|
|
1326
|
+
try {
|
|
1327
|
+
if (entry.isDirectory()) {
|
|
1328
|
+
const sub = walk(full, test, depth - 1);
|
|
1329
|
+
if (sub && (!best || sub.mtime > best.mtime)) best = sub;
|
|
1330
|
+
} else if (entry.isFile() && test(entry.name)) {
|
|
1331
|
+
const mtime = statSync2(full).mtimeMs;
|
|
1332
|
+
if (!best || mtime > best.mtime) best = { path: full, mtime };
|
|
1333
|
+
}
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return best;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/ledger.ts
|
|
1341
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4 } from "fs";
|
|
1342
|
+
import { homedir as homedir3 } from "os";
|
|
1343
|
+
import { dirname, join as join4 } from "path";
|
|
1344
|
+
function ledgerPath() {
|
|
1345
|
+
return process.env.GROUNDTRUTH_LEDGER ?? join4(homedir3(), ".groundtruth", "ledger.jsonl");
|
|
1346
|
+
}
|
|
1347
|
+
function recordRun(report, cwd, session) {
|
|
1348
|
+
if (report.summary.total === 0) return;
|
|
1349
|
+
const entry = {
|
|
1350
|
+
t: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1351
|
+
cwd,
|
|
1352
|
+
v: report.summary.verified,
|
|
1353
|
+
u: report.summary.unsupported,
|
|
1354
|
+
r: report.summary.unverifiable
|
|
1355
|
+
};
|
|
1356
|
+
if (session) entry.session = session;
|
|
1357
|
+
try {
|
|
1358
|
+
const path = ledgerPath();
|
|
1359
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1360
|
+
appendFileSync(path, `${JSON.stringify(entry)}
|
|
1361
|
+
`, "utf8");
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function readLedger() {
|
|
1366
|
+
const path = ledgerPath();
|
|
1367
|
+
if (!existsSync4(path)) return [];
|
|
1368
|
+
const out = [];
|
|
1369
|
+
try {
|
|
1370
|
+
for (const line of readFileSync4(path, "utf8").split("\n")) {
|
|
1371
|
+
const trimmed = line.trim();
|
|
1372
|
+
if (!trimmed) continue;
|
|
1373
|
+
try {
|
|
1374
|
+
const parsed = JSON.parse(trimmed);
|
|
1375
|
+
if (isEntry(parsed)) out.push(parsed);
|
|
1376
|
+
} catch {
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
return out;
|
|
1382
|
+
}
|
|
1383
|
+
function summarize(entries, opts = {}) {
|
|
1384
|
+
const cutoff = opts.sinceDays !== void 0 ? Date.now() - opts.sinceDays * 864e5 : 0;
|
|
1385
|
+
const sum = { runs: 0, verified: 0, unsupported: 0, unverifiable: 0 };
|
|
1386
|
+
for (const e of entries) {
|
|
1387
|
+
if (opts.cwd && e.cwd !== opts.cwd) continue;
|
|
1388
|
+
if (opts.session && e.session !== opts.session) continue;
|
|
1389
|
+
if (cutoff && Date.parse(e.t) < cutoff) continue;
|
|
1390
|
+
sum.runs += 1;
|
|
1391
|
+
sum.verified += e.v;
|
|
1392
|
+
sum.unsupported += e.u;
|
|
1393
|
+
sum.unverifiable += e.r;
|
|
1394
|
+
}
|
|
1395
|
+
return sum;
|
|
1396
|
+
}
|
|
1397
|
+
function isEntry(v) {
|
|
1398
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1399
|
+
const e = v;
|
|
1400
|
+
return typeof e.t === "string" && typeof e.cwd === "string" && typeof e.v === "number" && typeof e.u === "number" && typeof e.r === "number";
|
|
1401
|
+
}
|
|
815
1402
|
export {
|
|
1403
|
+
ADAPTERS,
|
|
1404
|
+
AGENT_NAMES,
|
|
1405
|
+
applyConfig,
|
|
1406
|
+
autoDetect,
|
|
816
1407
|
buildEvidence,
|
|
817
1408
|
buildReport,
|
|
818
1409
|
collectGitEvidence,
|
|
819
1410
|
emptyEvidence,
|
|
820
1411
|
extractClaims,
|
|
1412
|
+
failingCount,
|
|
1413
|
+
getAdapter,
|
|
1414
|
+
ledgerPath,
|
|
1415
|
+
loadConfig,
|
|
821
1416
|
mergeEvidence,
|
|
1417
|
+
parseCodex,
|
|
1418
|
+
parseCursor,
|
|
1419
|
+
parseGemini,
|
|
822
1420
|
parseTranscript,
|
|
823
1421
|
parseTranscriptFile,
|
|
1422
|
+
readLedger,
|
|
1423
|
+
recordRun,
|
|
824
1424
|
renderJson,
|
|
825
1425
|
renderMarkdown,
|
|
826
1426
|
renderTerminal,
|
|
827
1427
|
runPipeline,
|
|
1428
|
+
summarize,
|
|
828
1429
|
verifyClaims
|
|
829
1430
|
};
|