@aaac/observability 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,959 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+
3
+ /**
4
+ * Canonical event types — single source of truth for @aaac/observability.
5
+ * Matches event-catalog.md §1 exactly.
6
+ */
7
+ /** Primitive attribute value type accepted by all event fields. */
8
+ type AttrValue = string | number | boolean;
9
+ /** Span lifecycle semantics (event-catalog.md §1.3). */
10
+ type Lifecycle = "open" | "close" | "event" | "instant";
11
+ /**
12
+ * Cross-axis link to another span (event-catalog.md §1.2, §2.7).
13
+ * link_type is supplied by event_mapping — not hardcoded here.
14
+ */
15
+ interface CanonicalLink {
16
+ /** Semantic relationship type, e.g. 'executes', 'produces_change'. */
17
+ linkType: string;
18
+ /** spanId of the target span. */
19
+ targetSpanId: string;
20
+ /** traceId of the target span (if in a different trace). */
21
+ targetTraceId?: string;
22
+ /** Optional link-level attributes. */
23
+ attributes?: Record<string, string>;
24
+ }
25
+ /**
26
+ * Raw event — input to the pipeline from observer.emit() or registerExternalEvent().
27
+ * event-catalog.md §1.1
28
+ */
29
+ interface RawEvent {
30
+ /** Origin of the event: 'aaac-runtime' | 'git-hook' | 'cursor-hook' | 'ci' */
31
+ source: string;
32
+ /** ISO-8601 wall-clock time when this package received the event. */
33
+ receivedAt: string;
34
+ /** Span name following {axis}.{name} convention (event-catalog.md §2). */
35
+ eventType: string;
36
+ /** Lifecycle position of this event within its span. */
37
+ lifecycle: Lifecycle;
38
+ /** Caller-assigned span identifier. Generated if absent. */
39
+ spanId?: string;
40
+ /** Parent span's spanId for hierarchical correlation. */
41
+ parentSpanId?: string;
42
+ /** Arbitrary key-value attributes attached to this event. */
43
+ attributes: Record<string, AttrValue>;
44
+ /** Cross-axis links declared at emit time. */
45
+ links?: CanonicalLink[];
46
+ }
47
+ /**
48
+ * Normalised internal representation — the pipeline's primary data type.
49
+ * Same spanId group forms a logical Span; parentSpanId points to the parent span.
50
+ * event-catalog.md §1.2
51
+ */
52
+ interface CanonicalEvent {
53
+ /** Unique event identifier (UUIDv7). */
54
+ id: string;
55
+ /** Unix timestamp in nanoseconds (BigInt). */
56
+ timeUnixNano: bigint;
57
+ /** Origin of the event. */
58
+ source: string;
59
+ /** Span name, e.g. 'agent.session'. */
60
+ eventType: string;
61
+ /** Lifecycle position of this event. */
62
+ lifecycle: Lifecycle;
63
+ /** Trace identifier shared by all spans in the same trace. */
64
+ traceId: string;
65
+ /** Span identifier. Multiple events with the same spanId form one logical span. */
66
+ spanId: string;
67
+ /** Parent span's spanId, if any. */
68
+ parentSpanId?: string;
69
+ /** ExecutionEnvelope.run_id (event-catalog.md §1.5). */
70
+ runId?: string;
71
+ /** Extracted from attributes.session_id. */
72
+ sessionId?: string;
73
+ /** Extracted from attributes.task_id. */
74
+ taskId?: string;
75
+ /** Arbitrary key-value attributes. */
76
+ attributes: Record<string, AttrValue>;
77
+ /** Cross-axis links (first-class; maps to OTEL SpanLinks). */
78
+ links: CanonicalLink[];
79
+ }
80
+ /**
81
+ * Query return type — hides storage-backend differences (event-catalog.md §1.4).
82
+ * Core fields are identical to CanonicalEvent.
83
+ */
84
+ type StoredEvent = CanonicalEvent;
85
+
86
+ /**
87
+ * ExecutionEnvelope — agent-axis primary source from @aaac/runtime.
88
+ * event-catalog.md §1.5; shared with @aaac/runtime (Phase 1).
89
+ *
90
+ * All fields except run_id and timestamp are optional — runtime attaches
91
+ * whatever context is available at the time of execution.
92
+ */
93
+ interface ExecutionEnvelope {
94
+ /** UUIDv7 identifying this execution run. Maps to CanonicalEvent.runId. */
95
+ run_id: string;
96
+ /** Identifies the task instance within the run. */
97
+ task_instance_id?: string;
98
+ /** Component identifier from the agent-contracts definition. */
99
+ component_id?: string;
100
+ /** Agent that issued this execution (handoff source). */
101
+ from_agent?: string;
102
+ /** Agent that receives this execution (handoff target). */
103
+ to_agent?: string;
104
+ /** agent-contracts schema version used for this execution. */
105
+ contract_version?: string;
106
+ /** LLM adapter identifier (e.g. 'claude-agent-sdk', 'openai-agents-sdk'). */
107
+ adapter?: string;
108
+ /** LLM model identifier (e.g. 'claude-opus-4-5'). */
109
+ model?: string;
110
+ /** ISO-8601 timestamp of execution start. */
111
+ timestamp: string;
112
+ /** run_id of the parent execution (for subagent / handoff correlation). */
113
+ parent_run_id?: string;
114
+ /**
115
+ * Free-form metadata collected during execution:
116
+ * duration_ms, token_usage, cost_usd, hashes, etc.
117
+ */
118
+ execution_metadata?: Record<string, unknown>;
119
+ }
120
+
121
+ /**
122
+ * Generate a UUIDv7 string.
123
+ * Monotonically ordered by creation time at millisecond granularity.
124
+ */
125
+ declare function generateId(): string;
126
+ /**
127
+ * Current wall-clock time as Unix nanoseconds (BigInt).
128
+ * Resolution is millisecond (Date.now() × 1_000_000).
129
+ */
130
+ declare function currentTimeUnixNano(): bigint;
131
+ /**
132
+ * Convert an ISO-8601 / RFC-3339 string to Unix nanoseconds (BigInt).
133
+ * Falls back to current time if the string cannot be parsed.
134
+ */
135
+ declare function isoToUnixNano(iso: string): bigint;
136
+
137
+ /**
138
+ * EventCollector — in-process entry point for the write pipeline.
139
+ *
140
+ * Responsibilities:
141
+ * - Accept events from @aaac/runtime via emit() (in-process observer)
142
+ * - Accept events from external sources (git-hook, ci, …) via registerExternalEvent()
143
+ * - Wrap them as RawEvent with a receivedAt timestamp and forward to the downstream handler
144
+ *
145
+ * The downstream handler (pipeline) is injected via the constructor so that the
146
+ * collector stays decoupled from normalizer / correlator / sink.
147
+ */
148
+
149
+ /** Minimal shape expected by both emit() and registerExternalEvent(). */
150
+ interface EmitInput {
151
+ source: string;
152
+ eventType: string;
153
+ lifecycle: Lifecycle;
154
+ spanId?: string;
155
+ parentSpanId?: string;
156
+ attributes: Record<string, AttrValue>;
157
+ links?: CanonicalLink[];
158
+ }
159
+ /** Callback that receives each RawEvent produced by the collector. */
160
+ type RawEventHandler = (event: RawEvent) => void;
161
+ /**
162
+ * EventCollector wraps an injected pipeline handler and stamps each incoming
163
+ * event with a receivedAt timestamp before forwarding.
164
+ */
165
+ declare class EventCollector {
166
+ private readonly handler;
167
+ constructor(handler: RawEventHandler);
168
+ /**
169
+ * In-process observer entry point.
170
+ * Called by @aaac/runtime (or any in-process producer) with an event payload.
171
+ */
172
+ emit(event: EmitInput): void;
173
+ /**
174
+ * External event registration API.
175
+ * Called by git hooks, Cursor hooks, CI, or the `aaac-observ record` CLI.
176
+ * Identical wire format to emit(); separated to aid future routing logic.
177
+ */
178
+ registerExternalEvent(event: EmitInput): void;
179
+ }
180
+
181
+ /**
182
+ * ExternalEventRegistrar — thin facade implementing the external event
183
+ * registration contract from architecture.md §9.
184
+ *
185
+ * External processes (git hooks, Cursor hooks, CI) call registerEvent() to
186
+ * inject events into the same write pipeline as in-process events.
187
+ */
188
+
189
+ /** Typed input for external event sources (architecture.md §9). */
190
+ interface RawExternalEvent {
191
+ /** Event origin: 'git-hook' | 'cursor-hook' | 'ci' | ... */
192
+ source: string;
193
+ eventType: string;
194
+ lifecycle: Lifecycle;
195
+ spanId?: string;
196
+ parentSpanId?: string;
197
+ attributes: Record<string, AttrValue>;
198
+ links?: CanonicalLink[];
199
+ }
200
+ /** Interface contract for external event registration (architecture.md §9). */
201
+ interface ExternalEventRegistrar {
202
+ registerEvent(event: RawExternalEvent): void;
203
+ }
204
+ /**
205
+ * Default implementation — delegates to EventCollector.registerExternalEvent().
206
+ */
207
+ declare class DefaultExternalRegistrar implements ExternalEventRegistrar {
208
+ private readonly collector;
209
+ constructor(collector: EventCollector);
210
+ registerEvent(event: RawExternalEvent): void;
211
+ }
212
+
213
+ /**
214
+ * Normalizer — converts RawEvent → CanonicalEvent.
215
+ *
216
+ * Responsibilities (architecture.md §4.1 / §3):
217
+ * - Required-key validation (delegated to validator.ts)
218
+ * - Type coercion: attribute values are coerced to AttrValue
219
+ * - Default values: id, timeUnixNano, spanId, traceId are generated if absent
220
+ * - Link declaration conversion: RawEvent.links → CanonicalEvent.links
221
+ * - Extraction of well-known scalar fields: sessionId, taskId, runId
222
+ *
223
+ * traceId *propagation* (child → parent) is the Correlator's responsibility;
224
+ * the Normalizer only generates a fresh traceId as the default.
225
+ */
226
+
227
+ /**
228
+ * Normalizer converts a validated RawEvent into a CanonicalEvent.
229
+ * An instance is stateless — the same instance may be reused for many events.
230
+ */
231
+ declare class Normalizer {
232
+ /**
233
+ * Validate and normalise a single RawEvent.
234
+ * Throws NormalizationError on invalid input.
235
+ */
236
+ normalize(raw: RawEvent): CanonicalEvent;
237
+ }
238
+
239
+ /**
240
+ * Validator — checks that a RawEvent has all required fields before normalization.
241
+ * Throws NormalizationError with a descriptive message on failure.
242
+ */
243
+
244
+ /** Thrown when a RawEvent fails validation. */
245
+ declare class NormalizationError extends Error {
246
+ constructor(message: string);
247
+ }
248
+ /**
249
+ * Validate a RawEvent.
250
+ * Throws NormalizationError listing all violated constraints.
251
+ */
252
+ declare function validateRawEvent(event: RawEvent): void;
253
+
254
+ /**
255
+ * Correlator — generic, domain-agnostic span correlation.
256
+ *
257
+ * Responsibilities (architecture.md §4.1 / §8):
258
+ * - span lifecycle pairing: tracks 'open' events by spanId; pairs with 'close'
259
+ * - duration computation: stored in the in-memory state so SqliteSink can read
260
+ * startTime when processing the close event
261
+ * - traceId propagation: child spans inherit their parent's traceId
262
+ * - in-memory short-term cache: cacheGet / cacheSet for Enricher use (Phase 1+)
263
+ *
264
+ * No domain logic. The correlator does not know about agent / process / promotion axes.
265
+ */
266
+
267
+ /**
268
+ * Correlator maintains in-memory state across events in the same write session.
269
+ * A single instance should be reused for the lifetime of the write pipeline.
270
+ */
271
+ declare class Correlator {
272
+ /** Open spans awaiting a matching 'close' event (keyed by spanId). */
273
+ private readonly openSpans;
274
+ /** General-purpose short-term key-value cache (architecture.md §8). */
275
+ private readonly cache;
276
+ /**
277
+ * Correlate a single CanonicalEvent.
278
+ *
279
+ * Mutates the event's traceId if a parent span is found in the open set.
280
+ * Updates open span state according to the event's lifecycle.
281
+ *
282
+ * Returns the (possibly updated) CanonicalEvent.
283
+ */
284
+ correlate(event: CanonicalEvent): CanonicalEvent;
285
+ /**
286
+ * Retrieve the currently-open entry for a span (undefined if not open).
287
+ * Useful for SqliteSink to look up start_time without a DB round-trip.
288
+ */
289
+ getOpenSpan(spanId: string): CanonicalEvent | undefined;
290
+ /** Store a value under an arbitrary key (TTL not enforced in Phase 0). */
291
+ cacheSet(key: string, value: unknown): void;
292
+ /** Retrieve a cached value; returns undefined if absent. */
293
+ cacheGet<T = unknown>(key: string): T | undefined;
294
+ /** Remove a cached entry. */
295
+ cacheDel(key: string): void;
296
+ /** Return the number of currently-open spans (useful for testing). */
297
+ get openSpanCount(): number;
298
+ }
299
+
300
+ /**
301
+ * Cache key helpers for the Correlator's in-memory short-term store.
302
+ * architecture.md §8.2
303
+ */
304
+ declare const keys: {
305
+ /** Per-run context: run:{id} */
306
+ readonly run: (id: string) => string;
307
+ /** Latest span in a trace: trace:{id}:latest */
308
+ readonly traceLatest: (id: string) => string;
309
+ /** Per-session context: session:{id} */
310
+ readonly session: (id: string) => string;
311
+ /** Tool call tracking: toolcall:{id} */
312
+ readonly toolCall: (id: string) => string;
313
+ /** Request tracking: request:{id} */
314
+ readonly request: (id: string) => string;
315
+ };
316
+
317
+ /**
318
+ * EnrichApi — the capability surface exposed to every enrichment rule.
319
+ *
320
+ * shift-left-event-mapping.md §5.2 / architecture.md §5.5
321
+ *
322
+ * Design principles:
323
+ * - cacheGet/cacheSet reuse the Correlator's in-memory short-term store (no extra state)
324
+ * - emitRecord queues a derived CanonicalEvent to be written to OtelEmitter + SqliteSink
325
+ * after the current event's enrichment completes (re-injected into the pipeline)
326
+ * - addLink appends a cross-axis link to the event currently being enriched
327
+ * - lookupArtifact resolves file paths to artifact_ids via longest-match glob
328
+ * - All resolution uses STRUCTURED keys only — no natural-language guessing
329
+ */
330
+
331
+ /**
332
+ * A single artifact path-glob pattern entry.
333
+ * Provided via config / constructor from the artifact-contracts declarations
334
+ * (generated by Phase 4's binding; not hardcoded here).
335
+ * shift-left-event-mapping.md §3.3
336
+ */
337
+ interface ArtifactPattern {
338
+ /** The artifact identifier (e.g. 'feature-spec', 'impl-ts'). */
339
+ artifact_id: string;
340
+ /** A glob pattern (e.g. 'src/**\/*.ts', 'docs/*.md'). */
341
+ pattern: string;
342
+ }
343
+ /**
344
+ * The API surface available to enrichment rule functions.
345
+ * All methods are synchronous — rules run in the hot write path.
346
+ */
347
+ interface EnrichApi {
348
+ /**
349
+ * Read from the Correlator's shared in-memory cache.
350
+ * Returns undefined if the key is absent.
351
+ */
352
+ cacheGet<T = unknown>(key: string): T | undefined;
353
+ /**
354
+ * Write to the Correlator's shared in-memory cache.
355
+ * Overwrites any existing value for the key.
356
+ */
357
+ cacheSet(key: string, value: unknown): void;
358
+ /**
359
+ * Queue a fully-formed CanonicalEvent to be written to OtelEmitter + SqliteSink
360
+ * after the current event's enrichment pipeline completes.
361
+ *
362
+ * Derived events are NOT re-enriched (depth-1 limit to prevent cycles).
363
+ * Use this to emit promotion.file, attribution events, etc.
364
+ */
365
+ emitRecord(event: CanonicalEvent): void;
366
+ /**
367
+ * Append a cross-axis link to the event currently being enriched.
368
+ * Architecture.md §5.2 — post-discovery links are attached to the discovering event.
369
+ */
370
+ addLink(link: CanonicalLink): void;
371
+ /**
372
+ * Resolve a file path to an artifact_id via longest-match glob lookup.
373
+ * Returns "unknown" if no pattern matches (shift-left-event-mapping.md §3.3).
374
+ */
375
+ lookupArtifact(path: string): string;
376
+ /**
377
+ * Emit a debug log. fail-open: never throws.
378
+ * In production this is a no-op or routes to the configured logger.
379
+ */
380
+ logDebug(message: string, data?: unknown): void;
381
+ /** Current wall-clock time in Unix nanoseconds (for derived event timestamps). */
382
+ nowUnixNano(): bigint;
383
+ }
384
+ /**
385
+ * Context object passed to every enrichment rule.
386
+ *
387
+ * `ctx.event` is the MUTABLE event currently being enriched.
388
+ * Rules may set fields like `parentSpanId`, `taskId`, or `attributes` directly.
389
+ * `ctx.api` provides the enrichment capabilities.
390
+ *
391
+ * shift-left-event-mapping.md §5.2 example:
392
+ * function resolveProcessParent(event: CanonicalEvent, ctx: EnrichContext): void
393
+ */
394
+ interface EnrichContext {
395
+ /** The event being enriched. Mutable — rules set fields directly. */
396
+ event: CanonicalEvent;
397
+ /** Enrichment capabilities. */
398
+ api: EnrichApi;
399
+ }
400
+ /**
401
+ * Signature of an enrichment rule.
402
+ *
403
+ * - `event` is the same reference as `ctx.event` (convenience alias, consistent with spec)
404
+ * - Rules check their applicability at the top (eventType / lifecycle / presence of fields)
405
+ * - Rules MUST NOT throw — the Enricher wraps each call in a fail-open try/catch
406
+ *
407
+ * shift-left-event-mapping.md §5 — all resolution via STRUCTURED keys only.
408
+ */
409
+ type EnrichRule = (event: CanonicalEvent, ctx: EnrichContext) => void;
410
+
411
+ /**
412
+ * Enricher — domain-specific enrichment stage in the write-path pipeline.
413
+ *
414
+ * architecture.md §4.1 (Enricher role) / §5.5 / shift-left-event-mapping.md §5:
415
+ * Positioned between Correlator and the sinks (OtelEmitter + SqliteSink).
416
+ * Applies enrichment rules to each CanonicalEvent:
417
+ * - attribute completion (artifact_id, reconcile.status)
418
+ * - parent resolution (parentSpanId, taskId)
419
+ * - cross-axis link addition (materializes_as_commit, contains_change)
420
+ * - derived event emission (promotion.file)
421
+ *
422
+ * Design:
423
+ * - Reuses the Correlator's in-memory short-term store for cacheGet/cacheSet
424
+ * - fail-open: a rule that throws is logged and execution continues
425
+ * - emitRecord queues derived CanonicalEvents (written to sinks after the
426
+ * current event's enrichment completes; NOT re-enriched to prevent cycles)
427
+ * - dispatch: all registered rules run for every event; each rule checks
428
+ * its own applicability (eventType / lifecycle / field presence)
429
+ */
430
+
431
+ interface EnricherOptions {
432
+ /** Correlator instance — its cache is shared with the Enricher (architecture.md §8). */
433
+ correlator: Correlator;
434
+ /**
435
+ * Artifact path-glob patterns for lookupArtifact.
436
+ * Provided via config / createPipeline options; generated by Phase 4 binding.
437
+ * Pass [] for no artifact resolution (all paths → "unknown").
438
+ */
439
+ artifactPatterns?: ArtifactPattern[];
440
+ }
441
+ /** Return value of Enricher.enrich(). */
442
+ interface EnrichResult {
443
+ /** The (possibly mutated) canonical event ready to write to sinks. */
444
+ enriched: CanonicalEvent;
445
+ /**
446
+ * Derived canonical events queued by rules via api.emitRecord().
447
+ * Caller writes these to sinks after the primary event (no re-enrichment).
448
+ */
449
+ derived: CanonicalEvent[];
450
+ }
451
+ declare class Enricher {
452
+ private readonly correlator;
453
+ private readonly artifactPatterns;
454
+ private readonly rules;
455
+ /**
456
+ * @param options Correlator + optional artifact patterns
457
+ * @param rules Enrichment rules to apply in order.
458
+ * Use createDefaultRules() for the standard Phase 5 rule set.
459
+ */
460
+ constructor(options: EnricherOptions, rules: EnrichRule[]);
461
+ /**
462
+ * Enrich a single CanonicalEvent.
463
+ *
464
+ * Returns a shallow copy of the event (with mutated fields from rules) and
465
+ * a list of derived events queued by api.emitRecord().
466
+ *
467
+ * fail-open: each rule runs in a try/catch; a throwing rule logs to stderr
468
+ * and does NOT prevent the event from reaching the sinks.
469
+ */
470
+ enrich(event: CanonicalEvent): EnrichResult;
471
+ }
472
+ /**
473
+ * Build the standard Phase 5 enrichment rule set.
474
+ *
475
+ * Rule ordering:
476
+ * 1. R7 artifact.ts — resolve edit.artifact_id first (R3 disambiguation depends on it)
477
+ * 2. R3/R4 process.ts — maintain active-task stack; resolve process.edit/tool parent
478
+ * 3. R5 cross-axis.ts — materializes_as_commit on promotion.commit close
479
+ * 4. R1/R2 promotion.ts — cache changes; emit promotion.file on commit close
480
+ *
481
+ * @param artifactPatterns Passed to createArtifactRule for R7 lookupArtifact.
482
+ */
483
+ declare function createDefaultRules(artifactPatterns?: ArtifactPattern[]): EnrichRule[];
484
+
485
+ /**
486
+ * R7 — Artifact ID resolution for process.edit events.
487
+ *
488
+ * shift-left-event-mapping.md §5.1:
489
+ * "R7: artifact_id 未解決 → file_path → artifact-lookup (§3.3) — longest-match glob,
490
+ * 未マッチは unknown"
491
+ *
492
+ * Resolution is via STRUCTURED lookup only (path glob → artifact_id).
493
+ * No natural-language guessing; unresolved → "unknown".
494
+ */
495
+
496
+ declare function globToRegex(pattern: string): RegExp;
497
+ /**
498
+ * Resolve a file path to an artifact_id using longest-match glob.
499
+ *
500
+ * Among all patterns that match `filePath`, the one with the longest pattern
501
+ * string wins (longer = more specific). Ties are broken by declaration order
502
+ * (first wins). Returns "unknown" if no pattern matches.
503
+ *
504
+ * shift-left-event-mapping.md §3.3
505
+ */
506
+ declare function lookupArtifact(filePath: string, patterns: ArtifactPattern[]): string;
507
+ /**
508
+ * Create the R7 artifact-resolution enrichment rule.
509
+ *
510
+ * Applies to: process.edit (instant)
511
+ * Action: sets `edit.artifact_id` on the event attributes via lookupArtifact.
512
+ * If no pattern matches, sets `edit.artifact_id = "unknown"`.
513
+ * Skips if `edit.artifact_id` is already set to a non-empty, non-"unknown" value.
514
+ *
515
+ * The `patterns` list is provided via config/constructor (no hardcoded project values).
516
+ */
517
+ declare function createArtifactRule(patterns: ArtifactPattern[]): EnrichRule;
518
+
519
+ /**
520
+ * R3/R4 — Process parent resolution for process.edit and process.tool events.
521
+ *
522
+ * shift-left-event-mapping.md §5.1:
523
+ * R3: process.edit → process.task (parent)
524
+ * "session_id の active task スタック + file_path→artifact で一意化"
525
+ * "単一 active task なら確定、複数なら artifact 一致で一意化、不能なら ambiguous"
526
+ * R4: process.tool → process.task (parent)
527
+ * "単一なら確定、複数なら ambiguous"
528
+ *
529
+ * All resolution via STRUCTURED keys only:
530
+ * - active task stack per session (in-memory cache)
531
+ * - artifact_id already set by R7 (for disambiguation in R3)
532
+ * - Never guess from natural language or timing
533
+ *
534
+ * Cache schema maintained by this rule:
535
+ * session:{sid}:active_tasks → ActiveTask[] (currently open tasks)
536
+ * session:{sid}:all_task_spans → string[] (all task spanIds, open + closed, for R5)
537
+ * session:{sid}:task_files:{taskSpanId} → string[] (accumulated modified files, for R5)
538
+ */
539
+
540
+ /** A currently-open process.task span tracked on the active stack. */
541
+ interface ActiveTask {
542
+ spanId: string;
543
+ taskId: string | undefined;
544
+ }
545
+ declare function activeTasksKey(sessionId: string): string;
546
+ declare function allTaskSpansKey(sessionId: string): string;
547
+ declare function taskFilesKey(sessionId: string, taskSpanId: string): string;
548
+ /**
549
+ * Create the R3/R4 process-parent enrichment rule.
550
+ *
551
+ * Handles three event types:
552
+ * 1. process.task open — push task onto active-task stack
553
+ * 2. process.task close — pop task from active-task stack
554
+ * 3. process.edit instant — resolve parentSpanId/taskId (R3)
555
+ * 4. process.tool instant — resolve parentSpanId/taskId (R4)
556
+ *
557
+ * Resolution algorithm (R3/R4):
558
+ * - If event already has parentSpanId → skip (already resolved)
559
+ * - 0 active tasks → leave provisional (no parentSpanId)
560
+ * - 1 active task → set parentSpanId + taskId (confirmed)
561
+ * - 2+ active tasks (R3) → try to disambiguate by edit.artifact_id match
562
+ * against task's artifact_ids; if still ambiguous → set reconcile.status=ambiguous
563
+ * - 2+ active tasks (R4) → set reconcile.status=ambiguous (no artifact disambig)
564
+ */
565
+ declare function createProcessRule(): EnrichRule;
566
+ /**
567
+ * Record a task's primary artifact in the cache so that R3 disambiguation can use it.
568
+ * Call this when a process.task open event carries a known artifact_id attribute.
569
+ *
570
+ * This is an optional supplement to the createProcessRule — the main rule will
571
+ * call lookupArtifact on the task's own attributes if available, but the cache
572
+ * entry is more reliable for disambiguation.
573
+ */
574
+ declare function recordTaskArtifact(sessionId: string, taskSpanId: string, artifactId: string, cacheSet: (key: string, value: unknown) => void): void;
575
+
576
+ /**
577
+ * Create the R5 cross-axis enrichment rule.
578
+ *
579
+ * Applies to: promotion.commit (close)
580
+ * Action: for each known task whose accumulated modified_files intersect the
581
+ * committed_files attribute, add a materializes_as_commit link to the
582
+ * promotion.commit event (pointing to the task's spanId).
583
+ *
584
+ * committed_files must be a JSON-encoded string array on the commit event.
585
+ * If absent or unparseable, the rule skips silently (fail-open).
586
+ */
587
+ declare function createCrossAxisRule(): EnrichRule;
588
+
589
+ /**
590
+ * Create the R1/R2 promotion-correlation enrichment rule.
591
+ *
592
+ * Handles two event types:
593
+ * 1. promotion.change instant — cache the change by (session_id, file.path)
594
+ * 2. promotion.commit close — emit promotion.file per committed file (R2),
595
+ * with contains_change links to cached changes (R1)
596
+ *
597
+ * The committed_files attribute on promotion.commit must be a JSON-encoded
598
+ * string array (e.g., '["src/foo.ts","README.md"]').
599
+ * If absent or unparseable, the rule skips silently (fail-open).
600
+ */
601
+ declare function createPromotionRule(): EnrichRule;
602
+
603
+ /**
604
+ * SqliteSink — persists CanonicalEvents to a local SQLite database.
605
+ *
606
+ * architecture.md §10 / §4.1 (SqliteSink role):
607
+ * - canonical_events : append-only INSERT (immutable log)
608
+ * - spans : open→INSERT, close→UPDATE, instant→INSERT
609
+ * - canonical_links : expand event.links into normalised rows
610
+ *
611
+ * Default path: .agent-logs/observability.db
612
+ * Pass ':memory:' for an in-process in-memory database (tests, ephemeral use).
613
+ *
614
+ * Uses the built-in node:sqlite (stable in Node ≥ 24, experimental in 22.5+).
615
+ */
616
+
617
+ /** Default database file location (relative to cwd). */
618
+ declare const DEFAULT_DB_PATH = ".agent-logs/observability.db";
619
+ declare class SqliteSink {
620
+ private readonly db;
621
+ constructor(dbPath?: string);
622
+ /**
623
+ * Persist a single CanonicalEvent.
624
+ * 1. Append to canonical_events (immutable)
625
+ * 2. Upsert spans materialised view
626
+ * 3. Insert canonical_links rows
627
+ */
628
+ write(event: CanonicalEvent): void;
629
+ /** Close the database connection. */
630
+ close(): void;
631
+ /**
632
+ * Expose the underlying DatabaseSync for testing / read-path adapters.
633
+ * Should not be used in production write paths.
634
+ */
635
+ getDb(): DatabaseSync;
636
+ private insertCanonicalEvent;
637
+ private upsertSpan;
638
+ private insertLinks;
639
+ }
640
+
641
+ /**
642
+ * span-mapper — converts a group of CanonicalEvents sharing the same spanId
643
+ * into a structure ready for OTEL Span emission.
644
+ *
645
+ * architecture.md §11 / §4.1 (OtelEmitter role):
646
+ * - open → Span start_time
647
+ * - close → Span end_time
648
+ * - event → SpanEvent (attached to the span)
649
+ * - instant → start_time = end_time (duration-zero Span)
650
+ *
651
+ * attributes are merged across open / close / event events (open first,
652
+ * then close, then each event in time order — later keys win).
653
+ *
654
+ * parentSpanId → OTEL parent context.
655
+ * CanonicalEvent.links → OTEL SpanLinks (link_type stored as a SpanLink attribute).
656
+ */
657
+
658
+ /** A single SpanEvent (mid-span event attached to the logical span). */
659
+ interface MappedSpanEvent {
660
+ name: string;
661
+ timeUnixNano: bigint;
662
+ attributes: Record<string, AttrValue>;
663
+ }
664
+ /** A SpanLink pointing to another span, carrying link_type as an attribute. */
665
+ interface MappedSpanLink {
666
+ /** Target span identifier. */
667
+ targetSpanId: string;
668
+ /** Target trace identifier (may be same as the span's traceId). */
669
+ targetTraceId?: string;
670
+ /** Attributes including 'link_type' (the semantic relationship). */
671
+ attributes: Record<string, string>;
672
+ }
673
+ /**
674
+ * A logical OTEL Span derived from one or more CanonicalEvents.
675
+ * This is an intermediate representation, not coupled to any OTEL SDK type.
676
+ */
677
+ interface MappedSpan {
678
+ /** Span identifier. */
679
+ spanId: string;
680
+ /** Trace identifier. */
681
+ traceId: string;
682
+ /** Parent span identifier (undefined for root spans). */
683
+ parentSpanId?: string;
684
+ /** Span name (== eventType of the constituent events). */
685
+ name: string;
686
+ /** Span start time in Unix nanoseconds. */
687
+ startTimeUnixNano: bigint;
688
+ /** Span end time in Unix nanoseconds. */
689
+ endTimeUnixNano: bigint;
690
+ /** Merged attributes from all constituent events. */
691
+ attributes: Record<string, AttrValue>;
692
+ /** Mid-span events (from lifecycle='event' CanonicalEvents). */
693
+ spanEvents: MappedSpanEvent[];
694
+ /** Links to other spans. */
695
+ links: MappedSpanLink[];
696
+ }
697
+ /**
698
+ * Map a group of CanonicalEvents (all sharing the same spanId) to a MappedSpan.
699
+ *
700
+ * The caller must ensure all events belong to the same spanId.
701
+ * Events need not be pre-sorted; this function sorts by timeUnixNano internally.
702
+ *
703
+ * Returns undefined if the group is empty.
704
+ */
705
+ declare function mapEventsToSpan(events: CanonicalEvent[]): MappedSpan | undefined;
706
+ /**
707
+ * Group an array of CanonicalEvents by spanId, then map each group to a MappedSpan.
708
+ * Useful for batch conversion (e.g. flushing a set of events to OTEL).
709
+ */
710
+ declare function mapAllEventsToSpans(events: CanonicalEvent[]): MappedSpan[];
711
+
712
+ interface OtelEmitterOptions {
713
+ /**
714
+ * OTLP HTTP endpoint URL.
715
+ * Defaults to process.env.OTEL_EXPORTER_OTLP_ENDPOINT.
716
+ * If neither is set, OtelEmitter becomes a no-op.
717
+ */
718
+ endpoint?: string;
719
+ /** Service name reported to the OTEL backend. Default: '@aaac/observability'. */
720
+ serviceName?: string;
721
+ }
722
+ declare class OtelEmitter {
723
+ private readonly provider;
724
+ private readonly tracer;
725
+ private readonly enabled;
726
+ constructor(options?: OtelEmitterOptions);
727
+ /**
728
+ * Emit a single MappedSpan to the configured OTLP backend.
729
+ *
730
+ * fail-open: any error is caught, logged to stderr, and swallowed.
731
+ * This method never throws into the caller.
732
+ */
733
+ emit(span: MappedSpan): void;
734
+ /**
735
+ * Emit a batch of MappedSpans.
736
+ * Each span is emitted independently; one failure does not block others.
737
+ */
738
+ emitBatch(spans: MappedSpan[]): void;
739
+ /**
740
+ * Flush pending spans and shut down the OTEL provider.
741
+ * Returns a promise that resolves when the flush is complete (or on error).
742
+ */
743
+ shutdown(): Promise<void>;
744
+ private _emitSpan;
745
+ private _buildParentContext;
746
+ }
747
+
748
+ /**
749
+ * Query-layer models — decouple read-path return types from storage internals.
750
+ *
751
+ * event-catalog.md §1.4 / architecture.md §12.
752
+ *
753
+ * StoredEvent is already defined in types/canonical-event.ts as an alias of
754
+ * CanonicalEvent. Here we re-export it alongside query-specific types so
755
+ * consumers can import everything from '@aaac/observability/query' (or the
756
+ * barrel) without reaching into internal modules.
757
+ */
758
+
759
+ /**
760
+ * A logical Span as materialised in the `spans` table.
761
+ * Returned by span-level queries (QueryAdapter.querySpans, getSpan, etc.).
762
+ */
763
+ interface SpanRecord {
764
+ spanId: string;
765
+ traceId: string | null;
766
+ parentSpanId: string | null;
767
+ eventType: string;
768
+ /** Unix nanoseconds (bigint, may be null for spans with no open event). */
769
+ startTime: bigint | null;
770
+ /** Unix nanoseconds (bigint, may be null for open spans). */
771
+ endTime: bigint | null;
772
+ /** Computed duration in nanoseconds. */
773
+ durationNs: bigint | null;
774
+ /** 'open' | 'closed' | 'instant' */
775
+ status: string;
776
+ runId: string | null;
777
+ sessionId: string | null;
778
+ taskId: string | null;
779
+ attributes: Record<string, AttrValue>;
780
+ }
781
+ /** A single cross-axis link row from the `canonical_links` table. */
782
+ interface LinkRecord {
783
+ id: string;
784
+ sourceEventId: string;
785
+ sourceSpanId: string;
786
+ targetSpanId: string;
787
+ targetTraceId: string | null;
788
+ linkType: string;
789
+ attributes: Record<string, string>;
790
+ }
791
+ /** Filters for span-level queries. All fields are optional (AND-combined). */
792
+ interface SpanQueryFilter {
793
+ traceId?: string;
794
+ spanId?: string;
795
+ eventType?: string;
796
+ taskId?: string;
797
+ /** Inclusive lower bound (Unix nanoseconds). */
798
+ fromTimeUnixNano?: bigint;
799
+ /** Inclusive upper bound (Unix nanoseconds). */
800
+ toTimeUnixNano?: bigint;
801
+ /** Limit the number of returned rows. */
802
+ limit?: number;
803
+ }
804
+ /** Filters for canonical_events queries. All fields are optional (AND-combined). */
805
+ interface EventQueryFilter {
806
+ traceId?: string;
807
+ spanId?: string;
808
+ eventType?: string;
809
+ taskId?: string;
810
+ fromTimeUnixNano?: bigint;
811
+ toTimeUnixNano?: bigint;
812
+ limit?: number;
813
+ }
814
+ /** Direction for link traversal queries. */
815
+ type LinkDirection = "forward" | "reverse" | "both";
816
+
817
+ /**
818
+ * QueryAdapter — abstract interface for reading stored observability data.
819
+ *
820
+ * architecture.md §12:
821
+ * - Decouples the read path from the write path and from storage backends.
822
+ * - Implementations: SqliteQueryAdapter (local), future: OtelBackendAdapter.
823
+ * - Returns StoredEvent / SpanRecord / LinkRecord — backend-neutral types.
824
+ */
825
+
826
+ interface QueryAdapter {
827
+ /**
828
+ * Query the materialised `spans` table.
829
+ * Returns spans matching all supplied filter fields (AND-combined).
830
+ */
831
+ querySpans(filter: SpanQueryFilter): SpanRecord[];
832
+ /**
833
+ * Retrieve a single span by spanId.
834
+ * Returns undefined if not found.
835
+ */
836
+ getSpan(spanId: string): SpanRecord | undefined;
837
+ /**
838
+ * Retrieve all spans belonging to a trace, ordered by startTime.
839
+ */
840
+ getTrace(traceId: string): SpanRecord[];
841
+ /**
842
+ * Query raw CanonicalEvents from the `canonical_events` table.
843
+ */
844
+ queryEvents(filter: EventQueryFilter): StoredEvent[];
845
+ /**
846
+ * Retrieve all CanonicalEvents for a specific spanId (the constituent events
847
+ * of a logical span), ordered by timeUnixNano.
848
+ */
849
+ getSpanEvents(spanId: string): StoredEvent[];
850
+ /**
851
+ * Retrieve links involving the given spanId.
852
+ *
853
+ * direction:
854
+ * 'forward' — links where source_span_id = spanId (outgoing)
855
+ * 'reverse' — links where target_span_id = spanId (incoming)
856
+ * 'both' — union of forward and reverse
857
+ */
858
+ getLinks(spanId: string, direction?: LinkDirection): LinkRecord[];
859
+ /**
860
+ * Retrieve all links of a given link_type.
861
+ */
862
+ getLinksByType(linkType: string): LinkRecord[];
863
+ /** Release any resources held by this adapter (e.g. close DB connection). */
864
+ close(): void;
865
+ }
866
+
867
+ /**
868
+ * SqliteQueryAdapter — reads the SqliteSink database independently of the
869
+ * write path.
870
+ *
871
+ * architecture.md §12 / §12.3:
872
+ * - Queries the `spans` materialised table for span-level queries.
873
+ * - Queries `canonical_events` for raw event access.
874
+ * - Queries `canonical_links` for link traversal (source/target bidirectional).
875
+ * - Can open the same database file as SqliteSink, or an in-memory DB for tests.
876
+ * - Does NOT touch write-path state (no Correlator, no Normalizer, etc.).
877
+ */
878
+
879
+ declare class SqliteQueryAdapter implements QueryAdapter {
880
+ private readonly db;
881
+ private readonly ownsDb;
882
+ /**
883
+ * @param dbPathOrDb Either a file path string (opens a new connection) or
884
+ * an existing DatabaseSync instance (shared connection,
885
+ * e.g. from SqliteSink.getDb() in tests).
886
+ */
887
+ constructor(dbPathOrDb?: string | DatabaseSync);
888
+ querySpans(filter: SpanQueryFilter): SpanRecord[];
889
+ getSpan(spanId: string): SpanRecord | undefined;
890
+ getTrace(traceId: string): SpanRecord[];
891
+ queryEvents(filter: EventQueryFilter): StoredEvent[];
892
+ getSpanEvents(spanId: string): StoredEvent[];
893
+ getLinks(spanId: string, direction?: LinkDirection): LinkRecord[];
894
+ getLinksByType(linkType: string): LinkRecord[];
895
+ close(): void;
896
+ }
897
+
898
+ /**
899
+ * @aaac/observability — Phase 0–5 public API
900
+ *
901
+ * Write pipeline (Phase 5):
902
+ * EventCollector → Normalizer → Correlator → [afterCorrelate?] → Enricher → SqliteSink
903
+ * → OtelEmitter
904
+ *
905
+ * Quick start:
906
+ * import { createPipeline } from '@aaac/observability';
907
+ * const { collector, sink } = createPipeline();
908
+ * collector.emit({ source: 'aaac-runtime', eventType: 'agent.session', lifecycle: 'open', attributes: {} });
909
+ * sink.close();
910
+ */
911
+
912
+ interface PipelineOptions {
913
+ /** SQLite database path. Defaults to DEFAULT_DB_PATH. Pass ':memory:' for tests. */
914
+ dbPath?: string;
915
+ /**
916
+ * Optional post-correlate hook — runs before the Enricher.
917
+ * Kept for backward compatibility; prefer using enrichRules for Phase 5+.
918
+ */
919
+ afterCorrelate?: (event: CanonicalEvent) => CanonicalEvent;
920
+ /**
921
+ * Optional OTEL emitter options.
922
+ * If provided (and endpoint is set), events are also emitted to the OTEL backend.
923
+ * fail-open: OTEL failures never block SQLite recording.
924
+ */
925
+ otel?: OtelEmitterOptions;
926
+ /**
927
+ * Artifact path-glob patterns for the Enricher's lookupArtifact (R7).
928
+ * Provided from artifact-contracts declarations (generated by Phase 4 binding).
929
+ * Defaults to [] (all file paths resolve to "unknown").
930
+ */
931
+ artifactPatterns?: ArtifactPattern[];
932
+ /**
933
+ * Custom enrichment rule list.
934
+ * Defaults to createDefaultRules(artifactPatterns) — the standard Phase 5 set.
935
+ * Pass [] to disable all enrichment.
936
+ */
937
+ enrichRules?: EnrichRule[];
938
+ }
939
+ interface Pipeline {
940
+ collector: EventCollector;
941
+ correlator: Correlator;
942
+ sink: SqliteSink;
943
+ otelEmitter: OtelEmitter;
944
+ /** Phase 5 Enricher (Correlator → Enricher → OtelEmitter + SqliteSink). */
945
+ enricher: Enricher;
946
+ }
947
+ /**
948
+ * Create a complete write pipeline wired together:
949
+ *
950
+ * EventCollector → Normalizer → Correlator → [afterCorrelate?]
951
+ * → Enricher → SqliteSink
952
+ * → OtelEmitter (if configured, fail-open)
953
+ *
954
+ * Derived events from Enricher.emitRecord() are written to both sinks after
955
+ * the primary event (depth-1; derived events are NOT re-enriched).
956
+ */
957
+ declare function createPipeline(options?: PipelineOptions): Pipeline;
958
+
959
+ export { type ActiveTask, type ArtifactPattern, type AttrValue, type CanonicalEvent, type CanonicalLink, Correlator, DEFAULT_DB_PATH, DefaultExternalRegistrar, type EmitInput, type EnrichApi, type EnrichContext, type EnrichResult, type EnrichRule, Enricher, type EnricherOptions, EventCollector, type EventQueryFilter, type ExecutionEnvelope, type ExternalEventRegistrar, type Lifecycle, type LinkDirection, type LinkRecord, type MappedSpan, type MappedSpanEvent, type MappedSpanLink, NormalizationError, Normalizer, OtelEmitter, type OtelEmitterOptions, type Pipeline, type PipelineOptions, type QueryAdapter, type RawEvent, type RawEventHandler, type RawExternalEvent, type SpanQueryFilter, type SpanRecord, SqliteQueryAdapter, SqliteSink, type StoredEvent, activeTasksKey, allTaskSpansKey, keys as correlateKeys, createArtifactRule, createCrossAxisRule, createDefaultRules, createPipeline, createProcessRule, createPromotionRule, currentTimeUnixNano, generateId, globToRegex, isoToUnixNano, lookupArtifact, mapAllEventsToSpans, mapEventsToSpan, recordTaskArtifact, taskFilesKey, validateRawEvent };