@bli-cockpit/cli 0.1.19 → 0.1.21
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/README.md +34 -6
- package/dist/adapters/claude-attribution.js +47 -4
- package/dist/adapters/codex-attribution.js +41 -5
- package/dist/adapters/local-sources.js +2 -0
- package/dist/adapters/raw-evidence.js +487 -35
- package/dist/agent-rules.js +44 -19
- package/dist/commands/local-args.js +51 -2
- package/dist/commands/local.js +31 -15
- package/dist/commands/public-root.js +2 -2
- package/dist/commands/session-sync.js +14 -0
- package/dist/local-state.js +7 -1
- package/dist/upload.js +117 -1
- package/package.json +2 -2
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { SECRET_FILE_SEGMENT_PATTERN, SourceScanResultSchema, containsSecretLikeContent, } from "@bli-cockpit/telemetry-core";
|
|
2
|
-
import {
|
|
1
|
+
import { EvidenceCompletenessPayloadSchema, SECRET_FILE_SEGMENT_PATTERN, SourceScanResultSchema, containsSecretLikeContent, } from "@bli-cockpit/telemetry-core";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
7
|
import { makeSourceAdapterIdentity, } from "./common.js";
|
|
9
8
|
import { collectAgentImageEvidenceFromJsonlFile, } from "./agent-image-evidence.js";
|
|
10
|
-
const execFileAsync = promisify(execFile);
|
|
11
9
|
const DEFAULT_SINCE_MINUTES = 24 * 60;
|
|
12
10
|
const DEFAULT_SESSION_LIMIT = 50;
|
|
13
11
|
const MAX_GIT_DIFF_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const GIT_DIFF_TIMEOUT_MS = 3_000;
|
|
14
13
|
export const RAW_EVIDENCE_BUCKET = "ambient-raw-evidence";
|
|
15
14
|
export const RAW_EVIDENCE_RETENTION_MODE = "remote_durable";
|
|
16
15
|
// Per-sync upload budgets enforced at COLLECTION time (D7b). With sidecars a
|
|
@@ -28,39 +27,97 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
28
27
|
const filesDir = path.join(evidenceDir, "files");
|
|
29
28
|
const entries = [];
|
|
30
29
|
const skipped = [];
|
|
30
|
+
const truncated = [];
|
|
31
|
+
const failed = [];
|
|
31
32
|
const reused = [];
|
|
33
|
+
const sinceMinutes = options.sinceMinutes ?? DEFAULT_SINCE_MINUTES;
|
|
34
|
+
const sessionLimit = options.sessionLimit ?? DEFAULT_SESSION_LIMIT;
|
|
35
|
+
const byteBudget = options.byteBudget ?? RAW_EVIDENCE_DEFAULT_BYTE_BUDGET;
|
|
36
|
+
const objectBudget = options.objectBudget ?? RAW_EVIDENCE_DEFAULT_OBJECT_BUDGET;
|
|
32
37
|
const collection = {
|
|
33
38
|
context,
|
|
34
39
|
filesDir,
|
|
35
40
|
packId,
|
|
36
41
|
entries,
|
|
37
42
|
skipped,
|
|
43
|
+
truncated,
|
|
44
|
+
failed,
|
|
38
45
|
reused,
|
|
46
|
+
scanned: new Map(),
|
|
47
|
+
caps: [
|
|
48
|
+
{
|
|
49
|
+
source: "raw_evidence",
|
|
50
|
+
cap_type: "byte_budget",
|
|
51
|
+
limit: byteBudget,
|
|
52
|
+
observed: options.budget?.remainingBytes ?? byteBudget,
|
|
53
|
+
applied: false,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
source: "raw_evidence",
|
|
57
|
+
cap_type: "object_budget",
|
|
58
|
+
limit: objectBudget,
|
|
59
|
+
observed: options.budget?.remainingObjects ?? objectBudget,
|
|
60
|
+
applied: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
source: "git_diff",
|
|
64
|
+
cap_type: "max_bytes_per_diff",
|
|
65
|
+
limit: MAX_GIT_DIFF_BYTES,
|
|
66
|
+
applied: false,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
source: "git_diff",
|
|
70
|
+
cap_type: "timeout_ms",
|
|
71
|
+
limit: GIT_DIFF_TIMEOUT_MS,
|
|
72
|
+
applied: false,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
source: "claude_jsonl",
|
|
76
|
+
cap_type: "max_file_bytes",
|
|
77
|
+
limit: CLAUDE_MAX_COLLECT_FILE_BYTES,
|
|
78
|
+
applied: false,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
source: "claude_jsonl_sidecar",
|
|
82
|
+
cap_type: "max_file_bytes",
|
|
83
|
+
limit: CLAUDE_MAX_COLLECT_FILE_BYTES,
|
|
84
|
+
applied: false,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
39
87
|
skipContentHashes: options.skipContentHashes ?? new Set(),
|
|
40
88
|
budget: options.budget ?? {
|
|
41
|
-
remainingBytes:
|
|
42
|
-
remainingObjects:
|
|
89
|
+
remainingBytes: byteBudget,
|
|
90
|
+
remainingObjects: objectBudget,
|
|
43
91
|
},
|
|
44
92
|
index: { value: 0 },
|
|
45
93
|
};
|
|
46
94
|
try {
|
|
47
95
|
await ensurePrivateDir(evidenceDir);
|
|
48
96
|
await ensurePrivateDir(filesDir);
|
|
97
|
+
recordAttributionCompleteness(collection, {
|
|
98
|
+
codex: options.codexAttributionScan,
|
|
99
|
+
claude: options.claudeAttributionScan,
|
|
100
|
+
});
|
|
49
101
|
if (options.includeCodexJsonl !== false) {
|
|
50
102
|
await collectCodexJsonlFiles(collection, {
|
|
51
103
|
codexSessionFiles: options.codexSessionFiles,
|
|
52
104
|
sessionsDir: options.sessionsDir,
|
|
53
|
-
sinceMinutes
|
|
54
|
-
limit:
|
|
105
|
+
sinceMinutes,
|
|
106
|
+
limit: sessionLimit,
|
|
55
107
|
});
|
|
56
108
|
}
|
|
57
109
|
if (options.includeClaudeJsonl !== false && options.claudeSessionFiles) {
|
|
58
110
|
await collectClaudeJsonlFiles(collection, options.claudeSessionFiles);
|
|
59
111
|
}
|
|
60
112
|
await collectGitDiffFiles(collection, options.repoRoot);
|
|
61
|
-
const deferredByteBudgetCount = skipped.filter((entry) => entry.reason === "deferred_byte_budget").
|
|
62
|
-
const deferredObjectBudgetCount = skipped.filter((entry) => entry.reason === "deferred_object_budget").
|
|
113
|
+
const deferredByteBudgetCount = skipped.filter((entry) => entry.reason === "deferred_byte_budget").reduce((sum, entry) => sum + evidenceEntryCount(entry), 0);
|
|
114
|
+
const deferredObjectBudgetCount = skipped.filter((entry) => entry.reason === "deferred_object_budget").reduce((sum, entry) => sum + evidenceEntryCount(entry), 0);
|
|
63
115
|
if (entries.length === 0) {
|
|
116
|
+
const evidenceCompleteness = makeEvidenceCompleteness(collection, {
|
|
117
|
+
startedAt,
|
|
118
|
+
finishedAt: context.now.toISOString(),
|
|
119
|
+
sinceMinutes,
|
|
120
|
+
});
|
|
64
121
|
const facts = {
|
|
65
122
|
pack_id: packId,
|
|
66
123
|
manifest_path: path.join(evidenceDir, "manifest.json"),
|
|
@@ -68,11 +125,12 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
68
125
|
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
69
126
|
file_count: 0,
|
|
70
127
|
byte_size: 0,
|
|
71
|
-
skipped_count: skipped
|
|
128
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
72
129
|
reused_count: reused.length,
|
|
73
130
|
deferred_byte_budget_count: deferredByteBudgetCount,
|
|
74
131
|
deferred_object_budget_count: deferredObjectBudgetCount,
|
|
75
132
|
content_kinds: [],
|
|
133
|
+
evidence_completeness: evidenceCompleteness,
|
|
76
134
|
pointers: [],
|
|
77
135
|
upload_files: [],
|
|
78
136
|
reused,
|
|
@@ -109,6 +167,11 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
109
167
|
bytes: manifestBytes,
|
|
110
168
|
});
|
|
111
169
|
entries.push(manifestEntry);
|
|
170
|
+
const evidenceCompleteness = makeEvidenceCompleteness(collection, {
|
|
171
|
+
startedAt,
|
|
172
|
+
finishedAt: context.now.toISOString(),
|
|
173
|
+
sinceMinutes,
|
|
174
|
+
});
|
|
112
175
|
const facts = {
|
|
113
176
|
pack_id: packId,
|
|
114
177
|
manifest_path: manifestPath,
|
|
@@ -116,11 +179,12 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
116
179
|
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
117
180
|
file_count: entries.length,
|
|
118
181
|
byte_size: entries.reduce((sum, entry) => sum + entry.byte_size, 0),
|
|
119
|
-
skipped_count: skipped
|
|
182
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
120
183
|
reused_count: reused.length,
|
|
121
184
|
deferred_byte_budget_count: deferredByteBudgetCount,
|
|
122
185
|
deferred_object_budget_count: deferredObjectBudgetCount,
|
|
123
186
|
content_kinds: [...new Set(entries.map((entry) => entry.kind))],
|
|
187
|
+
evidence_completeness: evidenceCompleteness,
|
|
124
188
|
pointers: entries.map(pointerFromEntry),
|
|
125
189
|
upload_files: entries.map((entry) => ({
|
|
126
190
|
pointer: pointerFromEntry(entry),
|
|
@@ -143,18 +207,46 @@ export async function collectRawEvidencePack(context, options) {
|
|
|
143
207
|
}),
|
|
144
208
|
};
|
|
145
209
|
}
|
|
146
|
-
catch
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
status: "failed",
|
|
151
|
-
started_at: startedAt,
|
|
152
|
-
finished_at: context.now.toISOString(),
|
|
153
|
-
diagnostic_labels: [
|
|
154
|
-
`raw_evidence_failed:${error instanceof Error ? error.message : String(error)}`,
|
|
155
|
-
],
|
|
210
|
+
catch {
|
|
211
|
+
failed.push({
|
|
212
|
+
kind: "raw_evidence",
|
|
213
|
+
reason: "collection_failed",
|
|
156
214
|
});
|
|
157
|
-
|
|
215
|
+
const evidenceCompleteness = makeEvidenceCompleteness(collection, {
|
|
216
|
+
startedAt,
|
|
217
|
+
finishedAt: context.now.toISOString(),
|
|
218
|
+
sinceMinutes,
|
|
219
|
+
});
|
|
220
|
+
const facts = {
|
|
221
|
+
pack_id: packId,
|
|
222
|
+
manifest_path: path.join(evidenceDir, "manifest.json"),
|
|
223
|
+
evidence_dir: evidenceDir,
|
|
224
|
+
storage_bucket: RAW_EVIDENCE_BUCKET,
|
|
225
|
+
file_count: 0,
|
|
226
|
+
byte_size: 0,
|
|
227
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
228
|
+
reused_count: reused.length,
|
|
229
|
+
deferred_byte_budget_count: skipped
|
|
230
|
+
.filter((entry) => entry.reason === "deferred_byte_budget")
|
|
231
|
+
.reduce((sum, entry) => sum + evidenceEntryCount(entry), 0),
|
|
232
|
+
deferred_object_budget_count: skipped
|
|
233
|
+
.filter((entry) => entry.reason === "deferred_object_budget")
|
|
234
|
+
.reduce((sum, entry) => sum + evidenceEntryCount(entry), 0),
|
|
235
|
+
content_kinds: [],
|
|
236
|
+
evidence_completeness: evidenceCompleteness,
|
|
237
|
+
pointers: [],
|
|
238
|
+
upload_files: [],
|
|
239
|
+
reused,
|
|
240
|
+
};
|
|
241
|
+
return {
|
|
242
|
+
facts,
|
|
243
|
+
scan: makeRawEvidenceScan({
|
|
244
|
+
context,
|
|
245
|
+
startedAt,
|
|
246
|
+
status: "failed",
|
|
247
|
+
facts,
|
|
248
|
+
}),
|
|
249
|
+
};
|
|
158
250
|
}
|
|
159
251
|
}
|
|
160
252
|
function makeRawEvidenceScan(options) {
|
|
@@ -185,20 +277,128 @@ function makeRawEvidenceScan(options) {
|
|
|
185
277
|
`bytes:${options.facts.byte_size}`,
|
|
186
278
|
`skipped:${options.facts.skipped_count}`,
|
|
187
279
|
`reused:${options.facts.reused_count}`,
|
|
280
|
+
`completeness:${options.facts.evidence_completeness.status}`,
|
|
281
|
+
`truncated:${options.facts.evidence_completeness.totals.truncated_count}`,
|
|
282
|
+
`deferred:${options.facts.evidence_completeness.totals.deferred_count}`,
|
|
283
|
+
`failed:${options.facts.evidence_completeness.totals.failed_count}`,
|
|
188
284
|
...options.facts.content_kinds.map((kind) => `kind:${kind}`),
|
|
189
285
|
],
|
|
190
286
|
});
|
|
191
287
|
}
|
|
288
|
+
function recordAttributionCompleteness(collection, scans) {
|
|
289
|
+
if (scans.codex)
|
|
290
|
+
recordCodexAttributionCompleteness(collection, scans.codex);
|
|
291
|
+
if (scans.claude)
|
|
292
|
+
recordClaudeAttributionCompleteness(collection, scans.claude);
|
|
293
|
+
}
|
|
294
|
+
function recordCodexAttributionCompleteness(collection, scan) {
|
|
295
|
+
recordScanned(collection, "codex_attribution", scan.scanned_file_count);
|
|
296
|
+
collection.caps.push({
|
|
297
|
+
source: "codex_attribution",
|
|
298
|
+
cap_type: "scan_window_minutes",
|
|
299
|
+
limit: scan.since_minutes,
|
|
300
|
+
observed: scan.since_minutes,
|
|
301
|
+
applied: false,
|
|
302
|
+
}, {
|
|
303
|
+
source: "codex_attribution",
|
|
304
|
+
cap_type: "session_limit",
|
|
305
|
+
limit: scan.session_limit,
|
|
306
|
+
observed: scan.discovered_file_count,
|
|
307
|
+
applied: scan.session_limit_applied,
|
|
308
|
+
}, {
|
|
309
|
+
source: "codex_attribution",
|
|
310
|
+
cap_type: "max_file_bytes",
|
|
311
|
+
limit: scan.max_file_bytes,
|
|
312
|
+
applied: scan.results.some((result) => result.reason === "file_too_large"),
|
|
313
|
+
});
|
|
314
|
+
recordSkipCount(collection, "codex_attribution", "session_limit_overflow", Math.max(0, scan.discovered_file_count - scan.scanned_file_count));
|
|
315
|
+
recordSkipCount(collection, "codex_attribution", "directory_read_failed", scan.directory_read_failed_count);
|
|
316
|
+
recordSkipCount(collection, "codex_attribution", "file_stat_failed", scan.stat_failed_count);
|
|
317
|
+
recordSkipCount(collection, "codex_attribution", "secret_like_directory", scan.secret_path_skipped_count);
|
|
318
|
+
recordAttributionResultSkips(collection, "codex_attribution", scan.results);
|
|
319
|
+
}
|
|
320
|
+
function recordClaudeAttributionCompleteness(collection, scan) {
|
|
321
|
+
recordScanned(collection, "claude_attribution", scan.scanned_session_count);
|
|
322
|
+
if (scan.disabled_reason) {
|
|
323
|
+
recordSkipCount(collection, "claude_attribution", scan.disabled_reason, 1);
|
|
324
|
+
}
|
|
325
|
+
collection.caps.push({
|
|
326
|
+
source: "claude_attribution",
|
|
327
|
+
cap_type: "scan_window_minutes",
|
|
328
|
+
limit: scan.since_minutes,
|
|
329
|
+
observed: scan.since_minutes,
|
|
330
|
+
applied: false,
|
|
331
|
+
}, {
|
|
332
|
+
source: "claude_attribution",
|
|
333
|
+
cap_type: "session_limit",
|
|
334
|
+
limit: scan.session_limit,
|
|
335
|
+
observed: scan.discovered_session_count,
|
|
336
|
+
applied: scan.session_limit_applied,
|
|
337
|
+
}, {
|
|
338
|
+
source: "claude_attribution",
|
|
339
|
+
cap_type: "max_file_bytes",
|
|
340
|
+
limit: scan.max_file_bytes,
|
|
341
|
+
applied: scan.counts.mains_oversized > 0 ||
|
|
342
|
+
scan.results.some((result) => result.reason === "file_too_large"),
|
|
343
|
+
}, {
|
|
344
|
+
source: "claude_attribution",
|
|
345
|
+
cap_type: "max_sidecar_files",
|
|
346
|
+
limit: scan.max_sidecar_files,
|
|
347
|
+
observed: scan.max_sidecar_files + scan.counts.sidecars_capped,
|
|
348
|
+
applied: scan.counts.sidecars_capped > 0,
|
|
349
|
+
}, {
|
|
350
|
+
source: "claude_attribution",
|
|
351
|
+
cap_type: "max_line_buffer_bytes",
|
|
352
|
+
limit: scan.max_line_buffer_bytes,
|
|
353
|
+
applied: scan.counts.oversized_lines_skipped > 0,
|
|
354
|
+
});
|
|
355
|
+
recordSkipCount(collection, "claude_attribution", "session_limit_overflow", Math.max(0, scan.discovered_session_count - scan.scanned_session_count));
|
|
356
|
+
recordSkipCount(collection, "claude_attribution", "secret_like_project_dir", scan.project_dirs_skipped);
|
|
357
|
+
recordSkipCount(collection, "claude_attribution", "project_dir_read_failed", scan.project_dir_read_failed_count);
|
|
358
|
+
recordSkipCount(collection, "claude_attribution", "session_stat_failed", scan.session_stat_failed_count);
|
|
359
|
+
recordSkipCount(collection, "claude_attribution", "sidecar_dir_read_failed", scan.sidecar_dir_read_failed_count);
|
|
360
|
+
recordSkipCount(collection, "claude_attribution", "sidecar_stat_failed", scan.sidecar_stat_failed_count);
|
|
361
|
+
recordSkipCount(collection, "claude_attribution", "sidecar_limit_overflow", scan.counts.sidecars_capped);
|
|
362
|
+
recordTruncationCount(collection, "claude_attribution", "oversized_jsonl_line", scan.counts.oversized_lines_skipped, { max_bytes: scan.max_line_buffer_bytes });
|
|
363
|
+
recordAttributionResultSkips(collection, "claude_attribution", scan.results);
|
|
364
|
+
for (const result of scan.results) {
|
|
365
|
+
for (const sidecar of result.sidecar_files) {
|
|
366
|
+
if (!sidecar.skipped_reason)
|
|
367
|
+
continue;
|
|
368
|
+
recordSkipCount(collection, "claude_attribution", `sidecar_${sidecar.skipped_reason}`, 1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function recordAttributionResultSkips(collection, source, results) {
|
|
373
|
+
for (const result of results) {
|
|
374
|
+
if (result.state === "attributed")
|
|
375
|
+
continue;
|
|
376
|
+
const reason = result.state === "skipped"
|
|
377
|
+
? result.reason
|
|
378
|
+
: `attribution_${result.state}:${result.reason}`;
|
|
379
|
+
recordSkipCount(collection, source, reason, 1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
192
382
|
async function collectCodexJsonlFiles(collection, options) {
|
|
193
|
-
const
|
|
383
|
+
const resolvedCandidates = options.codexSessionFiles
|
|
194
384
|
? options.codexSessionFiles.map((file) => ({
|
|
195
385
|
filePath: file.local_path,
|
|
196
386
|
codexSessionId: file.codex_session_id,
|
|
197
387
|
}))
|
|
198
388
|
: (await walkJsonlFiles(options.sessionsDir ?? path.join(os.homedir(), ".codex", "sessions"), collection.context.now.getTime() - options.sinceMinutes * 60 * 1000))
|
|
199
|
-
.slice(0, options.limit)
|
|
200
389
|
.map((filePath) => ({ filePath, codexSessionId: null }));
|
|
390
|
+
collection.caps.push({
|
|
391
|
+
source: "codex_jsonl",
|
|
392
|
+
cap_type: "session_limit",
|
|
393
|
+
limit: options.limit,
|
|
394
|
+
observed: resolvedCandidates.length,
|
|
395
|
+
applied: !options.codexSessionFiles && resolvedCandidates.length > options.limit,
|
|
396
|
+
});
|
|
397
|
+
const candidates = options.codexSessionFiles
|
|
398
|
+
? resolvedCandidates
|
|
399
|
+
: resolvedCandidates.slice(0, options.limit);
|
|
201
400
|
for (const candidate of candidates) {
|
|
401
|
+
recordScanned(collection, "codex_jsonl");
|
|
202
402
|
const codexSessionId = candidate.codexSessionId ?? shortHash(candidate.filePath);
|
|
203
403
|
const transcriptAccepted = await collectOneEvidenceFile(collection, {
|
|
204
404
|
filePath: candidate.filePath,
|
|
@@ -220,6 +420,7 @@ async function collectCodexJsonlFiles(collection, options) {
|
|
|
220
420
|
}
|
|
221
421
|
}
|
|
222
422
|
async function collectClaudeJsonlFiles(collection, sessions) {
|
|
423
|
+
recordScanned(collection, "claude_jsonl", sessions.reduce((count, session) => count + 1 + session.sidecar_files.length, 0));
|
|
223
424
|
for (const session of sessions) {
|
|
224
425
|
const sessionId = session.claude_session_id;
|
|
225
426
|
if (session.main_file_oversized) {
|
|
@@ -300,6 +501,7 @@ async function collectAgentImagesFromTranscript(collection, options) {
|
|
|
300
501
|
sessionId: options.sessionId,
|
|
301
502
|
sidecarId: options.sidecarId,
|
|
302
503
|
});
|
|
504
|
+
recordScanned(collection, options.kind, result.images.length + result.skipped.length);
|
|
303
505
|
for (const skipped of result.skipped) {
|
|
304
506
|
collection.skipped.push({
|
|
305
507
|
kind: options.kind,
|
|
@@ -336,6 +538,7 @@ async function collectOneAgentImageFile(collection, options) {
|
|
|
336
538
|
}
|
|
337
539
|
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
338
540
|
if (deferReason) {
|
|
541
|
+
markBudgetCapApplied(collection, deferReason);
|
|
339
542
|
collection.skipped.push({
|
|
340
543
|
kind: options.kind,
|
|
341
544
|
label: options.image.label,
|
|
@@ -392,6 +595,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
392
595
|
return false;
|
|
393
596
|
}
|
|
394
597
|
if (options.maxFileBytes && raw.byteLength > options.maxFileBytes) {
|
|
598
|
+
markCapApplied(collection, options.kind, "max_file_bytes");
|
|
395
599
|
collection.skipped.push({
|
|
396
600
|
kind: options.kind,
|
|
397
601
|
label: fileName,
|
|
@@ -419,6 +623,7 @@ async function collectOneEvidenceFile(collection, options) {
|
|
|
419
623
|
}
|
|
420
624
|
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
421
625
|
if (deferReason) {
|
|
626
|
+
markBudgetCapApplied(collection, deferReason);
|
|
422
627
|
collection.skipped.push({
|
|
423
628
|
kind: options.kind,
|
|
424
629
|
label: fileName,
|
|
@@ -465,10 +670,34 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
465
670
|
{ label: "staged", args: ["diff", "--cached", "--no-ext-diff", "--"] },
|
|
466
671
|
];
|
|
467
672
|
for (const target of diffTargets) {
|
|
468
|
-
|
|
469
|
-
|
|
673
|
+
recordScanned(collection, "git_diff");
|
|
674
|
+
let diff;
|
|
675
|
+
try {
|
|
676
|
+
diff = await runGitDiff(target.args, repoRoot);
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
collection.skipped.push({
|
|
680
|
+
kind: "git_diff",
|
|
681
|
+
label: target.label,
|
|
682
|
+
reason: "git_diff_failed",
|
|
683
|
+
});
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (diff.truncated) {
|
|
687
|
+
markCapApplied(collection, "git_diff", diff.truncationCapType);
|
|
688
|
+
collection.truncated.push({
|
|
689
|
+
kind: "git_diff",
|
|
690
|
+
reason: diff.truncationReason,
|
|
691
|
+
...(diff.truncationCapType === "max_bytes_per_diff"
|
|
692
|
+
? { max_bytes: MAX_GIT_DIFF_BYTES }
|
|
693
|
+
: {}),
|
|
694
|
+
observed_bytes: diff.observedBytes,
|
|
695
|
+
included_bytes: Buffer.byteLength(diff.stdout, "utf8"),
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
if (!diff.stdout.trim())
|
|
470
699
|
continue;
|
|
471
|
-
if (containsSecretLikeContent(diff)) {
|
|
700
|
+
if (containsSecretLikeContent(diff.stdout)) {
|
|
472
701
|
collection.skipped.push({
|
|
473
702
|
kind: "git_diff",
|
|
474
703
|
label: target.label,
|
|
@@ -476,7 +705,7 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
476
705
|
});
|
|
477
706
|
continue;
|
|
478
707
|
}
|
|
479
|
-
const raw = Buffer.from(diff.
|
|
708
|
+
const raw = Buffer.from(diff.stdout, "utf8");
|
|
480
709
|
const contentHash = sha256(raw);
|
|
481
710
|
if (collection.skipContentHashes.has(contentHash)) {
|
|
482
711
|
collection.reused.push({
|
|
@@ -489,6 +718,7 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
489
718
|
}
|
|
490
719
|
const deferReason = admitToBudget(collection.budget, raw.byteLength);
|
|
491
720
|
if (deferReason) {
|
|
721
|
+
markBudgetCapApplied(collection, deferReason);
|
|
492
722
|
collection.skipped.push({
|
|
493
723
|
kind: "git_diff",
|
|
494
724
|
label: target.label,
|
|
@@ -507,7 +737,9 @@ async function collectGitDiffFiles(collection, repoRoot) {
|
|
|
507
737
|
localPath: destination,
|
|
508
738
|
relativePath,
|
|
509
739
|
mediaType: "text/x-diff",
|
|
510
|
-
redactedSummary:
|
|
740
|
+
redactedSummary: diff.truncated
|
|
741
|
+
? `Raw git ${target.label} diff truncated to the capture cap and preserved locally with env/secret paths excluded.`
|
|
742
|
+
: `Raw git ${target.label} diff preserved locally with env/secret paths excluded.`,
|
|
511
743
|
bytes: raw,
|
|
512
744
|
contentAddress: `git-diff/${target.label}-${contentHash.slice(0, 16)}.diff`,
|
|
513
745
|
}));
|
|
@@ -526,12 +758,232 @@ async function runGitDiff(args, repoRoot) {
|
|
|
526
758
|
":(exclude)**/*.pem",
|
|
527
759
|
":(exclude)**/*.key",
|
|
528
760
|
];
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
761
|
+
return new Promise((resolve, reject) => {
|
|
762
|
+
const child = spawn("git", [...args, ...pathspec], {
|
|
763
|
+
cwd: repoRoot,
|
|
764
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
765
|
+
});
|
|
766
|
+
const stdoutChunks = [];
|
|
767
|
+
const stderrChunks = [];
|
|
768
|
+
let observedBytes = 0;
|
|
769
|
+
let includedBytes = 0;
|
|
770
|
+
let truncated = false;
|
|
771
|
+
let timedOut = false;
|
|
772
|
+
const timeout = setTimeout(() => {
|
|
773
|
+
timedOut = true;
|
|
774
|
+
truncated = true;
|
|
775
|
+
child.kill("SIGTERM");
|
|
776
|
+
}, GIT_DIFF_TIMEOUT_MS);
|
|
777
|
+
child.stdout.on("data", (chunk) => {
|
|
778
|
+
observedBytes += chunk.byteLength;
|
|
779
|
+
if (includedBytes < MAX_GIT_DIFF_BYTES) {
|
|
780
|
+
const remaining = MAX_GIT_DIFF_BYTES - includedBytes;
|
|
781
|
+
const next = chunk.subarray(0, remaining);
|
|
782
|
+
stdoutChunks.push(next);
|
|
783
|
+
includedBytes += next.byteLength;
|
|
784
|
+
}
|
|
785
|
+
if (observedBytes > MAX_GIT_DIFF_BYTES) {
|
|
786
|
+
truncated = true;
|
|
787
|
+
child.kill("SIGTERM");
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
child.stderr.on("data", (chunk) => {
|
|
791
|
+
if (stderrChunks.reduce((sum, item) => sum + item.byteLength, 0) < 4096) {
|
|
792
|
+
stderrChunks.push(chunk.subarray(0, 4096));
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
child.on("error", (error) => {
|
|
796
|
+
clearTimeout(timeout);
|
|
797
|
+
reject(error);
|
|
798
|
+
});
|
|
799
|
+
child.on("close", (code, signal) => {
|
|
800
|
+
clearTimeout(timeout);
|
|
801
|
+
if (code === 0 || truncated || signal === "SIGTERM") {
|
|
802
|
+
resolve({
|
|
803
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
804
|
+
truncated,
|
|
805
|
+
observedBytes,
|
|
806
|
+
truncationReason: timedOut
|
|
807
|
+
? "git_diff_timeout"
|
|
808
|
+
: "max_git_diff_bytes",
|
|
809
|
+
truncationCapType: timedOut ? "timeout_ms" : "max_bytes_per_diff",
|
|
810
|
+
});
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
814
|
+
reject(new Error(stderr || `git diff failed with code ${code ?? signal}`));
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function recordScanned(collection, source, count = 1) {
|
|
819
|
+
collection.scanned.set(source, (collection.scanned.get(source) ?? 0) + count);
|
|
820
|
+
}
|
|
821
|
+
function markBudgetCapApplied(collection, reason) {
|
|
822
|
+
markCapApplied(collection, "raw_evidence", reason === "deferred_object_budget" ? "object_budget" : "byte_budget");
|
|
823
|
+
}
|
|
824
|
+
function markCapApplied(collection, source, capType) {
|
|
825
|
+
const cap = collection.caps.find((candidate) => candidate.source === source && candidate.cap_type === capType);
|
|
826
|
+
if (cap)
|
|
827
|
+
cap.applied = true;
|
|
828
|
+
}
|
|
829
|
+
function recordSkipCount(collection, source, reason, count) {
|
|
830
|
+
if (count <= 0)
|
|
831
|
+
return;
|
|
832
|
+
collection.skipped.push({
|
|
833
|
+
kind: source,
|
|
834
|
+
label: reason,
|
|
835
|
+
reason,
|
|
836
|
+
count,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function recordTruncationCount(collection, source, reason, count, details = {}) {
|
|
840
|
+
if (count <= 0)
|
|
841
|
+
return;
|
|
842
|
+
collection.truncated.push({
|
|
843
|
+
kind: source,
|
|
844
|
+
reason,
|
|
845
|
+
count,
|
|
846
|
+
...details,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function evidenceEntryCount(entry) {
|
|
850
|
+
return entry.count ?? 1;
|
|
851
|
+
}
|
|
852
|
+
function countEvidenceEntries(entries, predicate = () => true) {
|
|
853
|
+
return entries.reduce((sum, entry) => sum + (predicate(entry) ? evidenceEntryCount(entry) : 0), 0);
|
|
854
|
+
}
|
|
855
|
+
function makeEvidenceCompleteness(collection, options) {
|
|
856
|
+
const sources = new Set(collection.scanned.keys());
|
|
857
|
+
for (const entry of collection.entries)
|
|
858
|
+
sources.add(entry.kind);
|
|
859
|
+
for (const entry of collection.skipped)
|
|
860
|
+
sources.add(entry.kind);
|
|
861
|
+
for (const entry of collection.reused)
|
|
862
|
+
sources.add(entry.kind);
|
|
863
|
+
for (const entry of collection.truncated)
|
|
864
|
+
sources.add(entry.kind);
|
|
865
|
+
for (const entry of collection.failed)
|
|
866
|
+
sources.add(entry.kind);
|
|
867
|
+
const sourceCounts = [...sources].sort().map((source) => {
|
|
868
|
+
const skipped = collection.skipped.filter((entry) => entry.kind === source);
|
|
869
|
+
return {
|
|
870
|
+
source,
|
|
871
|
+
scanned_count: collection.scanned.get(source) ?? 0,
|
|
872
|
+
included_count: collection.entries.filter((entry) => entry.kind === source).length,
|
|
873
|
+
skipped_count: countEvidenceEntries(skipped),
|
|
874
|
+
truncated_count: countEvidenceEntries(collection.truncated, (entry) => entry.kind === source),
|
|
875
|
+
deferred_count: countEvidenceEntries(skipped, (entry) => entry.reason.startsWith("deferred_")),
|
|
876
|
+
reused_count: collection.reused.filter((entry) => entry.kind === source).length,
|
|
877
|
+
failed_count: countEvidenceEntries(collection.failed, (entry) => entry.kind === source),
|
|
878
|
+
};
|
|
879
|
+
});
|
|
880
|
+
const totals = sourceCounts.reduce((sum, count) => ({
|
|
881
|
+
scanned_count: sum.scanned_count + count.scanned_count,
|
|
882
|
+
included_count: sum.included_count + count.included_count,
|
|
883
|
+
skipped_count: sum.skipped_count + count.skipped_count,
|
|
884
|
+
truncated_count: sum.truncated_count + count.truncated_count,
|
|
885
|
+
deferred_count: sum.deferred_count + count.deferred_count,
|
|
886
|
+
reused_count: sum.reused_count + count.reused_count,
|
|
887
|
+
failed_count: sum.failed_count + count.failed_count,
|
|
888
|
+
}), {
|
|
889
|
+
scanned_count: 0,
|
|
890
|
+
included_count: 0,
|
|
891
|
+
skipped_count: 0,
|
|
892
|
+
truncated_count: 0,
|
|
893
|
+
deferred_count: 0,
|
|
894
|
+
reused_count: 0,
|
|
895
|
+
failed_count: 0,
|
|
896
|
+
});
|
|
897
|
+
const skipReasonCounts = new Map();
|
|
898
|
+
for (const skipped of collection.skipped) {
|
|
899
|
+
const key = `${skipped.kind}:${skipped.reason}`;
|
|
900
|
+
const existing = skipReasonCounts.get(key);
|
|
901
|
+
if (existing) {
|
|
902
|
+
existing.count += evidenceEntryCount(skipped);
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
skipReasonCounts.set(key, {
|
|
906
|
+
source: skipped.kind,
|
|
907
|
+
reason: skipped.reason,
|
|
908
|
+
count: evidenceEntryCount(skipped),
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const failureReasonCounts = new Map();
|
|
913
|
+
for (const failed of collection.failed) {
|
|
914
|
+
const key = `${failed.kind}:${failed.reason}`;
|
|
915
|
+
const existing = failureReasonCounts.get(key);
|
|
916
|
+
if (existing) {
|
|
917
|
+
existing.count += evidenceEntryCount(failed);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
failureReasonCounts.set(key, {
|
|
921
|
+
source: failed.kind,
|
|
922
|
+
reason: failed.reason,
|
|
923
|
+
count: evidenceEntryCount(failed),
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const truncationCounts = new Map();
|
|
928
|
+
for (const truncated of collection.truncated) {
|
|
929
|
+
const key = `${truncated.kind}:${truncated.reason}`;
|
|
930
|
+
const existing = truncationCounts.get(key);
|
|
931
|
+
if (existing) {
|
|
932
|
+
existing.count += evidenceEntryCount(truncated);
|
|
933
|
+
existing.observed_bytes = Math.max(existing.observed_bytes ?? 0, truncated.observed_bytes ?? 0);
|
|
934
|
+
existing.included_bytes = Math.max(existing.included_bytes ?? 0, truncated.included_bytes ?? 0);
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
truncationCounts.set(key, {
|
|
938
|
+
source: truncated.kind,
|
|
939
|
+
reason: truncated.reason,
|
|
940
|
+
count: evidenceEntryCount(truncated),
|
|
941
|
+
...(truncated.max_bytes !== undefined
|
|
942
|
+
? { max_bytes: truncated.max_bytes }
|
|
943
|
+
: {}),
|
|
944
|
+
...(truncated.observed_bytes !== undefined
|
|
945
|
+
? { observed_bytes: truncated.observed_bytes }
|
|
946
|
+
: {}),
|
|
947
|
+
...(truncated.included_bytes !== undefined
|
|
948
|
+
? { included_bytes: truncated.included_bytes }
|
|
949
|
+
: {}),
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const hasGaps = totals.skipped_count > 0 ||
|
|
954
|
+
totals.truncated_count > 0 ||
|
|
955
|
+
totals.deferred_count > 0 ||
|
|
956
|
+
totals.failed_count > 0 ||
|
|
957
|
+
collection.caps.some((cap) => cap.applied);
|
|
958
|
+
const status = totals.failed_count > 0 &&
|
|
959
|
+
totals.included_count + totals.reused_count === 0
|
|
960
|
+
? "failed"
|
|
961
|
+
: totals.included_count + totals.reused_count === 0 && !hasGaps
|
|
962
|
+
? "empty"
|
|
963
|
+
: hasGaps
|
|
964
|
+
? "partial"
|
|
965
|
+
: "complete";
|
|
966
|
+
return EvidenceCompletenessPayloadSchema.parse({
|
|
967
|
+
schema_version: "evidence-completeness.v1",
|
|
968
|
+
status,
|
|
969
|
+
generated_at: options.finishedAt,
|
|
970
|
+
scan_window: {
|
|
971
|
+
started_at: options.startedAt,
|
|
972
|
+
finished_at: options.finishedAt,
|
|
973
|
+
since_minutes: options.sinceMinutes,
|
|
974
|
+
},
|
|
975
|
+
source_counts: sourceCounts,
|
|
976
|
+
totals,
|
|
977
|
+
caps: collection.caps,
|
|
978
|
+
skip_reasons: [...skipReasonCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
979
|
+
failure_reasons: [...failureReasonCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
980
|
+
truncation_markers: [...truncationCounts.values()].sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
|
|
981
|
+
notes: totals.failed_count > 0
|
|
982
|
+
? ["Evidence collection failed; downstream analysis should not infer confidence."]
|
|
983
|
+
: hasGaps
|
|
984
|
+
? ["Evidence is incomplete; downstream analysis should lower confidence."]
|
|
985
|
+
: [],
|
|
533
986
|
});
|
|
534
|
-
return stdout;
|
|
535
987
|
}
|
|
536
988
|
async function walkJsonlFiles(dir, cutoffMs) {
|
|
537
989
|
const out = [];
|