@tangle-network/traces 0.23.1 → 1.0.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.
Files changed (75) hide show
  1. package/README.md +61 -1078
  2. package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
  3. package/dist/ask-2KF4KSPB.js +39 -0
  4. package/dist/check-YF6ZHPGG.js +322 -0
  5. package/dist/chunk-2XXABIRL.js +25 -0
  6. package/dist/chunk-4VKPN432.js +138 -0
  7. package/dist/chunk-6GOG5KIK.js +53 -0
  8. package/dist/chunk-7USMM4MA.js +5695 -0
  9. package/dist/chunk-CG5QHQO4.js +492 -0
  10. package/dist/chunk-CQ6UYZG6.js +1217 -0
  11. package/dist/chunk-DU534HAX.js +263 -0
  12. package/dist/chunk-FC4V25CG.js +503 -0
  13. package/dist/chunk-GLUDGUGJ.js +232 -0
  14. package/dist/chunk-GNRXKQEQ.js +120 -0
  15. package/dist/chunk-K2KH2HHD.js +228 -0
  16. package/dist/chunk-M3W6TZHQ.js +381 -0
  17. package/dist/chunk-O52XK7OB.js +19 -0
  18. package/dist/chunk-PYH3PL3N.js +62 -0
  19. package/dist/chunk-QLVP37YB.js +782 -0
  20. package/dist/chunk-QUHUMZ4W.js +759 -0
  21. package/dist/chunk-REO2JQJY.js +535 -0
  22. package/dist/chunk-RZL5GAWH.js +60 -0
  23. package/dist/chunk-TOCNGML5.js +273 -0
  24. package/dist/chunk-UQSAGXPH.js +208 -0
  25. package/dist/chunk-VFOLC4OX.js +407 -0
  26. package/dist/chunk-WF7PTDCZ.js +3921 -0
  27. package/dist/chunk-YAX72TST.js +259 -0
  28. package/dist/cli.js +2086 -2050
  29. package/dist/diff-J4RAIOXO.js +16 -0
  30. package/dist/improvement-LCCVGGUS.js +25 -0
  31. package/dist/index.d.ts +1235 -4357
  32. package/dist/index.js +53 -1204
  33. package/dist/live-XXL7VZWB.js +14 -0
  34. package/dist/mcp-EYIDKBEK.js +85 -0
  35. package/dist/report-E3G2T5TJ.js +338 -0
  36. package/dist/run-watch-RAOOAXOF.js +649 -0
  37. package/dist/supervisor-report-RMPLLBGZ.js +29 -0
  38. package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
  39. package/dist/upload-3R7GRSKS.js +20 -0
  40. package/dist/verify-safe-C5ZXZOZ2.js +8 -0
  41. package/docs/analyze.md +76 -0
  42. package/docs/ask.md +88 -0
  43. package/docs/bundle.md +40 -0
  44. package/docs/check.md +77 -0
  45. package/docs/convert.md +44 -0
  46. package/docs/develop.md +32 -0
  47. package/docs/diff.md +38 -0
  48. package/docs/facts.md +114 -0
  49. package/docs/mcp.md +36 -0
  50. package/docs/migrate-1.0.md +33 -0
  51. package/docs/parsers.md +66 -0
  52. package/docs/release-1.0.0.md +147 -0
  53. package/docs/release-1.0.1.md +4 -0
  54. package/docs/sdk.md +77 -0
  55. package/docs/select.md +70 -0
  56. package/docs/skills.md +46 -0
  57. package/docs/upload.md +47 -0
  58. package/docs/watch-jsonl.v1.example.jsonl +5 -0
  59. package/docs/watch.md +66 -0
  60. package/examples/README.md +9 -0
  61. package/examples/custom-analyst.ts +65 -74
  62. package/install.sh +22 -2
  63. package/package.json +10 -9
  64. package/skills/inspect-agent-traces/SKILL.md +47 -110
  65. package/dist/chunk-7DVZCCZI.js +0 -20983
  66. package/docs/replay-verify.md +0 -123
  67. package/docs/stream-findings.svg +0 -39
  68. package/docs/trace-analysts.md +0 -643
  69. package/examples/custom-backend.ts +0 -21
  70. package/examples/external-engines.ts +0 -62
  71. package/examples/improvement-config.mjs +0 -63
  72. package/examples/observe-and-alert.ts +0 -19
  73. package/examples/register-harness.ts +0 -26
  74. package/skills/build-trace-analyst/SKILL.md +0 -76
  75. package/skills/build-trace-analyst/agents/openai.yaml +0 -4
@@ -0,0 +1,381 @@
1
+ // src/supervisor-run-context.ts
2
+ import { readFile, stat } from "fs/promises";
3
+ import { basename, join } from "path";
4
+ import { materializeTreeView } from "@tangle-network/agent-runtime/kernel";
5
+ import {
6
+ readRuntimeSupervisorRun
7
+ } from "@tangle-network/agent-eval/supervisor-run";
8
+ var SPAWN_JOURNAL_FILE = "spawn-journal.jsonl";
9
+ var COORDINATION_LOG_FILE = "coordination-log.jsonl";
10
+ var RESULT_FILE = "result.json";
11
+ async function readMaybe(path) {
12
+ return readFile(path, "utf8").catch(() => null);
13
+ }
14
+ function readJsonlLines(text) {
15
+ if (text === null) {
16
+ return { rows: [], invalidRows: 0, partialTail: false, present: false };
17
+ }
18
+ const lines = text.split("\n");
19
+ const partialTail = text.length > 0 && !text.endsWith("\n");
20
+ if (partialTail) lines.pop();
21
+ const rows = [];
22
+ let invalidRows = 0;
23
+ for (const line of lines) {
24
+ const trimmed = line.trim();
25
+ if (trimmed.length === 0) continue;
26
+ try {
27
+ const parsed = JSON.parse(trimmed);
28
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
29
+ rows.push(parsed);
30
+ } else invalidRows += 1;
31
+ } catch {
32
+ invalidRows += 1;
33
+ }
34
+ }
35
+ return { rows, invalidRows, partialTail, present: true };
36
+ }
37
+ function asRecord(v) {
38
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
39
+ }
40
+ function str(v) {
41
+ return typeof v === "string" && v.length > 0 ? v : null;
42
+ }
43
+ var RETIRED_WAIT_KINDS = /* @__PURE__ */ new Set(["waiting", "woken"]);
44
+ function readRunContextJournal(text) {
45
+ const parsed = readJsonlLines(text);
46
+ let root = null;
47
+ let begunAt = null;
48
+ const events = [];
49
+ let invalidRows = parsed.invalidRows;
50
+ let retiredWaitRows = 0;
51
+ for (const record of parsed.rows) {
52
+ const recordRoot = str(record.root);
53
+ if (root === null && recordRoot !== null) root = recordRoot;
54
+ if (record.kind === "begin") {
55
+ if (begunAt === null) begunAt = str(record.at);
56
+ continue;
57
+ }
58
+ if (record.kind !== "event") {
59
+ invalidRows += 1;
60
+ continue;
61
+ }
62
+ const event = asRecord(record.event);
63
+ if (str(event.kind) === null || str(event.id) === null) {
64
+ invalidRows += 1;
65
+ continue;
66
+ }
67
+ if (RETIRED_WAIT_KINDS.has(event.kind)) {
68
+ retiredWaitRows += 1;
69
+ continue;
70
+ }
71
+ events.push(event);
72
+ }
73
+ const supervisorIds = /* @__PURE__ */ new Set();
74
+ for (const event of events) {
75
+ if (event.kind !== "spawned") continue;
76
+ const parent = event.parent;
77
+ if (typeof parent === "string" && parent.length > 0) supervisorIds.add(parent);
78
+ }
79
+ return {
80
+ root,
81
+ begunAt,
82
+ events,
83
+ supervisorIds,
84
+ invalidRows,
85
+ retiredWaitRows,
86
+ partialTail: parsed.partialTail,
87
+ present: parsed.present
88
+ };
89
+ }
90
+ function inferRole(id, parent, supervisorIds) {
91
+ if (parent === null || parent === void 0) return "supervisor";
92
+ return supervisorIds.has(id) ? "supervisor" : "worker";
93
+ }
94
+ var EMPTY_COORDINATION = {
95
+ present: false,
96
+ steers: [],
97
+ deliveries: [],
98
+ questions: [],
99
+ invalidRows: 0,
100
+ partialTail: false
101
+ };
102
+ function readCoordinationLog(text) {
103
+ const parsed = readJsonlLines(text);
104
+ if (!parsed.present) return EMPTY_COORDINATION;
105
+ const steers = [];
106
+ const deliveries = [];
107
+ const questions = [];
108
+ let invalidRows = parsed.invalidRows;
109
+ for (const row of parsed.rows) {
110
+ const record = asRecord(row.record);
111
+ const event = asRecord(Object.keys(record).length > 0 ? record.event : row.event);
112
+ const at = typeof record.at === "number" ? record.at : parseInstant(row.at);
113
+ switch (event.type) {
114
+ case "steer": {
115
+ const down = asRecord(event.down);
116
+ steers.push({
117
+ messageId: str(down.messageId),
118
+ toWorker: str(down.toWorker),
119
+ instruction: typeof down.instruction === "string" ? down.instruction : "",
120
+ deliveryMode: str(down.deliveryMode),
121
+ at
122
+ });
123
+ break;
124
+ }
125
+ case "delivery": {
126
+ const receipt = asRecord(event.receipt);
127
+ deliveries.push({
128
+ messageId: str(receipt.messageId),
129
+ delivered: typeof receipt.delivered === "boolean" ? receipt.delivered : null
130
+ });
131
+ break;
132
+ }
133
+ case "question": {
134
+ const q = asRecord(event.question);
135
+ const urgency = str(q.urgency);
136
+ questions.push({
137
+ id: str(q.id),
138
+ from: str(q.from),
139
+ question: typeof q.question === "string" ? q.question : "",
140
+ reason: str(q.reason),
141
+ urgency,
142
+ status: str(q.status),
143
+ blocking: urgency === "blocks-run",
144
+ at: typeof q.openedAt === "number" ? q.openedAt : at
145
+ });
146
+ break;
147
+ }
148
+ case "settled":
149
+ break;
150
+ default:
151
+ if (str(event.type) === null) invalidRows += 1;
152
+ }
153
+ }
154
+ return {
155
+ present: true,
156
+ steers,
157
+ deliveries,
158
+ questions,
159
+ invalidRows,
160
+ partialTail: parsed.partialTail
161
+ };
162
+ }
163
+ function parseInstant(v) {
164
+ if (typeof v === "number" && Number.isFinite(v)) return v;
165
+ if (typeof v !== "string") return null;
166
+ const t = Date.parse(v);
167
+ return Number.isFinite(t) ? t : null;
168
+ }
169
+ async function readFileRunContext(runDir, opts = {}) {
170
+ const sources = await readRuntimeSupervisorRun(runDir);
171
+ const coordination = readCoordinationLog(await readMaybe(join(runDir, COORDINATION_LOG_FILE)));
172
+ return {
173
+ ...sources,
174
+ instanceId: opts.instanceId !== void 0 ? opts.instanceId : sources.instanceId,
175
+ arm: opts.arm !== void 0 ? opts.arm : basename(runDir),
176
+ workers: sources.workers !== null && coordination.present ? workerSources(sources.workers, coordination) : sources.workers,
177
+ ...sources.journal !== null ? {
178
+ rootTranscriptRef: join(runDir, SPAWN_JOURNAL_FILE),
179
+ traceCommand: `traces watch ${runDir}`
180
+ } : {}
181
+ };
182
+ }
183
+ function fileRunContextSupervisorRunReader(runDir, opts = {}) {
184
+ return { runRef: runDir, read: () => readFileRunContext(runDir, opts) };
185
+ }
186
+ async function isFileRunContextDir(runDir) {
187
+ return stat(join(runDir, SPAWN_JOURNAL_FILE)).then((s) => s.isFile()).catch(() => false);
188
+ }
189
+ function parseJsonObject(text) {
190
+ if (text === null) return null;
191
+ try {
192
+ const parsed = JSON.parse(text);
193
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+ function workerSources(workers, coordination) {
199
+ const deliveredById = /* @__PURE__ */ new Map();
200
+ for (const receipt of coordination.deliveries) {
201
+ if (receipt.messageId !== null) deliveredById.set(receipt.messageId, receipt.delivered);
202
+ }
203
+ return workers.map((worker) => {
204
+ const steers = coordination.steers.filter((steer) => steer.toWorker === worker.workerId);
205
+ const questions = coordination.questions.filter((question) => question.from === worker.workerId);
206
+ return {
207
+ ...worker,
208
+ events: [
209
+ ...steers.map((steer) => JSON.stringify({
210
+ kind: "message",
211
+ requestId: steer.messageId,
212
+ delivered: steer.messageId === null ? null : deliveredById.get(steer.messageId) ?? null
213
+ })),
214
+ ...questions.map((question) => JSON.stringify({ kind: "message", direction: "up", id: question.id }))
215
+ ].join("\n"),
216
+ inbox: steers.map((steer) => JSON.stringify({ id: steer.messageId, message: steer.instruction })).join("\n")
217
+ };
218
+ });
219
+ }
220
+ var ZERO_ROLL = {
221
+ records: 0,
222
+ iterations: 0,
223
+ tokensIn: 0,
224
+ tokensOut: 0,
225
+ usd: 0,
226
+ usdKnownRecords: 0,
227
+ usdUnknownRecords: 0
228
+ };
229
+ function addSpend(roll, spend) {
230
+ const known = spend.usdKnown !== false;
231
+ return {
232
+ records: roll.records + 1,
233
+ iterations: roll.iterations + (spend.iterations ?? 0),
234
+ tokensIn: roll.tokensIn + (spend.tokens?.input ?? 0),
235
+ tokensOut: roll.tokensOut + (spend.tokens?.output ?? 0),
236
+ usd: roll.usd + (known ? spend.usd ?? 0 : 0),
237
+ usdKnownRecords: roll.usdKnownRecords + (known ? 1 : 0),
238
+ usdUnknownRecords: roll.usdUnknownRecords + (known ? 0 : 1)
239
+ };
240
+ }
241
+ function mergeRolls(a, b) {
242
+ return {
243
+ records: a.records + b.records,
244
+ iterations: a.iterations + b.iterations,
245
+ tokensIn: a.tokensIn + b.tokensIn,
246
+ tokensOut: a.tokensOut + b.tokensOut,
247
+ usd: a.usd + b.usd,
248
+ usdKnownRecords: a.usdKnownRecords + b.usdKnownRecords,
249
+ usdUnknownRecords: a.usdUnknownRecords + b.usdUnknownRecords
250
+ };
251
+ }
252
+ async function readRunContextSnapshot(runDir) {
253
+ const journal = readRunContextJournal(await readMaybe(join(runDir, SPAWN_JOURNAL_FILE)));
254
+ const coordination = readCoordinationLog(await readMaybe(join(runDir, COORDINATION_LOG_FILE)));
255
+ const resultText = await readMaybe(join(runDir, RESULT_FILE));
256
+ const result = parseResult(resultText);
257
+ const view = safeTreeView(journal.events);
258
+ const nodes = view === null ? [] : orderNodes(view, journal);
259
+ return {
260
+ runDir,
261
+ runId: journal.root,
262
+ present: journal.present,
263
+ nodes,
264
+ maxDepth: nodes.reduce((max, n) => Math.max(max, n.depth), 0),
265
+ begunAt: parseInstant(journal.begunAt),
266
+ lastEventAt: journal.events.reduce((latest, event) => {
267
+ const at = parseInstant(event.at);
268
+ return at === null ? latest : latest === null ? at : Math.max(latest, at);
269
+ }, null),
270
+ questions: coordination.questions,
271
+ steers: coordination.steers,
272
+ result,
273
+ terminal: resultText !== null,
274
+ invalidJournalRows: journal.invalidRows,
275
+ retiredWaitRows: journal.retiredWaitRows,
276
+ invalidCoordinationRows: coordination.invalidRows,
277
+ partialTail: journal.partialTail || coordination.partialTail,
278
+ treeError: view === null && journal.present ? "spawn journal could not be folded into a tree (a settle or cancel names a node that was never spawned)" : null
279
+ };
280
+ }
281
+ function safeTreeView(events) {
282
+ if (events.length === 0) return null;
283
+ try {
284
+ return materializeTreeView([...events]);
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+ function orderNodes(view, journal) {
290
+ const driver = /* @__PURE__ */ new Map();
291
+ const settled = /* @__PURE__ */ new Map();
292
+ const childRoll = /* @__PURE__ */ new Map();
293
+ const spawnedAt = /* @__PURE__ */ new Map();
294
+ const settledAt = /* @__PURE__ */ new Map();
295
+ const cancelReason = /* @__PURE__ */ new Map();
296
+ const parentOf = new Map(view.nodes.map((n) => [n.id, n.parent]));
297
+ for (const event of journal.events) {
298
+ const at = parseInstant(event.at);
299
+ if (event.kind === "spawned") {
300
+ spawnedAt.set(event.id, at);
301
+ } else if (event.kind === "metered") {
302
+ driver.set(event.id, addSpend(driver.get(event.id) ?? ZERO_ROLL, event.spend));
303
+ } else if (event.kind === "settled") {
304
+ settled.set(event.id, addSpend(settled.get(event.id) ?? ZERO_ROLL, event.spent));
305
+ settledAt.set(event.id, at);
306
+ const parent = parentOf.get(event.id);
307
+ if (parent !== void 0) {
308
+ childRoll.set(parent, addSpend(childRoll.get(parent) ?? ZERO_ROLL, event.spent));
309
+ }
310
+ } else if (event.kind === "cancelled") {
311
+ settledAt.set(event.id, at);
312
+ cancelReason.set(event.id, event.reason);
313
+ }
314
+ }
315
+ const byParent = /* @__PURE__ */ new Map();
316
+ for (const node of view.nodes) {
317
+ const key = node.parent ?? null;
318
+ const bucket = byParent.get(key);
319
+ if (bucket) bucket.push(node);
320
+ else byParent.set(key, [node]);
321
+ }
322
+ const out = [];
323
+ const seen = /* @__PURE__ */ new Set();
324
+ const walk = (id, depth) => {
325
+ if (seen.has(id)) return;
326
+ seen.add(id);
327
+ const node = view.nodes.find((n) => n.id === id);
328
+ if (node === void 0) return;
329
+ const spawned = spawnedAt.get(id) ?? null;
330
+ const closed = settledAt.get(id) ?? null;
331
+ out.push({
332
+ id: node.id,
333
+ parent: node.parent ?? null,
334
+ label: node.label,
335
+ depth,
336
+ role: inferRole(node.id, node.parent, journal.supervisorIds),
337
+ status: node.status,
338
+ runtime: node.runtime ?? null,
339
+ budget: node.budget,
340
+ driver: driver.get(id) ?? ZERO_ROLL,
341
+ children: childRoll.get(id) ?? ZERO_ROLL,
342
+ settled: settled.get(id) ?? ZERO_ROLL,
343
+ spawnedAt: spawned,
344
+ settledAt: closed,
345
+ wallMs: spawned !== null && closed !== null ? closed - spawned : null,
346
+ outRef: node.outRef ?? null,
347
+ cancelReason: cancelReason.get(id) ?? null
348
+ });
349
+ for (const child of byParent.get(id) ?? []) walk(child.id, depth + 1);
350
+ };
351
+ walk(view.root, 0);
352
+ for (const node of view.nodes) if (!seen.has(node.id)) walk(node.id, 0);
353
+ return out;
354
+ }
355
+ function parseResult(text) {
356
+ const result = parseJsonObject(text);
357
+ if (result === null) return null;
358
+ const error = asRecord(result.error);
359
+ return {
360
+ kind: str(result.kind),
361
+ reason: str(result.reason),
362
+ errorName: str(error.name),
363
+ errorMessage: str(error.message)
364
+ };
365
+ }
366
+
367
+ export {
368
+ SPAWN_JOURNAL_FILE,
369
+ COORDINATION_LOG_FILE,
370
+ RESULT_FILE,
371
+ readRunContextJournal,
372
+ inferRole,
373
+ readCoordinationLog,
374
+ readFileRunContext,
375
+ fileRunContextSupervisorRunReader,
376
+ isFileRunContextDir,
377
+ ZERO_ROLL,
378
+ addSpend,
379
+ mergeRolls,
380
+ readRunContextSnapshot
381
+ };
@@ -0,0 +1,19 @@
1
+ // src/ask-defaults.ts
2
+ var DEFAULT_ANALYST_MODEL = "gpt-5.6-luna";
3
+ var DEFAULT_QUESTION_MAX_COST_USD = 2;
4
+ var DEFAULT_QUESTION_TIMEOUT_MS = 18e5;
5
+ var DEFAULT_ASK_CONCURRENCY = 4;
6
+ var QUESTION_FIELD_MAX_CHARS = 900;
7
+ var ASK_QUESTION_HEADER = "Answer QUESTION with the trace tools and follow TRACES ASK RULES in analyst_instructions. The trace list is JSON after PREPARED CONTEXT: there. Cite trace://<trace_id>/span/<span_id>.";
8
+ var QUESTION_LABEL = "\n\nQUESTION: ";
9
+ var MAX_TRACE_QUESTION_CHARS = QUESTION_FIELD_MAX_CHARS - ASK_QUESTION_HEADER.length - QUESTION_LABEL.length;
10
+
11
+ export {
12
+ DEFAULT_ANALYST_MODEL,
13
+ DEFAULT_QUESTION_MAX_COST_USD,
14
+ DEFAULT_QUESTION_TIMEOUT_MS,
15
+ DEFAULT_ASK_CONCURRENCY,
16
+ ASK_QUESTION_HEADER,
17
+ QUESTION_LABEL,
18
+ MAX_TRACE_QUESTION_CHARS
19
+ };
@@ -0,0 +1,62 @@
1
+ // src/finding-rejections.ts
2
+ var REJECTION_MESSAGE = /^(?:\[([^\]]+)\] )?finding rejected: (.+)$/;
3
+ function findingRejection(message, fields) {
4
+ const match = REJECTION_MESSAGE.exec(message);
5
+ if (!match) return void 0;
6
+ const kind = match[2].trim();
7
+ const reason = kind === "unresolved evidence" && typeof fields?.reason === "string" && fields.reason ? fields.reason : kind;
8
+ return match[1] ? { analystId: match[1], reason } : { reason };
9
+ }
10
+ function rejectionCause(fields) {
11
+ if (typeof fields?.reason === "string" && fields.reason) return fields.reason;
12
+ const issues = fields?.issues;
13
+ if (typeof issues === "string" && issues) return issues;
14
+ if (Array.isArray(issues) && issues.length > 0) return issues.map((issue) => String(issue)).join("; ");
15
+ return void 0;
16
+ }
17
+ function findingRejectionDetail(message, fields) {
18
+ const rejection = findingRejection(message, fields);
19
+ if (!rejection) return void 0;
20
+ const lead = rejectionCause(fields) ?? rejection.reason;
21
+ const parts = message.endsWith(lead) ? [] : [lead];
22
+ if (typeof fields?.uri === "string" && fields.uri) parts.push(`uri ${fields.uri}`);
23
+ if (typeof fields?.required === "number" && typeof fields?.distinct === "number") {
24
+ parts.push(`${fields.distinct} of ${fields.required} required distinct citation(s)`);
25
+ }
26
+ if (typeof fields?.subject === "string" && fields.subject) parts.push(`subject ${fields.subject}`);
27
+ return parts.join("; ");
28
+ }
29
+ function createFindingRejectionTally(defaultAnalystId = "unknown") {
30
+ const byAnalyst = /* @__PURE__ */ new Map();
31
+ return {
32
+ record(message, fields) {
33
+ const rejection = findingRejection(message, fields);
34
+ if (!rejection) return;
35
+ const analystId = rejection.analystId ?? defaultAnalystId;
36
+ const reasons = byAnalyst.get(analystId) ?? /* @__PURE__ */ new Map();
37
+ reasons.set(rejection.reason, (reasons.get(rejection.reason) ?? 0) + 1);
38
+ byAnalyst.set(analystId, reasons);
39
+ },
40
+ counts() {
41
+ return Object.fromEntries(
42
+ [...byAnalyst].map(([analystId, reasons]) => [analystId, Object.fromEntries(reasons)])
43
+ );
44
+ }
45
+ };
46
+ }
47
+ function totalFindingRejections(reasons) {
48
+ return Object.values(reasons ?? {}).reduce((sum, count) => sum + count, 0);
49
+ }
50
+ function formatFindingRejections(reasons) {
51
+ const total = totalFindingRejections(reasons);
52
+ if (total === 0) return "";
53
+ const ordered = Object.entries(reasons ?? {}).filter(([, count]) => count > 0).sort(([a, left], [b, right]) => right - left || a.localeCompare(b)).map(([reason, count]) => `${reason} \xD7${count}`);
54
+ return `${total} finding(s) rejected: ${ordered.join("; ")}`;
55
+ }
56
+
57
+ export {
58
+ findingRejectionDetail,
59
+ createFindingRejectionTally,
60
+ totalFindingRejections,
61
+ formatFindingRejections
62
+ };