@tryinget/pi-agent-vent 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.
- package/LICENSE +78 -0
- package/README.md +188 -0
- package/biome.jsonc +43 -0
- package/docs/adr/2026-05-22-agent-vent-retention-delete-policy.md +65 -0
- package/docs/engineering.local.md +57 -0
- package/docs/project/2026-05-21-agent-vent-design.md +182 -0
- package/docs/project/2026-05-21-agent-vent-implementation-plan.md +66 -0
- package/docs/project/extension-sop.md +52 -0
- package/docs/project/product-posture.md +191 -0
- package/docs/project/trusted-publishing.md +53 -0
- package/docs/project/vision.md +70 -0
- package/examples/.gitkeep +0 -0
- package/extensions/agent-vent.ts +1384 -0
- package/package.json +95 -0
- package/policy/engineering-lane.json +22 -0
- package/policy/security-policy.json +10 -0
- package/prompts/implementation-planning.md +20 -0
- package/prompts/security-review.md +20 -0
- package/scripts/docs-list.sh +50 -0
- package/scripts/quality-gate.sh +168 -0
- package/scripts/release-artifact-check.mjs +221 -0
- package/scripts/release-check.sh +196 -0
- package/scripts/release-smoke-check.mjs +367 -0
- package/scripts/release-smoke.sh +90 -0
- package/scripts/validate-structure.mjs +248 -0
- package/scripts/validate-structure.sh +145 -0
- package/src/vent-store.js +2839 -0
- package/tests/.gitkeep +0 -0
- package/tests/agent-vent-extension.test.js +762 -0
- package/tests/release-artifact-check.test.js +168 -0
- package/tests/release-smoke-check.test.js +235 -0
- package/tests/vent-store.test.js +1972 -0
|
@@ -0,0 +1,2839 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export const SCHEMA_VERSION = 1;
|
|
7
|
+
export const DEFAULT_LIMIT = 10;
|
|
8
|
+
export const STORE_FILE_NAME = "vents.jsonl";
|
|
9
|
+
export const REVIEW_EVENT_FILE_NAME = "review-events.jsonl";
|
|
10
|
+
export const CURATION_EVENT_FILE_NAME = "curation-events.jsonl";
|
|
11
|
+
export const RETENTION_EVENT_FILE_NAME = "retention-events.jsonl";
|
|
12
|
+
export const BACKUP_DIR_NAME = "backups";
|
|
13
|
+
export const CATEGORIES = [
|
|
14
|
+
"bug",
|
|
15
|
+
"friction",
|
|
16
|
+
"missing_capability",
|
|
17
|
+
"tool_failure",
|
|
18
|
+
"context_loss",
|
|
19
|
+
"permission",
|
|
20
|
+
"performance",
|
|
21
|
+
"documentation",
|
|
22
|
+
"workflow",
|
|
23
|
+
"other",
|
|
24
|
+
];
|
|
25
|
+
export const SEVERITIES = ["low", "medium", "high", "critical"];
|
|
26
|
+
export const REVIEW_STATES = ["new", "acknowledged", "dismissed", "escalation_drafted"];
|
|
27
|
+
export const CURATION_ACTIONS = ["merge", "rename", "remove"];
|
|
28
|
+
export const DRAFT_TARGETS = ["github_issue", "ak_task", "incident_review", "maintainer_note"];
|
|
29
|
+
export const RETENTION_ACTIONS = ["preview", "archive", "restore", "candidates", "history"];
|
|
30
|
+
export const RETENTION_EVENT_ACTIONS = ["archive", "restore"];
|
|
31
|
+
export const MAX_JSONL_FILE_BYTES = 5 * 1024 * 1024;
|
|
32
|
+
export const MAX_JSONL_LINE_BYTES = 64 * 1024;
|
|
33
|
+
|
|
34
|
+
const SEVERITY_RANK = new Map(SEVERITIES.map((severity, index) => [severity, index]));
|
|
35
|
+
const SENSITIVE_PATTERNS = [
|
|
36
|
+
{
|
|
37
|
+
name: "bearer_token",
|
|
38
|
+
pattern: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
|
|
39
|
+
replacement: "Bearer [REDACTED]",
|
|
40
|
+
},
|
|
41
|
+
{ name: "openai_like_key", pattern: /sk-[A-Za-z0-9_-]{16,}/g, replacement: "sk-[REDACTED]" },
|
|
42
|
+
{ name: "github_token", pattern: /gh[pousr]_[A-Za-z0-9_]{16,}/g, replacement: "gh*_REDACTED" },
|
|
43
|
+
{
|
|
44
|
+
name: "assigned_secret",
|
|
45
|
+
pattern: /\b(api[_-]?key|token|password|secret)\s*=\s*[^\s,;]+/gi,
|
|
46
|
+
replacement: "$1=[REDACTED]",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export function defaultStoreDir(env = process.env) {
|
|
51
|
+
return env.PI_AGENT_VENT_DIR || path.join(os.homedir(), ".pi", "agent", "agent-vent");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defaultStorePath(env = process.env) {
|
|
55
|
+
return path.join(defaultStoreDir(env), STORE_FILE_NAME);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function defaultReviewPath(env = process.env) {
|
|
59
|
+
return path.join(defaultStoreDir(env), REVIEW_EVENT_FILE_NAME);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function defaultCurationPath(env = process.env) {
|
|
63
|
+
return path.join(defaultStoreDir(env), CURATION_EVENT_FILE_NAME);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function defaultRetentionPath(env = process.env) {
|
|
67
|
+
return path.join(defaultStoreDir(env), RETENTION_EVENT_FILE_NAME);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function defaultBackupDir(env = process.env) {
|
|
71
|
+
return path.join(defaultStoreDir(env), BACKUP_DIR_NAME);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeCategory(value) {
|
|
75
|
+
const normalized = String(value || "other")
|
|
76
|
+
.trim()
|
|
77
|
+
.toLowerCase()
|
|
78
|
+
.replaceAll("-", "_");
|
|
79
|
+
return CATEGORIES.includes(normalized) ? normalized : "other";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeSeverity(value) {
|
|
83
|
+
const normalized = String(value || "medium")
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase();
|
|
86
|
+
return SEVERITIES.includes(normalized) ? normalized : "medium";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeReviewState(value) {
|
|
90
|
+
const normalized = String(value || "new")
|
|
91
|
+
.trim()
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replaceAll("-", "_");
|
|
94
|
+
if (!REVIEW_STATES.includes(normalized)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`invalid agent_vent review state: ${value}; expected one of ${REVIEW_STATES.join(", ")}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeCurationAction(value) {
|
|
103
|
+
const normalized = String(value || "")
|
|
104
|
+
.trim()
|
|
105
|
+
.toLowerCase()
|
|
106
|
+
.replaceAll("-", "_");
|
|
107
|
+
if (!CURATION_ACTIONS.includes(normalized)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`invalid agent_vent curation action: ${value}; expected one of ${CURATION_ACTIONS.join(", ")}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function normalizeDraftTarget(value) {
|
|
116
|
+
const normalized = String(value || "")
|
|
117
|
+
.trim()
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.replaceAll("-", "_");
|
|
120
|
+
if (!DRAFT_TARGETS.includes(normalized)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`invalid agent_vent draft target: ${value}; expected one of ${DRAFT_TARGETS.join(", ")}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return normalized;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function normalizeRetentionAction(value) {
|
|
129
|
+
const normalized = String(value || "")
|
|
130
|
+
.trim()
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.replaceAll("-", "_");
|
|
133
|
+
if (!RETENTION_ACTIONS.includes(normalized)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`invalid agent_vent retention action: ${value}; expected one of ${RETENTION_ACTIONS.join(", ")}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return normalized;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function clampLimit(value, fallback = DEFAULT_LIMIT) {
|
|
142
|
+
const numeric = Number(value);
|
|
143
|
+
if (!Number.isFinite(numeric)) return fallback;
|
|
144
|
+
return Math.min(100, Math.max(1, Math.trunc(numeric)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function redactSensitiveText(value) {
|
|
148
|
+
if (value === undefined || value === null)
|
|
149
|
+
return { text: undefined, redacted: false, patterns: [] };
|
|
150
|
+
let text = String(value).slice(0, 4000);
|
|
151
|
+
const patterns = [];
|
|
152
|
+
for (const entry of SENSITIVE_PATTERNS) {
|
|
153
|
+
const next = text.replace(entry.pattern, entry.replacement);
|
|
154
|
+
if (next !== text) {
|
|
155
|
+
patterns.push(entry.name);
|
|
156
|
+
text = next;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { text, redacted: patterns.length > 0, patterns };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function compactText(value, maxLength = 1200) {
|
|
163
|
+
if (value === undefined || value === null) return undefined;
|
|
164
|
+
const text = String(value).replace(/\s+/g, " ").trim();
|
|
165
|
+
if (!text) return undefined;
|
|
166
|
+
if (text.length <= maxLength) return text;
|
|
167
|
+
return `${text.slice(0, maxLength - 1)}…`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function recurrenceSlug(value) {
|
|
171
|
+
const redacted = redactSensitiveText(value).text;
|
|
172
|
+
const slug = String(redacted || "")
|
|
173
|
+
.toLowerCase()
|
|
174
|
+
.replace(/[`'"“”‘’]/g, "")
|
|
175
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
176
|
+
.replace(/^-+|-+$/g, "")
|
|
177
|
+
.slice(0, 80);
|
|
178
|
+
return slug || "unspecified";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildRecurrenceKey(input) {
|
|
182
|
+
const category = normalizeCategory(input?.category);
|
|
183
|
+
const basis = input?.recurrenceKey || input?.summary || "unspecified";
|
|
184
|
+
return `${category}:${recurrenceSlug(basis)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function sanitizeOptionalText(value, maxLength) {
|
|
188
|
+
const compact = compactText(value, maxLength);
|
|
189
|
+
if (compact === undefined) return { value: undefined, redacted: false, patterns: [] };
|
|
190
|
+
const redacted = redactSensitiveText(compact);
|
|
191
|
+
return { value: redacted.text, redacted: redacted.redacted, patterns: redacted.patterns };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function sanitizeFacetText(value, maxLength = 160) {
|
|
195
|
+
const sanitized = sanitizeOptionalText(value, maxLength);
|
|
196
|
+
if (sanitized.value === undefined) return sanitized;
|
|
197
|
+
const normalized = recurrenceSlug(sanitized.value);
|
|
198
|
+
return {
|
|
199
|
+
value: normalized === "unspecified" ? undefined : normalized,
|
|
200
|
+
redacted: sanitized.redacted,
|
|
201
|
+
patterns: sanitized.patterns,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sanitizeTagList(value) {
|
|
206
|
+
if (!Array.isArray(value)) return { values: [], redacted: false, patterns: [] };
|
|
207
|
+
const patterns = new Set();
|
|
208
|
+
let redacted = false;
|
|
209
|
+
const values = [];
|
|
210
|
+
for (const tag of value) {
|
|
211
|
+
const sanitized = sanitizeFacetText(tag, 120);
|
|
212
|
+
redacted = redacted || sanitized.redacted;
|
|
213
|
+
for (const pattern of sanitized.patterns) patterns.add(pattern);
|
|
214
|
+
if (sanitized.value) values.push(sanitized.value);
|
|
215
|
+
if (values.length >= 12) break;
|
|
216
|
+
}
|
|
217
|
+
return { values, redacted, patterns: [...patterns].sort() };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function collectRedactionMetadata(fields) {
|
|
221
|
+
const redactionPatterns = new Set();
|
|
222
|
+
let redacted = false;
|
|
223
|
+
for (const field of fields) {
|
|
224
|
+
redacted = redacted || Boolean(field?.redacted);
|
|
225
|
+
for (const pattern of field?.patterns || []) redactionPatterns.add(pattern);
|
|
226
|
+
}
|
|
227
|
+
return { redacted, redactionPatterns: [...redactionPatterns].sort() };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function createVentRecord(input, context = {}) {
|
|
231
|
+
const summary = sanitizeOptionalText(input?.summary, 600);
|
|
232
|
+
if (!summary.value) {
|
|
233
|
+
throw new Error("agent_vent record requires a non-empty summary");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const optionalFields = {
|
|
237
|
+
frustration: sanitizeOptionalText(input?.frustration, 1200),
|
|
238
|
+
evidence: sanitizeOptionalText(input?.evidence, 1600),
|
|
239
|
+
expected: sanitizeOptionalText(input?.expected, 800),
|
|
240
|
+
actual: sanitizeOptionalText(input?.actual, 800),
|
|
241
|
+
reproduction: sanitizeOptionalText(input?.reproduction, 1200),
|
|
242
|
+
};
|
|
243
|
+
const localFacets = {
|
|
244
|
+
tool: sanitizeFacetText(input?.tool || input?.toolName, 160),
|
|
245
|
+
packageName: sanitizeFacetText(input?.packageName, 200),
|
|
246
|
+
};
|
|
247
|
+
const tags = sanitizeTagList(input?.tags);
|
|
248
|
+
const privacy = collectRedactionMetadata([
|
|
249
|
+
summary,
|
|
250
|
+
...Object.values(optionalFields),
|
|
251
|
+
...Object.values(localFacets),
|
|
252
|
+
tags,
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const category = normalizeCategory(input?.category);
|
|
256
|
+
const severity = normalizeSeverity(input?.severity);
|
|
257
|
+
|
|
258
|
+
return removeUndefined({
|
|
259
|
+
schemaVersion: SCHEMA_VERSION,
|
|
260
|
+
id: context.id || randomUUID(),
|
|
261
|
+
createdAt: context.now || new Date().toISOString(),
|
|
262
|
+
category,
|
|
263
|
+
severity,
|
|
264
|
+
recurrenceKey: buildRecurrenceKey({
|
|
265
|
+
category,
|
|
266
|
+
recurrenceKey: input?.recurrenceKey,
|
|
267
|
+
summary: summary.value,
|
|
268
|
+
}),
|
|
269
|
+
summary: summary.value,
|
|
270
|
+
tool: localFacets.tool.value,
|
|
271
|
+
packageName: localFacets.packageName.value,
|
|
272
|
+
frustration: optionalFields.frustration.value,
|
|
273
|
+
evidence: optionalFields.evidence.value,
|
|
274
|
+
expected: optionalFields.expected.value,
|
|
275
|
+
actual: optionalFields.actual.value,
|
|
276
|
+
reproduction: optionalFields.reproduction.value,
|
|
277
|
+
tags: tags.values,
|
|
278
|
+
context: removeUndefined({
|
|
279
|
+
cwd: context.cwd,
|
|
280
|
+
sessionFile: context.sessionFile,
|
|
281
|
+
source: context.source || "agent_vent",
|
|
282
|
+
}),
|
|
283
|
+
privacy: {
|
|
284
|
+
classification: "local-diagnostic-user-data",
|
|
285
|
+
redacted: privacy.redacted,
|
|
286
|
+
redactionPatterns: privacy.redactionPatterns,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function createReviewEvent(input, context = {}) {
|
|
292
|
+
const recurrenceKey = sanitizeDisplayText(input?.recurrenceKey, 200);
|
|
293
|
+
if (!recurrenceKey) {
|
|
294
|
+
throw new Error("agent_vent review state requires a recurrenceKey");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const state = normalizeReviewState(input?.state || input?.reviewState);
|
|
298
|
+
const note = sanitizeOptionalText(input?.note || input?.reviewNote, 1200);
|
|
299
|
+
|
|
300
|
+
return removeUndefined({
|
|
301
|
+
schemaVersion: SCHEMA_VERSION,
|
|
302
|
+
eventType: "review_state",
|
|
303
|
+
id: context.id || randomUUID(),
|
|
304
|
+
createdAt: context.now || new Date().toISOString(),
|
|
305
|
+
recurrenceKey,
|
|
306
|
+
state,
|
|
307
|
+
note: note.value,
|
|
308
|
+
context: removeUndefined({
|
|
309
|
+
source: context.source || "agent_vent_review",
|
|
310
|
+
}),
|
|
311
|
+
privacy: {
|
|
312
|
+
classification: "local-diagnostic-user-data",
|
|
313
|
+
redacted: note.redacted,
|
|
314
|
+
redactionPatterns: [...note.patterns].sort(),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function createCurationEvent(input, context = {}) {
|
|
320
|
+
const action = normalizeCurationAction(input?.action || input?.curationAction);
|
|
321
|
+
const sourceRecurrenceKey = sanitizeDisplayText(input?.sourceRecurrenceKey, 200);
|
|
322
|
+
const targetRecurrenceKey = sanitizeDisplayText(input?.targetRecurrenceKey, 200);
|
|
323
|
+
if (!sourceRecurrenceKey || (action !== "remove" && !targetRecurrenceKey)) {
|
|
324
|
+
throw new Error("agent_vent curation requires sourceRecurrenceKey and targetRecurrenceKey");
|
|
325
|
+
}
|
|
326
|
+
if (targetRecurrenceKey && sourceRecurrenceKey === targetRecurrenceKey) {
|
|
327
|
+
throw new Error("agent_vent curation source and target must differ");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const note = sanitizeOptionalText(input?.note || input?.curationNote, 1200);
|
|
331
|
+
return removeUndefined({
|
|
332
|
+
schemaVersion: SCHEMA_VERSION,
|
|
333
|
+
eventType: "recurrence_curation",
|
|
334
|
+
id: context.id || randomUUID(),
|
|
335
|
+
createdAt: context.now || new Date().toISOString(),
|
|
336
|
+
action,
|
|
337
|
+
sourceRecurrenceKey,
|
|
338
|
+
targetRecurrenceKey,
|
|
339
|
+
note: note.value,
|
|
340
|
+
context: removeUndefined({
|
|
341
|
+
source: context.source || "agent_vent_curation",
|
|
342
|
+
}),
|
|
343
|
+
privacy: {
|
|
344
|
+
classification: "local-diagnostic-user-data",
|
|
345
|
+
redacted: note.redacted,
|
|
346
|
+
redactionPatterns: [...note.patterns].sort(),
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function createRetentionEvent(input, context = {}) {
|
|
352
|
+
const action = String(input?.action || "")
|
|
353
|
+
.trim()
|
|
354
|
+
.toLowerCase();
|
|
355
|
+
if (!RETENTION_EVENT_ACTIONS.includes(action)) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`invalid agent_vent retention event action: ${input?.action}; expected one of ${RETENTION_EVENT_ACTIONS.join(", ")}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const note = sanitizeOptionalText(input?.note || input?.retentionNote, 1200);
|
|
361
|
+
return removeUndefined({
|
|
362
|
+
schemaVersion: SCHEMA_VERSION,
|
|
363
|
+
eventType: "retention_lifecycle",
|
|
364
|
+
id: context.id || randomUUID(),
|
|
365
|
+
createdAt: context.now || new Date().toISOString(),
|
|
366
|
+
action,
|
|
367
|
+
recurrenceKey: sanitizeDisplayText(input?.recurrenceKey, 200),
|
|
368
|
+
requestedRecurrenceKey: sanitizeDisplayText(input?.requestedRecurrenceKey, 200),
|
|
369
|
+
backupPath: sanitizeDisplayText(input?.backupPath, 1200),
|
|
370
|
+
archivedRecordCount: Number.isFinite(Number(input?.archivedRecordCount))
|
|
371
|
+
? Number(input.archivedRecordCount)
|
|
372
|
+
: undefined,
|
|
373
|
+
archivedRecordIds: Array.isArray(input?.archivedRecordIds)
|
|
374
|
+
? input.archivedRecordIds.map((id) => sanitizeDisplayText(id, 120)).filter(Boolean)
|
|
375
|
+
: undefined,
|
|
376
|
+
beforeHash: sanitizeDisplayText(input?.beforeHash, 128),
|
|
377
|
+
afterHash: sanitizeDisplayText(input?.afterHash, 128),
|
|
378
|
+
note: note.value,
|
|
379
|
+
context: removeUndefined({
|
|
380
|
+
source: context.source || "agent_vent_retention",
|
|
381
|
+
}),
|
|
382
|
+
privacy: {
|
|
383
|
+
classification: "local-diagnostic-user-data",
|
|
384
|
+
redacted: note.redacted,
|
|
385
|
+
redactionPatterns: [...note.patterns].sort(),
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function ensureStore(filePath) {
|
|
391
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
392
|
+
const existing = safeLstat(filePath);
|
|
393
|
+
if (existing) {
|
|
394
|
+
assertSafeJsonlFile(filePath, existing);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const fd = fs.openSync(filePath, createFileFlags(), 0o600);
|
|
399
|
+
fs.closeSync(fd);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function appendVentRecord(filePath, record) {
|
|
403
|
+
appendJsonlRecord(filePath, record, { lockPath: `${filePath}.lock` });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function appendReviewEvent(filePath, event) {
|
|
407
|
+
appendJsonlRecord(filePath, event);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function appendCurationEvent(filePath, event) {
|
|
411
|
+
appendJsonlRecord(filePath, event);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function appendRetentionEvent(filePath, event) {
|
|
415
|
+
appendJsonlRecord(filePath, event);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function readVentRecords(filePath) {
|
|
419
|
+
const { values, malformedLines, oversizedLines, fileHash } = readJsonlRecords(filePath);
|
|
420
|
+
const { records, invalidRecords } = sanitizeVentRecords(values);
|
|
421
|
+
return { records, malformedLines, oversizedLines, invalidRecords, fileHash };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function readReviewEvents(filePath) {
|
|
425
|
+
const { values, malformedLines, oversizedLines, fileHash } = readJsonlRecords(filePath);
|
|
426
|
+
const { events, invalidEvents } = sanitizeReviewEvents(values);
|
|
427
|
+
return {
|
|
428
|
+
events,
|
|
429
|
+
malformedLines,
|
|
430
|
+
oversizedLines,
|
|
431
|
+
invalidEvents,
|
|
432
|
+
fileHash,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function readCurationEvents(filePath) {
|
|
437
|
+
const { values, malformedLines, oversizedLines, fileHash } = readJsonlRecords(filePath);
|
|
438
|
+
const { events, invalidEvents, quarantinedEvents } = sanitizeCurationEvents(values);
|
|
439
|
+
return {
|
|
440
|
+
events,
|
|
441
|
+
malformedLines,
|
|
442
|
+
oversizedLines,
|
|
443
|
+
invalidEvents,
|
|
444
|
+
quarantinedEvents,
|
|
445
|
+
fileHash,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function readRetentionEvents(filePath) {
|
|
450
|
+
const { values, malformedLines, oversizedLines, fileHash } = readJsonlRecords(filePath);
|
|
451
|
+
const { events, invalidEvents } = sanitizeRetentionEvents(values);
|
|
452
|
+
return {
|
|
453
|
+
events,
|
|
454
|
+
malformedLines,
|
|
455
|
+
oversizedLines,
|
|
456
|
+
invalidEvents,
|
|
457
|
+
fileHash,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function loadDiagnosticState(options = {}) {
|
|
462
|
+
const storePath = options.storePath || defaultStorePath();
|
|
463
|
+
const reviewPath = options.reviewPath || defaultReviewPath();
|
|
464
|
+
const curationPath = options.curationPath || defaultCurationPath();
|
|
465
|
+
const retentionPath = options.retentionPath || defaultRetentionPath();
|
|
466
|
+
const backupDir = options.backupDir || defaultBackupDir();
|
|
467
|
+
const vents = readVentRecords(storePath);
|
|
468
|
+
const reviews = readReviewEvents(reviewPath);
|
|
469
|
+
const curations = readCurationEvents(curationPath);
|
|
470
|
+
const retentions = readRetentionEvents(retentionPath);
|
|
471
|
+
return {
|
|
472
|
+
storePath,
|
|
473
|
+
reviewPath,
|
|
474
|
+
curationPath,
|
|
475
|
+
retentionPath,
|
|
476
|
+
backupDir,
|
|
477
|
+
records: vents.records,
|
|
478
|
+
ventsHash: vents.fileHash,
|
|
479
|
+
reviewEvents: reviews.events,
|
|
480
|
+
reviewEventsHash: reviews.fileHash,
|
|
481
|
+
curationEvents: curations.events,
|
|
482
|
+
curationEventsHash: curations.fileHash,
|
|
483
|
+
retentionEvents: retentions.events,
|
|
484
|
+
retentionEventsHash: retentions.fileHash,
|
|
485
|
+
malformedLines: vents.malformedLines,
|
|
486
|
+
malformedReviewLines: reviews.malformedLines,
|
|
487
|
+
malformedCurationLines: curations.malformedLines,
|
|
488
|
+
malformedRetentionLines: retentions.malformedLines,
|
|
489
|
+
oversizedLines: vents.oversizedLines,
|
|
490
|
+
oversizedReviewLines: reviews.oversizedLines,
|
|
491
|
+
oversizedCurationLines: curations.oversizedLines,
|
|
492
|
+
oversizedRetentionLines: retentions.oversizedLines,
|
|
493
|
+
invalidRecords: vents.invalidRecords,
|
|
494
|
+
invalidReviewEvents: reviews.invalidEvents,
|
|
495
|
+
invalidCurationEvents: curations.invalidEvents,
|
|
496
|
+
invalidRetentionEvents: retentions.invalidEvents,
|
|
497
|
+
quarantinedCurationEvents: curations.quarantinedEvents,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function summarizeRecords(records, options = {}) {
|
|
502
|
+
const groupSummaries = buildGroupSummaries(records, options.curationEvents);
|
|
503
|
+
return {
|
|
504
|
+
totalRecords: records.length,
|
|
505
|
+
groupCount: groupSummaries.length,
|
|
506
|
+
candidateIncidentCount: groupSummaries.filter((group) => group.candidateIncident).length,
|
|
507
|
+
groups: groupSummaries.slice(0, clampLimit(options.limit, 20)),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function resolveRecurrenceGroup(records, recurrenceKey, curationEvents = []) {
|
|
512
|
+
const requestedRecurrenceKey = sanitizeDisplayText(recurrenceKey, 200);
|
|
513
|
+
if (!requestedRecurrenceKey) return undefined;
|
|
514
|
+
const curationMap = buildCurationMap(curationEvents);
|
|
515
|
+
const resolvedKey = resolveRecurrenceKey(requestedRecurrenceKey, curationMap);
|
|
516
|
+
const group = buildGroupSummaries(records, curationEvents).find(
|
|
517
|
+
(entry) => entry.recurrenceKey === resolvedKey,
|
|
518
|
+
);
|
|
519
|
+
if (!group) return undefined;
|
|
520
|
+
return {
|
|
521
|
+
requestedRecurrenceKey,
|
|
522
|
+
recurrenceKey: resolvedKey,
|
|
523
|
+
resolvedThroughCuration: requestedRecurrenceKey !== resolvedKey,
|
|
524
|
+
group,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function hasRecurrenceGroup(records, recurrenceKey, curationEvents = []) {
|
|
529
|
+
return Boolean(resolveRecurrenceGroup(records, recurrenceKey, curationEvents));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function latestReviewStates(reviewEvents, curationEvents = []) {
|
|
533
|
+
const states = new Map();
|
|
534
|
+
const curationMap = buildCurationMap(curationEvents);
|
|
535
|
+
for (const event of reviewEvents) {
|
|
536
|
+
if (!event?.recurrenceKey || !REVIEW_STATES.includes(event.state)) continue;
|
|
537
|
+
states.set(resolveRecurrenceKey(String(event.recurrenceKey), curationMap), event);
|
|
538
|
+
}
|
|
539
|
+
return states;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function summarizeReviewQueue(records, reviewEvents, options = {}) {
|
|
543
|
+
const stateFilter =
|
|
544
|
+
options.state === "all" ? "all" : options.state && normalizeReviewState(options.state);
|
|
545
|
+
const filters = normalizeReviewFilters(options.filters);
|
|
546
|
+
const allItems = buildReviewQueueItems(records, reviewEvents, options.curationEvents);
|
|
547
|
+
const facetFilteredItems = allItems.filter((item) => reviewItemMatchesFilters(item, filters));
|
|
548
|
+
const items = facetFilteredItems.filter((item) => {
|
|
549
|
+
if (stateFilter === "all") return true;
|
|
550
|
+
if (stateFilter) return item.reviewState === stateFilter;
|
|
551
|
+
return item.reviewState !== "dismissed";
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
totalRecords: records.length,
|
|
556
|
+
groupCount: allItems.length,
|
|
557
|
+
matchingGroupCount: facetFilteredItems.length,
|
|
558
|
+
reviewEventCount: reviewEvents.length,
|
|
559
|
+
queueCount: items.length,
|
|
560
|
+
stateFilter: stateFilter || "active",
|
|
561
|
+
filters,
|
|
562
|
+
hasFilters: hasReviewFilters(filters),
|
|
563
|
+
items: items.slice(0, clampLimit(options.limit, 20)),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function buildFacetSummary(input = {}) {
|
|
568
|
+
const records = input.records || [];
|
|
569
|
+
const reviewEvents = input.reviewEvents || [];
|
|
570
|
+
const curationEvents = input.curationEvents || [];
|
|
571
|
+
const groups = buildReviewQueueItems(records, reviewEvents, curationEvents);
|
|
572
|
+
const limit = clampLimit(input.limit, 10);
|
|
573
|
+
|
|
574
|
+
const recordCounts = {
|
|
575
|
+
categories: new Map(),
|
|
576
|
+
severities: new Map(),
|
|
577
|
+
tags: new Map(),
|
|
578
|
+
tools: new Map(),
|
|
579
|
+
packages: new Map(),
|
|
580
|
+
};
|
|
581
|
+
for (const record of records) {
|
|
582
|
+
incrementCount(recordCounts.categories, normalizeCategory(record.category));
|
|
583
|
+
incrementCount(recordCounts.severities, normalizeSeverity(record.severity));
|
|
584
|
+
if (record.tool) incrementCount(recordCounts.tools, sanitizeFacetText(record.tool, 160).value);
|
|
585
|
+
if (record.packageName)
|
|
586
|
+
incrementCount(recordCounts.packages, sanitizeFacetText(record.packageName, 200).value);
|
|
587
|
+
if (Array.isArray(record.tags)) {
|
|
588
|
+
for (const tag of record.tags) incrementCount(recordCounts.tags, recurrenceSlug(tag));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const groupCounts = {
|
|
593
|
+
reviewStates: new Map(),
|
|
594
|
+
categories: new Map(),
|
|
595
|
+
tags: new Map(),
|
|
596
|
+
tools: new Map(),
|
|
597
|
+
packages: new Map(),
|
|
598
|
+
};
|
|
599
|
+
for (const group of groups) {
|
|
600
|
+
incrementCount(groupCounts.reviewStates, group.reviewState);
|
|
601
|
+
for (const category of group.categories || []) incrementCount(groupCounts.categories, category);
|
|
602
|
+
for (const tag of group.tags || []) incrementCount(groupCounts.tags, tag);
|
|
603
|
+
for (const tool of group.tools || []) incrementCount(groupCounts.tools, tool);
|
|
604
|
+
for (const packageName of group.packages || [])
|
|
605
|
+
incrementCount(groupCounts.packages, packageName);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
610
|
+
classification: "local-diagnostic-user-data",
|
|
611
|
+
boundary:
|
|
612
|
+
"Read-only local diagnostic facet projection. Facets are caller-supplied local labels, not owner routing, evidence, tasks, issues, incidents, telemetry, publication, or ASC/self state.",
|
|
613
|
+
totalRecords: records.length,
|
|
614
|
+
groupCount: groups.length,
|
|
615
|
+
records: mapFacetCounts(recordCounts, limit),
|
|
616
|
+
groups: mapFacetCounts(groupCounts, limit),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function buildReviewOutcomes(input = {}) {
|
|
621
|
+
const records = input.records || [];
|
|
622
|
+
const reviewEvents = input.reviewEvents || [];
|
|
623
|
+
const curationEvents = input.curationEvents || [];
|
|
624
|
+
const stateFilter =
|
|
625
|
+
input.state === "all" || input.state === undefined ? "all" : normalizeReviewState(input.state);
|
|
626
|
+
const filters = normalizeReviewFilters(input.filters);
|
|
627
|
+
const allItems = buildReviewQueueItems(records, reviewEvents, curationEvents);
|
|
628
|
+
const matchingItems = allItems.filter((item) => reviewItemMatchesFilters(item, filters));
|
|
629
|
+
const visibleStates = stateFilter === "all" ? REVIEW_STATES : [stateFilter];
|
|
630
|
+
const counts = Object.fromEntries(REVIEW_STATES.map((state) => [state, 0]));
|
|
631
|
+
for (const item of matchingItems) counts[item.reviewState] += 1;
|
|
632
|
+
const limit = clampLimit(input.limit, 5);
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
636
|
+
classification: "local-diagnostic-user-data",
|
|
637
|
+
boundary:
|
|
638
|
+
"Read-only local diagnostic review-outcome projection. No AK task, GitHub issue, incident, evidence, telemetry, publication, owner assignment, or ASC/self state mutation occurred.",
|
|
639
|
+
totalRecords: records.length,
|
|
640
|
+
groupCount: allItems.length,
|
|
641
|
+
matchingGroupCount: matchingItems.length,
|
|
642
|
+
stateFilter,
|
|
643
|
+
filters,
|
|
644
|
+
hasFilters: hasReviewFilters(filters),
|
|
645
|
+
counts,
|
|
646
|
+
limitPerBucket: limit,
|
|
647
|
+
buckets: visibleStates.map((state) => {
|
|
648
|
+
const items = matchingItems.filter((item) => item.reviewState === state);
|
|
649
|
+
return {
|
|
650
|
+
state,
|
|
651
|
+
count: items.length,
|
|
652
|
+
description: reviewOutcomeDescription(state),
|
|
653
|
+
items: items.slice(0, limit),
|
|
654
|
+
};
|
|
655
|
+
}),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function buildReviewComparison(input = {}) {
|
|
660
|
+
const records = input.records || [];
|
|
661
|
+
const reviewEvents = input.reviewEvents || [];
|
|
662
|
+
const curationEvents = input.curationEvents || [];
|
|
663
|
+
const filters = normalizeReviewFilters(input.filters);
|
|
664
|
+
const allItems = buildReviewQueueItems(records, reviewEvents, curationEvents);
|
|
665
|
+
const matchingItems = allItems.filter((item) => reviewItemMatchesFilters(item, filters));
|
|
666
|
+
const limit = clampLimit(input.limit, 5);
|
|
667
|
+
const totals = Object.fromEntries(
|
|
668
|
+
REVIEW_STATES.map((state) => [
|
|
669
|
+
state,
|
|
670
|
+
{ groups: 0, records: 0, candidateIncidents: 0, criticalGroups: 0 },
|
|
671
|
+
]),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
for (const item of matchingItems) {
|
|
675
|
+
const total = totals[item.reviewState];
|
|
676
|
+
total.groups += 1;
|
|
677
|
+
total.records += item.count;
|
|
678
|
+
if (item.candidateIncident) total.candidateIncidents += 1;
|
|
679
|
+
if (item.maxSeverity === "critical") total.criticalGroups += 1;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
684
|
+
classification: "local-diagnostic-user-data",
|
|
685
|
+
boundary:
|
|
686
|
+
"Read-only local diagnostic review-state comparison projection. No AK task, GitHub issue, incident, evidence, telemetry, publication, owner assignment, archive, restore, or ASC/self state mutation occurred; no retention confirmation tokens are emitted here.",
|
|
687
|
+
totalRecords: records.length,
|
|
688
|
+
groupCount: allItems.length,
|
|
689
|
+
matchingGroupCount: matchingItems.length,
|
|
690
|
+
filters,
|
|
691
|
+
hasFilters: hasReviewFilters(filters),
|
|
692
|
+
limitPerState: limit,
|
|
693
|
+
totals,
|
|
694
|
+
buckets: REVIEW_STATES.map((state) => {
|
|
695
|
+
const items = matchingItems.filter((item) => item.reviewState === state);
|
|
696
|
+
return {
|
|
697
|
+
state,
|
|
698
|
+
description: reviewOutcomeDescription(state),
|
|
699
|
+
...totals[state],
|
|
700
|
+
items: items.slice(0, limit),
|
|
701
|
+
};
|
|
702
|
+
}),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export function buildReviewDetail(input = {}) {
|
|
707
|
+
const recurrenceKey = sanitizeDisplayText(input.recurrenceKey, 200);
|
|
708
|
+
if (!recurrenceKey) throw new Error("agent_vent review detail requires a recurrenceKey");
|
|
709
|
+
|
|
710
|
+
const records = input.records || [];
|
|
711
|
+
const reviewEvents = input.reviewEvents || [];
|
|
712
|
+
const curationEvents = input.curationEvents || [];
|
|
713
|
+
const resolved = resolveRecurrenceGroup(records, recurrenceKey, curationEvents);
|
|
714
|
+
if (!resolved) {
|
|
715
|
+
throw new Error(`cannot inspect unknown recurrence group: ${recurrenceKey}`);
|
|
716
|
+
}
|
|
717
|
+
const curationMap = buildCurationMap(curationEvents);
|
|
718
|
+
const resolvedKey = resolved.recurrenceKey;
|
|
719
|
+
const group = buildReviewQueueItems(records, reviewEvents, curationEvents).find(
|
|
720
|
+
(item) => item.recurrenceKey === resolvedKey,
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
const samples = records
|
|
724
|
+
.filter(
|
|
725
|
+
(record) =>
|
|
726
|
+
resolveRecurrenceKey(
|
|
727
|
+
String(record.recurrenceKey || buildRecurrenceKey(record)),
|
|
728
|
+
curationMap,
|
|
729
|
+
) === resolvedKey,
|
|
730
|
+
)
|
|
731
|
+
.slice(-clampLimit(input.limit, 5))
|
|
732
|
+
.reverse()
|
|
733
|
+
.map((record) => ({
|
|
734
|
+
id: sanitizeDisplayText(record.id, 120),
|
|
735
|
+
createdAt: sanitizeDisplayText(record.createdAt, 80),
|
|
736
|
+
severity: normalizeSeverity(record.severity),
|
|
737
|
+
category: normalizeCategory(record.category),
|
|
738
|
+
tool: sanitizeFacetText(record.tool, 160).value,
|
|
739
|
+
packageName: sanitizeFacetText(record.packageName, 200).value,
|
|
740
|
+
summary: sanitizeDisplayText(record.summary, 300),
|
|
741
|
+
frustration: sanitizeDisplayText(record.frustration, 500),
|
|
742
|
+
evidence: sanitizeDisplayText(record.evidence, 500),
|
|
743
|
+
expected: sanitizeDisplayText(record.expected, 400),
|
|
744
|
+
actual: sanitizeDisplayText(record.actual, 400),
|
|
745
|
+
reproduction: sanitizeDisplayText(record.reproduction, 500),
|
|
746
|
+
tags: Array.isArray(record.tags)
|
|
747
|
+
? record.tags.map((tag) => recurrenceSlug(tag)).filter((tag) => tag !== "unspecified")
|
|
748
|
+
: [],
|
|
749
|
+
}));
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
753
|
+
classification: "local-diagnostic-user-data",
|
|
754
|
+
boundary:
|
|
755
|
+
"Read-only local diagnostic review projection. No AK task, GitHub issue, incident, evidence, telemetry, publication, or ASC/self state mutation occurred.",
|
|
756
|
+
recurrenceKey: resolvedKey,
|
|
757
|
+
requestedRecurrenceKey: recurrenceKey,
|
|
758
|
+
group,
|
|
759
|
+
samples,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export function buildLifecycleSnapshot(input = {}) {
|
|
764
|
+
const records = input.records || [];
|
|
765
|
+
const reviewEvents = input.reviewEvents || [];
|
|
766
|
+
const curationEvents = input.curationEvents || [];
|
|
767
|
+
const retentionEvents = input.retentionEvents || [];
|
|
768
|
+
const storePath = input.storePath || defaultStorePath();
|
|
769
|
+
const reviewPath = input.reviewPath || defaultReviewPath();
|
|
770
|
+
const curationPath = input.curationPath || defaultCurationPath();
|
|
771
|
+
const retentionPath = input.retentionPath || defaultRetentionPath();
|
|
772
|
+
const backupDir = input.backupDir || defaultBackupDir();
|
|
773
|
+
const filters = normalizeReviewFilters(input.filters);
|
|
774
|
+
const hasFilters = hasReviewFilters(filters);
|
|
775
|
+
const stateFilter =
|
|
776
|
+
input.state === "all" || input.state === undefined ? "all" : normalizeReviewState(input.state);
|
|
777
|
+
const curationMap = buildCurationMap(curationEvents);
|
|
778
|
+
const allReviewItems = buildReviewQueueItems(records, reviewEvents, curationEvents);
|
|
779
|
+
const facetScopedRecords = hasFilters
|
|
780
|
+
? records.filter((record) => recordMatchesReviewFilters(record, filters))
|
|
781
|
+
: records;
|
|
782
|
+
const facetScopedReviewItems = buildReviewQueueItems(
|
|
783
|
+
facetScopedRecords,
|
|
784
|
+
reviewEvents,
|
|
785
|
+
curationEvents,
|
|
786
|
+
);
|
|
787
|
+
const scopedReviewItems = facetScopedReviewItems.filter((item) =>
|
|
788
|
+
stateFilter === "all" ? true : item.reviewState === stateFilter,
|
|
789
|
+
);
|
|
790
|
+
const scopedKeys = new Set(scopedReviewItems.map((item) => item.recurrenceKey));
|
|
791
|
+
const scopedRecords = facetScopedRecords.filter((record) =>
|
|
792
|
+
scopedKeys.has(
|
|
793
|
+
resolveRecurrenceKey(String(record.recurrenceKey || buildRecurrenceKey(record)), curationMap),
|
|
794
|
+
),
|
|
795
|
+
);
|
|
796
|
+
const reviewStateCounts = Object.fromEntries(REVIEW_STATES.map((state) => [state, 0]));
|
|
797
|
+
for (const item of scopedReviewItems) {
|
|
798
|
+
reviewStateCounts[item.reviewState] += 1;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const exportLimit = clampLimit(input.limit, 20);
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
805
|
+
classification: "local-diagnostic-user-data",
|
|
806
|
+
boundary:
|
|
807
|
+
"Local diagnostic projection only; no AK task, GitHub issue, incident, evidence, telemetry, publication, owner assignment, or ASC/self state was created. Facet filters are local diagnostic labels only, not owner routing.",
|
|
808
|
+
scope: {
|
|
809
|
+
hasFilters,
|
|
810
|
+
filters,
|
|
811
|
+
stateFilter,
|
|
812
|
+
totalRecords: records.length,
|
|
813
|
+
totalGroups: allReviewItems.length,
|
|
814
|
+
facetMatchingRecords: facetScopedRecords.length,
|
|
815
|
+
facetMatchingGroups: facetScopedReviewItems.length,
|
|
816
|
+
matchingRecords: scopedRecords.length,
|
|
817
|
+
matchingGroups: scopedReviewItems.length,
|
|
818
|
+
},
|
|
819
|
+
paths: {
|
|
820
|
+
vents: storePath,
|
|
821
|
+
reviewEvents: reviewPath,
|
|
822
|
+
curationEvents: curationPath,
|
|
823
|
+
retentionEvents: retentionPath,
|
|
824
|
+
backups: backupDir,
|
|
825
|
+
},
|
|
826
|
+
files: {
|
|
827
|
+
vents: jsonlFileInfo(storePath),
|
|
828
|
+
reviewEvents: jsonlFileInfo(reviewPath),
|
|
829
|
+
curationEvents: jsonlFileInfo(curationPath),
|
|
830
|
+
retentionEvents: jsonlFileInfo(retentionPath),
|
|
831
|
+
},
|
|
832
|
+
malformedLines: {
|
|
833
|
+
vents: input.malformedLines || 0,
|
|
834
|
+
reviewEvents: input.malformedReviewLines || 0,
|
|
835
|
+
curationEvents: input.malformedCurationLines || 0,
|
|
836
|
+
retentionEvents: input.malformedRetentionLines || 0,
|
|
837
|
+
},
|
|
838
|
+
oversizedLines: {
|
|
839
|
+
vents: input.oversizedLines || 0,
|
|
840
|
+
reviewEvents: input.oversizedReviewLines || 0,
|
|
841
|
+
curationEvents: input.oversizedCurationLines || 0,
|
|
842
|
+
retentionEvents: input.oversizedRetentionLines || 0,
|
|
843
|
+
},
|
|
844
|
+
invalidEntries: {
|
|
845
|
+
vents: input.invalidRecords || 0,
|
|
846
|
+
reviewEvents: input.invalidReviewEvents || 0,
|
|
847
|
+
curationEvents: input.invalidCurationEvents || 0,
|
|
848
|
+
retentionEvents: input.invalidRetentionEvents || 0,
|
|
849
|
+
quarantinedCurationEvents: input.quarantinedCurationEvents || 0,
|
|
850
|
+
},
|
|
851
|
+
counts: {
|
|
852
|
+
vents: scopedRecords.length,
|
|
853
|
+
recurrenceGroups: scopedReviewItems.length,
|
|
854
|
+
reviewEvents: reviewEvents.length,
|
|
855
|
+
curationEvents: curationEvents.length,
|
|
856
|
+
retentionEvents: retentionEvents.length,
|
|
857
|
+
candidateIncidents: scopedReviewItems.filter((item) => item.candidateIncident).length,
|
|
858
|
+
reviewStates: reviewStateCounts,
|
|
859
|
+
},
|
|
860
|
+
summary: summarizeRecords(scopedRecords, {
|
|
861
|
+
limit: exportLimit,
|
|
862
|
+
curationEvents,
|
|
863
|
+
}),
|
|
864
|
+
reviewQueue: summarizeReviewQueue(scopedRecords, reviewEvents, {
|
|
865
|
+
state: "all",
|
|
866
|
+
limit: exportLimit,
|
|
867
|
+
curationEvents,
|
|
868
|
+
}),
|
|
869
|
+
nextActions: buildExportNextActions(scopedReviewItems, {
|
|
870
|
+
limit: exportLimit,
|
|
871
|
+
filters,
|
|
872
|
+
}),
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function formatRecord(record) {
|
|
877
|
+
return [
|
|
878
|
+
`- ${record.createdAt || "unknown-time"} [${record.severity || "medium"}/${record.category || "other"}] ${record.summary || "(no summary)"}`,
|
|
879
|
+
` recurrence: ${record.recurrenceKey || "unknown"}`,
|
|
880
|
+
].join("\n");
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export function formatSummary(summary) {
|
|
884
|
+
if (summary.totalRecords === 0) {
|
|
885
|
+
return "No agent vent records found yet.";
|
|
886
|
+
}
|
|
887
|
+
const lines = [
|
|
888
|
+
`Agent vent summary: ${summary.totalRecords} record(s), ${summary.groupCount} recurrence group(s), ${summary.candidateIncidentCount} candidate incident(s).`,
|
|
889
|
+
];
|
|
890
|
+
for (const group of summary.groups) {
|
|
891
|
+
const marker = group.candidateIncident ? "candidate incident for human review" : "watch";
|
|
892
|
+
lines.push(
|
|
893
|
+
`- ${group.recurrenceKey} — ${group.count}x, max=${group.maxSeverity}, ${marker}; latest: ${group.latestSummary}`,
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
return lines.join("\n");
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function formatFacetSummary(summary) {
|
|
900
|
+
if (summary.totalRecords === 0) {
|
|
901
|
+
return [
|
|
902
|
+
"No agent vent records found yet. Record minimized vents before reviewing local facets.",
|
|
903
|
+
`Boundary: ${summary.boundary}`,
|
|
904
|
+
].join("\n");
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const lines = [
|
|
908
|
+
`Agent vent facets: ${summary.totalRecords} record(s), ${summary.groupCount} recurrence group(s).`,
|
|
909
|
+
`Boundary: ${summary.boundary}`,
|
|
910
|
+
"Record facets:",
|
|
911
|
+
`- categories: ${formatFacetEntries(summary.records.categories)}`,
|
|
912
|
+
`- severities: ${formatFacetEntries(summary.records.severities)}`,
|
|
913
|
+
`- tags: ${formatFacetEntries(summary.records.tags)}`,
|
|
914
|
+
`- tools: ${formatFacetEntries(summary.records.tools)}`,
|
|
915
|
+
`- packages: ${formatFacetEntries(summary.records.packages)}`,
|
|
916
|
+
"Group facets:",
|
|
917
|
+
`- review states: ${formatFacetEntries(summary.groups.reviewStates)}`,
|
|
918
|
+
`- categories: ${formatFacetEntries(summary.groups.categories)}`,
|
|
919
|
+
`- tags: ${formatFacetEntries(summary.groups.tags)}`,
|
|
920
|
+
`- tools: ${formatFacetEntries(summary.groups.tools)}`,
|
|
921
|
+
`- packages: ${formatFacetEntries(summary.groups.packages)}`,
|
|
922
|
+
"Next: /agent_vent review [state] [limit] [category=bug] [tag=reload] [tool=pi-reload] [package=tryinget-pi-agent-vent] | /agent_vent review show <recurrenceKey> [limit]",
|
|
923
|
+
];
|
|
924
|
+
return lines.join("\n");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function formatReviewOutcomes(outcomes) {
|
|
928
|
+
const filterText = formatReviewFilters(outcomes.filters);
|
|
929
|
+
if (outcomes.totalRecords === 0) {
|
|
930
|
+
return [
|
|
931
|
+
"No agent vent records found yet. Record minimized vents before reviewing outcome follow-up.",
|
|
932
|
+
outcomes.hasFilters
|
|
933
|
+
? `Filters requested: ${filterText}. Local diagnostic labels only; not owner routing.`
|
|
934
|
+
: undefined,
|
|
935
|
+
`Boundary: ${outcomes.boundary}`,
|
|
936
|
+
]
|
|
937
|
+
.filter(Boolean)
|
|
938
|
+
.join("\n");
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const scopeText = outcomes.hasFilters
|
|
942
|
+
? `${outcomes.matchingGroupCount} matching of ${outcomes.groupCount} total recurrence group(s)`
|
|
943
|
+
: `${outcomes.groupCount} total recurrence group(s)`;
|
|
944
|
+
const lines = [
|
|
945
|
+
`Agent vent review outcomes: ${scopeText}; state filter=${outcomes.stateFilter}; showing up to ${outcomes.limitPerBucket} group(s) per state bucket.`,
|
|
946
|
+
`State counts: new=${outcomes.counts.new}, acknowledged=${outcomes.counts.acknowledged}, dismissed=${outcomes.counts.dismissed}, escalation_drafted=${outcomes.counts.escalation_drafted}`,
|
|
947
|
+
outcomes.hasFilters
|
|
948
|
+
? `Filters: ${filterText}. Local diagnostic labels only; not owner routing or owner assignment.`
|
|
949
|
+
: undefined,
|
|
950
|
+
"States are local review markers only, not resolution, assignment, issue status, task truth, incident state, evidence, publication, or telemetry.",
|
|
951
|
+
`Boundary: ${outcomes.boundary}`,
|
|
952
|
+
].filter(Boolean);
|
|
953
|
+
|
|
954
|
+
for (const bucket of outcomes.buckets) {
|
|
955
|
+
lines.push("", `${bucket.state}: ${bucket.count} group(s) — ${bucket.description}`);
|
|
956
|
+
if (!bucket.items.length) {
|
|
957
|
+
lines.push("- none");
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
for (const item of bucket.items) {
|
|
961
|
+
const marker = item.candidateIncident ? "candidate incident for human review" : "watch";
|
|
962
|
+
lines.push(
|
|
963
|
+
`- ${item.recurrenceKey} — ${item.count}x, max=${item.maxSeverity}, ${marker}; latest: ${item.latestSummary}`,
|
|
964
|
+
);
|
|
965
|
+
lines.push(` decision posture: ${formatDecisionPosture(item)}`);
|
|
966
|
+
if (item.reviewNote) lines.push(` review note: ${item.reviewNote}`);
|
|
967
|
+
for (const followup of buildReviewOutcomeFollowupLines(item, outcomes.filters)) {
|
|
968
|
+
lines.push(` ${followup}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (outcomes.hasFilters) {
|
|
973
|
+
lines.push(
|
|
974
|
+
"Filter note: category/tag/tool/package values are local diagnostic labels only, not owner routing or owner assignment.",
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
return lines.join("\n");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
export function formatReviewComparison(comparison) {
|
|
981
|
+
const filterText = formatReviewFilters(comparison.filters);
|
|
982
|
+
if (comparison.totalRecords === 0) {
|
|
983
|
+
return [
|
|
984
|
+
"No agent vent records found yet. Record minimized vents before comparing local review states.",
|
|
985
|
+
comparison.hasFilters
|
|
986
|
+
? `Filters requested: ${filterText}. Local diagnostic labels only; not owner routing.`
|
|
987
|
+
: undefined,
|
|
988
|
+
`Boundary: ${comparison.boundary}`,
|
|
989
|
+
]
|
|
990
|
+
.filter(Boolean)
|
|
991
|
+
.join("\n");
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const scopeText = comparison.hasFilters
|
|
995
|
+
? `${comparison.matchingGroupCount} matching of ${comparison.groupCount} total recurrence group(s)`
|
|
996
|
+
: `${comparison.groupCount} total recurrence group(s)`;
|
|
997
|
+
const lines = [
|
|
998
|
+
`Agent vent review comparison: ${scopeText}; showing up to ${comparison.limitPerState} group(s) per state bucket.`,
|
|
999
|
+
comparison.hasFilters
|
|
1000
|
+
? `Filters: ${filterText}. Local diagnostic labels only; not owner routing or owner assignment.`
|
|
1001
|
+
: undefined,
|
|
1002
|
+
"States are local review markers only, not resolution, assignment, issue status, task truth, incident state, evidence, publication, or telemetry.",
|
|
1003
|
+
"This comparison is read-only and intentionally emits no archive or restore confirmation tokens.",
|
|
1004
|
+
`Boundary: ${comparison.boundary}`,
|
|
1005
|
+
"",
|
|
1006
|
+
"State totals:",
|
|
1007
|
+
].filter(Boolean);
|
|
1008
|
+
|
|
1009
|
+
for (const state of REVIEW_STATES) {
|
|
1010
|
+
const total = comparison.totals[state];
|
|
1011
|
+
lines.push(
|
|
1012
|
+
`- ${state}: groups=${total.groups}, records=${total.records}, candidateIncidents=${total.candidateIncidents}, criticalGroups=${total.criticalGroups}`,
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
for (const bucket of comparison.buckets) {
|
|
1017
|
+
lines.push(
|
|
1018
|
+
"",
|
|
1019
|
+
`${bucket.state}: ${bucket.groups} group(s), ${bucket.records} record(s), ${bucket.candidateIncidents} candidate incident(s) — ${bucket.description}`,
|
|
1020
|
+
);
|
|
1021
|
+
if (!bucket.items.length) {
|
|
1022
|
+
lines.push("- none");
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
for (const item of bucket.items) {
|
|
1026
|
+
const marker = item.candidateIncident ? "candidate incident for human review" : "watch";
|
|
1027
|
+
lines.push(
|
|
1028
|
+
`- ${item.recurrenceKey} — ${item.count}x, max=${item.maxSeverity}, ${marker}; latest: ${item.latestSummary}`,
|
|
1029
|
+
);
|
|
1030
|
+
lines.push(` decision posture: ${formatDecisionPosture(item)}`);
|
|
1031
|
+
lines.push(
|
|
1032
|
+
` inspect: ${formatAgentVentCommand("review", "show", item.recurrenceKey)} [limit]`,
|
|
1033
|
+
);
|
|
1034
|
+
lines.push(
|
|
1035
|
+
` outcomes: ${formatAgentVentCommandWithFilters(comparison.filters, "outcomes", item.reviewState)} [per-state-limit]`,
|
|
1036
|
+
);
|
|
1037
|
+
lines.push(
|
|
1038
|
+
` ${formatExportBucketLine("export bucket", item.reviewState, comparison.filters)}`,
|
|
1039
|
+
);
|
|
1040
|
+
if (item.reviewState === "new") {
|
|
1041
|
+
lines.push(
|
|
1042
|
+
` choose local outcome: ${formatAgentVentCommand("review", "set", "acknowledged", item.recurrenceKey)} [note] | ${formatAgentVentCommand("review", "set", "dismissed", item.recurrenceKey)} [note] | ${formatAgentVentCommand("review", "set", "escalation_drafted", item.recurrenceKey)} [note]`,
|
|
1043
|
+
);
|
|
1044
|
+
} else {
|
|
1045
|
+
lines.push(
|
|
1046
|
+
` retention planning: ${formatAgentVentCommandWithFilters(comparison.filters, "retention", "candidates", item.reviewState)} [limit]`,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
lines.push(
|
|
1050
|
+
" boundary: local diagnostics only; no owner routing, assignment, filing, task creation, incident declaration, evidence, publication, archive, restore, telemetry, or ASC/self mutation occurred",
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (comparison.hasFilters) {
|
|
1055
|
+
lines.push(
|
|
1056
|
+
"Filter note: category/tag/tool/package values are local diagnostic labels only, not owner routing or owner assignment.",
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
return lines.join("\n");
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
export function formatReviewQueue(queue) {
|
|
1063
|
+
const filterText = formatReviewFilters(queue.filters);
|
|
1064
|
+
const filterSuffix = queue.hasFilters ? ` for filters: ${filterText}` : "";
|
|
1065
|
+
if (queue.totalRecords === 0) {
|
|
1066
|
+
return [
|
|
1067
|
+
"No agent vent records found yet. Record minimized vents before reviewing recurrence groups.",
|
|
1068
|
+
queue.hasFilters
|
|
1069
|
+
? `Filters requested: ${filterText}. Local diagnostic labels only; not owner routing.`
|
|
1070
|
+
: undefined,
|
|
1071
|
+
]
|
|
1072
|
+
.filter(Boolean)
|
|
1073
|
+
.join("\n");
|
|
1074
|
+
}
|
|
1075
|
+
if (queue.queueCount === 0) {
|
|
1076
|
+
return [
|
|
1077
|
+
`Agent vent review queue: no ${queue.stateFilter} recurrence group(s) to show${filterSuffix}.`,
|
|
1078
|
+
queue.hasFilters
|
|
1079
|
+
? "Filters are local diagnostic labels only; not owner routing or owner assignment."
|
|
1080
|
+
: undefined,
|
|
1081
|
+
"Boundary: local review state only; no AK task, GitHub issue, incident, evidence, telemetry, or ASC/self state was created.",
|
|
1082
|
+
]
|
|
1083
|
+
.filter(Boolean)
|
|
1084
|
+
.join("\n");
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const scopeText = queue.hasFilters
|
|
1088
|
+
? `${queue.matchingGroupCount} matching of ${queue.groupCount} total group(s)`
|
|
1089
|
+
: `${queue.groupCount} total group(s)`;
|
|
1090
|
+
const lines = [
|
|
1091
|
+
`Agent vent review queue: ${queue.items.length} of ${queue.queueCount} ${queue.stateFilter} recurrence group(s) shown (${scopeText}).`,
|
|
1092
|
+
queue.hasFilters
|
|
1093
|
+
? `Filters: ${filterText}. Local diagnostic labels only; not owner routing or owner assignment.`
|
|
1094
|
+
: undefined,
|
|
1095
|
+
"States: new -> acknowledged | dismissed | escalation_drafted. Review state is local diagnostic state only.",
|
|
1096
|
+
"Boundary: no AK task, GitHub issue, incident, evidence, telemetry, or ASC/self state was created.",
|
|
1097
|
+
].filter(Boolean);
|
|
1098
|
+
for (const item of queue.items) {
|
|
1099
|
+
const marker = item.candidateIncident ? "candidate incident for human review" : "watch";
|
|
1100
|
+
const facets = [
|
|
1101
|
+
item.categories?.length ? `categories=${item.categories.join(",")}` : undefined,
|
|
1102
|
+
item.tags?.length ? `tags=${item.tags.join(",")}` : undefined,
|
|
1103
|
+
item.tools?.length ? `tools=${item.tools.join(",")}` : undefined,
|
|
1104
|
+
item.packages?.length ? `packages=${item.packages.join(",")}` : undefined,
|
|
1105
|
+
]
|
|
1106
|
+
.filter(Boolean)
|
|
1107
|
+
.join("; ");
|
|
1108
|
+
const facetText = facets ? `; ${facets}` : "";
|
|
1109
|
+
lines.push(
|
|
1110
|
+
`- [${item.reviewState}] ${item.recurrenceKey} — ${item.count}x, max=${item.maxSeverity}, ${marker}${facetText}; latest: ${item.latestSummary}`,
|
|
1111
|
+
);
|
|
1112
|
+
lines.push(` decision posture: ${formatDecisionPosture(item)}`);
|
|
1113
|
+
if (item.reviewNote) lines.push(` review note: ${item.reviewNote}`);
|
|
1114
|
+
lines.push(` human review hints: ${formatReviewGuidance(item)}`);
|
|
1115
|
+
lines.push(
|
|
1116
|
+
` inspect: ${formatAgentVentCommand("review", "show", item.recurrenceKey)} [limit]`,
|
|
1117
|
+
);
|
|
1118
|
+
for (const nextAction of buildReviewNextActionLines(item, { compact: true })) {
|
|
1119
|
+
lines.push(` ${nextAction}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (queue.hasFilters) {
|
|
1123
|
+
lines.push(
|
|
1124
|
+
"Filter note: category/tag/tool/package values are local diagnostic labels only, not owner routing or owner assignment.",
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
return lines.join("\n");
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
export function formatReviewDetail(detail) {
|
|
1131
|
+
const group = detail.group;
|
|
1132
|
+
const lines = [
|
|
1133
|
+
`Agent vent review detail for ${detail.recurrenceKey}: ${group.count} local diagnostic record(s).`,
|
|
1134
|
+
`Review state: ${group.reviewState}; max severity: ${group.maxSeverity}; candidate incident for human review: ${group.candidateIncident ? "yes" : "no"}`,
|
|
1135
|
+
`Boundary: ${detail.boundary}`,
|
|
1136
|
+
];
|
|
1137
|
+
if (detail.requestedRecurrenceKey !== detail.recurrenceKey) {
|
|
1138
|
+
lines.push(`Requested key resolved through local curation: ${detail.requestedRecurrenceKey}`);
|
|
1139
|
+
}
|
|
1140
|
+
if (group.categories?.length) lines.push(`Categories: ${group.categories.join(", ")}`);
|
|
1141
|
+
if (group.tags?.length) lines.push(`Tags: ${group.tags.join(", ")}`);
|
|
1142
|
+
if (group.tools?.length) lines.push(`Tools: ${group.tools.join(", ")}`);
|
|
1143
|
+
if (group.packages?.length) lines.push(`Packages: ${group.packages.join(", ")}`);
|
|
1144
|
+
lines.push(`Decision posture: ${formatDecisionPosture(group)}`);
|
|
1145
|
+
if (group.reviewNote) lines.push(`Review note: ${group.reviewNote}`);
|
|
1146
|
+
lines.push(`Human review hints: ${formatReviewGuidance(group)}`);
|
|
1147
|
+
lines.push("", "Representative local samples:");
|
|
1148
|
+
for (const sample of detail.samples || []) {
|
|
1149
|
+
lines.push(
|
|
1150
|
+
`- ${sample.id || "unknown-id"} ${sample.createdAt || "unknown-time"} [${sample.severity}/${sample.category}] ${sample.summary || "(no summary)"}`,
|
|
1151
|
+
);
|
|
1152
|
+
if (sample.tags?.length) lines.push(` tags: ${sample.tags.join(", ")}`);
|
|
1153
|
+
if (sample.tool) lines.push(` tool: ${sample.tool}`);
|
|
1154
|
+
if (sample.packageName) lines.push(` package: ${sample.packageName}`);
|
|
1155
|
+
if (sample.frustration) lines.push(` frustration: ${sample.frustration}`);
|
|
1156
|
+
if (sample.evidence) lines.push(` evidence: ${sample.evidence}`);
|
|
1157
|
+
if (sample.expected) lines.push(` expected: ${sample.expected}`);
|
|
1158
|
+
if (sample.actual) lines.push(` actual: ${sample.actual}`);
|
|
1159
|
+
if (sample.reproduction) lines.push(` reproduction: ${sample.reproduction}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (!detail.samples?.length) lines.push("No sample vents available.");
|
|
1162
|
+
lines.push("", "Local next actions:");
|
|
1163
|
+
for (const nextAction of buildReviewNextActionLines(group)) {
|
|
1164
|
+
lines.push(`- ${nextAction}`);
|
|
1165
|
+
}
|
|
1166
|
+
return lines.join("\n");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export function formatRecent(records, limit = DEFAULT_LIMIT) {
|
|
1170
|
+
const recent = records.slice(-clampLimit(limit)).reverse();
|
|
1171
|
+
if (recent.length === 0) return "No agent vent records found yet.";
|
|
1172
|
+
return recent.map(formatRecord).join("\n");
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export function formatPath(
|
|
1176
|
+
storePath = defaultStorePath(),
|
|
1177
|
+
reviewPath = defaultReviewPath(),
|
|
1178
|
+
curationPath = defaultCurationPath(),
|
|
1179
|
+
retentionPath = defaultRetentionPath(),
|
|
1180
|
+
backupDir = defaultBackupDir(),
|
|
1181
|
+
) {
|
|
1182
|
+
return [
|
|
1183
|
+
`Agent vent store: ${storePath}`,
|
|
1184
|
+
`Agent vent review events: ${reviewPath}`,
|
|
1185
|
+
`Agent vent curation events: ${curationPath}`,
|
|
1186
|
+
`Agent vent retention events: ${retentionPath}`,
|
|
1187
|
+
`Agent vent retention backups: ${backupDir}`,
|
|
1188
|
+
"Schema: append-only JSONL events plus confirmation-gated local retention backup artifacts.",
|
|
1189
|
+
"Override: set PI_AGENT_VENT_DIR to use a different private directory.",
|
|
1190
|
+
"Authority boundary: records, review states, and curation projections are local diagnostics, not tasks, issues, incidents, evidence, telemetry, or ASC/self state; retention receipts and backups are local diagnostics too.",
|
|
1191
|
+
].join("\n");
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
export function formatLifecycleStats(snapshot) {
|
|
1195
|
+
return [
|
|
1196
|
+
"Agent vent lifecycle stats:",
|
|
1197
|
+
`- vents: ${snapshot.counts.vents} record(s), ${snapshot.files.vents.sizeBytes} byte(s), exists=${snapshot.files.vents.exists}`,
|
|
1198
|
+
`- recurrence groups: ${snapshot.counts.recurrenceGroups}; candidate incidents for human review: ${snapshot.counts.candidateIncidents}`,
|
|
1199
|
+
`- review events: ${snapshot.counts.reviewEvents} event(s), ${snapshot.files.reviewEvents.sizeBytes} byte(s), exists=${snapshot.files.reviewEvents.exists}`,
|
|
1200
|
+
`- curation events: ${snapshot.counts.curationEvents} event(s), ${snapshot.files.curationEvents.sizeBytes} byte(s), exists=${snapshot.files.curationEvents.exists}`,
|
|
1201
|
+
`- retention events: ${snapshot.counts.retentionEvents} event(s), ${snapshot.files.retentionEvents.sizeBytes} byte(s), exists=${snapshot.files.retentionEvents.exists}`,
|
|
1202
|
+
`- review states: new=${snapshot.counts.reviewStates.new}, acknowledged=${snapshot.counts.reviewStates.acknowledged}, dismissed=${snapshot.counts.reviewStates.dismissed}, escalation_drafted=${snapshot.counts.reviewStates.escalation_drafted}`,
|
|
1203
|
+
`- malformed lines: vents=${snapshot.malformedLines.vents}, reviewEvents=${snapshot.malformedLines.reviewEvents}, curationEvents=${snapshot.malformedLines.curationEvents}, retentionEvents=${snapshot.malformedLines.retentionEvents}`,
|
|
1204
|
+
`- oversized lines: vents=${snapshot.oversizedLines.vents}, reviewEvents=${snapshot.oversizedLines.reviewEvents}, curationEvents=${snapshot.oversizedLines.curationEvents}, retentionEvents=${snapshot.oversizedLines.retentionEvents}`,
|
|
1205
|
+
`- invalid/quarantined entries: vents=${snapshot.invalidEntries.vents}, reviewEvents=${snapshot.invalidEntries.reviewEvents}, curationEvents=${snapshot.invalidEntries.curationEvents}, retentionEvents=${snapshot.invalidEntries.retentionEvents}, quarantinedCurationEvents=${snapshot.invalidEntries.quarantinedCurationEvents}`,
|
|
1206
|
+
snapshot.scope?.hasFilters
|
|
1207
|
+
? `- scope filters: ${formatReviewFilters(snapshot.scope.filters)} (${snapshot.scope.matchingGroups} matching of ${snapshot.scope.totalGroups} total group(s))`
|
|
1208
|
+
: undefined,
|
|
1209
|
+
`- paths: ${snapshot.paths.vents}; ${snapshot.paths.reviewEvents}; ${snapshot.paths.curationEvents}`,
|
|
1210
|
+
`Boundary: ${snapshot.boundary}`,
|
|
1211
|
+
]
|
|
1212
|
+
.filter(Boolean)
|
|
1213
|
+
.join("\n");
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
export function formatExportMarkdown(snapshot) {
|
|
1217
|
+
const lines = [
|
|
1218
|
+
"# Agent vent local diagnostic export",
|
|
1219
|
+
"",
|
|
1220
|
+
`Generated: ${snapshot.generatedAt}`,
|
|
1221
|
+
`Classification: ${snapshot.classification}`,
|
|
1222
|
+
`Boundary: ${snapshot.boundary}`,
|
|
1223
|
+
"",
|
|
1224
|
+
"## Scope",
|
|
1225
|
+
"",
|
|
1226
|
+
snapshot.scope?.hasFilters
|
|
1227
|
+
? `- Filters: ${formatReviewFilters(snapshot.scope.filters)} (local diagnostic labels only; not owner routing or owner assignment)`
|
|
1228
|
+
: "- Filters: none",
|
|
1229
|
+
snapshot.scope?.hasFilters
|
|
1230
|
+
? `- Matching groups: ${snapshot.scope.matchingGroups} of ${snapshot.scope.totalGroups}`
|
|
1231
|
+
: `- Groups: ${snapshot.counts.recurrenceGroups}`,
|
|
1232
|
+
snapshot.scope?.hasFilters
|
|
1233
|
+
? `- Matching vent records: ${snapshot.scope.matchingRecords} of ${snapshot.scope.totalRecords}`
|
|
1234
|
+
: `- Vent records: ${snapshot.counts.vents}`,
|
|
1235
|
+
"- Export is a local diagnostic projection only, not evidence, publication, owner routing, task truth, issue truth, or incident truth.",
|
|
1236
|
+
"",
|
|
1237
|
+
"## Stats",
|
|
1238
|
+
"",
|
|
1239
|
+
`- Vent records in scope: ${snapshot.counts.vents}`,
|
|
1240
|
+
`- Recurrence groups in scope: ${snapshot.counts.recurrenceGroups}`,
|
|
1241
|
+
`- Candidate incidents for human review in scope: ${snapshot.counts.candidateIncidents}`,
|
|
1242
|
+
`- Review events: ${snapshot.counts.reviewEvents}`,
|
|
1243
|
+
`- Curation events: ${snapshot.counts.curationEvents}`,
|
|
1244
|
+
`- Retention events: ${snapshot.counts.retentionEvents}`,
|
|
1245
|
+
`- Review states: ${JSON.stringify(snapshot.counts.reviewStates)}`,
|
|
1246
|
+
`- Malformed lines: vents=${snapshot.malformedLines.vents}, reviewEvents=${snapshot.malformedLines.reviewEvents}, curationEvents=${snapshot.malformedLines.curationEvents}, retentionEvents=${snapshot.malformedLines.retentionEvents}`,
|
|
1247
|
+
`- Oversized lines: vents=${snapshot.oversizedLines.vents}, reviewEvents=${snapshot.oversizedLines.reviewEvents}, curationEvents=${snapshot.oversizedLines.curationEvents}, retentionEvents=${snapshot.oversizedLines.retentionEvents}`,
|
|
1248
|
+
`- Invalid/quarantined entries: vents=${snapshot.invalidEntries.vents}, reviewEvents=${snapshot.invalidEntries.reviewEvents}, curationEvents=${snapshot.invalidEntries.curationEvents}, retentionEvents=${snapshot.invalidEntries.retentionEvents}, quarantinedCurationEvents=${snapshot.invalidEntries.quarantinedCurationEvents}`,
|
|
1249
|
+
"",
|
|
1250
|
+
"## Review queue",
|
|
1251
|
+
"",
|
|
1252
|
+
];
|
|
1253
|
+
|
|
1254
|
+
for (const item of snapshot.reviewQueue.items) {
|
|
1255
|
+
lines.push(
|
|
1256
|
+
`- [${item.reviewState}] ${item.recurrenceKey} — ${item.count}x, max=${item.maxSeverity}; latest: ${item.latestSummary}`,
|
|
1257
|
+
);
|
|
1258
|
+
lines.push(` - Decision posture: ${formatDecisionPosture(item)}`);
|
|
1259
|
+
if (item.reviewNote) lines.push(` - Review note: ${item.reviewNote}`);
|
|
1260
|
+
}
|
|
1261
|
+
if (snapshot.reviewQueue.items.length === 0)
|
|
1262
|
+
lines.push("No recurrence groups in this export filter.");
|
|
1263
|
+
|
|
1264
|
+
lines.push("", "## Safe local follow-up", "");
|
|
1265
|
+
if (!snapshot.nextActions?.length) {
|
|
1266
|
+
lines.push("No local follow-up commands in this export scope.");
|
|
1267
|
+
}
|
|
1268
|
+
for (const action of snapshot.nextActions || []) {
|
|
1269
|
+
lines.push(`- ${action.recurrenceKey} [${action.reviewState}]`);
|
|
1270
|
+
lines.push(` - Inspect local samples: ${action.inspectCommand}`);
|
|
1271
|
+
if (action.reviewCommands?.length) {
|
|
1272
|
+
lines.push(` - Choose local review state: ${action.reviewCommands.join(" | ")}`);
|
|
1273
|
+
}
|
|
1274
|
+
if (action.retentionPreviewCommand) {
|
|
1275
|
+
lines.push(` - Optional local retention preview: ${action.retentionPreviewCommand}`);
|
|
1276
|
+
}
|
|
1277
|
+
lines.push(` - Draft-only handoff: ${action.draftCommands.join(" | ")}`);
|
|
1278
|
+
lines.push(` - ${action.boundary}`);
|
|
1279
|
+
}
|
|
1280
|
+
lines.push(
|
|
1281
|
+
"Follow-up boundary: commands above are local diagnostics or draft-only text. They do not file, create, declare, assign, record evidence, publish, archive, restore, or mutate owner systems.",
|
|
1282
|
+
);
|
|
1283
|
+
return lines.join("\n");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export function formatExportJson(snapshot) {
|
|
1287
|
+
return JSON.stringify(snapshot, null, 2);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
export function buildRetentionPreview(input = {}) {
|
|
1291
|
+
const recurrenceKey = sanitizeDisplayText(input.recurrenceKey, 200);
|
|
1292
|
+
if (!recurrenceKey) throw new Error("agent_vent retention preview requires a recurrenceKey");
|
|
1293
|
+
const records = input.records || [];
|
|
1294
|
+
const reviewEvents = input.reviewEvents || [];
|
|
1295
|
+
const curationEvents = input.curationEvents || [];
|
|
1296
|
+
const plan = planRetentionArchive({
|
|
1297
|
+
records,
|
|
1298
|
+
reviewEvents,
|
|
1299
|
+
curationEvents,
|
|
1300
|
+
recurrenceKey,
|
|
1301
|
+
storeHash: input.storeHash,
|
|
1302
|
+
reviewHash: input.reviewHash,
|
|
1303
|
+
curationHash: input.curationHash,
|
|
1304
|
+
});
|
|
1305
|
+
return {
|
|
1306
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
1307
|
+
classification: "local-diagnostic-user-data",
|
|
1308
|
+
boundary:
|
|
1309
|
+
"Local diagnostic retention preview only. No archive, restore, AK task, GitHub issue, incident, evidence, telemetry, or ASC/self state mutation occurred.",
|
|
1310
|
+
...plan,
|
|
1311
|
+
samples: plan.records.slice(0, clampLimit(input.limit, 5)).map((record) => ({
|
|
1312
|
+
id: record.id,
|
|
1313
|
+
createdAt: record.createdAt,
|
|
1314
|
+
severity: normalizeSeverity(record.severity),
|
|
1315
|
+
category: normalizeCategory(record.category),
|
|
1316
|
+
summary: sanitizeDisplayText(record.summary, 300),
|
|
1317
|
+
})),
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
export function buildRetentionCandidates(input = {}) {
|
|
1322
|
+
const records = input.records || [];
|
|
1323
|
+
const reviewEvents = input.reviewEvents || [];
|
|
1324
|
+
const curationEvents = input.curationEvents || [];
|
|
1325
|
+
const stateFilter = normalizeRetentionCandidateState(input.state);
|
|
1326
|
+
const filters = normalizeReviewFilters(input.filters);
|
|
1327
|
+
const allItems = buildReviewQueueItems(records, reviewEvents, curationEvents);
|
|
1328
|
+
const facetFilteredItems = allItems.filter((item) => reviewItemMatchesFilters(item, filters));
|
|
1329
|
+
const visibleItems = facetFilteredItems.filter((item) => {
|
|
1330
|
+
if (stateFilter === "all") return true;
|
|
1331
|
+
if (stateFilter === "reviewed") return item.reviewState !== "new";
|
|
1332
|
+
return item.reviewState === stateFilter;
|
|
1333
|
+
});
|
|
1334
|
+
const limit = clampLimit(input.limit, 20);
|
|
1335
|
+
|
|
1336
|
+
return {
|
|
1337
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
1338
|
+
classification: "local-diagnostic-user-data",
|
|
1339
|
+
boundary:
|
|
1340
|
+
"Read-only local diagnostic retention planning projection. No archive, restore, AK task, GitHub issue, incident, evidence, telemetry, publication, owner assignment, or ASC/self state mutation occurred; no archive confirmation tokens are emitted here.",
|
|
1341
|
+
totalRecords: records.length,
|
|
1342
|
+
groupCount: allItems.length,
|
|
1343
|
+
matchingGroupCount: facetFilteredItems.length,
|
|
1344
|
+
candidateCount: visibleItems.length,
|
|
1345
|
+
stateFilter,
|
|
1346
|
+
filters,
|
|
1347
|
+
hasFilters: hasReviewFilters(filters),
|
|
1348
|
+
limit,
|
|
1349
|
+
items: visibleItems.slice(0, limit),
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
export function buildRetentionHistory(input = {}) {
|
|
1354
|
+
const retentionEvents = input.retentionEvents || [];
|
|
1355
|
+
const backupDir = input.backupDir || defaultBackupDir();
|
|
1356
|
+
const limit = clampLimit(input.limit, 20);
|
|
1357
|
+
const items = retentionEvents
|
|
1358
|
+
.slice()
|
|
1359
|
+
.reverse()
|
|
1360
|
+
.slice(0, limit)
|
|
1361
|
+
.map((event) => buildRetentionHistoryItem(event, backupDir));
|
|
1362
|
+
|
|
1363
|
+
return {
|
|
1364
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
1365
|
+
classification: "local-diagnostic-user-data",
|
|
1366
|
+
boundary:
|
|
1367
|
+
"Read-only local diagnostic retention receipt history. No archive, restore, AK task, GitHub issue, incident, evidence, telemetry, publication, owner assignment, or ASC/self state mutation occurred; restore commands are candidates guarded by the restore command's backup containment and stale-store checks.",
|
|
1368
|
+
backupDir,
|
|
1369
|
+
totalEvents: retentionEvents.length,
|
|
1370
|
+
eventCount: items.length,
|
|
1371
|
+
limit,
|
|
1372
|
+
items,
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
export function formatRetentionHistory(history) {
|
|
1377
|
+
if (!history.totalEvents) {
|
|
1378
|
+
return [
|
|
1379
|
+
"No agent vent retention events found yet. Archive reviewed local diagnostic groups before retention history exists.",
|
|
1380
|
+
`Boundary: ${history.boundary}`,
|
|
1381
|
+
].join("\n");
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const lines = [
|
|
1385
|
+
`Agent vent retention history: ${history.eventCount} of ${history.totalEvents} local retention event(s) shown; limit=${history.limit}.`,
|
|
1386
|
+
"This is a read-only receipt projection. It does not archive, restore, delete, file, create tasks, declare incidents, record evidence, publish, assign owners, or mutate ASC/self state.",
|
|
1387
|
+
"Restore commands shown here are rollback candidates only; actual restore still requires package-created backup containment and current active-store hash checks, so stale/moved/path-invalid backups fail closed.",
|
|
1388
|
+
`Boundary: ${history.boundary}`,
|
|
1389
|
+
];
|
|
1390
|
+
|
|
1391
|
+
for (const item of history.items) {
|
|
1392
|
+
lines.push(
|
|
1393
|
+
`- [${item.action}] ${item.createdAt || "unknown-time"} ${item.recurrenceKey || "unknown-recurrence"} — ${item.archivedRecordCount || 0} record(s); backup=${item.backupPath || "none"}`,
|
|
1394
|
+
);
|
|
1395
|
+
if (item.note) lines.push(` note: ${item.note}`);
|
|
1396
|
+
if (item.restoreCommand) {
|
|
1397
|
+
lines.push(
|
|
1398
|
+
` rollback candidate: ${item.restoreCommand}`,
|
|
1399
|
+
` restore guard: requires backup inside ${history.backupDir} and current active vents store hash ${item.afterHashPrefix}`,
|
|
1400
|
+
);
|
|
1401
|
+
} else {
|
|
1402
|
+
lines.push(` rollback candidate: unavailable (${item.restoreUnavailableReason})`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return lines.join("\n");
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
export function archiveRecurrenceGroup(input = {}) {
|
|
1409
|
+
const storePath = input.storePath || defaultStorePath();
|
|
1410
|
+
const reviewPath = input.reviewPath || defaultReviewPath();
|
|
1411
|
+
const curationPath = input.curationPath || defaultCurationPath();
|
|
1412
|
+
const retentionPath = input.retentionPath || defaultRetentionPath();
|
|
1413
|
+
const backupDir = input.backupDir || defaultBackupDir();
|
|
1414
|
+
const confirmationToken = sanitizeDisplayText(input.confirmationToken, 300);
|
|
1415
|
+
const lock = acquireFileLock(`${storePath}.lock`);
|
|
1416
|
+
try {
|
|
1417
|
+
const state = loadDiagnosticState({
|
|
1418
|
+
storePath,
|
|
1419
|
+
reviewPath,
|
|
1420
|
+
curationPath,
|
|
1421
|
+
retentionPath,
|
|
1422
|
+
backupDir,
|
|
1423
|
+
});
|
|
1424
|
+
const plan = planRetentionArchive({
|
|
1425
|
+
records: state.records,
|
|
1426
|
+
reviewEvents: state.reviewEvents,
|
|
1427
|
+
curationEvents: state.curationEvents,
|
|
1428
|
+
recurrenceKey: input.recurrenceKey,
|
|
1429
|
+
storeHash: state.ventsHash,
|
|
1430
|
+
reviewHash: state.reviewEventsHash,
|
|
1431
|
+
curationHash: state.curationEventsHash,
|
|
1432
|
+
});
|
|
1433
|
+
if (!plan.archivable) {
|
|
1434
|
+
throw new Error(
|
|
1435
|
+
`cannot archive recurrence group ${plan.recurrenceKey} before local review; set review state to acknowledged, dismissed, or escalation_drafted first`,
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
if (confirmationToken !== plan.confirmationToken) {
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
`agent_vent retention archive requires exact confirmation token: ${plan.confirmationToken}`,
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const existing = safeLstat(storePath);
|
|
1445
|
+
if (!existing) throw new Error(`cannot archive from missing agent_vent store: ${storePath}`);
|
|
1446
|
+
assertSafeJsonlFile(storePath, existing);
|
|
1447
|
+
if (existing.size > MAX_JSONL_FILE_BYTES) {
|
|
1448
|
+
throw new Error(
|
|
1449
|
+
`agent_vent store file is too large: ${storePath} (${existing.size} bytes > ${MAX_JSONL_FILE_BYTES})`,
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
const beforeText = fs.readFileSync(storePath, "utf8");
|
|
1453
|
+
const beforeHash = sha256Hex(beforeText);
|
|
1454
|
+
if (beforeHash !== state.ventsHash) {
|
|
1455
|
+
throw new Error("agent_vent retention archive refused changing vents store");
|
|
1456
|
+
}
|
|
1457
|
+
const archivedRecords = new Set(plan.records);
|
|
1458
|
+
const remainingRecords = state.records.filter((record) => !archivedRecords.has(record));
|
|
1459
|
+
const afterText = recordsToJsonl(remainingRecords);
|
|
1460
|
+
const afterHash = sha256Hex(afterText);
|
|
1461
|
+
const now = input.now || new Date().toISOString();
|
|
1462
|
+
const backupPath = writeRetentionBackup({
|
|
1463
|
+
backupDir,
|
|
1464
|
+
now,
|
|
1465
|
+
recurrenceKey: plan.recurrenceKey,
|
|
1466
|
+
requestedRecurrenceKey: plan.requestedRecurrenceKey,
|
|
1467
|
+
confirmationToken: plan.confirmationToken,
|
|
1468
|
+
restoreConfirmationToken: buildRestoreToken(plan.recurrenceKey, beforeHash, afterHash),
|
|
1469
|
+
beforeHash,
|
|
1470
|
+
afterHash,
|
|
1471
|
+
archivedRecordIds: plan.archivedRecordIds,
|
|
1472
|
+
archivedRecordCount: plan.archivedRecordCount,
|
|
1473
|
+
ventsJsonl: beforeText,
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
const event = createRetentionEvent(
|
|
1477
|
+
{
|
|
1478
|
+
action: "archive",
|
|
1479
|
+
recurrenceKey: plan.recurrenceKey,
|
|
1480
|
+
requestedRecurrenceKey: plan.requestedRecurrenceKey,
|
|
1481
|
+
backupPath,
|
|
1482
|
+
archivedRecordCount: plan.archivedRecordCount,
|
|
1483
|
+
archivedRecordIds: plan.archivedRecordIds,
|
|
1484
|
+
beforeHash,
|
|
1485
|
+
afterHash,
|
|
1486
|
+
note: input.note || input.retentionNote,
|
|
1487
|
+
},
|
|
1488
|
+
{ source: input.source || "agent_vent_retention", now },
|
|
1489
|
+
);
|
|
1490
|
+
try {
|
|
1491
|
+
writeTextFileAtomically(storePath, afterText, 0o600);
|
|
1492
|
+
appendRetentionEvent(retentionPath, event);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
restoreTextIfHashMatches(storePath, afterHash, beforeText);
|
|
1495
|
+
fs.rmSync(backupPath, { force: true });
|
|
1496
|
+
throw error;
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
generatedAt: now,
|
|
1500
|
+
classification: "local-diagnostic-user-data",
|
|
1501
|
+
boundary:
|
|
1502
|
+
"Local diagnostic records were archived from the active vents store only. No AK task, GitHub issue, incident, evidence, telemetry, publication, or ASC/self state mutation occurred.",
|
|
1503
|
+
...plan,
|
|
1504
|
+
backupPath,
|
|
1505
|
+
retentionPath,
|
|
1506
|
+
beforeHash,
|
|
1507
|
+
afterHash,
|
|
1508
|
+
restoreConfirmationToken: buildRestoreToken(plan.recurrenceKey, beforeHash, afterHash),
|
|
1509
|
+
retentionEvent: event,
|
|
1510
|
+
};
|
|
1511
|
+
} finally {
|
|
1512
|
+
releaseFileLock(lock);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
export function restoreRetentionBackup(input = {}) {
|
|
1517
|
+
const storePath = input.storePath || defaultStorePath();
|
|
1518
|
+
const retentionPath = input.retentionPath || defaultRetentionPath();
|
|
1519
|
+
const backupDir = input.backupDir || defaultBackupDir();
|
|
1520
|
+
const confirmationToken = sanitizeDisplayText(input.confirmationToken, 300);
|
|
1521
|
+
const backupPath = assertBackupPathInsideDir(input.backupPath, backupDir);
|
|
1522
|
+
const lock = acquireFileLock(`${storePath}.lock`);
|
|
1523
|
+
try {
|
|
1524
|
+
const backup = readRetentionBackup(backupPath);
|
|
1525
|
+
const expectedRestoreToken = buildRestoreToken(
|
|
1526
|
+
backup.recurrenceKey,
|
|
1527
|
+
backup.beforeHash,
|
|
1528
|
+
backup.afterHash,
|
|
1529
|
+
);
|
|
1530
|
+
if (backup.restoreConfirmationToken !== expectedRestoreToken) {
|
|
1531
|
+
throw new Error("agent_vent retention backup restore token failed integrity check");
|
|
1532
|
+
}
|
|
1533
|
+
if (confirmationToken !== expectedRestoreToken) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
`agent_vent retention restore requires exact confirmation token: ${expectedRestoreToken}`,
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const existing = safeLstat(storePath);
|
|
1540
|
+
if (!existing)
|
|
1541
|
+
throw new Error(`agent_vent retention restore requires current vents store: ${storePath}`);
|
|
1542
|
+
assertSafeJsonlFile(storePath, existing);
|
|
1543
|
+
const currentText = fs.readFileSync(storePath, "utf8");
|
|
1544
|
+
const currentHash = sha256Hex(currentText);
|
|
1545
|
+
if (currentHash !== backup.afterHash) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
"agent_vent retention restore refused stale backup: current vents store no longer matches the archived-after hash",
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const restoredHash = backup.beforeHash;
|
|
1552
|
+
writeTextFileAtomically(storePath, backup.ventsJsonl, 0o600);
|
|
1553
|
+
const now = input.now || new Date().toISOString();
|
|
1554
|
+
const event = createRetentionEvent(
|
|
1555
|
+
{
|
|
1556
|
+
action: "restore",
|
|
1557
|
+
recurrenceKey: backup.recurrenceKey,
|
|
1558
|
+
requestedRecurrenceKey: backup.requestedRecurrenceKey,
|
|
1559
|
+
backupPath,
|
|
1560
|
+
archivedRecordCount: backup.archivedRecordCount,
|
|
1561
|
+
archivedRecordIds: backup.archivedRecordIds,
|
|
1562
|
+
beforeHash: backup.afterHash,
|
|
1563
|
+
afterHash: backup.beforeHash,
|
|
1564
|
+
note: input.note || input.retentionNote,
|
|
1565
|
+
},
|
|
1566
|
+
{ source: input.source || "agent_vent_retention", now },
|
|
1567
|
+
);
|
|
1568
|
+
try {
|
|
1569
|
+
appendRetentionEvent(retentionPath, event);
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
restoreTextIfHashMatches(storePath, restoredHash, currentText);
|
|
1572
|
+
throw error;
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
generatedAt: now,
|
|
1576
|
+
classification: "local-diagnostic-user-data",
|
|
1577
|
+
boundary:
|
|
1578
|
+
"Local diagnostic backup was restored to the active vents store only. No AK task, GitHub issue, incident, evidence, telemetry, publication, or ASC/self state mutation occurred.",
|
|
1579
|
+
recurrenceKey: backup.recurrenceKey,
|
|
1580
|
+
requestedRecurrenceKey: backup.requestedRecurrenceKey,
|
|
1581
|
+
restoredRecordCount: backup.archivedRecordCount,
|
|
1582
|
+
backupPath,
|
|
1583
|
+
retentionPath,
|
|
1584
|
+
beforeHash: backup.afterHash,
|
|
1585
|
+
afterHash: backup.beforeHash,
|
|
1586
|
+
retentionEvent: event,
|
|
1587
|
+
};
|
|
1588
|
+
} finally {
|
|
1589
|
+
releaseFileLock(lock);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
export function formatRetentionPreview(preview) {
|
|
1594
|
+
const lines = [
|
|
1595
|
+
`Agent vent retention preview for ${preview.recurrenceKey}: ${preview.archivedRecordCount} active local diagnostic record(s).`,
|
|
1596
|
+
`Review state: ${preview.reviewState}; archivable=${preview.archivable}`,
|
|
1597
|
+
`Boundary: ${preview.boundary}`,
|
|
1598
|
+
];
|
|
1599
|
+
if (preview.archivable) {
|
|
1600
|
+
lines.push(
|
|
1601
|
+
`Confirmation token: ${preview.confirmationToken}`,
|
|
1602
|
+
`Next: ${formatAgentVentCommand("retention", "archive", preview.recurrenceKey, preview.confirmationToken)} [note]`,
|
|
1603
|
+
);
|
|
1604
|
+
} else {
|
|
1605
|
+
lines.push(
|
|
1606
|
+
`Next: ${formatAgentVentCommand("review", "set", "acknowledged", preview.recurrenceKey)} [note] before archiving.`,
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
for (const sample of preview.samples || []) {
|
|
1610
|
+
lines.push(`- ${sample.id || "unknown-id"}: ${sample.summary || "(no summary)"}`);
|
|
1611
|
+
}
|
|
1612
|
+
return lines.join("\n");
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
export function formatRetentionCandidates(candidates) {
|
|
1616
|
+
const filterText = formatReviewFilters(candidates.filters);
|
|
1617
|
+
if (candidates.totalRecords === 0) {
|
|
1618
|
+
return [
|
|
1619
|
+
"No agent vent records found yet. Record minimized vents before retention planning.",
|
|
1620
|
+
candidates.hasFilters
|
|
1621
|
+
? `Filters requested: ${filterText}. Local diagnostic labels only; not owner routing.`
|
|
1622
|
+
: undefined,
|
|
1623
|
+
`Boundary: ${candidates.boundary}`,
|
|
1624
|
+
]
|
|
1625
|
+
.filter(Boolean)
|
|
1626
|
+
.join("\n");
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const scopeText = candidates.hasFilters
|
|
1630
|
+
? `${candidates.matchingGroupCount} matching of ${candidates.groupCount} total recurrence group(s)`
|
|
1631
|
+
: `${candidates.groupCount} total recurrence group(s)`;
|
|
1632
|
+
const lines = [
|
|
1633
|
+
`Agent vent retention candidates: ${candidates.items.length} of ${candidates.candidateCount} group(s) shown (${scopeText}); state filter=${candidates.stateFilter}; limit=${candidates.limit}.`,
|
|
1634
|
+
candidates.hasFilters
|
|
1635
|
+
? `Filters: ${filterText}. Local diagnostic labels only; not owner routing or owner assignment.`
|
|
1636
|
+
: undefined,
|
|
1637
|
+
"This is a read-only planning view. It does not archive records and intentionally does not emit archive confirmation tokens.",
|
|
1638
|
+
`Boundary: ${candidates.boundary}`,
|
|
1639
|
+
].filter(Boolean);
|
|
1640
|
+
|
|
1641
|
+
if (!candidates.items.length) {
|
|
1642
|
+
lines.push("- none");
|
|
1643
|
+
if (candidates.stateFilter === "reviewed") {
|
|
1644
|
+
lines.push("Next: review local groups first, then run retention candidates again.");
|
|
1645
|
+
}
|
|
1646
|
+
return lines.join("\n");
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
for (const item of candidates.items) {
|
|
1650
|
+
const facets = [
|
|
1651
|
+
item.categories?.length ? `categories=${item.categories.join(",")}` : undefined,
|
|
1652
|
+
item.tags?.length ? `tags=${item.tags.join(",")}` : undefined,
|
|
1653
|
+
item.tools?.length ? `tools=${item.tools.join(",")}` : undefined,
|
|
1654
|
+
item.packages?.length ? `packages=${item.packages.join(",")}` : undefined,
|
|
1655
|
+
]
|
|
1656
|
+
.filter(Boolean)
|
|
1657
|
+
.join("; ");
|
|
1658
|
+
const facetText = facets ? `; ${facets}` : "";
|
|
1659
|
+
lines.push(
|
|
1660
|
+
`- [${item.reviewState}] ${item.recurrenceKey} — ${item.count}x, max=${item.maxSeverity}, first=${item.firstSeen || "unknown"}, last=${item.lastSeen || "unknown"}${facetText}; latest: ${item.latestSummary}`,
|
|
1661
|
+
);
|
|
1662
|
+
lines.push(` decision posture: ${formatDecisionPosture(item)}`);
|
|
1663
|
+
if (item.reviewNote) lines.push(` review note: ${item.reviewNote}`);
|
|
1664
|
+
lines.push(
|
|
1665
|
+
` inspect: ${formatAgentVentCommand("review", "show", item.recurrenceKey)} [limit]`,
|
|
1666
|
+
);
|
|
1667
|
+
if (item.reviewState === "new") {
|
|
1668
|
+
lines.push(
|
|
1669
|
+
` review before archive: ${formatAgentVentCommand("review", "set", "acknowledged", item.recurrenceKey)} [note] | ${formatAgentVentCommand("review", "set", "dismissed", item.recurrenceKey)} [note]`,
|
|
1670
|
+
);
|
|
1671
|
+
} else {
|
|
1672
|
+
lines.push(
|
|
1673
|
+
` preview to request archive token: ${formatAgentVentCommand("retention", "preview", item.recurrenceKey)}`,
|
|
1674
|
+
` ${formatExportBucketLine("export this outcome bucket", item.reviewState, candidates.filters)}`,
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (candidates.hasFilters) {
|
|
1679
|
+
lines.push(
|
|
1680
|
+
"Filter note: category/tag/tool/package values are local diagnostic labels only, not owner routing or owner assignment.",
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
return lines.join("\n");
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
export function formatRetentionArchiveResult(result) {
|
|
1687
|
+
return [
|
|
1688
|
+
`Archived ${result.archivedRecordCount} local diagnostic record(s) from ${result.recurrenceKey}.`,
|
|
1689
|
+
`Backup: ${result.backupPath}`,
|
|
1690
|
+
`Restore token: ${result.restoreConfirmationToken}`,
|
|
1691
|
+
`Next rollback: ${formatAgentVentCommand("retention", "restore", result.backupPath, result.restoreConfirmationToken)}`,
|
|
1692
|
+
`Boundary: ${result.boundary}`,
|
|
1693
|
+
].join("\n");
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
export function formatRetentionRestoreResult(result) {
|
|
1697
|
+
return [
|
|
1698
|
+
`Restored local diagnostic backup for ${result.recurrenceKey}; restored ${result.restoredRecordCount} archived record(s).`,
|
|
1699
|
+
`Backup: ${result.backupPath}`,
|
|
1700
|
+
`Boundary: ${result.boundary}`,
|
|
1701
|
+
].join("\n");
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
export function buildEscalationDraft(input = {}) {
|
|
1705
|
+
const target = normalizeDraftTarget(input.target || input.draftTarget);
|
|
1706
|
+
const recurrenceKey = sanitizeDisplayText(input.recurrenceKey, 200);
|
|
1707
|
+
if (!recurrenceKey) throw new Error("agent_vent draft requires a recurrenceKey");
|
|
1708
|
+
|
|
1709
|
+
const records = input.records || [];
|
|
1710
|
+
const reviewEvents = input.reviewEvents || [];
|
|
1711
|
+
const curationEvents = input.curationEvents || [];
|
|
1712
|
+
const curationMap = buildCurationMap(curationEvents);
|
|
1713
|
+
const resolvedKey = resolveRecurrenceKey(recurrenceKey, curationMap);
|
|
1714
|
+
const allGroups = buildReviewQueueItems(records, reviewEvents, curationEvents);
|
|
1715
|
+
const group = allGroups.find((item) => item.recurrenceKey === resolvedKey);
|
|
1716
|
+
if (!group)
|
|
1717
|
+
throw new Error(`cannot draft escalation for unknown recurrence group: ${recurrenceKey}`);
|
|
1718
|
+
|
|
1719
|
+
const samples = records
|
|
1720
|
+
.filter(
|
|
1721
|
+
(record) =>
|
|
1722
|
+
resolveRecurrenceKey(
|
|
1723
|
+
String(record.recurrenceKey || buildRecurrenceKey(record)),
|
|
1724
|
+
curationMap,
|
|
1725
|
+
) === resolvedKey,
|
|
1726
|
+
)
|
|
1727
|
+
.slice(-clampLimit(input.limit, 5))
|
|
1728
|
+
.reverse()
|
|
1729
|
+
.map((record) => ({
|
|
1730
|
+
id: record.id,
|
|
1731
|
+
createdAt: record.createdAt,
|
|
1732
|
+
severity: normalizeSeverity(record.severity),
|
|
1733
|
+
category: normalizeCategory(record.category),
|
|
1734
|
+
tool: sanitizeFacetText(record.tool, 160).value,
|
|
1735
|
+
packageName: sanitizeFacetText(record.packageName, 200).value,
|
|
1736
|
+
tags: Array.isArray(record.tags)
|
|
1737
|
+
? record.tags.map((tag) => recurrenceSlug(tag)).filter((tag) => tag !== "unspecified")
|
|
1738
|
+
: [],
|
|
1739
|
+
summary: sanitizeDisplayText(record.summary, 300),
|
|
1740
|
+
evidence: sanitizeDisplayText(record.evidence, 500),
|
|
1741
|
+
reproduction: sanitizeDisplayText(record.reproduction, 500),
|
|
1742
|
+
}));
|
|
1743
|
+
|
|
1744
|
+
const draft = {
|
|
1745
|
+
generatedAt: input.now || new Date().toISOString(),
|
|
1746
|
+
target,
|
|
1747
|
+
recurrenceKey: resolvedKey,
|
|
1748
|
+
requestedRecurrenceKey: recurrenceKey,
|
|
1749
|
+
classification: "local-diagnostic-user-data",
|
|
1750
|
+
boundary:
|
|
1751
|
+
"Draft-only local diagnostic projection. No AK task, GitHub issue, incident, evidence, telemetry, publication, or ASC/self state mutation occurred.",
|
|
1752
|
+
group,
|
|
1753
|
+
samples,
|
|
1754
|
+
};
|
|
1755
|
+
return { ...draft, text: formatEscalationDraftText(draft) };
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
export function formatEscalationDraftText(draft) {
|
|
1759
|
+
const targetLabel = {
|
|
1760
|
+
github_issue: "GitHub issue",
|
|
1761
|
+
ak_task: "AK task",
|
|
1762
|
+
incident_review: "incident review",
|
|
1763
|
+
maintainer_note: "maintainer note",
|
|
1764
|
+
}[draft.target];
|
|
1765
|
+
const lines = [
|
|
1766
|
+
`# Draft-only ${targetLabel} text`,
|
|
1767
|
+
"",
|
|
1768
|
+
`Boundary: ${draft.boundary}`,
|
|
1769
|
+
"Next action: a human may copy/edit this text into the owner system after review.",
|
|
1770
|
+
`Optional local follow-up: ${formatAgentVentCommand("review", "set", "escalation_drafted", draft.recurrenceKey)}`,
|
|
1771
|
+
"",
|
|
1772
|
+
`Title: Investigate recurring agent friction: ${draft.recurrenceKey}`,
|
|
1773
|
+
"",
|
|
1774
|
+
"## Local recurrence summary",
|
|
1775
|
+
"",
|
|
1776
|
+
`- Recurrence key: ${draft.recurrenceKey}`,
|
|
1777
|
+
`- Count: ${draft.group.count}`,
|
|
1778
|
+
`- Max severity: ${draft.group.maxSeverity}`,
|
|
1779
|
+
`- Candidate incident for human review: ${draft.group.candidateIncident ? "yes" : "no"}`,
|
|
1780
|
+
`- Review state: ${draft.group.reviewState}`,
|
|
1781
|
+
];
|
|
1782
|
+
if (draft.group.reviewNote) lines.push(`- Local review note: ${draft.group.reviewNote}`);
|
|
1783
|
+
const localFacetLines = [
|
|
1784
|
+
draft.group.categories?.length
|
|
1785
|
+
? `- Categories: ${draft.group.categories.join(", ")}`
|
|
1786
|
+
: undefined,
|
|
1787
|
+
draft.group.tags?.length ? `- Tags: ${draft.group.tags.join(", ")}` : undefined,
|
|
1788
|
+
draft.group.tools?.length ? `- Tools: ${draft.group.tools.join(", ")}` : undefined,
|
|
1789
|
+
draft.group.packages?.length ? `- Packages: ${draft.group.packages.join(", ")}` : undefined,
|
|
1790
|
+
].filter(Boolean);
|
|
1791
|
+
if (localFacetLines.length) {
|
|
1792
|
+
lines.push("", "## Local diagnostic facets (not owner routing)", "", ...localFacetLines);
|
|
1793
|
+
}
|
|
1794
|
+
lines.push("", "## Representative local samples", "");
|
|
1795
|
+
for (const sample of draft.samples) {
|
|
1796
|
+
lines.push(
|
|
1797
|
+
`- ${sample.createdAt || "unknown-time"} [${sample.severity}/${sample.category}] ${sample.summary}`,
|
|
1798
|
+
);
|
|
1799
|
+
if (sample.tags?.length) lines.push(` - Tags: ${sample.tags.join(", ")}`);
|
|
1800
|
+
if (sample.tool) lines.push(` - Tool: ${sample.tool}`);
|
|
1801
|
+
if (sample.packageName) lines.push(` - Package: ${sample.packageName}`);
|
|
1802
|
+
if (sample.evidence) lines.push(` - Evidence: ${sample.evidence}`);
|
|
1803
|
+
if (sample.reproduction) lines.push(` - Reproduction: ${sample.reproduction}`);
|
|
1804
|
+
}
|
|
1805
|
+
if (draft.samples.length === 0) lines.push("No sample vents available.");
|
|
1806
|
+
lines.push(
|
|
1807
|
+
"",
|
|
1808
|
+
"## Owner-system handoff reminder",
|
|
1809
|
+
"",
|
|
1810
|
+
"This text is only a local draft. Local facets are hints, not owner routing. The target owner system remains authoritative for acceptance, schema, lifecycle, evidence, and publication.",
|
|
1811
|
+
);
|
|
1812
|
+
return lines.join("\n");
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function sanitizeDisplayText(value, maxLength) {
|
|
1816
|
+
const compact = compactText(value, maxLength);
|
|
1817
|
+
if (compact === undefined) return undefined;
|
|
1818
|
+
return redactSensitiveText(compact).text;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function sanitizeVentRecords(values) {
|
|
1822
|
+
const records = [];
|
|
1823
|
+
let invalidRecords = 0;
|
|
1824
|
+
for (const value of values) {
|
|
1825
|
+
try {
|
|
1826
|
+
const summary = sanitizeOptionalText(value?.summary, 600);
|
|
1827
|
+
if (!summary.value) throw new Error("missing summary");
|
|
1828
|
+
const optionalFields = {
|
|
1829
|
+
frustration: sanitizeOptionalText(value?.frustration, 1200),
|
|
1830
|
+
evidence: sanitizeOptionalText(value?.evidence, 1600),
|
|
1831
|
+
expected: sanitizeOptionalText(value?.expected, 800),
|
|
1832
|
+
actual: sanitizeOptionalText(value?.actual, 800),
|
|
1833
|
+
reproduction: sanitizeOptionalText(value?.reproduction, 1200),
|
|
1834
|
+
};
|
|
1835
|
+
const localFacets = {
|
|
1836
|
+
tool: sanitizeFacetText(value?.tool || value?.toolName, 160),
|
|
1837
|
+
packageName: sanitizeFacetText(value?.packageName, 200),
|
|
1838
|
+
};
|
|
1839
|
+
const tags = sanitizeTagList(value?.tags);
|
|
1840
|
+
const privacy = collectRedactionMetadata([
|
|
1841
|
+
summary,
|
|
1842
|
+
...Object.values(optionalFields),
|
|
1843
|
+
...Object.values(localFacets),
|
|
1844
|
+
tags,
|
|
1845
|
+
]);
|
|
1846
|
+
const category = normalizeCategory(value?.category);
|
|
1847
|
+
const record = removeUndefined({
|
|
1848
|
+
...value,
|
|
1849
|
+
schemaVersion: Number(value?.schemaVersion) || SCHEMA_VERSION,
|
|
1850
|
+
id: compactText(value?.id, 120) || randomUUID(),
|
|
1851
|
+
createdAt: compactText(value?.createdAt, 80),
|
|
1852
|
+
category,
|
|
1853
|
+
severity: normalizeSeverity(value?.severity),
|
|
1854
|
+
recurrenceKey:
|
|
1855
|
+
sanitizeDisplayText(value?.recurrenceKey, 200) ||
|
|
1856
|
+
buildRecurrenceKey({ ...value, category, summary: summary.value }),
|
|
1857
|
+
summary: summary.value,
|
|
1858
|
+
tool: localFacets.tool.value,
|
|
1859
|
+
packageName: localFacets.packageName.value,
|
|
1860
|
+
frustration: optionalFields.frustration.value,
|
|
1861
|
+
evidence: optionalFields.evidence.value,
|
|
1862
|
+
expected: optionalFields.expected.value,
|
|
1863
|
+
actual: optionalFields.actual.value,
|
|
1864
|
+
reproduction: optionalFields.reproduction.value,
|
|
1865
|
+
tags: tags.values,
|
|
1866
|
+
privacy: {
|
|
1867
|
+
classification: "local-diagnostic-user-data",
|
|
1868
|
+
redacted: privacy.redacted,
|
|
1869
|
+
redactionPatterns: privacy.redactionPatterns,
|
|
1870
|
+
},
|
|
1871
|
+
});
|
|
1872
|
+
records.push(record);
|
|
1873
|
+
} catch {
|
|
1874
|
+
invalidRecords += 1;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return { records, invalidRecords };
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function sanitizeReviewEvents(values) {
|
|
1881
|
+
const events = [];
|
|
1882
|
+
let invalidEvents = 0;
|
|
1883
|
+
for (const value of values) {
|
|
1884
|
+
try {
|
|
1885
|
+
if (value?.eventType !== "review_state") continue;
|
|
1886
|
+
const recurrenceKey = sanitizeDisplayText(value?.recurrenceKey, 200);
|
|
1887
|
+
if (!recurrenceKey) throw new Error("missing recurrence key");
|
|
1888
|
+
events.push(
|
|
1889
|
+
removeUndefined({
|
|
1890
|
+
...value,
|
|
1891
|
+
schemaVersion: Number(value?.schemaVersion) || SCHEMA_VERSION,
|
|
1892
|
+
eventType: "review_state",
|
|
1893
|
+
id: compactText(value?.id, 120) || randomUUID(),
|
|
1894
|
+
createdAt: compactText(value?.createdAt, 80),
|
|
1895
|
+
recurrenceKey,
|
|
1896
|
+
state: normalizeReviewState(value?.state),
|
|
1897
|
+
note: sanitizeDisplayText(value?.note, 1200),
|
|
1898
|
+
}),
|
|
1899
|
+
);
|
|
1900
|
+
} catch {
|
|
1901
|
+
invalidEvents += 1;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return { events, invalidEvents };
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function sanitizeCurationEvents(values) {
|
|
1908
|
+
const events = [];
|
|
1909
|
+
let invalidEvents = 0;
|
|
1910
|
+
let quarantinedEvents = 0;
|
|
1911
|
+
for (const value of values) {
|
|
1912
|
+
try {
|
|
1913
|
+
if (value?.eventType !== "recurrence_curation") continue;
|
|
1914
|
+
const action = normalizeCurationAction(value?.action);
|
|
1915
|
+
const sourceRecurrenceKey = sanitizeDisplayText(value?.sourceRecurrenceKey, 200);
|
|
1916
|
+
const targetRecurrenceKey = sanitizeDisplayText(value?.targetRecurrenceKey, 200);
|
|
1917
|
+
if (!sourceRecurrenceKey || (action !== "remove" && !targetRecurrenceKey)) {
|
|
1918
|
+
throw new Error("missing recurrence key");
|
|
1919
|
+
}
|
|
1920
|
+
if (targetRecurrenceKey && sourceRecurrenceKey === targetRecurrenceKey) {
|
|
1921
|
+
throw new Error("self alias");
|
|
1922
|
+
}
|
|
1923
|
+
const candidate = removeUndefined({
|
|
1924
|
+
...value,
|
|
1925
|
+
schemaVersion: Number(value?.schemaVersion) || SCHEMA_VERSION,
|
|
1926
|
+
eventType: "recurrence_curation",
|
|
1927
|
+
id: compactText(value?.id, 120) || randomUUID(),
|
|
1928
|
+
createdAt: compactText(value?.createdAt, 80),
|
|
1929
|
+
action,
|
|
1930
|
+
sourceRecurrenceKey,
|
|
1931
|
+
targetRecurrenceKey,
|
|
1932
|
+
note: sanitizeDisplayText(value?.note, 1200),
|
|
1933
|
+
});
|
|
1934
|
+
buildCurationMap([...events, candidate]);
|
|
1935
|
+
events.push(candidate);
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
if (String(error?.message || "").includes("curation cycle")) quarantinedEvents += 1;
|
|
1938
|
+
else invalidEvents += 1;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return { events, invalidEvents, quarantinedEvents };
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function sanitizeRetentionEvents(values) {
|
|
1945
|
+
const events = [];
|
|
1946
|
+
let invalidEvents = 0;
|
|
1947
|
+
for (const value of values) {
|
|
1948
|
+
try {
|
|
1949
|
+
if (value?.eventType !== "retention_lifecycle") continue;
|
|
1950
|
+
const action = String(value?.action || "")
|
|
1951
|
+
.trim()
|
|
1952
|
+
.toLowerCase();
|
|
1953
|
+
if (!RETENTION_EVENT_ACTIONS.includes(action)) throw new Error("invalid action");
|
|
1954
|
+
events.push(
|
|
1955
|
+
removeUndefined({
|
|
1956
|
+
...value,
|
|
1957
|
+
schemaVersion: Number(value?.schemaVersion) || SCHEMA_VERSION,
|
|
1958
|
+
eventType: "retention_lifecycle",
|
|
1959
|
+
id: compactText(value?.id, 120) || randomUUID(),
|
|
1960
|
+
createdAt: compactText(value?.createdAt, 80),
|
|
1961
|
+
action,
|
|
1962
|
+
recurrenceKey: sanitizeDisplayText(value?.recurrenceKey, 200),
|
|
1963
|
+
requestedRecurrenceKey: sanitizeDisplayText(value?.requestedRecurrenceKey, 200),
|
|
1964
|
+
backupPath: sanitizeDisplayText(value?.backupPath, 1200),
|
|
1965
|
+
archivedRecordCount: Number.isFinite(Number(value?.archivedRecordCount))
|
|
1966
|
+
? Number(value.archivedRecordCount)
|
|
1967
|
+
: undefined,
|
|
1968
|
+
archivedRecordIds: Array.isArray(value?.archivedRecordIds)
|
|
1969
|
+
? value.archivedRecordIds.map((id) => sanitizeDisplayText(id, 120)).filter(Boolean)
|
|
1970
|
+
: undefined,
|
|
1971
|
+
beforeHash: sanitizeDisplayText(value?.beforeHash, 128),
|
|
1972
|
+
afterHash: sanitizeDisplayText(value?.afterHash, 128),
|
|
1973
|
+
note: sanitizeDisplayText(value?.note, 1200),
|
|
1974
|
+
}),
|
|
1975
|
+
);
|
|
1976
|
+
} catch {
|
|
1977
|
+
invalidEvents += 1;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return { events, invalidEvents };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function normalizeRetentionCandidateState(value) {
|
|
1984
|
+
if (value === undefined || value === null || value === "") return "reviewed";
|
|
1985
|
+
const normalized = String(value).trim().toLowerCase().replaceAll("-", "_");
|
|
1986
|
+
if (normalized === "reviewed" || normalized === "all") return normalized;
|
|
1987
|
+
return normalizeReviewState(normalized);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function buildRetentionHistoryItem(event, backupDir) {
|
|
1991
|
+
const item = {
|
|
1992
|
+
id: event.id,
|
|
1993
|
+
createdAt: event.createdAt,
|
|
1994
|
+
action: event.action,
|
|
1995
|
+
recurrenceKey: event.recurrenceKey,
|
|
1996
|
+
requestedRecurrenceKey: event.requestedRecurrenceKey,
|
|
1997
|
+
backupPath: event.backupPath,
|
|
1998
|
+
archivedRecordCount: event.archivedRecordCount,
|
|
1999
|
+
archivedRecordIds: Array.isArray(event.archivedRecordIds) ? event.archivedRecordIds : [],
|
|
2000
|
+
beforeHash: event.beforeHash,
|
|
2001
|
+
afterHash: event.afterHash,
|
|
2002
|
+
afterHashPrefix: event.afterHash ? `${String(event.afterHash).slice(0, 12)}…` : "unknown",
|
|
2003
|
+
note: event.note,
|
|
2004
|
+
restoreConfirmationToken: undefined,
|
|
2005
|
+
restoreCommand: undefined,
|
|
2006
|
+
restoreUnavailableReason: undefined,
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
const restoreIssue = retentionRestoreCommandIssue(event, backupDir);
|
|
2010
|
+
if (restoreIssue) {
|
|
2011
|
+
item.restoreUnavailableReason = restoreIssue;
|
|
2012
|
+
return item;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
const token = buildRestoreToken(event.recurrenceKey, event.beforeHash, event.afterHash);
|
|
2016
|
+
item.restoreConfirmationToken = token;
|
|
2017
|
+
item.restoreCommand = formatAgentVentCommand("retention", "restore", event.backupPath, token);
|
|
2018
|
+
return item;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function retentionRestoreCommandIssue(event, backupDir) {
|
|
2022
|
+
if (event?.action !== "archive") return "event is not an archive receipt";
|
|
2023
|
+
if (!event.recurrenceKey) return "archive receipt is missing recurrence key";
|
|
2024
|
+
if (!event.backupPath) return "archive receipt is missing backup path";
|
|
2025
|
+
if (!isSha256Hex(event.beforeHash) || !isSha256Hex(event.afterHash)) {
|
|
2026
|
+
return "archive receipt is missing valid before/after hashes";
|
|
2027
|
+
}
|
|
2028
|
+
const safeBackupDir = path.resolve(backupDir);
|
|
2029
|
+
const resolvedBackupPath = path.resolve(String(event.backupPath));
|
|
2030
|
+
if (!resolvedBackupPath.startsWith(`${safeBackupDir}${path.sep}`)) {
|
|
2031
|
+
return "backup path is outside the configured backup directory";
|
|
2032
|
+
}
|
|
2033
|
+
return undefined;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function isSha256Hex(value) {
|
|
2037
|
+
return /^[a-f0-9]{64}$/i.test(String(value || ""));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function planRetentionArchive({
|
|
2041
|
+
records = [],
|
|
2042
|
+
reviewEvents = [],
|
|
2043
|
+
curationEvents = [],
|
|
2044
|
+
recurrenceKey,
|
|
2045
|
+
storeHash,
|
|
2046
|
+
reviewHash,
|
|
2047
|
+
curationHash,
|
|
2048
|
+
}) {
|
|
2049
|
+
const requestedRecurrenceKey = sanitizeDisplayText(recurrenceKey, 200);
|
|
2050
|
+
if (!requestedRecurrenceKey) throw new Error("agent_vent retention requires a recurrenceKey");
|
|
2051
|
+
if (!storeHash || !reviewHash || !curationHash) {
|
|
2052
|
+
throw new Error(
|
|
2053
|
+
"agent_vent retention preview requires current store, review, and curation hashes",
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
const curationMap = buildCurationMap(curationEvents);
|
|
2057
|
+
const resolvedKey = resolveRecurrenceKey(requestedRecurrenceKey, curationMap);
|
|
2058
|
+
const groupRecords = records.filter(
|
|
2059
|
+
(record) =>
|
|
2060
|
+
resolveRecurrenceKey(
|
|
2061
|
+
String(record.recurrenceKey || buildRecurrenceKey(record)),
|
|
2062
|
+
curationMap,
|
|
2063
|
+
) === resolvedKey,
|
|
2064
|
+
);
|
|
2065
|
+
if (groupRecords.length === 0) {
|
|
2066
|
+
throw new Error(`cannot archive unknown recurrence group: ${requestedRecurrenceKey}`);
|
|
2067
|
+
}
|
|
2068
|
+
const reviewStates = latestReviewStates(reviewEvents, curationEvents);
|
|
2069
|
+
const reviewState = reviewStates.get(resolvedKey)?.state || "new";
|
|
2070
|
+
const archivedRecordIds = groupRecords
|
|
2071
|
+
.map((record) => sanitizeDisplayText(record.id, 120))
|
|
2072
|
+
.filter(Boolean);
|
|
2073
|
+
const fingerprint = sha256Hex(
|
|
2074
|
+
`${resolvedKey}\n${storeHash}\n${reviewHash}\n${curationHash}\n${reviewState}\n${archivedRecordIds.join("\n")}`,
|
|
2075
|
+
);
|
|
2076
|
+
return {
|
|
2077
|
+
requestedRecurrenceKey,
|
|
2078
|
+
recurrenceKey: resolvedKey,
|
|
2079
|
+
reviewState,
|
|
2080
|
+
archivable: reviewState !== "new",
|
|
2081
|
+
archivedRecordCount: groupRecords.length,
|
|
2082
|
+
archivedRecordIds,
|
|
2083
|
+
confirmationToken: `archive:${fingerprint.slice(0, 16)}`,
|
|
2084
|
+
records: groupRecords,
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function recordsToJsonl(records) {
|
|
2089
|
+
return records.map((record) => JSON.stringify(record)).join("\n") + (records.length ? "\n" : "");
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function writeRetentionBackup(input) {
|
|
2093
|
+
ensurePrivateDirectory(input.backupDir);
|
|
2094
|
+
const stamp = input.now.replace(/[:.]/g, "-");
|
|
2095
|
+
const slug = recurrenceSlug(input.recurrenceKey).slice(0, 60);
|
|
2096
|
+
const backupPath = path.join(
|
|
2097
|
+
input.backupDir,
|
|
2098
|
+
`${stamp}-${slug}-${randomUUID()}.agent-vent-backup.json`,
|
|
2099
|
+
);
|
|
2100
|
+
const backup = {
|
|
2101
|
+
schemaVersion: SCHEMA_VERSION,
|
|
2102
|
+
artifactType: "agent_vent_retention_backup",
|
|
2103
|
+
createdAt: input.now,
|
|
2104
|
+
recurrenceKey: input.recurrenceKey,
|
|
2105
|
+
requestedRecurrenceKey: input.requestedRecurrenceKey,
|
|
2106
|
+
confirmationToken: input.confirmationToken,
|
|
2107
|
+
restoreConfirmationToken: input.restoreConfirmationToken,
|
|
2108
|
+
beforeHash: input.beforeHash,
|
|
2109
|
+
afterHash: input.afterHash,
|
|
2110
|
+
archivedRecordCount: input.archivedRecordCount,
|
|
2111
|
+
archivedRecordIds: input.archivedRecordIds,
|
|
2112
|
+
classification: "local-diagnostic-user-data",
|
|
2113
|
+
boundary:
|
|
2114
|
+
"Local diagnostic rollback artifact only; not evidence, task truth, issue truth, incident truth, publication, telemetry, or ASC/self state.",
|
|
2115
|
+
ventsJsonl: input.ventsJsonl,
|
|
2116
|
+
};
|
|
2117
|
+
writeNewFileNoFollow(backupPath, `${JSON.stringify(backup, null, 2)}\n`, 0o600);
|
|
2118
|
+
return backupPath;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function readRetentionBackup(backupPath) {
|
|
2122
|
+
const stat = safeLstat(backupPath);
|
|
2123
|
+
if (!stat) throw new Error(`agent_vent retention backup not found: ${backupPath}`);
|
|
2124
|
+
assertSafeJsonlFile(backupPath, stat);
|
|
2125
|
+
if (stat.size > MAX_JSONL_FILE_BYTES * 2) {
|
|
2126
|
+
throw new Error(`agent_vent retention backup is too large: ${backupPath}`);
|
|
2127
|
+
}
|
|
2128
|
+
const backup = JSON.parse(fs.readFileSync(backupPath, "utf8"));
|
|
2129
|
+
if (backup?.artifactType !== "agent_vent_retention_backup") {
|
|
2130
|
+
throw new Error("agent_vent retention restore requires a package-created backup artifact");
|
|
2131
|
+
}
|
|
2132
|
+
for (const field of [
|
|
2133
|
+
"recurrenceKey",
|
|
2134
|
+
"beforeHash",
|
|
2135
|
+
"afterHash",
|
|
2136
|
+
"restoreConfirmationToken",
|
|
2137
|
+
"ventsJsonl",
|
|
2138
|
+
]) {
|
|
2139
|
+
if (!backup[field]) throw new Error(`agent_vent retention backup is missing ${field}`);
|
|
2140
|
+
}
|
|
2141
|
+
if (sha256Hex(String(backup.ventsJsonl)) !== backup.beforeHash) {
|
|
2142
|
+
throw new Error("agent_vent retention backup failed integrity check");
|
|
2143
|
+
}
|
|
2144
|
+
return backup;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function buildRestoreToken(recurrenceKey, beforeHash, afterHash) {
|
|
2148
|
+
return `restore:${sha256Hex(`${recurrenceKey}\n${beforeHash}\n${afterHash}`).slice(0, 16)}`;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function sha256Hex(value) {
|
|
2152
|
+
return createHash("sha256").update(String(value), "utf8").digest("hex");
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function assertBackupPathInsideDir(backupPath, backupDir) {
|
|
2156
|
+
const safeBackupDir = path.resolve(backupDir);
|
|
2157
|
+
const backupDirStat = safeLstat(safeBackupDir);
|
|
2158
|
+
if (!backupDirStat || backupDirStat.isSymbolicLink() || !backupDirStat.isDirectory()) {
|
|
2159
|
+
throw new Error(
|
|
2160
|
+
`agent_vent retention backup directory must be a real directory: ${safeBackupDir}`,
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
const resolved = path.resolve(String(backupPath || ""));
|
|
2164
|
+
if (!resolved.startsWith(`${safeBackupDir}${path.sep}`)) {
|
|
2165
|
+
throw new Error(`agent_vent retention backup path must stay inside ${safeBackupDir}`);
|
|
2166
|
+
}
|
|
2167
|
+
const realBackupDir = fs.realpathSync.native(safeBackupDir);
|
|
2168
|
+
const realBackupPath = fs.realpathSync.native(resolved);
|
|
2169
|
+
if (!realBackupPath.startsWith(`${realBackupDir}${path.sep}`)) {
|
|
2170
|
+
throw new Error(`agent_vent retention backup path must stay inside ${realBackupDir}`);
|
|
2171
|
+
}
|
|
2172
|
+
return realBackupPath;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function ensurePrivateDirectory(dirPath) {
|
|
2176
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
2177
|
+
const stat = fs.lstatSync(dirPath);
|
|
2178
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
2179
|
+
throw new Error(`agent_vent path must be a real private directory: ${dirPath}`);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function writeNewFileNoFollow(filePath, text, mode) {
|
|
2184
|
+
const fd = fs.openSync(filePath, createFileFlags(), mode);
|
|
2185
|
+
try {
|
|
2186
|
+
fs.writeFileSync(fd, text, "utf8");
|
|
2187
|
+
} finally {
|
|
2188
|
+
fs.closeSync(fd);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function writeTextFileAtomically(filePath, text, mode) {
|
|
2193
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
2194
|
+
const existing = safeLstat(filePath);
|
|
2195
|
+
if (existing) assertSafeJsonlFile(filePath, existing);
|
|
2196
|
+
const tempPath = path.join(
|
|
2197
|
+
path.dirname(filePath),
|
|
2198
|
+
`.${path.basename(filePath)}.${randomUUID()}.tmp`,
|
|
2199
|
+
);
|
|
2200
|
+
writeNewFileNoFollow(tempPath, text, mode);
|
|
2201
|
+
try {
|
|
2202
|
+
fs.renameSync(tempPath, filePath);
|
|
2203
|
+
} catch (error) {
|
|
2204
|
+
fs.rmSync(tempPath, { force: true });
|
|
2205
|
+
throw error;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function restoreTextIfHashMatches(filePath, expectedHash, text) {
|
|
2210
|
+
const existing = safeLstat(filePath);
|
|
2211
|
+
if (!existing) return;
|
|
2212
|
+
assertSafeJsonlFile(filePath, existing);
|
|
2213
|
+
const currentText = fs.readFileSync(filePath, "utf8");
|
|
2214
|
+
if (sha256Hex(currentText) === expectedHash) {
|
|
2215
|
+
writeTextFileAtomically(filePath, text, 0o600);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function acquireFileLock(lockPath, timeoutMs = 2000) {
|
|
2220
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
2221
|
+
const started = Date.now();
|
|
2222
|
+
while (true) {
|
|
2223
|
+
try {
|
|
2224
|
+
const fd = fs.openSync(lockPath, createFileFlags(), 0o600);
|
|
2225
|
+
fs.writeFileSync(fd, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
|
|
2226
|
+
fs.closeSync(fd);
|
|
2227
|
+
return lockPath;
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
if (error?.code === "EACCES" || error?.code === "EPERM") throw error;
|
|
2230
|
+
if (error?.code === "EEXIST" && removeStaleLock(lockPath)) continue;
|
|
2231
|
+
if (error?.code !== "EEXIST" || Date.now() - started >= timeoutMs) {
|
|
2232
|
+
throw new Error(`agent_vent lock unavailable: ${lockPath}`);
|
|
2233
|
+
}
|
|
2234
|
+
sleepSync(25);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function releaseFileLock(lockPath) {
|
|
2240
|
+
if (lockPath) fs.rmSync(lockPath, { force: true });
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
function sleepSync(ms) {
|
|
2244
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
function removeStaleLock(lockPath) {
|
|
2248
|
+
const stat = safeLstat(lockPath);
|
|
2249
|
+
if (!stat || stat.isSymbolicLink() || !stat.isFile()) return false;
|
|
2250
|
+
if (Date.now() - stat.mtimeMs < 30_000) return false;
|
|
2251
|
+
fs.rmSync(lockPath, { force: true });
|
|
2252
|
+
return true;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function quoteCommandArg(value) {
|
|
2256
|
+
const text = String(value || "");
|
|
2257
|
+
if (!/[\s"'\\]/.test(text)) return text;
|
|
2258
|
+
return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
export function assertCanCurateRecurrence(records, curationEvents, input) {
|
|
2262
|
+
const action = normalizeCurationAction(input?.action || input?.curationAction);
|
|
2263
|
+
const sourceRecurrenceKey = sanitizeDisplayText(input?.sourceRecurrenceKey, 200);
|
|
2264
|
+
const targetRecurrenceKey = sanitizeDisplayText(input?.targetRecurrenceKey, 200);
|
|
2265
|
+
if (!sourceRecurrenceKey || (action !== "remove" && !targetRecurrenceKey)) {
|
|
2266
|
+
throw new Error("agent_vent curation requires sourceRecurrenceKey and targetRecurrenceKey");
|
|
2267
|
+
}
|
|
2268
|
+
if (targetRecurrenceKey && sourceRecurrenceKey === targetRecurrenceKey) {
|
|
2269
|
+
throw new Error("agent_vent curation source and target must differ");
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
const knownKeys = new Set(
|
|
2273
|
+
buildGroupSummaries(records, curationEvents).map((group) => group.recurrenceKey),
|
|
2274
|
+
);
|
|
2275
|
+
for (const group of buildGroupSummaries(records, [])) knownKeys.add(group.recurrenceKey);
|
|
2276
|
+
if (!knownKeys.has(sourceRecurrenceKey)) {
|
|
2277
|
+
throw new Error(`cannot curate unknown recurrence group: ${sourceRecurrenceKey}`);
|
|
2278
|
+
}
|
|
2279
|
+
if (action === "merge" && !knownKeys.has(targetRecurrenceKey)) {
|
|
2280
|
+
throw new Error(`cannot merge into unknown recurrence group: ${targetRecurrenceKey}`);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
const nextMap = buildCurationMap(curationEvents);
|
|
2284
|
+
if (action === "remove") nextMap.delete(sourceRecurrenceKey);
|
|
2285
|
+
else nextMap.set(sourceRecurrenceKey, targetRecurrenceKey);
|
|
2286
|
+
assertNoCurationCycles(nextMap);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
function buildGroupSummaries(records, curationEvents = []) {
|
|
2290
|
+
const groups = new Map();
|
|
2291
|
+
const curationMap = buildCurationMap(curationEvents);
|
|
2292
|
+
for (const record of records) {
|
|
2293
|
+
const rawKey = String(record.recurrenceKey || buildRecurrenceKey(record));
|
|
2294
|
+
const key = resolveRecurrenceKey(rawKey, curationMap);
|
|
2295
|
+
const existing = groups.get(key) || {
|
|
2296
|
+
recurrenceKey: key,
|
|
2297
|
+
count: 0,
|
|
2298
|
+
maxSeverity: "low",
|
|
2299
|
+
categories: new Set(),
|
|
2300
|
+
firstSeen: undefined,
|
|
2301
|
+
lastSeen: undefined,
|
|
2302
|
+
latestSummary: undefined,
|
|
2303
|
+
sampleIds: [],
|
|
2304
|
+
tags: new Set(),
|
|
2305
|
+
tools: new Set(),
|
|
2306
|
+
packages: new Set(),
|
|
2307
|
+
};
|
|
2308
|
+
existing.count += 1;
|
|
2309
|
+
const severity = normalizeSeverity(record.severity);
|
|
2310
|
+
if (rankSeverity(severity) > rankSeverity(existing.maxSeverity)) {
|
|
2311
|
+
existing.maxSeverity = severity;
|
|
2312
|
+
}
|
|
2313
|
+
existing.categories.add(normalizeCategory(record.category));
|
|
2314
|
+
if (Array.isArray(record.tags)) {
|
|
2315
|
+
for (const tag of record.tags) {
|
|
2316
|
+
const normalizedTag = recurrenceSlug(tag);
|
|
2317
|
+
if (normalizedTag !== "unspecified") existing.tags.add(normalizedTag);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
const tool = sanitizeFacetText(record.tool, 160).value;
|
|
2321
|
+
if (tool) existing.tools.add(tool);
|
|
2322
|
+
const packageName = sanitizeFacetText(record.packageName, 200).value;
|
|
2323
|
+
if (packageName) existing.packages.add(packageName);
|
|
2324
|
+
if (!existing.firstSeen || String(record.createdAt) < existing.firstSeen)
|
|
2325
|
+
existing.firstSeen = record.createdAt;
|
|
2326
|
+
if (!existing.lastSeen || String(record.createdAt) > existing.lastSeen) {
|
|
2327
|
+
existing.lastSeen = record.createdAt;
|
|
2328
|
+
existing.latestSummary = record.summary;
|
|
2329
|
+
}
|
|
2330
|
+
if (existing.sampleIds.length < 5 && record.id) existing.sampleIds.push(record.id);
|
|
2331
|
+
groups.set(key, existing);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
return [...groups.values()]
|
|
2335
|
+
.map((group) => {
|
|
2336
|
+
const candidateIncident = isCandidateIncident(group);
|
|
2337
|
+
return {
|
|
2338
|
+
recurrenceKey: group.recurrenceKey,
|
|
2339
|
+
count: group.count,
|
|
2340
|
+
maxSeverity: group.maxSeverity,
|
|
2341
|
+
candidateIncident,
|
|
2342
|
+
categories: [...group.categories].sort(),
|
|
2343
|
+
tags: [...group.tags].sort(),
|
|
2344
|
+
tools: [...group.tools].sort(),
|
|
2345
|
+
packages: [...group.packages].sort(),
|
|
2346
|
+
firstSeen: group.firstSeen,
|
|
2347
|
+
lastSeen: group.lastSeen,
|
|
2348
|
+
latestSummary: group.latestSummary,
|
|
2349
|
+
sampleIds: group.sampleIds,
|
|
2350
|
+
};
|
|
2351
|
+
})
|
|
2352
|
+
.sort(compareGroups);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function buildReviewQueueItems(records, reviewEvents, curationEvents = []) {
|
|
2356
|
+
const reviewStates = latestReviewStates(reviewEvents, curationEvents);
|
|
2357
|
+
const allGroups = buildGroupSummaries(records, curationEvents);
|
|
2358
|
+
return allGroups.map((group) => {
|
|
2359
|
+
const latestReview = reviewStates.get(group.recurrenceKey);
|
|
2360
|
+
const reviewState = latestReview?.state || "new";
|
|
2361
|
+
return {
|
|
2362
|
+
...group,
|
|
2363
|
+
reviewState,
|
|
2364
|
+
decisionPosture: buildDecisionPosture({ ...group, reviewState }),
|
|
2365
|
+
reviewedAt: latestReview?.createdAt,
|
|
2366
|
+
reviewNote: latestReview?.note,
|
|
2367
|
+
reviewEventId: latestReview?.id,
|
|
2368
|
+
};
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function buildDecisionPosture(group) {
|
|
2373
|
+
const priority = group.candidateIncident ? "human_review_candidate" : "watch";
|
|
2374
|
+
const state = group.reviewState || "new";
|
|
2375
|
+
if (state === "new") {
|
|
2376
|
+
return {
|
|
2377
|
+
state: "needs_local_review",
|
|
2378
|
+
priority,
|
|
2379
|
+
summary:
|
|
2380
|
+
"No local review decision has been recorded; choose acknowledge, dismiss, or draft locally before retention archive.",
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
if (state === "acknowledged") {
|
|
2384
|
+
return {
|
|
2385
|
+
state: "reviewed_retention_ready",
|
|
2386
|
+
priority,
|
|
2387
|
+
summary:
|
|
2388
|
+
"Locally acknowledged; a human may export, draft, revisit, or preview local retention archive if useful.",
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
if (state === "dismissed") {
|
|
2392
|
+
return {
|
|
2393
|
+
state: "dismissed_retention_ready",
|
|
2394
|
+
priority,
|
|
2395
|
+
summary:
|
|
2396
|
+
"Locally dismissed; a human may revisit, export, or preview local retention archive if useful.",
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
return {
|
|
2400
|
+
state: "draft_recorded_owner_external",
|
|
2401
|
+
priority,
|
|
2402
|
+
summary:
|
|
2403
|
+
"Draft status was recorded locally; any submitted work remains owned by the external owner system.",
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
function formatDecisionPosture(group) {
|
|
2408
|
+
const posture = group.decisionPosture || buildDecisionPosture(group);
|
|
2409
|
+
return `${posture.state}; priority=${posture.priority}; ${posture.summary} This is not resolution, assignment, evidence, publication, issue status, task truth, or incident state.`;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
function formatReviewGuidance(group) {
|
|
2413
|
+
const hints = [];
|
|
2414
|
+
if (group.candidateIncident) {
|
|
2415
|
+
hints.push(
|
|
2416
|
+
"incident_review draft may help a human decide whether this is operationally significant",
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
if ((group.packages || []).length || (group.tools || []).length) {
|
|
2420
|
+
hints.push(
|
|
2421
|
+
"maintainer_note draft may help package/tool maintainers inspect local diagnostic facets",
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
const categories = new Set(group.categories || []);
|
|
2425
|
+
if (categories.has("bug") || categories.has("tool_failure") || categories.has("performance")) {
|
|
2426
|
+
hints.push(
|
|
2427
|
+
"github_issue draft may help if the target repo accepts issue-based maintenance intake",
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
if (
|
|
2431
|
+
categories.has("workflow") ||
|
|
2432
|
+
categories.has("documentation") ||
|
|
2433
|
+
categories.has("missing_capability") ||
|
|
2434
|
+
categories.has("context_loss") ||
|
|
2435
|
+
categories.has("permission")
|
|
2436
|
+
) {
|
|
2437
|
+
hints.push("ak_task draft may help if a human chooses to create durable task truth");
|
|
2438
|
+
}
|
|
2439
|
+
if (!hints.length) {
|
|
2440
|
+
hints.push("inspect samples, then choose a local review state or draft target if useful");
|
|
2441
|
+
}
|
|
2442
|
+
return `${hints.join("; ")}. Hints are local diagnostics only, not owner routing, assignment, filing, task creation, incident declaration, evidence, publication, or telemetry.`;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
function reviewOutcomeDescription(state) {
|
|
2446
|
+
if (state === "new") return "needs local human review before local retention archive";
|
|
2447
|
+
if (state === "acknowledged")
|
|
2448
|
+
return "reviewed locally; a human may inspect, export, draft, or archive if useful";
|
|
2449
|
+
if (state === "dismissed")
|
|
2450
|
+
return "locally dismissed; a human may revisit, export, or archive reviewed diagnostics";
|
|
2451
|
+
return "draft noted locally; owner system remains authoritative for any submitted work";
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
function buildExportNextActions(items = [], options = {}) {
|
|
2455
|
+
const limit = clampLimit(options.limit, 20);
|
|
2456
|
+
return items.slice(0, limit).map((item) => {
|
|
2457
|
+
const key = item.recurrenceKey;
|
|
2458
|
+
const reviewCommands =
|
|
2459
|
+
item.reviewState === "new"
|
|
2460
|
+
? [
|
|
2461
|
+
formatAgentVentCommand("review", "set", "acknowledged", key),
|
|
2462
|
+
formatAgentVentCommand("review", "set", "dismissed", key),
|
|
2463
|
+
formatAgentVentCommand("review", "set", "escalation_drafted", key),
|
|
2464
|
+
]
|
|
2465
|
+
: [];
|
|
2466
|
+
return {
|
|
2467
|
+
recurrenceKey: key,
|
|
2468
|
+
reviewState: item.reviewState,
|
|
2469
|
+
decisionPosture: item.decisionPosture || buildDecisionPosture(item),
|
|
2470
|
+
inspectCommand: `${formatAgentVentCommand("review", "show", key)} [limit]`,
|
|
2471
|
+
reviewCommands,
|
|
2472
|
+
retentionPreviewCommand:
|
|
2473
|
+
item.reviewState === "new"
|
|
2474
|
+
? undefined
|
|
2475
|
+
: formatAgentVentCommand("retention", "preview", key),
|
|
2476
|
+
exportBucketCommand: formatAgentVentCommandWithFilters(
|
|
2477
|
+
options.filters,
|
|
2478
|
+
"export",
|
|
2479
|
+
"markdown",
|
|
2480
|
+
item.reviewState,
|
|
2481
|
+
),
|
|
2482
|
+
draftCommands: [
|
|
2483
|
+
formatAgentVentCommand("draft", "github_issue", key),
|
|
2484
|
+
formatAgentVentCommand("draft", "ak_task", key),
|
|
2485
|
+
formatAgentVentCommand("draft", "incident_review", key),
|
|
2486
|
+
formatAgentVentCommand("draft", "maintainer_note", key),
|
|
2487
|
+
],
|
|
2488
|
+
boundary:
|
|
2489
|
+
"local diagnostics/drafts only; no owner routing, assignment, filing, task creation, incident declaration, evidence, publication, archive, restore, telemetry, or ASC/self mutation occurred",
|
|
2490
|
+
};
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function buildReviewOutcomeFollowupLines(group, filters = {}) {
|
|
2495
|
+
const key = group.recurrenceKey;
|
|
2496
|
+
const lines = [`inspect: ${formatAgentVentCommand("review", "show", key)} [limit]`];
|
|
2497
|
+
if (group.reviewState === "new") {
|
|
2498
|
+
lines.push(
|
|
2499
|
+
`choose local outcome: ${formatAgentVentCommand("review", "set", "acknowledged", key)} [note] | ${formatAgentVentCommand("review", "set", "dismissed", key)} [note] | ${formatAgentVentCommand("review", "set", "escalation_drafted", key)} [note]`,
|
|
2500
|
+
"retention waits for local review; draft commands still generate text only",
|
|
2501
|
+
);
|
|
2502
|
+
} else {
|
|
2503
|
+
lines.push(
|
|
2504
|
+
`optional local lifecycle: ${formatAgentVentCommand("retention", "preview", key)}`,
|
|
2505
|
+
formatExportBucketLine("export this outcome bucket", group.reviewState, filters),
|
|
2506
|
+
`revisit local state: ${formatAgentVentCommand("review", "set", "new", key)} [note]`,
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
lines.push(
|
|
2510
|
+
`draft-only handoff if a human wants text: ${formatAgentVentCommand("draft", "github_issue", key)} | ${formatAgentVentCommand("draft", "ak_task", key)} | ${formatAgentVentCommand("draft", "incident_review", key)} | ${formatAgentVentCommand("draft", "maintainer_note", key)}`,
|
|
2511
|
+
"boundary: follow-up commands are local diagnostics/drafts only; they do not file, create, declare, assign, record evidence, publish, or mutate owner systems",
|
|
2512
|
+
);
|
|
2513
|
+
return lines;
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function buildReviewNextActionLines(group, options = {}) {
|
|
2517
|
+
const key = group.recurrenceKey;
|
|
2518
|
+
const state = group.reviewState || "new";
|
|
2519
|
+
const prefix = options.compact ? "next:" : "Set local review state:";
|
|
2520
|
+
const lines = [
|
|
2521
|
+
`${prefix} ${formatAgentVentCommand("review", "set", "acknowledged", key)} [note] | ${formatAgentVentCommand("review", "set", "dismissed", key)} [note] | ${formatAgentVentCommand("review", "set", "escalation_drafted", key)} [note]`,
|
|
2522
|
+
];
|
|
2523
|
+
if (state === "new") {
|
|
2524
|
+
lines.push(
|
|
2525
|
+
"review first before local retention archive; drafts are still draft-only and require human owner-system action",
|
|
2526
|
+
);
|
|
2527
|
+
} else {
|
|
2528
|
+
lines.push(`optional local lifecycle: ${formatAgentVentCommand("retention", "preview", key)}`);
|
|
2529
|
+
}
|
|
2530
|
+
lines.push(
|
|
2531
|
+
`draft-only handoff options: ${formatAgentVentCommand("draft", "github_issue", key)} | ${formatAgentVentCommand("draft", "ak_task", key)} | ${formatAgentVentCommand("draft", "incident_review", key)} | ${formatAgentVentCommand("draft", "maintainer_note", key)}`,
|
|
2532
|
+
"boundary: draft commands only generate local text; they do not file, create, declare, assign, record evidence, publish, or mutate owner systems",
|
|
2533
|
+
);
|
|
2534
|
+
return lines;
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
function formatAgentVentCommand(...args) {
|
|
2538
|
+
return `/agent_vent ${args.map((arg) => quoteCommandArg(arg)).join(" ")}`;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
function formatAgentVentCommandWithFilters(filters, ...args) {
|
|
2542
|
+
return formatAgentVentCommand(...args, ...reviewFilterCommandArgs(filters));
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
function reviewFilterCommandArgs(filters = {}) {
|
|
2546
|
+
const args = [];
|
|
2547
|
+
if (filters.category) args.push(`category=${filters.category}`);
|
|
2548
|
+
if (filters.tool) args.push(`tool=${filters.tool}`);
|
|
2549
|
+
if (filters.packageName) args.push(`package=${filters.packageName}`);
|
|
2550
|
+
if (filters.tags?.length) args.push(`tag=${filters.tags.join(",")}`);
|
|
2551
|
+
return args;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function formatExportBucketLine(label, state, filters = {}) {
|
|
2555
|
+
const command = `${formatAgentVentCommandWithFilters(filters, "export", "markdown", state)} [limit]`;
|
|
2556
|
+
return `${label}: ${command}`;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function normalizeReviewFilters(input = {}) {
|
|
2560
|
+
assertNoEmptyReviewFilterValues(input);
|
|
2561
|
+
const category = normalizeReviewFilterCategory(input?.category);
|
|
2562
|
+
const tool = sanitizeFacetText(input?.tool || input?.toolName, 160).value;
|
|
2563
|
+
const packageName = sanitizeFacetText(input?.packageName || input?.package, 200).value;
|
|
2564
|
+
const tags = sanitizeReviewFilterTags(input?.tags || input?.tag);
|
|
2565
|
+
return removeUndefined({ category, tool, packageName, tags });
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
function assertNoEmptyReviewFilterValues(input = {}) {
|
|
2569
|
+
const entries = [
|
|
2570
|
+
["category", input?.category],
|
|
2571
|
+
["tool", input?.tool ?? input?.toolName],
|
|
2572
|
+
["package", input?.packageName ?? input?.package],
|
|
2573
|
+
["tag", input?.tags ?? input?.tag],
|
|
2574
|
+
];
|
|
2575
|
+
for (const [name, value] of entries) {
|
|
2576
|
+
if (value === undefined || value === null) continue;
|
|
2577
|
+
if (Array.isArray(value)) {
|
|
2578
|
+
if (value.some((entry) => String(entry || "").trim() === "")) {
|
|
2579
|
+
throw new Error(`invalid agent_vent review filter ${name}: empty value`);
|
|
2580
|
+
}
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
if (String(value).trim() === "") {
|
|
2584
|
+
throw new Error(`invalid agent_vent review filter ${name}: empty value`);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
function normalizeReviewFilterCategory(value) {
|
|
2590
|
+
if (value === undefined) return undefined;
|
|
2591
|
+
const normalized = String(value).trim().toLowerCase().replaceAll("-", "_");
|
|
2592
|
+
if (!CATEGORIES.includes(normalized)) {
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
`invalid agent_vent review filter category: ${value}; expected one of ${CATEGORIES.join(", ")}`,
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
return normalized;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
function sanitizeReviewFilterTags(value) {
|
|
2601
|
+
const rawTags = Array.isArray(value) ? value : value ? String(value).split(",") : [];
|
|
2602
|
+
const tags = [];
|
|
2603
|
+
for (const rawTag of rawTags) {
|
|
2604
|
+
const tag = sanitizeFacetText(rawTag, 120).value;
|
|
2605
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
2606
|
+
if (tags.length >= 12) break;
|
|
2607
|
+
}
|
|
2608
|
+
return tags;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
function hasReviewFilters(filters = {}) {
|
|
2612
|
+
return Boolean(
|
|
2613
|
+
filters.category || filters.tool || filters.packageName || (filters.tags || []).length,
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
function reviewItemMatchesFilters(item, filters = {}) {
|
|
2618
|
+
if (!hasReviewFilters(filters)) return true;
|
|
2619
|
+
if (filters.category && !(item.categories || []).includes(filters.category)) return false;
|
|
2620
|
+
if (filters.tool && !(item.tools || []).includes(filters.tool)) return false;
|
|
2621
|
+
if (filters.packageName && !(item.packages || []).includes(filters.packageName)) return false;
|
|
2622
|
+
for (const tag of filters.tags || []) {
|
|
2623
|
+
if (!(item.tags || []).includes(tag)) return false;
|
|
2624
|
+
}
|
|
2625
|
+
return true;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
function recordMatchesReviewFilters(record, filters = {}) {
|
|
2629
|
+
if (!hasReviewFilters(filters)) return true;
|
|
2630
|
+
if (filters.category && normalizeCategory(record.category) !== filters.category) return false;
|
|
2631
|
+
if (filters.tool && sanitizeFacetText(record.tool, 160).value !== filters.tool) return false;
|
|
2632
|
+
if (
|
|
2633
|
+
filters.packageName &&
|
|
2634
|
+
sanitizeFacetText(record.packageName, 200).value !== filters.packageName
|
|
2635
|
+
) {
|
|
2636
|
+
return false;
|
|
2637
|
+
}
|
|
2638
|
+
const recordTags = new Set(
|
|
2639
|
+
Array.isArray(record.tags)
|
|
2640
|
+
? record.tags.map((tag) => recurrenceSlug(tag)).filter((tag) => tag !== "unspecified")
|
|
2641
|
+
: [],
|
|
2642
|
+
);
|
|
2643
|
+
for (const tag of filters.tags || []) {
|
|
2644
|
+
if (!recordTags.has(tag)) return false;
|
|
2645
|
+
}
|
|
2646
|
+
return true;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
function formatReviewFilters(filters = {}) {
|
|
2650
|
+
const parts = [];
|
|
2651
|
+
if (filters.category) parts.push(`category=${filters.category}`);
|
|
2652
|
+
if (filters.tool) parts.push(`tool=${filters.tool}`);
|
|
2653
|
+
if (filters.packageName) parts.push(`package=${filters.packageName}`);
|
|
2654
|
+
if (filters.tags?.length) parts.push(`tags=${filters.tags.join(",")}`);
|
|
2655
|
+
return parts.join("; ") || "none";
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
function incrementCount(counts, value) {
|
|
2659
|
+
if (!value || value === "unspecified") return;
|
|
2660
|
+
counts.set(value, (counts.get(value) || 0) + 1);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function mapFacetCounts(facetMaps, limit) {
|
|
2664
|
+
return Object.fromEntries(
|
|
2665
|
+
Object.entries(facetMaps).map(([name, counts]) => [name, sortedFacetEntries(counts, limit)]),
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
function sortedFacetEntries(counts, limit) {
|
|
2670
|
+
return [...counts.entries()]
|
|
2671
|
+
.map(([name, count]) => ({ name, count }))
|
|
2672
|
+
.sort((left, right) => right.count - left.count || left.name.localeCompare(right.name))
|
|
2673
|
+
.slice(0, limit);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
function formatFacetEntries(entries = []) {
|
|
2677
|
+
if (!entries.length) return "none";
|
|
2678
|
+
return entries.map((entry) => `${entry.name}=${entry.count}`).join(", ");
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function buildCurationMap(curationEvents = []) {
|
|
2682
|
+
const aliases = new Map();
|
|
2683
|
+
for (const event of curationEvents) {
|
|
2684
|
+
if (
|
|
2685
|
+
event?.eventType !== "recurrence_curation" ||
|
|
2686
|
+
!CURATION_ACTIONS.includes(event.action) ||
|
|
2687
|
+
!event.sourceRecurrenceKey ||
|
|
2688
|
+
(event.action !== "remove" && !event.targetRecurrenceKey) ||
|
|
2689
|
+
(event.targetRecurrenceKey && event.sourceRecurrenceKey === event.targetRecurrenceKey)
|
|
2690
|
+
) {
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
if (event.action === "remove") aliases.delete(String(event.sourceRecurrenceKey));
|
|
2694
|
+
else aliases.set(String(event.sourceRecurrenceKey), String(event.targetRecurrenceKey));
|
|
2695
|
+
}
|
|
2696
|
+
assertNoCurationCycles(aliases);
|
|
2697
|
+
return aliases;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
function resolveRecurrenceKey(recurrenceKey, aliases) {
|
|
2701
|
+
let current = recurrenceKey;
|
|
2702
|
+
const seen = new Set();
|
|
2703
|
+
while (aliases.has(current)) {
|
|
2704
|
+
if (seen.has(current)) throw new Error(`agent_vent curation cycle detected at ${current}`);
|
|
2705
|
+
seen.add(current);
|
|
2706
|
+
current = aliases.get(current);
|
|
2707
|
+
}
|
|
2708
|
+
return current;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
function assertNoCurationCycles(aliases) {
|
|
2712
|
+
for (const key of aliases.keys()) resolveRecurrenceKey(key, aliases);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function jsonlFileInfo(filePath) {
|
|
2716
|
+
const stat = safeLstat(filePath);
|
|
2717
|
+
if (!stat) return { path: filePath, exists: false, sizeBytes: 0 };
|
|
2718
|
+
assertSafeJsonlFile(filePath, stat);
|
|
2719
|
+
return {
|
|
2720
|
+
path: filePath,
|
|
2721
|
+
exists: true,
|
|
2722
|
+
sizeBytes: stat.size,
|
|
2723
|
+
modifiedAt: stat.mtime.toISOString(),
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
function appendJsonlRecord(filePath, record, options = {}) {
|
|
2728
|
+
const lock = options.lockPath ? acquireFileLock(options.lockPath) : undefined;
|
|
2729
|
+
try {
|
|
2730
|
+
ensureStore(filePath);
|
|
2731
|
+
const fd = fs.openSync(filePath, appendFileFlags(), 0o600);
|
|
2732
|
+
try {
|
|
2733
|
+
fs.writeSync(fd, `${JSON.stringify(record)}\n`, undefined, "utf8");
|
|
2734
|
+
} finally {
|
|
2735
|
+
fs.closeSync(fd);
|
|
2736
|
+
}
|
|
2737
|
+
} finally {
|
|
2738
|
+
releaseFileLock(lock);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
function readJsonlRecords(filePath) {
|
|
2743
|
+
const existing = safeLstat(filePath);
|
|
2744
|
+
if (!existing)
|
|
2745
|
+
return { values: [], malformedLines: 0, oversizedLines: 0, fileHash: sha256Hex("") };
|
|
2746
|
+
assertSafeJsonlFile(filePath, existing);
|
|
2747
|
+
if (existing.size > MAX_JSONL_FILE_BYTES) {
|
|
2748
|
+
throw new Error(
|
|
2749
|
+
`agent_vent store file is too large: ${filePath} (${existing.size} bytes > ${MAX_JSONL_FILE_BYTES})`,
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
const fd = fs.openSync(filePath, readFileFlags());
|
|
2754
|
+
let text;
|
|
2755
|
+
try {
|
|
2756
|
+
text = fs.readFileSync(fd, "utf8");
|
|
2757
|
+
} finally {
|
|
2758
|
+
fs.closeSync(fd);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
const values = [];
|
|
2762
|
+
let malformedLines = 0;
|
|
2763
|
+
let oversizedLines = 0;
|
|
2764
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2765
|
+
if (!line.trim()) continue;
|
|
2766
|
+
if (Buffer.byteLength(line, "utf8") > MAX_JSONL_LINE_BYTES) {
|
|
2767
|
+
oversizedLines += 1;
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
try {
|
|
2771
|
+
const value = JSON.parse(line);
|
|
2772
|
+
if (value && typeof value === "object") {
|
|
2773
|
+
values.push(value);
|
|
2774
|
+
}
|
|
2775
|
+
} catch {
|
|
2776
|
+
malformedLines += 1;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return { values, malformedLines, oversizedLines, fileHash: sha256Hex(text) };
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
function safeLstat(filePath) {
|
|
2783
|
+
try {
|
|
2784
|
+
return fs.lstatSync(filePath);
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
if (error?.code === "ENOENT") return undefined;
|
|
2787
|
+
throw error;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
function assertSafeJsonlFile(filePath, stat) {
|
|
2792
|
+
if (stat.isSymbolicLink()) {
|
|
2793
|
+
throw new Error(`agent_vent store file must not be a symlink: ${filePath}`);
|
|
2794
|
+
}
|
|
2795
|
+
if (!stat.isFile()) {
|
|
2796
|
+
throw new Error(`agent_vent store path must be a regular file: ${filePath}`);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
function createFileFlags() {
|
|
2801
|
+
return fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | noFollowFlag();
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
function appendFileFlags() {
|
|
2805
|
+
return fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY | noFollowFlag();
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function readFileFlags() {
|
|
2809
|
+
return fs.constants.O_RDONLY | noFollowFlag();
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
function noFollowFlag() {
|
|
2813
|
+
return fs.constants.O_NOFOLLOW || 0;
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
function isCandidateIncident(group) {
|
|
2817
|
+
const severityRank = rankSeverity(group.maxSeverity);
|
|
2818
|
+
return (
|
|
2819
|
+
group.maxSeverity === "critical" ||
|
|
2820
|
+
(group.count >= 3 && severityRank >= rankSeverity("medium")) ||
|
|
2821
|
+
(group.count >= 2 && severityRank >= rankSeverity("high"))
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
function compareGroups(a, b) {
|
|
2826
|
+
if (a.candidateIncident !== b.candidateIncident) return a.candidateIncident ? -1 : 1;
|
|
2827
|
+
const severityDelta = rankSeverity(b.maxSeverity) - rankSeverity(a.maxSeverity);
|
|
2828
|
+
if (severityDelta !== 0) return severityDelta;
|
|
2829
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
2830
|
+
return String(b.lastSeen || "").localeCompare(String(a.lastSeen || ""));
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
function rankSeverity(severity) {
|
|
2834
|
+
return SEVERITY_RANK.get(normalizeSeverity(severity)) ?? 1;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
function removeUndefined(value) {
|
|
2838
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
2839
|
+
}
|