@tonyclaw/agent-inspector 2.0.29 → 2.0.31

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.
Files changed (45) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DcqxlgSQ.js → CompareDrawer-tIUf2EJm.js} +1 -1
  3. package/.output/public/assets/ProxyViewerContainer-BxRaXhKR.js +115 -0
  4. package/.output/public/assets/{ReplayDialog-CBcnPgx1.js → ReplayDialog-D6v1xcuC.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DSgSqCYt.js → RequestAnatomy-D6PniTlx.js} +1 -1
  6. package/.output/public/assets/{ResponseView-CjqzBSoF.js → ResponseView-Sx8PjuvU.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-DdBBKolI.js → StreamingChunkSequence-D-Rbfnxh.js} +1 -1
  8. package/.output/public/assets/_sessionId-BhNTuZ31.js +1 -0
  9. package/.output/public/assets/index-Byk60-jA.css +1 -0
  10. package/.output/public/assets/index-Kxptlkpg.js +1 -0
  11. package/.output/public/assets/{main-B6OLZCp9.js → main-B1kdhY98.js} +2 -2
  12. package/.output/server/_libs/lucide-react.mjs +159 -134
  13. package/.output/server/{_sessionId-CCfKJJ19.mjs → _sessionId-BceHFKf4.mjs} +3 -3
  14. package/.output/server/_ssr/{CompareDrawer-uaPI5R6e.mjs → CompareDrawer-uSxK5Ei5.mjs} +3 -3
  15. package/.output/server/_ssr/{ProxyViewerContainer-BV2nIxXy.mjs → ProxyViewerContainer-_npQ_f2i.mjs} +566 -17
  16. package/.output/server/_ssr/{ReplayDialog-eE2Hy2Nl.mjs → ReplayDialog-BHAzSLCT.mjs} +4 -4
  17. package/.output/server/_ssr/{RequestAnatomy-B7vwWo57.mjs → RequestAnatomy-DjlZLXBd.mjs} +3 -3
  18. package/.output/server/_ssr/{ResponseView-Bm6Qp8mo.mjs → ResponseView-BuDEJ3fl.mjs} +3 -3
  19. package/.output/server/_ssr/{StreamingChunkSequence-CbAJKJQx.mjs → StreamingChunkSequence-DFcu0bD8.mjs} +3 -3
  20. package/.output/server/_ssr/{index-u14_Aj60.mjs → index-Bw6Fw33c.mjs} +2 -2
  21. package/.output/server/_ssr/index.mjs +2 -2
  22. package/.output/server/_ssr/{router-qgeGUp1k.mjs → router-D90tMCb3.mjs} +1236 -143
  23. package/.output/server/_tanstack-start-manifest_v-BVM4AUlx.mjs +4 -0
  24. package/.output/server/index.mjs +60 -60
  25. package/README.md +24 -1
  26. package/package.json +1 -1
  27. package/src/components/ProxyViewer.tsx +9 -2
  28. package/src/components/groups/GroupsDialog.tsx +732 -0
  29. package/src/lib/groupContract.ts +148 -0
  30. package/src/lib/runContract.ts +2 -0
  31. package/src/lib/useGroupEvidence.ts +33 -0
  32. package/src/lib/useGroups.ts +28 -0
  33. package/src/mcp/server.ts +282 -1
  34. package/src/mcp/toolHandlers.ts +123 -0
  35. package/src/proxy/groupEvidenceExporter.ts +354 -0
  36. package/src/proxy/groupStore.ts +259 -0
  37. package/src/routes/api/groups.$groupId.evidence.ts +74 -0
  38. package/src/routes/api/groups.$groupId.sessions.ts +50 -0
  39. package/src/routes/api/groups.$groupId.ts +50 -0
  40. package/src/routes/api/groups.ts +39 -0
  41. package/.output/public/assets/ProxyViewerContainer-D650kQlv.js +0 -115
  42. package/.output/public/assets/_sessionId-wctOoGh_.js +0 -1
  43. package/.output/public/assets/index-BhQGSdhG.js +0 -1
  44. package/.output/public/assets/index-CwlHPmgL.css +0 -1
  45. package/.output/server/_tanstack-start-manifest_v-BrfUXnre.mjs +0 -4
@@ -17,6 +17,14 @@ import { Buffer } from "node:buffer";
17
17
  import { z } from "zod";
18
18
  import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
19
19
  import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
20
+ import {
21
+ GroupEvidenceExportResultSchema,
22
+ InspectorGroupSchema,
23
+ InspectorGroupsListResponseSchema,
24
+ type AddInspectorGroupSessionInput,
25
+ type CreateInspectorGroupInput,
26
+ type UpdateInspectorGroupInput,
27
+ } from "../lib/groupContract";
20
28
  import {
21
29
  EvidenceExportResultSchema,
22
30
  InspectorRunSchema,
@@ -375,6 +383,121 @@ export async function exportEvidenceImpl(
375
383
  return textJson(parsed.data);
376
384
  }
377
385
 
386
+ export async function listGroupsImpl(callApi: CallApiFn): Promise<ToolResult> {
387
+ const res = await callApi("/api/groups");
388
+ if (!res.ok) return toolError(`GET /api/groups returned ${res.status}`);
389
+ const rawBody: unknown = await res.json();
390
+ const parsed = InspectorGroupsListResponseSchema.safeParse(rawBody);
391
+ if (!parsed.success) return toolError("GET /api/groups returned an unparseable group list");
392
+ return textJson(parsed.data);
393
+ }
394
+
395
+ export async function createGroupImpl(
396
+ callApi: CallApiFn,
397
+ args: CreateInspectorGroupInput,
398
+ ): Promise<ToolResult> {
399
+ const res = await callApi("/api/groups", {
400
+ method: "POST",
401
+ headers: { "content-type": "application/json" },
402
+ body: JSON.stringify(args ?? {}),
403
+ });
404
+ if (!res.ok) return toolError(`POST /api/groups returned ${res.status}`);
405
+ const rawBody: unknown = await res.json();
406
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
407
+ if (!parsed.success) return toolError("POST /api/groups returned an unparseable group");
408
+ return textJson(parsed.data);
409
+ }
410
+
411
+ export type GetGroupArgs = {
412
+ groupId: string;
413
+ };
414
+
415
+ export async function getGroupImpl(callApi: CallApiFn, args: GetGroupArgs): Promise<ToolResult> {
416
+ const path = `/api/groups/${encodeURIComponent(args.groupId)}`;
417
+ const res = await callApi(path);
418
+ if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
419
+ const rawBody: unknown = await res.json();
420
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
421
+ if (!parsed.success) return toolError("GET /api/groups/{groupId} returned an unparseable group");
422
+ return textJson(parsed.data);
423
+ }
424
+
425
+ export type UpdateGroupArgs = {
426
+ groupId: string;
427
+ } & UpdateInspectorGroupInput;
428
+
429
+ export async function updateGroupImpl(
430
+ callApi: CallApiFn,
431
+ args: UpdateGroupArgs,
432
+ ): Promise<ToolResult> {
433
+ const { groupId, ...patch } = args;
434
+ const path = `/api/groups/${encodeURIComponent(groupId)}`;
435
+ const res = await callApi(path, {
436
+ method: "PATCH",
437
+ headers: { "content-type": "application/json" },
438
+ body: JSON.stringify(patch),
439
+ });
440
+ if (!res.ok) return toolError(`PATCH ${path} returned ${res.status}`);
441
+ const rawBody: unknown = await res.json();
442
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
443
+ if (!parsed.success) {
444
+ return toolError("PATCH /api/groups/{groupId} returned an unparseable group");
445
+ }
446
+ return textJson(parsed.data);
447
+ }
448
+
449
+ export type AddGroupSessionArgs = {
450
+ groupId: string;
451
+ } & AddInspectorGroupSessionInput;
452
+
453
+ export async function addGroupSessionImpl(
454
+ callApi: CallApiFn,
455
+ args: AddGroupSessionArgs,
456
+ ): Promise<ToolResult> {
457
+ const { groupId, ...session } = args;
458
+ const path = `/api/groups/${encodeURIComponent(groupId)}/sessions`;
459
+ const res = await callApi(path, {
460
+ method: "POST",
461
+ headers: { "content-type": "application/json" },
462
+ body: JSON.stringify(session),
463
+ });
464
+ if (!res.ok) return toolError(`POST ${path} returned ${res.status}`);
465
+ const rawBody: unknown = await res.json();
466
+ const parsed = InspectorGroupSchema.safeParse(rawBody);
467
+ if (!parsed.success) {
468
+ return toolError("POST /api/groups/{groupId}/sessions returned an unparseable group");
469
+ }
470
+ return textJson(parsed.data);
471
+ }
472
+
473
+ export type ExportGroupEvidenceArgs = {
474
+ groupId: string;
475
+ includeHistory?: boolean;
476
+ latestLogLimit?: number;
477
+ };
478
+
479
+ export async function exportGroupEvidenceImpl(
480
+ callApi: CallApiFn,
481
+ args: ExportGroupEvidenceArgs,
482
+ ): Promise<ToolResult> {
483
+ const path = `/api/groups/${encodeURIComponent(args.groupId)}/evidence`;
484
+ const res = await callApi(path, {
485
+ method: "POST",
486
+ headers: { "content-type": "application/json" },
487
+ body: JSON.stringify({
488
+ includeHistory: args.includeHistory,
489
+ latestLogLimit: args.latestLogLimit,
490
+ }),
491
+ });
492
+ if (!res.ok) return toolError(`POST ${path} returned ${res.status}`);
493
+ const rawBody: unknown = await res.json();
494
+ const parsed = GroupEvidenceExportResultSchema.safeParse(rawBody);
495
+ if (!parsed.success) {
496
+ return toolError("POST /api/groups/{groupId}/evidence returned an unparseable evidence result");
497
+ }
498
+ return textJson(parsed.data);
499
+ }
500
+
378
501
  export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
379
502
  const res = await callApi("/api/models");
380
503
  if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
@@ -0,0 +1,354 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ GroupEvidenceExportResultSchema,
5
+ type GroupEvidenceExportOptions,
6
+ type GroupEvidenceExportResult,
7
+ type InspectorGroup,
8
+ type InspectorGroupEvidence,
9
+ type InspectorGroupEvidenceSummary,
10
+ type InspectorGroupExportMember,
11
+ } from "../lib/groupContract";
12
+ import type { JenkinsReport } from "../lib/runContract";
13
+ import type { SessionInfo, SessionTokenUsage } from "../lib/sessionInfoContract";
14
+ import { getDataDir } from "./dataDir";
15
+ import { getRun } from "./runStore";
16
+ import { getSessionInfo } from "./store";
17
+ import { updateGroupEvidence } from "./groupStore";
18
+
19
+ type GroupEvidenceDocument = GroupEvidenceExportResult;
20
+
21
+ function groupEvidenceDir(groupId: string): string {
22
+ return join(getDataDir(), "evidence", "groups", groupId);
23
+ }
24
+
25
+ function jsonPath(groupId: string): string {
26
+ return join(groupEvidenceDir(groupId), "evidence.json");
27
+ }
28
+
29
+ function markdownPath(groupId: string): string {
30
+ return join(groupEvidenceDir(groupId), "evidence.md");
31
+ }
32
+
33
+ function htmlPath(groupId: string): string {
34
+ return join(groupEvidenceDir(groupId), "evidence.html");
35
+ }
36
+
37
+ function escapeMarkdownCell(value: string): string {
38
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
39
+ }
40
+
41
+ function escapeHtml(value: string): string {
42
+ return value
43
+ .replace(/&/g, "&amp;")
44
+ .replace(/</g, "&lt;")
45
+ .replace(/>/g, "&gt;")
46
+ .replace(/"/g, "&quot;");
47
+ }
48
+
49
+ function formatText(value: string | null | undefined): string {
50
+ if (value === null || value === undefined || value.length === 0) return "n/a";
51
+ return value;
52
+ }
53
+
54
+ function formatNumber(value: number | null | undefined): string {
55
+ if (value === null || value === undefined) return "n/a";
56
+ return String(value);
57
+ }
58
+
59
+ function collectSortedValues(values: readonly (string | null | undefined)[]): string[] {
60
+ const set = new Set<string>();
61
+ for (const value of values) {
62
+ if (value !== null && value !== undefined && value.length > 0) {
63
+ set.add(value);
64
+ }
65
+ }
66
+ return [...set].sort();
67
+ }
68
+
69
+ function addTokenUsage(total: SessionTokenUsage, session: SessionInfo | null): SessionTokenUsage {
70
+ if (session === null) return total;
71
+ return {
72
+ input: total.input + session.tokenUsage.input,
73
+ output: total.output + session.tokenUsage.output,
74
+ cacheCreate: total.cacheCreate + session.tokenUsage.cacheCreate,
75
+ cacheRead: total.cacheRead + session.tokenUsage.cacheRead,
76
+ total: total.total + session.tokenUsage.total,
77
+ };
78
+ }
79
+
80
+ function memberEffectiveStatus(member: InspectorGroupExportMember): string {
81
+ if (member.member.status !== null) return member.member.status;
82
+ if (member.run !== null) return member.run.status;
83
+ return member.session?.status ?? "unknown";
84
+ }
85
+
86
+ function isCompletedMember(member: InspectorGroupExportMember): boolean {
87
+ const status = memberEffectiveStatus(member);
88
+ return status === "completed";
89
+ }
90
+
91
+ function isFailedMember(member: InspectorGroupExportMember): boolean {
92
+ const status = memberEffectiveStatus(member);
93
+ return status === "failed" || status === "cancelled";
94
+ }
95
+
96
+ function isRunningMember(member: InspectorGroupExportMember): boolean {
97
+ const status = memberEffectiveStatus(member);
98
+ return status === "running" || status === "active";
99
+ }
100
+
101
+ function latestLogMetric(
102
+ session: SessionInfo | null,
103
+ field: "firstChunkMs" | "totalStreamMs" | "tokensPerSecond",
104
+ ): number | null {
105
+ if (session === null) return null;
106
+ for (const log of session.latestLogs) {
107
+ const value = log[field];
108
+ if (value !== undefined && value !== null) return value;
109
+ }
110
+ return null;
111
+ }
112
+
113
+ function buildSummary(
114
+ members: readonly InspectorGroupExportMember[],
115
+ ): InspectorGroupEvidenceSummary {
116
+ const tokenUsage = members.reduce<SessionTokenUsage>(
117
+ (total, member) => addTokenUsage(total, member.session),
118
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, total: 0 },
119
+ );
120
+ const sessionsWithData = members.filter((member) => member.session !== null).length;
121
+ const requestCount = members.reduce(
122
+ (total, member) => total + (member.session?.requestCount ?? 0),
123
+ 0,
124
+ );
125
+ const errorCount = members.reduce(
126
+ (total, member) => total + (member.session?.errorCount ?? 0),
127
+ 0,
128
+ );
129
+
130
+ return {
131
+ memberCount: members.length,
132
+ sessionsWithData,
133
+ missingSessions: members.length - sessionsWithData,
134
+ requestCount,
135
+ errorCount,
136
+ tokenUsage,
137
+ models: collectSortedValues(
138
+ members.flatMap((member) => [member.member.model, ...(member.session?.models ?? [])]),
139
+ ),
140
+ providers: collectSortedValues(
141
+ members.flatMap((member) => [member.member.provider, ...(member.session?.providers ?? [])]),
142
+ ),
143
+ completedMembers: members.filter(isCompletedMember).length,
144
+ failedMembers: members.filter(isFailedMember).length,
145
+ runningMembers: members.filter(isRunningMember).length,
146
+ };
147
+ }
148
+
149
+ function buildJenkinsReport(
150
+ group: InspectorGroup,
151
+ summary: InspectorGroupEvidenceSummary,
152
+ ): JenkinsReport {
153
+ if (summary.failedMembers > 0 || summary.errorCount > 0 || group.status === "failed") {
154
+ return {
155
+ status: "FAIL",
156
+ summary: `[FAIL] ${summary.failedMembers} failed members, ${summary.errorCount} captured errors.`,
157
+ markdown: `# Jenkins Report: ${group.title}\n\nStatus: FAIL\n\n${summary.failedMembers} failed members and ${summary.errorCount} captured errors need triage.`,
158
+ };
159
+ }
160
+
161
+ if (summary.runningMembers > 0 || group.status === "running") {
162
+ return {
163
+ status: "RUNNING",
164
+ summary: `[RUNNING] ${summary.runningMembers} group members are still active.`,
165
+ markdown: `# Jenkins Report: ${group.title}\n\nStatus: RUNNING\n\n${summary.runningMembers} group members are still active.`,
166
+ };
167
+ }
168
+
169
+ if (
170
+ summary.memberCount > 0 &&
171
+ summary.sessionsWithData > 0 &&
172
+ summary.errorCount === 0 &&
173
+ group.status === "completed"
174
+ ) {
175
+ return {
176
+ status: "PASS",
177
+ summary: `[PASS] ${summary.memberCount} group members completed with no captured errors.`,
178
+ markdown: `# Jenkins Report: ${group.title}\n\nStatus: PASS\n\n${summary.memberCount} group members completed with no captured errors.`,
179
+ };
180
+ }
181
+
182
+ return {
183
+ status: "UNKNOWN",
184
+ summary: `[UNKNOWN] Group has ${summary.memberCount} members and ${summary.missingSessions} missing sessions.`,
185
+ markdown: `# Jenkins Report: ${group.title}\n\nStatus: UNKNOWN\n\nExported group evidence, but the outcome is not conclusive yet.`,
186
+ };
187
+ }
188
+
189
+ function buildMemberRows(members: readonly InspectorGroupExportMember[]): string[] {
190
+ if (members.length === 0) return ["No sessions have been attached to this group.", ""];
191
+
192
+ const rows = [
193
+ "| Label | Provider | Model | Session | Run | Status | Requests | Errors | Tokens | First Chunk | Stream Total | TPS | Inspector |",
194
+ "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
195
+ ];
196
+
197
+ for (const member of members) {
198
+ const session = member.session;
199
+ rows.push(
200
+ [
201
+ formatText(member.member.label),
202
+ formatText(member.member.provider ?? session?.providers[0]),
203
+ formatText(member.member.model ?? session?.lastModel),
204
+ member.member.sessionId,
205
+ formatText(member.member.runId),
206
+ memberEffectiveStatus(member),
207
+ formatNumber(session?.requestCount),
208
+ formatNumber(session?.errorCount),
209
+ formatNumber(session?.tokenUsage.total),
210
+ formatNumber(latestLogMetric(session, "firstChunkMs")),
211
+ formatNumber(latestLogMetric(session, "totalStreamMs")),
212
+ formatNumber(latestLogMetric(session, "tokensPerSecond")),
213
+ formatText(session?.inspectorUrl),
214
+ ]
215
+ .map(escapeMarkdownCell)
216
+ .join(" | ")
217
+ .replace(/^/, "| ")
218
+ .replace(/$/, " |"),
219
+ );
220
+ }
221
+
222
+ rows.push("");
223
+ return rows;
224
+ }
225
+
226
+ function buildMarkdown(document: GroupEvidenceDocument): string {
227
+ const group = document.group;
228
+ const summary = document.summary;
229
+ const lines = [
230
+ `# ${group.title}`,
231
+ "",
232
+ `- Group ID: ${group.id}`,
233
+ `- Kind: ${group.kind}`,
234
+ `- Status: ${group.status}`,
235
+ `- Project: ${group.project ?? "n/a"}`,
236
+ `- Created: ${group.createdAt}`,
237
+ `- Members: ${summary.memberCount}`,
238
+ `- Sessions With Data: ${summary.sessionsWithData}`,
239
+ `- Missing Sessions: ${summary.missingSessions}`,
240
+ `- Requests: ${summary.requestCount}`,
241
+ `- Errors: ${summary.errorCount}`,
242
+ `- Tokens: ${summary.tokenUsage.total}`,
243
+ `- Models: ${summary.models.length > 0 ? summary.models.join(", ") : "n/a"}`,
244
+ `- Providers: ${summary.providers.length > 0 ? summary.providers.join(", ") : "n/a"}`,
245
+ `- Evidence JSON: ${document.evidence.jsonPath}`,
246
+ `- Evidence HTML: ${document.evidence.htmlPath}`,
247
+ "",
248
+ ];
249
+
250
+ if (group.task !== null) {
251
+ lines.push("## Task", "", group.task, "");
252
+ }
253
+
254
+ lines.push("## Evaluation Matrix", "", ...buildMemberRows(document.members));
255
+ lines.push("## Jenkins Report", "", document.jenkinsReport.markdown, "");
256
+
257
+ return `${lines.join("\n")}\n`;
258
+ }
259
+
260
+ function buildHtml(markdown: string, title: string): string {
261
+ return `<!doctype html>
262
+ <html lang="en">
263
+ <head>
264
+ <meta charset="utf-8">
265
+ <title>${escapeHtml(title)}</title>
266
+ <style>
267
+ body { font-family: system-ui, sans-serif; max-width: 1120px; margin: 40px auto; line-height: 1.5; }
268
+ pre { white-space: pre-wrap; background: #f6f8fa; padding: 16px; border-radius: 8px; }
269
+ </style>
270
+ </head>
271
+ <body>
272
+ <pre>${escapeHtml(markdown)}</pre>
273
+ </body>
274
+ </html>
275
+ `;
276
+ }
277
+
278
+ async function buildExportMembers(
279
+ group: InspectorGroup,
280
+ options: GroupEvidenceExportOptions,
281
+ baseUrl: string,
282
+ ): Promise<InspectorGroupExportMember[]> {
283
+ const members: InspectorGroupExportMember[] = [];
284
+ for (const member of group.members) {
285
+ const run = member.runId === null ? null : getRun(member.runId);
286
+ const session = await getSessionInfo(member.sessionId, {
287
+ baseUrl,
288
+ includeHistory: options?.includeHistory,
289
+ latestLogLimit: options?.latestLogLimit,
290
+ });
291
+ members.push({ member, run, session });
292
+ }
293
+ return members;
294
+ }
295
+
296
+ export async function exportGroupEvidence(
297
+ group: InspectorGroup,
298
+ options: GroupEvidenceExportOptions,
299
+ baseUrl: string,
300
+ ): Promise<GroupEvidenceExportResult> {
301
+ const members = await buildExportMembers(group, options, baseUrl);
302
+ const exportedAt = new Date().toISOString();
303
+ const evidence: InspectorGroupEvidence = {
304
+ jsonPath: jsonPath(group.id),
305
+ markdownPath: markdownPath(group.id),
306
+ htmlPath: htmlPath(group.id),
307
+ exportedAt,
308
+ };
309
+ const documentGroup: InspectorGroup = {
310
+ ...group,
311
+ evidence,
312
+ updatedAt: exportedAt,
313
+ };
314
+ const summary = buildSummary(members);
315
+ const jenkinsReport = buildJenkinsReport(documentGroup, summary);
316
+ const document: GroupEvidenceDocument = {
317
+ group: documentGroup,
318
+ evidence,
319
+ members,
320
+ summary,
321
+ jenkinsReport,
322
+ };
323
+ const markdown = buildMarkdown(document);
324
+
325
+ mkdirSync(groupEvidenceDir(group.id), { recursive: true });
326
+ writeFileSync(evidence.jsonPath, `${JSON.stringify(document, null, 2)}\n`, "utf8");
327
+ writeFileSync(evidence.markdownPath, markdown, "utf8");
328
+ writeFileSync(evidence.htmlPath, buildHtml(markdown, group.title), "utf8");
329
+
330
+ const updatedGroup = updateGroupEvidence(group.id, evidence) ?? documentGroup;
331
+ return { ...document, group: updatedGroup };
332
+ }
333
+
334
+ export function readGroupEvidenceMarkdown(group: InspectorGroup): string | null {
335
+ const path = group.evidence?.markdownPath;
336
+ if (path === undefined || !existsSync(path)) return null;
337
+ try {
338
+ return readFileSync(path, "utf8");
339
+ } catch {
340
+ return null;
341
+ }
342
+ }
343
+
344
+ export function readGroupEvidenceDocument(group: InspectorGroup): GroupEvidenceExportResult | null {
345
+ const path = group.evidence?.jsonPath;
346
+ if (path === undefined || !existsSync(path)) return null;
347
+ try {
348
+ const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
349
+ const parsed = GroupEvidenceExportResultSchema.safeParse(raw);
350
+ return parsed.success ? parsed.data : null;
351
+ } catch {
352
+ return null;
353
+ }
354
+ }