@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/{chunk-J2F5GEMO.js → chunk-3DXZNA3E.js} +1011 -13
- package/dist/chunk-3DXZNA3E.js.map +1 -0
- package/dist/chunk-EKFRH7PX.js +266 -0
- package/dist/chunk-EKFRH7PX.js.map +1 -0
- package/dist/cli/global-record.d.ts +53 -0
- package/dist/cli/global-record.js +239 -0
- package/dist/cli/global-record.js.map +1 -0
- package/dist/cli/index.js +330 -210
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +385 -6
- package/dist/index.js +43 -3
- package/package.json +3 -2
- package/dist/chunk-J2F5GEMO.js.map +0 -1
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
|
-
|
|
8
|
-
emitPromotionPr,
|
|
9
|
-
emitQualityGateResult,
|
|
10
|
-
evaluateMapping,
|
|
14
|
+
generateDiagnosticReport,
|
|
11
15
|
generateId,
|
|
12
16
|
isoToUnixNano,
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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 <
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
946
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
827
947
|
var pkg = JSON.parse(
|
|
828
948
|
readFileSync(resolve(__dirname, "../../package.json"), "utf8")
|
|
829
949
|
);
|