crumbtrail-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2798 @@
1
+ import http from 'node:http';
2
+ import { Symptom, EvidenceSourceDescriptor, EvidenceQuery, EvidenceSourceResult, DbEngine, TargetDescriptor, BugEvent, EvidenceItem, IntentSignal, EvidenceGap, EvidenceJoinKey, GitHostClient, DbDiffOp } from 'crumbtrail-core';
3
+ export { EvidenceJoinKey, EvidenceSourceDescriptor } from 'crumbtrail-core';
4
+
5
+ type TicketProvider = "jira" | "zendesk" | "trello";
6
+ declare function jiraToSymptom(payload: unknown): Symptom;
7
+
8
+ interface TicketConnector {
9
+ fetchSymptom(id: string): Promise<Symptom>;
10
+ }
11
+ /**
12
+ * Extension of TicketConnector for providers that can write an advisory
13
+ * comment back to the ticket. Declared as a separate interface (not an
14
+ * optional method on TicketConnector) so read-only connectors stay honest and
15
+ * callers that need commenting — the cloud webhook writer — type against the
16
+ * capability explicitly (contract decision #10).
17
+ */
18
+ interface CommentingTicketConnector extends TicketConnector {
19
+ postComment(ticketKey: string, adfBody: unknown, retry?: BoundedRetryOptions): Promise<void>;
20
+ }
21
+ /** Thrown when a ticket provider REST call fails, or required env config is missing (status 0). */
22
+ declare class TicketError extends Error {
23
+ status: number;
24
+ constructor(status: number, message: string);
25
+ }
26
+ /**
27
+ * Source-identification User-Agent sent on EVERY outbound integration request.
28
+ * Atlassian's third-party app guidance requires apps to "identify their source
29
+ * transparently"; a stable, descriptive UA is how a Jira/Confluence admin (and
30
+ * Atlassian's own abuse tooling) can attribute traffic to Crumbtrail. Applied to
31
+ * Zendesk/Trello too for consistency. Kept version-light on purpose so it does
32
+ * not churn every release — it identifies the source, not an exact build.
33
+ */
34
+ declare const CRUMBTRAIL_USER_AGENT = "Crumbtrail/0.1 (+https://crumbtrail.dev; integrations@crumbtrail.dev)";
35
+ /** Options for {@link withBoundedRetry}. All fields optional; production callers
36
+ * rely on the defaults, tests inject `baseDelayMs: 0`/a fake `sleep` to run fast. */
37
+ interface BoundedRetryOptions {
38
+ /** Total attempts (not extra retries). Default 3. */
39
+ attempts?: number;
40
+ /** Linear backoff base; the wait before attempt N is `baseDelayMs * (N-1)`. */
41
+ baseDelayMs?: number;
42
+ /** Decide whether a thrown error is worth retrying. Default: transient only. */
43
+ isRetryable?: (error: unknown) => boolean;
44
+ /** Sleep hook (overridable in tests). Default: real setTimeout. */
45
+ sleep?: (ms: number) => Promise<void>;
46
+ }
47
+ /**
48
+ * Run `fn` with a small bounded retry (default 3 attempts, short linear
49
+ * backoff). Re-throws the last error once attempts are exhausted or the error is
50
+ * non-retryable. Deliberately tiny — no queue, no jitter; comment posting is the
51
+ * one caller and a v1 best-effort write.
52
+ */
53
+ declare function withBoundedRetry<T>(fn: () => Promise<T>, options?: BoundedRetryOptions): Promise<T>;
54
+ /** HTTP Basic auth: Atlassian API token bound to an account email
55
+ * (`email:token`, base64). This is the self-host / API-token path. */
56
+ interface JiraBasicAuth {
57
+ type: "basic";
58
+ email: string;
59
+ apiToken: string;
60
+ }
61
+ /** OAuth 3LO bearer auth: a short-lived access token. Per Atlassian, OAuth API
62
+ * calls go to `https://api.atlassian.com/ex/jira/{cloudId}` (NOT the
63
+ * `*.atlassian.net` site base) with `Authorization: Bearer <accessToken>`; the
64
+ * caller sets `baseUrl` accordingly. The client never refreshes — the cloud
65
+ * connector boundary hands in an already-valid access token. */
66
+ interface JiraBearerAuth {
67
+ type: "bearer";
68
+ accessToken: string;
69
+ }
70
+ type JiraAuth = JiraBasicAuth | JiraBearerAuth;
71
+ /** Legacy positional shape — kept byte-compatible so existing callers
72
+ * (ticketClientFromEnv, self-host) construct exactly as before. */
73
+ interface JiraTicketClientConfigLegacy {
74
+ baseUrl: string;
75
+ email: string;
76
+ apiToken: string;
77
+ fetchImpl?: typeof fetch;
78
+ }
79
+ /** Discriminated-auth shape (basic OR bearer) with an explicit baseUrl. */
80
+ interface JiraTicketClientConfigWithAuth {
81
+ baseUrl: string;
82
+ auth: JiraAuth;
83
+ fetchImpl?: typeof fetch;
84
+ }
85
+ type JiraTicketClientConfig = JiraTicketClientConfigLegacy | JiraTicketClientConfigWithAuth;
86
+ declare class JiraTicketClient implements CommentingTicketConnector {
87
+ private baseUrl;
88
+ private auth;
89
+ private fetchImpl;
90
+ constructor(config: JiraTicketClientConfig);
91
+ /** The Authorization header value for the configured auth mode. The secret
92
+ * (token/access token) only ever lives inside this header string — it is
93
+ * never surfaced in an error message (only sanitized URLs are). */
94
+ private authHeader;
95
+ fetchSymptom(id: string): Promise<Symptom>;
96
+ /**
97
+ * POST an advisory comment to a Jira issue. `adfBody` is an Atlassian Document
98
+ * Format doc (see buildAdvisoryComment); it is sent as `{ body: <adf> }`.
99
+ * Wrapped in a small bounded retry so a transient 5xx/429/network blip does not
100
+ * drop the comment. Throws TicketError on a non-2xx that survives the retries —
101
+ * the API token never appears in the message (only the sanitized URL does).
102
+ */
103
+ postComment(issueIdOrKey: string, adfBody: unknown, retry?: BoundedRetryOptions): Promise<void>;
104
+ }
105
+
106
+ /**
107
+ * Evidence-source framework (runtime half). Mirrors the ticket-connector
108
+ * pattern in `ticket/clients.ts`: env-var credentials for self-host, a provider
109
+ * is present ⇔ its required vars are all set, and every outbound request carries
110
+ * {@link CRUMBTRAIL_USER_AGENT}. Adapters (CP3+) register here; this file is the
111
+ * contract + presence logic only.
112
+ */
113
+ /**
114
+ * Health of one configured source. Emitted by `EvidenceSource.health()` (a
115
+ * cheap authenticated no-op) and surfaced by `crumbtrail-server doctor`.
116
+ */
117
+ interface SourceHealth {
118
+ ok: boolean;
119
+ provider: string;
120
+ /** ms epoch when the check ran. */
121
+ checkedAt: number;
122
+ /** Sanitized failure reason when `ok` is false (never a raw secret). */
123
+ error?: string;
124
+ }
125
+ /**
126
+ * One provider's runtime surface. `fetchEvidence` returns neutral evidence.v1
127
+ * items (no ranking); it never gates and — through the framework — degrades to
128
+ * a gap rather than throwing. The optional `signal` lets the framework's
129
+ * per-source timeout abort an in-flight outbound fetch; adapters should thread
130
+ * it into their `fetch` calls when present.
131
+ */
132
+ interface EvidenceSource {
133
+ readonly descriptor: EvidenceSourceDescriptor;
134
+ health(signal?: AbortSignal): Promise<SourceHealth>;
135
+ fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
136
+ }
137
+ /**
138
+ * A registered provider: how to detect its env credentials and construct a
139
+ * source from them. Adapters append an entry to {@link EVIDENCE_SOURCE_PROVIDERS}.
140
+ */
141
+ interface EvidenceSourceProvider {
142
+ provider: string;
143
+ /**
144
+ * Required env-var names. The provider is considered configured iff every one
145
+ * is set to a non-empty string — mirroring `ticketClientFromEnv`'s
146
+ * present ⇔ required-vars-set rule.
147
+ */
148
+ authFields: string[];
149
+ fromEnv(env: Record<string, string | undefined>): EvidenceSource;
150
+ }
151
+ /**
152
+ * Provider registry. Adapters (Sentry in CP3, then CloudWatch/Splunk/…) append
153
+ * their {@link EvidenceSourceProvider} here via {@link registerEvidenceProvider}.
154
+ * `evidenceSourcesFromEnv` walks it to build the configured sources.
155
+ */
156
+ declare const EVIDENCE_SOURCE_PROVIDERS: EvidenceSourceProvider[];
157
+ /**
158
+ * Register a provider once (idempotent by `provider` id). Adapter modules call
159
+ * this at import time; `../evidence-sources/index.ts` imports every adapter so a
160
+ * single import of the barrel populates the registry. Idempotency keeps repeated
161
+ * imports (test isolation, HMR) from double-registering.
162
+ */
163
+ declare function registerEvidenceProvider(provider: EvidenceSourceProvider): void;
164
+ /**
165
+ * Build the configured evidence sources from env. A provider is included iff all
166
+ * its `authFields` are set. Never throws for a partially-configured provider —
167
+ * it is simply omitted (adapter absence degrades to a gap downstream, never an
168
+ * error). `providers` is injectable for tests.
169
+ */
170
+ declare function evidenceSourcesFromEnv(env?: Record<string, string | undefined>, providers?: EvidenceSourceProvider[]): EvidenceSource[];
171
+ /**
172
+ * Base headers every adapter's outbound request must include. Exposes the
173
+ * shared source-identifying User-Agent so adapters do not re-declare it.
174
+ */
175
+ declare function evidenceRequestHeaders(extra?: Record<string, string>): Record<string, string>;
176
+
177
+ type AudioTranscriptionState = "not-requested" | "transcription-ready" | "transcription-unavailable" | "transcription-error";
178
+ interface PostProcessAudioSummary {
179
+ artifact: "audio.webm";
180
+ bytes: number;
181
+ upload?: {
182
+ metadataFile: "audio.json";
183
+ uploadedAt?: number;
184
+ contentType?: string;
185
+ mimeType?: string;
186
+ durationMs?: number;
187
+ chunkCount?: number;
188
+ transcriptionRequested?: boolean;
189
+ };
190
+ transcription: {
191
+ state: AudioTranscriptionState;
192
+ code?: "transcription_unavailable" | "transcription_failed";
193
+ message?: string;
194
+ transcriptFile?: "transcript.json";
195
+ eventCount?: number;
196
+ };
197
+ }
198
+
199
+ type Severity = 'critical' | 'high' | 'medium' | 'low';
200
+ interface SessionSummary {
201
+ id: string;
202
+ release?: string;
203
+ build?: string;
204
+ start: number;
205
+ end?: number;
206
+ dur?: number;
207
+ evts?: number;
208
+ errors: number;
209
+ failedReqs: number;
210
+ topSeverity?: Severity;
211
+ title?: string;
212
+ hasVideo: boolean;
213
+ hasDiagnosis: boolean;
214
+ }
215
+ interface SessionFileFlags {
216
+ hasVideo: boolean;
217
+ hasDiagnosis: boolean;
218
+ topSeverity?: Severity;
219
+ }
220
+ declare function buildSessionSummary(metaJson: unknown, indexJson: unknown, fileFlags: SessionFileFlags): SessionSummary;
221
+
222
+ interface SessionManagerConfig {
223
+ outputDir: string;
224
+ whisperModel?: string;
225
+ postProcess?: (sessionDir: string, whisperModel?: string) => Promise<void>;
226
+ }
227
+ interface SessionListItem {
228
+ id: string;
229
+ release?: string;
230
+ build?: string;
231
+ start: number;
232
+ end?: number;
233
+ }
234
+ interface SessionFinalizationResult {
235
+ ok: true;
236
+ sessionId: string;
237
+ processed: boolean;
238
+ degraded: boolean;
239
+ finalizedAt: number;
240
+ postProcess: {
241
+ ok: boolean;
242
+ error?: string;
243
+ audio?: PostProcessAudioSummary;
244
+ warnings?: Array<{
245
+ capability: "audio" | "video";
246
+ code: string;
247
+ message: string;
248
+ }>;
249
+ };
250
+ }
251
+ declare class SessionManager {
252
+ private outputDir;
253
+ private whisperModel?;
254
+ private postProcess;
255
+ constructor(config: string | SessionManagerConfig);
256
+ create(sessionId: string, metadata: Record<string, unknown>): void;
257
+ getSessionDir(sessionId: string): string;
258
+ getExistingSessionDir(sessionId: string): string | undefined;
259
+ private validateSessionId;
260
+ private resolveInsideOutput;
261
+ private resolveStagingSessionDir;
262
+ private assertSafeSessionDir;
263
+ private findExistingSessionDir;
264
+ private moveSessionToV2Partition;
265
+ finalize(sessionId: string, options?: {
266
+ refinalize?: boolean;
267
+ }): Promise<SessionFinalizationResult>;
268
+ private eachSessionDir;
269
+ list(filters?: {
270
+ app?: string;
271
+ after?: number;
272
+ before?: number;
273
+ release?: string;
274
+ build?: string;
275
+ }): SessionListItem[];
276
+ listSummaries(): SessionSummary[];
277
+ private isSessionDir;
278
+ private ensurePartitionParent;
279
+ private assertSafeDirectoryInsideOutput;
280
+ private writeSessionArtifact;
281
+ private assertNoSymlinkedGeneratedArtifacts;
282
+ }
283
+
284
+ interface AiDiagnosisConfig {
285
+ enabled: boolean;
286
+ apiKey?: string;
287
+ model?: string;
288
+ allowAutoModel?: boolean;
289
+ maxWindows?: number;
290
+ maxPromptBytes?: number;
291
+ fetchImpl?: typeof fetch;
292
+ log?: (message: string) => void;
293
+ /** Diagnose finalized sessions already on disk when the server starts. */
294
+ backfillOnStart?: boolean;
295
+ /** Bounded provider concurrency for startup backfill. Default 2. */
296
+ backfillConcurrency?: number;
297
+ }
298
+
299
+ interface ServerConfig {
300
+ port: number;
301
+ outputDir: string;
302
+ whisperModel?: string;
303
+ /** Override the session finalization post-processing step (e.g. to skip whisper transcription in tests). */
304
+ postProcess?: SessionManagerConfig["postProcess"];
305
+ staticDir?: string;
306
+ authToken?: string;
307
+ allowedOrigins?: string[];
308
+ maxJsonBodyBytes?: number;
309
+ maxEventBatchSize?: number;
310
+ maxSessionEventBytes?: number;
311
+ maxBlobBytes?: number;
312
+ allowRemoteApi?: boolean;
313
+ ai?: AiDiagnosisConfig;
314
+ otlpAutoSessionWindowMs?: number;
315
+ maxOtlpTraceSessionCache?: number;
316
+ /**
317
+ * Idle-session sweep: server-side finalization for sessions that never call
318
+ * POST /api/session/end (autoCapture backends, OTLP auto-sessions). Enabled
319
+ * by default — without it those sessions never produce index.json and every
320
+ * dashboard detail read 404s. Set `enabled: false` to opt out (tests).
321
+ */
322
+ sessionSweep?: {
323
+ enabled?: boolean;
324
+ idleMs?: number;
325
+ /** Max age before a never-idle (still-active) session is checkpoint-finalized. */
326
+ checkpointMs?: number;
327
+ intervalMs?: number;
328
+ maxPerSweep?: number;
329
+ };
330
+ /**
331
+ * Severity-triggered fast finalize: high-severity ingest events (uncaught
332
+ * errors/rejections, 5xx, backend errors, OTel ERROR spans/logs) schedule a
333
+ * debounced checkpoint-finalize for their session, cutting worst-case
334
+ * detection latency from the idle-sweep cadence (~40 min) to roughly the
335
+ * debounce window. Enabled by default; set `enabled: false` to opt out
336
+ * (tests). Scheduler state is in-memory only — a fast finalize lost to a
337
+ * restart falls back to the normal idle sweep.
338
+ * Defaults: debounceMs 45_000, maxConcurrent 2, cooldownMs 300_000.
339
+ */
340
+ fastFinalize?: {
341
+ enabled?: boolean;
342
+ /** Delay from the first severe event to the finalize attempt. Default 45_000. */
343
+ debounceMs?: number;
344
+ /** Global cap on concurrently running fast finalizes. Default 2. */
345
+ maxConcurrent?: number;
346
+ /** Minimum spacing between finalize attempts per session. Default 300_000. */
347
+ cooldownMs?: number;
348
+ };
349
+ /**
350
+ * Fired after every successful session finalization (including
351
+ * re-finalizations), from all three finalize paths: POST /api/session/end,
352
+ * the idle sweeper, and the severity-triggered fast finalizer. Embedders
353
+ * (e.g. crumbtrail-cloud) use it to learn about finalization in-process
354
+ * instead of polling. Must be fast and non-blocking — it runs on the
355
+ * finalize path. A throwing hook is swallowed and logged; it never fails
356
+ * the finalize or suppresses AI-diagnosis scheduling.
357
+ */
358
+ onSessionFinalized?: (sessionId: string, info: {
359
+ refinalized: boolean;
360
+ }) => void;
361
+ /**
362
+ * Test-only seam: overrides how the client's evidence sources are constructed
363
+ * for the inner /api/solve-context adapter phase (sessionless Mode A +
364
+ * blended). Production leaves this unset; gatherAdapterEvidence then builds
365
+ * them from env via evidenceSourcesFromEnv().
366
+ */
367
+ evidenceSourcesFactory?: () => EvidenceSource[];
368
+ }
369
+ declare function createServer(config: ServerConfig): http.Server;
370
+
371
+ type CausalConfidence = "high" | "medium" | "low";
372
+
373
+ interface LlmBundleFullStackEventRef {
374
+ t: number;
375
+ iso?: string;
376
+ offsetMs?: number;
377
+ kind?: string;
378
+ }
379
+ interface LlmBundleFrontendRequestEvidenceSummary {
380
+ ref?: LlmBundleFullStackEventRef;
381
+ requestId?: string;
382
+ sessionId?: string;
383
+ method?: string;
384
+ url?: string;
385
+ status?: number;
386
+ durationMs?: number;
387
+ error?: {
388
+ message?: string;
389
+ transport?: string;
390
+ };
391
+ }
392
+ interface LlmBundleBackendRequestEvidenceSummary {
393
+ requestId?: string;
394
+ sessionId?: string;
395
+ correlation?: {
396
+ status?: string;
397
+ sessionIdSource?: string;
398
+ requestIdSource?: string;
399
+ };
400
+ start?: LlmBundleFullStackEventRef;
401
+ end?: LlmBundleFullStackEventRef;
402
+ errorRef?: LlmBundleFullStackEventRef;
403
+ method?: string;
404
+ url?: string;
405
+ pathname?: string;
406
+ route?: string;
407
+ statusCode?: number;
408
+ durationMs?: number;
409
+ error?: {
410
+ name?: string;
411
+ code?: string;
412
+ message?: string;
413
+ statusCode?: number;
414
+ };
415
+ }
416
+ /**
417
+ * Redaction-aware summary of one `k:'db.diff'` event (a row that changed during a request),
418
+ * correlated to the request via `requestId`. CP5 DB diffing. Sensitive columns were dropped in the
419
+ * shim; bundle build re-runs the redaction policy as defense-in-depth.
420
+ */
421
+ interface LlmBundleDbDiff {
422
+ t: number;
423
+ iso?: string;
424
+ offsetMs?: number;
425
+ engine: DbEngine;
426
+ op: "insert" | "update" | "delete";
427
+ table: string;
428
+ pk: Record<string, unknown> | null;
429
+ after?: Record<string, unknown>;
430
+ before?: Record<string, unknown>;
431
+ /**
432
+ * Only set on an image-less statement-level fallback event (`pk: null`, no `after`/`before`)
433
+ * where per-row images were unobtainable — records how many rows the statement changed so the
434
+ * write stays visible to differencing.
435
+ */
436
+ rowCount?: number;
437
+ requestId?: string;
438
+ }
439
+ interface LlmBundleDbRead {
440
+ t: number;
441
+ iso?: string;
442
+ offsetMs?: number;
443
+ engine: DbEngine;
444
+ table: string;
445
+ pk: Record<string, unknown> | null;
446
+ row: Record<string, unknown>;
447
+ requestId?: string;
448
+ }
449
+ interface LlmBundleDbActivity {
450
+ t: number;
451
+ iso?: string;
452
+ offsetMs?: number;
453
+ evidenceType: "otel_db_activity_statements_not_row_diffs";
454
+ system?: string;
455
+ operation?: string;
456
+ statement?: string;
457
+ spanName?: string;
458
+ serviceName?: string;
459
+ requestId?: string;
460
+ upgradeHint: string;
461
+ }
462
+
463
+ declare const CANDIDATE_SCHEMA_VERSION: 1;
464
+ interface EvidenceCandidate {
465
+ schemaVersion: typeof CANDIDATE_SCHEMA_VERSION;
466
+ id: string;
467
+ detector: string;
468
+ title: string;
469
+ severity: "critical" | "high" | "medium" | "low";
470
+ score: number;
471
+ confidence: "high" | "medium" | "low";
472
+ anchor: {
473
+ t: number;
474
+ offsetMs?: number;
475
+ route?: string;
476
+ elementLabel?: string;
477
+ requestId?: string;
478
+ method?: string;
479
+ url?: string;
480
+ status?: number;
481
+ errorCode?: string;
482
+ message?: string;
483
+ source?: string;
484
+ target?: TargetDescriptor;
485
+ };
486
+ /** Causal role assigned by the confidence-gated re-rank (CP3). Additive/optional. */
487
+ causalRole?: "root" | "symptom" | "isolated";
488
+ /** For a symptom, the candidate id of its attributed root cause. */
489
+ rootCauseId?: string;
490
+ /** For a root, the sorted candidate ids of the symptoms attributed to it. */
491
+ causes?: string[];
492
+ /** Weakest edge confidence along the causal path from root to this symptom. */
493
+ attributionConfidence?: CausalConfidence;
494
+ evidenceWindow: {
495
+ start: number;
496
+ end: number;
497
+ windowId: string;
498
+ };
499
+ }
500
+
501
+ declare const DISTINCT_BUGS_SCHEMA_VERSION: 1;
502
+ type DistinctBugSeverity = EvidenceCandidate["severity"];
503
+ /**
504
+ * A reference back to one ranked {@link EvidenceCandidate} that contributed to a distinct bug.
505
+ * Carries only already-redacted, candidate-derived fields so the grouped view re-exposes nothing.
506
+ */
507
+ interface DistinctBugEvidenceRef {
508
+ candidateId: string;
509
+ detector: string;
510
+ t: number;
511
+ offsetMs?: number;
512
+ requestId?: string;
513
+ method?: string;
514
+ status?: number;
515
+ route?: string;
516
+ target?: TargetDescriptor;
517
+ message?: string;
518
+ }
519
+ /**
520
+ * One DISTINCT, labeled bug a single session hit, grouped deterministically from the ranked
521
+ * evidence candidates. Front-end and back-end evidence are split so the bug carries its correlated
522
+ * window; correlated requests/traces share one bug via {@link DistinctBug.requestIds}.
523
+ */
524
+ interface DistinctBug {
525
+ schemaVersion: typeof DISTINCT_BUGS_SCHEMA_VERSION;
526
+ bugId: string;
527
+ title: string;
528
+ severity: DistinctBugSeverity;
529
+ firstSeen: number;
530
+ lastSeen: number;
531
+ window: {
532
+ start: number;
533
+ end: number;
534
+ };
535
+ requestIds: string[];
536
+ representative: {
537
+ title: string;
538
+ detector: string;
539
+ severity: DistinctBugSeverity;
540
+ message?: string;
541
+ route?: string;
542
+ method?: string;
543
+ status?: number;
544
+ target?: TargetDescriptor;
545
+ requestId?: string;
546
+ };
547
+ frontendEvidence: DistinctBugEvidenceRef[];
548
+ backendEvidence: DistinctBugEvidenceRef[];
549
+ dbDiffs?: DistinctBugEvidenceRef[];
550
+ candidateIds: string[];
551
+ }
552
+ interface DistinctBugRecurrenceInput {
553
+ bug: DistinctBug;
554
+ session: {
555
+ sessionId: string;
556
+ dir?: string;
557
+ app?: string;
558
+ tenant?: string;
559
+ release?: string;
560
+ build?: string;
561
+ start?: number;
562
+ };
563
+ }
564
+ interface DistinctBugRecurrenceOccurrence {
565
+ sessionId: string;
566
+ bugId: string;
567
+ title: string;
568
+ severity: DistinctBugSeverity;
569
+ firstSeen: number;
570
+ lastSeen: number;
571
+ app?: string;
572
+ tenant?: string;
573
+ release?: string;
574
+ build?: string;
575
+ dir?: string;
576
+ }
577
+ interface DistinctBugRecurrence {
578
+ signature: string;
579
+ title: string;
580
+ severity: DistinctBugSeverity;
581
+ first_seen: number;
582
+ last_seen: number;
583
+ session_count: number;
584
+ release_span?: {
585
+ first: string;
586
+ last: string;
587
+ label: string;
588
+ };
589
+ apps: string[];
590
+ tenants: string[];
591
+ occurrences: DistinctBugRecurrenceOccurrence[];
592
+ }
593
+ /**
594
+ * Groups the ranked candidates into DISTINCT bugs deterministically.
595
+ *
596
+ * Grouping key heuristics (deterministic, order-independent):
597
+ * - Candidates sharing a correlated `anchor.requestId` (Crumbtrail request id or W3C trace id)
598
+ * collapse into ONE bug, so the front-end signal and its back-end span/log land together.
599
+ * - Remaining candidates cluster by `(detector + normalized message/error signature + component
600
+ * signature)` within a {@link CLUSTER_WINDOW_MS} time window. Identical signatures far apart in
601
+ * time become separate bugs (disambiguated with a stable `#n` suffix so their ids never collide).
602
+ *
603
+ * The `bugId` is `bug_<hash>` where the hash is a stable FNV-1a digest of the dedup key — identical
604
+ * input always yields identical ids and ordering. Bugs are sorted by severity desc, then firstSeen
605
+ * asc, then bugId asc.
606
+ */
607
+ declare function groupDistinctBugs(candidates: EvidenceCandidate[]): DistinctBug[];
608
+ declare function buildDistinctBugSignature(bug: Pick<DistinctBug, "title" | "representative">): string;
609
+ declare function groupDistinctBugRecurrences(inputs: DistinctBugRecurrenceInput[]): DistinctBugRecurrence[];
610
+
611
+ interface AppendEventsOptions {
612
+ maxEventBytes?: number;
613
+ }
614
+ interface AppendEventsResult {
615
+ accepted: number;
616
+ dropped: number;
617
+ truncated: boolean;
618
+ bytesWritten: number;
619
+ }
620
+ interface ArtifactStat {
621
+ bytes: number;
622
+ isDir: boolean;
623
+ }
624
+ interface ResolveSessionScope {
625
+ tenant?: string;
626
+ app?: string;
627
+ date?: string;
628
+ }
629
+ /**
630
+ * Storage boundary for a single session's on-disk artifacts. Every filesystem
631
+ * write/read primitive that the node package performs against a session
632
+ * directory flows through this seam so the layout can later move behind an
633
+ * alternate backend (e.g. R2) without touching call sites.
634
+ */
635
+ interface SessionStore {
636
+ createSessionDir(sessionId: string): string;
637
+ appendEvents(sessionDir: string, events: BugEvent[], opts?: AppendEventsOptions): AppendEventsResult;
638
+ writeArtifact(sessionDir: string, name: string, data: string | Buffer): void;
639
+ writeBlob(sessionDir: string, name: string, data: Buffer): void;
640
+ writeSessionArtifact(sessionDir: string, name: string, data: string | Buffer): void;
641
+ readArtifact(sessionDir: string, name: string): Buffer | undefined;
642
+ statArtifact(sessionDir: string, name: string): ArtifactStat | undefined;
643
+ listSessions(outputDir: string): Array<{
644
+ id: string;
645
+ dir: string;
646
+ }>;
647
+ listArtifacts(sessionDir: string): string[];
648
+ /**
649
+ * Atomically relocate `sessionDir` to `targetDir` (staging -> finalized partition).
650
+ * This is the load-bearing FS-only "atomic rename" semantic the R2 adapter will have
651
+ * to emulate. Path/containment/symlink policy is enforced by the caller before and
652
+ * after the move (see SessionManager.moveSessionToV2Partition); this primitive owns
653
+ * only the atomic move itself.
654
+ */
655
+ moveToPartition(sessionDir: string, targetDir: string): string;
656
+ resolveSessionDir(idOrDir: string, outputDir?: string, scope?: ResolveSessionScope): string;
657
+ /**
658
+ * Tenant-scoped lookup with the cloud isolation contract: returns the session dir only
659
+ * when it exists inside {sessionsDir}/{tenant}/{app}/{YYYY-MM-DD}/{sessionId}, else
660
+ * undefined (never a fallback path, never a cross-tenant hit). Backs cloud's findSessionDir.
661
+ */
662
+ resolveScopedSessionDir(sessionsDir: string, tenant: string, app: string, sessionId: string): string | undefined;
663
+ deleteSessionDir(sessionDir: string): void;
664
+ }
665
+ declare class FilesystemSessionStore implements SessionStore {
666
+ createSessionDir(sessionId: string): string;
667
+ appendEvents(sessionDir: string, events: BugEvent[], opts?: AppendEventsOptions): AppendEventsResult;
668
+ writeArtifact(sessionDir: string, name: string, data: string | Buffer): void;
669
+ writeBlob(sessionDir: string, name: string, data: Buffer): void;
670
+ writeSessionArtifact(sessionDir: string, name: string, data: string | Buffer): void;
671
+ readArtifact(sessionDir: string, name: string): Buffer | undefined;
672
+ statArtifact(sessionDir: string, name: string): ArtifactStat | undefined;
673
+ listSessions(outputDir: string): Array<{
674
+ id: string;
675
+ dir: string;
676
+ }>;
677
+ listArtifacts(sessionDir: string): string[];
678
+ moveToPartition(sessionDir: string, targetDir: string): string;
679
+ resolveSessionDir(idOrDir: string, outputDir?: string, scope?: ResolveSessionScope): string;
680
+ resolveScopedSessionDir(sessionsDir: string, tenant: string, app: string, sessionId: string): string | undefined;
681
+ deleteSessionDir(sessionDir: string): void;
682
+ }
683
+ declare const defaultSessionStore: SessionStore;
684
+
685
+ declare const DEFAULT_SWEEP_IDLE_MS: number;
686
+ declare const DEFAULT_SWEEP_INTERVAL_MS: number;
687
+ declare const DEFAULT_SWEEP_CHECKPOINT_MS: number;
688
+ interface SessionSweepOptions {
689
+ sessions: SessionManager;
690
+ outputDir: string;
691
+ /** Inactivity threshold before an un-finalized session is swept. */
692
+ idleMs?: number;
693
+ /**
694
+ * Checkpoint threshold for sessions that never go idle: an un-finalized
695
+ * session older than this is finalized even while active, and an active
696
+ * finalized session with late events is re-finalized once per window.
697
+ */
698
+ checkpointMs?: number;
699
+ /** Upper bound on finalizations (incl. re-finalizations) per sweep. */
700
+ maxPerSweep?: number;
701
+ /** Called after each successful finalize (e.g. to schedule AI diagnosis). */
702
+ onFinalized?: (sessionId: string, refinalized: boolean) => void;
703
+ /** Clock seam for tests. */
704
+ now?: () => number;
705
+ }
706
+ interface SessionSweepResult {
707
+ scanned: number;
708
+ finalized: number;
709
+ refinalized: number;
710
+ failed: number;
711
+ /** Candidates left alone because they saw activity within idleMs. */
712
+ active: number;
713
+ }
714
+ /**
715
+ * One sweep pass: finalize every idle un-finalized session, and re-finalize
716
+ * every finalized session that received events after its last finalization.
717
+ * Serial by design. Returns counts for logging; never throws.
718
+ */
719
+ declare function sweepIdleSessions(options: SessionSweepOptions): Promise<SessionSweepResult>;
720
+ interface SessionSweeperHandle {
721
+ stop(): void;
722
+ /** Run one sweep immediately (test seam; also used by the interval). */
723
+ sweepNow(): Promise<SessionSweepResult>;
724
+ }
725
+ /**
726
+ * Start the periodic sweeper. The timer is unref'd so it never keeps a
727
+ * process alive, and ticks never overlap (a long postProcess simply delays
728
+ * the next pass).
729
+ */
730
+ declare function startSessionSweeper(options: SessionSweepOptions & {
731
+ intervalMs?: number;
732
+ /** Called with each sweep's counts whenever a sweep did any work. */
733
+ onSweep?: (result: SessionSweepResult) => void;
734
+ }): SessionSweeperHandle;
735
+
736
+ interface BugReport {
737
+ bugId: string;
738
+ sessionId: string;
739
+ flaggedAt: number;
740
+ windowMs: number;
741
+ note?: string;
742
+ voiceNote?: string;
743
+ url: string;
744
+ userAgent: string;
745
+ tags?: string[];
746
+ status?: "open" | "resolved";
747
+ summary: {
748
+ errorCount: number;
749
+ failedRequestCount: number;
750
+ eventCount: number;
751
+ eventKinds: Record<string, number>;
752
+ durationMs: number;
753
+ };
754
+ }
755
+ interface BugQueueConfig {
756
+ bugsDir: string;
757
+ whisperModel?: string;
758
+ }
759
+ interface LlmBugContext {
760
+ v: 1;
761
+ id: string;
762
+ sid: string;
763
+ ts: number;
764
+ w: number;
765
+ u: string;
766
+ n?: string;
767
+ g?: string[];
768
+ s: {
769
+ e: number;
770
+ f: number;
771
+ c: number;
772
+ d: number;
773
+ k: Record<string, number>;
774
+ };
775
+ err?: Array<{
776
+ t: number;
777
+ m: string;
778
+ }>;
779
+ req?: Array<{
780
+ t: number;
781
+ m: string;
782
+ u: string;
783
+ s: number;
784
+ }>;
785
+ nav?: Array<{
786
+ t: number;
787
+ to: string;
788
+ }>;
789
+ }
790
+ declare class BugQueueManager {
791
+ private bugsDir;
792
+ private whisperModel?;
793
+ constructor(config: BugQueueConfig);
794
+ create(report: BugReport, events: BugEvent[]): Promise<void>;
795
+ writeVoice(bugId: string, data: Buffer): Promise<boolean>;
796
+ list(filters?: {
797
+ after?: number;
798
+ before?: number;
799
+ status?: string;
800
+ tags?: string[];
801
+ }): BugReport[];
802
+ get(bugId: string): BugReport | null;
803
+ getBugDir(bugId: string): string;
804
+ resolve(bugId: string): void;
805
+ getLlmContext(bugId: string): LlmBugContext | null;
806
+ private resolveBugDir;
807
+ private resolveBugStagingDir;
808
+ private assertBugDirAvailable;
809
+ private assertSafeBugDir;
810
+ private writeLlmContext;
811
+ private writeLlmContextForDir;
812
+ }
813
+
814
+ /**
815
+ * Outcome of an on-demand reproduction attempt against a symptom. Reserved
816
+ * for the injected `Reproducer` seam consumed by `solveContext` when
817
+ * historical evidence is thin — see `docs/integrations/reproduction.md`.
818
+ */
819
+ interface ReproductionResult {
820
+ attempted: boolean;
821
+ /** Where a fresh session was recorded, if any. */
822
+ sessionDir?: string;
823
+ /** Fresh evidence gathered (empty if none). */
824
+ evidence: EvidenceItem[];
825
+ /** Usually empty; reserved. */
826
+ intent: IntentSignal[];
827
+ /** Human/agent-readable outcome. */
828
+ note: string;
829
+ }
830
+ interface Reproducer {
831
+ reproduce(symptom: Symptom): Promise<ReproductionResult>;
832
+ }
833
+
834
+ /**
835
+ * Parallel adapter fan-out. Queries every configured {@link EvidenceSource} at
836
+ * once, bounds each by a per-source timeout, redacts every result at the
837
+ * boundary, and enforces a global byte cap across all sources. It NEVER throws:
838
+ * a failed or slow source becomes an {@link EvidenceGap} ("sentry: timeout after
839
+ * 10s"), because evidence is advisory and "inconclusive" is a valid outcome.
840
+ */
841
+ /** Per-source timeout when neither the query nor options specify one. */
842
+ declare const DEFAULT_SOURCE_TIMEOUT_MS = 10000;
843
+ /** Global cap on normalized (redacted) evidence bytes across all sources. */
844
+ declare const DEFAULT_MAX_TOTAL_BYTES: number;
845
+ interface AdapterSourceStats {
846
+ provider: string;
847
+ /**
848
+ * Health signal for this source. INVARIANT: `ok` is false iff the source
849
+ * could not deliver its PRIMARY evidence — a total failure/timeout that
850
+ * returned zero items — regardless of whether the adapter threw or
851
+ * self-degraded internally. A source that returned any items (even partial,
852
+ * with an enrichment/secondary gap) stays `ok: true`. The framework is the
853
+ * single source of truth: it derives `ok` from the throw path OR a
854
+ * `kind: "source-unavailable"` gap on a zero-item result (see below), never
855
+ * from gap `reason` text.
856
+ */
857
+ ok: boolean;
858
+ /** Items the source reported fetching (0 on failure). */
859
+ fetched: number;
860
+ /** Items actually included after the global byte cap. */
861
+ returned: number;
862
+ /** True if the source's own maxItems OR the global byte cap dropped items. */
863
+ truncated: boolean;
864
+ /** Framework-measured wall time for this source's fetch. */
865
+ latencyMs: number;
866
+ /** Bytes of this source's included normalized items. */
867
+ bytes: number;
868
+ /** Sanitized failure/timeout reason when `ok` is false. */
869
+ error?: string;
870
+ }
871
+ interface AdapterEvidence {
872
+ items: EvidenceItem[];
873
+ gaps: EvidenceGap[];
874
+ stats: AdapterSourceStats[];
875
+ }
876
+ interface FetchAdapterEvidenceOptions {
877
+ /** Per-source timeout override (else query.limits.timeoutMs, else default). */
878
+ timeoutMs?: number;
879
+ /** Global byte cap across sources. Default 512 KB. */
880
+ maxTotalBytes?: number;
881
+ /** Clock hook (tests). Default Date.now. */
882
+ now?: () => number;
883
+ }
884
+ /**
885
+ * Fan out `query` to every source in parallel, degrade failures to gaps, redact
886
+ * at the boundary, and cap total normalized bytes. Deterministic: outcomes are
887
+ * folded in source order, so the byte cap trims from a stable point.
888
+ */
889
+ declare function fetchAdapterEvidence(sources: EvidenceSource[], query: EvidenceQuery, options?: FetchAdapterEvidenceOptions): Promise<AdapterEvidence>;
890
+
891
+ declare function redactEvidenceItem(item: EvidenceItem, index?: number): EvidenceItem;
892
+ declare function redactEvidenceGap(gap: EvidenceGap, index?: number): EvidenceGap;
893
+ /**
894
+ * Redact a whole adapter result. Returns a new result (items + gaps redacted);
895
+ * `stats` and `schemaVersion` pass through untouched.
896
+ */
897
+ declare function redactSourceResult(result: EvidenceSourceResult): EvidenceSourceResult;
898
+
899
+ /**
900
+ * Sentry evidence adapter — the reference implementation every other adapter
901
+ * (CloudWatch, Splunk, Datadog, PostHog, Cloudflare) copies.
902
+ *
903
+ * It is a query-at-incident-time pull: given a located incident window and the
904
+ * correlation keys known for it, ask Sentry's REST API for the issues/events in
905
+ * that window and normalize each into the neutral `evidence.v1` contract. It is
906
+ * zero-copy — nothing Sentry returns is persisted; only the derived bundle is.
907
+ *
908
+ * Load-bearing patterns for CP4–CP7 to mirror:
909
+ * - **Primary-first, enrichment-best-effort**: neutral items are built from the
910
+ * list/query call (the PRIMARY evidence — `brief`, `ref`, `whenObserved`) and
911
+ * emitted regardless of any secondary enrichment. Enrichment (here: per-issue
912
+ * stack heads) fans out with capped concurrency inside a sub-budget carved
913
+ * from `limits.timeoutMs`; if it is slow, fails, or the abort signal fires,
914
+ * the primary items still ship with a less-enriched field (`after: null`) —
915
+ * never nothing. This keeps a timed-out source degrading to "fewer/less-rich
916
+ * items + a gap," never "zero items."
917
+ * - **Injectable transport**: the constructor takes `fetchImpl?: typeof fetch`.
918
+ * Production uses global `fetch`; contract tests inject a stub that replays
919
+ * recorded fixtures, so there is zero network in CI.
920
+ * - **Descriptor-keyed query construction**: the search query is built strictly
921
+ * from `descriptor.joinKeys` ∩ the keys present in `EvidenceQuery.keys`, and a
922
+ * requested key the adapter cannot use becomes an honest {@link EvidenceGap}
923
+ * rather than a silent drop.
924
+ * - **Boundaries**: never throws out of `fetchEvidence` in a way the framework
925
+ * can't turn into a gap; honors `limits.maxItems`/`maxBytes`; no pagination
926
+ * walk beyond `maxItems`; every request carries `CRUMBTRAIL_USER_AGENT`; the
927
+ * auth token lives only in the Authorization header, never in a message.
928
+ * - **Redaction stays at the framework boundary** (`redact.ts` /
929
+ * `fetchAdapterEvidence`); this file only populates the fields that boundary
930
+ * scrubs (`brief`, `after`, `ref.url`).
931
+ */
932
+ /** Env var carrying the Sentry auth token (Bearer). Required. */
933
+ declare const SENTRY_AUTH_TOKEN_ENV = "CRUMBTRAIL_SENTRY_AUTH_TOKEN";
934
+ /** Env var carrying the Sentry organization slug. Required. */
935
+ declare const SENTRY_ORG_ENV = "CRUMBTRAIL_SENTRY_ORG";
936
+ /** Env var overriding the API host (self-hosted Sentry). Optional. */
937
+ declare const SENTRY_HOST_ENV = "CRUMBTRAIL_SENTRY_HOST";
938
+ /** Default SaaS host when {@link SENTRY_HOST_ENV} is unset. */
939
+ declare const SENTRY_DEFAULT_HOST = "https://sentry.io";
940
+ /** Presence of these two ⇒ the provider is configured (mirrors ticket clients). */
941
+ declare const SENTRY_AUTH_FIELDS: string[];
942
+ declare const SENTRY_DESCRIPTOR: EvidenceSourceDescriptor;
943
+ interface SentrySourceConfig {
944
+ authToken: string;
945
+ org: string;
946
+ /** API host, no trailing slash. Defaults to {@link SENTRY_DEFAULT_HOST}. */
947
+ host?: string;
948
+ /** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
949
+ fetchImpl?: typeof fetch;
950
+ }
951
+ interface SentryQueryPlan {
952
+ /** Sentry search query string (may be empty ⇒ time-window only). */
953
+ search: string;
954
+ /** Join keys actually used to narrow beyond the time window, best-first. */
955
+ usedKeys: EvidenceJoinKey[];
956
+ /** Honest gaps: a requested key Sentry cannot filter by. */
957
+ gaps: EvidenceGap[];
958
+ }
959
+ /**
960
+ * Build the Sentry search query from `descriptor.joinKeys` ∩ present keys.
961
+ * Precedence: a `trace:{traceId}` token when a trace is present (tightest), else
962
+ * `url:` / `release:` / `user.email:` tokens for whichever of those keys is
963
+ * present. Any requested key Sentry does NOT support (requestId, sessionId,
964
+ * service, and — when no trace is present — nothing to pin on) yields a gap so
965
+ * the bundle states plainly what filtered the result.
966
+ */
967
+ declare function buildSentryQuery(query: EvidenceQuery): SentryQueryPlan;
968
+ /** Minimal shape of the Sentry issues-list rows we consume. */
969
+ interface SentryIssue {
970
+ id: string;
971
+ title?: string;
972
+ culprit?: string;
973
+ permalink?: string;
974
+ lastSeen?: string;
975
+ firstSeen?: string;
976
+ metadata?: {
977
+ value?: string;
978
+ type?: string;
979
+ };
980
+ }
981
+ /** Minimal shape of a Sentry event (from `/issues/{id}/events/latest/`). */
982
+ interface SentryEvent {
983
+ id?: string;
984
+ dateCreated?: string;
985
+ entries?: Array<{
986
+ type?: string;
987
+ data?: unknown;
988
+ }>;
989
+ }
990
+ /** Sentry issue + its latest event → neutral evidence.v1 item. Pure. */
991
+ declare function normalizeSentryIssue(issue: SentryIssue, event?: SentryEvent): EvidenceItem;
992
+ declare class SentryEvidenceSource implements EvidenceSource {
993
+ readonly descriptor: EvidenceSourceDescriptor;
994
+ private readonly authToken;
995
+ private readonly org;
996
+ private readonly host;
997
+ private readonly fetchImpl;
998
+ constructor(config: SentrySourceConfig);
999
+ private headers;
1000
+ private getJson;
1001
+ /** Cheap authenticated no-op: GET the org endpoint. */
1002
+ health(signal?: AbortSignal): Promise<SourceHealth>;
1003
+ /** GET the latest event for one issue (for its stack head). A per-issue
1004
+ * enrichment failure is non-fatal: we return undefined and the item is still
1005
+ * emitted with `after: null` rather than sinking the whole fetch. Single
1006
+ * attempt on purpose — this runs once per issue, so retrying it would let the
1007
+ * best-effort enrichment blow the per-source timeout budget; the primary
1008
+ * issues query keeps the bounded retry. */
1009
+ private fetchLatestEvent;
1010
+ /**
1011
+ * OPTIONAL stack-head enrichment — the resilient pattern CP4–CP7 mirror.
1012
+ *
1013
+ * Fans out `/events/latest/` calls with capped concurrency, bounded by BOTH
1014
+ * the caller's abort signal AND a sub-budget carved from the per-source
1015
+ * timeout (`ENRICH_BUDGET_FRACTION` of `limits.timeoutMs`). A slow, failed, or
1016
+ * aborted event fetch simply leaves that issue's event `undefined` (→
1017
+ * `after: null`); it never blocks or drops the primary issues-list evidence.
1018
+ * Returns the per-issue events (index-aligned to `issues`) plus `incomplete`,
1019
+ * set when the signal fired or the sub-budget was exhausted before every
1020
+ * enrichment finished.
1021
+ */
1022
+ private enrichStackHeads;
1023
+ fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
1024
+ }
1025
+ /** Registry entry: build a Sentry source from env when its auth fields are set. */
1026
+ declare const sentryEvidenceProvider: EvidenceSourceProvider;
1027
+
1028
+ /**
1029
+ * CloudWatch Logs Insights evidence adapter — the second adapter, built to the
1030
+ * Sentry reference shape (`sentry.ts`). Query-at-incident-time pull: given a
1031
+ * located incident window and the correlation keys known for it, run a Logs
1032
+ * Insights query over the configured log group(s) and normalize each matching
1033
+ * log line into the neutral `evidence.v1` contract. Zero-copy — nothing AWS
1034
+ * returns is persisted; only the derived bundle is.
1035
+ *
1036
+ * Load-bearing patterns mirrored from the Sentry reference:
1037
+ * - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
1038
+ * defaulting to global `fetch`; contract tests inject a fixture-routing stub so
1039
+ * there is zero network in CI.
1040
+ * - **Resilience / self-limiting poll budget**: Logs Insights is async —
1041
+ * `StartQuery` then poll `GetQueryResults`. The polling loop is the analog of
1042
+ * Sentry's best-effort enrichment: it self-limits to a sub-budget carved from
1043
+ * `limits.timeoutMs` (`POLL_BUDGET_FRACTION`) so the adapter resolves BEFORE
1044
+ * the framework's per-source race fires, and it keeps the newest partial result
1045
+ * snapshot. A query that out-runs the budget degrades to the rows that
1046
+ * completed + an honest gap ("cloudwatch: query did not complete within Ns"),
1047
+ * never "no items."
1048
+ * - **Descriptor-keyed query construction**: the `filter` term is built strictly
1049
+ * from `descriptor.joinKeys` ∩ present keys; a requested key CloudWatch cannot
1050
+ * use becomes an honest {@link EvidenceGap} rather than a silent drop.
1051
+ * - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; no
1052
+ * pagination walk beyond `maxItems`; every request is SigV4-signed and carries
1053
+ * `CRUMBTRAIL_USER_AGENT`; credentials live only in the signing path, never in a
1054
+ * gap, stat, thrown message, or log.
1055
+ * - **Redaction stays at the framework boundary** (`redact.ts`); this file only
1056
+ * populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
1057
+ */
1058
+ /** Env var carrying the AWS access key id. Required. */
1059
+ declare const CLOUDWATCH_ACCESS_KEY_ID_ENV = "CRUMBTRAIL_CLOUDWATCH_ACCESS_KEY_ID";
1060
+ /** Env var carrying the AWS secret access key. Required. */
1061
+ declare const CLOUDWATCH_SECRET_ACCESS_KEY_ENV = "CRUMBTRAIL_CLOUDWATCH_SECRET_ACCESS_KEY";
1062
+ /** Env var carrying the AWS region, e.g. "us-east-1". Required. */
1063
+ declare const CLOUDWATCH_REGION_ENV = "CRUMBTRAIL_CLOUDWATCH_REGION";
1064
+ /** Env var carrying a comma-separated list of log group names. Required. */
1065
+ declare const CLOUDWATCH_LOG_GROUPS_ENV = "CRUMBTRAIL_CLOUDWATCH_LOG_GROUPS";
1066
+ /** Env var carrying an STS session token for temporary/role creds. Optional. */
1067
+ declare const CLOUDWATCH_SESSION_TOKEN_ENV = "CRUMBTRAIL_CLOUDWATCH_SESSION_TOKEN";
1068
+ /** Env var overriding the Logs endpoint host (govcloud/localstack). Optional. */
1069
+ declare const CLOUDWATCH_ENDPOINT_ENV = "CRUMBTRAIL_CLOUDWATCH_ENDPOINT";
1070
+ /** Presence of these four ⇒ the provider is configured (mirrors ticket clients). */
1071
+ declare const CLOUDWATCH_AUTH_FIELDS: string[];
1072
+ declare const CLOUDWATCH_DESCRIPTOR: EvidenceSourceDescriptor;
1073
+ interface CloudWatchSourceConfig {
1074
+ accessKeyId: string;
1075
+ secretAccessKey: string;
1076
+ region: string;
1077
+ /** Log group names to query, one Logs Insights query each. */
1078
+ logGroups: string[];
1079
+ /** STS session token for temporary/role credentials. Optional. */
1080
+ sessionToken?: string;
1081
+ /** Logs endpoint host override (no trailing slash). Defaults to the regional
1082
+ * `https://logs.{region}.amazonaws.com`. */
1083
+ endpoint?: string;
1084
+ /** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
1085
+ fetchImpl?: typeof fetch;
1086
+ /** Poll interval override (ms) for the GetQueryResults loop. Tests shrink it. */
1087
+ pollIntervalMs?: number;
1088
+ }
1089
+ interface CloudWatchQueryPlan {
1090
+ /** The Logs Insights query string (fields/filter/sort/limit). */
1091
+ queryString: string;
1092
+ /** Join keys actually used to narrow beyond the time window, best-first. */
1093
+ usedKeys: EvidenceJoinKey[];
1094
+ /** Honest gaps: a requested key CloudWatch cannot filter by. */
1095
+ gaps: EvidenceGap[];
1096
+ }
1097
+ /**
1098
+ * Build the Logs Insights query from `descriptor.joinKeys` ∩ present keys.
1099
+ * Precedence: a `filter @message like "<id>"` term when a requestId/traceId is
1100
+ * present (the tightest correlation Logs Insights offers on unstructured logs),
1101
+ * else a time-window-only scan (the window is expressed via StartQuery's
1102
+ * start/end, not the query string). Any requested key CloudWatch does NOT support
1103
+ * (sessionId, release, url, user) yields a gap so the bundle states plainly what
1104
+ * filtered the result.
1105
+ */
1106
+ declare function buildCloudWatchQuery(query: EvidenceQuery): CloudWatchQueryPlan;
1107
+ /** One Logs Insights result row: a list of `{ field, value }` cells. */
1108
+ type CloudWatchResultRow = Array<{
1109
+ field?: string;
1110
+ value?: string;
1111
+ }>;
1112
+ /**
1113
+ * CloudWatch Logs Insights console deep link for a log group in a region, so a
1114
+ * human can open the source. The console double-encodes path separators
1115
+ * (`/` → `$252F`). This links to the log group; a token-bearing URL would be
1116
+ * scrubbed at the redaction boundary anyway (there is none here).
1117
+ */
1118
+ declare function cloudWatchDeepLink(region: string, logGroup: string): string;
1119
+ /** One Logs Insights row → neutral evidence.v1 item. Pure. */
1120
+ declare function normalizeCloudWatchRow(row: CloudWatchResultRow, region: string, logGroup: string): EvidenceItem;
1121
+ declare class CloudWatchEvidenceSource implements EvidenceSource {
1122
+ readonly descriptor: EvidenceSourceDescriptor;
1123
+ private readonly accessKeyId;
1124
+ private readonly secretAccessKey;
1125
+ private readonly region;
1126
+ private readonly logGroups;
1127
+ private readonly sessionToken?;
1128
+ private readonly endpoint;
1129
+ private readonly fetchImpl;
1130
+ private readonly pollIntervalMs;
1131
+ constructor(config: CloudWatchSourceConfig);
1132
+ /**
1133
+ * One signed POST to the Logs JSON API. `action` is the short target name
1134
+ * (e.g. "StartQuery"). The SigV4 signature and the (non-secret) User-Agent are
1135
+ * the only auth material on the wire; the secret key never leaves the signer.
1136
+ */
1137
+ private post;
1138
+ private startQuery;
1139
+ private getQueryResults;
1140
+ /** Cheap authenticated no-op: DescribeLogGroups with limit 1. */
1141
+ health(signal?: AbortSignal): Promise<SourceHealth>;
1142
+ /**
1143
+ * Run one log group's Logs Insights query end-to-end: StartQuery, then poll
1144
+ * GetQueryResults until Complete, a terminal non-complete status, or the
1145
+ * shared deadline/abort fires. Keeps the newest partial `results` snapshot so
1146
+ * a query that out-runs the budget still yields the rows that had completed —
1147
+ * never empty. A group whose query throws (bad creds, missing group) degrades
1148
+ * to `error` on the outcome rather than sinking the other groups' rows.
1149
+ */
1150
+ private runGroupQuery;
1151
+ fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
1152
+ }
1153
+ /** Registry entry: build a CloudWatch source from env when its auth fields are set. */
1154
+ declare const cloudWatchEvidenceProvider: EvidenceSourceProvider;
1155
+
1156
+ interface SigV4Input {
1157
+ method: string;
1158
+ /** Absolute request URL (host + path + optional query). */
1159
+ url: string;
1160
+ region: string;
1161
+ /** AWS service name, e.g. "logs". */
1162
+ service: string;
1163
+ accessKeyId: string;
1164
+ secretAccessKey: string;
1165
+ /** Temporary-credential session token (STS/role). Signed + echoed when set. */
1166
+ sessionToken?: string;
1167
+ /** Request body (already serialized). Hashed into the canonical request. */
1168
+ body: string;
1169
+ /** Non-auth headers to sign (e.g. content-type, x-amz-target). */
1170
+ headers: Record<string, string>;
1171
+ /** Signing instant. Injectable for deterministic tests. Default `new Date()`. */
1172
+ now?: Date;
1173
+ }
1174
+ interface SignedHeaders {
1175
+ [name: string]: string;
1176
+ }
1177
+ /**
1178
+ * Sign `input` and return the request headers to send (the caller's headers plus
1179
+ * `X-Amz-Date`, `Authorization`, and `X-Amz-Security-Token` when a session token
1180
+ * is present). Pure and synchronous.
1181
+ */
1182
+ declare function signSigV4(input: SigV4Input): SignedHeaders;
1183
+
1184
+ /**
1185
+ * Splunk evidence adapter — built to the Sentry reference shape (`sentry.ts`) and
1186
+ * the CloudWatch async-poll shape (`cloudwatch.ts`). Query-at-incident-time pull:
1187
+ * given a located incident window and the correlation keys known for it, run a
1188
+ * bounded SPL search over the configured index(es) and normalize each matching
1189
+ * event into the neutral `evidence.v1` contract. Zero-copy — nothing Splunk
1190
+ * returns is persisted; only the derived bundle is.
1191
+ *
1192
+ * Load-bearing patterns mirrored from the references:
1193
+ * - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
1194
+ * defaulting to global `fetch`; contract tests inject a fixture-routing stub so
1195
+ * there is zero network in CI.
1196
+ * - **Resilience / self-limiting search budget**: a Splunk search is async —
1197
+ * `POST .../jobs` dispatches a job, then we poll `.../results_preview`. The poll
1198
+ * loop is the analog of CloudWatch's poll: it self-limits to a sub-budget carved
1199
+ * from `limits.timeoutMs` (`POLL_BUDGET_FRACTION`) so the adapter resolves BEFORE
1200
+ * the framework's per-source race fires, and it keeps the newest partial preview
1201
+ * snapshot. A search that out-runs the budget degrades to the rows Splunk had
1202
+ * previewed + an honest gap ("splunk: search did not complete within Ns"),
1203
+ * never "no items."
1204
+ * - **Descriptor-keyed query construction**: the SPL terms are built strictly from
1205
+ * `descriptor.joinKeys` ∩ present keys; a requested key Splunk cannot use becomes
1206
+ * an honest {@link EvidenceGap} rather than a silent drop. Every declared join
1207
+ * key is genuinely applied (traceId/requestId as a term, service as a field
1208
+ * filter, time as earliest/latest) — no silent no-op.
1209
+ * - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; the `count`
1210
+ * arg caps results server-side so there is no pagination walk beyond `maxItems`;
1211
+ * every request carries `CRUMBTRAIL_USER_AGENT`; the token lives only in the
1212
+ * Authorization header, never in a gap, stat, thrown message, or log.
1213
+ * - **Redaction stays at the framework boundary** (`redact.ts`); this file only
1214
+ * populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
1215
+ */
1216
+ /** Env var carrying the Splunk search head base URL (incl. mgmt port, e.g.
1217
+ * `https://splunk.example.com:8089`). Required. */
1218
+ declare const SPLUNK_HOST_ENV = "CRUMBTRAIL_SPLUNK_HOST";
1219
+ /** Env var carrying the Splunk authentication (JWT) token. Required. */
1220
+ declare const SPLUNK_TOKEN_ENV = "CRUMBTRAIL_SPLUNK_TOKEN";
1221
+ /** Env var carrying a comma-separated list of allowed indexes. Required. */
1222
+ declare const SPLUNK_INDEX_ENV = "CRUMBTRAIL_SPLUNK_INDEX";
1223
+ /** Env var overriding the web UI base URL for deep links (e.g.
1224
+ * `https://splunk.example.com:8000`). Optional — derived from the host if unset. */
1225
+ declare const SPLUNK_WEB_URL_ENV = "CRUMBTRAIL_SPLUNK_WEB_URL";
1226
+ /** Presence of these three ⇒ the provider is configured (mirrors ticket clients). */
1227
+ declare const SPLUNK_AUTH_FIELDS: string[];
1228
+ declare const SPLUNK_DESCRIPTOR: EvidenceSourceDescriptor;
1229
+ interface SplunkSourceConfig {
1230
+ /** Search head base URL (no trailing slash), typically the :8089 mgmt port. */
1231
+ host: string;
1232
+ /** Splunk authentication (JWT) token. */
1233
+ token: string;
1234
+ /** Allowed index names; the SPL scans these (OR-joined). */
1235
+ indexes: string[];
1236
+ /** Web UI base URL for deep links. Defaults to a best-effort host derivation. */
1237
+ webUrl?: string;
1238
+ /** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
1239
+ fetchImpl?: typeof fetch;
1240
+ /** Poll interval override (ms) for the results_preview loop. Tests shrink it. */
1241
+ pollIntervalMs?: number;
1242
+ }
1243
+ interface SplunkQueryPlan {
1244
+ /** The full SPL string (starts with `search`, incl. index + earliest/latest). */
1245
+ spl: string;
1246
+ /** Join keys actually used to narrow beyond the time window, best-first. */
1247
+ usedKeys: EvidenceJoinKey[];
1248
+ /** Honest gaps: a requested key Splunk cannot filter by. */
1249
+ gaps: EvidenceGap[];
1250
+ }
1251
+ /**
1252
+ * Build the SPL from `descriptor.joinKeys` ∩ present keys.
1253
+ * Precedence: a bare quoted correlation-id term (traceId, else requestId) is the
1254
+ * tightest filter over raw events; `service="<svc>"` is applied additively as a
1255
+ * field filter; the time window is always expressed via `earliest`/`latest`. Any
1256
+ * requested key Splunk does NOT support (sessionId, release, url, user) yields a
1257
+ * gap so the bundle states plainly what filtered the result.
1258
+ */
1259
+ declare function buildSplunkQuery(query: EvidenceQuery, indexes: string[]): SplunkQueryPlan;
1260
+ /** One Splunk search result row (the `results[]` entries in the JSON output). */
1261
+ interface SplunkResultRow {
1262
+ _raw?: string;
1263
+ _time?: string;
1264
+ _cd?: string;
1265
+ _bkt?: string;
1266
+ index?: string;
1267
+ sourcetype?: string;
1268
+ [field: string]: unknown;
1269
+ }
1270
+ /**
1271
+ * Derive a best-effort web UI base for deep links from the mgmt host: the mgmt
1272
+ * port (8089) becomes the default web port (8000); otherwise the origin is used.
1273
+ * A configured {@link SplunkSourceConfig.webUrl} always wins.
1274
+ */
1275
+ declare function splunkWebBase(host: string, webUrl?: string): string;
1276
+ /**
1277
+ * Splunk search-app deep link that lands on the SPL + time range the event came
1278
+ * from — the practical "actual search where the log lives" link (better than a
1279
+ * bare index link). The mgmt API host carries no UI, so we point at the web base.
1280
+ */
1281
+ declare function splunkSearchDeepLink(webBase: string, spl: string, window: {
1282
+ start: number;
1283
+ end: number;
1284
+ }): string;
1285
+ /** One Splunk result row → neutral evidence.v1 item. Pure. */
1286
+ declare function normalizeSplunkRow(row: SplunkResultRow, webBase: string, spl: string, window: {
1287
+ start: number;
1288
+ end: number;
1289
+ }): EvidenceItem;
1290
+ declare class SplunkEvidenceSource implements EvidenceSource {
1291
+ readonly descriptor: EvidenceSourceDescriptor;
1292
+ private readonly host;
1293
+ private readonly token;
1294
+ private readonly indexes;
1295
+ private readonly webBase;
1296
+ private readonly fetchImpl;
1297
+ private readonly pollIntervalMs;
1298
+ constructor(config: SplunkSourceConfig);
1299
+ private headers;
1300
+ private getJson;
1301
+ /** Dispatch a search job (form-encoded), returning its sid. */
1302
+ private createJob;
1303
+ /** Fetch the newest preview snapshot for a job (also final rows once done). */
1304
+ private previewResults;
1305
+ /** Cheap authenticated no-op: GET server info. */
1306
+ health(signal?: AbortSignal): Promise<SourceHealth>;
1307
+ fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
1308
+ }
1309
+ /** Registry entry: build a Splunk source from env when its auth fields are set. */
1310
+ declare const splunkEvidenceProvider: EvidenceSourceProvider;
1311
+
1312
+ /**
1313
+ * Datadog evidence adapter — built to the Sentry reference shape (`sentry.ts`).
1314
+ * Query-at-incident-time pull: given a located incident window and the
1315
+ * correlation keys known for it, ask Datadog's Logs Search v2 API (primary) and
1316
+ * Spans Search v2 API (secondary) for matching records in that window, and
1317
+ * normalize each into the neutral `evidence.v1` contract. Zero-copy — nothing
1318
+ * Datadog returns is persisted; only the derived bundle is.
1319
+ *
1320
+ * Load-bearing patterns mirrored from the Sentry reference:
1321
+ * - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
1322
+ * defaulting to global `fetch`; contract tests inject a fixture-routing stub so
1323
+ * there is zero network in CI.
1324
+ * - **Resilience / self-limiting secondary budget**: logs are the PRIMARY
1325
+ * evidence (fetched with the bounded retry). Spans are a SECONDARY fetch, run
1326
+ * best-effort inside a sub-budget carved from `limits.timeoutMs`
1327
+ * (`SPAN_BUDGET_FRACTION`) and bounded by the caller's abort signal. If the span
1328
+ * search is slow, fails, or is aborted, the log items still ship + an honest gap
1329
+ * — one API's slowness NEVER drops the other API's items, and the adapter always
1330
+ * resolves before the framework's per-source timeout fires.
1331
+ * - **Descriptor-keyed query construction**: the query string is built strictly
1332
+ * from `descriptor.joinKeys` ∩ present keys; a requested key Datadog cannot use
1333
+ * becomes an honest {@link EvidenceGap}. Every declared join key is genuinely
1334
+ * applied (traceId → `@trace_id`, service → `service:`, url → `@http.url`, time
1335
+ * → filter from/to) — no silent no-op.
1336
+ * - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; the page
1337
+ * limit caps results server-side so there is no pagination walk beyond
1338
+ * `maxItems`; every request carries `CRUMBTRAIL_USER_AGENT`; the API/app keys live
1339
+ * only in the request headers, never in a gap, stat, thrown message, or log.
1340
+ * - **Redaction stays at the framework boundary** (`redact.ts`); this file only
1341
+ * populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
1342
+ */
1343
+ /** Env var carrying the Datadog API key. Required. */
1344
+ declare const DATADOG_API_KEY_ENV = "CRUMBTRAIL_DATADOG_API_KEY";
1345
+ /** Env var carrying the Datadog application key. Required. */
1346
+ declare const DATADOG_APP_KEY_ENV = "CRUMBTRAIL_DATADOG_APP_KEY";
1347
+ /** Env var carrying the Datadog site (datadoghq.com | datadoghq.eu | us3… ). Optional. */
1348
+ declare const DATADOG_SITE_ENV = "CRUMBTRAIL_DATADOG_SITE";
1349
+ /** Default site when {@link DATADOG_SITE_ENV} is unset. */
1350
+ declare const DATADOG_DEFAULT_SITE = "datadoghq.com";
1351
+ /** Presence of these two ⇒ the provider is configured (mirrors ticket clients).
1352
+ * Site has a default so it is not required. */
1353
+ declare const DATADOG_AUTH_FIELDS: string[];
1354
+ declare const DATADOG_DESCRIPTOR: EvidenceSourceDescriptor;
1355
+ interface DatadogSourceConfig {
1356
+ apiKey: string;
1357
+ appKey: string;
1358
+ /** Datadog site, e.g. "datadoghq.com". Defaults to {@link DATADOG_DEFAULT_SITE}. */
1359
+ site?: string;
1360
+ /** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
1361
+ fetchImpl?: typeof fetch;
1362
+ }
1363
+ interface DatadogQueryPlan {
1364
+ /** The Datadog search query string (may be empty ⇒ time-window only). */
1365
+ queryString: string;
1366
+ /** Join keys actually used to narrow beyond the time window, best-first. */
1367
+ usedKeys: EvidenceJoinKey[];
1368
+ /** Honest gaps: a requested key Datadog cannot filter by. */
1369
+ gaps: EvidenceGap[];
1370
+ }
1371
+ /**
1372
+ * Build the Datadog query from `descriptor.joinKeys` ∩ present keys. The same
1373
+ * query string filters both the logs and spans search bodies.
1374
+ * Precedence: a `@trace_id:<id>` term when a trace is present (tightest), plus
1375
+ * additive `service:<svc>` and `@http.url:<url>` terms for whichever of those
1376
+ * keys is present. Any requested key Datadog does NOT support (requestId,
1377
+ * sessionId, release, user) yields a gap so the bundle states plainly what
1378
+ * filtered the result.
1379
+ */
1380
+ declare function buildDatadogQuery(query: EvidenceQuery): DatadogQueryPlan;
1381
+ /** Minimal shape of a Datadog Logs Search v2 result row. */
1382
+ interface DatadogLog {
1383
+ id?: string;
1384
+ attributes?: {
1385
+ timestamp?: string | number;
1386
+ message?: string;
1387
+ service?: string;
1388
+ status?: string;
1389
+ };
1390
+ }
1391
+ /** Minimal shape of a Datadog Spans Search v2 result row. */
1392
+ interface DatadogSpan {
1393
+ id?: string;
1394
+ attributes?: {
1395
+ start_timestamp?: string | number;
1396
+ resource_name?: string;
1397
+ operation_name?: string;
1398
+ service?: string;
1399
+ duration?: number;
1400
+ trace_id?: string;
1401
+ span_id?: string;
1402
+ };
1403
+ }
1404
+ /**
1405
+ * Derive the Datadog app UI base for deep links from the API site.
1406
+ * `datadoghq.com`/`.eu` use an `app.` subdomain; regional sites (us3/us5/ap1)
1407
+ * already carry their subdomain, so the site is the app host.
1408
+ */
1409
+ declare function datadogAppBase(site: string): string;
1410
+ /** One Datadog log → neutral evidence.v1 item (lane "logs"). Pure. */
1411
+ declare function normalizeDatadogLog(log: DatadogLog, appBase: string, queryString: string, window: {
1412
+ start: number;
1413
+ end: number;
1414
+ }): EvidenceItem;
1415
+ /** One Datadog span → neutral evidence.v1 item (lane "network"). Pure. */
1416
+ declare function normalizeDatadogSpan(span: DatadogSpan, appBase: string): EvidenceItem;
1417
+ declare class DatadogEvidenceSource implements EvidenceSource {
1418
+ readonly descriptor: EvidenceSourceDescriptor;
1419
+ private readonly apiKey;
1420
+ private readonly appKey;
1421
+ private readonly site;
1422
+ private readonly apiBase;
1423
+ private readonly appBase;
1424
+ private readonly fetchImpl;
1425
+ constructor(config: DatadogSourceConfig);
1426
+ private headers;
1427
+ private post;
1428
+ /** Cheap authenticated no-op: validate the API key. */
1429
+ health(signal?: AbortSignal): Promise<SourceHealth>;
1430
+ /** PRIMARY: Logs Search v2. Bounded retry — this is the evidence we must ship. */
1431
+ private searchLogs;
1432
+ /** SECONDARY: Spans Search v2. Single attempt on purpose — it runs inside a
1433
+ * sub-budget, so retrying would risk blowing the per-source timeout. */
1434
+ private searchSpans;
1435
+ /**
1436
+ * Best-effort span search inside a sub-budget carved from `limits.timeoutMs`
1437
+ * and bounded by the caller's abort signal. A slow/failed/aborted span search
1438
+ * leaves `spans` empty (→ the log items still ship) and flips `incomplete`.
1439
+ * Mirrors Sentry's `enrichStackHeads`.
1440
+ */
1441
+ private fetchSpansBestEffort;
1442
+ fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
1443
+ }
1444
+ /** Registry entry: build a Datadog source from env when its auth fields are set. */
1445
+ declare const datadogEvidenceProvider: EvidenceSourceProvider;
1446
+
1447
+ /**
1448
+ * PostHog evidence adapter — built to the Sentry reference shape (`sentry.ts`)
1449
+ * and the Datadog two-API shape (`datadog.ts`). Query-at-incident-time pull:
1450
+ * given a located incident window and the correlation keys known for it, ask
1451
+ * PostHog's Events REST API (primary) and Session-Recordings list API
1452
+ * (secondary) for records in that window, and normalize each into the neutral
1453
+ * `evidence.v1` contract. Zero-copy — nothing PostHog returns is persisted; only
1454
+ * the derived bundle is.
1455
+ *
1456
+ * Load-bearing patterns mirrored from the references:
1457
+ * - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
1458
+ * defaulting to global `fetch`; contract tests inject a fixture-routing stub so
1459
+ * there is zero network in CI.
1460
+ * - **Resilience / self-limiting secondary budget**: events are the PRIMARY
1461
+ * evidence (fetched with the bounded retry). Session recordings are a SECONDARY
1462
+ * fetch, run best-effort inside a sub-budget carved from `limits.timeoutMs`
1463
+ * (`RECORDINGS_BUDGET_FRACTION`) and bounded by the caller's abort signal. If
1464
+ * the recordings list is slow, fails, or is aborted, the event items still ship
1465
+ * + an honest gap — one API's slowness NEVER drops the other API's items, and
1466
+ * the adapter always resolves before the framework's per-source timeout fires.
1467
+ * - **Descriptor-keyed query construction**: the filters are built strictly from
1468
+ * `descriptor.joinKeys` ∩ present keys; a requested key PostHog cannot use
1469
+ * becomes an honest {@link EvidenceGap}. Every declared join key is genuinely
1470
+ * applied (user → `distinct_id`, sessionId → `$session_id` event property +
1471
+ * recordings `session_ids`, url → `$current_url` event property, time → the
1472
+ * window) — no silent no-op.
1473
+ * - **Recordings are LINK-ONLY**: the adapter lists recordings and links them by
1474
+ * `ref.url` (a replay player deep link). It NEVER downloads or stores recording
1475
+ * content — `before`/`after` stay null. See the brief's Out of Scope.
1476
+ * - **Boundaries**: honors `limits.maxItems`/`maxBytes`/`timeoutMs`; the `limit`
1477
+ * arg caps results server-side so there is no pagination walk beyond
1478
+ * `maxItems`; every request carries `CRUMBTRAIL_USER_AGENT`; the personal API key
1479
+ * lives only in the Authorization header, never in a gap, stat, thrown message,
1480
+ * or log.
1481
+ * - **Redaction stays at the framework boundary** (`redact.ts`); this file only
1482
+ * populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
1483
+ */
1484
+ /** Env var carrying the PostHog personal API key (Bearer). Required. */
1485
+ declare const POSTHOG_API_KEY_ENV = "CRUMBTRAIL_POSTHOG_API_KEY";
1486
+ /** Env var carrying the PostHog project id (numeric or short id). Required. */
1487
+ declare const POSTHOG_PROJECT_ID_ENV = "CRUMBTRAIL_POSTHOG_PROJECT_ID";
1488
+ /** Env var overriding the API/UI host (self-hosted or EU cloud). Optional. */
1489
+ declare const POSTHOG_HOST_ENV = "CRUMBTRAIL_POSTHOG_HOST";
1490
+ /** Default cloud host when {@link POSTHOG_HOST_ENV} is unset. The API and UI
1491
+ * share this origin, so it also anchors the deep links. Use
1492
+ * `https://eu.posthog.com` (EU cloud) or your self-hosted origin otherwise. */
1493
+ declare const POSTHOG_DEFAULT_HOST = "https://us.posthog.com";
1494
+ /** Presence of these two ⇒ the provider is configured (mirrors ticket clients).
1495
+ * Host has a default so it is not required. */
1496
+ declare const POSTHOG_AUTH_FIELDS: string[];
1497
+ declare const POSTHOG_DESCRIPTOR: EvidenceSourceDescriptor;
1498
+ interface PostHogSourceConfig {
1499
+ apiKey: string;
1500
+ projectId: string;
1501
+ /** API/UI host, no trailing slash. Defaults to {@link POSTHOG_DEFAULT_HOST}. */
1502
+ host?: string;
1503
+ /** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
1504
+ fetchImpl?: typeof fetch;
1505
+ }
1506
+ /** One PostHog event-property filter (the `properties` query param is a JSON
1507
+ * array of these). */
1508
+ interface PostHogPropertyFilter {
1509
+ key: string;
1510
+ value: string;
1511
+ operator: "exact";
1512
+ type: "event";
1513
+ }
1514
+ interface PostHogQueryPlan {
1515
+ /** `distinct_id` filter (from the `user` key), applied to events + recordings. */
1516
+ distinctId?: string;
1517
+ /** `$session_id` filter (from the `sessionId` key), applied to recordings too. */
1518
+ sessionId?: string;
1519
+ /** Event-property filters (`$session_id`, `$current_url`) for the events query. */
1520
+ properties: PostHogPropertyFilter[];
1521
+ /** Join keys actually used to narrow beyond the time window, best-first. */
1522
+ usedKeys: EvidenceJoinKey[];
1523
+ /** Honest gaps: a requested key PostHog cannot filter by. */
1524
+ gaps: EvidenceGap[];
1525
+ }
1526
+ /**
1527
+ * Build the PostHog query from `descriptor.joinKeys` ∩ present keys.
1528
+ * Precedence: `user` → `distinct_id` (the person filter, tightest and applied to
1529
+ * BOTH the events and recordings requests); `sessionId` → a `$session_id` event
1530
+ * property filter AND the recordings `session_ids` filter; `url` → a
1531
+ * `$current_url` event property filter. The time window is always applied via the
1532
+ * request date range. Any requested key PostHog does NOT support (traceId,
1533
+ * requestId, release, service) yields a gap so the bundle states plainly what
1534
+ * filtered the result.
1535
+ */
1536
+ declare function buildPostHogQuery(query: EvidenceQuery): PostHogQueryPlan;
1537
+ /** Minimal shape of a PostHog event row (from `GET /api/projects/{id}/events/`). */
1538
+ interface PostHogEvent {
1539
+ id?: string;
1540
+ distinct_id?: string;
1541
+ event?: string;
1542
+ timestamp?: string;
1543
+ properties?: Record<string, unknown>;
1544
+ }
1545
+ /** Minimal shape of a PostHog session-recording row (from the list endpoint). */
1546
+ interface PostHogRecording {
1547
+ id?: string;
1548
+ distinct_id?: string;
1549
+ start_time?: string;
1550
+ end_time?: string;
1551
+ /** Recording length in SECONDS (PostHog reports `recording_duration`). */
1552
+ recording_duration?: number;
1553
+ }
1554
+ /** Human-readable recording duration, e.g. "45s" or "3m 20s". */
1555
+ declare function formatDuration(seconds: number | undefined): string;
1556
+ /** A person-scoped deep link when the distinct id is known (the reliable link),
1557
+ * else the project activity explorer. */
1558
+ declare function posthogEventDeepLink(appBase: string, projectId: string, distinctId: string | undefined): string;
1559
+ /** The replay player deep link for a recording (LINK-ONLY — no content fetched). */
1560
+ declare function posthogRecordingDeepLink(appBase: string, projectId: string, recordingId: string): string;
1561
+ /** One PostHog event → neutral evidence.v1 item (lane "browser"/"flow"). Pure. */
1562
+ declare function normalizePostHogEvent(event: PostHogEvent, appBase: string, projectId: string): EvidenceItem;
1563
+ /**
1564
+ * One PostHog session recording → neutral evidence.v1 item (lane "flow").
1565
+ * LINK-ONLY: the value is the replay deep link in `ref.url`; NO recording content
1566
+ * is fetched or stored, so `before`/`after` are null. Pure.
1567
+ */
1568
+ declare function normalizePostHogRecording(recording: PostHogRecording, appBase: string, projectId: string): EvidenceItem;
1569
+ declare class PostHogEvidenceSource implements EvidenceSource {
1570
+ readonly descriptor: EvidenceSourceDescriptor;
1571
+ private readonly apiKey;
1572
+ private readonly projectId;
1573
+ private readonly appBase;
1574
+ private readonly fetchImpl;
1575
+ constructor(config: PostHogSourceConfig);
1576
+ private headers;
1577
+ private getJson;
1578
+ private projectPath;
1579
+ /** Cheap authenticated no-op: GET the project endpoint. */
1580
+ health(signal?: AbortSignal): Promise<SourceHealth>;
1581
+ /** PRIMARY: events query. Bounded retry — this is the evidence we must ship. */
1582
+ private searchEvents;
1583
+ /** SECONDARY: session-recordings list. Single attempt on purpose — it runs
1584
+ * inside a sub-budget, so retrying would risk blowing the per-source timeout.
1585
+ * LINK-ONLY: this lists recordings; it NEVER reads snapshot/content blobs. */
1586
+ private searchRecordings;
1587
+ /**
1588
+ * Best-effort recordings list inside a sub-budget carved from `limits.timeoutMs`
1589
+ * and bounded by the caller's abort signal. A slow/failed/aborted list leaves
1590
+ * `recordings` empty (→ the event items still ship) and flips `incomplete`.
1591
+ * Mirrors Datadog's `fetchSpansBestEffort`.
1592
+ */
1593
+ private fetchRecordingsBestEffort;
1594
+ fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
1595
+ }
1596
+ /** Registry entry: build a PostHog source from env when its auth fields are set. */
1597
+ declare const posthogEvidenceProvider: EvidenceSourceProvider;
1598
+
1599
+ /**
1600
+ * Cloudflare evidence adapter — built to the Sentry reference shape
1601
+ * (`sentry.ts`). Cloudflare has no ad-hoc log-query API on most plans, so this
1602
+ * adapter reads a **Logpush-to-R2 sink**: the client configures a Logpush job
1603
+ * that streams HTTP-request logs (or Workers Trace Events) as gzipped NDJSON
1604
+ * objects into an R2 bucket, and this adapter lists+reads the objects whose
1605
+ * time-partitioned keys fall inside the located incident window.
1606
+ *
1607
+ * R2 exposes an **S3-compatible API**, so we reuse the hand-rolled
1608
+ * {@link signSigV4} signer (service `s3`, region `auto`, R2 access key/secret,
1609
+ * endpoint `https://{account}.r2.cloudflarestorage.com`) rather than adding an
1610
+ * S3 SDK — the same zero-dependency decision made for CloudWatch. R2 egress is
1611
+ * FREE, so bulk reads cost nothing (the one provider where this is true), but we
1612
+ * still bound total work by `maxItems`/`maxBytes`/`timeoutMs` — we never walk the
1613
+ * whole bucket; truncation is recorded as a gap. Zero-copy: nothing R2 returns is
1614
+ * persisted; only the derived bundle is.
1615
+ *
1616
+ * Load-bearing patterns mirrored from the reference adapters:
1617
+ * - **Injectable transport**: constructor takes `fetchImpl?: typeof fetch`
1618
+ * defaulting to global `fetch`; contract tests inject a fixture-routing stub so
1619
+ * there is zero network in CI.
1620
+ * - **Descriptor-keyed filtering**: R2 has no server-side query, so `url` and
1621
+ * `requestId` are applied as LINE-LEVEL filters after read; `time` is applied
1622
+ * both by mapping the window to the minimal set of date-partitioned object keys
1623
+ * AND at the line level. Any requested key the dataset cannot use becomes an
1624
+ * honest {@link EvidenceGap} rather than a silent no-op.
1625
+ * - **Self-limiting read budget**: listing is the PRIMARY step (bounded retry);
1626
+ * object reads fan out with capped concurrency inside a sub-budget carved from
1627
+ * `limits.timeoutMs` (`READ_BUDGET_FRACTION`) so the adapter resolves BEFORE the
1628
+ * framework's per-source timeout fires. Objects that could not be read in budget
1629
+ * degrade to whatever was parsed + an honest gap, never "no items."
1630
+ * - **Resilience / source-unavailable marker**: a hard failure (bucket
1631
+ * unreachable / auth fail) that yielded zero items self-degrades to a
1632
+ * `kind: "source-unavailable"` gap → `ok:false`; partial success stays
1633
+ * `ok:true` (see `fetch-all.ts`).
1634
+ * - **Secrets discipline**: the R2 secret lives ONLY inside the signer; it never
1635
+ * appears in a gap, stat, thrown message, or log.
1636
+ * - **Redaction stays at the framework boundary** (`redact.ts`); this file only
1637
+ * populates the fields that boundary scrubs (`brief`, `after`, `ref.url`).
1638
+ */
1639
+ /** Env var carrying the Cloudflare account id (R2 endpoint host). Required. */
1640
+ declare const CLOUDFLARE_R2_ACCOUNT_ID_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_ACCOUNT_ID";
1641
+ /** Env var carrying the R2 access key id. Required. */
1642
+ declare const CLOUDFLARE_R2_ACCESS_KEY_ID_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_ACCESS_KEY_ID";
1643
+ /** Env var carrying the R2 secret access key. Required. */
1644
+ declare const CLOUDFLARE_R2_SECRET_ACCESS_KEY_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_SECRET_ACCESS_KEY";
1645
+ /** Env var carrying the R2 bucket name Logpush writes into. Required. */
1646
+ declare const CLOUDFLARE_R2_BUCKET_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_BUCKET";
1647
+ /** Env var carrying the key prefix BEFORE the `{DATE}` Logpush partition. Optional. */
1648
+ declare const CLOUDFLARE_R2_PREFIX_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_PREFIX";
1649
+ /** Env var selecting the Logpush dataset. Optional (default http_requests). */
1650
+ declare const CLOUDFLARE_R2_DATASET_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_DATASET";
1651
+ /** Env var overriding the R2 endpoint host (testing/localstack). Optional. */
1652
+ declare const CLOUDFLARE_R2_ENDPOINT_ENV = "CRUMBTRAIL_CLOUDFLARE_R2_ENDPOINT";
1653
+ /** Logpush datasets this adapter knows how to normalize. */
1654
+ type CloudflareDataset = "http_requests" | "workers_trace_events";
1655
+ /** Presence of these four ⇒ the provider is configured (mirrors ticket clients).
1656
+ * Prefix, dataset, and endpoint have defaults so they are not required. */
1657
+ declare const CLOUDFLARE_AUTH_FIELDS: string[];
1658
+ declare const CLOUDFLARE_DESCRIPTOR: EvidenceSourceDescriptor;
1659
+ interface CloudflareSourceConfig {
1660
+ accountId: string;
1661
+ accessKeyId: string;
1662
+ secretAccessKey: string;
1663
+ bucket: string;
1664
+ /** Key prefix BEFORE the `{DATE}` Logpush partition (no leading slash). */
1665
+ prefix?: string;
1666
+ /** Logpush dataset. Defaults to http_requests. */
1667
+ dataset?: CloudflareDataset;
1668
+ /** Endpoint host override (no trailing slash). Defaults to the R2 endpoint. */
1669
+ endpoint?: string;
1670
+ /** Injectable transport. Defaults to global `fetch`. Tests pass a stub. */
1671
+ fetchImpl?: typeof fetch;
1672
+ }
1673
+ interface CloudflarePlan {
1674
+ /** Case-insensitive substring the request URL must contain, when url keyed. */
1675
+ urlFilter?: string;
1676
+ /** Exact RayID/request id a line must carry, when requestId keyed. */
1677
+ requestIdFilter?: string;
1678
+ /** Join keys actually used to narrow beyond the time window, best-first. */
1679
+ usedKeys: EvidenceJoinKey[];
1680
+ /** Honest gaps: a requested key this dataset cannot filter by. */
1681
+ gaps: EvidenceGap[];
1682
+ }
1683
+ /**
1684
+ * Build the line-level filter plan from `descriptor.joinKeys` ∩ present keys.
1685
+ * R2 has no query API, so filters are applied per NDJSON line after read:
1686
+ * `requestId` → exact match on the line's RayID (both datasets); `url` →
1687
+ * case-insensitive substring on the request URL (http_requests only — Workers
1688
+ * Trace Events carry no URL, so a requested `url` becomes a gap). Any requested
1689
+ * key outside the descriptor (traceId, sessionId, release, user, service) yields
1690
+ * a gap so the bundle states plainly what filtered the result.
1691
+ */
1692
+ declare function buildCloudflarePlan(query: EvidenceQuery, dataset: CloudflareDataset): CloudflarePlan;
1693
+ declare class CloudflareEvidenceSource implements EvidenceSource {
1694
+ readonly descriptor: EvidenceSourceDescriptor;
1695
+ private readonly accessKeyId;
1696
+ private readonly secretAccessKey;
1697
+ private readonly bucket;
1698
+ private readonly prefix;
1699
+ private readonly dataset;
1700
+ private readonly endpoint;
1701
+ private readonly fetchImpl;
1702
+ constructor(config: CloudflareSourceConfig);
1703
+ /** One SigV4-signed GET against R2 (S3 API). The secret lives only in the
1704
+ * signer; `x-amz-content-sha256` (empty-body hash) is signed as S3 requires.
1705
+ * Returns the raw Response so callers can read text() or arrayBuffer(). */
1706
+ private signedGet;
1707
+ /** Build a ListObjectsV2 URL for a key prefix, capped to a single page. */
1708
+ private listUrl;
1709
+ private objectUrl;
1710
+ private listObjects;
1711
+ private readObject;
1712
+ /** Cheap authenticated no-op: ListObjectsV2 on the bucket, max-keys 1. */
1713
+ health(signal?: AbortSignal): Promise<SourceHealth>;
1714
+ fetchEvidence(query: EvidenceQuery, signal?: AbortSignal): Promise<EvidenceSourceResult>;
1715
+ }
1716
+ /** Registry entry: build a Cloudflare source from env when its auth fields are set. */
1717
+ declare const cloudflareEvidenceProvider: EvidenceSourceProvider;
1718
+
1719
+ interface JsonRpcRequest {
1720
+ jsonrpc: "2.0";
1721
+ id?: number | string;
1722
+ method: string;
1723
+ params?: Record<string, unknown>;
1724
+ }
1725
+ interface JsonRpcResponse {
1726
+ jsonrpc: "2.0";
1727
+ id: number | string;
1728
+ result?: unknown;
1729
+ error?: {
1730
+ code: number;
1731
+ message: string;
1732
+ data?: unknown;
1733
+ };
1734
+ }
1735
+ interface McpServerConfig {
1736
+ outputDir: string;
1737
+ /**
1738
+ * Test-only seam: overrides how the git-host client is constructed for
1739
+ * `solveContext`'s intent-inference path. Production code leaves this
1740
+ * unset and builds a `GitHubRestClient` from `CRUMBTRAIL_GITHUB_TOKEN`.
1741
+ */
1742
+ gitHostClientFactory?: (gitHost: {
1743
+ owner: string;
1744
+ repo: string;
1745
+ }) => GitHostClient;
1746
+ /**
1747
+ * Test-only seam: overrides how the ticket connector is constructed for
1748
+ * `solveContext`'s `ticket` input. Production code leaves this unset and
1749
+ * builds a connector from the documented provider env vars.
1750
+ */
1751
+ ticketConnectorFactory?: (provider: TicketProvider) => TicketConnector;
1752
+ /**
1753
+ * Test-only seam: overrides how the on-demand `Reproducer` is constructed
1754
+ * for `solveContext`'s thin-evidence reproduction path. Production code
1755
+ * leaves this unset, which disables reproduction entirely — the gate
1756
+ * short-circuits and no reproducer (not even `NoopReproducer`) is built.
1757
+ */
1758
+ reproducerFactory?: () => Reproducer;
1759
+ /**
1760
+ * Test-only seam: overrides how the client's evidence sources are constructed
1761
+ * for `solveContext`'s adapter phase (sessionless Mode A + blended). Production
1762
+ * code leaves this unset and builds them from env via evidenceSourcesFromEnv().
1763
+ */
1764
+ evidenceSourcesFactory?: () => EvidenceSource[];
1765
+ }
1766
+ declare class McpServer {
1767
+ private outputDir;
1768
+ private bugQueue;
1769
+ private gitHostClientFactory?;
1770
+ private ticketConnectorFactory?;
1771
+ private reproducerFactory?;
1772
+ private evidenceSourcesFactory?;
1773
+ constructor(config: McpServerConfig);
1774
+ /** Evidence sources for solveContext's adapter phase — injected in tests,
1775
+ * built from env in production. */
1776
+ private evidenceSources;
1777
+ start(): void;
1778
+ handleMessage(msg: JsonRpcRequest): Promise<JsonRpcResponse | null>;
1779
+ private callTool;
1780
+ private sessionDir;
1781
+ private isSafeSessionId;
1782
+ private readEvents;
1783
+ /**
1784
+ * Resolve a tool's args to a target directory. A bug is a named window into
1785
+ * a session: if `bugId` is present, gate on its existence and resolve to the
1786
+ * bug's dir; otherwise resolve to the session's dir. Session targets are not
1787
+ * pre-checked here (existence is discovered downstream by the specific tool,
1788
+ * matching pre-seam behavior), so this only errors for a missing bug.
1789
+ */
1790
+ private resolveTarget;
1791
+ /** Shared kind/after/before filtering; per-tool limit/compact stay caller-side. */
1792
+ private filterEvents;
1793
+ /** Shared error-context body for both session and bug tools. */
1794
+ private errorContextFor;
1795
+ /** Shared failed-requests body; notFoundMsg differs per caller. */
1796
+ private failedRequestsFor;
1797
+ private toolListSessions;
1798
+ private sessionMetadataMatches;
1799
+ private toolGetIndex;
1800
+ private toolGetEvents;
1801
+ private toolGetErrorContext;
1802
+ private toolGetFailedRequests;
1803
+ private toolGetLinkedRequestContext;
1804
+ private matchesFullStackGap;
1805
+ private linkedRequestDiagnostics;
1806
+ private fullStackCorrelationStatus;
1807
+ private compactLinkedFullStackRequest;
1808
+ private compactFullStackGap;
1809
+ private compactFrontendEvidence;
1810
+ private compactBackendEvidence;
1811
+ private compactEventRef;
1812
+ private compactCorrelation;
1813
+ private compactFrontendError;
1814
+ private compactBackendError;
1815
+ /** Parses the optional `maxTokens` arg. `{}` when absent; error when invalid. */
1816
+ private maxTokensOf;
1817
+ /**
1818
+ * Shared budgeted-response path: fills `payload[itemsKey]` from `items` in
1819
+ * their given rank order via the one shared fillToBudget helper, then
1820
+ * attaches `tokenEstimate` (of the final serialized payload) and, when
1821
+ * anything was dropped, a structured `dropReport`. `onKept` lets a caller
1822
+ * patch dependent fields (e.g. getWindow's returned/truncated) after the
1823
+ * fill. Deterministic; logs drops to stderr only (stdout carries only
1824
+ * JSON-RPC frames).
1825
+ */
1826
+ private budgetedTextResult;
1827
+ /**
1828
+ * Shared response path for getFixContext and getLatestIssue. Unbudgeted
1829
+ * stays byte-identical to the raw contract; budgeted fills ranked_candidates
1830
+ * in candidate-rank order (refs are candidate ids resolvable via getEvidence).
1831
+ */
1832
+ private fixContextResult;
1833
+ private toolGetFixContext;
1834
+ /**
1835
+ * One-call entry point: resolve the latest finalized session with
1836
+ * error-class evidence via the SAME resolveLatestIssue shared with the
1837
+ * `fix-context --latest` CLI flag, then reuse the getFixContext response
1838
+ * path (including optional maxTokens budgeting — its only input).
1839
+ */
1840
+ private toolGetLatestIssue;
1841
+ private toolGetRegressionContext;
1842
+ private toolSolveContext;
1843
+ private toolListDistinctBugs;
1844
+ private toolGetRecurrence;
1845
+ private toolGetBug;
1846
+ /** Reads the grouped distinct bugs from the finalized hot-plane bundle (llm.json, else bundle.json). */
1847
+ private readDistinctBugs;
1848
+ private recurrenceRollups;
1849
+ private compactRecurrence;
1850
+ private signatureForBug;
1851
+ /**
1852
+ * Recall past issues that rhyme with a session or a free-text description. In
1853
+ * cloud deployments (CRUMBTRAIL_CLOUD_URL + CRUMBTRAIL_API_KEY set) this delegates
1854
+ * to the org-wide semantic index; otherwise it scans the local session store
1855
+ * with a text-overlap + facet analogue so self-hosted users still get recall
1856
+ * without a vector DB.
1857
+ */
1858
+ private toolRecallSimilarIssues;
1859
+ /** Adapt this server's storage readers to the recall engine's injected seam.
1860
+ * Delegates to the shared buildRecallStore so the MCP tool and the inner
1861
+ * /api/solve-context endpoint locate against an identical store. */
1862
+ private recallStore;
1863
+ private isDistinctBugRecord;
1864
+ private firstString;
1865
+ private toolGetSessionManifest;
1866
+ private synthesizeManifest;
1867
+ private toolGetWindow;
1868
+ private toolGetEvidence;
1869
+ private windowCap;
1870
+ private sessionExists;
1871
+ /** Reads the sanitized cold event stream first; falls back to legacy/plain events when zstd is absent. */
1872
+ private readColdEvents;
1873
+ private parseEvents;
1874
+ private readCandidatesJsonl;
1875
+ private readSignatureEntries;
1876
+ private readInteractiveElement;
1877
+ /**
1878
+ * Reads the finalized hot-plane interactive-element identity map (bundle.browserEvidence
1879
+ * .interactiveElements). Prefers llm.json then bundle.json — the same finalized artifacts
1880
+ * listDistinctBugs/getBug read. Values are already redaction-sanitized at bundle build time;
1881
+ * this only ever surfaces those redacted descriptors, never raw masked values.
1882
+ */
1883
+ private readInteractiveElements;
1884
+ private toolResolveSignature;
1885
+ private toolLocateInteractiveElements;
1886
+ private locateLimit;
1887
+ /**
1888
+ * Builds a deterministic, redaction-safe descriptor for one signature by combining the
1889
+ * finalized interactive-element map (label/text, tag, occurrence count, path) with the
1890
+ * cold-plane signature dictionary (first-seen, first event kind). Returns undefined when the
1891
+ * signature is absent from the interactive-element map.
1892
+ */
1893
+ private buildElementDescriptor;
1894
+ /**
1895
+ * Pure descriptor builder over already-read interactive-element and signature-dictionary
1896
+ * arrays. Hoisting the reads out of callers keeps locateInteractiveElements O(n) instead of
1897
+ * re-parsing the bundle/signature files once per element.
1898
+ */
1899
+ private buildElementDescriptorFrom;
1900
+ private affordanceFor;
1901
+ private readJsonRecord;
1902
+ private toolGetStorageSnapshot;
1903
+ private toolGetCookieChanges;
1904
+ private toolGetStorageChanges;
1905
+ private toolGetTranscript;
1906
+ private toolGetFrame;
1907
+ private toolGetFrameById;
1908
+ private toolListBugs;
1909
+ private toolGetBugReport;
1910
+ private toolGetBugEvents;
1911
+ private toolGetBugErrorContext;
1912
+ private toolGetBugFailedRequests;
1913
+ private toolGetBugVoiceTranscript;
1914
+ private toolResolveBug;
1915
+ private toolGetBugLlmContext;
1916
+ private safeGetBug;
1917
+ private safeGetBugLlmContext;
1918
+ }
1919
+
1920
+ /** A database row diff correlated to the primary window. See {@link LlmBundleDbDiff}. */
1921
+ type FixContextDbDiff = LlmBundleDbDiff;
1922
+ type FixContextDbRead = LlmBundleDbRead;
1923
+ type FixContextDbActivity = LlmBundleDbActivity;
1924
+ /**
1925
+ * Versioned, ranked, correlated, LLM-ready "hand it to the model" bundle for a finalized
1926
+ * session. This is the keystone fix-context contract (V2.5).
1927
+ *
1928
+ * The shape is intentionally stable: `primary_window.db_diffs` defaults to an empty array
1929
+ * and `environment` defaults to null so later checkpoints (CP5 DB diffing, CP3 environment
1930
+ * capture) can populate them without breaking the contract.
1931
+ */
1932
+ /**
1933
+ * @stability stable
1934
+ * Version-bump policy follows the same Fork A decision as fusion.v1 — see
1935
+ * wargames/wargames/01-solve-context-wargame-fields.md.
1936
+ */
1937
+ declare const FIX_CONTEXT_SCHEMA_VERSION: "fix-context.v1";
1938
+ interface FixContextSession {
1939
+ id: string;
1940
+ name?: string;
1941
+ app?: string;
1942
+ source?: string;
1943
+ startMs: number;
1944
+ endMs: number;
1945
+ durationMs: number;
1946
+ }
1947
+ interface FixContextReproHint {
1948
+ title?: string;
1949
+ detector?: string;
1950
+ severity?: EvidenceCandidate["severity"];
1951
+ route?: string;
1952
+ target?: EvidenceCandidate["anchor"]["target"];
1953
+ elementLabel?: string;
1954
+ errorCode?: string;
1955
+ message?: string;
1956
+ requestId?: string;
1957
+ offsetMs?: number;
1958
+ }
1959
+ interface FixContextPrimaryWindow {
1960
+ frontend: {
1961
+ window: {
1962
+ start: number;
1963
+ end: number;
1964
+ windowId: string;
1965
+ } | null;
1966
+ anchor: EvidenceCandidate["anchor"] | null;
1967
+ requests: LlmBundleFrontendRequestEvidenceSummary[];
1968
+ };
1969
+ backend: {
1970
+ requests: LlmBundleBackendRequestEvidenceSummary[];
1971
+ };
1972
+ /**
1973
+ * Database row diffs correlated to the primary window (CP5 DB diffing). Empty when the session
1974
+ * captured no `db.diff` events in the window. Consumers MUST treat `[]` as "no DB evidence".
1975
+ */
1976
+ db_diffs: FixContextDbDiff[];
1977
+ /**
1978
+ * Database rows read in the primary window (`db.read`, pre-state capture). Empty when read
1979
+ * capture is disabled or no reads matched the primary request/window.
1980
+ */
1981
+ db_reads: FixContextDbRead[];
1982
+ /**
1983
+ * OTel DB span activity in the primary window. These are statements/operations only, never
1984
+ * before/after row diffs.
1985
+ */
1986
+ db_activity: FixContextDbActivity[];
1987
+ }
1988
+ /** One downstream symptom of the primary root cause, resolved from `ranked_candidates`. */
1989
+ interface FixContextCausalSymptom {
1990
+ id: string;
1991
+ detector: string;
1992
+ title: string;
1993
+ attributionConfidence?: CausalConfidence;
1994
+ }
1995
+ /**
1996
+ * The primary root-cause → symptom chain, derived ONLY from causal fields already present on the
1997
+ * ranked candidates (CP3). `null` when the primary candidate is isolated or has no attributed
1998
+ * symptoms. Purely a projection — no attribution is recomputed here.
1999
+ */
2000
+ interface FixContextCausalChain {
2001
+ root: {
2002
+ id: string;
2003
+ detector: string;
2004
+ title: string;
2005
+ };
2006
+ symptoms: FixContextCausalSymptom[];
2007
+ }
2008
+ interface FixContext {
2009
+ schemaVersion: typeof FIX_CONTEXT_SCHEMA_VERSION;
2010
+ session: FixContextSession;
2011
+ ranked_candidates: EvidenceCandidate[];
2012
+ primary_window: FixContextPrimaryWindow;
2013
+ /**
2014
+ * Account/environment state snapshot. Defaults to `null`; CP3 (environment capture)
2015
+ * populates this. Consumers MUST treat null as "not captured".
2016
+ */
2017
+ environment: Record<string, unknown> | null;
2018
+ /**
2019
+ * Primary root-cause → symptom chain projected from the ranked candidates' causal fields (CP4).
2020
+ * `null` when the top candidate is isolated or attributes no symptoms. Consumers MUST treat null
2021
+ * as "no causal structure surfaced".
2022
+ */
2023
+ causal_chain: FixContextCausalChain | null;
2024
+ repro_hint: FixContextReproHint | null;
2025
+ }
2026
+ interface BuildFixContextOptions {
2027
+ /** Base sessions directory used to resolve a bare session id to a directory. */
2028
+ outputDir?: string;
2029
+ }
2030
+ declare class FixContextError extends Error {
2031
+ readonly code: "session-not-found";
2032
+ constructor(code: "session-not-found", message: string);
2033
+ }
2034
+ /**
2035
+ * Builds the fix-context contract for a finalized session by reading hot-plane artifacts
2036
+ * (index.json, candidates.jsonl, llm.json). It never reads raw NDJSON at query time.
2037
+ */
2038
+ declare function buildFixContext(sessionDirOrId: string, opts?: BuildFixContextOptions): FixContext;
2039
+
2040
+ type HeaderValue = string | number | readonly string[] | undefined;
2041
+ type BackendRequestHeaders = Record<string, HeaderValue>;
2042
+ interface BackendRequestEventInput {
2043
+ method?: string;
2044
+ url?: string;
2045
+ originalUrl?: string;
2046
+ path?: string;
2047
+ route?: string;
2048
+ headers?: BackendRequestHeaders;
2049
+ sessionId?: string;
2050
+ requestId?: string;
2051
+ sessionStartedAt?: number | Date;
2052
+ now?: number;
2053
+ }
2054
+
2055
+ /**
2056
+ * Default sensitive column names whose values are always dropped before a `db.diff` event rests.
2057
+ * Hosts can extend this list per shim via `redactColumns`. Matching is name-based and
2058
+ * normalization-aware (case-insensitive; `apiKey`/`api_key`/`APIKEY` all match `api_key`).
2059
+ */
2060
+ declare const DEFAULT_SENSITIVE_DB_COLUMNS: readonly ["password", "token", "secret", "api_key", "ssn"];
2061
+
2062
+ interface BuildDbDiffEventInput {
2063
+ /** Engine that produced the mutation. Defaults to `"postgres"` for back-compat. */
2064
+ engine?: DbEngine;
2065
+ op: DbDiffOp;
2066
+ table: string;
2067
+ /** Primary-key column→value map, or `null` when it could not be resolved. */
2068
+ pk: Record<string, unknown> | null;
2069
+ /** Post-image of the affected row (insert/update). */
2070
+ after?: Record<string, unknown>;
2071
+ /** Pre-image of the affected row (deletes, or updates with before-capture enabled). */
2072
+ before?: Record<string, unknown>;
2073
+ /** Set only on image-less statement-level fallback events (pk `null`, no after/before). */
2074
+ rowCount?: number;
2075
+ /** Correlation id; MUST equal the active request's traceId/requestId. */
2076
+ requestId: string;
2077
+ sessionId?: string;
2078
+ /** Extra sensitive column names to drop, on top of {@link DEFAULT_SENSITIVE_DB_COLUMNS}. */
2079
+ redactColumns?: readonly string[];
2080
+ now?: number;
2081
+ sessionStartedAt?: number | Date;
2082
+ }
2083
+ /**
2084
+ * Builds the canonical `k:'db.diff'` event for one changed row. Sensitive columns are dropped from
2085
+ * `after`/`before`/`pk` via the shared redaction policy BEFORE the event is returned, so secret
2086
+ * values never rest in the event. The event carries `requestId` so it lands in the same evidence
2087
+ * window as the `backend.req.*` and front-end network events of the request that caused the write.
2088
+ */
2089
+ declare function buildDbDiffEvent(input: BuildDbDiffEventInput): BugEvent;
2090
+
2091
+ interface BuildDbReadEventInput {
2092
+ /** Engine that produced the read. Defaults to `"postgres"` for back-compat. */
2093
+ engine?: DbEngine;
2094
+ table: string;
2095
+ pk: Record<string, unknown> | null;
2096
+ row: Record<string, unknown>;
2097
+ requestId: string;
2098
+ sessionId?: string;
2099
+ redactColumns?: readonly string[];
2100
+ now?: number;
2101
+ sessionStartedAt?: number | Date;
2102
+ }
2103
+ interface BuildDbReadBulkEventInput {
2104
+ /** Engine that produced the read. Defaults to `"postgres"` for back-compat. */
2105
+ engine?: DbEngine;
2106
+ table: string;
2107
+ requestId: string;
2108
+ rowCount: number;
2109
+ emittedRows: number;
2110
+ samplePks: Array<Record<string, unknown>>;
2111
+ sessionId?: string;
2112
+ now?: number;
2113
+ sessionStartedAt?: number | Date;
2114
+ }
2115
+ declare function buildDbReadEvent(input: BuildDbReadEventInput): BugEvent;
2116
+ declare function buildDbReadBulkEvent(input: BuildDbReadBulkEventInput): BugEvent;
2117
+
2118
+ /**
2119
+ * Options accepted by every `instrument*` adapter. Every field is engine-agnostic; the Postgres
2120
+ * shim keeps `InstrumentPgClientOptions` as a back-compat alias of this type.
2121
+ */
2122
+ interface InstrumentDbClientOptions {
2123
+ /** Active request correlation id (equals the request's traceId). */
2124
+ requestId?: string;
2125
+ /** Lazily resolve the active request id (e.g. from AsyncLocalStorage); wins when `requestId` is absent. */
2126
+ getRequestId?: () => string | undefined;
2127
+ sessionId?: string;
2128
+ /** Sink for emitted `db.diff` events (e.g. forward to `sendBackendEvent`). */
2129
+ emit: (event: BugEvent) => void;
2130
+ /** When true, capture the pre-image of UPDATE rows via a SELECT-by-WHERE before mutating. */
2131
+ captureBefore?: boolean;
2132
+ /** When true, capture capped/redacted SELECT result rows as pre-state read evidence. Default off. */
2133
+ captureReads?: boolean;
2134
+ /** Extra sensitive column names dropped on top of the defaults. */
2135
+ redactColumns?: readonly string[];
2136
+ /** Primary-key columns per table; defaults to `['id']` for unlisted tables. */
2137
+ pkColumns?: Record<string, readonly string[]>;
2138
+ /** Maximum per-row `db.diff` events to emit for one statement before adding a bulk summary. */
2139
+ maxRowsPerStatement?: number;
2140
+ /** Maximum per-row `db.read` events to emit for one SELECT before adding a bulk summary. */
2141
+ maxReadRowsPerStatement?: number;
2142
+ /** Maximum per-row `db.read` events to emit for one request scope. */
2143
+ maxReadRowsPerRequest?: number;
2144
+ now?: () => number;
2145
+ sessionStartedAt?: number | Date;
2146
+ }
2147
+
2148
+ /**
2149
+ * Dialect-aware SQL statement parsing shared by every DB adapter. The parsers are deliberately
2150
+ * conservative: they recognize single-table INSERT/UPDATE/DELETE and simple SELECT-FROM shapes
2151
+ * across Postgres, MySQL, MSSQL, and SQLite quoting/keyword variants, and return `undefined` for
2152
+ * anything they do not understand so callers degrade to "no diff" rather than mis-instrument.
2153
+ */
2154
+ interface ParsedMutation {
2155
+ op: DbDiffOp;
2156
+ table: string;
2157
+ whereClause?: string;
2158
+ }
2159
+ interface ParsedRead {
2160
+ table: string;
2161
+ }
2162
+ /** Parses op + table (+ WHERE clause) from a SQL statement. Returns undefined for non-mutations. */
2163
+ declare function parseMutation(sql: string): ParsedMutation | undefined;
2164
+ /** Parses the first table from a simple SELECT statement. Returns undefined for non-reads. */
2165
+ declare function parseRead(sql: string): ParsedRead | undefined;
2166
+
2167
+ /**
2168
+ * Minimal duck-typed view of a `pg` Client/Pool. We never import `pg` at module top-level — the
2169
+ * host injects its own client/pool, so `pg` stays an optional peer and tests use a fake client.
2170
+ */
2171
+ interface DuckTypedPgQueryResult {
2172
+ rows?: unknown[];
2173
+ rowCount?: number | null;
2174
+ command?: string;
2175
+ }
2176
+ interface DuckTypedPgClient {
2177
+ query(text: unknown, params?: unknown): Promise<DuckTypedPgQueryResult>;
2178
+ }
2179
+ /** Back-compat alias: the Postgres shim shares the engine-agnostic option shape. */
2180
+ type InstrumentPgClientOptions = InstrumentDbClientOptions;
2181
+ /**
2182
+ * Wraps a duck-typed `pg` client/pool so INSERT/UPDATE/DELETE statements executed within a request
2183
+ * scope record a `db.diff` event (op, table, primary key, after-image; before-image behind
2184
+ * `captureBefore`). The shim appends `RETURNING *` when absent to read the after-image, and reads
2185
+ * the result rows otherwise. Only the promise-returning `query(text, params)` form is instrumented;
2186
+ * config-object and callback forms pass straight through. Engine is Postgres only; the builder is
2187
+ * driver-agnostic so other engines can slot in later.
2188
+ *
2189
+ * Limitations: trigger/cascade side effects and rows changed by other tables are not captured; the
2190
+ * pre-image SELECT for `captureBefore` reuses the statement's WHERE clause + params, so it supports
2191
+ * single-table UPDATEs (not CTEs, joins, or sub-selects).
2192
+ */
2193
+ declare function instrumentPgClient<T extends DuckTypedPgClient>(client: T, options: InstrumentPgClientOptions): T;
2194
+
2195
+ /**
2196
+ * Minimal duck-typed view of a `mysql2/promise` connection/pool. We never import `mysql2` at module
2197
+ * top-level — the host injects its own client, so `mysql2` stays an optional peer and tests use a
2198
+ * fake client. Both `query` and `execute` resolve `[rowsOrResultHeader, fields]`.
2199
+ */
2200
+ interface DuckTypedMysqlClient {
2201
+ query(sql: unknown, values?: unknown): Promise<unknown>;
2202
+ execute?(sql: unknown, values?: unknown): Promise<unknown>;
2203
+ }
2204
+ /** The mysql2 result-set header duck shape for a mutation (INSERT/UPDATE/DELETE). */
2205
+ interface DuckTypedMysqlResultHeader {
2206
+ affectedRows: number;
2207
+ insertId?: number;
2208
+ }
2209
+ /**
2210
+ * Wraps a duck-typed `mysql2/promise` client/pool so INSERT/UPDATE/DELETE statements executed within
2211
+ * a request scope record a `db.diff` event (op, table, primary key, after-image; before-image behind
2212
+ * `captureBefore`). MySQL has no `RETURNING`, so images come from best-effort extra SELECTs:
2213
+ *
2214
+ * - INSERT: a single-row auto-increment insert (`affectedRows === 1`, positive `insertId`, single-column
2215
+ * pk) is re-read by `insertId`; anything else (multi-row, missing insertId, composite pk) degrades to
2216
+ * an image-less `db.diff` carrying `rowCount`.
2217
+ * - UPDATE: a pre-SELECT (WHERE + trailing params) captures pks (and before-images when `captureBefore`),
2218
+ * then a post-SELECT by those pks captures after-images.
2219
+ * - DELETE: a pre-SELECT (WHERE + trailing params) captures the before-images.
2220
+ *
2221
+ * Both the promise-returning `query(sql, values)` and `execute(sql, values)` forms are instrumented
2222
+ * (identically); non-string sql / config-object forms pass straight through. The host statement is
2223
+ * never altered and runs exactly once — every capture SELECT and emit is wrapped so instrumentation
2224
+ * can never fail, double-execute, or alter the host query; the host result is returned unchanged.
2225
+ *
2226
+ * Limitations: trigger/cascade side effects and rows changed by other tables are not captured; the
2227
+ * WHERE-based pre-image SELECT supports single-table UPDATE/DELETE (not CTEs, joins, or sub-selects).
2228
+ */
2229
+ declare function instrumentMysqlClient<T extends DuckTypedMysqlClient>(client: T, options: InstrumentDbClientOptions): T;
2230
+
2231
+ /**
2232
+ * Minimal duck-typed view of an `mssql`-package result. We never import `mssql` at module top level —
2233
+ * the host injects its own pool, so `mssql` stays an optional peer and tests use a fake pool. Only the
2234
+ * fields the shim reads are typed; every other property (`output`, `returnValue`, …) rides through the
2235
+ * host-visible result untouched.
2236
+ */
2237
+ interface DuckTypedMssqlResult {
2238
+ recordset?: unknown[];
2239
+ recordsets?: unknown[][];
2240
+ rowsAffected?: number[];
2241
+ [key: string]: unknown;
2242
+ }
2243
+ /** Duck-typed view of an `mssql` Request (`pool.request()`), recording `input()` and running `query()`. */
2244
+ interface DuckTypedMssqlRequest {
2245
+ input(name: string, ...rest: unknown[]): unknown;
2246
+ query(text: unknown): Promise<DuckTypedMssqlResult>;
2247
+ [key: string]: unknown;
2248
+ }
2249
+ /** Duck-typed view of an `mssql` ConnectionPool. `query` (the convenience single-string form) is optional. */
2250
+ interface DuckTypedMssqlPool {
2251
+ request(): DuckTypedMssqlRequest;
2252
+ query?(text: unknown, ...rest: unknown[]): Promise<DuckTypedMssqlResult>;
2253
+ }
2254
+ /**
2255
+ * Wraps a duck-typed `mssql` pool so INSERT/UPDATE/DELETE statements executed within a request scope
2256
+ * record a `db.diff` event (op, table, primary key, after-image; before-image behind `captureBefore`).
2257
+ * After-images are read by injecting an `OUTPUT INSERTED.*` / `OUTPUT DELETED.*` clause into the
2258
+ * statement (no extra round trip); the injected rows are consumed for diffs and then stripped from the
2259
+ * host-visible result, so the caller sees exactly what the un-instrumented statement would have
2260
+ * returned. SELECTs are optionally captured as read evidence (`captureReads`).
2261
+ *
2262
+ * Never-fail guarantees:
2263
+ * - Parse/correlation failures, missing request scope, unparseable SQL → the original statement runs
2264
+ * untouched.
2265
+ * - A pre-injection safety gate (`scanStatementSafety`) refuses to edit any statement we cannot
2266
+ * confidently tokenize as a single statement — multi-statement batches, unterminated
2267
+ * strings/comments, and quote-bearing comments run the ORIGINAL untouched (image-less diff), so the
2268
+ * host never receives corrupt T-SQL or loses a caller recordset.
2269
+ * - The host mutation executes exactly once. The only re-run is the compile-class fallback (errors
2270
+ * 334 / 156 / 102), which fail at COMPILE time before any rows change, so re-running the ORIGINAL
2271
+ * text on a fresh request with replayed inputs is safe. Every other error propagates as-is.
2272
+ * - All capture/emit work is wrapped; a failure degrades to fewer/no diffs, never to a failed host
2273
+ * query. When a mutation clearly ran but no images are obtainable, one image-less `db.diff`
2274
+ * (`pk: null`, `rowCount`) still records the write.
2275
+ */
2276
+ declare function instrumentMssqlPool<T extends DuckTypedMssqlPool>(pool: T, options: InstrumentDbClientOptions): T;
2277
+
2278
+ /**
2279
+ * Minimal duck-typed view of a better-sqlite3 / `node:sqlite` statement. The host injects the real
2280
+ * database, so the sqlite driver stays an optional peer and tests use a synchronous fake. `run`
2281
+ * returns the mutation summary (`changes` + `lastInsertRowid`); `all`/`get` read rows back.
2282
+ */
2283
+ interface DuckTypedSqliteStatement {
2284
+ run(...params: unknown[]): unknown;
2285
+ all(...params: unknown[]): unknown;
2286
+ get(...params: unknown[]): unknown;
2287
+ [key: string]: unknown;
2288
+ }
2289
+ interface DuckTypedSqliteRunResult {
2290
+ changes: number;
2291
+ lastInsertRowid: number | bigint;
2292
+ }
2293
+ /** Minimal duck-typed view of a better-sqlite3 / `node:sqlite` database: `prepare(sql)` → statement. */
2294
+ interface DuckTypedSqliteDatabase {
2295
+ prepare(sql: unknown, ...rest: unknown[]): DuckTypedSqliteStatement;
2296
+ [key: string]: unknown;
2297
+ }
2298
+ /**
2299
+ * Wraps a duck-typed better-sqlite3 / `node:sqlite` database so INSERT/UPDATE/DELETE statements
2300
+ * executed within a request scope record a `db.diff` event (op, table, primary key, after-image;
2301
+ * before-image behind `captureBefore`), and SELECTs optionally record `db.read` evidence. Everything
2302
+ * is fully synchronous — the sync driver's `run`/`all` return values are observed inline, so events
2303
+ * are visible the instant the host call returns.
2304
+ *
2305
+ * The database is wrapped with a Proxy that intercepts `prepare`; the returned statement is itself
2306
+ * proxied so `run` (mutations) and `all` (reads, when `captureReads`) are instrumented and every
2307
+ * other member (`get`, `iterate`, `pluck`, …) binds straight through. Non-string or unparseable SQL
2308
+ * yields the real statement untouched.
2309
+ *
2310
+ * After-images come from post-SELECTs (INSERT by `rowid`, UPDATE by the pks captured in a pre-SELECT)
2311
+ * — the host's SQL text is never rewritten, so `run()` returns exactly what the real driver produced.
2312
+ * Every capture step is wrapped: a failing pre/post-SELECT, missing rowid, WITHOUT-ROWID table, or
2313
+ * emit error degrades to fewer/no images (image-less fallback carrying `rowCount`), never a failed
2314
+ * host query.
2315
+ *
2316
+ * Limitations: trigger/cascade side effects and rows changed via other tables are not captured; the
2317
+ * pre-image SELECT reuses the statement's WHERE clause + params, so it supports single-table
2318
+ * UPDATE/DELETE (not CTEs, joins, or sub-selects).
2319
+ */
2320
+ declare function instrumentSqliteDatabase<T extends DuckTypedSqliteDatabase>(db: T, options: InstrumentDbClientOptions): T;
2321
+
2322
+ interface DbRequestContext {
2323
+ requestId: string;
2324
+ sessionId?: string;
2325
+ }
2326
+ /**
2327
+ * Resolves the `db.diff` request scope from the SAME inputs `backend.req.*` events use — the
2328
+ * `X-Crumbtrail-Request-Id` / `X-Crumbtrail-Session-Id` headers (where the request id already equals
2329
+ * the W3C trace id) or explicit options. This guarantees a `db.diff` produced inside a request
2330
+ * shares the request's requestId, instead of inventing a parallel correlation scheme.
2331
+ */
2332
+ declare function resolveDbRequestContext(input: BackendRequestEventInput): DbRequestContext;
2333
+
2334
+ /**
2335
+ * Concise, hot-plane-only summary of a finalized session. `crumbtrail-server inspect <session>` reads
2336
+ * the session manifest (manifest.json) when present and otherwise falls back to index.json. It
2337
+ * never reads raw events.ndjson — counts come from the pre-computed index/manifest.
2338
+ */
2339
+ interface SessionInspectionArtifact {
2340
+ name: string;
2341
+ bytes: number;
2342
+ }
2343
+ interface SessionInspection {
2344
+ id: string;
2345
+ durationMs: number;
2346
+ eventCount: number;
2347
+ errorCount: number;
2348
+ failedRequestCount: number;
2349
+ candidateCount: number;
2350
+ truncated: boolean;
2351
+ /** Which hot-plane artifact the summary was primarily derived from. */
2352
+ source: "manifest" | "index";
2353
+ /**
2354
+ * Earliest error-class evidence timestamp stamped into llm.json by bundle build.
2355
+ * Omitted when the bundle carries no latency self-measurement (or llm.json is absent).
2356
+ */
2357
+ firstErrorEventAt?: number;
2358
+ /** Self-measured detect-to-bundle latency (ms) stamped into llm.json. Omitted when absent. */
2359
+ detectToBundleMs?: number;
2360
+ artifacts: SessionInspectionArtifact[];
2361
+ }
2362
+ interface InspectSessionOptions {
2363
+ /** Base sessions directory used to resolve a bare session id to a directory. */
2364
+ outputDir?: string;
2365
+ }
2366
+ declare class InspectError extends Error {
2367
+ readonly code: "session-not-found";
2368
+ constructor(code: "session-not-found", message: string);
2369
+ }
2370
+ declare function inspectSession(sessionDirOrId: string, opts?: InspectSessionOptions): SessionInspection;
2371
+ declare function formatInspection(inspection: SessionInspection): string;
2372
+
2373
+ /**
2374
+ * Resolve the crumbtrail-node package version by walking up from this module to the nearest
2375
+ * package.json named `crumbtrail-node`. The same layout holds in dev/test (src/version.ts →
2376
+ * ../package.json) and in the published build (dist/cli.cjs → ../package.json), so no
2377
+ * build-time baking is required.
2378
+ */
2379
+ declare function readPackageVersion(): string;
2380
+
2381
+ type ProviderId = "datadog" | "otel" | "sentry" | "grafana" | "splunk";
2382
+ interface ProviderRecipe {
2383
+ id: ProviderId;
2384
+ title: string;
2385
+ docFile: string;
2386
+ description: string;
2387
+ config: string;
2388
+ notes: string[];
2389
+ }
2390
+ declare const PROVIDER_RECIPES: ProviderRecipe[];
2391
+ declare const PROVIDER_IDS: ProviderId[];
2392
+ declare function isProviderId(value: string | undefined): value is ProviderId;
2393
+ declare function getProviderRecipe(provider: ProviderId): ProviderRecipe;
2394
+ declare function renderProviderConfig(provider: ProviderId, endpoint?: string): string;
2395
+ declare function renderProviderCliOutput(provider: ProviderId, endpoint?: string): string;
2396
+ declare function renderProviderDoc(provider: ProviderId, endpoint?: string): string;
2397
+ declare function renderProviderReadme(): string;
2398
+
2399
+ type BackendIntakeWarningKind = "missing-session" | "missing-fetch" | "fetch-rejected" | "http-error" | "malformed-response";
2400
+ interface BackendIntakeWarning {
2401
+ kind: BackendIntakeWarningKind;
2402
+ message: string;
2403
+ status?: number;
2404
+ sessionId?: string;
2405
+ requestId?: string;
2406
+ eventKind?: string;
2407
+ }
2408
+ type FetchLike$1 = (input: string | URL, init?: FetchInitLike) => Promise<ResponseLike>;
2409
+ type HeadersInitLike = Record<string, string>;
2410
+ interface FetchInitLike {
2411
+ method?: string;
2412
+ headers?: HeadersInitLike;
2413
+ body?: string;
2414
+ signal?: AbortSignal;
2415
+ }
2416
+ interface ResponseLike {
2417
+ ok: boolean;
2418
+ status: number;
2419
+ text?: () => Promise<string>;
2420
+ json?: () => Promise<unknown>;
2421
+ }
2422
+ interface SendBackendEventOptions {
2423
+ event: BugEvent;
2424
+ sessionId?: string;
2425
+ endpoint?: string;
2426
+ authToken?: string;
2427
+ fetch?: FetchLike$1;
2428
+ signal?: AbortSignal;
2429
+ onWarning?: (warning: BackendIntakeWarning) => void;
2430
+ }
2431
+ declare function sendBackendEvent(options: SendBackendEventOptions): Promise<void>;
2432
+
2433
+ type CrumbtrailExpressNext = (error?: unknown) => void;
2434
+ type CrumbtrailExpressErrorNext = (error: unknown) => void;
2435
+ interface CrumbtrailExpressRequest {
2436
+ method?: string;
2437
+ url?: string;
2438
+ originalUrl?: string;
2439
+ path?: string;
2440
+ route?: string | {
2441
+ path?: unknown;
2442
+ };
2443
+ headers?: BackendRequestHeaders;
2444
+ }
2445
+ interface CrumbtrailExpressResponse {
2446
+ statusCode?: number;
2447
+ once?: (event: "finish", listener: () => void) => unknown;
2448
+ }
2449
+ type CrumbtrailExpressMiddleware = (req: CrumbtrailExpressRequest, res: CrumbtrailExpressResponse, next: CrumbtrailExpressNext) => void;
2450
+ type CrumbtrailExpressErrorMiddleware = (error: unknown, req: CrumbtrailExpressRequest, res: CrumbtrailExpressResponse, next: CrumbtrailExpressErrorNext) => void;
2451
+ type RequestValueResolver = string | undefined | ((req: CrumbtrailExpressRequest) => string | undefined);
2452
+ type SessionStartedAtResolver = number | Date | undefined | ((req: CrumbtrailExpressRequest) => number | Date | undefined);
2453
+ type NowResolver = () => number;
2454
+ type FetchLike = Parameters<typeof sendBackendEvent>[0]["fetch"];
2455
+ interface CrumbtrailExpressOptions {
2456
+ sessionId?: RequestValueResolver;
2457
+ requestId?: RequestValueResolver;
2458
+ endpoint?: string;
2459
+ authToken?: string;
2460
+ fetch?: FetchLike;
2461
+ signal?: AbortSignal;
2462
+ sessionStartedAt?: SessionStartedAtResolver;
2463
+ now?: NowResolver;
2464
+ onWarning?: (warning: BackendIntakeWarning) => void;
2465
+ }
2466
+ declare function createCrumbtrailExpressMiddleware(options?: CrumbtrailExpressOptions): CrumbtrailExpressMiddleware;
2467
+ declare function createCrumbtrailExpressErrorMiddleware(options?: CrumbtrailExpressOptions): CrumbtrailExpressErrorMiddleware;
2468
+
2469
+ interface HeadlessSessionOptions {
2470
+ endpoint: string;
2471
+ sessionId: string;
2472
+ metadata?: Record<string, unknown>;
2473
+ authToken?: string;
2474
+ fetchImpl?: typeof fetch;
2475
+ }
2476
+ interface HeadlessSession {
2477
+ sessionId: string;
2478
+ record(events: BugEvent | BugEvent[]): Promise<void>;
2479
+ end(): Promise<Record<string, unknown>>;
2480
+ }
2481
+ declare function startHeadlessSession(options: HeadlessSessionOptions): Promise<HeadlessSession>;
2482
+
2483
+ declare const SESSION_COMPARE_SCHEMA_VERSION: "session-compare.v1";
2484
+ type ComparisonVerdict = "regression" | "clean";
2485
+ type ComparisonConfidence = "high" | "medium" | "low";
2486
+ interface SessionComparison {
2487
+ schemaVersion: typeof SESSION_COMPARE_SCHEMA_VERSION;
2488
+ verdict: ComparisonVerdict;
2489
+ confidence: ComparisonConfidence;
2490
+ a: SessionRef;
2491
+ b: SessionRef;
2492
+ alignment: {
2493
+ matchedSteps: number;
2494
+ unmatchedA: number;
2495
+ unmatchedB: number;
2496
+ };
2497
+ divergences: Divergence[];
2498
+ noise: {
2499
+ suppressedCount: number;
2500
+ rules: string[];
2501
+ };
2502
+ /** Neutral, complete projection of divergences. Consumed by cloud fusion. */
2503
+ evidence: EvidenceItem[];
2504
+ /** Intent inference. Empty until the git-host connector (slice 2) lands. */
2505
+ intent: IntentSignal[];
2506
+ }
2507
+ interface SessionRef {
2508
+ sessionId: string;
2509
+ release?: string;
2510
+ build?: string;
2511
+ }
2512
+ interface Divergence {
2513
+ plane: "flow" | "network" | "db" | "env";
2514
+ kind: string;
2515
+ sig?: string;
2516
+ requestId?: string;
2517
+ table?: string;
2518
+ pk?: Record<string, unknown>;
2519
+ before: unknown;
2520
+ after: unknown;
2521
+ brief: string;
2522
+ }
2523
+ interface CompareOptions {
2524
+ alignmentWindow?: number;
2525
+ disableNoiseRules?: string[];
2526
+ }
2527
+
2528
+ declare class CompareError extends Error {
2529
+ constructor(message: string);
2530
+ }
2531
+ declare function compareSessions(aDir: string, bDir: string, options?: CompareOptions): Promise<SessionComparison>;
2532
+
2533
+ declare function renderCompareReport(comparison: SessionComparison): string;
2534
+ declare function formatComparisonSummary(comparison: SessionComparison): string;
2535
+
2536
+ declare const REGRESSION_CONTEXT_SCHEMA_VERSION: "regression-context.v1";
2537
+ interface RegressionContext {
2538
+ schemaVersion: typeof REGRESSION_CONTEXT_SCHEMA_VERSION;
2539
+ comparison: SessionComparison;
2540
+ divergent_interaction: {
2541
+ sig: string;
2542
+ label: string;
2543
+ path: string;
2544
+ } | null;
2545
+ causal_window: {
2546
+ requestIds: string[];
2547
+ t0: number;
2548
+ t1: number;
2549
+ hints: string[];
2550
+ } | null;
2551
+ db_rows: Array<{
2552
+ table: string;
2553
+ pk: Record<string, unknown>;
2554
+ before: unknown;
2555
+ after: unknown;
2556
+ }>;
2557
+ repro_hint: string;
2558
+ }
2559
+ declare function buildRegressionContext(comparison: SessionComparison, bDir: string): RegressionContext;
2560
+
2561
+ /** The subset of the locate envelope this builder reads. `outcome` is the only
2562
+ * signal that flips the comment shape; `confidence` is display-only. */
2563
+ interface AdvisoryCommentMatch {
2564
+ outcome: "matched" | "inconclusive";
2565
+ confidence: number;
2566
+ reasons?: string[];
2567
+ }
2568
+ /** One evidence gap surfaced from the bundle (mirrors core's EvidenceGap). */
2569
+ interface AdvisoryCommentGap {
2570
+ lane?: string;
2571
+ reason: string;
2572
+ suggestion?: string;
2573
+ }
2574
+ /**
2575
+ * Correlation keys carried INTO the ticket so the reader can line the matched
2576
+ * incident up against their own logs/traces. Every value here must come from the
2577
+ * located evidence — NEVER fabricated. Rendered only in the matched variant.
2578
+ */
2579
+ interface AdvisoryCommentCorrelation {
2580
+ /** The located session id (match.sessionId). */
2581
+ sessionId?: string;
2582
+ /** Distinct request/trace ids pulled from the bundle's evidence refs. */
2583
+ requestIds?: string[];
2584
+ }
2585
+ interface BuildAdvisoryCommentInput {
2586
+ match: AdvisoryCommentMatch;
2587
+ /** Public link to the persisted bundle (`/api/bundles/:id`). Always rendered. */
2588
+ bundleUrl: string;
2589
+ gaps?: AdvisoryCommentGap[];
2590
+ /** Correlation keys from the located evidence (matched variant only). */
2591
+ correlation?: AdvisoryCommentCorrelation;
2592
+ }
2593
+ /** A minimally-typed ADF node. ADF is an open tree; we only build the handful of
2594
+ * node kinds we need (doc/paragraph/text/link/bulletList/listItem). */
2595
+ interface AdfNode {
2596
+ type: string;
2597
+ [key: string]: unknown;
2598
+ }
2599
+ interface AdfDoc {
2600
+ version: 1;
2601
+ type: "doc";
2602
+ content: AdfNode[];
2603
+ }
2604
+ /**
2605
+ * Build the advisory ADF comment. Two shapes, chosen by `match.outcome`:
2606
+ *
2607
+ * - matched: names that a candidate incident was located, shows the rounded
2608
+ * confidence as an advisory percentage, lists the match reasons in plain
2609
+ * language (if any), carries the correlation keys (located session + up to a
2610
+ * few request/trace ids, when the evidence has them) onto the ticket, and
2611
+ * links the full evidence bundle.
2612
+ * - inconclusive: states honestly that no recorded incident matched, lists the
2613
+ * evidence gaps (if any) so the reader knows what is missing, and still links
2614
+ * the (empty) bundle. It fabricates no match and reports no percentage.
2615
+ *
2616
+ * Pure and side-effect free — unit-testable in isolation.
2617
+ */
2618
+ declare function buildAdvisoryComment(input: BuildAdvisoryCommentInput): AdfDoc;
2619
+
2620
+ declare const REPLAY_RESULT_SCHEMA_VERSION: "replay-result.v1";
2621
+ type StepResolution = 'exact' | 'role-label' | 'structural' | 'failed';
2622
+ interface ReplayStepResult {
2623
+ index: number;
2624
+ sig: string;
2625
+ action: string;
2626
+ resolution: StepResolution;
2627
+ durationMs: number;
2628
+ }
2629
+ interface ReplayDivergence {
2630
+ index: number;
2631
+ sig: string;
2632
+ reason: string;
2633
+ }
2634
+ interface ReplayResult {
2635
+ schemaVersion: typeof REPLAY_RESULT_SCHEMA_VERSION;
2636
+ sourceSessionId: string;
2637
+ actuatedSessionId: string;
2638
+ steps: ReplayStepResult[];
2639
+ divergences: ReplayDivergence[];
2640
+ completed: boolean;
2641
+ }
2642
+ declare function buildReplayResult(input: {
2643
+ sourceSessionId: string;
2644
+ actuatedSessionId: string;
2645
+ steps: ReplayStepResult[];
2646
+ divergences: ReplayDivergence[];
2647
+ completed: boolean;
2648
+ }): ReplayResult;
2649
+ declare function parseReplayResult(raw: unknown): ReplayResult;
2650
+ declare function writeReplayResult(filePath: string, result: ReplayResult): void;
2651
+
2652
+ /**
2653
+ * Canonical event kind emitted for an auto-captured backend error (crash or
2654
+ * console.error). It is deliberately NOT `backend.req.error` (that kind is
2655
+ * request-scoped and joins on a requestId this hook never has) and NOT
2656
+ * `backend.error` (that literal is a causal-graph NODE kind, not an event kind —
2657
+ * reusing it would collide). This request-less kind carries only the error and
2658
+ * the hook that surfaced it.
2659
+ *
2660
+ * Downstream wiring (all requestId-free, so every site fits): causal-graph
2661
+ * `nodeKindFor` maps it onto the `backend.error` node kind, post-process
2662
+ * `FULL_STACK_BACKEND_KINDS` + `mergeBackendEvent` summarize its error, and
2663
+ * evidence-index surfaces it as a `backend_request_error` candidate + error
2664
+ * moment — mirroring `backend.req.error` at each site.
2665
+ */
2666
+ declare const AUTO_CAPTURE_ERROR_EVENT = "backend.uncaught";
2667
+ /** Hooks a crash/console capture handler can surface an error from. */
2668
+ type AutoCaptureSource = "uncaughtException" | "unhandledRejection" | "console.error";
2669
+ interface AutoCaptureOptions {
2670
+ /** Ingest endpoint (baked into the injected snippet by the CLI). */
2671
+ endpoint: string;
2672
+ /**
2673
+ * Ingest key. Defaults to `process.env.CRUMBTRAIL_KEY`, which is populated from
2674
+ * the project's `.env` by `autoCapture` itself (see `loadEnv`).
2675
+ */
2676
+ authToken?: string;
2677
+ /** Explicit session id; a stable auto-generated one is used when omitted. */
2678
+ sessionId?: string;
2679
+ /** Extra session metadata merged into the headless session start. */
2680
+ metadata?: Record<string, unknown>;
2681
+ /** Injectable fetch (tests); forwarded to `startHeadlessSession`. */
2682
+ fetchImpl?: typeof fetch;
2683
+ /**
2684
+ * When true (default) attempt `process.loadEnvFile()` so the key in `.env`
2685
+ * lands in `process.env` before the session starts. Guarded: a no-op when the
2686
+ * API is unavailable (<20.12) or the `.env` file is missing/unreadable.
2687
+ */
2688
+ loadEnv?: boolean;
2689
+ /** Console object to patch (tests). Defaults to the global `console`. */
2690
+ consoleImpl?: Pick<Console, "error">;
2691
+ /** Process to hook (tests). Defaults to the global `process`. */
2692
+ processImpl?: NodeJS.Process;
2693
+ /**
2694
+ * Called after a best-effort record on an unrecoverable crash
2695
+ * (`uncaughtException` / `unhandledRejection`) IN PLACE of `process.exit`.
2696
+ * Tests inject this to assert crash semantics are preserved without killing
2697
+ * the runner. Defaults to `process.exit`.
2698
+ */
2699
+ onCrashExit?: (code: number) => void;
2700
+ }
2701
+ interface AutoCaptureHandle {
2702
+ /** The started session id, when the session start succeeded. */
2703
+ sessionId?: string;
2704
+ /** Restore the original console.error and remove the process hooks. */
2705
+ stop(): void;
2706
+ }
2707
+ /**
2708
+ * Install best-effort backend crash + console.error capture and start a headless
2709
+ * ingest session. Returns a handle whose `stop()` restores every hook.
2710
+ *
2711
+ * Crash semantics are preserved: on `uncaughtException` (and a suppressed
2712
+ * `unhandledRejection`) we best-effort record the error, bound-flush it (race the
2713
+ * record against a hard ~150ms ceiling so the crash event can actually reach
2714
+ * ingest before the process dies), then exit non-zero — the bounded flush can
2715
+ * never hang, and capture never converts a crash into survival.
2716
+ */
2717
+ declare function autoCapture(options: AutoCaptureOptions): Promise<AutoCaptureHandle>;
2718
+
2719
+ interface OtlpAnyValue {
2720
+ stringValue?: string;
2721
+ boolValue?: boolean;
2722
+ intValue?: string | number;
2723
+ doubleValue?: number;
2724
+ arrayValue?: {
2725
+ values?: OtlpAnyValue[];
2726
+ };
2727
+ kvlistValue?: {
2728
+ values?: OtlpKeyValue[];
2729
+ };
2730
+ bytesValue?: string;
2731
+ }
2732
+ interface OtlpKeyValue {
2733
+ key?: string;
2734
+ value?: OtlpAnyValue;
2735
+ }
2736
+
2737
+ interface OtlpSpan {
2738
+ traceId?: string;
2739
+ spanId?: string;
2740
+ parentSpanId?: string;
2741
+ name?: string;
2742
+ kind?: number;
2743
+ startTimeUnixNano?: string | number;
2744
+ endTimeUnixNano?: string | number;
2745
+ attributes?: OtlpKeyValue[];
2746
+ status?: {
2747
+ code?: number;
2748
+ message?: string;
2749
+ };
2750
+ }
2751
+ interface OtlpScopeSpans {
2752
+ scope?: {
2753
+ name?: string;
2754
+ version?: string;
2755
+ };
2756
+ spans?: OtlpSpan[];
2757
+ }
2758
+ interface OtlpResourceSpans {
2759
+ resource?: {
2760
+ attributes?: OtlpKeyValue[];
2761
+ };
2762
+ scopeSpans?: OtlpScopeSpans[];
2763
+ }
2764
+ interface OtlpTraceRequest {
2765
+ resourceSpans?: OtlpResourceSpans[];
2766
+ }
2767
+ interface OtlpLogRecord {
2768
+ timeUnixNano?: string | number;
2769
+ observedTimeUnixNano?: string | number;
2770
+ severityText?: string;
2771
+ severityNumber?: number;
2772
+ body?: {
2773
+ stringValue?: string;
2774
+ };
2775
+ traceId?: string;
2776
+ spanId?: string;
2777
+ attributes?: OtlpKeyValue[];
2778
+ }
2779
+ interface OtlpScopeLogs {
2780
+ scope?: {
2781
+ name?: string;
2782
+ };
2783
+ logRecords?: OtlpLogRecord[];
2784
+ }
2785
+ interface OtlpResourceLogs {
2786
+ resource?: {
2787
+ attributes?: OtlpKeyValue[];
2788
+ };
2789
+ scopeLogs?: OtlpScopeLogs[];
2790
+ }
2791
+ interface OtlpLogsRequest {
2792
+ resourceLogs?: OtlpResourceLogs[];
2793
+ }
2794
+
2795
+ declare function decodeOtlpTraceProtobuf(buffer: Uint8Array): OtlpTraceRequest;
2796
+ declare function decodeOtlpLogsProtobuf(buffer: Uint8Array): OtlpLogsRequest;
2797
+
2798
+ export { AUTO_CAPTURE_ERROR_EVENT, type AdapterEvidence, type AdapterSourceStats, type AdfDoc, type AdfNode, type AdvisoryCommentGap, type AdvisoryCommentMatch, type AutoCaptureHandle, type AutoCaptureOptions, type AutoCaptureSource, type BoundedRetryOptions, type BugQueueConfig, BugQueueManager, type BuildAdvisoryCommentInput, type BuildDbDiffEventInput, type BuildDbReadBulkEventInput, type BuildDbReadEventInput, type BuildFixContextOptions, CLOUDFLARE_AUTH_FIELDS, CLOUDFLARE_DESCRIPTOR, CLOUDFLARE_R2_ACCESS_KEY_ID_ENV, CLOUDFLARE_R2_ACCOUNT_ID_ENV, CLOUDFLARE_R2_BUCKET_ENV, CLOUDFLARE_R2_DATASET_ENV, CLOUDFLARE_R2_ENDPOINT_ENV, CLOUDFLARE_R2_PREFIX_ENV, CLOUDFLARE_R2_SECRET_ACCESS_KEY_ENV, CLOUDWATCH_ACCESS_KEY_ID_ENV, CLOUDWATCH_AUTH_FIELDS, CLOUDWATCH_DESCRIPTOR, CLOUDWATCH_ENDPOINT_ENV, CLOUDWATCH_LOG_GROUPS_ENV, CLOUDWATCH_REGION_ENV, CLOUDWATCH_SECRET_ACCESS_KEY_ENV, CLOUDWATCH_SESSION_TOKEN_ENV, CRUMBTRAIL_USER_AGENT, CloudWatchEvidenceSource, type CloudWatchQueryPlan, type CloudWatchResultRow, type CloudWatchSourceConfig, type CloudflareDataset, CloudflareEvidenceSource, type CloudflarePlan, type CloudflareSourceConfig, type CommentingTicketConnector, CompareError, type CompareOptions, type ComparisonConfidence, type ComparisonVerdict, type CrumbtrailExpressErrorMiddleware, type CrumbtrailExpressErrorNext, type CrumbtrailExpressMiddleware, type CrumbtrailExpressNext, type CrumbtrailExpressOptions, type CrumbtrailExpressRequest, type CrumbtrailExpressResponse, type BackendIntakeWarning as CrumbtrailExpressWarning, type BackendIntakeWarningKind as CrumbtrailExpressWarningKind, DATADOG_API_KEY_ENV, DATADOG_APP_KEY_ENV, DATADOG_AUTH_FIELDS, DATADOG_DEFAULT_SITE, DATADOG_DESCRIPTOR, DATADOG_SITE_ENV, DEFAULT_MAX_TOTAL_BYTES, DEFAULT_SENSITIVE_DB_COLUMNS, DEFAULT_SOURCE_TIMEOUT_MS, DEFAULT_SWEEP_CHECKPOINT_MS, DEFAULT_SWEEP_IDLE_MS, DEFAULT_SWEEP_INTERVAL_MS, DISTINCT_BUGS_SCHEMA_VERSION, DatadogEvidenceSource, type DatadogLog, type DatadogQueryPlan, type DatadogSourceConfig, type DatadogSpan, type DbRequestContext, type DistinctBug, type DistinctBugEvidenceRef, type DistinctBugRecurrence, type DistinctBugRecurrenceInput, type DistinctBugRecurrenceOccurrence, type DistinctBugSeverity, type Divergence, type DuckTypedMssqlPool, type DuckTypedMssqlRequest, type DuckTypedMssqlResult, type DuckTypedMysqlClient, type DuckTypedMysqlResultHeader, type DuckTypedPgClient, type DuckTypedPgQueryResult, type DuckTypedSqliteDatabase, type DuckTypedSqliteRunResult, type DuckTypedSqliteStatement, EVIDENCE_SOURCE_PROVIDERS, CRUMBTRAIL_USER_AGENT as EVIDENCE_SOURCE_USER_AGENT, type EvidenceSource, type EvidenceSourceProvider, FIX_CONTEXT_SCHEMA_VERSION, type FetchAdapterEvidenceOptions, FilesystemSessionStore, type FixContext, type FixContextDbDiff, type FixContextDbRead, FixContextError, type FixContextPrimaryWindow, type FixContextReproHint, type FixContextSession, type HeadlessSession, type HeadlessSessionOptions, InspectError, type InspectSessionOptions, type InstrumentDbClientOptions, type InstrumentPgClientOptions, type JiraAuth, type JiraBasicAuth, type JiraBearerAuth, JiraTicketClient, type JiraTicketClientConfig, type JiraTicketClientConfigLegacy, type JiraTicketClientConfigWithAuth, McpServer, type McpServerConfig, type OtlpLogsRequest, type OtlpResourceLogs, type OtlpResourceSpans, type OtlpTraceRequest, POSTHOG_API_KEY_ENV, POSTHOG_AUTH_FIELDS, POSTHOG_DEFAULT_HOST, POSTHOG_DESCRIPTOR, POSTHOG_HOST_ENV, POSTHOG_PROJECT_ID_ENV, PROVIDER_IDS, PROVIDER_RECIPES, type PostHogEvent, PostHogEvidenceSource, type PostHogPropertyFilter, type PostHogQueryPlan, type PostHogRecording, type PostHogSourceConfig, type ProviderId, type ProviderRecipe, REGRESSION_CONTEXT_SCHEMA_VERSION, REPLAY_RESULT_SCHEMA_VERSION, type RegressionContext, type ReplayDivergence, type ReplayResult, type ReplayStepResult, SENTRY_AUTH_FIELDS, SENTRY_AUTH_TOKEN_ENV, SENTRY_DEFAULT_HOST, SENTRY_DESCRIPTOR, SENTRY_HOST_ENV, SENTRY_ORG_ENV, SESSION_COMPARE_SCHEMA_VERSION, SPLUNK_AUTH_FIELDS, SPLUNK_DESCRIPTOR, SPLUNK_HOST_ENV, SPLUNK_INDEX_ENV, SPLUNK_TOKEN_ENV, SPLUNK_WEB_URL_ENV, SentryEvidenceSource, type SentryQueryPlan, type SentrySourceConfig, type BugReport as ServerBugReport, type ServerConfig, type SessionComparison, type SessionFileFlags, type SessionFinalizationResult, type SessionInspection, type SessionInspectionArtifact, type SessionListItem, SessionManager, type SessionStore, type SessionSummary, type SessionSweepOptions, type SessionSweepResult, type SessionSweeperHandle, type Severity, type SigV4Input, type SignedHeaders, type SourceHealth, SplunkEvidenceSource, type SplunkQueryPlan, type SplunkResultRow, type SplunkSourceConfig, type StepResolution, type TicketConnector, TicketError, type TicketProvider, autoCapture, buildAdvisoryComment, buildCloudWatchQuery, buildCloudflarePlan, buildDatadogQuery, buildDbDiffEvent, buildDbReadBulkEvent, buildDbReadEvent, buildDistinctBugSignature, buildFixContext, buildPostHogQuery, buildRegressionContext, buildReplayResult, buildSentryQuery, buildSessionSummary, buildSplunkQuery, cloudWatchDeepLink, cloudWatchEvidenceProvider, cloudflareEvidenceProvider, compareSessions, createCrumbtrailExpressErrorMiddleware, createCrumbtrailExpressMiddleware, createServer, datadogAppBase, datadogEvidenceProvider, decodeOtlpLogsProtobuf, decodeOtlpTraceProtobuf, defaultSessionStore, evidenceRequestHeaders, evidenceSourcesFromEnv, fetchAdapterEvidence, formatComparisonSummary, formatDuration, formatInspection, getProviderRecipe, groupDistinctBugRecurrences, groupDistinctBugs, inspectSession, instrumentMssqlPool, instrumentMysqlClient, instrumentPgClient, instrumentSqliteDatabase, isProviderId, jiraToSymptom, normalizeCloudWatchRow, normalizeDatadogLog, normalizeDatadogSpan, normalizePostHogEvent, normalizePostHogRecording, normalizeSentryIssue, normalizeSplunkRow, parseMutation, parseRead, parseReplayResult, posthogEventDeepLink, posthogEvidenceProvider, posthogRecordingDeepLink, readPackageVersion, redactEvidenceGap, redactEvidenceItem, redactSourceResult, registerEvidenceProvider, renderCompareReport, renderProviderCliOutput, renderProviderConfig, renderProviderDoc, renderProviderReadme, resolveDbRequestContext, sentryEvidenceProvider, signSigV4, splunkEvidenceProvider, splunkSearchDeepLink, splunkWebBase, startHeadlessSession, startSessionSweeper, sweepIdleSessions, withBoundedRetry, writeReplayResult };