crumbtrail-node 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -123,6 +123,7 @@ __export(index_exports, {
123
123
  cloudWatchEvidenceProvider: () => cloudWatchEvidenceProvider,
124
124
  cloudflareEvidenceProvider: () => cloudflareEvidenceProvider,
125
125
  compareSessions: () => compareSessions,
126
+ comparisonTitle: () => comparisonTitle,
126
127
  createCrumbtrailExpressErrorMiddleware: () => createCrumbtrailExpressErrorMiddleware,
127
128
  createCrumbtrailExpressMiddleware: () => createCrumbtrailExpressMiddleware,
128
129
  createServer: () => createServer,
@@ -172,6 +173,7 @@ __export(index_exports, {
172
173
  renderProviderReadme: () => renderProviderReadme,
173
174
  resolveDbRequestContext: () => resolveDbRequestContext,
174
175
  sentryEvidenceProvider: () => sentryEvidenceProvider,
176
+ sessionRefLabel: () => sessionRefLabel,
175
177
  signSigV4: () => signSigV4,
176
178
  splunkEvidenceProvider: () => splunkEvidenceProvider,
177
179
  splunkSearchDeepLink: () => splunkSearchDeepLink,
@@ -3081,6 +3083,7 @@ var SentryError = class extends Error {
3081
3083
  this.status = status;
3082
3084
  this.name = "SentryError";
3083
3085
  }
3086
+ status;
3084
3087
  };
3085
3088
  function sanitizeUrl2(u) {
3086
3089
  try {
@@ -3494,6 +3497,8 @@ var CloudWatchError = class extends Error {
3494
3497
  this.target = target;
3495
3498
  this.name = "CloudWatchError";
3496
3499
  }
3500
+ status;
3501
+ target;
3497
3502
  };
3498
3503
  function isTransient2(error) {
3499
3504
  if (error instanceof CloudWatchError) {
@@ -3906,6 +3911,7 @@ var SplunkError = class extends Error {
3906
3911
  this.status = status;
3907
3912
  this.name = "SplunkError";
3908
3913
  }
3914
+ status;
3909
3915
  };
3910
3916
  function sanitizeUrl3(u) {
3911
3917
  try {
@@ -4256,6 +4262,7 @@ var DatadogError = class extends Error {
4256
4262
  this.status = status;
4257
4263
  this.name = "DatadogError";
4258
4264
  }
4265
+ status;
4259
4266
  };
4260
4267
  function sanitizeUrl4(u) {
4261
4268
  try {
@@ -4619,6 +4626,7 @@ var PostHogError = class extends Error {
4619
4626
  this.status = status;
4620
4627
  this.name = "PostHogError";
4621
4628
  }
4629
+ status;
4622
4630
  };
4623
4631
  function sanitizeUrl5(u) {
4624
4632
  try {
@@ -5009,6 +5017,8 @@ var CloudflareError = class extends Error {
5009
5017
  this.op = op;
5010
5018
  this.name = "CloudflareError";
5011
5019
  }
5020
+ status;
5021
+ op;
5012
5022
  };
5013
5023
  function isTransient6(error) {
5014
5024
  if (error instanceof CloudflareError) {
@@ -11027,6 +11037,8 @@ var AudioProcessingError = class extends Error {
11027
11037
  this.cause = cause;
11028
11038
  this.name = "AudioProcessingError";
11029
11039
  }
11040
+ phase;
11041
+ cause;
11030
11042
  };
11031
11043
  var AudioTranscriptionUnavailableError = class extends Error {
11032
11044
  constructor() {
@@ -12777,12 +12789,12 @@ var import_node_fs10 = __toESM(require("fs"), 1);
12777
12789
  // package.json
12778
12790
  var package_default = {
12779
12791
  name: "crumbtrail-node",
12780
- version: "0.1.0",
12792
+ version: "0.2.0",
12781
12793
  description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
12782
12794
  license: "MIT",
12783
12795
  repository: {
12784
12796
  type: "git",
12785
- url: "https://github.com/oshabana/crumbtrail.git",
12797
+ url: "git+https://github.com/crumbtrail-dev/crumbtrail-js.git",
12786
12798
  directory: "packages/node"
12787
12799
  },
12788
12800
  keywords: [
@@ -12835,6 +12847,10 @@ var package_default = {
12835
12847
  "@types/node": "^25.5.0",
12836
12848
  express: "^5.2.1",
12837
12849
  typescript: "^5.7.0"
12850
+ },
12851
+ homepage: "https://github.com/crumbtrail-dev/crumbtrail-js/tree/main/packages/node#readme",
12852
+ bugs: {
12853
+ url: "https://github.com/crumbtrail-dev/crumbtrail-js/issues"
12838
12854
  }
12839
12855
  };
12840
12856
 
@@ -13485,6 +13501,10 @@ var RequestValidationError = class extends Error {
13485
13501
  this.retryable = retryable;
13486
13502
  this.name = "RequestValidationError";
13487
13503
  }
13504
+ status;
13505
+ publicMessage;
13506
+ code;
13507
+ retryable;
13488
13508
  };
13489
13509
  function serveStatic(res, staticDir, urlPath) {
13490
13510
  const resolved = resolveStaticPath(staticDir, urlPath);
@@ -14214,6 +14234,7 @@ var BoundedTraceSessionCache = class {
14214
14234
  constructor(maxEntries) {
14215
14235
  this.maxEntries = maxEntries;
14216
14236
  }
14237
+ maxEntries;
14217
14238
  entries = /* @__PURE__ */ new Map();
14218
14239
  get(traceId) {
14219
14240
  const existing = this.entries.get(traceId);
@@ -14912,6 +14933,20 @@ function createServer(config) {
14912
14933
  });
14913
14934
  server.on("close", () => sweeper.stop());
14914
14935
  }
14936
+ if (config.ai?.enabled && config.ai.backfillOnStart) {
14937
+ const sessionDirs = sessions.listSummaries().map((summary) => sessions.getExistingSessionDir(summary.id)).filter((dir) => dir !== void 0);
14938
+ const timer = setTimeout(() => {
14939
+ void backfillAiDiagnoses(
14940
+ sessionDirs,
14941
+ config.ai
14942
+ ).then(
14943
+ (result) => config.ai?.log?.(
14944
+ `Crumbtrail AI diagnosis backfill complete: ${JSON.stringify(result)}`
14945
+ )
14946
+ );
14947
+ }, 0);
14948
+ server.on("close", () => clearTimeout(timer));
14949
+ }
14915
14950
  if (fastFinalizer) {
14916
14951
  server.on("close", () => fastFinalizer.stop());
14917
14952
  }
@@ -14961,6 +14996,7 @@ var FixContextError = class extends Error {
14961
14996
  this.code = code2;
14962
14997
  this.name = "FixContextError";
14963
14998
  }
14999
+ code;
14964
15000
  };
14965
15001
  function buildFixContext(sessionDirOrId, opts = {}) {
14966
15002
  const sessionDir = resolveSessionDir(sessionDirOrId, opts);
@@ -16020,16 +16056,85 @@ function diffEnvironment(a, b, rules, disabled) {
16020
16056
  addSuppressedRules(rules, beforeRules, afterRules, disabled);
16021
16057
  return [];
16022
16058
  }
16059
+ const envDelta = buildEnvDiff(a, b, disabled);
16023
16060
  return [
16024
16061
  {
16025
16062
  plane: "env",
16026
16063
  kind: "env.snapshot",
16027
16064
  before: a,
16028
16065
  after: b,
16029
- brief: "feature flag, config, release, or build snapshot changed between sessions"
16066
+ brief: envBrief(envDelta),
16067
+ envDelta
16030
16068
  }
16031
16069
  ];
16032
16070
  }
16071
+ function buildEnvDiff(a, b, disabled) {
16072
+ const delta = {
16073
+ flags: channelDelta(recordField(a.flags), recordField(b.flags), disabled),
16074
+ config: channelDelta(
16075
+ recordField(a.config),
16076
+ recordField(b.config),
16077
+ disabled
16078
+ )
16079
+ };
16080
+ const releaseBefore = stringOrUndefined(a.release);
16081
+ const releaseAfter = stringOrUndefined(b.release);
16082
+ if (releaseBefore !== releaseAfter) {
16083
+ delta.release = {
16084
+ ...releaseBefore !== void 0 ? { before: releaseBefore } : {},
16085
+ ...releaseAfter !== void 0 ? { after: releaseAfter } : {}
16086
+ };
16087
+ }
16088
+ const buildBefore = stringOrUndefined(a.build);
16089
+ const buildAfter = stringOrUndefined(b.build);
16090
+ if (buildBefore !== buildAfter) {
16091
+ delta.build = {
16092
+ ...buildBefore !== void 0 ? { before: buildBefore } : {},
16093
+ ...buildAfter !== void 0 ? { after: buildAfter } : {}
16094
+ };
16095
+ }
16096
+ return delta;
16097
+ }
16098
+ function recordField(value) {
16099
+ return isRecord14(value) ? value : {};
16100
+ }
16101
+ function channelDelta(a, b, disabled) {
16102
+ const added = [];
16103
+ const removed = [];
16104
+ const changed = [];
16105
+ for (const key of Object.keys(a).sort()) {
16106
+ if (!(key in b)) {
16107
+ removed.push({ key, before: a[key] });
16108
+ } else if (envValuesDiffer(a[key], b[key], disabled)) {
16109
+ changed.push({ key, before: a[key], after: b[key] });
16110
+ }
16111
+ }
16112
+ for (const key of Object.keys(b).sort()) {
16113
+ if (!(key in a)) added.push({ key, after: b[key] });
16114
+ }
16115
+ return { added, removed, changed };
16116
+ }
16117
+ function envValuesDiffer(before, after, disabled) {
16118
+ const beforeRules = /* @__PURE__ */ new Set();
16119
+ const afterRules = /* @__PURE__ */ new Set();
16120
+ if (canonicalize(before, beforeRules) !== canonicalize(after, afterRules))
16121
+ return true;
16122
+ const rawDiffers = stableStringify(before) !== stableStringify(after);
16123
+ return rawDiffers && hasDisabledRule(beforeRules, afterRules, disabled);
16124
+ }
16125
+ function envBrief(delta) {
16126
+ const counts = delta.flags.added.length + delta.flags.removed.length + delta.flags.changed.length + delta.config.added.length + delta.config.removed.length + delta.config.changed.length + (delta.release ? 1 : 0) + (delta.build ? 1 : 0);
16127
+ const parts = [];
16128
+ const flagCount = delta.flags.added.length + delta.flags.removed.length + delta.flags.changed.length;
16129
+ const configCount = delta.config.added.length + delta.config.removed.length + delta.config.changed.length;
16130
+ if (flagCount > 0) parts.push(`${flagCount} flag(s)`);
16131
+ if (configCount > 0) parts.push(`${configCount} config value(s)`);
16132
+ if (delta.release) parts.push("release");
16133
+ if (delta.build) parts.push("build");
16134
+ if (counts === 0 || parts.length === 0)
16135
+ return "feature flag, config, release, or build snapshot changed between sessions";
16136
+ return `environment delta between sessions: ${parts.join(", ")} changed`;
16137
+ }
16033
16138
  function confidenceFor(divergences) {
16034
16139
  if (divergences.length === 0) return "high";
16035
16140
  if (divergences.some(
@@ -16056,6 +16161,9 @@ async function compareSessions(aDir, bDir, options = {}) {
16056
16161
  ...diffDb(a.dbWrites, b.dbWrites, rules, disabled),
16057
16162
  ...diffEnvironment(a.environment, b.environment, rules, disabled)
16058
16163
  ];
16164
+ const envDelta = divergences.find(
16165
+ (divergence) => divergence.kind === "env.snapshot"
16166
+ )?.envDelta;
16059
16167
  return {
16060
16168
  schemaVersion: SESSION_COMPARE_SCHEMA_VERSION,
16061
16169
  verdict: divergences.length > 0 ? "regression" : "clean",
@@ -16078,20 +16186,187 @@ async function compareSessions(aDir, bDir, options = {}) {
16078
16186
  divergences,
16079
16187
  noise: { suppressedCount: rules.size, rules: [...rules].sort() },
16080
16188
  evidence: divergencesToEvidence(divergences),
16081
- intent: []
16189
+ intent: [],
16190
+ ...envDelta ? { envDelta } : {}
16082
16191
  };
16083
16192
  }
16084
16193
 
16085
16194
  // src/compare/regression-context.ts
16086
16195
  var import_node_fs13 = __toESM(require("fs"), 1);
16087
16196
  var import_node_path13 = __toESM(require("path"), 1);
16197
+
16198
+ // src/compare/report.ts
16199
+ var PLANE_ORDER = ["flow", "network", "db", "env"];
16200
+ var PLANE_LABELS = {
16201
+ flow: "Flow steps",
16202
+ network: "Network calls",
16203
+ db: "Database rows",
16204
+ env: "Environment and flags"
16205
+ };
16206
+ function renderCompareReport(comparison) {
16207
+ const lines = [];
16208
+ const regressed = comparison.verdict === "regression";
16209
+ lines.push(
16210
+ `# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
16211
+ );
16212
+ lines.push("");
16213
+ lines.push(
16214
+ regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
16215
+ );
16216
+ lines.push("");
16217
+ lines.push(
16218
+ "A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
16219
+ );
16220
+ lines.push("");
16221
+ lines.push("## Aligned flow");
16222
+ lines.push("");
16223
+ const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
16224
+ lines.push(
16225
+ `${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
16226
+ );
16227
+ lines.push("");
16228
+ if (comparison.divergences.length > 0) {
16229
+ lines.push("| Plane | Kind | Component | Divergence |");
16230
+ lines.push("|---|---|---|---|");
16231
+ for (const d of comparison.divergences) {
16232
+ const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
16233
+ lines.push(
16234
+ `| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
16235
+ );
16236
+ }
16237
+ lines.push("");
16238
+ }
16239
+ for (const plane of PLANE_ORDER) {
16240
+ const planeDivergences = comparison.divergences.filter(
16241
+ (d) => d.plane === plane
16242
+ );
16243
+ if (planeDivergences.length === 0) continue;
16244
+ lines.push(`## ${PLANE_LABELS[plane]}`);
16245
+ lines.push("");
16246
+ for (const d of planeDivergences) {
16247
+ lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
16248
+ lines.push("");
16249
+ if (d.sig) lines.push(`- Component: \`${d.sig}\``);
16250
+ if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
16251
+ if (d.table)
16252
+ lines.push(
16253
+ `- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
16254
+ );
16255
+ lines.push("");
16256
+ if (d.envDelta) {
16257
+ lines.push(...renderEnvDelta(d.envDelta));
16258
+ } else {
16259
+ lines.push("```diff");
16260
+ lines.push(`- before: ${JSON.stringify(d.before)}`);
16261
+ lines.push(`+ after: ${JSON.stringify(d.after)}`);
16262
+ lines.push("```");
16263
+ }
16264
+ lines.push("");
16265
+ }
16266
+ }
16267
+ lines.push("---");
16268
+ lines.push("");
16269
+ lines.push(
16270
+ comparison.noise.suppressedCount > 0 ? `*${comparison.noise.suppressedCount} expected-variance difference(s) suppressed by the noise model (${comparison.noise.rules.join(", ")}).*` : "*No differences were suppressed by the noise model.*"
16271
+ );
16272
+ lines.push("");
16273
+ return lines.join("\n");
16274
+ }
16275
+ function formatComparisonSummary(comparison) {
16276
+ const lines = [];
16277
+ lines.push(
16278
+ `crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
16279
+ );
16280
+ lines.push(` Schema: ${comparison.schemaVersion}`);
16281
+ lines.push(
16282
+ ` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
16283
+ );
16284
+ lines.push(
16285
+ ` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
16286
+ );
16287
+ lines.push(` Divergences: ${comparison.divergences.length}`);
16288
+ for (const d of comparison.divergences.slice(0, 5)) {
16289
+ lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
16290
+ }
16291
+ if (comparison.divergences.length > 5) {
16292
+ lines.push(
16293
+ ` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
16294
+ );
16295
+ }
16296
+ const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
16297
+ lines.push(
16298
+ ` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
16299
+ );
16300
+ return lines.join("\n");
16301
+ }
16302
+ function escapeCell(text3) {
16303
+ return text3.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
16304
+ }
16305
+ function sessionRefLabel(ref) {
16306
+ return ref.release ?? ref.sessionId;
16307
+ }
16308
+ function comparisonTitle(comparison) {
16309
+ return `${sessionRefLabel(comparison.a)} vs ${sessionRefLabel(comparison.b)}`;
16310
+ }
16311
+ function renderEnvDelta(delta) {
16312
+ const lines = ["```diff"];
16313
+ renderEnvChannel(lines, "flags", delta.flags);
16314
+ renderEnvChannel(lines, "config", delta.config);
16315
+ if (delta.release) {
16316
+ lines.push(
16317
+ `~ release: ${JSON.stringify(delta.release.before ?? null)} -> ${JSON.stringify(delta.release.after ?? null)}`
16318
+ );
16319
+ }
16320
+ if (delta.build) {
16321
+ lines.push(
16322
+ `~ build: ${JSON.stringify(delta.build.before ?? null)} -> ${JSON.stringify(delta.build.after ?? null)}`
16323
+ );
16324
+ }
16325
+ if (lines.length === 1) lines.push(" (no field-level changes)");
16326
+ lines.push("```");
16327
+ return lines;
16328
+ }
16329
+ function renderEnvChannel(lines, channel, delta) {
16330
+ for (const change of delta.added) {
16331
+ lines.push(
16332
+ `+ ${channel}.${change.key}: ${JSON.stringify(change.after ?? null)}`
16333
+ );
16334
+ }
16335
+ for (const change of delta.removed) {
16336
+ lines.push(
16337
+ `- ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)}`
16338
+ );
16339
+ }
16340
+ for (const change of delta.changed) {
16341
+ lines.push(
16342
+ `~ ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)} -> ${JSON.stringify(change.after ?? null)}`
16343
+ );
16344
+ }
16345
+ }
16346
+ function formatSessionRef(ref) {
16347
+ if (ref.release) {
16348
+ const detail = [
16349
+ `session ${ref.sessionId}`,
16350
+ ref.build ? `build ${ref.build}` : void 0
16351
+ ].filter(Boolean);
16352
+ return `${ref.release} (${detail.join(", ")})`;
16353
+ }
16354
+ return ref.build ? `${ref.sessionId} (build ${ref.build})` : ref.sessionId;
16355
+ }
16356
+
16357
+ // src/compare/regression-context.ts
16088
16358
  var REGRESSION_CONTEXT_SCHEMA_VERSION = "regression-context.v1";
16089
16359
  function buildRegressionContext(comparison, bDir) {
16090
- const requestIds = unique(comparison.divergences.map((d) => d.requestId).filter(isString));
16091
- const times = requestIds.flatMap((requestId) => requestTimesFromIndex(bDir, requestId));
16360
+ const requestIds = unique(
16361
+ comparison.divergences.map((d) => d.requestId).filter(isString)
16362
+ );
16363
+ const times = requestIds.flatMap(
16364
+ (requestId) => requestTimesFromIndex(bDir, requestId)
16365
+ );
16092
16366
  const firstSig = comparison.divergences.map((d) => d.sig).find(isString);
16093
16367
  return {
16094
16368
  schemaVersion: REGRESSION_CONTEXT_SCHEMA_VERSION,
16369
+ title: comparisonTitle(comparison),
16095
16370
  comparison,
16096
16371
  divergent_interaction: firstSig ? interactionForSig(bDir, firstSig) : null,
16097
16372
  causal_window: requestIds.length > 0 ? {
@@ -16109,6 +16384,7 @@ function buildRegressionContext(comparison, bDir) {
16109
16384
  before: d.before,
16110
16385
  after: d.after
16111
16386
  })),
16387
+ env_delta: comparison.envDelta ?? null,
16112
16388
  repro_hint: reproHint(bDir)
16113
16389
  };
16114
16390
  }
@@ -16153,7 +16429,8 @@ function collectRefTimes(value, times) {
16153
16429
  const start = isRecord15(value.start) ? value.start : void 0;
16154
16430
  const end = isRecord15(value.end) ? value.end : void 0;
16155
16431
  for (const candidate of [ref, start, end]) {
16156
- if (typeof candidate?.t === "number" && Number.isFinite(candidate.t)) times.push(candidate.t);
16432
+ if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
16433
+ times.push(candidate.t);
16157
16434
  }
16158
16435
  }
16159
16436
  function reproHint(sessionDir) {
@@ -17078,12 +17355,29 @@ var McpServer = class {
17078
17355
  "sha"
17079
17356
  ]))
17080
17357
  continue;
17081
- sessions.push(meta);
17358
+ sessions.push(this.withReleaseBuild(meta));
17082
17359
  } catch {
17083
17360
  }
17084
17361
  }
17085
17362
  return textResult(sessions);
17086
17363
  }
17364
+ /**
17365
+ * Surfaces release/build as first-class list-row fields regardless of which
17366
+ * alias the app used (release/releaseId/version, build/buildId/commit/sha), so
17367
+ * an agent can label and group sessions by release without re-reading each
17368
+ * meta. Additive: the raw meta keys are preserved.
17369
+ */
17370
+ withReleaseBuild(meta) {
17371
+ const release = stringField5(meta.release ?? meta.releaseId ?? meta.version);
17372
+ const build = stringField5(
17373
+ meta.build ?? meta.buildId ?? meta.commit ?? meta.sha
17374
+ );
17375
+ return {
17376
+ ...meta,
17377
+ ...release !== void 0 ? { release } : {},
17378
+ ...build !== void 0 ? { build } : {}
17379
+ };
17380
+ }
17087
17381
  sessionMetadataMatches(meta, expected, keys) {
17088
17382
  return keys.some((key) => meta[key] === expected);
17089
17383
  }
@@ -20089,6 +20383,7 @@ var InspectError = class extends Error {
20089
20383
  this.code = code2;
20090
20384
  this.name = "InspectError";
20091
20385
  }
20386
+ code;
20092
20387
  };
20093
20388
  function inspectSession(sessionDirOrId, opts = {}) {
20094
20389
  const sessionDir = resolveSessionDir2(sessionDirOrId, opts);
@@ -20715,117 +21010,6 @@ function isRecord21(value) {
20715
21010
  return value !== null && typeof value === "object" && !Array.isArray(value);
20716
21011
  }
20717
21012
 
20718
- // src/compare/report.ts
20719
- var PLANE_ORDER = ["flow", "network", "db", "env"];
20720
- var PLANE_LABELS = {
20721
- flow: "Flow steps",
20722
- network: "Network calls",
20723
- db: "Database rows",
20724
- env: "Environment and flags"
20725
- };
20726
- function renderCompareReport(comparison) {
20727
- const lines = [];
20728
- const regressed = comparison.verdict === "regression";
20729
- lines.push(
20730
- `# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
20731
- );
20732
- lines.push("");
20733
- lines.push(
20734
- regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
20735
- );
20736
- lines.push("");
20737
- lines.push(
20738
- "A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
20739
- );
20740
- lines.push("");
20741
- lines.push("## Aligned flow");
20742
- lines.push("");
20743
- const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
20744
- lines.push(
20745
- `${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
20746
- );
20747
- lines.push("");
20748
- if (comparison.divergences.length > 0) {
20749
- lines.push("| Plane | Kind | Component | Divergence |");
20750
- lines.push("|---|---|---|---|");
20751
- for (const d of comparison.divergences) {
20752
- const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
20753
- lines.push(
20754
- `| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
20755
- );
20756
- }
20757
- lines.push("");
20758
- }
20759
- for (const plane of PLANE_ORDER) {
20760
- const planeDivergences = comparison.divergences.filter(
20761
- (d) => d.plane === plane
20762
- );
20763
- if (planeDivergences.length === 0) continue;
20764
- lines.push(`## ${PLANE_LABELS[plane]}`);
20765
- lines.push("");
20766
- for (const d of planeDivergences) {
20767
- lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
20768
- lines.push("");
20769
- if (d.sig) lines.push(`- Component: \`${d.sig}\``);
20770
- if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
20771
- if (d.table)
20772
- lines.push(
20773
- `- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
20774
- );
20775
- lines.push("");
20776
- lines.push("```diff");
20777
- lines.push(`- before: ${JSON.stringify(d.before)}`);
20778
- lines.push(`+ after: ${JSON.stringify(d.after)}`);
20779
- lines.push("```");
20780
- lines.push("");
20781
- }
20782
- }
20783
- lines.push("---");
20784
- lines.push("");
20785
- lines.push(
20786
- comparison.noise.suppressedCount > 0 ? `*${comparison.noise.suppressedCount} expected-variance difference(s) suppressed by the noise model (${comparison.noise.rules.join(", ")}).*` : "*No differences were suppressed by the noise model.*"
20787
- );
20788
- lines.push("");
20789
- return lines.join("\n");
20790
- }
20791
- function formatComparisonSummary(comparison) {
20792
- const lines = [];
20793
- lines.push(
20794
- `crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
20795
- );
20796
- lines.push(` Schema: ${comparison.schemaVersion}`);
20797
- lines.push(
20798
- ` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
20799
- );
20800
- lines.push(
20801
- ` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
20802
- );
20803
- lines.push(` Divergences: ${comparison.divergences.length}`);
20804
- for (const d of comparison.divergences.slice(0, 5)) {
20805
- lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
20806
- }
20807
- if (comparison.divergences.length > 5) {
20808
- lines.push(
20809
- ` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
20810
- );
20811
- }
20812
- const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
20813
- lines.push(
20814
- ` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
20815
- );
20816
- return lines.join("\n");
20817
- }
20818
- function escapeCell(text3) {
20819
- return text3.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
20820
- }
20821
- function formatSessionRef(ref) {
20822
- const tags = [
20823
- ref.release ? `release ${ref.release}` : void 0,
20824
- ref.build ? `build ${ref.build}` : void 0
20825
- ].filter(Boolean);
20826
- return tags.length > 0 ? `${ref.sessionId} (${tags.join(", ")})` : ref.sessionId;
20827
- }
20828
-
20829
21013
  // src/ticket/comment.ts
20830
21014
  var REASON_PHRASES = {
20831
21015
  semantic: "wording overlap with the captured incident",
@@ -21276,6 +21460,7 @@ function bounded(value, max) {
21276
21460
  cloudWatchEvidenceProvider,
21277
21461
  cloudflareEvidenceProvider,
21278
21462
  compareSessions,
21463
+ comparisonTitle,
21279
21464
  createCrumbtrailExpressErrorMiddleware,
21280
21465
  createCrumbtrailExpressMiddleware,
21281
21466
  createServer,
@@ -21325,6 +21510,7 @@ function bounded(value, max) {
21325
21510
  renderProviderReadme,
21326
21511
  resolveDbRequestContext,
21327
21512
  sentryEvidenceProvider,
21513
+ sessionRefLabel,
21328
21514
  signSigV4,
21329
21515
  splunkEvidenceProvider,
21330
21516
  splunkSearchDeepLink,