agentpostmortem 0.1.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.
@@ -0,0 +1,547 @@
1
+ // src/storage/paths.ts
2
+ import fs2 from "fs/promises";
3
+ import os from "os";
4
+ import path2 from "path";
5
+ import { z } from "zod";
6
+
7
+ // src/storage/project.ts
8
+ import crypto from "crypto";
9
+ import fs from "fs/promises";
10
+ import path from "path";
11
+ var PROJECT_HASH_LENGTH = 24;
12
+ async function canonicalWorktree(worktree) {
13
+ const resolved = path.resolve(worktree);
14
+ const canonical = await fs.realpath(resolved).catch(() => resolved);
15
+ return process.platform === "win32" ? canonical.toLowerCase() : canonical;
16
+ }
17
+ async function projectId(worktree) {
18
+ const canonical = await canonicalWorktree(worktree);
19
+ return crypto.createHash("sha256").update(canonical).digest("hex").slice(0, PROJECT_HASH_LENGTH);
20
+ }
21
+
22
+ // src/storage/paths.ts
23
+ var PostmortemConfigSchema = z.object({
24
+ storage: z.enum(["user", "repo"]).optional(),
25
+ storeRaw: z.boolean().optional()
26
+ });
27
+ function postmortemConfigPath(worktree) {
28
+ return path2.join(path2.resolve(worktree), ".opencode", "postmortem.json");
29
+ }
30
+ async function isSymlink(p) {
31
+ const stat = await fs2.lstat(p).catch(() => void 0);
32
+ return stat?.isSymbolicLink() ?? false;
33
+ }
34
+ async function symlinkError(p, operation, label) {
35
+ if (!await isSymlink(p)) return void 0;
36
+ return `refusing to ${operation} postmortem artifact because ${label} is a symlink: ${p}`;
37
+ }
38
+ async function assertSafeArtifactPath(p, operation, label) {
39
+ const error = await symlinkError(p, operation, label);
40
+ if (!error) return;
41
+ throw new Error(error);
42
+ }
43
+ async function repoStorageSafety(worktree) {
44
+ const root = path2.resolve(worktree);
45
+ const opencodeDir = path2.join(root, ".opencode");
46
+ if (await isSymlink(opencodeDir)) {
47
+ return {
48
+ safe: false,
49
+ error: "repo-local postmortem storage is unsafe because .opencode is a symlink"
50
+ };
51
+ }
52
+ const postmortemsDir = path2.join(opencodeDir, "postmortems");
53
+ if (await isSymlink(postmortemsDir)) {
54
+ return {
55
+ safe: false,
56
+ error: "repo-local postmortem storage is unsafe because .opencode/postmortems is a symlink"
57
+ };
58
+ }
59
+ const id = await projectId(worktree);
60
+ const projectDir = path2.join(postmortemsDir, id);
61
+ if (await isSymlink(projectDir)) {
62
+ return {
63
+ safe: false,
64
+ error: `repo-local postmortem storage is unsafe because .opencode/postmortems/${id} is a symlink`
65
+ };
66
+ }
67
+ const artifacts = [
68
+ ["last-run.json", "last-run.json"],
69
+ ["failures.jsonl", "failures.jsonl"],
70
+ ["rules.json", "rules.json"],
71
+ ["index.json", "index.json"],
72
+ ["write.lock", "write.lock"]
73
+ ];
74
+ for (const [name, label] of artifacts) {
75
+ const artifactPath = path2.join(projectDir, name);
76
+ if (await isSymlink(artifactPath)) {
77
+ return {
78
+ safe: false,
79
+ error: `repo-local postmortem storage is unsafe because ${label} is a symlink at ${artifactPath}`
80
+ };
81
+ }
82
+ }
83
+ return { safe: true };
84
+ }
85
+ async function loadPostmortemConfig(worktree) {
86
+ const raw = await fs2.readFile(postmortemConfigPath(worktree), "utf8").catch(() => "");
87
+ if (!raw.trim()) return {};
88
+ const parsed = await Promise.resolve().then(() => JSON.parse(raw)).catch(() => ({}));
89
+ const config = PostmortemConfigSchema.safeParse(parsed);
90
+ if (!config.success) return {};
91
+ return config.data;
92
+ }
93
+ async function savePostmortemConfig(worktree, config) {
94
+ const parsed = PostmortemConfigSchema.parse(config);
95
+ const configPath = postmortemConfigPath(worktree);
96
+ await fs2.mkdir(path2.dirname(configPath), { recursive: true });
97
+ await fs2.writeFile(configPath, `${JSON.stringify(parsed, null, 2)}
98
+ `, "utf8");
99
+ return {
100
+ path: configPath,
101
+ config: parsed
102
+ };
103
+ }
104
+ function baseDataDir() {
105
+ const home = os.homedir();
106
+ if (process.platform === "darwin") {
107
+ return path2.join(home, "Library", "Application Support");
108
+ }
109
+ if (process.platform === "win32") {
110
+ return process.env.LOCALAPPDATA || process.env.APPDATA || path2.join(home, "AppData", "Local");
111
+ }
112
+ return process.env.XDG_DATA_HOME || path2.join(home, ".local", "share");
113
+ }
114
+ function globalPostmortemRoot() {
115
+ return path2.join(baseDataDir(), "opencode", "postmortems");
116
+ }
117
+ async function postmortemPaths(worktree) {
118
+ const resolvedWorktree = path2.resolve(worktree);
119
+ const id = await projectId(worktree);
120
+ return {
121
+ projectId: id,
122
+ globalRoot: globalPostmortemRoot(),
123
+ defaultRoot: path2.join(globalPostmortemRoot(), id),
124
+ localOverrideRoot: path2.join(resolvedWorktree, ".opencode", "postmortems", id)
125
+ };
126
+ }
127
+ async function resolvePostmortemRoot(worktree) {
128
+ const [paths, config] = await Promise.all([postmortemPaths(worktree), loadPostmortemConfig(worktree)]);
129
+ const repoSafe = config.storage === "repo" ? (await repoStorageSafety(worktree)).safe : false;
130
+ return {
131
+ projectId: paths.projectId,
132
+ defaultRoot: paths.defaultRoot,
133
+ localOverrideRoot: paths.localOverrideRoot,
134
+ root: repoSafe ? paths.localOverrideRoot : paths.defaultRoot
135
+ };
136
+ }
137
+
138
+ // src/store/failures.ts
139
+ import fs5 from "fs/promises";
140
+
141
+ // src/model.ts
142
+ import { z as z2 } from "zod";
143
+ var MAX_REDACTED_TEXT = 2e3;
144
+ var MAX_RULE_TEXT = 160;
145
+ var MAX_SIGNATURES = 10;
146
+ var MAX_PATHS = 50;
147
+ var MAX_TOOLS = 20;
148
+ var FAILURE_RECORD_SCHEMA_VERSION = 1;
149
+ var EvidenceType = z2.enum([
150
+ "tool",
151
+ "error",
152
+ "diff_summary",
153
+ "git_status",
154
+ "context_gap"
155
+ ]);
156
+ var EvidenceItem = z2.object({
157
+ type: EvidenceType,
158
+ redactedText: z2.string().max(MAX_REDACTED_TEXT),
159
+ hash: z2.string(),
160
+ byteCount: z2.number().int().nonnegative(),
161
+ tokenEstimate: z2.number().int().nonnegative()
162
+ });
163
+ var Signature = z2.object({
164
+ messageHash: z2.string(),
165
+ toolFailureHash: z2.string().optional()
166
+ });
167
+ var GuardrailMatch = z2.object({
168
+ signatures: z2.array(z2.string()).max(MAX_SIGNATURES).optional(),
169
+ paths: z2.array(z2.string()).max(MAX_PATHS).optional(),
170
+ tools: z2.array(z2.string()).max(MAX_TOOLS).optional(),
171
+ keywords: z2.array(z2.string()).optional()
172
+ });
173
+ var GuardrailRule = z2.object({
174
+ id: z2.string(),
175
+ enabled: z2.boolean(),
176
+ match: GuardrailMatch,
177
+ rule: z2.object({
178
+ text: z2.string().max(MAX_RULE_TEXT),
179
+ severity: z2.enum(["must", "should"])
180
+ }),
181
+ userFeedbackRating: z2.enum(["positive", "negative"]).optional(),
182
+ userFeedbackNote: z2.string().max(500).optional()
183
+ });
184
+ var FailureKind = z2.enum([
185
+ "missing_env",
186
+ "missing_file",
187
+ "test_failure",
188
+ "wrong_file",
189
+ "unknown"
190
+ ]);
191
+ var WhyFailedCitation = z2.object({
192
+ type: EvidenceType,
193
+ hash: z2.string()
194
+ });
195
+ var WhyFailedHypothesis = z2.object({
196
+ type: FailureKind,
197
+ confidence: z2.number().min(0).max(1),
198
+ explanation: z2.string().max(300),
199
+ citations: z2.array(WhyFailedCitation).min(1).max(5)
200
+ });
201
+ var WhyFailedRuleSuggestion = z2.object({
202
+ text: z2.string().max(MAX_RULE_TEXT),
203
+ severity: z2.enum(["must", "should"]),
204
+ match: GuardrailMatch
205
+ });
206
+ var WhyFailedAnalysis = z2.object({
207
+ version: z2.literal(1),
208
+ generatedAt: z2.string(),
209
+ hierarchy: z2.array(FailureKind).min(1).max(5),
210
+ hypotheses: z2.array(WhyFailedHypothesis).min(1).max(3),
211
+ rules: z2.array(WhyFailedRuleSuggestion).min(1).max(3)
212
+ });
213
+ var FailureRecord = z2.object({
214
+ schemaVersion: z2.literal(FAILURE_RECORD_SCHEMA_VERSION),
215
+ id: z2.string(),
216
+ projectId: z2.string(),
217
+ createdAt: z2.string(),
218
+ sessionId: z2.string(),
219
+ parentFailureId: z2.string().optional(),
220
+ runId: z2.string().optional(),
221
+ signature: Signature,
222
+ evidence: z2.array(EvidenceItem).optional(),
223
+ redactionReport: z2.unknown().optional(),
224
+ selectionTrace: z2.unknown().optional(),
225
+ analysis: WhyFailedAnalysis.optional()
226
+ });
227
+ var FailureRecordPartial = FailureRecord.partial();
228
+
229
+ // src/storage/lock.ts
230
+ import fs3 from "fs/promises";
231
+ import path3 from "path";
232
+ var DEFAULT_LOCK_STALE_TTL_MS = 12e4;
233
+ var LockBusyError = class extends Error {
234
+ constructor(lockPath) {
235
+ super(`lock already held: ${lockPath}`);
236
+ this.lockPath = lockPath;
237
+ this.name = "LockBusyError";
238
+ }
239
+ };
240
+ function stale(data, now, ttlMs) {
241
+ const createdAt = Date.parse(data.createdAt);
242
+ if (Number.isNaN(createdAt)) return true;
243
+ return now - createdAt > ttlMs;
244
+ }
245
+ async function read(lockPath) {
246
+ await assertSafeArtifactPath(lockPath, "read", "write.lock");
247
+ const text = await fs3.readFile(lockPath, "utf8");
248
+ return JSON.parse(text);
249
+ }
250
+ async function readOptional(lockPath) {
251
+ return read(lockPath).catch((error) => {
252
+ const err = error;
253
+ if (err.code === "ENOENT") return null;
254
+ throw error;
255
+ });
256
+ }
257
+ async function claim(lockPath, now) {
258
+ await assertSafeArtifactPath(lockPath, "lock", "write.lock");
259
+ const data = {
260
+ pid: process.pid,
261
+ createdAt: new Date(now).toISOString()
262
+ };
263
+ await fs3.mkdir(path3.dirname(lockPath), { recursive: true });
264
+ await fs3.writeFile(lockPath, JSON.stringify(data), { flag: "wx" });
265
+ return data;
266
+ }
267
+ async function acquireWriteLock(lockPath, options = {}) {
268
+ const staleTtlMs = options.staleTtlMs ?? DEFAULT_LOCK_STALE_TTL_MS;
269
+ const now = options.now ?? Date.now;
270
+ try {
271
+ const data = await claim(lockPath, now());
272
+ return {
273
+ data,
274
+ path: lockPath,
275
+ release: async () => {
276
+ const current2 = await readOptional(lockPath);
277
+ if (!current2) return;
278
+ if (current2.pid !== data.pid || current2.createdAt !== data.createdAt) return;
279
+ await fs3.rm(lockPath, { force: true });
280
+ }
281
+ };
282
+ } catch (error) {
283
+ const err = error;
284
+ if (err.code !== "EEXIST") throw err;
285
+ }
286
+ const current = await readOptional(lockPath);
287
+ if (!current || !stale(current, now(), staleTtlMs)) {
288
+ throw new LockBusyError(lockPath);
289
+ }
290
+ await fs3.rm(lockPath, { force: true });
291
+ try {
292
+ const data = await claim(lockPath, now());
293
+ return {
294
+ data,
295
+ path: lockPath,
296
+ release: async () => {
297
+ const latest = await readOptional(lockPath);
298
+ if (!latest) return;
299
+ if (latest.pid !== data.pid || latest.createdAt !== data.createdAt) return;
300
+ await fs3.rm(lockPath, { force: true });
301
+ }
302
+ };
303
+ } catch (error) {
304
+ const err = error;
305
+ if (err.code === "EEXIST") throw new LockBusyError(lockPath);
306
+ throw err;
307
+ }
308
+ }
309
+
310
+ // src/store/paths.ts
311
+ import path4 from "path";
312
+ function storePathsFromRoot(root) {
313
+ return {
314
+ root,
315
+ failures: path4.join(root, "failures.jsonl"),
316
+ rules: path4.join(root, "rules.json"),
317
+ index: path4.join(root, "index.json"),
318
+ summary: path4.join(root, "SUMMARY.md"),
319
+ lock: path4.join(root, "write.lock")
320
+ };
321
+ }
322
+
323
+ // src/store/summary.ts
324
+ import fs4 from "fs/promises";
325
+ function parseDate(value) {
326
+ const time = Date.parse(value);
327
+ if (Number.isNaN(time)) return 0;
328
+ return time;
329
+ }
330
+ function safe(value) {
331
+ if (!value) return "-";
332
+ return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim() || "-";
333
+ }
334
+ function renderSummary(records) {
335
+ const sorted = [...records].sort((a, b) => {
336
+ const timeDiff = parseDate(b.createdAt) - parseDate(a.createdAt);
337
+ if (timeDiff !== 0) return timeDiff;
338
+ return a.id.localeCompare(b.id);
339
+ });
340
+ const lines = [
341
+ "# Failure Summary",
342
+ "",
343
+ `- Total records: ${sorted.length}`,
344
+ "",
345
+ "## Records",
346
+ "",
347
+ "| id | createdAt | sessionId | messageHash | toolFailureHash | evidenceCount |",
348
+ "| --- | --- | --- | --- | --- | --- |",
349
+ ...sorted.map((record) => {
350
+ const evidenceCount = record.evidence?.length ?? 0;
351
+ return [
352
+ `| ${safe(record.id)}`,
353
+ `${safe(record.createdAt)}`,
354
+ `${safe(record.sessionId)}`,
355
+ `${safe(record.signature.messageHash)}`,
356
+ `${safe(record.signature.toolFailureHash)}`,
357
+ `${evidenceCount} |`
358
+ ].join(" | ");
359
+ })
360
+ ];
361
+ return `${lines.join("\n")}
362
+ `;
363
+ }
364
+ async function writeSummary(root, records) {
365
+ const paths = storePathsFromRoot(root);
366
+ const content = renderSummary(records);
367
+ await fs4.mkdir(paths.root, { recursive: true });
368
+ const lock = await acquireWriteLock(paths.lock);
369
+ try {
370
+ await fs4.writeFile(paths.summary, content, "utf8");
371
+ } finally {
372
+ await lock.release();
373
+ }
374
+ return {
375
+ path: paths.summary,
376
+ lockPath: paths.lock,
377
+ content
378
+ };
379
+ }
380
+
381
+ // src/store/failures.ts
382
+ function parseFailureLines(text) {
383
+ if (!text) {
384
+ return {
385
+ records: [],
386
+ skipped: 0,
387
+ warnings: []
388
+ };
389
+ }
390
+ const lines = text.split("\n");
391
+ const records = [];
392
+ const warnings = [];
393
+ lines.forEach((line, index) => {
394
+ if (line.length === 0) return;
395
+ try {
396
+ const item = FailureRecord.parse(JSON.parse(line));
397
+ records.push(item);
398
+ } catch {
399
+ warnings.push({
400
+ line: index + 1,
401
+ message: "skipped corrupted or invalid failure record"
402
+ });
403
+ }
404
+ });
405
+ return {
406
+ records,
407
+ skipped: warnings.length,
408
+ warnings
409
+ };
410
+ }
411
+ async function writeFailureRecords(path5, records) {
412
+ await assertSafeArtifactPath(path5, "write", "failures.jsonl");
413
+ const lines = records.map((record) => JSON.stringify(record)).join("\n");
414
+ await fs5.writeFile(path5, lines.length > 0 ? `${lines}
415
+ ` : "", "utf8");
416
+ }
417
+ async function appendFailureRecord(root, record) {
418
+ const paths = storePathsFromRoot(root);
419
+ const parsed = FailureRecord.parse(record);
420
+ await fs5.mkdir(paths.root, { recursive: true });
421
+ await assertSafeArtifactPath(paths.failures, "append", "failures.jsonl");
422
+ const lock = await acquireWriteLock(paths.lock);
423
+ try {
424
+ await fs5.appendFile(paths.failures, `${JSON.stringify(parsed)}
425
+ `, "utf8");
426
+ } finally {
427
+ await lock.release();
428
+ }
429
+ return {
430
+ path: paths.failures,
431
+ lockPath: paths.lock
432
+ };
433
+ }
434
+ async function loadFailureRecords(root) {
435
+ const paths = storePathsFromRoot(root);
436
+ await assertSafeArtifactPath(paths.failures, "read", "failures.jsonl");
437
+ const text = await fs5.readFile(paths.failures, "utf8").catch((error) => {
438
+ const err = error;
439
+ if (err.code === "ENOENT") return "";
440
+ throw err;
441
+ });
442
+ return parseFailureLines(text);
443
+ }
444
+ async function updateFailureRecord(root, id, update) {
445
+ const paths = storePathsFromRoot(root);
446
+ await fs5.mkdir(paths.root, { recursive: true });
447
+ await assertSafeArtifactPath(paths.failures, "read", "failures.jsonl");
448
+ const lock = await acquireWriteLock(paths.lock);
449
+ let result;
450
+ try {
451
+ const text = await fs5.readFile(paths.failures, "utf8").catch((error) => {
452
+ const err = error;
453
+ if (err.code === "ENOENT") return "";
454
+ throw err;
455
+ });
456
+ const loaded = parseFailureLines(text);
457
+ const index = loaded.records.findIndex((record) => record.id === id);
458
+ if (index < 0) {
459
+ result = {
460
+ notFound: true,
461
+ records: loaded.records.length,
462
+ skipped: loaded.skipped,
463
+ warnings: loaded.warnings
464
+ };
465
+ } else {
466
+ const next = FailureRecord.parse(
467
+ update(loaded.records[index])
468
+ );
469
+ loaded.records.splice(index, 1, next);
470
+ await writeFailureRecords(paths.failures, loaded.records);
471
+ result = {
472
+ updated: next,
473
+ notFound: false,
474
+ records: loaded.records.length,
475
+ skipped: loaded.skipped,
476
+ warnings: loaded.warnings
477
+ };
478
+ }
479
+ } finally {
480
+ await lock.release();
481
+ }
482
+ if (!result.notFound)
483
+ await writeSummary(root, (await loadFailureRecords(root)).records);
484
+ return result;
485
+ }
486
+ function pruneFailureRecords(records, options = {}) {
487
+ const nowMs = options.nowMs ?? (options.nowIso ? Date.parse(options.nowIso) : Date.now());
488
+ const cmp = (a, b) => {
489
+ const ta = Date.parse(a.createdAt);
490
+ const tb = Date.parse(b.createdAt);
491
+ if (ta !== tb) return tb - ta;
492
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
493
+ };
494
+ const sorted = [...records].sort(cmp);
495
+ const dropped = [];
496
+ let kept = sorted;
497
+ if (typeof options.maxAgeDays === "number") {
498
+ const cutoff = nowMs - options.maxAgeDays * 24 * 60 * 60 * 1e3;
499
+ const pass = [];
500
+ for (const r of kept) {
501
+ const t = Date.parse(r.createdAt);
502
+ if (t >= cutoff) pass.push(r);
503
+ else dropped.push(r);
504
+ }
505
+ kept = pass;
506
+ }
507
+ const keepN = typeof options.keepLastN === "number" ? options.keepLastN : options.maxRecords;
508
+ if (typeof keepN === "number" && kept.length > keepN) {
509
+ const toDrop = kept.slice(keepN);
510
+ dropped.push(...toDrop);
511
+ kept = kept.slice(0, keepN);
512
+ }
513
+ if (typeof options.maxBytes === "number") {
514
+ const sizes = kept.map(
515
+ (r) => Buffer.byteLength(`${JSON.stringify(r)}
516
+ `, "utf8")
517
+ );
518
+ let total = sizes.reduce((s, n) => s + n, 0);
519
+ while (total > options.maxBytes && kept.length > 0) {
520
+ const removed = kept.pop();
521
+ const sz = sizes.pop();
522
+ total -= sz;
523
+ dropped.push(removed);
524
+ }
525
+ }
526
+ dropped.sort(cmp);
527
+ return { kept, dropped };
528
+ }
529
+
530
+ export {
531
+ postmortemConfigPath,
532
+ assertSafeArtifactPath,
533
+ repoStorageSafety,
534
+ loadPostmortemConfig,
535
+ savePostmortemConfig,
536
+ resolvePostmortemRoot,
537
+ MAX_RULE_TEXT,
538
+ FAILURE_RECORD_SCHEMA_VERSION,
539
+ GuardrailRule,
540
+ acquireWriteLock,
541
+ storePathsFromRoot,
542
+ writeSummary,
543
+ appendFailureRecord,
544
+ loadFailureRecords,
545
+ updateFailureRecord,
546
+ pruneFailureRecords
547
+ };
@@ -0,0 +1,63 @@
1
+ import {
2
+ loadFailureRecords,
3
+ resolvePostmortemRoot
4
+ } from "./chunk-Q7YDLORD.js";
5
+
6
+ // src/eval.ts
7
+ function signatureKey(record) {
8
+ return record.signature.toolFailureHash ?? record.signature.messageHash;
9
+ }
10
+ function deterministicSort(records) {
11
+ return [...records].sort((a, b) => {
12
+ const ta = Date.parse(a.createdAt);
13
+ const tb = Date.parse(b.createdAt);
14
+ if (ta !== tb) return ta - tb;
15
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
16
+ });
17
+ }
18
+ async function renderEval(worktree, args = {}) {
19
+ const paths = await resolvePostmortemRoot(worktree);
20
+ const loaded = await loadFailureRecords(paths.root);
21
+ const records = deterministicSort(loaded.records);
22
+ const totalRecords = records.length;
23
+ const sigs = records.map(signatureKey);
24
+ const window = Math.max(1, Number(args.window ?? 10));
25
+ let repeatWithinWindowCount = 0;
26
+ const repeatCounts = {};
27
+ for (let i = 0; i < sigs.length; i++) {
28
+ const s = sigs[i];
29
+ let repeated = false;
30
+ for (let j = i + 1; j <= i + window && j < sigs.length; j++) {
31
+ if (sigs[j] === s) {
32
+ repeated = true;
33
+ repeatCounts[s] = (repeatCounts[s] ?? 0) + 1;
34
+ }
35
+ }
36
+ if (repeated) repeatWithinWindowCount++;
37
+ }
38
+ const uniqueSignatures = new Set(sigs).size;
39
+ const repeatCountsBySignature = Object.entries(repeatCounts).sort((a, b) => {
40
+ if (b[1] !== a[1]) return b[1] - a[1];
41
+ return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
42
+ }).map(([signature, count]) => ({ signature, count }));
43
+ const payload = {
44
+ totalRecords,
45
+ uniqueSignatures,
46
+ repeatRateWithinWindow: totalRecords === 0 ? 0 : repeatWithinWindowCount / totalRecords,
47
+ repeatCountsBySignature
48
+ };
49
+ if (args.json) return JSON.stringify(payload, null, 2);
50
+ const lines = [];
51
+ lines.push(`postmortem root: ${paths.root}`);
52
+ lines.push(`total records: ${totalRecords}`);
53
+ lines.push(`unique signatures: ${uniqueSignatures}`);
54
+ lines.push(`repeat rate within window=${window}: ${payload.repeatRateWithinWindow}`);
55
+ lines.push(`repeat counts by signature:`);
56
+ for (const r of repeatCountsBySignature) lines.push(` - ${r.signature}: ${r.count}`);
57
+ return lines.join("\n");
58
+ }
59
+ var eval_default = renderEval;
60
+ export {
61
+ eval_default as default,
62
+ renderEval
63
+ };