@aaac/observability 0.1.13 → 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.
@@ -154,6 +154,112 @@ function extractString(attrs, key) {
154
154
  return typeof v === "string" && v.length > 0 ? v : void 0;
155
155
  }
156
156
 
157
+ // src/normalize/vendor-otel-mapper.ts
158
+ var VENDOR_OTEL_SOURCE = "claude-code-otel";
159
+ var VENDOR_OTEL_DEDUP_PREFIX = "vendor-otel";
160
+ function convertAnyValue(value) {
161
+ if (value === void 0 || value === null) return void 0;
162
+ if (typeof value.stringValue === "string") return value.stringValue;
163
+ if (typeof value.boolValue === "boolean") return value.boolValue;
164
+ if (value.intValue !== void 0) {
165
+ const n = typeof value.intValue === "string" ? Number(value.intValue) : value.intValue;
166
+ return Number.isFinite(n) ? n : String(value.intValue);
167
+ }
168
+ if (typeof value.doubleValue === "number") return value.doubleValue;
169
+ if (value.arrayValue !== void 0) return JSON.stringify(value.arrayValue);
170
+ if (value.kvlistValue !== void 0) return JSON.stringify(value.kvlistValue);
171
+ return void 0;
172
+ }
173
+ function convertAttributes(attrs) {
174
+ const out = {};
175
+ if (!Array.isArray(attrs)) return out;
176
+ for (const kv of attrs) {
177
+ if (!kv || typeof kv.key !== "string" || kv.key.length === 0) continue;
178
+ const v = convertAnyValue(kv.value);
179
+ if (v !== void 0) out[kv.key] = v;
180
+ }
181
+ return out;
182
+ }
183
+ var SESSION_ID_KEYS = [
184
+ "session.id",
185
+ "session_id",
186
+ "agent.session_id",
187
+ "claude.session_id",
188
+ "conversation_id",
189
+ "conversation.id"
190
+ ];
191
+ function firstString(attrs, keys2) {
192
+ for (const k of keys2) {
193
+ const v = attrs[k];
194
+ if (typeof v === "string" && v.length > 0) return v;
195
+ }
196
+ return void 0;
197
+ }
198
+ function toBigIntNano(value) {
199
+ if (value === void 0) return void 0;
200
+ try {
201
+ if (typeof value === "number") {
202
+ return Number.isFinite(value) ? BigInt(Math.trunc(value)) : void 0;
203
+ }
204
+ const trimmed = value.trim();
205
+ if (trimmed === "" || !/^\d+$/.test(trimmed)) return void 0;
206
+ return BigInt(trimmed);
207
+ } catch {
208
+ return void 0;
209
+ }
210
+ }
211
+ function mapVendorOtelExport(doc, options = {}) {
212
+ const source = options.source ?? VENDOR_OTEL_SOURCE;
213
+ const events = [];
214
+ let skipped = 0;
215
+ const resourceSpans = doc?.resourceSpans;
216
+ if (!Array.isArray(resourceSpans)) return { events, skipped };
217
+ for (const rs of resourceSpans) {
218
+ const resourceAttrs = convertAttributes(rs?.resource?.attributes);
219
+ const projectId = firstString(resourceAttrs, ["project.id", "service.name", "service.namespace"]) ?? void 0;
220
+ for (const ss of rs?.scopeSpans ?? []) {
221
+ for (const span of ss?.spans ?? []) {
222
+ const traceId = typeof span?.traceId === "string" ? span.traceId : "";
223
+ const spanId = typeof span?.spanId === "string" ? span.spanId : "";
224
+ if (traceId === "" || spanId === "") {
225
+ skipped += 1;
226
+ continue;
227
+ }
228
+ const spanAttrs = convertAttributes(span.attributes);
229
+ const attributes = { ...resourceAttrs, ...spanAttrs };
230
+ const start = toBigIntNano(span.startTimeUnixNano);
231
+ const end = toBigIntNano(span.endTimeUnixNano);
232
+ if (start !== void 0 && end !== void 0 && end >= start) {
233
+ attributes["vendor.duration_ns"] = Number(end - start);
234
+ }
235
+ if (projectId !== void 0 && attributes["project.id"] === void 0) {
236
+ attributes["project.id"] = projectId;
237
+ }
238
+ const sessionId = firstString(attributes, SESSION_ID_KEYS);
239
+ if (sessionId !== void 0 && attributes["agent.session_id"] === void 0) {
240
+ attributes["agent.session_id"] = sessionId;
241
+ }
242
+ events.push({
243
+ id: generateId(),
244
+ timeUnixNano: start ?? 0n,
245
+ source,
246
+ eventType: typeof span.name === "string" && span.name.length > 0 ? span.name : "vendor.otel",
247
+ lifecycle: "instant",
248
+ traceId,
249
+ spanId,
250
+ ...typeof span.parentSpanId === "string" && span.parentSpanId.length > 0 ? { parentSpanId: span.parentSpanId } : {},
251
+ ...sessionId !== void 0 ? { sessionId } : {},
252
+ attributes,
253
+ links: [],
254
+ // Deterministic identity → idempotent re-ingestion (§9.2).
255
+ dedupKey: `${VENDOR_OTEL_DEDUP_PREFIX}:${traceId}:${spanId}`
256
+ });
257
+ }
258
+ }
259
+ }
260
+ return { events, skipped };
261
+ }
262
+
157
263
  // src/correlate/cache-store.ts
158
264
  var MemoryCacheStore = class {
159
265
  map = /* @__PURE__ */ new Map();
@@ -588,6 +694,143 @@ function createPromotionRule() {
588
694
  };
589
695
  }
590
696
 
697
+ // src/enrich/rules/worktree.ts
698
+ var WORKTREE_WINDOW_MS = 60 * 60 * 1e3;
699
+ function worktreeRepoKey(repoRoot) {
700
+ return `worktree:repo:${repoRoot}`;
701
+ }
702
+ function nanoToMs(timeUnixNano) {
703
+ return Number(timeUnixNano / 1000000n);
704
+ }
705
+ function parseCommittedFiles(raw) {
706
+ if (typeof raw !== "string") return void 0;
707
+ try {
708
+ const parsed = JSON.parse(raw);
709
+ if (!Array.isArray(parsed)) return void 0;
710
+ return parsed.filter((x) => typeof x === "string");
711
+ } catch {
712
+ return void 0;
713
+ }
714
+ }
715
+ function createWorktreeRule(windowMs = WORKTREE_WINDOW_MS) {
716
+ return function worktreeRule(event, ctx) {
717
+ if (event.eventType === "promotion.worktree" && event.lifecycle === "instant") {
718
+ const repoRoot2 = event.attributes["repo.root"];
719
+ if (typeof repoRoot2 !== "string" || repoRoot2.length === 0) return;
720
+ const key = worktreeRepoKey(repoRoot2);
721
+ const ref = {
722
+ spanId: event.spanId,
723
+ traceId: event.traceId,
724
+ sessionId: event.sessionId,
725
+ timeMs: nanoToMs(event.timeUnixNano)
726
+ };
727
+ const existing = ctx.api.cacheGet(key);
728
+ if (!existing || ref.timeMs >= existing.timeMs) {
729
+ ctx.api.cacheSet(key, ref);
730
+ ctx.api.logDebug(
731
+ `R8: cached promotion.worktree span=${event.spanId} repo.root=${repoRoot2}`
732
+ );
733
+ }
734
+ return;
735
+ }
736
+ if (event.eventType !== "promotion.commit" || event.lifecycle !== "close") {
737
+ return;
738
+ }
739
+ const repoRoot = event.attributes["repo.root"];
740
+ if (typeof repoRoot !== "string" || repoRoot.length === 0) {
741
+ ctx.api.logDebug("R8: promotion.commit has no repo.root \u2014 skip");
742
+ return;
743
+ }
744
+ const wt = ctx.api.cacheGet(worktreeRepoKey(repoRoot));
745
+ if (!wt) {
746
+ ctx.api.logDebug(`R8: no cached worktree for repo.root=${repoRoot} \u2014 skip`);
747
+ return;
748
+ }
749
+ const commitMs = nanoToMs(event.timeUnixNano);
750
+ if (Math.abs(commitMs - wt.timeMs) > windowMs) {
751
+ ctx.api.logDebug(
752
+ `R8: worktree snapshot stale (\u0394=${Math.abs(commitMs - wt.timeMs)}ms) \u2014 skip`
753
+ );
754
+ return;
755
+ }
756
+ const alreadyLinked = event.links.some(
757
+ (l) => l.linkType === "materializes_as_commit" && l.targetSpanId === wt.spanId
758
+ );
759
+ if (alreadyLinked) return;
760
+ let confidence = "provisional";
761
+ const committedFiles = parseCommittedFiles(event.attributes["committed_files"]);
762
+ if (committedFiles && committedFiles.length > 0 && wt.sessionId) {
763
+ const committedSet = new Set(committedFiles);
764
+ const taskSpans = ctx.api.cacheGet(allTaskSpansKey(wt.sessionId)) ?? [];
765
+ const overlap = taskSpans.some((taskSpanId) => {
766
+ const files = ctx.api.cacheGet(taskFilesKey(wt.sessionId, taskSpanId)) ?? [];
767
+ return files.some((f) => committedSet.has(f));
768
+ });
769
+ if (overlap) confidence = "probable";
770
+ }
771
+ ctx.api.addLink({
772
+ linkType: "materializes_as_commit",
773
+ targetSpanId: wt.spanId,
774
+ targetTraceId: wt.traceId !== event.traceId ? wt.traceId : void 0,
775
+ attributes: { confidence }
776
+ });
777
+ ctx.api.logDebug(
778
+ `R8: linked promotion.commit \u2192 worktree=${wt.spanId} confidence=${confidence}`
779
+ );
780
+ };
781
+ }
782
+
783
+ // src/enrich/rules/issue-anchor.ts
784
+ function issueIdCacheKey(sessionId) {
785
+ return `session:${sessionId}:issue_id`;
786
+ }
787
+ function issueTypeCacheKey(sessionId) {
788
+ return `session:${sessionId}:issue_type`;
789
+ }
790
+ function createIssueAnchorRule() {
791
+ return function issueAnchorRule(event, ctx) {
792
+ const sid = event.sessionId;
793
+ if (!sid) return;
794
+ const attrIssueId = event.attributes["issue.id"];
795
+ const attrIssueType = event.attributes["issue.type"];
796
+ if (attrIssueId !== void 0 && attrIssueId !== "") {
797
+ const cached = ctx.api.cacheGet(issueIdCacheKey(sid));
798
+ if (cached === void 0) {
799
+ ctx.api.cacheSet(issueIdCacheKey(sid), String(attrIssueId));
800
+ ctx.api.logDebug(
801
+ `R-ISSUE: cached issue.id=${attrIssueId} for session=${sid}`
802
+ );
803
+ }
804
+ }
805
+ if (attrIssueType !== void 0 && attrIssueType !== "") {
806
+ const cached = ctx.api.cacheGet(issueTypeCacheKey(sid));
807
+ if (cached === void 0) {
808
+ ctx.api.cacheSet(issueTypeCacheKey(sid), String(attrIssueType));
809
+ ctx.api.logDebug(
810
+ `R-ISSUE: cached issue.type=${attrIssueType} for session=${sid}`
811
+ );
812
+ }
813
+ }
814
+ if (attrIssueId === void 0 || attrIssueId === "") {
815
+ const cachedId = ctx.api.cacheGet(issueIdCacheKey(sid));
816
+ if (cachedId !== void 0) {
817
+ event.attributes["issue.id"] = cachedId;
818
+ ctx.api.logDebug(
819
+ `R-ISSUE: propagated issue.id=${cachedId} to span=${event.spanId}`
820
+ );
821
+ } else {
822
+ event.attributes["issue.id_missing"] = true;
823
+ }
824
+ }
825
+ if ((attrIssueType === void 0 || attrIssueType === "") && event.attributes["issue.type"] === void 0) {
826
+ const cachedType = ctx.api.cacheGet(issueTypeCacheKey(sid));
827
+ if (cachedType !== void 0) {
828
+ event.attributes["issue.type"] = cachedType;
829
+ }
830
+ }
831
+ };
832
+ }
833
+
591
834
  // src/enrich/enricher.ts
592
835
  var Enricher = class {
593
836
  correlator;
@@ -649,17 +892,63 @@ var Enricher = class {
649
892
  };
650
893
  function createDefaultRules(artifactPatterns = []) {
651
894
  return [
895
+ createIssueAnchorRule(),
896
+ // R-ISSUE (#178 T-E1)
652
897
  createArtifactRule(artifactPatterns),
653
898
  // R7
654
899
  createProcessRule(),
655
900
  // R3/R4
656
901
  createCrossAxisRule(),
657
902
  // R5
658
- createPromotionRule()
903
+ createPromotionRule(),
659
904
  // R1/R2
905
+ createWorktreeRule()
906
+ // R8 (#175)
660
907
  ];
661
908
  }
662
909
 
910
+ // src/cli/worktree.ts
911
+ import { execFileSync } from "child_process";
912
+ function defaultGitRunner(args) {
913
+ try {
914
+ const out = execFileSync("git", args, {
915
+ encoding: "utf8",
916
+ stdio: ["ignore", "pipe", "ignore"],
917
+ timeout: 2e3
918
+ });
919
+ return out;
920
+ } catch {
921
+ return void 0;
922
+ }
923
+ }
924
+ function captureWorktree(cwd, git = defaultGitRunner) {
925
+ const repoRootRaw = git(["-C", cwd, "rev-parse", "--show-toplevel"]);
926
+ const repoRoot = repoRootRaw?.trim();
927
+ if (!repoRoot) {
928
+ return void 0;
929
+ }
930
+ const shaRaw = git(["-C", cwd, "rev-parse", "HEAD"]);
931
+ const commitSha = shaRaw?.trim();
932
+ const statusRaw = git(["-C", cwd, "status", "--porcelain"]);
933
+ const dirty = typeof statusRaw === "string" ? statusRaw.trim().length > 0 : false;
934
+ const snapshot = { repoRoot, dirty };
935
+ if (commitSha && commitSha.length > 0) snapshot.commitSha = commitSha;
936
+ return snapshot;
937
+ }
938
+ function worktreeAttributes(snapshot, sessionId) {
939
+ const attributes = {
940
+ axis: "promotion",
941
+ dirty: snapshot.dirty
942
+ };
943
+ if (snapshot.commitSha) attributes["commit.sha"] = snapshot.commitSha;
944
+ if (snapshot.repoRoot) attributes["repo.root"] = snapshot.repoRoot;
945
+ if (sessionId) {
946
+ attributes["agent.session_id"] = sessionId;
947
+ attributes["session_id"] = sessionId;
948
+ }
949
+ return attributes;
950
+ }
951
+
663
952
  // src/sink/sqlite-sink.ts
664
953
  import { DatabaseSync } from "node:sqlite";
665
954
  import { mkdirSync } from "fs";
@@ -686,7 +975,8 @@ CREATE TABLE IF NOT EXISTS canonical_events (
686
975
  run_id TEXT,
687
976
  session_id TEXT,
688
977
  task_id TEXT,
689
- attributes TEXT NOT NULL DEFAULT '{}'
978
+ attributes TEXT NOT NULL DEFAULT '{}',
979
+ dedup_key TEXT
690
980
  );
691
981
 
692
982
  CREATE INDEX IF NOT EXISTS idx_ce_trace_id ON canonical_events (trace_id);
@@ -752,6 +1042,24 @@ var SqliteSink = class {
752
1042
  }
753
1043
  configureSqliteDatabase(this.db);
754
1044
  this.db.exec(SCHEMA);
1045
+ this.migrateDedupKey();
1046
+ }
1047
+ /**
1048
+ * Restart-safe migration for the dedup_key column + UNIQUE index (§9.2).
1049
+ *
1050
+ * `CREATE TABLE IF NOT EXISTS` is a no-op on databases created before dedup_key
1051
+ * existed, so the column must be added in-place. PRAGMA table_info lets us detect
1052
+ * its absence and ALTER it in (idempotent: skipped once present). The UNIQUE index
1053
+ * is created only after the column is guaranteed to exist.
1054
+ */
1055
+ migrateDedupKey() {
1056
+ const columns = this.db.prepare("PRAGMA table_info(canonical_events)").all();
1057
+ if (!columns.some((c) => c.name === "dedup_key")) {
1058
+ this.db.exec("ALTER TABLE canonical_events ADD COLUMN dedup_key TEXT");
1059
+ }
1060
+ this.db.exec(
1061
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_ce_dedup_key ON canonical_events (dedup_key)"
1062
+ );
755
1063
  }
756
1064
  // ── Public write API ───────────────────────────────────────────────────────
757
1065
  /**
@@ -759,13 +1067,19 @@ var SqliteSink = class {
759
1067
  * 1. Append to canonical_events (immutable)
760
1068
  * 2. Upsert spans materialised view
761
1069
  * 3. Insert canonical_links rows
1070
+ *
1071
+ * Returns `false` when the event carried a dedup_key that already exists (the
1072
+ * INSERT was ignored and no span/link side effects were applied — making
1073
+ * re-runs of a Canonical Projection batch fully idempotent), otherwise `true`.
762
1074
  */
763
1075
  write(event) {
764
- this.insertCanonicalEvent(event);
1076
+ const inserted = this.insertCanonicalEvent(event);
1077
+ if (!inserted) return false;
765
1078
  this.upsertSpan(event);
766
1079
  if (event.links.length > 0) {
767
1080
  this.insertLinks(event);
768
1081
  }
1082
+ return true;
769
1083
  }
770
1084
  /** Close the database connection. */
771
1085
  close() {
@@ -779,14 +1093,23 @@ var SqliteSink = class {
779
1093
  return this.db;
780
1094
  }
781
1095
  // ── Private helpers ────────────────────────────────────────────────────────
1096
+ /**
1097
+ * Append the immutable canonical_events row.
1098
+ *
1099
+ * Uses INSERT OR IGNORE so a dedup_key collision (UNIQUE idx_ce_dedup_key) is a
1100
+ * silent no-op rather than an error — the basis of §9.2 idempotent re-ingestion.
1101
+ * Returns true when a new row was written, false when ignored on conflict.
1102
+ * Events without a dedup_key (NULL) are never ignored (multiple NULLs are
1103
+ * distinct under SQLite's UNIQUE semantics).
1104
+ */
782
1105
  insertCanonicalEvent(event) {
783
1106
  const stmt = this.db.prepare(`
784
- INSERT INTO canonical_events
1107
+ INSERT OR IGNORE INTO canonical_events
785
1108
  (id, time_unix_nano, source, event_type, lifecycle,
786
- trace_id, span_id, parent_span_id, run_id, session_id, task_id, attributes)
787
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1109
+ trace_id, span_id, parent_span_id, run_id, session_id, task_id, attributes, dedup_key)
1110
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
788
1111
  `);
789
- stmt.run(
1112
+ const result = stmt.run(
790
1113
  event.id,
791
1114
  event.timeUnixNano,
792
1115
  event.source,
@@ -798,8 +1121,10 @@ var SqliteSink = class {
798
1121
  event.runId ?? null,
799
1122
  event.sessionId ?? null,
800
1123
  event.taskId ?? null,
801
- JSON.stringify(event.attributes)
1124
+ JSON.stringify(event.attributes),
1125
+ event.dedupKey ?? null
802
1126
  );
1127
+ return Number(result.changes) > 0;
803
1128
  }
804
1129
  upsertSpan(event) {
805
1130
  switch (event.lifecycle) {
@@ -826,9 +1151,15 @@ var SqliteSink = class {
826
1151
  UPDATE spans
827
1152
  SET end_time = ?,
828
1153
  duration_ns = (? - start_time),
829
- status = 'closed'
1154
+ status = 'closed',
1155
+ attributes = json_patch(attributes, ?)
830
1156
  WHERE span_id = ?
831
- `).run(event.timeUnixNano, event.timeUnixNano, event.spanId);
1157
+ `).run(
1158
+ event.timeUnixNano,
1159
+ event.timeUnixNano,
1160
+ JSON.stringify(event.attributes),
1161
+ event.spanId
1162
+ );
832
1163
  break;
833
1164
  case "instant":
834
1165
  this.db.prepare(`
@@ -851,6 +1182,11 @@ var SqliteSink = class {
851
1182
  );
852
1183
  break;
853
1184
  case "event":
1185
+ this.db.prepare(`
1186
+ UPDATE spans
1187
+ SET attributes = json_patch(attributes, ?)
1188
+ WHERE span_id = ?
1189
+ `).run(JSON.stringify(event.attributes), event.spanId);
854
1190
  break;
855
1191
  }
856
1192
  }
@@ -962,7 +1298,7 @@ var OtelEmitter = class {
962
1298
  {
963
1299
  kind: SpanKind.INTERNAL,
964
1300
  startTime: startMs,
965
- attributes: convertAttributes(mapped.attributes),
1301
+ attributes: convertAttributes2(mapped.attributes),
966
1302
  links: mapped.links.map((l) => ({
967
1303
  context: {
968
1304
  traceId: l.targetTraceId ?? mapped.traceId,
@@ -977,7 +1313,7 @@ var OtelEmitter = class {
977
1313
  for (const ev of mapped.spanEvents) {
978
1314
  otelSpan.addEvent(
979
1315
  ev.name,
980
- convertAttributes(ev.attributes),
1316
+ convertAttributes2(ev.attributes),
981
1317
  Number(ev.timeUnixNano / 1000000n)
982
1318
  );
983
1319
  }
@@ -995,7 +1331,7 @@ var OtelEmitter = class {
995
1331
  return trace.setSpanContext(ROOT_CONTEXT, spanContext);
996
1332
  }
997
1333
  };
998
- function convertAttributes(attrs) {
1334
+ function convertAttributes2(attrs) {
999
1335
  return attrs;
1000
1336
  }
1001
1337
 
@@ -1227,8 +1563,9 @@ function parseEventRow(row) {
1227
1563
  sessionId: row.session_id ?? void 0,
1228
1564
  taskId: row.task_id ?? void 0,
1229
1565
  attributes: safeParseJson(row.attributes),
1230
- links: []
1566
+ links: [],
1231
1567
  // links are stored in canonical_links, not inline on events
1568
+ ...row.dedup_key ? { dedupKey: row.dedup_key } : {}
1232
1569
  };
1233
1570
  }
1234
1571
  function parseLinkRow(row) {
@@ -1587,6 +1924,658 @@ function emitPromotionPr(collector, options) {
1587
1924
  return spanId;
1588
1925
  }
1589
1926
 
1927
+ // src/promotion/github-recording.ts
1928
+ function parseIssueIdFromText(text) {
1929
+ const hashMatch = text.match(/#(\d+)/);
1930
+ if (hashMatch) {
1931
+ return Number(hashMatch[1]);
1932
+ }
1933
+ const branchMatch = text.match(/issue-(\d+)/i);
1934
+ if (branchMatch) {
1935
+ return Number(branchMatch[1]);
1936
+ }
1937
+ return null;
1938
+ }
1939
+ function buildGithubPromotionAttributes(input) {
1940
+ const issueId = input.commitMessage !== void 0 ? parseIssueIdFromText(input.commitMessage) : null;
1941
+ const attrs = {
1942
+ axis: "promotion",
1943
+ "commit.sha": input.sha
1944
+ };
1945
+ if (issueId !== null) {
1946
+ attrs["issue.id"] = issueId;
1947
+ }
1948
+ if (input.kind === "ci") {
1949
+ attrs["ci.run_id"] = input.runId;
1950
+ attrs["ci.verdict"] = input.verdict;
1951
+ if (input.workflowName) {
1952
+ attrs["ci.workflow"] = input.workflowName;
1953
+ }
1954
+ return attrs;
1955
+ }
1956
+ attrs["deploy.id"] = input.runId;
1957
+ attrs["deploy.status"] = input.verdict;
1958
+ attrs["deploy.target"] = input.deployTarget ?? "unknown";
1959
+ return attrs;
1960
+ }
1961
+ function githubPromotionEventType(kind) {
1962
+ return kind === "ci" ? "promotion.ci" : "promotion.deploy";
1963
+ }
1964
+
1965
+ // src/analysis/outcome-chain.ts
1966
+ var OUTCOME_CHAIN_STAGES = [
1967
+ "agent.session",
1968
+ "process.task",
1969
+ "process.edit",
1970
+ "promotion.commit",
1971
+ "promotion.ci",
1972
+ "promotion.deploy"
1973
+ ];
1974
+ function observationMatchesIssue(observation, issueId) {
1975
+ const attrs = observation.attributes ?? {};
1976
+ const attrIssue = attrs["issue.id"];
1977
+ if (attrIssue !== void 0 && String(attrIssue) === issueId) {
1978
+ return true;
1979
+ }
1980
+ const commitMessage = attrs["commit.message"];
1981
+ if (typeof commitMessage === "string") {
1982
+ const parsed = parseIssueIdFromText(commitMessage);
1983
+ if (parsed !== null && String(parsed) === issueId) {
1984
+ return true;
1985
+ }
1986
+ }
1987
+ const prCommand = attrs["pr.command"];
1988
+ if (typeof prCommand === "string" && prCommand.includes(`#${issueId}`)) {
1989
+ return true;
1990
+ }
1991
+ return false;
1992
+ }
1993
+ function isProvisionalObservation(observation) {
1994
+ const sessionId = observation.sessionId ?? observation.attributes?.["session_id"];
1995
+ if (sessionId === "unknown" || sessionId === void 0 || sessionId === "") {
1996
+ return true;
1997
+ }
1998
+ const confidence = observation.attributes?.["link.confidence"] ?? observation.attributes?.["confidence"];
1999
+ return confidence === "provisional";
2000
+ }
2001
+ function collectObservations(adapter) {
2002
+ const latest = /* @__PURE__ */ new Map();
2003
+ for (const event of adapter.queryEvents({})) {
2004
+ latest.set(`${event.spanId}:${event.eventType}`, event);
2005
+ }
2006
+ for (const span of adapter.querySpans({})) {
2007
+ latest.set(`${span.spanId}:${span.eventType}`, {
2008
+ id: span.spanId,
2009
+ timeUnixNano: span.startTime ?? 0n,
2010
+ source: "spans",
2011
+ eventType: span.eventType,
2012
+ lifecycle: span.status === "closed" ? "close" : span.status === "open" ? "open" : "instant",
2013
+ traceId: span.traceId ?? "",
2014
+ spanId: span.spanId,
2015
+ parentSpanId: span.parentSpanId ?? void 0,
2016
+ sessionId: span.sessionId ?? void 0,
2017
+ attributes: span.attributes,
2018
+ links: []
2019
+ });
2020
+ }
2021
+ return [...latest.values()];
2022
+ }
2023
+ function analyzeOutcomeChain(adapter, issueId) {
2024
+ const normalizedIssueId = String(issueId);
2025
+ const observations = collectObservations(adapter);
2026
+ const issueObservations = observations.filter(
2027
+ (obs) => observationMatchesIssue(obs, normalizedIssueId)
2028
+ );
2029
+ const stages = OUTCOME_CHAIN_STAGES.map((eventType) => {
2030
+ const matched = issueObservations.filter((obs) => obs.eventType === eventType);
2031
+ if (matched.length === 0) {
2032
+ return { eventType, status: "missing", spanIds: [] };
2033
+ }
2034
+ const provisionalOnly = matched.every(isProvisionalObservation);
2035
+ return {
2036
+ eventType,
2037
+ status: provisionalOnly ? "provisional" : "present",
2038
+ spanIds: matched.map((obs) => obs.spanId)
2039
+ };
2040
+ });
2041
+ const gaps = stages.filter((s) => s.status === "missing").map((s) => s.eventType);
2042
+ const provisional = stages.filter((s) => s.status === "provisional").map((s) => s.eventType);
2043
+ return {
2044
+ issueId: normalizedIssueId,
2045
+ stages,
2046
+ complete: gaps.length === 0,
2047
+ gaps,
2048
+ provisional
2049
+ };
2050
+ }
2051
+
2052
+ // src/analysis/diagnostic-report.ts
2053
+ var STAGE_SET = new Set(OUTCOME_CHAIN_STAGES);
2054
+ function attrStr(attrs, key) {
2055
+ const v = attrs[key];
2056
+ return v === void 0 || v === null ? "" : String(v);
2057
+ }
2058
+ function attrNum(attrs, key) {
2059
+ const v = attrs[key];
2060
+ if (v === void 0 || v === null || v === "") return 0;
2061
+ const n = typeof v === "number" ? v : Number(v);
2062
+ return Number.isFinite(n) ? n : 0;
2063
+ }
2064
+ function projectIdOf(span) {
2065
+ const p = attrStr(span.attributes, "project.id");
2066
+ if (p !== "") return p;
2067
+ const s = attrStr(span.attributes, "service.name");
2068
+ return s;
2069
+ }
2070
+ function issueIdOf(span) {
2071
+ return attrStr(span.attributes, "issue.id");
2072
+ }
2073
+ function sessionIdOf(span) {
2074
+ if (span.sessionId !== null && span.sessionId !== void 0 && span.sessionId !== "") {
2075
+ return span.sessionId;
2076
+ }
2077
+ const a = attrStr(span.attributes, "agent.session_id") || attrStr(span.attributes, "session_id");
2078
+ return a;
2079
+ }
2080
+ function round(value, decimals) {
2081
+ if (!Number.isFinite(value)) return 0;
2082
+ const f = 10 ** decimals;
2083
+ return Math.round(value * f) / f;
2084
+ }
2085
+ function average(values) {
2086
+ if (values.length === 0) return 0;
2087
+ return values.reduce((a, b) => a + b, 0) / values.length;
2088
+ }
2089
+ function inWindow(span, fromNano, toNano) {
2090
+ if (fromNano === void 0 && toNano === void 0) return true;
2091
+ const t = span.startTime;
2092
+ if (t === null) return false;
2093
+ if (fromNano !== void 0 && t < fromNano) return false;
2094
+ if (toNano !== void 0 && t > toNano) return false;
2095
+ return true;
2096
+ }
2097
+ function filterSpans(spans, opts) {
2098
+ return spans.filter((s) => {
2099
+ if (opts.projectId !== void 0 && projectIdOf(s) !== opts.projectId) {
2100
+ return false;
2101
+ }
2102
+ return inWindow(s, opts.fromNano, opts.toNano);
2103
+ });
2104
+ }
2105
+ function computeLayer1(spans) {
2106
+ const groups = /* @__PURE__ */ new Map();
2107
+ for (const s of spans) {
2108
+ const issueId = issueIdOf(s);
2109
+ if (issueId === "") continue;
2110
+ const key = `${issueId}\0${projectIdOf(s)}`;
2111
+ (groups.get(key) ?? groups.set(key, []).get(key)).push(s);
2112
+ }
2113
+ const rows = [];
2114
+ for (const group of groups.values()) {
2115
+ const issueId = issueIdOf(group[0]);
2116
+ const projectId = projectIdOf(group[0]);
2117
+ const issueType = group.map((s) => attrStr(s.attributes, "issue.type")).find((t) => t !== "") ?? "";
2118
+ const sessionIds = /* @__PURE__ */ new Set();
2119
+ let editCount = 0;
2120
+ let commitCount = 0;
2121
+ let ciCount = 0;
2122
+ let ciPassed = 0;
2123
+ for (const s of group) {
2124
+ if (s.eventType === "agent.session") {
2125
+ const sid = sessionIdOf(s);
2126
+ if (sid !== "") sessionIds.add(sid);
2127
+ }
2128
+ if (s.eventType === "process.edit") editCount += 1;
2129
+ if (s.eventType === "promotion.commit") commitCount += 1;
2130
+ if (s.eventType === "promotion.ci") {
2131
+ ciCount += 1;
2132
+ if (attrStr(s.attributes, "ci.verdict") === "passed") ciPassed += 1;
2133
+ }
2134
+ }
2135
+ const sessionCount = sessionIds.size;
2136
+ rows.push({
2137
+ issueId,
2138
+ issueType,
2139
+ projectId,
2140
+ sessionCount,
2141
+ editCount,
2142
+ commitCount,
2143
+ ciCount,
2144
+ ciPassed,
2145
+ leverage: sessionCount > 0 ? round(editCount / sessionCount, 2) : 0
2146
+ });
2147
+ }
2148
+ return rows.sort((a, b) => a.issueId.localeCompare(b.issueId));
2149
+ }
2150
+ function computeLayer2(spans) {
2151
+ const groups = /* @__PURE__ */ new Map();
2152
+ for (const s of spans) {
2153
+ const issueId = issueIdOf(s);
2154
+ if (issueId === "") continue;
2155
+ const key = `${issueId}\0${projectIdOf(s)}`;
2156
+ (groups.get(key) ?? groups.set(key, []).get(key)).push(s);
2157
+ }
2158
+ const rows = [];
2159
+ for (const group of groups.values()) {
2160
+ const issueId = issueIdOf(group[0]);
2161
+ const projectId = projectIdOf(group[0]);
2162
+ const sessionIds = /* @__PURE__ */ new Set();
2163
+ let commits = 0;
2164
+ let ciPassed = 0;
2165
+ let totalCostUsd = 0;
2166
+ for (const s of group) {
2167
+ if (s.eventType === "agent.session") {
2168
+ const sid = sessionIdOf(s);
2169
+ if (sid !== "") sessionIds.add(sid);
2170
+ totalCostUsd += attrNum(s.attributes, "cost_usd");
2171
+ }
2172
+ if (s.eventType === "promotion.commit") commits += 1;
2173
+ if (s.eventType === "promotion.ci" && attrStr(s.attributes, "ci.verdict") === "passed") {
2174
+ ciPassed += 1;
2175
+ }
2176
+ }
2177
+ const sessions = sessionIds.size;
2178
+ rows.push({
2179
+ issueId,
2180
+ projectId,
2181
+ sessions,
2182
+ commits,
2183
+ ciPassed,
2184
+ totalCostUsd: round(totalCostUsd, 4),
2185
+ commitRate: sessions > 0 ? round(commits / sessions, 4) : 0,
2186
+ ciPassRate: commits > 0 ? round(ciPassed / commits, 4) : 0,
2187
+ commitsPerDollar: totalCostUsd > 0 ? round(commits / totalCostUsd, 4) : 0
2188
+ });
2189
+ }
2190
+ return rows.sort((a, b) => a.issueId.localeCompare(b.issueId));
2191
+ }
2192
+ function computeLayer3(spans) {
2193
+ const groups = /* @__PURE__ */ new Map();
2194
+ for (const s of spans) {
2195
+ const issueId = issueIdOf(s);
2196
+ if (issueId === "") continue;
2197
+ const key = `${issueId}\0${projectIdOf(s)}`;
2198
+ (groups.get(key) ?? groups.set(key, []).get(key)).push(s);
2199
+ }
2200
+ const rows = [];
2201
+ for (const group of groups.values()) {
2202
+ const issueId = issueIdOf(group[0]);
2203
+ const projectId = projectIdOf(group[0]);
2204
+ const issueType = group.map((s) => attrStr(s.attributes, "issue.type")).find((t) => t !== "") ?? "";
2205
+ const stages = /* @__PURE__ */ new Set();
2206
+ let totalCostUsd = 0;
2207
+ let totalInputTokens = 0;
2208
+ let totalOutputTokens = 0;
2209
+ for (const s of group) {
2210
+ if (STAGE_SET.has(s.eventType)) stages.add(s.eventType);
2211
+ if (s.eventType === "agent.session") {
2212
+ totalCostUsd += attrNum(s.attributes, "cost_usd");
2213
+ totalInputTokens += attrNum(s.attributes, "gen_ai.usage.input_tokens");
2214
+ totalOutputTokens += attrNum(s.attributes, "gen_ai.usage.output_tokens");
2215
+ }
2216
+ }
2217
+ const stagesPresent = stages.size;
2218
+ rows.push({
2219
+ issueId,
2220
+ issueType,
2221
+ projectId,
2222
+ stagesPresent,
2223
+ stagesTotal: OUTCOME_CHAIN_STAGES.length,
2224
+ chainCompleteness: round(stagesPresent / OUTCOME_CHAIN_STAGES.length, 4),
2225
+ totalCostUsd: round(totalCostUsd, 4),
2226
+ totalInputTokens,
2227
+ totalOutputTokens
2228
+ });
2229
+ }
2230
+ return rows.sort((a, b) => a.issueId.localeCompare(b.issueId));
2231
+ }
2232
+ function computeSessionLinkRate(spans) {
2233
+ const byProject = /* @__PURE__ */ new Map();
2234
+ for (const s of spans) {
2235
+ if (s.eventType !== "agent.session") continue;
2236
+ const projectId = projectIdOf(s);
2237
+ const acc = byProject.get(projectId) ?? { total: 0, linked: 0 };
2238
+ acc.total += 1;
2239
+ if (issueIdOf(s) !== "") acc.linked += 1;
2240
+ byProject.set(projectId, acc);
2241
+ }
2242
+ return [...byProject.entries()].map(([projectId, { total, linked }]) => ({
2243
+ projectId,
2244
+ totalSessions: total,
2245
+ linkedSessions: linked,
2246
+ linkRate: total > 0 ? round(linked / total, 4) : 0
2247
+ })).sort((a, b) => a.projectId.localeCompare(b.projectId));
2248
+ }
2249
+ function computeStageArrival(spans) {
2250
+ const byProject = /* @__PURE__ */ new Map();
2251
+ for (const s of spans) {
2252
+ const issueId = issueIdOf(s);
2253
+ if (issueId === "") continue;
2254
+ const projectId = projectIdOf(s);
2255
+ const acc = byProject.get(projectId) ?? { commit: /* @__PURE__ */ new Set(), ci: /* @__PURE__ */ new Set(), deploy: /* @__PURE__ */ new Set() };
2256
+ if (s.eventType === "promotion.commit") acc.commit.add(issueId);
2257
+ if (s.eventType === "promotion.ci") acc.ci.add(issueId);
2258
+ if (s.eventType === "promotion.deploy") acc.deploy.add(issueId);
2259
+ byProject.set(projectId, acc);
2260
+ }
2261
+ return [...byProject.entries()].map(([projectId, { commit, ci, deploy }]) => {
2262
+ const issuesWithCommit = commit.size;
2263
+ const issuesWithCi = ci.size;
2264
+ const issuesWithDeploy = deploy.size;
2265
+ return {
2266
+ projectId,
2267
+ issuesWithCommit,
2268
+ issuesWithCi,
2269
+ issuesWithDeploy,
2270
+ ciArrivalRate: issuesWithCommit > 0 ? round(issuesWithCi / issuesWithCommit, 4) : 0,
2271
+ deployArrivalRate: issuesWithCommit > 0 ? round(issuesWithDeploy / issuesWithCommit, 4) : 0
2272
+ };
2273
+ }).sort((a, b) => a.projectId.localeCompare(b.projectId));
2274
+ }
2275
+ function computeProvisional(spans) {
2276
+ const byProject = /* @__PURE__ */ new Map();
2277
+ for (const s of spans) {
2278
+ const projectId = projectIdOf(s);
2279
+ const acc = byProject.get(projectId) ?? { total: 0, unknown: 0, provisional: 0 };
2280
+ acc.total += 1;
2281
+ const sid = sessionIdOf(s);
2282
+ if (sid === "" || sid === "unknown") acc.unknown += 1;
2283
+ const confidence = attrStr(s.attributes, "link.confidence") || attrStr(s.attributes, "confidence");
2284
+ if (confidence === "provisional") acc.provisional += 1;
2285
+ byProject.set(projectId, acc);
2286
+ }
2287
+ return [...byProject.entries()].map(([projectId, { total, unknown, provisional }]) => ({
2288
+ projectId,
2289
+ totalSpans: total,
2290
+ unknownSessionSpans: unknown,
2291
+ provisionalSpans: provisional,
2292
+ unknownRatio: total > 0 ? round(unknown / total, 4) : 0,
2293
+ provisionalRatio: total > 0 ? round(provisional / total, 4) : 0
2294
+ })).sort((a, b) => a.projectId.localeCompare(b.projectId));
2295
+ }
2296
+ function tierOf(issuesTouched) {
2297
+ if (issuesTouched >= 10) return "high";
2298
+ if (issuesTouched >= 3) return "mid";
2299
+ return "low";
2300
+ }
2301
+ function computeTierRatio(spans) {
2302
+ const byPerson = /* @__PURE__ */ new Map();
2303
+ for (const s of spans) {
2304
+ if (s.eventType !== "promotion.commit") continue;
2305
+ const email = attrStr(s.attributes, "git.user.email");
2306
+ if (email === "") continue;
2307
+ const projectId = projectIdOf(s);
2308
+ const key = `${email}\0${projectId}`;
2309
+ const acc = byPerson.get(key) ?? { projectId, commits: 0, issues: /* @__PURE__ */ new Set() };
2310
+ acc.commits += 1;
2311
+ const issueId = issueIdOf(s);
2312
+ if (issueId !== "") acc.issues.add(issueId);
2313
+ byPerson.set(key, acc);
2314
+ }
2315
+ const byTier = /* @__PURE__ */ new Map();
2316
+ for (const { projectId, commits, issues } of byPerson.values()) {
2317
+ const tier = tierOf(issues.size);
2318
+ const key = `${projectId}\0${tier}`;
2319
+ const acc = byTier.get(key) ?? { projectId, tier, personCount: 0, commits: 0, issues: 0 };
2320
+ acc.personCount += 1;
2321
+ acc.commits += commits;
2322
+ acc.issues += issues.size;
2323
+ byTier.set(key, acc);
2324
+ }
2325
+ const order = { high: 0, mid: 1, low: 2 };
2326
+ return [...byTier.values()].map((t) => ({
2327
+ projectId: t.projectId,
2328
+ tier: t.tier,
2329
+ personCount: t.personCount,
2330
+ tierCommits: t.commits,
2331
+ tierIssues: t.issues
2332
+ })).sort(
2333
+ (a, b) => a.projectId.localeCompare(b.projectId) || order[a.tier] - order[b.tier]
2334
+ );
2335
+ }
2336
+ function summarize(layer1, layer2, layer3, sessionLinkRate, tierRatio) {
2337
+ const totalSessions = sessionLinkRate.reduce((a, r) => a + r.totalSessions, 0);
2338
+ const linkedSessions = sessionLinkRate.reduce((a, r) => a + r.linkedSessions, 0);
2339
+ const tierCounts = { high: 0, mid: 0, low: 0 };
2340
+ for (const t of tierRatio) tierCounts[t.tier] += t.personCount;
2341
+ return {
2342
+ issueCount: new Set(layer1.map((r) => r.issueId)).size,
2343
+ sessionCount: totalSessions,
2344
+ totalCostUsd: round(
2345
+ layer2.reduce((a, r) => a + r.totalCostUsd, 0),
2346
+ 4
2347
+ ),
2348
+ avgLeverage: round(average(layer1.map((r) => r.leverage)), 4),
2349
+ avgCommitRate: round(average(layer2.map((r) => r.commitRate)), 4),
2350
+ avgCiPassRate: round(average(layer2.map((r) => r.ciPassRate)), 4),
2351
+ avgChainCompleteness: round(average(layer3.map((r) => r.chainCompleteness)), 4),
2352
+ sessionLinkRate: totalSessions > 0 ? round(linkedSessions / totalSessions, 4) : 0,
2353
+ tierCounts
2354
+ };
2355
+ }
2356
+ function computeMetrics(spans) {
2357
+ const layer1 = computeLayer1(spans);
2358
+ const layer2 = computeLayer2(spans);
2359
+ const layer3 = computeLayer3(spans);
2360
+ const sessionLinkRate = computeSessionLinkRate(spans);
2361
+ const stageArrival = computeStageArrival(spans);
2362
+ const provisional = computeProvisional(spans);
2363
+ const tierRatio = computeTierRatio(spans);
2364
+ return {
2365
+ layer1,
2366
+ layer2,
2367
+ layer3,
2368
+ linkCompletion: { sessionLinkRate, stageArrival, provisional },
2369
+ tierRatio,
2370
+ summary: summarize(layer1, layer2, layer3, sessionLinkRate, tierRatio)
2371
+ };
2372
+ }
2373
+ function diffSummary(after, before) {
2374
+ return {
2375
+ issueCount: after.issueCount - before.issueCount,
2376
+ sessionCount: after.sessionCount - before.sessionCount,
2377
+ totalCostUsd: round(after.totalCostUsd - before.totalCostUsd, 4),
2378
+ avgLeverage: round(after.avgLeverage - before.avgLeverage, 4),
2379
+ avgCommitRate: round(after.avgCommitRate - before.avgCommitRate, 4),
2380
+ avgCiPassRate: round(after.avgCiPassRate - before.avgCiPassRate, 4),
2381
+ avgChainCompleteness: round(after.avgChainCompleteness - before.avgChainCompleteness, 4),
2382
+ sessionLinkRate: round(after.sessionLinkRate - before.sessionLinkRate, 4)
2383
+ };
2384
+ }
2385
+ function generateDiagnosticReport(adapter, options = {}) {
2386
+ const allSpans = adapter.querySpans({});
2387
+ const fromNano = options.from !== void 0 ? isoToUnixNano(options.from) : void 0;
2388
+ const toNano = options.to !== void 0 ? isoToUnixNano(options.to) : void 0;
2389
+ const currentSpans = filterSpans(allSpans, {
2390
+ projectId: options.projectId,
2391
+ fromNano,
2392
+ toNano
2393
+ });
2394
+ const metrics = computeMetrics(currentSpans);
2395
+ const report = {
2396
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2397
+ projectId: options.projectId ?? null,
2398
+ period: { from: options.from ?? null, to: options.to ?? null },
2399
+ metrics
2400
+ };
2401
+ if (options.baselineFrom !== void 0 || options.baselineTo !== void 0) {
2402
+ const bFromNano = options.baselineFrom !== void 0 ? isoToUnixNano(options.baselineFrom) : void 0;
2403
+ const bToNano = options.baselineTo !== void 0 ? isoToUnixNano(options.baselineTo) : void 0;
2404
+ const baselineSpans = filterSpans(allSpans, {
2405
+ projectId: options.projectId,
2406
+ fromNano: bFromNano,
2407
+ toNano: bToNano
2408
+ });
2409
+ const baselineMetrics = computeMetrics(baselineSpans);
2410
+ report.baseline = {
2411
+ period: { from: options.baselineFrom ?? null, to: options.baselineTo ?? null },
2412
+ metrics: baselineMetrics,
2413
+ comparison: diffSummary(metrics.summary, baselineMetrics.summary)
2414
+ };
2415
+ }
2416
+ return report;
2417
+ }
2418
+ function bigIntReplacer(_key, value) {
2419
+ return typeof value === "bigint" ? value.toString() : value;
2420
+ }
2421
+ function renderReportJson(report) {
2422
+ return JSON.stringify(report, bigIntReplacer, 2);
2423
+ }
2424
+ function fmtPeriod(p) {
2425
+ if (p.from === null && p.to === null) return "all time";
2426
+ return `${p.from ?? "-\u221E"} \u2026 ${p.to ?? "now"}`;
2427
+ }
2428
+ function summaryTable(summary) {
2429
+ return [
2430
+ "| Metric | Value |",
2431
+ "|---|---|",
2432
+ `| Issues | ${summary.issueCount} |`,
2433
+ `| Sessions | ${summary.sessionCount} |`,
2434
+ `| Total cost (USD) | ${summary.totalCostUsd} |`,
2435
+ `| Avg leverage (edits/session) | ${summary.avgLeverage} |`,
2436
+ `| Avg commit rate | ${summary.avgCommitRate} |`,
2437
+ `| Avg CI pass rate | ${summary.avgCiPassRate} |`,
2438
+ `| Avg chain completeness | ${summary.avgChainCompleteness} |`,
2439
+ `| Session link rate | ${summary.sessionLinkRate} |`,
2440
+ `| Person tier (high/mid/low) | ${summary.tierCounts.high} / ${summary.tierCounts.mid} / ${summary.tierCounts.low} |`
2441
+ ];
2442
+ }
2443
+ function signed(n) {
2444
+ return n > 0 ? `+${n}` : `${n}`;
2445
+ }
2446
+ function renderReportMarkdown(report) {
2447
+ const lines = [];
2448
+ lines.push("# AaaC Observability \u8A3A\u65AD\u30EC\u30DD\u30FC\u30C8");
2449
+ lines.push("");
2450
+ lines.push(`- Generated at: ${report.generatedAt}`);
2451
+ lines.push(`- Project: ${report.projectId ?? "(all)"}`);
2452
+ lines.push(`- Period: ${fmtPeriod(report.period)}`);
2453
+ lines.push("");
2454
+ lines.push("## Summary");
2455
+ lines.push("");
2456
+ lines.push(...summaryTable(report.metrics.summary));
2457
+ lines.push("");
2458
+ lines.push("## Layer 1 \u2014 Scope / Leverage");
2459
+ lines.push("");
2460
+ if (report.metrics.layer1.length === 0) {
2461
+ lines.push("_No issue-linked activity in range._");
2462
+ } else {
2463
+ lines.push("| Issue | Type | Project | Sessions | Edits | Commits | CI | CI passed | Leverage |");
2464
+ lines.push("|---|---|---|---|---|---|---|---|---|");
2465
+ for (const r of report.metrics.layer1) {
2466
+ lines.push(
2467
+ `| ${r.issueId} | ${r.issueType} | ${r.projectId} | ${r.sessionCount} | ${r.editCount} | ${r.commitCount} | ${r.ciCount} | ${r.ciPassed} | ${r.leverage} |`
2468
+ );
2469
+ }
2470
+ }
2471
+ lines.push("");
2472
+ lines.push("## Layer 2 \u2014 Outcome Efficiency");
2473
+ lines.push("");
2474
+ if (report.metrics.layer2.length === 0) {
2475
+ lines.push("_No issue-linked activity in range._");
2476
+ } else {
2477
+ lines.push("| Issue | Project | Sessions | Commits | CI passed | Cost (USD) | Commit rate | CI pass rate | Commits/$ |");
2478
+ lines.push("|---|---|---|---|---|---|---|---|---|");
2479
+ for (const r of report.metrics.layer2) {
2480
+ lines.push(
2481
+ `| ${r.issueId} | ${r.projectId} | ${r.sessions} | ${r.commits} | ${r.ciPassed} | ${r.totalCostUsd} | ${r.commitRate} | ${r.ciPassRate} | ${r.commitsPerDollar} |`
2482
+ );
2483
+ }
2484
+ }
2485
+ lines.push("");
2486
+ lines.push("## Layer 3 \u2014 Outcome Chain & Cost");
2487
+ lines.push("");
2488
+ if (report.metrics.layer3.length === 0) {
2489
+ lines.push("_No issue-linked activity in range._");
2490
+ } else {
2491
+ lines.push("| Issue | Type | Project | Stages | Completeness | Cost (USD) | Input tokens | Output tokens |");
2492
+ lines.push("|---|---|---|---|---|---|---|---|");
2493
+ for (const r of report.metrics.layer3) {
2494
+ lines.push(
2495
+ `| ${r.issueId} | ${r.issueType} | ${r.projectId} | ${r.stagesPresent}/${r.stagesTotal} | ${r.chainCompleteness} | ${r.totalCostUsd} | ${r.totalInputTokens} | ${r.totalOutputTokens} |`
2496
+ );
2497
+ }
2498
+ }
2499
+ lines.push("");
2500
+ lines.push("## Link Completion KPIs");
2501
+ lines.push("");
2502
+ lines.push("### Session link rate");
2503
+ lines.push("");
2504
+ if (report.metrics.linkCompletion.sessionLinkRate.length === 0) {
2505
+ lines.push("_No sessions in range._");
2506
+ } else {
2507
+ lines.push("| Project | Total sessions | Linked | Link rate |");
2508
+ lines.push("|---|---|---|---|");
2509
+ for (const r of report.metrics.linkCompletion.sessionLinkRate) {
2510
+ lines.push(`| ${r.projectId} | ${r.totalSessions} | ${r.linkedSessions} | ${r.linkRate} |`);
2511
+ }
2512
+ }
2513
+ lines.push("");
2514
+ lines.push("### Stage arrival rate (commit \u2192 CI \u2192 deploy)");
2515
+ lines.push("");
2516
+ if (report.metrics.linkCompletion.stageArrival.length === 0) {
2517
+ lines.push("_No issue-linked commits in range._");
2518
+ } else {
2519
+ lines.push("| Project | Commit | CI | Deploy | CI arrival | Deploy arrival |");
2520
+ lines.push("|---|---|---|---|---|---|");
2521
+ for (const r of report.metrics.linkCompletion.stageArrival) {
2522
+ lines.push(
2523
+ `| ${r.projectId} | ${r.issuesWithCommit} | ${r.issuesWithCi} | ${r.issuesWithDeploy} | ${r.ciArrivalRate} | ${r.deployArrivalRate} |`
2524
+ );
2525
+ }
2526
+ }
2527
+ lines.push("");
2528
+ lines.push("### Provisional / unknown ratio");
2529
+ lines.push("");
2530
+ if (report.metrics.linkCompletion.provisional.length === 0) {
2531
+ lines.push("_No spans in range._");
2532
+ } else {
2533
+ lines.push("| Project | Total spans | Unknown session | Provisional | Unknown ratio | Provisional ratio |");
2534
+ lines.push("|---|---|---|---|---|---|");
2535
+ for (const r of report.metrics.linkCompletion.provisional) {
2536
+ lines.push(
2537
+ `| ${r.projectId} | ${r.totalSpans} | ${r.unknownSessionSpans} | ${r.provisionalSpans} | ${r.unknownRatio} | ${r.provisionalRatio} |`
2538
+ );
2539
+ }
2540
+ }
2541
+ lines.push("");
2542
+ lines.push("## Person Tier Ratio");
2543
+ lines.push("");
2544
+ if (report.metrics.tierRatio.length === 0) {
2545
+ lines.push("_No committer activity in range._");
2546
+ } else {
2547
+ lines.push("| Project | Tier | People | Commits | Issues |");
2548
+ lines.push("|---|---|---|---|---|");
2549
+ for (const r of report.metrics.tierRatio) {
2550
+ lines.push(`| ${r.projectId} | ${r.tier} | ${r.personCount} | ${r.tierCommits} | ${r.tierIssues} |`);
2551
+ }
2552
+ }
2553
+ lines.push("");
2554
+ if (report.baseline !== void 0) {
2555
+ const b = report.baseline;
2556
+ lines.push("## Before / After Comparison");
2557
+ lines.push("");
2558
+ lines.push(`- Baseline (before): ${fmtPeriod(b.period)}`);
2559
+ lines.push(`- Current (after): ${fmtPeriod(report.period)}`);
2560
+ lines.push("");
2561
+ lines.push("| Metric | Before | After | \u0394 |");
2562
+ lines.push("|---|---|---|---|");
2563
+ const before = b.metrics.summary;
2564
+ const after = report.metrics.summary;
2565
+ const c = b.comparison;
2566
+ lines.push(`| Issues | ${before.issueCount} | ${after.issueCount} | ${signed(c.issueCount)} |`);
2567
+ lines.push(`| Sessions | ${before.sessionCount} | ${after.sessionCount} | ${signed(c.sessionCount)} |`);
2568
+ lines.push(`| Total cost (USD) | ${before.totalCostUsd} | ${after.totalCostUsd} | ${signed(c.totalCostUsd)} |`);
2569
+ lines.push(`| Avg leverage | ${before.avgLeverage} | ${after.avgLeverage} | ${signed(c.avgLeverage)} |`);
2570
+ lines.push(`| Avg commit rate | ${before.avgCommitRate} | ${after.avgCommitRate} | ${signed(c.avgCommitRate)} |`);
2571
+ lines.push(`| Avg CI pass rate | ${before.avgCiPassRate} | ${after.avgCiPassRate} | ${signed(c.avgCiPassRate)} |`);
2572
+ lines.push(`| Avg chain completeness | ${before.avgChainCompleteness} | ${after.avgChainCompleteness} | ${signed(c.avgChainCompleteness)} |`);
2573
+ lines.push(`| Session link rate | ${before.sessionLinkRate} | ${after.sessionLinkRate} | ${signed(c.sessionLinkRate)} |`);
2574
+ lines.push("");
2575
+ }
2576
+ return lines.join("\n");
2577
+ }
2578
+
1590
2579
  // src/index.ts
1591
2580
  function createPipeline(options = {}) {
1592
2581
  const {
@@ -1671,6 +2660,9 @@ export {
1671
2660
  NormalizationError,
1672
2661
  validateRawEvent,
1673
2662
  Normalizer,
2663
+ VENDOR_OTEL_SOURCE,
2664
+ VENDOR_OTEL_DEDUP_PREFIX,
2665
+ mapVendorOtelExport,
1674
2666
  MemoryCacheStore,
1675
2667
  SqliteCacheStore,
1676
2668
  Correlator,
@@ -1685,8 +2677,17 @@ export {
1685
2677
  recordTaskArtifact,
1686
2678
  createCrossAxisRule,
1687
2679
  createPromotionRule,
2680
+ WORKTREE_WINDOW_MS,
2681
+ worktreeRepoKey,
2682
+ createWorktreeRule,
2683
+ issueIdCacheKey,
2684
+ issueTypeCacheKey,
2685
+ createIssueAnchorRule,
1688
2686
  Enricher,
1689
2687
  createDefaultRules,
2688
+ defaultGitRunner,
2689
+ captureWorktree,
2690
+ worktreeAttributes,
1690
2691
  DEFAULT_DB_PATH,
1691
2692
  SqliteSink,
1692
2693
  OtelEmitter,
@@ -1705,6 +2706,14 @@ export {
1705
2706
  emitQualityGateResult,
1706
2707
  extractPrUrl,
1707
2708
  emitPromotionPr,
2709
+ parseIssueIdFromText,
2710
+ buildGithubPromotionAttributes,
2711
+ githubPromotionEventType,
2712
+ OUTCOME_CHAIN_STAGES,
2713
+ analyzeOutcomeChain,
2714
+ generateDiagnosticReport,
2715
+ renderReportJson,
2716
+ renderReportMarkdown,
1708
2717
  createPipeline
1709
2718
  };
1710
- //# sourceMappingURL=chunk-RUVM5AAO.js.map
2719
+ //# sourceMappingURL=chunk-3DXZNA3E.js.map