@aaac/observability 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,21 +1,27 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ handleRecordHook
4
+ } from "../chunk-EKFRH7PX.js";
2
5
  import {
3
6
  DEFAULT_DB_PATH,
7
+ OUTCOME_CHAIN_STAGES,
4
8
  OtelEmitter,
5
9
  SqliteQueryAdapter,
10
+ SqliteSink,
11
+ VENDOR_OTEL_SOURCE,
12
+ analyzeOutcomeChain,
6
13
  createPipeline,
7
- emitHumanInstruction,
8
- emitPromotionPr,
9
- emitQualityGateResult,
10
- evaluateMapping,
14
+ generateDiagnosticReport,
11
15
  generateId,
12
16
  isoToUnixNano,
13
- loadEventMappingConfig
14
- } from "../chunk-J2F5GEMO.js";
17
+ mapVendorOtelExport,
18
+ renderReportJson,
19
+ renderReportMarkdown
20
+ } from "../chunk-3DXZNA3E.js";
15
21
 
16
22
  // src/cli/index.ts
17
23
  import { readFileSync } from "fs";
18
- import { dirname as dirname2, resolve } from "path";
24
+ import { dirname, resolve } from "path";
19
25
  import { fileURLToPath } from "url";
20
26
 
21
27
  // src/generated/program.ts
@@ -154,6 +160,9 @@ command_sets:
154
160
  - name: dry-run
155
161
  schema: { type: boolean }
156
162
  description: Print span count without emitting to OTLP backend
163
+ - name: flush-timeout-ms
164
+ schema: { type: integer }
165
+ description: "Maximum ms to wait for OTLP flush before exit (default: 2000; use \u226530000 for >5k spans)"
157
166
  exits:
158
167
  '0':
159
168
  description: Backfill completed
@@ -173,6 +182,56 @@ command_sets:
173
182
  stderr:
174
183
  format: text
175
184
 
185
+ ingest-vendor-otel:
186
+ summary: Project a vendor OTLP/JSON trace export (e.g. Claude Code token/cost/trace) into the SQLite Ledger
187
+ description: |
188
+ Canonical Projection batch (observability-integration.md \xA79.2 / T-D2).
189
+
190
+ Reads a vendor OTLP/JSON trace export from --input (file) or stdin, projects
191
+ each span into a Canonical Event, and backfills it into the SQLite Ledger.
192
+
193
+ Idempotent: every event carries a deterministic dedup_key
194
+ (vendor-otel:{traceId}:{spanId}) enforced by UNIQUE + INSERT OR IGNORE, so
195
+ re-running on the same export inserts nothing new (reported as
196
+ skippedDuplicate). Hook/runtime records remain the primary source; vendor
197
+ OTEL is auxiliary evidence.
198
+ options:
199
+ - name: db
200
+ schema: { type: string }
201
+ description: Path to observability SQLite database (overrides default)
202
+ - name: input
203
+ aliases: [i]
204
+ schema: { type: string }
205
+ description: "Path to the OTLP/JSON trace export (reads stdin when omitted)"
206
+ - name: source
207
+ schema: { type: string }
208
+ description: "Source tag for projected events (default: claude-code-otel)"
209
+ - name: dry-run
210
+ schema: { type: boolean }
211
+ description: Parse and project without writing to the Ledger
212
+ exits:
213
+ '0':
214
+ description: Ingestion completed
215
+ stdout:
216
+ format: json
217
+ schema:
218
+ type: object
219
+ properties:
220
+ found:
221
+ type: integer
222
+ inserted:
223
+ type: integer
224
+ skippedDuplicate:
225
+ type: integer
226
+ skippedInvalid:
227
+ type: integer
228
+ dryRun:
229
+ type: boolean
230
+ '1':
231
+ description: Ingestion failed (unreadable input, invalid JSON, DB error)
232
+ stderr:
233
+ format: text
234
+
176
235
  query:
177
236
  summary: Query the observability store via QueryAdapter
178
237
  options:
@@ -180,8 +239,11 @@ command_sets:
180
239
  aliases: [k]
181
240
  schema:
182
241
  type: string
183
- enum: [trace, span, search, links]
184
- description: "Query kind: trace | span | search | links"
242
+ enum: [trace, span, search, links, issue]
243
+ description: "Query kind: trace | span | search | links | issue"
244
+ - name: issue
245
+ schema: { type: string }
246
+ description: "Issue ID for issue queries (--kind issue)"
185
247
  - name: trace-id
186
248
  schema: { type: string }
187
249
  description: Trace ID for trace queries (--kind trace)
@@ -221,8 +283,56 @@ command_sets:
221
283
  description: Query failed
222
284
  stderr:
223
285
  format: text
286
+
287
+ report:
288
+ summary: Generate an Observability diagnostic report (Layer 1-3 + link completion + Person Tier) from the SQLite Ledger
289
+ description: |
290
+ Customer-facing Observability diagnostic (#179 WS-F T-F1). Computes the
291
+ same Layer 1-3 / link-completion / Person-Tier metrics rendered by the
292
+ \`aaac-diagnostic\` Grafana dashboard, over the local SQLite Ledger.
293
+
294
+ Output is Markdown (default) or JSON. Use --project-id to scope to a
295
+ single project (internal or a cooperating customer project). Supply
296
+ --baseline-from / --baseline-to to add a Before/After comparison
297
+ section against a baseline window; --from / --to bound the current
298
+ (after) window.
299
+ options:
300
+ - name: project-id
301
+ aliases: [p]
302
+ schema: { type: string }
303
+ description: Restrict the report to a single project_id (project.id attr / service.name)
304
+ - name: from
305
+ schema: { type: string }
306
+ description: Start time (ISO 8601) for the current/after window
307
+ - name: to
308
+ schema: { type: string }
309
+ description: End time (ISO 8601) for the current/after window
310
+ - name: baseline-from
311
+ schema: { type: string }
312
+ description: Start time (ISO 8601) of the baseline/before window (enables Before/After comparison)
313
+ - name: baseline-to
314
+ schema: { type: string }
315
+ description: End time (ISO 8601) of the baseline/before window (enables Before/After comparison)
316
+ - name: format
317
+ aliases: [f]
318
+ schema:
319
+ type: string
320
+ enum: [markdown, json]
321
+ description: "Output format: markdown (default) | json"
322
+ - name: db
323
+ schema: { type: string }
324
+ description: Path to observability SQLite database (overrides default)
325
+ exits:
326
+ '0':
327
+ description: Report generated \u2014 Markdown or JSON on stdout
328
+ stdout:
329
+ format: text
330
+ '1':
331
+ description: Report generation failed
332
+ stderr:
333
+ format: text
224
334
  `;
225
- var CONTRACT_JSON_STR = '{\n "cli_contracts": "0.1.0",\n "info": {\n "title": "AaaC Observability CLI",\n "version": "0.1.0",\n "description": "aaac-observ \u2014 external event registration and query for @aaac/observability"\n },\n "command_sets": {\n "aaac-observ": {\n "summary": "aaac-observ \u2014 register external events and query the observability store",\n "executable": "aaac-observ",\n "commands": {\n "record": {\n "summary": "Register an external event into the observability pipeline (thin entrypoint for EventCollector.registerExternalEvent)",\n "options": [\n {\n "name": "event-type",\n "aliases": [\n "t"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Event type (e.g. promotion.commit, process.edit, agent.session)"\n },\n {\n "name": "lifecycle",\n "aliases": [\n "l"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "open",\n "close",\n "event",\n "instant"\n ]\n },\n "description": "Event lifecycle phase: open | close | event | instant"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID (auto-generated if omitted)"\n },\n {\n "name": "parent-span-id",\n "schema": {\n "type": "string"\n },\n "description": "Parent span ID for hierarchy"\n },\n {\n "name": "session-id",\n "schema": {\n "type": "string"\n },\n "description": "Session ID to associate with the event"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for distributed tracing"\n },\n {\n "name": "source",\n "schema": {\n "type": "string"\n },\n "description": "Event source identifier (default: external)"\n },\n {\n "name": "attr",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Attribute as key=value (repeatable, e.g. --attr git.commit=abc123)"\n },\n {\n "name": "link",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Cross-axis link as targetSpanId:linkType[:targetTraceId] (repeatable)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Event registered successfully",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "eventId": {\n "type": "string"\n },\n "spanId": {\n "type": "string"\n }\n }\n }\n }\n },\n "1": {\n "description": "Registration failed (validation error or write error)",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "record-hook": {\n "summary": "Record a Cursor/git hook event \u2014 parses stdin JSON, resolves event_mapping into 3-axis spans/links, injects session_id, and emits human-events (fail-open)",\n "arguments": [\n {\n "name": "hook-name",\n "index": 0,\n "required": true,\n "schema": {\n "type": "string"\n },\n "description": "Cursor/git hook name (e.g. afterFileEdit, subagentStart, beforeShellExecution)"\n }\n ],\n "options": [\n {\n "name": "mapping-config",\n "schema": {\n "type": "string"\n },\n "description": "Path to event-mapping.json (default: .agent-logs/config/event-mapping.json)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Hook recorded (or no-op when stdin/mapping unavailable) \u2014 always fail-open",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "recorded": {\n "type": "integer"\n },\n "fallback": {\n "type": "boolean"\n }\n }\n }\n }\n }\n }\n },\n "backfill": {\n "summary": "Re-emit historical spans from SQLite to an OTLP backend",\n "description": "Reads closed/instant spans from the local SQLite database and emits them\\nvia OtelEmitter to an OTLP-compatible backend.\\n\\nIMPORTANT \u2014 idempotency: The OTel SDK does not support specifying custom\\nspanIds, so each backfill invocation creates NEW spans in the backend.\\nRepeated invocations will produce DUPLICATE data. Use --from/--to to\\ncontrol scope. See #125 \xA77 for full idempotency guidance.\\n",\n "options": [\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n },\n {\n "name": "endpoint",\n "schema": {\n "type": "string"\n },\n "description": "OTLP HTTP endpoint URL (fallback: OTEL_EXPORTER_OTLP_ENDPOINT)"\n },\n {\n "name": "service-name",\n "schema": {\n "type": "string"\n },\n "description": "OTEL service name reported to backend (default: @aaac/observability)"\n },\n {\n "name": "from",\n "schema": {\n "type": "string"\n },\n "description": "Backfill spans starting at or after this ISO 8601 time"\n },\n {\n "name": "to",\n "schema": {\n "type": "string"\n },\n "description": "Backfill spans starting at or before this ISO 8601 time"\n },\n {\n "name": "event-type",\n "schema": {\n "type": "string"\n },\n "description": "Filter by event type (e.g. agent.session)"\n },\n {\n "name": "dry-run",\n "schema": {\n "type": "boolean"\n },\n "description": "Print span count without emitting to OTLP backend"\n }\n ],\n "exits": {\n "0": {\n "description": "Backfill completed",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "spansFound": {\n "type": "integer"\n },\n "spansEmitted": {\n "type": "integer"\n },\n "dryRun": {\n "type": "boolean"\n }\n }\n }\n }\n },\n "1": {\n "description": "Backfill failed (missing endpoint, DB error, etc.)",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "query": {\n "summary": "Query the observability store via QueryAdapter",\n "options": [\n {\n "name": "kind",\n "aliases": [\n "k"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "trace",\n "span",\n "search",\n "links"\n ]\n },\n "description": "Query kind: trace | span | search | links"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for trace queries (--kind trace)"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID for span/links queries (--kind span or --kind links)"\n },\n {\n "name": "event-type",\n "schema": {\n "type": "string"\n },\n "description": "Event type filter for search queries (--kind search)"\n },\n {\n "name": "task-id",\n "schema": {\n "type": "string"\n },\n "description": "Task ID filter for search queries (--kind search)"\n },\n {\n "name": "from",\n "schema": {\n "type": "string"\n },\n "description": "Start time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "to",\n "schema": {\n "type": "string"\n },\n "description": "End time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "direction",\n "aliases": [\n "d"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "forward",\n "reverse",\n "both"\n ]\n },\n "description": "Link traversal direction for links queries (default: both)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Query succeeded \u2014 results as JSON array",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object"\n }\n }\n },\n "1": {\n "description": "Query failed",\n "stderr": {\n "format": "text"\n }\n }\n }\n }\n }\n }\n }\n}';
335
+ var CONTRACT_JSON_STR = '{\n "cli_contracts": "0.1.0",\n "info": {\n "title": "AaaC Observability CLI",\n "version": "0.1.0",\n "description": "aaac-observ \u2014 external event registration and query for @aaac/observability"\n },\n "command_sets": {\n "aaac-observ": {\n "summary": "aaac-observ \u2014 register external events and query the observability store",\n "executable": "aaac-observ",\n "commands": {\n "record": {\n "summary": "Register an external event into the observability pipeline (thin entrypoint for EventCollector.registerExternalEvent)",\n "options": [\n {\n "name": "event-type",\n "aliases": [\n "t"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Event type (e.g. promotion.commit, process.edit, agent.session)"\n },\n {\n "name": "lifecycle",\n "aliases": [\n "l"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "open",\n "close",\n "event",\n "instant"\n ]\n },\n "description": "Event lifecycle phase: open | close | event | instant"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID (auto-generated if omitted)"\n },\n {\n "name": "parent-span-id",\n "schema": {\n "type": "string"\n },\n "description": "Parent span ID for hierarchy"\n },\n {\n "name": "session-id",\n "schema": {\n "type": "string"\n },\n "description": "Session ID to associate with the event"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for distributed tracing"\n },\n {\n "name": "source",\n "schema": {\n "type": "string"\n },\n "description": "Event source identifier (default: external)"\n },\n {\n "name": "attr",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Attribute as key=value (repeatable, e.g. --attr git.commit=abc123)"\n },\n {\n "name": "link",\n "schema": {\n "type": "array",\n "items": {\n "type": "string"\n }\n },\n "description": "Cross-axis link as targetSpanId:linkType[:targetTraceId] (repeatable)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Event registered successfully",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "eventId": {\n "type": "string"\n },\n "spanId": {\n "type": "string"\n }\n }\n }\n }\n },\n "1": {\n "description": "Registration failed (validation error or write error)",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "record-hook": {\n "summary": "Record a Cursor/git hook event \u2014 parses stdin JSON, resolves event_mapping into 3-axis spans/links, injects session_id, and emits human-events (fail-open)",\n "arguments": [\n {\n "name": "hook-name",\n "index": 0,\n "required": true,\n "schema": {\n "type": "string"\n },\n "description": "Cursor/git hook name (e.g. afterFileEdit, subagentStart, beforeShellExecution)"\n }\n ],\n "options": [\n {\n "name": "mapping-config",\n "schema": {\n "type": "string"\n },\n "description": "Path to event-mapping.json (default: .agent-logs/config/event-mapping.json)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Hook recorded (or no-op when stdin/mapping unavailable) \u2014 always fail-open",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "recorded": {\n "type": "integer"\n },\n "fallback": {\n "type": "boolean"\n }\n }\n }\n }\n }\n }\n },\n "backfill": {\n "summary": "Re-emit historical spans from SQLite to an OTLP backend",\n "description": "Reads closed/instant spans from the local SQLite database and emits them\\nvia OtelEmitter to an OTLP-compatible backend.\\n\\nIMPORTANT \u2014 idempotency: The OTel SDK does not support specifying custom\\nspanIds, so each backfill invocation creates NEW spans in the backend.\\nRepeated invocations will produce DUPLICATE data. Use --from/--to to\\ncontrol scope. See #125 \xA77 for full idempotency guidance.\\n",\n "options": [\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n },\n {\n "name": "endpoint",\n "schema": {\n "type": "string"\n },\n "description": "OTLP HTTP endpoint URL (fallback: OTEL_EXPORTER_OTLP_ENDPOINT)"\n },\n {\n "name": "service-name",\n "schema": {\n "type": "string"\n },\n "description": "OTEL service name reported to backend (default: @aaac/observability)"\n },\n {\n "name": "from",\n "schema": {\n "type": "string"\n },\n "description": "Backfill spans starting at or after this ISO 8601 time"\n },\n {\n "name": "to",\n "schema": {\n "type": "string"\n },\n "description": "Backfill spans starting at or before this ISO 8601 time"\n },\n {\n "name": "event-type",\n "schema": {\n "type": "string"\n },\n "description": "Filter by event type (e.g. agent.session)"\n },\n {\n "name": "dry-run",\n "schema": {\n "type": "boolean"\n },\n "description": "Print span count without emitting to OTLP backend"\n },\n {\n "name": "flush-timeout-ms",\n "schema": {\n "type": "integer"\n },\n "description": "Maximum ms to wait for OTLP flush before exit (default: 2000; use \u226530000 for >5k spans)"\n }\n ],\n "exits": {\n "0": {\n "description": "Backfill completed",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "spansFound": {\n "type": "integer"\n },\n "spansEmitted": {\n "type": "integer"\n },\n "dryRun": {\n "type": "boolean"\n }\n }\n }\n }\n },\n "1": {\n "description": "Backfill failed (missing endpoint, DB error, etc.)",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "ingest-vendor-otel": {\n "summary": "Project a vendor OTLP/JSON trace export (e.g. Claude Code token/cost/trace) into the SQLite Ledger",\n "description": "Canonical Projection batch (observability-integration.md \xA79.2 / T-D2).\\n\\nReads a vendor OTLP/JSON trace export from --input (file) or stdin, projects\\neach span into a Canonical Event, and backfills it into the SQLite Ledger.\\n\\nIdempotent: every event carries a deterministic dedup_key\\n(vendor-otel:{traceId}:{spanId}) enforced by UNIQUE + INSERT OR IGNORE, so\\nre-running on the same export inserts nothing new (reported as\\nskippedDuplicate). Hook/runtime records remain the primary source; vendor\\nOTEL is auxiliary evidence.\\n",\n "options": [\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n },\n {\n "name": "input",\n "aliases": [\n "i"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Path to the OTLP/JSON trace export (reads stdin when omitted)"\n },\n {\n "name": "source",\n "schema": {\n "type": "string"\n },\n "description": "Source tag for projected events (default: claude-code-otel)"\n },\n {\n "name": "dry-run",\n "schema": {\n "type": "boolean"\n },\n "description": "Parse and project without writing to the Ledger"\n }\n ],\n "exits": {\n "0": {\n "description": "Ingestion completed",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object",\n "properties": {\n "found": {\n "type": "integer"\n },\n "inserted": {\n "type": "integer"\n },\n "skippedDuplicate": {\n "type": "integer"\n },\n "skippedInvalid": {\n "type": "integer"\n },\n "dryRun": {\n "type": "boolean"\n }\n }\n }\n }\n },\n "1": {\n "description": "Ingestion failed (unreadable input, invalid JSON, DB error)",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "query": {\n "summary": "Query the observability store via QueryAdapter",\n "options": [\n {\n "name": "kind",\n "aliases": [\n "k"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "trace",\n "span",\n "search",\n "links",\n "issue"\n ]\n },\n "description": "Query kind: trace | span | search | links | issue"\n },\n {\n "name": "issue",\n "schema": {\n "type": "string"\n },\n "description": "Issue ID for issue queries (--kind issue)"\n },\n {\n "name": "trace-id",\n "schema": {\n "type": "string"\n },\n "description": "Trace ID for trace queries (--kind trace)"\n },\n {\n "name": "span-id",\n "aliases": [\n "s"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Span ID for span/links queries (--kind span or --kind links)"\n },\n {\n "name": "event-type",\n "schema": {\n "type": "string"\n },\n "description": "Event type filter for search queries (--kind search)"\n },\n {\n "name": "task-id",\n "schema": {\n "type": "string"\n },\n "description": "Task ID filter for search queries (--kind search)"\n },\n {\n "name": "from",\n "schema": {\n "type": "string"\n },\n "description": "Start time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "to",\n "schema": {\n "type": "string"\n },\n "description": "End time in ISO 8601 format for search queries (--kind search)"\n },\n {\n "name": "direction",\n "aliases": [\n "d"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "forward",\n "reverse",\n "both"\n ]\n },\n "description": "Link traversal direction for links queries (default: both)"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Query succeeded \u2014 results as JSON array",\n "stdout": {\n "format": "json",\n "schema": {\n "type": "object"\n }\n }\n },\n "1": {\n "description": "Query failed",\n "stderr": {\n "format": "text"\n }\n }\n }\n },\n "report": {\n "summary": "Generate an Observability diagnostic report (Layer 1-3 + link completion + Person Tier) from the SQLite Ledger",\n "description": "Customer-facing Observability diagnostic (#179 WS-F T-F1). Computes the\\nsame Layer 1-3 / link-completion / Person-Tier metrics rendered by the\\n`aaac-diagnostic` Grafana dashboard, over the local SQLite Ledger.\\n\\nOutput is Markdown (default) or JSON. Use --project-id to scope to a\\nsingle project (internal or a cooperating customer project). Supply\\n--baseline-from / --baseline-to to add a Before/After comparison\\nsection against a baseline window; --from / --to bound the current\\n(after) window.\\n",\n "options": [\n {\n "name": "project-id",\n "aliases": [\n "p"\n ],\n "schema": {\n "type": "string"\n },\n "description": "Restrict the report to a single project_id (project.id attr / service.name)"\n },\n {\n "name": "from",\n "schema": {\n "type": "string"\n },\n "description": "Start time (ISO 8601) for the current/after window"\n },\n {\n "name": "to",\n "schema": {\n "type": "string"\n },\n "description": "End time (ISO 8601) for the current/after window"\n },\n {\n "name": "baseline-from",\n "schema": {\n "type": "string"\n },\n "description": "Start time (ISO 8601) of the baseline/before window (enables Before/After comparison)"\n },\n {\n "name": "baseline-to",\n "schema": {\n "type": "string"\n },\n "description": "End time (ISO 8601) of the baseline/before window (enables Before/After comparison)"\n },\n {\n "name": "format",\n "aliases": [\n "f"\n ],\n "schema": {\n "type": "string",\n "enum": [\n "markdown",\n "json"\n ]\n },\n "description": "Output format: markdown (default) | json"\n },\n {\n "name": "db",\n "schema": {\n "type": "string"\n },\n "description": "Path to observability SQLite database (overrides default)"\n }\n ],\n "exits": {\n "0": {\n "description": "Report generated \u2014 Markdown or JSON on stdout",\n "stdout": {\n "format": "text"\n }\n },\n "1": {\n "description": "Report generation failed",\n "stderr": {\n "format": "text"\n }\n }\n }\n }\n }\n }\n }\n}';
226
336
 
227
337
  // src/generated/program.ts
228
338
  function createProgram(handlers2, version) {
@@ -234,12 +344,18 @@ function createProgram(handlers2, version) {
234
344
  program2.command("record-hook").description("Record a Cursor/git hook event \u2014 parses stdin JSON, resolves event_mapping into 3-axis spans/links, injects session_id, and emits human-events (fail-open)").argument("<hook-name>", "Cursor/git hook name (e.g. afterFileEdit, subagentStart, beforeShellExecution)").option("--mapping-config <value>", "Path to event-mapping.json (default: .agent-logs/config/event-mapping.json)").option("--db <value>", "Path to observability SQLite database (overrides default)").action(async (hookName, opts, cmd) => {
235
345
  await handlers2.recordHook(hookName, opts, cmd.optsWithGlobals());
236
346
  });
237
- program2.command("backfill").description("Re-emit historical spans from SQLite to an OTLP backend").option("--db <value>", "Path to observability SQLite database (overrides default)").option("--endpoint <value>", "OTLP HTTP endpoint URL (fallback: OTEL_EXPORTER_OTLP_ENDPOINT)").option("--service-name <value>", "OTEL service name reported to backend (default: @aaac/observability)").option("--from <value>", "Backfill spans starting at or after this ISO 8601 time").option("--to <value>", "Backfill spans starting at or before this ISO 8601 time").option("--event-type <value>", "Filter by event type (e.g. agent.session)").option("--dry-run", "Print span count without emitting to OTLP backend").option("--flush-timeout-ms <ms>", "Maximum ms to wait for OTLP flush before exit (default: 2000; use \u226530000 for >5k spans)", (v) => parseInt(v, 10)).action(async (opts, cmd) => {
347
+ program2.command("backfill").description("Re-emit historical spans from SQLite to an OTLP backend").option("--db <value>", "Path to observability SQLite database (overrides default)").option("--endpoint <value>", "OTLP HTTP endpoint URL (fallback: OTEL_EXPORTER_OTLP_ENDPOINT)").option("--service-name <value>", "OTEL service name reported to backend (default: @aaac/observability)").option("--from <value>", "Backfill spans starting at or after this ISO 8601 time").option("--to <value>", "Backfill spans starting at or before this ISO 8601 time").option("--event-type <value>", "Filter by event type (e.g. agent.session)").option("--dry-run", "Print span count without emitting to OTLP backend").option("--flush-timeout-ms <value>", "Maximum ms to wait for OTLP flush before exit (default: 2000; use \u226530000 for >5k spans)").action(async (opts, cmd) => {
238
348
  await handlers2.backfill(opts, cmd.optsWithGlobals());
239
349
  });
240
- program2.command("query").description("Query the observability store via QueryAdapter").option("-k, --kind <value>", "Query kind: trace | span | search | links").option("--trace-id <value>", "Trace ID for trace queries (--kind trace)").option("-s, --span-id <value>", "Span ID for span/links queries (--kind span or --kind links)").option("--event-type <value>", "Event type filter for search queries (--kind search)").option("--task-id <value>", "Task ID filter for search queries (--kind search)").option("--from <value>", "Start time in ISO 8601 format for search queries (--kind search)").option("--to <value>", "End time in ISO 8601 format for search queries (--kind search)").option("-d, --direction <value>", "Link traversal direction for links queries (default: both)").option("--db <value>", "Path to observability SQLite database (overrides default)").action(async (opts, cmd) => {
350
+ program2.command("ingest-vendor-otel").description("Project a vendor OTLP/JSON trace export (e.g. Claude Code token/cost/trace) into the SQLite Ledger").option("--db <value>", "Path to observability SQLite database (overrides default)").option("-i, --input <value>", "Path to the OTLP/JSON trace export (reads stdin when omitted)").option("--source <value>", "Source tag for projected events (default: claude-code-otel)").option("--dry-run", "Parse and project without writing to the Ledger").action(async (opts, cmd) => {
351
+ await handlers2.ingestVendorOtel(opts, cmd.optsWithGlobals());
352
+ });
353
+ program2.command("query").description("Query the observability store via QueryAdapter").option("-k, --kind <value>", "Query kind: trace | span | search | links | issue").option("--issue <value>", "Issue ID for issue queries (--kind issue)").option("--trace-id <value>", "Trace ID for trace queries (--kind trace)").option("-s, --span-id <value>", "Span ID for span/links queries (--kind span or --kind links)").option("--event-type <value>", "Event type filter for search queries (--kind search)").option("--task-id <value>", "Task ID filter for search queries (--kind search)").option("--from <value>", "Start time in ISO 8601 format for search queries (--kind search)").option("--to <value>", "End time in ISO 8601 format for search queries (--kind search)").option("-d, --direction <value>", "Link traversal direction for links queries (default: both)").option("--db <value>", "Path to observability SQLite database (overrides default)").action(async (opts, cmd) => {
241
354
  await handlers2.query(opts, cmd.optsWithGlobals());
242
355
  });
356
+ program2.command("report").description("Generate an Observability diagnostic report (Layer 1-3 + link completion + Person Tier) from the SQLite Ledger").option("-p, --project-id <value>", "Restrict the report to a single project_id (project.id attr / service.name)").option("--from <value>", "Start time (ISO 8601) for the current/after window").option("--to <value>", "End time (ISO 8601) for the current/after window").option("--baseline-from <value>", "Start time (ISO 8601) of the baseline/before window (enables Before/After comparison)").option("--baseline-to <value>", "End time (ISO 8601) of the baseline/before window (enables Before/After comparison)").option("-f, --format <value>", "Output format: markdown (default) | json").option("--db <value>", "Path to observability SQLite database (overrides default)").action(async (opts, cmd) => {
357
+ await handlers2.report(opts, cmd.optsWithGlobals());
358
+ });
243
359
  program2.command("extract").description("Extract contract specification for this CLI tool.").argument("[commands...]", "Command IDs to extract. Use dot notation.").option("-a, --all", "Extract all commands.", false).option("--include-meta", "Include extraction metadata.", true).option("-F, --format <format>", "Output format (yaml or json).", "yaml").action(async (commands, opts, cmd) => {
244
360
  if (commands.length === 0 && !opts.all) {
245
361
  process.stderr.write(JSON.stringify({ code: "INVALID_ARGS", message: "Specify command IDs or use --all" }) + "\n");
@@ -257,7 +373,7 @@ function createProgram(handlers2, version) {
257
373
  type: "cli-contracts/extract",
258
374
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
259
375
  specVersion: doc.cli_contracts ?? "0.1.0",
260
- commands: ["aaac-observ.record", "aaac-observ.record-hook", "aaac-observ.backfill", "aaac-observ.query"]
376
+ commands: ["aaac-observ.record", "aaac-observ.record-hook", "aaac-observ.backfill", "aaac-observ.ingest-vendor-otel", "aaac-observ.query", "aaac-observ.report"]
261
377
  };
262
378
  }
263
379
  Object.assign(out, doc);
@@ -274,7 +390,7 @@ function createProgram(handlers2, version) {
274
390
  yamlLines.push("extractedAt: " + (/* @__PURE__ */ new Date()).toISOString());
275
391
  yamlLines.push("spec_version: " + (doc.cli_contracts ?? "0.1.0"));
276
392
  yamlLines.push("commands:");
277
- for (const id of ["aaac-observ.record", "aaac-observ.record-hook", "aaac-observ.backfill", "aaac-observ.query"]) {
393
+ for (const id of ["aaac-observ.record", "aaac-observ.record-hook", "aaac-observ.backfill", "aaac-observ.ingest-vendor-otel", "aaac-observ.query", "aaac-observ.report"]) {
278
394
  yamlLines.push(" - " + id);
279
395
  }
280
396
  }
@@ -419,203 +535,15 @@ async function handleRecord(options, _parentOpts) {
419
535
  process.stdout.write(JSON.stringify(result) + "\n");
420
536
  }
421
537
 
422
- // src/cli/record-hook.ts
423
- import { mkdir, rename, unlink, writeFile } from "fs/promises";
424
- import { dirname, join } from "path";
425
- var GIT_CONTEXT_COMMAND_RE = /\bgit\s+(?:commit|push|merge)\b/;
426
- var GIT_HOOK_NAMES = /* @__PURE__ */ new Set([
427
- "pre-commit",
428
- "post-commit",
429
- "pre-push",
430
- "post-merge",
431
- "post-checkout"
432
- ]);
433
- function defaultMappingConfigPath(dbPath) {
434
- return join(dirname(dbPath), "config", "event-mapping.json");
435
- }
436
- function contextFilePath(dbPath) {
437
- return join(dirname(dbPath), ".observ-context.json");
438
- }
439
- async function readStdin() {
440
- if (process.stdin.isTTY) return "";
441
- return new Promise((resolve2) => {
442
- let data = "";
443
- let settled = false;
444
- const finish = () => {
445
- if (settled) return;
446
- settled = true;
447
- resolve2(data);
448
- };
449
- try {
450
- process.stdin.setEncoding("utf8");
451
- process.stdin.on("data", (chunk) => {
452
- data += chunk;
453
- });
454
- process.stdin.on("end", finish);
455
- process.stdin.on("error", finish);
456
- process.stdin.on("close", finish);
457
- const t = setTimeout(finish, 1e3);
458
- if (typeof t.unref === "function") t.unref();
459
- } catch {
460
- finish();
461
- }
462
- });
463
- }
464
- function parseHookInput(raw) {
465
- const trimmed = raw.trim();
466
- if (!trimmed) return {};
467
- try {
468
- const parsed = JSON.parse(trimmed);
469
- return parsed !== null && typeof parsed === "object" ? parsed : {};
470
- } catch {
471
- return {};
472
- }
473
- }
474
- function asString(value) {
475
- return typeof value === "string" && value.length > 0 ? value : void 0;
476
- }
477
- function asNumber(value) {
478
- if (typeof value === "number" && Number.isFinite(value)) return value;
479
- if (typeof value === "string" && value.trim() !== "") {
480
- const n = Number(value);
481
- if (Number.isFinite(n)) return n;
482
- }
483
- return void 0;
484
- }
485
- function emitMappedSpans(collector, hook, rule, hookInput, sessionId) {
486
- const { spans, links } = evaluateMapping(rule, hookInput);
487
- const linksBySpan = /* @__PURE__ */ new Map();
488
- for (const link of links) {
489
- const arr = linksBySpan.get(link.fromSpanId) ?? [];
490
- arr.push({ targetSpanId: link.toSpanId, linkType: link.linkType });
491
- linksBySpan.set(link.fromSpanId, arr);
492
- }
493
- const parentConversationId = hook === "subagentStart" ? asString(hookInput.parent_conversation_id) : void 0;
494
- for (const span of spans) {
495
- const attributes = { ...span.attributes };
496
- if (sessionId) attributes["session_id"] = sessionId;
497
- if (parentConversationId) {
498
- attributes["parent_conversation_id"] = parentConversationId;
499
- }
500
- collector.emit({
501
- source: "cursor-hook",
502
- eventType: span.eventType,
503
- lifecycle: span.lifecycle,
504
- spanId: span.spanId,
505
- parentSpanId: span.parentSpanId,
506
- attributes,
507
- links: linksBySpan.get(span.spanId) ?? []
508
- });
509
- }
510
- return spans.length;
511
- }
512
- function emitGenericFallback(collector, hook, sessionId) {
513
- const eventType = (GIT_HOOK_NAMES.has(hook) ? "git." : "cursor.") + hook;
514
- const attributes = { "hook.name": hook };
515
- if (sessionId) attributes["session_id"] = sessionId;
516
- collector.emit({
517
- source: "cursor-hook",
518
- eventType,
519
- lifecycle: "instant",
520
- attributes
521
- });
522
- return 1;
523
- }
524
- function emitHumanEvents(collector, hook, hookInput, sessionId) {
525
- let count = 0;
526
- if (hook === "beforeSubmitPrompt") {
527
- const prompt = asString(hookInput.prompt);
528
- if (prompt !== void 0) {
529
- const attachments = Array.isArray(hookInput.attachments) ? hookInput.attachments : [];
530
- emitHumanInstruction(collector, { sessionId, prompt, attachments });
531
- count += 1;
532
- }
533
- } else if (hook === "afterShellExecution") {
534
- const command = asString(hookInput.command);
535
- if (command !== void 0) {
536
- const exitCode = asNumber(hookInput.exit_code) ?? asNumber(hookInput.exitCode) ?? 0;
537
- const durationMs = asNumber(hookInput.duration);
538
- if (emitQualityGateResult(collector, { sessionId, command, exitCode, durationMs })) {
539
- count += 1;
540
- }
541
- const output = asString(hookInput.output) ?? "";
542
- if (emitPromotionPr(collector, { sessionId, command, output })) {
543
- count += 1;
544
- }
545
- }
546
- }
547
- return count;
548
- }
549
- async function manageContextFile(hook, hookInput, sessionId, dbPath) {
550
- const command = asString(hookInput.command) ?? "";
551
- const isGitContextCommand = GIT_CONTEXT_COMMAND_RE.test(command);
552
- const file = contextFilePath(dbPath);
553
- if (hook === "beforeShellExecution" && isGitContextCommand) {
554
- await mkdir(dirname(file), { recursive: true });
555
- const payload = JSON.stringify({
556
- session_id: sessionId || null,
557
- nonce: generateId(),
558
- created_at_ms: Date.now()
559
- }) + "\n";
560
- const tmp = `${file}.${process.pid}.${generateId()}.tmp`;
561
- await writeFile(tmp, payload, "utf8");
562
- await rename(tmp, file);
563
- } else if (hook === "afterShellExecution" && isGitContextCommand) {
564
- await unlink(file).catch(() => {
565
- });
566
- }
567
- }
568
- async function runRecordHook(args) {
569
- const { hook, hookInput, dbPath, mappingConfigPath } = args;
570
- const sessionId = asString(hookInput.conversation_id) ?? "";
571
- await manageContextFile(hook, hookInput, sessionId, dbPath).catch(() => {
572
- });
573
- let config;
574
- try {
575
- config = await loadEventMappingConfig(mappingConfigPath);
576
- } catch {
577
- config = void 0;
578
- }
579
- const rule = config?.mappings?.[hook];
580
- let recorded = 0;
581
- let fallback = false;
582
- const { collector, sink } = createPipeline({ dbPath });
583
- try {
584
- if (rule) {
585
- recorded += emitMappedSpans(collector, hook, rule, hookInput, sessionId);
586
- } else {
587
- recorded += emitGenericFallback(collector, hook, sessionId);
588
- fallback = true;
589
- }
590
- recorded += emitHumanEvents(collector, hook, hookInput, sessionId);
591
- } finally {
592
- sink.close();
593
- }
594
- return { recorded, fallback };
595
- }
596
- async function handleRecordHook(hookName, options, _parentOpts) {
597
- const hook = hookName ?? "unknown";
598
- const dbPath = options.db ?? DEFAULT_DB_PATH;
599
- const mappingConfigPath = options.mappingConfig ?? defaultMappingConfigPath(dbPath);
600
- let result = { recorded: 0, fallback: false };
601
- try {
602
- const hookInput = parseHookInput(await readStdin());
603
- result = await runRecordHook({ hook, hookInput, dbPath, mappingConfigPath });
604
- } catch (err) {
605
- process.stderr.write(
606
- `record-hook: ${err instanceof Error ? err.message : String(err)}
607
- `
608
- );
609
- }
610
- process.stdout.write(JSON.stringify(result) + "\n");
611
- }
612
-
613
538
  // src/cli/query.ts
614
539
  function bigIntReplacer(_key, value) {
615
540
  return typeof value === "bigint" ? value.toString() : value;
616
541
  }
617
542
  async function handleQuery(options, _parentOpts) {
618
- const validKinds = ["trace", "span", "search", "links"];
543
+ if (options.issue && !options.kind) {
544
+ options.kind = "issue";
545
+ }
546
+ const validKinds = ["trace", "span", "search", "links", "issue"];
619
547
  if (!options.kind) {
620
548
  process.stderr.write(
621
549
  `Error: --kind (-k) is required (${validKinds.join("|")})
@@ -643,6 +571,10 @@ async function handleQuery(options, _parentOpts) {
643
571
  process.stderr.write("Error: --span-id is required for --kind links\n");
644
572
  process.exit(1);
645
573
  }
574
+ if (kind === "issue" && !options.issue) {
575
+ process.stderr.write("Error: --issue is required for --kind issue\n");
576
+ process.exit(1);
577
+ }
646
578
  const dbPath = options.db ?? DEFAULT_DB_PATH;
647
579
  const adapter = new SqliteQueryAdapter(dbPath);
648
580
  try {
@@ -677,12 +609,34 @@ async function handleQuery(options, _parentOpts) {
677
609
  result = adapter.getLinks(options.spanId, dir);
678
610
  break;
679
611
  }
612
+ // ── issue: Outcome chain analysis for an Issue (#178 T-E2) ──────────
613
+ case "issue": {
614
+ result = analyzeOutcomeChain(adapter, options.issue);
615
+ break;
616
+ }
680
617
  }
681
618
  process.stdout.write(JSON.stringify(result, bigIntReplacer) + "\n");
682
619
  } catch (err) {
683
620
  const msg = err instanceof Error ? err.message : String(err);
684
621
  if (msg.includes("no such table") || msg.includes("no such column")) {
685
- const emptyResult = kind === "span" ? null : [];
622
+ let emptyResult;
623
+ if (kind === "span") {
624
+ emptyResult = null;
625
+ } else if (kind === "issue") {
626
+ emptyResult = {
627
+ issueId: options.issue ?? "",
628
+ stages: OUTCOME_CHAIN_STAGES.map((s) => ({
629
+ eventType: s,
630
+ status: "missing",
631
+ spanIds: []
632
+ })),
633
+ complete: false,
634
+ gaps: [...OUTCOME_CHAIN_STAGES],
635
+ provisional: []
636
+ };
637
+ } else {
638
+ emptyResult = [];
639
+ }
686
640
  process.stdout.write(JSON.stringify(emptyResult, bigIntReplacer) + "\n");
687
641
  adapter.close();
688
642
  return;
@@ -787,7 +741,7 @@ async function handleBackfill(options, _parentOpts) {
787
741
  mappedSpans.push(spanRecordToMappedSpan(span, links));
788
742
  }
789
743
  adapter.close();
790
- const effectiveFlushMs = options.flushTimeoutMs ?? 2e3;
744
+ const effectiveFlushMs = options.flushTimeoutMs != null ? Number(options.flushTimeoutMs) : 2e3;
791
745
  if (spansFound > 1e3 && effectiveFlushMs < 1e4) {
792
746
  process.stderr.write(
793
747
  `Warning: ${spansFound} spans queued but --flush-timeout-ms is ${effectiveFlushMs} ms (< 10 000 ms). Large batches may not fully flush before the deadline. Recommended: --flush-timeout-ms 30000
@@ -814,16 +768,182 @@ async function handleBackfill(options, _parentOpts) {
814
768
  process.stdout.write(JSON.stringify(result) + "\n");
815
769
  }
816
770
 
771
+ // src/cli/ingest-vendor-otel.ts
772
+ import { readFile } from "fs/promises";
773
+ async function readStdin() {
774
+ if (process.stdin.isTTY) return "";
775
+ return new Promise((resolve2) => {
776
+ let data = "";
777
+ let settled = false;
778
+ const finish = () => {
779
+ if (settled) return;
780
+ settled = true;
781
+ resolve2(data);
782
+ };
783
+ try {
784
+ process.stdin.setEncoding("utf8");
785
+ process.stdin.on("data", (chunk) => {
786
+ data += chunk;
787
+ });
788
+ process.stdin.on("end", finish);
789
+ process.stdin.on("error", finish);
790
+ process.stdin.on("close", finish);
791
+ } catch {
792
+ finish();
793
+ }
794
+ });
795
+ }
796
+ async function handleIngestVendorOtel(options, _parentOpts) {
797
+ const dbPath = options.db ?? DEFAULT_DB_PATH;
798
+ const dryRun = options.dryRun ?? false;
799
+ const source = options.source ?? VENDOR_OTEL_SOURCE;
800
+ let raw;
801
+ try {
802
+ raw = options.input ? await readFile(options.input, "utf8") : await readStdin();
803
+ } catch (err) {
804
+ process.stderr.write(
805
+ `Error: failed to read input \u2014 ${err instanceof Error ? err.message : String(err)}
806
+ `
807
+ );
808
+ process.exit(1);
809
+ return;
810
+ }
811
+ if (raw.trim() === "") {
812
+ process.stderr.write(
813
+ "Error: no input \u2014 provide --input <file> or pipe an OTLP/JSON trace export via stdin\n"
814
+ );
815
+ process.exit(1);
816
+ return;
817
+ }
818
+ let doc;
819
+ try {
820
+ doc = JSON.parse(raw);
821
+ } catch (err) {
822
+ process.stderr.write(
823
+ `Error: invalid JSON input \u2014 ${err instanceof Error ? err.message : String(err)}
824
+ `
825
+ );
826
+ process.exit(1);
827
+ return;
828
+ }
829
+ const { events, skipped: skippedInvalid } = mapVendorOtelExport(doc, { source });
830
+ const found = events.length;
831
+ if (dryRun) {
832
+ const result2 = {
833
+ found,
834
+ inserted: 0,
835
+ skippedDuplicate: 0,
836
+ skippedInvalid,
837
+ dryRun: true
838
+ };
839
+ process.stdout.write(JSON.stringify(result2) + "\n");
840
+ return;
841
+ }
842
+ const sink = new SqliteSink(dbPath);
843
+ let inserted = 0;
844
+ let skippedDuplicate = 0;
845
+ try {
846
+ for (const event of events) {
847
+ if (sink.write(event)) {
848
+ inserted += 1;
849
+ } else {
850
+ skippedDuplicate += 1;
851
+ }
852
+ }
853
+ } catch (err) {
854
+ sink.close();
855
+ process.stderr.write(
856
+ `Error: failed to write to Ledger \u2014 ${err instanceof Error ? err.message : String(err)}
857
+ `
858
+ );
859
+ process.exit(1);
860
+ return;
861
+ }
862
+ sink.close();
863
+ const result = {
864
+ found,
865
+ inserted,
866
+ skippedDuplicate,
867
+ skippedInvalid,
868
+ dryRun: false
869
+ };
870
+ process.stdout.write(JSON.stringify(result) + "\n");
871
+ }
872
+
873
+ // src/cli/report.ts
874
+ function emptyReport(options) {
875
+ const report = {
876
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
877
+ projectId: options.projectId ?? null,
878
+ period: { from: options.from ?? null, to: options.to ?? null },
879
+ metrics: {
880
+ layer1: [],
881
+ layer2: [],
882
+ layer3: [],
883
+ linkCompletion: { sessionLinkRate: [], stageArrival: [], provisional: [] },
884
+ tierRatio: [],
885
+ summary: {
886
+ issueCount: 0,
887
+ sessionCount: 0,
888
+ totalCostUsd: 0,
889
+ avgLeverage: 0,
890
+ avgCommitRate: 0,
891
+ avgCiPassRate: 0,
892
+ avgChainCompleteness: 0,
893
+ sessionLinkRate: 0,
894
+ tierCounts: { high: 0, mid: 0, low: 0 }
895
+ }
896
+ }
897
+ };
898
+ return report;
899
+ }
900
+ async function handleReport(options, _parentOpts) {
901
+ const validFormats = ["markdown", "json"];
902
+ if (options.format !== void 0 && !validFormats.includes(options.format)) {
903
+ process.stderr.write(`Error: --format must be one of: ${validFormats.join("|")}
904
+ `);
905
+ process.exit(1);
906
+ }
907
+ const format = options.format ?? "markdown";
908
+ const render = (report) => format === "json" ? renderReportJson(report) : renderReportMarkdown(report);
909
+ const dbPath = options.db ?? DEFAULT_DB_PATH;
910
+ const adapter = new SqliteQueryAdapter(dbPath);
911
+ try {
912
+ const reportOptions = {};
913
+ if (options.projectId !== void 0) reportOptions.projectId = options.projectId;
914
+ if (options.from !== void 0) reportOptions.from = options.from;
915
+ if (options.to !== void 0) reportOptions.to = options.to;
916
+ if (options.baselineFrom !== void 0) reportOptions.baselineFrom = options.baselineFrom;
917
+ if (options.baselineTo !== void 0) reportOptions.baselineTo = options.baselineTo;
918
+ const report = generateDiagnosticReport(adapter, reportOptions);
919
+ process.stdout.write(render(report) + "\n");
920
+ } catch (err) {
921
+ const msg = err instanceof Error ? err.message : String(err);
922
+ if (msg.includes("no such table") || msg.includes("no such column")) {
923
+ process.stdout.write(render(emptyReport(options)) + "\n");
924
+ adapter.close();
925
+ return;
926
+ }
927
+ process.stderr.write(`Error: ${msg}
928
+ `);
929
+ adapter.close();
930
+ process.exit(1);
931
+ }
932
+ adapter.close();
933
+ }
934
+
817
935
  // src/cli/handlers.ts
818
936
  var handlers = {
819
937
  record: handleRecord,
820
938
  recordHook: handleRecordHook,
821
939
  query: handleQuery,
822
- backfill: handleBackfill
940
+ backfill: handleBackfill,
941
+ ingestVendorOtel: handleIngestVendorOtel,
942
+ report: handleReport
823
943
  };
824
944
 
825
945
  // src/cli/index.ts
826
- var __dirname = dirname2(fileURLToPath(import.meta.url));
946
+ var __dirname = dirname(fileURLToPath(import.meta.url));
827
947
  var pkg = JSON.parse(
828
948
  readFileSync(resolve(__dirname, "../../package.json"), "utf8")
829
949
  );