@bli-cockpit/cli 0.2.54 → 0.2.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +247 -0
  2. package/dist/adapters/attribution-core-paths.js +182 -0
  3. package/dist/adapters/attribution-core-score.js +159 -0
  4. package/dist/adapters/attribution-core-types.js +13 -0
  5. package/dist/adapters/attribution-core.js +13 -565
  6. package/dist/adapters/claude-attribution-discovery.js +186 -0
  7. package/dist/adapters/claude-attribution-score.js +204 -0
  8. package/dist/adapters/claude-attribution-signals.js +180 -0
  9. package/dist/adapters/claude-attribution-types.js +25 -0
  10. package/dist/adapters/claude-attribution.js +14 -569
  11. package/dist/commands/doctor-access.js +129 -0
  12. package/dist/commands/doctor-pipeline.js +326 -0
  13. package/dist/commands/doctor-registration.js +105 -0
  14. package/dist/commands/doctor-report.js +111 -0
  15. package/dist/commands/doctor-update.js +120 -0
  16. package/dist/commands/doctor.js +8 -753
  17. package/dist/commands/heartbeat.js +8 -0
  18. package/dist/commands/jarvis-contracts.js +8 -0
  19. package/dist/commands/jarvis-render.js +413 -0
  20. package/dist/commands/jarvis-turn.js +305 -0
  21. package/dist/commands/jarvis.js +23 -698
  22. package/dist/commands/local-args-collector-setup.js +250 -0
  23. package/dist/commands/local-args-collector-status.js +227 -0
  24. package/dist/commands/local-args-collector-work.js +175 -0
  25. package/dist/commands/local-args-collector.js +19 -624
  26. package/dist/commands/local-args-tower-admin.js +456 -0
  27. package/dist/commands/local-args-tower-chat.js +194 -0
  28. package/dist/commands/local-args-tower-pages.js +314 -0
  29. package/dist/commands/local-args-tower.js +13 -880
  30. package/dist/commands/local-help.js +10 -2
  31. package/dist/commands/onboard-completion.js +136 -0
  32. package/dist/commands/onboard-flows.js +165 -0
  33. package/dist/commands/onboard-setup.js +102 -0
  34. package/dist/commands/onboard.js +5 -392
  35. package/dist/commands/public-root.js +1 -1
  36. package/dist/commands/session-sync-counters.js +55 -0
  37. package/dist/commands/session-sync-health.js +8 -1
  38. package/dist/commands/session-sync-plan.js +47 -7
  39. package/dist/commands/session-sync-scan.js +4 -4
  40. package/dist/commands/session-sync.js +6 -0
  41. package/dist/commands/settings-render.js +27 -0
  42. package/dist/commands/sync-followups.js +5 -1
  43. package/dist/commands/sync.js +5 -1
  44. package/dist/commands/team-device-reasons.js +16 -0
  45. package/dist/commands/team.js +87 -7
  46. package/dist/evidence-upload-client.js +14 -763
  47. package/dist/evidence-upload-object.js +181 -0
  48. package/dist/evidence-upload-plan.js +233 -0
  49. package/dist/evidence-upload-terminal.js +309 -0
  50. package/dist/evidence-upload-transport.js +104 -0
  51. package/dist/spool/local-spool-io.js +122 -0
  52. package/dist/spool/local-spool-mutations.js +174 -0
  53. package/dist/spool/local-spool-parse.js +143 -0
  54. package/dist/spool/local-spool-types.js +22 -0
  55. package/dist/spool/local-spool.js +20 -426
  56. package/dist/upload-evidence-delivery-offer.js +144 -0
  57. package/dist/upload-evidence-delivery-reconcile.js +134 -0
  58. package/dist/upload-evidence-delivery-summary.js +205 -0
  59. package/dist/upload-evidence-delivery.js +12 -482
  60. package/package.json +3 -3
@@ -20,486 +20,16 @@
20
20
  * own terms. Confusing them is what kept a Mac in `retry_pending` through 13
21
21
  * identical syncs (BLI-2528), and what let a transcript sit undelivered for nine
22
22
  * days while the sync read clean (BLI-3066).
23
- */
24
- import { EvidenceCompletenessPayloadSchema, isPermanentUploadFailure, } from "@bli-cockpit/telemetry-core";
25
- import { clearDeliveryAttempt, DELIVERY_BACKOFF_BYPASS_REASON, DELIVERY_BACKOFF_HOLDING_REASON, deliveryBackoffApplies, deliveryHold, recordDeliveryFailure, summarizeStuckEvidence, writeRawEvidenceStagingState, } from "./raw-evidence-staging.js";
26
- // --------------------------------------------------------------------------
27
- // 1. What may we offer this sync?
28
- // --------------------------------------------------------------------------
29
- /**
30
- * Split the pack's files into "offer these now" and "still in backoff".
31
- *
32
- * A held file becomes an `upload_failed` outcome labelled
33
- * `delivery_backoff_holding`. That is deliberate rather than a quiet omission:
34
- * the pointer gets pruned from the envelope (the object is genuinely not
35
- * durable), the sync stays in `retry_pending`, and the reason travels to the
36
- * status output. A hold that read as success would be the green-status-hiding-
37
- * missing-collection failure the fleet contract forbids.
38
- */
39
- export function partitionHeldEvidenceFiles(files, staging, now, mode) {
40
- const deliverable = [];
41
- const held = [];
42
- const bypassed = [];
43
- const backoffApplies = deliveryBackoffApplies(mode);
44
- for (const file of files) {
45
- const hold = deliveryHold(staging, file.pointer.content_hash_sha256, now);
46
- if (hold && !backoffApplies) {
47
- // BLI-3118: a person asked for this one now. Offering it is the whole
48
- // point of the retry command Cockpit printed, and the bypass is logged
49
- // rather than assumed.
50
- bypassed.push(hold);
51
- deliverable.push(file);
52
- continue;
53
- }
54
- if (!hold) {
55
- deliverable.push(file);
56
- continue;
57
- }
58
- held.push({
59
- pointer: file.pointer,
60
- object_key: file.pointer.object_key ?? "",
61
- codex_session_id: file.codex_session_id ?? null,
62
- kind: file.kind ?? "raw_evidence",
63
- ...(file.artifact_metadata
64
- ? { artifact_metadata: file.artifact_metadata }
65
- : {}),
66
- upload_state: "upload_failed",
67
- reason: DELIVERY_BACKOFF_HOLDING_REASON,
68
- uploaded_chunk_count: 0,
69
- });
70
- }
71
- return { deliverable, held, bypassed };
72
- }
73
- /**
74
- * Say out loud that bytes were withheld on purpose.
75
23
  *
76
- * Counts and sizes only, never a path. Without this line an unattended machine
77
- * withholds evidence for hours and leaves no trace of having done so.
78
- */
79
- export function logEvidenceHeldByBackoff(held, attemptedAt) {
80
- if (held.length === 0)
81
- return;
82
- console.error("[cockpit-sync] raw evidence held by delivery backoff", JSON.stringify({
83
- attempted_at: attemptedAt,
84
- reason: DELIVERY_BACKOFF_HOLDING_REASON,
85
- object_count: held.length,
86
- byte_size: held.reduce((sum, outcome) => sum + (outcome.pointer.byte_size ?? 0), 0),
87
- }));
88
- }
89
- /**
90
- * Say out loud that an operator's retry ignored a live backoff window.
91
- *
92
- * The success branch of BLI-3118: without this line the only trace of the
93
- * decision is an object that was held on one run and offered on the next, and
94
- * nothing on the machine says which rule made the difference.
95
- */
96
- export function logEvidenceBackoffBypassed(bypassed, attemptedAt) {
97
- if (bypassed.length === 0)
98
- return;
99
- console.error("[cockpit-sync] raw evidence delivery backoff bypassed", JSON.stringify({
100
- attempted_at: attemptedAt,
101
- reason: DELIVERY_BACKOFF_BYPASS_REASON,
102
- object_count: bypassed.length,
103
- byte_size: bypassed.reduce((sum, entry) => sum + entry.byte_size, 0),
104
- max_attempts: bypassed.reduce((max, entry) => Math.max(max, entry.attempts), 0),
105
- last_reasons: [...new Set(bypassed.map((entry) => entry.last_reason))]
106
- .sort(),
107
- }));
108
- }
109
- // --------------------------------------------------------------------------
110
- // 2. What just happened to each object?
111
- // --------------------------------------------------------------------------
112
- /**
113
- * Count what just happened to each object, and when it may be offered again.
114
- *
115
- * Written immediately after the upload pass and before ingest, so an ingest
116
- * failure cannot lose the attempt counts — losing them resets every backoff to
117
- * zero and the fleet is back to 15-minute retries forever. A held outcome is
118
- * not itself an attempt: counting it would push its own next attempt further
119
- * out on every sync and eventually never retry at all.
120
- */
121
- export async function persistDeliveryAttempts(stateDir, staging, outcomes, attemptedAt) {
122
- let changed = false;
123
- for (const outcome of outcomes) {
124
- const contentHash = outcome.pointer.content_hash_sha256;
125
- if (!contentHash)
126
- continue;
127
- if (outcome.reason === DELIVERY_BACKOFF_HOLDING_REASON)
128
- continue;
129
- if (outcome.upload_state === "upload_failed") {
130
- const entry = recordDeliveryFailure(staging, contentHash, {
131
- reason: outcome.reason ?? "upload_failed",
132
- attemptedAt,
133
- byteSize: outcome.pointer.byte_size ?? 0,
134
- });
135
- changed = true;
136
- console.error("[cockpit-sync] raw evidence delivery failed", JSON.stringify({
137
- reason: entry.last_reason,
138
- kind: outcome.kind,
139
- attempts: entry.attempts,
140
- first_failed_at: entry.first_failed_at,
141
- next_attempt_at: entry.next_attempt_at,
142
- byte_size: entry.byte_size,
143
- }));
144
- continue;
145
- }
146
- if (clearDeliveryAttempt(staging, contentHash)) {
147
- changed = true;
148
- console.error("[cockpit-sync] raw evidence delivery recovered", JSON.stringify({
149
- reason: "delivery_recovered",
150
- kind: outcome.kind,
151
- upload_state: outcome.upload_state,
152
- }));
153
- }
154
- }
155
- if (!changed)
156
- return;
157
- staging.updated_at = attemptedAt.toISOString();
158
- await writeRawEvidenceStagingState(stateDir, staging).catch((error) => {
159
- console.error("[cockpit-sync] delivery attempt state write failed", JSON.stringify({
160
- reason: "staging_state_write_failed",
161
- detail: error instanceof Error ? error.name : typeof error,
162
- tracked_count: Object.keys(staging.delivery_attempts).length,
163
- }));
164
- });
165
- }
166
- // --------------------------------------------------------------------------
167
- // 3. What do we still owe the operator?
168
- // --------------------------------------------------------------------------
169
- /**
170
- * Which adapters a queued retry has to re-run.
171
- *
172
- * Anything this sync touched or tried to touch counts, including a directory it
173
- * could not read — a source that failed to scan is exactly the one a retry must
174
- * come back to.
175
- */
176
- export function retrySourcesForFailedSync(options, facts) {
177
- const sources = new Set();
178
- if ((options.codexSessionFiles?.length ?? 0) > 0 ||
179
- (options.codexAttributionScan?.directory_read_failed_count ?? 0) > 0 ||
180
- (options.codexAttributionScan?.stat_failed_count ?? 0) > 0 ||
181
- facts?.evidence_completeness.source_counts.some((count) => count.source.startsWith("codex_") &&
182
- count.scanned_count + count.included_count + count.reused_count > 0)) {
183
- sources.add("codex");
184
- }
185
- if ((options.claudeSessionFiles?.length ?? 0) > 0 ||
186
- (options.claudeAttributionScan?.project_dir_read_failed_count ?? 0) > 0 ||
187
- (options.claudeAttributionScan?.session_stat_failed_count ?? 0) > 0 ||
188
- (options.claudeAttributionScan?.sidecar_dir_read_failed_count ?? 0) > 0 ||
189
- (options.claudeAttributionScan?.sidecar_stat_failed_count ?? 0) > 0 ||
190
- facts?.evidence_completeness.source_counts.some((count) => count.source.startsWith("claude_") &&
191
- count.scanned_count + count.included_count + count.reused_count > 0)) {
192
- sources.add("claude_code");
193
- }
194
- return [...sources];
195
- }
196
- /**
197
- * The failed uploads a later attempt could still rescue.
198
- *
199
- * An object storage has already refused on its own terms is not one of them,
200
- * and counting it as one is what kept Edward's Mac in `retry_pending` through
201
- * 13 consecutive syncs that were never going to end differently (BLI-2528).
202
- */
203
- function retryableFailedOutcomes(outcomes) {
204
- return outcomes.filter((outcome) => outcome.upload_state === "upload_failed" &&
205
- !isPermanentUploadFailure(outcome.reason));
206
- }
207
- /** Failed uploads that no retry can rescue, kept so they can still be named. */
208
- function permanentFailedOutcomes(outcomes) {
209
- return outcomes.filter((outcome) => outcome.upload_state === "upload_failed" &&
210
- isPermanentUploadFailure(outcome.reason));
211
- }
212
- /**
213
- * The reason to show for objects that failed for good.
214
- *
215
- * Returns null when there are none. These never queue a retry, but they must
216
- * never disappear either: a machine with a permanently rejected object has
217
- * missing collection, and a status that reads clean would hide it.
218
- */
219
- export function permanentEvidenceFailureReason(outcomes) {
220
- const reasons = new Set(permanentFailedOutcomes(outcomes).map((outcome) => outcome.reason ?? "unknown"));
221
- if (reasons.size === 0)
222
- return null;
223
- return `raw_evidence_permanently_rejected:${[...reasons].sort().join(",")}`;
224
- }
225
- /**
226
- * Leave a dated record of the objects this machine gave up on.
227
- *
228
- * stderr, which launchd captures to `sync.err.log`, so an unattended machine
229
- * still says what it lost. Reason labels and counts only — never a path or a
230
- * byte of content.
231
- */
232
- export function logPermanentlyRejectedEvidence(options) {
233
- console.error("[cockpit-sync] raw evidence permanently rejected", JSON.stringify({
234
- attempted_at: options.attemptedAt,
235
- reason: options.reason,
236
- object_count: permanentFailedOutcomes(options.outcomes).length,
237
- }));
238
- }
239
- /**
240
- * Is there anything a later sync could still turn into evidence?
241
- *
242
- * Any one of these is enough: an upload that failed for a rescuable reason,
243
- * evidence deferred by this sync's byte or object budget, a completeness
244
- * payload that reports failure, or a skip a retry could undo.
245
- */
246
- export function hasRetryableEvidenceGap(facts, outcomes) {
247
- if (retryableFailedOutcomes(outcomes).length > 0) {
248
- return true;
249
- }
250
- if (!facts)
251
- return false;
252
- if (facts.deferred_byte_budget_count > 0 ||
253
- facts.deferred_object_budget_count > 0) {
254
- return true;
255
- }
256
- if (facts.evidence_completeness.status === "failed" ||
257
- facts.evidence_completeness.totals.failed_count > 0 ||
258
- facts.evidence_completeness.failure_reasons.length > 0) {
259
- return true;
260
- }
261
- return facts.evidence_completeness.skip_reasons.some(({ reason }) => isRetryableEvidenceSkipReason(reason));
262
- }
263
- /**
264
- * Skip reasons a later sync can still turn into evidence.
265
- *
266
- * `delivery_backoff_holding` is one of them, and it has to be: a source held by
267
- * backoff is missing collection right now. If it did not land here the sync
268
- * would read clean while a transcript sat undelivered for nine days, which is
269
- * the exact shape of BLI-3066.
270
- */
271
- function isRetryableEvidenceSkipReason(reason) {
272
- if (reason === DELIVERY_BACKOFF_HOLDING_REASON)
273
- return true;
274
- return /(?:read|stat|directory)_failed|session_limit_overflow/iu.test(reason);
275
- }
276
- /** Every distinct reason behind the gap, sorted, as one spool-ready label. */
277
- export function retryableEvidenceGapReason(facts, outcomes) {
278
- const reasons = new Set();
279
- for (const outcome of retryableFailedOutcomes(outcomes)) {
280
- reasons.add(outcome.reason ?? "upload_failed");
281
- }
282
- if (facts) {
283
- if (facts.deferred_byte_budget_count > 0)
284
- reasons.add("deferred_byte_budget");
285
- if (facts.deferred_object_budget_count > 0) {
286
- reasons.add("deferred_object_budget");
287
- }
288
- for (const { reason } of facts.evidence_completeness.failure_reasons) {
289
- reasons.add(reason);
290
- }
291
- if (facts.evidence_completeness.status === "failed" &&
292
- facts.evidence_completeness.failure_reasons.length === 0) {
293
- reasons.add("evidence_completeness_failed");
294
- }
295
- for (const { reason } of facts.evidence_completeness.skip_reasons) {
296
- if (isRetryableEvidenceSkipReason(reason)) {
297
- reasons.add(reason);
298
- }
299
- }
300
- }
301
- return `partial_raw_evidence_retry_required:${[
302
- ...reasons,
303
- ].sort().join(",") || "unknown"}`;
304
- }
305
- /**
306
- * The counts `cockpit sync --json` and `cockpit status` read.
307
- *
308
- * Two kinds of reuse are added together on purpose: files this machine skipped
309
- * because its own cursor already had the bytes, and files the server recognised
310
- * from their content hash.
311
- */
312
- export function summarizeRawEvidenceDelivery(built, outcomes, uploadedChunkCount, cursor, staging, now) {
313
- const stuck = summarizeStuckEvidence(staging, now);
314
- const cursorReused = built.raw_evidence_facts?.reused ?? [];
315
- const serverReusedCount = outcomes.filter((outcome) => outcome.upload_state === "reused_existing").length;
316
- const failed = outcomes.filter((outcome) => outcome.upload_state === "upload_failed");
317
- const retryRequired = hasRetryableEvidenceGap(built.raw_evidence_facts, outcomes);
318
- const retryReason = retryRequired
319
- ? retryableEvidenceGapReason(built.raw_evidence_facts, outcomes)
320
- : null;
321
- const cursorReusedOutcomes = cursorReused.map((entry) => ({
322
- object_key: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
323
- raw_evidence_pointer_id: cursor.objects[entry.content_hash_sha256]?.object_key ?? "",
324
- kind: entry.kind,
325
- codex_session_id: entry.codex_session_id,
326
- ...(entry.artifact_metadata
327
- ? { artifact_metadata: entry.artifact_metadata }
328
- : {}),
329
- upload_state: "reused_existing",
330
- reason: "cursor_content_match",
331
- }));
332
- return {
333
- raw_evidence_file_count: built.raw_evidence_upload_files.length,
334
- raw_evidence_uploaded_object_count: outcomes.filter((outcome) => outcome.upload_state === "uploaded").length,
335
- raw_evidence_uploaded_chunk_count: uploadedChunkCount,
336
- raw_evidence_reused_count: cursorReused.length + serverReusedCount,
337
- raw_evidence_failed_count: failed.length,
338
- raw_evidence_sanitized_count: built.raw_evidence_facts?.sanitized_count ?? 0,
339
- raw_evidence_failure_reasons: [
340
- ...new Set(failed.map((outcome) => outcome.reason ?? "unknown")),
341
- ],
342
- raw_evidence_retry_required: retryRequired,
343
- raw_evidence_retry_reasons: retryReason
344
- ? retryReason
345
- .replace(/^partial_raw_evidence_retry_required:/u, "")
346
- .split(",")
347
- .filter(Boolean)
348
- : [],
349
- raw_evidence_outcomes: [
350
- ...outcomes.map((outcome) => ({
351
- object_key: outcome.object_key,
352
- raw_evidence_pointer_id: outcome.pointer.raw_evidence_pointer_id,
353
- kind: outcome.kind,
354
- codex_session_id: outcome.codex_session_id,
355
- ...(outcome.artifact_metadata
356
- ? { artifact_metadata: outcome.artifact_metadata }
357
- : {}),
358
- upload_state: outcome.upload_state,
359
- reason: outcome.reason,
360
- })),
361
- ...cursorReusedOutcomes,
362
- ],
363
- raw_evidence_deferred_byte_budget: built.raw_evidence_facts?.deferred_byte_budget_count ?? 0,
364
- raw_evidence_deferred_object_budget: built.raw_evidence_facts?.deferred_object_budget_count ?? 0,
365
- cursor_tracked_object_count: Object.keys(cursor.objects).length,
366
- raw_evidence_delivery_held_count: outcomes.filter((outcome) => outcome.reason === DELIVERY_BACKOFF_HOLDING_REASON).length + (built.raw_evidence_facts?.delivery_held_count ?? 0),
367
- raw_evidence_stuck_object_count: stuck.stuck_object_count,
368
- raw_evidence_max_delivery_attempts: stuck.max_attempts,
369
- raw_evidence_oldest_delivery_failure_at: stuck.oldest_first_failed_at,
370
- };
371
- }
372
- // --------------------------------------------------------------------------
373
- // 4. Reconciling the envelope with what actually became durable
374
- // --------------------------------------------------------------------------
375
- /**
376
- * Ingest refuses pointers whose objects never became durable, so failed
377
- * uploads are pruned from the envelope instead of failing the whole sync.
378
- * Successful upload responses can also carry server-side sanitized hash and
379
- * redaction metadata; apply those before ingest so refs describe the bytes
380
- * actually stored in the durable bucket.
381
- */
382
- export function applyRawEvidenceUploadOutcomes(envelope, outcomes) {
383
- // Content-addressed keys mean one pointer id can carry several outcomes
384
- // (byte-identical files); the pointer is durable if ANY outcome succeeded.
385
- const durablePointers = new Map();
386
- for (const outcome of outcomes) {
387
- if (outcome.upload_state === "upload_failed")
388
- continue;
389
- durablePointers.set(outcome.pointer.raw_evidence_pointer_id, outcome.pointer);
390
- }
391
- const durablePointerIds = new Set([...durablePointers.keys()]);
392
- const failedPointerIds = new Set(outcomes
393
- .filter((outcome) => outcome.upload_state === "upload_failed" &&
394
- !durablePointerIds.has(outcome.pointer.raw_evidence_pointer_id))
395
- .map((outcome) => outcome.pointer.raw_evidence_pointer_id));
396
- const failedOutcomes = outcomes.filter((outcome) => failedPointerIds.has(outcome.pointer.raw_evidence_pointer_id));
397
- if (failedPointerIds.size === 0 && durablePointers.size === 0)
398
- return envelope;
399
- return {
400
- ...envelope,
401
- events: envelope.events.map((event) => ({
402
- ...event,
403
- metrics: failedPointerIds.size > 0
404
- ? {
405
- ...event.metrics,
406
- evidence_failed_count: (event.metrics["evidence_failed_count"] ?? 0) +
407
- failedOutcomes.length,
408
- }
409
- : event.metrics,
410
- attributes: failedPointerIds.size > 0 && event.evidence_completeness
411
- ? {
412
- ...event.attributes,
413
- evidence_completeness_schema_version: event.evidence_completeness.schema_version,
414
- evidence_completeness_status: "partial",
415
- evidence_incomplete: true,
416
- }
417
- : event.attributes,
418
- evidence_completeness: failedPointerIds.size > 0 && event.evidence_completeness
419
- ? markCompletenessUploadFailures(event.evidence_completeness, failedOutcomes)
420
- : event.evidence_completeness,
421
- raw_evidence_pointers: event.raw_evidence_pointers.flatMap((pointer) => {
422
- const pointerId = pointer.raw_evidence_pointer_id;
423
- if (failedPointerIds.has(pointerId))
424
- return [];
425
- return [durablePointers.get(pointerId) ?? pointer];
426
- }),
427
- redaction: failedPointerIds.size > 0
428
- ? {
429
- ...event.redaction,
430
- raw_evidence_pointer_ids: event.redaction.raw_evidence_pointer_ids.filter((pointerId) => !failedPointerIds.has(pointerId)),
431
- }
432
- : event.redaction,
433
- })),
434
- };
435
- }
436
- /**
437
- * Fold upload failures back into the completeness payload the event carries, so
438
- * a downstream reader sees `partial` and the per-source failure counts rather
439
- * than a complete-looking payload with fewer pointers than it claims.
440
- */
441
- function markCompletenessUploadFailures(completeness, failedOutcomes) {
442
- const failureCounts = new Map();
443
- for (const outcome of failedOutcomes) {
444
- const source = outcome.kind ?? "raw_evidence";
445
- failureCounts.set(source, (failureCounts.get(source) ?? 0) + 1);
446
- }
447
- const totalFailures = [...failureCounts.values()].reduce((sum, count) => sum + count, 0);
448
- const sourceCounts = [...completeness.source_counts];
449
- for (const [source, count] of failureCounts) {
450
- const existingIndex = sourceCounts.findIndex((entry) => entry.source === source);
451
- if (existingIndex === -1) {
452
- sourceCounts.push({
453
- source,
454
- scanned_count: 0,
455
- included_count: 0,
456
- skipped_count: 0,
457
- truncated_count: 0,
458
- deferred_count: 0,
459
- reused_count: 0,
460
- failed_count: count,
461
- });
462
- continue;
463
- }
464
- const existing = sourceCounts[existingIndex];
465
- if (!existing)
466
- continue;
467
- sourceCounts[existingIndex] = {
468
- ...existing,
469
- failed_count: existing.failed_count + count,
470
- };
471
- }
472
- const failureReasons = [...completeness.failure_reasons];
473
- for (const [source, count] of failureCounts) {
474
- const reason = "upload_failed";
475
- const existingIndex = failureReasons.findIndex((entry) => entry.source === source && entry.reason === reason);
476
- if (existingIndex === -1) {
477
- failureReasons.push({ source, reason, count });
478
- }
479
- else {
480
- const existing = failureReasons[existingIndex];
481
- if (existing) {
482
- failureReasons[existingIndex] = {
483
- ...existing,
484
- count: existing.count + count,
485
- };
486
- }
487
- }
488
- }
489
- return EvidenceCompletenessPayloadSchema.parse({
490
- ...completeness,
491
- status: "partial",
492
- source_counts: sourceCounts,
493
- totals: {
494
- ...completeness.totals,
495
- failed_count: completeness.totals.failed_count + totalFailures,
496
- },
497
- failure_reasons: failureReasons.sort((a, b) => `${a.source}:${a.reason}`.localeCompare(`${b.source}:${b.reason}`)),
498
- notes: [
499
- ...new Set([
500
- ...completeness.notes,
501
- "Some collected evidence did not become durable; downstream analysis should lower confidence.",
502
- ]),
503
- ],
504
- });
505
- }
24
+ * This file is the table of contents; every responsibility above is a
25
+ * `upload-evidence-delivery-*.ts` sibling and is re-exported here so the
26
+ * module's public surface never moves (BLI-3637):
27
+ * -offer questions 1 and 2 — what may we offer, what just happened
28
+ * -summary question 3 retry sources, retryable/permanent reasons, and
29
+ * the counts `cockpit status` reads
30
+ * -reconcile question 4 folding upload outcomes back into the envelope
31
+ * ingest actually sees
32
+ */
33
+ export { logEvidenceBackoffBypassed, logEvidenceHeldByBackoff, partitionHeldEvidenceFiles, persistDeliveryAttempts, } from "./upload-evidence-delivery-offer.js";
34
+ export { hasRetryableEvidenceGap, logPermanentlyRejectedEvidence, permanentEvidenceFailureReason, retrySourcesForFailedSync, retryableEvidenceGapReason, summarizeRawEvidenceDelivery, } from "./upload-evidence-delivery-summary.js";
35
+ export { applyRawEvidenceUploadOutcomes } from "./upload-evidence-delivery-reconcile.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.54",
3
+ "version": "0.2.56",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.2",
31
- "@bli-cockpit/telemetry-core": "0.1.27"
30
+ "@bli-cockpit/memory-mcp": "0.1.4",
31
+ "@bli-cockpit/telemetry-core": "0.1.28"
32
32
  }
33
33
  }