@twarc_net/groundtruth 0.1.0 → 0.3.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/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
- const kind = classifyToken(tok);
85
- if (kind === "file") {
90
+ if (looksLikePath(tok)) {
86
91
  add("file", tok, polarity, clause);
87
92
  concrete = true;
88
- } else if (kind === "symbol") {
89
- add("symbol", stripCall(tok), polarity, clause);
90
- concrete = true;
91
- } else if (kind === "command") {
92
- add("command", tok, polarity, clause);
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 classifyToken(tok) {
252
- if (looksLikePath(tok)) return "file";
253
- if (tok.includes(" ")) {
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
- if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(id)) return "symbol";
259
- return null;
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,104 @@ 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, opts = {}) {
322
423
  const ev = emptyEvidence();
424
+ if (opts.staged) {
425
+ const diff2 = git(["diff", "--cached", "--no-color", "--unified=0"], cwd);
426
+ if (diff2 !== null) parseDiff(diff2, ev);
427
+ const names = git(["diff", "--cached", "--name-status"], cwd);
428
+ if (names !== null) parseNameStatus(names, ev);
429
+ return ev;
430
+ }
431
+ if (opts.base) {
432
+ const range = `${opts.base}...HEAD`;
433
+ const diff2 = git(["diff", range, "--no-color", "--unified=0"], cwd);
434
+ if (diff2 !== null) parseDiff(diff2, ev);
435
+ const names = git(["diff", "--name-status", range], cwd);
436
+ if (names !== null) parseNameStatus(names, ev);
437
+ return ev;
438
+ }
323
439
  const diff = git(["diff", "HEAD", "--no-color", "--unified=0"], cwd);
324
440
  if (diff !== null) parseDiff(diff, ev);
325
441
  const status = git(["status", "--porcelain"], cwd);
@@ -357,6 +473,17 @@ function stripDiffPath(raw) {
357
473
  if (t === "/dev/null") return "";
358
474
  return t.replace(/^[ab]\//, "");
359
475
  }
476
+ function parseNameStatus(out, ev) {
477
+ for (const line of out.split("\n")) {
478
+ if (!line.trim()) continue;
479
+ const parts = line.split(" ");
480
+ const code = parts[0] ?? "";
481
+ const path = (parts.length > 2 ? parts[parts.length - 1] : parts[1]) ?? "";
482
+ if (!path) continue;
483
+ pushUnique(ev.touchedFiles, path);
484
+ if (code.startsWith("A")) pushUnique(ev.createdFiles, path);
485
+ }
486
+ }
360
487
  function parseStatus(status, ev) {
361
488
  for (const line of status.split("\n")) {
362
489
  if (!line.trim()) continue;
@@ -375,10 +502,10 @@ function pushUnique(arr, value) {
375
502
  }
376
503
 
377
504
  // src/evidence.ts
378
- function buildEvidence(toolUses, cwd) {
505
+ function buildEvidence(toolUses, cwd, git2 = {}) {
379
506
  const ev = emptyEvidence();
380
507
  collectToolEvidence(toolUses, ev);
381
- if (cwd) mergeEvidence(ev, collectGitEvidence(cwd));
508
+ if (cwd) mergeEvidence(ev, collectGitEvidence(cwd, git2));
382
509
  return ev;
383
510
  }
384
511
  function emptyEvidence() {
@@ -410,7 +537,7 @@ ${str(input.old_string)}`;
410
537
  if (file) addFile(ev, file, false);
411
538
  const edits = Array.isArray(input.edits) ? input.edits : [];
412
539
  for (const edit of edits) {
413
- if (!isRecord2(edit)) continue;
540
+ if (!isRecord3(edit)) continue;
414
541
  ev.addedText += `
415
542
  ${str(edit.new_string)}`;
416
543
  ev.removedText += `
@@ -458,7 +585,7 @@ function pushUnique2(arr, value) {
458
585
  function str(v) {
459
586
  return typeof v === "string" ? v : "";
460
587
  }
461
- function isRecord2(v) {
588
+ function isRecord3(v) {
462
589
  return typeof v === "object" && v !== null && !Array.isArray(v);
463
590
  }
464
591
 
@@ -556,8 +683,20 @@ function fileMatches(claimPath, actual) {
556
683
  if (b.endsWith(`/${a}`) || a.endsWith(`/${b}`)) return true;
557
684
  if (!a.includes("/") && base(b) === a) return true;
558
685
  if (!b.includes("/") && base(a) === b) return true;
686
+ if (!hasExt(a)) {
687
+ const bNoExt = stripExt(b);
688
+ if (a === bNoExt) return true;
689
+ if (bNoExt.endsWith(`/${a}`)) return true;
690
+ if (!a.includes("/") && base(bNoExt) === a) return true;
691
+ }
559
692
  return false;
560
693
  }
694
+ function hasExt(path) {
695
+ return /\.[A-Za-z0-9]+$/.test(base(path));
696
+ }
697
+ function stripExt(path) {
698
+ return path.replace(/\.[A-Za-z0-9]+$/, "");
699
+ }
561
700
  function identifierPresent(haystack, id) {
562
701
  if (!haystack) return false;
563
702
  const esc = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -807,23 +946,709 @@ function truncate(s, max) {
807
946
  // src/pipeline.ts
808
947
  function runPipeline(input) {
809
948
  const turn = input.turn ?? (input.transcriptPath ? parseTranscriptFile(input.transcriptPath) : { summary: "", toolUses: [] });
810
- const evidence = buildEvidence(turn.toolUses, input.cwd);
811
- const claims = extractClaims(turn.summary);
949
+ const config = input.config ?? (input.cwd ? loadConfig(input.cwd) : {});
950
+ const evidence = buildEvidence(turn.toolUses, input.cwd, {
951
+ base: input.base,
952
+ staged: input.staged
953
+ });
954
+ const claims = applyConfig(extractClaims(turn.summary), config);
812
955
  const verdicts = verifyClaims(claims, evidence);
813
956
  return buildReport(verdicts);
814
957
  }
958
+
959
+ // src/adapters/index.ts
960
+ import { createHash } from "crypto";
961
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
962
+ import { homedir as homedir2 } from "os";
963
+ import { join as join4 } from "path";
964
+
965
+ // src/locate.ts
966
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
967
+ import { homedir } from "os";
968
+ import { join as join2, resolve } from "path";
969
+ function findLatestTranscript(cwd) {
970
+ const dir = projectDir(cwd);
971
+ if (!existsSync2(dir)) return null;
972
+ let newest = null;
973
+ for (const name of readdirSync(dir)) {
974
+ if (!name.endsWith(".jsonl")) continue;
975
+ const path = join2(dir, name);
976
+ try {
977
+ const mtime = statSync(path).mtimeMs;
978
+ if (!newest || mtime > newest.mtime) newest = { path, mtime };
979
+ } catch {
980
+ }
981
+ }
982
+ return newest?.path ?? null;
983
+ }
984
+ function projectDir(cwd) {
985
+ const encoded = resolve(cwd).replace(/[^A-Za-z0-9]/g, "-");
986
+ return join2(homedir(), ".claude", "projects", encoded);
987
+ }
988
+
989
+ // src/adapters/turn.ts
990
+ function assembleTurn(events) {
991
+ let start = -1;
992
+ for (let i = events.length - 1; i >= 0; i--) {
993
+ if (events[i]?.role === "user") {
994
+ start = i;
995
+ break;
996
+ }
997
+ }
998
+ let summary = "";
999
+ const toolUses = [];
1000
+ for (const e of events.slice(start + 1)) {
1001
+ if (e.role === "assistant" && e.text && e.text.trim()) summary = e.text.trim();
1002
+ if (e.tool) toolUses.push(e.tool);
1003
+ }
1004
+ return { summary, toolUses };
1005
+ }
1006
+ function parseJsonlLines(raw) {
1007
+ const out = [];
1008
+ for (const line of raw.split("\n")) {
1009
+ const trimmed = line.trim();
1010
+ if (!trimmed) continue;
1011
+ try {
1012
+ out.push(JSON.parse(trimmed));
1013
+ } catch {
1014
+ }
1015
+ }
1016
+ return out;
1017
+ }
1018
+ function parseApplyPatch(patch) {
1019
+ const tools = [];
1020
+ let file = null;
1021
+ let op = null;
1022
+ let added = [];
1023
+ let removed = [];
1024
+ const flush = () => {
1025
+ if (!file || !op) return;
1026
+ if (op === "add") {
1027
+ tools.push({ name: "Write", input: { file_path: file, content: added.join("\n") } });
1028
+ } else {
1029
+ tools.push({
1030
+ name: "Edit",
1031
+ input: { file_path: file, new_string: added.join("\n"), old_string: removed.join("\n") }
1032
+ });
1033
+ }
1034
+ added = [];
1035
+ removed = [];
1036
+ };
1037
+ for (const line of patch.split("\n")) {
1038
+ const add = /^\*\*\* Add File: (.+)$/.exec(line);
1039
+ const upd = /^\*\*\* Update File: (.+)$/.exec(line);
1040
+ if (add?.[1]) {
1041
+ flush();
1042
+ file = add[1].trim();
1043
+ op = "add";
1044
+ } else if (upd?.[1]) {
1045
+ flush();
1046
+ file = upd[1].trim();
1047
+ op = "update";
1048
+ } else if (/^\*\*\* (End Patch|Begin Patch|Delete File:)/.test(line)) {
1049
+ flush();
1050
+ if (line.startsWith("*** End Patch")) {
1051
+ file = null;
1052
+ op = null;
1053
+ }
1054
+ } else if (line.startsWith("+") && !line.startsWith("+++")) {
1055
+ added.push(line.slice(1));
1056
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
1057
+ removed.push(line.slice(1));
1058
+ }
1059
+ }
1060
+ flush();
1061
+ return tools;
1062
+ }
1063
+ function isRecord4(v) {
1064
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1065
+ }
1066
+ function str2(v) {
1067
+ return typeof v === "string" ? v : "";
1068
+ }
1069
+
1070
+ // src/adapters/aider.ts
1071
+ function parseAider(raw) {
1072
+ const sessions = raw.split(/^# aider chat started at .*$/m);
1073
+ const last = sessions[sessions.length - 1] ?? raw;
1074
+ const events = [];
1075
+ let assistant = [];
1076
+ let user = [];
1077
+ const flushAssistant = () => {
1078
+ const text = assistant.join("\n").trim();
1079
+ if (text) {
1080
+ events.push({ role: "assistant", text });
1081
+ for (const tool of parseSearchReplace(text)) events.push({ role: "assistant", tool });
1082
+ }
1083
+ assistant = [];
1084
+ };
1085
+ const flushUser = () => {
1086
+ const text = user.join("\n").trim();
1087
+ if (text) events.push({ role: "user", text });
1088
+ user = [];
1089
+ };
1090
+ for (const line of last.split("\n")) {
1091
+ if (line.startsWith("#### ")) {
1092
+ flushAssistant();
1093
+ user.push(line.slice(5));
1094
+ } else if (line.startsWith("> ")) {
1095
+ } else {
1096
+ flushUser();
1097
+ assistant.push(line);
1098
+ }
1099
+ }
1100
+ flushUser();
1101
+ flushAssistant();
1102
+ return assembleTurn(events);
1103
+ }
1104
+ function parseSearchReplace(text) {
1105
+ const tools = [];
1106
+ const lines = text.split("\n");
1107
+ for (let i = 0; i < lines.length; i++) {
1108
+ if (!/^<{5,9} SEARCH\s*$/.test(lines[i] ?? "")) continue;
1109
+ let path = "";
1110
+ for (let j = i - 1; j >= 0; j--) {
1111
+ const l = (lines[j] ?? "").trim();
1112
+ if (!l || l.startsWith("```")) continue;
1113
+ path = l;
1114
+ break;
1115
+ }
1116
+ const added = [];
1117
+ let inReplace = false;
1118
+ let k = i + 1;
1119
+ for (; k < lines.length; k++) {
1120
+ const l = lines[k] ?? "";
1121
+ if (/^={5,9}\s*$/.test(l)) {
1122
+ inReplace = true;
1123
+ continue;
1124
+ }
1125
+ if (/^>{5,9} REPLACE\s*$/.test(l)) break;
1126
+ if (inReplace) added.push(l);
1127
+ }
1128
+ if (path)
1129
+ tools.push({ name: "Edit", input: { file_path: path, new_string: added.join("\n") } });
1130
+ i = k;
1131
+ }
1132
+ return tools;
1133
+ }
1134
+
1135
+ // src/adapters/codex.ts
1136
+ function parseCodex(raw) {
1137
+ const events = [];
1138
+ for (const line of parseJsonlLines(raw)) {
1139
+ if (!isRecord4(line) || line.type !== "response_item" || !isRecord4(line.payload)) continue;
1140
+ const p = line.payload;
1141
+ switch (p.type) {
1142
+ case "message": {
1143
+ events.push({ role: p.role === "user" ? "user" : "assistant", text: textOf(p.content) });
1144
+ break;
1145
+ }
1146
+ case "function_call": {
1147
+ for (const tool of fromFunctionCall(str2(p.name), str2(p.arguments))) {
1148
+ events.push({ role: "assistant", tool });
1149
+ }
1150
+ break;
1151
+ }
1152
+ case "custom_tool_call": {
1153
+ if (str2(p.name) === "apply_patch") {
1154
+ for (const tool of parseApplyPatch(str2(p.input)))
1155
+ events.push({ role: "assistant", tool });
1156
+ }
1157
+ break;
1158
+ }
1159
+ case "local_shell_call": {
1160
+ const cmd = isRecord4(p.action) ? commandToString(p.action.command) : "";
1161
+ if (cmd)
1162
+ events.push({ role: "assistant", tool: { name: "Bash", input: { command: cmd } } });
1163
+ break;
1164
+ }
1165
+ default:
1166
+ break;
1167
+ }
1168
+ }
1169
+ return assembleTurn(events);
1170
+ }
1171
+ function textOf(content) {
1172
+ if (typeof content === "string") return content;
1173
+ if (!Array.isArray(content)) return "";
1174
+ return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
1175
+ }
1176
+ function fromFunctionCall(name, argsJson) {
1177
+ if (name === "apply_patch") {
1178
+ const parsed = safeJson(argsJson);
1179
+ const patch = isRecord4(parsed) && typeof parsed.input === "string" ? parsed.input : argsJson;
1180
+ return parseApplyPatch(patch);
1181
+ }
1182
+ if (name === "shell" || name === "shell_command" || name === "bash") {
1183
+ const parsed = safeJson(argsJson);
1184
+ const cmd = isRecord4(parsed) ? commandToString(parsed.command) : "";
1185
+ if (!cmd) return [];
1186
+ if (cmd.includes("apply_patch") && cmd.includes("*** Begin Patch")) {
1187
+ const m = /\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch/.exec(cmd);
1188
+ if (m) return parseApplyPatch(m[0]);
1189
+ }
1190
+ return [{ name: "Bash", input: { command: cmd } }];
1191
+ }
1192
+ return [];
1193
+ }
1194
+ function commandToString(command) {
1195
+ if (typeof command === "string") return command;
1196
+ if (Array.isArray(command)) return command.filter((c2) => typeof c2 === "string").join(" ");
1197
+ return "";
1198
+ }
1199
+ function safeJson(s) {
1200
+ try {
1201
+ return JSON.parse(s);
1202
+ } catch {
1203
+ return null;
1204
+ }
1205
+ }
1206
+
1207
+ // src/adapters/cursor.ts
1208
+ function parseCursor(raw) {
1209
+ const events = [];
1210
+ const seen = /* @__PURE__ */ new Set();
1211
+ let resultText = "";
1212
+ for (const line of parseJsonlLines(raw)) {
1213
+ if (!isRecord4(line)) continue;
1214
+ switch (line.type) {
1215
+ case "user":
1216
+ events.push({ role: "user", text: messageText(line.message) });
1217
+ break;
1218
+ case "assistant":
1219
+ events.push({ role: "assistant", text: messageText(line.message) });
1220
+ break;
1221
+ case "tool_call": {
1222
+ const id = str2(line.call_id);
1223
+ if (id && seen.has(id)) break;
1224
+ if (id) seen.add(id);
1225
+ const tool = fromToolCall(line.tool_call);
1226
+ if (tool) events.push({ role: "assistant", tool });
1227
+ break;
1228
+ }
1229
+ case "result":
1230
+ resultText = str2(line.result);
1231
+ break;
1232
+ default:
1233
+ break;
1234
+ }
1235
+ }
1236
+ if (resultText.trim()) events.push({ role: "assistant", text: resultText });
1237
+ return assembleTurn(events);
1238
+ }
1239
+ function messageText(message) {
1240
+ if (!isRecord4(message)) return "";
1241
+ const content = message.content;
1242
+ if (typeof content === "string") return content;
1243
+ if (!Array.isArray(content)) return "";
1244
+ return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
1245
+ }
1246
+ function fromToolCall(tc) {
1247
+ if (!isRecord4(tc)) return null;
1248
+ if (isRecord4(tc.writeToolCall)) {
1249
+ const a = argsOf(tc.writeToolCall);
1250
+ return { name: "Write", input: { file_path: str2(a.path), content: str2(a.fileText) } };
1251
+ }
1252
+ if (isRecord4(tc.editToolCall)) {
1253
+ const a = argsOf(tc.editToolCall);
1254
+ return {
1255
+ name: "Edit",
1256
+ input: {
1257
+ file_path: str2(a.path),
1258
+ new_string: str2(a.fileText ?? a.newString),
1259
+ old_string: str2(a.oldString)
1260
+ }
1261
+ };
1262
+ }
1263
+ if (isRecord4(tc.shellToolCall)) {
1264
+ const a = argsOf(tc.shellToolCall);
1265
+ return { name: "Bash", input: { command: str2(a.command) } };
1266
+ }
1267
+ return null;
1268
+ }
1269
+ function argsOf(x) {
1270
+ return isRecord4(x.args) ? x.args : {};
1271
+ }
1272
+
1273
+ // src/adapters/gemini.ts
1274
+ function parseGemini(raw) {
1275
+ const events = [];
1276
+ for (const rec of records(raw)) {
1277
+ if (!isRecord4(rec)) continue;
1278
+ if (rec.type === "user") {
1279
+ events.push({ role: "user", text: textOf2(rec.content) });
1280
+ } else if (rec.type === "gemini") {
1281
+ events.push({ role: "assistant", text: textOf2(rec.content) });
1282
+ const calls = Array.isArray(rec.toolCalls) ? rec.toolCalls : [];
1283
+ for (const call of calls) {
1284
+ const tool = fromToolCall2(call);
1285
+ if (tool) events.push({ role: "assistant", tool });
1286
+ }
1287
+ }
1288
+ }
1289
+ return assembleTurn(events);
1290
+ }
1291
+ function records(raw) {
1292
+ const trimmed = raw.trim();
1293
+ if (trimmed.startsWith("{")) {
1294
+ try {
1295
+ const obj = JSON.parse(trimmed);
1296
+ if (isRecord4(obj) && Array.isArray(obj.messages)) return obj.messages;
1297
+ } catch {
1298
+ }
1299
+ }
1300
+ return parseJsonlLines(raw);
1301
+ }
1302
+ function textOf2(content) {
1303
+ if (typeof content === "string") return content;
1304
+ if (!Array.isArray(content)) return "";
1305
+ return content.filter(isRecord4).map((b) => typeof b.text === "string" ? b.text : "").join("\n").trim();
1306
+ }
1307
+ function fromToolCall2(call) {
1308
+ if (!isRecord4(call)) return null;
1309
+ const name = str2(call.name);
1310
+ const args = isRecord4(call.args) ? call.args : {};
1311
+ if (name === "write_file") {
1312
+ return { name: "Write", input: { file_path: str2(args.file_path), content: str2(args.content) } };
1313
+ }
1314
+ if (name === "replace") {
1315
+ return {
1316
+ name: "Edit",
1317
+ input: {
1318
+ file_path: str2(args.file_path),
1319
+ new_string: str2(args.new_string),
1320
+ old_string: str2(args.old_string)
1321
+ }
1322
+ };
1323
+ }
1324
+ if (name === "run_shell_command") {
1325
+ return { name: "Bash", input: { command: str2(args.command) } };
1326
+ }
1327
+ return null;
1328
+ }
1329
+
1330
+ // src/adapters/opencode.ts
1331
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1332
+ import { dirname, join as join3 } from "path";
1333
+ function parseOpenCode(input) {
1334
+ const storageRoot = resolveStorageRoot(input);
1335
+ const messageRoot = join3(storageRoot, "message");
1336
+ if (!existsSync3(messageRoot)) return { summary: "", toolUses: [] };
1337
+ const session = latestSession(messageRoot);
1338
+ if (!session) return { summary: "", toolUses: [] };
1339
+ const messages = loadMessages(join3(messageRoot, session)).sort((a, b) => a.created - b.created);
1340
+ const events = [];
1341
+ for (const msg of messages) {
1342
+ const parts = loadParts(join3(storageRoot, "part", msg.id));
1343
+ if (msg.role === "user") {
1344
+ const text = parts.filter((p) => p.type === "text").map((p) => str2(p.text)).join("\n").trim();
1345
+ events.push({ role: "user", text });
1346
+ continue;
1347
+ }
1348
+ for (const part of parts) {
1349
+ if (part.type === "text" && str2(part.text).trim()) {
1350
+ events.push({ role: "assistant", text: str2(part.text) });
1351
+ } else if (part.type === "tool") {
1352
+ const tool = fromToolPart(part);
1353
+ if (tool) events.push({ role: "assistant", tool });
1354
+ }
1355
+ }
1356
+ }
1357
+ return assembleTurn(events);
1358
+ }
1359
+ function resolveStorageRoot(p) {
1360
+ try {
1361
+ if (statSync2(p).isFile()) return dirname(dirname(dirname(p)));
1362
+ } catch {
1363
+ }
1364
+ return p;
1365
+ }
1366
+ function latestSession(messageRoot) {
1367
+ let best = null;
1368
+ for (const session of listDirs(messageRoot)) {
1369
+ for (const file of listJson(join3(messageRoot, session))) {
1370
+ try {
1371
+ const mtime = statSync2(join3(messageRoot, session, file)).mtimeMs;
1372
+ if (!best || mtime > best.mtime) best = { session, mtime };
1373
+ } catch {
1374
+ }
1375
+ }
1376
+ }
1377
+ return best?.session ?? null;
1378
+ }
1379
+ function loadMessages(dir) {
1380
+ const out = [];
1381
+ for (const file of listJson(dir)) {
1382
+ const obj = readJson2(join3(dir, file));
1383
+ if (!isRecord4(obj)) continue;
1384
+ const id = str2(obj.id) || file.replace(/\.json$/, "");
1385
+ const role = str2(obj.role);
1386
+ const created = isRecord4(obj.time) && typeof obj.time.created === "number" ? obj.time.created : 0;
1387
+ out.push({ id, role, created });
1388
+ }
1389
+ return out;
1390
+ }
1391
+ function loadParts(dir) {
1392
+ if (!existsSync3(dir)) return [];
1393
+ const out = [];
1394
+ for (const file of listJson(dir)) {
1395
+ const obj = readJson2(join3(dir, file));
1396
+ if (isRecord4(obj) && typeof obj.type === "string") {
1397
+ out.push({
1398
+ type: obj.type,
1399
+ text: typeof obj.text === "string" ? obj.text : void 0,
1400
+ tool: typeof obj.tool === "string" ? obj.tool : void 0,
1401
+ state: obj.state
1402
+ });
1403
+ }
1404
+ }
1405
+ return out;
1406
+ }
1407
+ function fromToolPart(part) {
1408
+ const tool = str2(part.tool);
1409
+ const state = isRecord4(part.state) ? part.state : {};
1410
+ const input = isRecord4(state.input) ? state.input : {};
1411
+ if (tool === "write") {
1412
+ return {
1413
+ name: "Write",
1414
+ input: { file_path: str2(input.filePath), content: str2(input.content) }
1415
+ };
1416
+ }
1417
+ if (tool === "edit") {
1418
+ return {
1419
+ name: "Edit",
1420
+ input: {
1421
+ file_path: str2(input.filePath),
1422
+ new_string: str2(input.newString),
1423
+ old_string: str2(input.oldString)
1424
+ }
1425
+ };
1426
+ }
1427
+ if (tool === "bash") {
1428
+ return { name: "Bash", input: { command: str2(input.command) } };
1429
+ }
1430
+ return null;
1431
+ }
1432
+ function listDirs(dir) {
1433
+ try {
1434
+ return readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1435
+ } catch {
1436
+ return [];
1437
+ }
1438
+ }
1439
+ function listJson(dir) {
1440
+ try {
1441
+ return readdirSync2(dir).filter((n) => n.endsWith(".json"));
1442
+ } catch {
1443
+ return [];
1444
+ }
1445
+ }
1446
+ function readJson2(path) {
1447
+ try {
1448
+ return JSON.parse(readFileSync3(path, "utf8"));
1449
+ } catch {
1450
+ return null;
1451
+ }
1452
+ }
1453
+
1454
+ // src/adapters/index.ts
1455
+ var claude = {
1456
+ name: "claude",
1457
+ locate: (cwd) => findLatestTranscript(cwd),
1458
+ parse: (path) => parseTranscriptFile(path)
1459
+ };
1460
+ var codex = {
1461
+ name: "codex",
1462
+ locate() {
1463
+ const home = process.env.CODEX_HOME ?? join4(homedir2(), ".codex");
1464
+ return newestMatch(
1465
+ join4(home, "sessions"),
1466
+ (n) => n.startsWith("rollout-") && n.endsWith(".jsonl")
1467
+ );
1468
+ },
1469
+ parse: (path) => parseCodex(readFileSync4(path, "utf8"))
1470
+ };
1471
+ var gemini = {
1472
+ name: "gemini",
1473
+ locate(cwd) {
1474
+ const home = process.env.GEMINI_DIR ?? join4(homedir2(), ".gemini");
1475
+ const hash = createHash("sha256").update(cwd).digest("hex");
1476
+ const scoped = newestMatch(
1477
+ join4(home, "tmp", hash, "chats"),
1478
+ (n) => n.endsWith(".jsonl") || n.endsWith(".json")
1479
+ );
1480
+ if (scoped) return scoped;
1481
+ return newestMatch(join4(home, "tmp"), (n) => n.startsWith("session") && /\.jsonl?$/.test(n));
1482
+ },
1483
+ parse: (path) => parseGemini(readFileSync4(path, "utf8"))
1484
+ };
1485
+ var cursor = {
1486
+ name: "cursor",
1487
+ locate() {
1488
+ return newestMatch(join4(homedir2(), ".cursor", "projects"), (n) => n.endsWith(".jsonl"));
1489
+ },
1490
+ parse: (path) => parseCursor(readFileSync4(path, "utf8"))
1491
+ };
1492
+ var opencode = {
1493
+ name: "opencode",
1494
+ locate() {
1495
+ const base2 = process.env.XDG_DATA_HOME ? join4(process.env.XDG_DATA_HOME, "opencode") : join4(homedir2(), ".local", "share", "opencode");
1496
+ return newestMatch(join4(base2, "storage", "message"), (n) => n.endsWith(".json"));
1497
+ },
1498
+ parse: (path) => parseOpenCode(path)
1499
+ };
1500
+ var aider = {
1501
+ name: "aider",
1502
+ locate(cwd) {
1503
+ const file = join4(cwd, ".aider.chat.history.md");
1504
+ return existsSync4(file) ? file : null;
1505
+ },
1506
+ parse: (path) => parseAider(readFileSync4(path, "utf8"))
1507
+ };
1508
+ var ADAPTERS = {
1509
+ claude,
1510
+ codex,
1511
+ gemini,
1512
+ cursor,
1513
+ opencode,
1514
+ aider
1515
+ };
1516
+ var AGENT_NAMES = Object.keys(ADAPTERS);
1517
+ function getAdapter(name) {
1518
+ return ADAPTERS[name] ?? null;
1519
+ }
1520
+ function autoDetect(cwd) {
1521
+ let best = null;
1522
+ for (const adapter of Object.values(ADAPTERS)) {
1523
+ const path = adapter.locate(cwd);
1524
+ if (!path) continue;
1525
+ try {
1526
+ const mtime = statSync3(path).mtimeMs;
1527
+ if (!best || mtime > best.mtime) best = { adapter, path, mtime };
1528
+ } catch {
1529
+ }
1530
+ }
1531
+ return best ? { adapter: best.adapter, path: best.path } : null;
1532
+ }
1533
+ function newestMatch(dir, test, depth = 6) {
1534
+ const found = walk(dir, test, depth);
1535
+ return found?.path ?? null;
1536
+ }
1537
+ function walk(dir, test, depth) {
1538
+ if (depth < 0 || !existsSync4(dir)) return null;
1539
+ let entries;
1540
+ try {
1541
+ entries = readdirSync3(dir, { withFileTypes: true });
1542
+ } catch {
1543
+ return null;
1544
+ }
1545
+ let best = null;
1546
+ for (const entry of entries) {
1547
+ const full = join4(dir, entry.name);
1548
+ try {
1549
+ if (entry.isDirectory()) {
1550
+ const sub = walk(full, test, depth - 1);
1551
+ if (sub && (!best || sub.mtime > best.mtime)) best = sub;
1552
+ } else if (entry.isFile() && test(entry.name)) {
1553
+ const mtime = statSync3(full).mtimeMs;
1554
+ if (!best || mtime > best.mtime) best = { path: full, mtime };
1555
+ }
1556
+ } catch {
1557
+ }
1558
+ }
1559
+ return best;
1560
+ }
1561
+
1562
+ // src/ledger.ts
1563
+ import { appendFileSync, existsSync as existsSync5, mkdirSync, readFileSync as readFileSync5 } from "fs";
1564
+ import { homedir as homedir3 } from "os";
1565
+ import { dirname as dirname2, join as join5 } from "path";
1566
+ function ledgerPath() {
1567
+ return process.env.GROUNDTRUTH_LEDGER ?? join5(homedir3(), ".groundtruth", "ledger.jsonl");
1568
+ }
1569
+ function recordRun(report, cwd, session) {
1570
+ if (report.summary.total === 0) return;
1571
+ const entry = {
1572
+ t: (/* @__PURE__ */ new Date()).toISOString(),
1573
+ cwd,
1574
+ v: report.summary.verified,
1575
+ u: report.summary.unsupported,
1576
+ r: report.summary.unverifiable
1577
+ };
1578
+ if (session) entry.session = session;
1579
+ try {
1580
+ const path = ledgerPath();
1581
+ mkdirSync(dirname2(path), { recursive: true });
1582
+ appendFileSync(path, `${JSON.stringify(entry)}
1583
+ `, "utf8");
1584
+ } catch {
1585
+ }
1586
+ }
1587
+ function readLedger() {
1588
+ const path = ledgerPath();
1589
+ if (!existsSync5(path)) return [];
1590
+ const out = [];
1591
+ try {
1592
+ for (const line of readFileSync5(path, "utf8").split("\n")) {
1593
+ const trimmed = line.trim();
1594
+ if (!trimmed) continue;
1595
+ try {
1596
+ const parsed = JSON.parse(trimmed);
1597
+ if (isEntry(parsed)) out.push(parsed);
1598
+ } catch {
1599
+ }
1600
+ }
1601
+ } catch {
1602
+ }
1603
+ return out;
1604
+ }
1605
+ function summarize(entries, opts = {}) {
1606
+ const cutoff = opts.sinceDays !== void 0 ? Date.now() - opts.sinceDays * 864e5 : 0;
1607
+ const sum = { runs: 0, verified: 0, unsupported: 0, unverifiable: 0 };
1608
+ for (const e of entries) {
1609
+ if (opts.cwd && e.cwd !== opts.cwd) continue;
1610
+ if (opts.session && e.session !== opts.session) continue;
1611
+ if (cutoff && Date.parse(e.t) < cutoff) continue;
1612
+ sum.runs += 1;
1613
+ sum.verified += e.v;
1614
+ sum.unsupported += e.u;
1615
+ sum.unverifiable += e.r;
1616
+ }
1617
+ return sum;
1618
+ }
1619
+ function isEntry(v) {
1620
+ if (typeof v !== "object" || v === null) return false;
1621
+ const e = v;
1622
+ return typeof e.t === "string" && typeof e.cwd === "string" && typeof e.v === "number" && typeof e.u === "number" && typeof e.r === "number";
1623
+ }
815
1624
  export {
1625
+ ADAPTERS,
1626
+ AGENT_NAMES,
1627
+ applyConfig,
1628
+ autoDetect,
816
1629
  buildEvidence,
817
1630
  buildReport,
818
1631
  collectGitEvidence,
819
1632
  emptyEvidence,
820
1633
  extractClaims,
1634
+ failingCount,
1635
+ getAdapter,
1636
+ ledgerPath,
1637
+ loadConfig,
821
1638
  mergeEvidence,
1639
+ parseAider,
1640
+ parseCodex,
1641
+ parseCursor,
1642
+ parseGemini,
1643
+ parseOpenCode,
822
1644
  parseTranscript,
823
1645
  parseTranscriptFile,
1646
+ readLedger,
1647
+ recordRun,
824
1648
  renderJson,
825
1649
  renderMarkdown,
826
1650
  renderTerminal,
827
1651
  runPipeline,
1652
+ summarize,
828
1653
  verifyClaims
829
1654
  };