@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.
@@ -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) {
@@ -973,7 +1298,7 @@ var OtelEmitter = class {
973
1298
  {
974
1299
  kind: SpanKind.INTERNAL,
975
1300
  startTime: startMs,
976
- attributes: convertAttributes(mapped.attributes),
1301
+ attributes: convertAttributes2(mapped.attributes),
977
1302
  links: mapped.links.map((l) => ({
978
1303
  context: {
979
1304
  traceId: l.targetTraceId ?? mapped.traceId,
@@ -988,7 +1313,7 @@ var OtelEmitter = class {
988
1313
  for (const ev of mapped.spanEvents) {
989
1314
  otelSpan.addEvent(
990
1315
  ev.name,
991
- convertAttributes(ev.attributes),
1316
+ convertAttributes2(ev.attributes),
992
1317
  Number(ev.timeUnixNano / 1000000n)
993
1318
  );
994
1319
  }
@@ -1006,7 +1331,7 @@ var OtelEmitter = class {
1006
1331
  return trace.setSpanContext(ROOT_CONTEXT, spanContext);
1007
1332
  }
1008
1333
  };
1009
- function convertAttributes(attrs) {
1334
+ function convertAttributes2(attrs) {
1010
1335
  return attrs;
1011
1336
  }
1012
1337
 
@@ -1238,8 +1563,9 @@ function parseEventRow(row) {
1238
1563
  sessionId: row.session_id ?? void 0,
1239
1564
  taskId: row.task_id ?? void 0,
1240
1565
  attributes: safeParseJson(row.attributes),
1241
- links: []
1566
+ links: [],
1242
1567
  // links are stored in canonical_links, not inline on events
1568
+ ...row.dedup_key ? { dedupKey: row.dedup_key } : {}
1243
1569
  };
1244
1570
  }
1245
1571
  function parseLinkRow(row) {
@@ -1598,6 +1924,658 @@ function emitPromotionPr(collector, options) {
1598
1924
  return spanId;
1599
1925
  }
1600
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
+
1601
2579
  // src/index.ts
1602
2580
  function createPipeline(options = {}) {
1603
2581
  const {
@@ -1682,6 +2660,9 @@ export {
1682
2660
  NormalizationError,
1683
2661
  validateRawEvent,
1684
2662
  Normalizer,
2663
+ VENDOR_OTEL_SOURCE,
2664
+ VENDOR_OTEL_DEDUP_PREFIX,
2665
+ mapVendorOtelExport,
1685
2666
  MemoryCacheStore,
1686
2667
  SqliteCacheStore,
1687
2668
  Correlator,
@@ -1696,8 +2677,17 @@ export {
1696
2677
  recordTaskArtifact,
1697
2678
  createCrossAxisRule,
1698
2679
  createPromotionRule,
2680
+ WORKTREE_WINDOW_MS,
2681
+ worktreeRepoKey,
2682
+ createWorktreeRule,
2683
+ issueIdCacheKey,
2684
+ issueTypeCacheKey,
2685
+ createIssueAnchorRule,
1699
2686
  Enricher,
1700
2687
  createDefaultRules,
2688
+ defaultGitRunner,
2689
+ captureWorktree,
2690
+ worktreeAttributes,
1701
2691
  DEFAULT_DB_PATH,
1702
2692
  SqliteSink,
1703
2693
  OtelEmitter,
@@ -1716,6 +2706,14 @@ export {
1716
2706
  emitQualityGateResult,
1717
2707
  extractPrUrl,
1718
2708
  emitPromotionPr,
2709
+ parseIssueIdFromText,
2710
+ buildGithubPromotionAttributes,
2711
+ githubPromotionEventType,
2712
+ OUTCOME_CHAIN_STAGES,
2713
+ analyzeOutcomeChain,
2714
+ generateDiagnosticReport,
2715
+ renderReportJson,
2716
+ renderReportMarkdown,
1719
2717
  createPipeline
1720
2718
  };
1721
- //# sourceMappingURL=chunk-J2F5GEMO.js.map
2719
+ //# sourceMappingURL=chunk-3DXZNA3E.js.map