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.
@@ -2702,6 +2702,7 @@ var SentryError = class extends Error {
2702
2702
  this.status = status;
2703
2703
  this.name = "SentryError";
2704
2704
  }
2705
+ status;
2705
2706
  };
2706
2707
  function sanitizeUrl2(u) {
2707
2708
  try {
@@ -3115,6 +3116,8 @@ var CloudWatchError = class extends Error {
3115
3116
  this.target = target;
3116
3117
  this.name = "CloudWatchError";
3117
3118
  }
3119
+ status;
3120
+ target;
3118
3121
  };
3119
3122
  function isTransient2(error) {
3120
3123
  if (error instanceof CloudWatchError) {
@@ -3527,6 +3530,7 @@ var SplunkError = class extends Error {
3527
3530
  this.status = status;
3528
3531
  this.name = "SplunkError";
3529
3532
  }
3533
+ status;
3530
3534
  };
3531
3535
  function sanitizeUrl3(u) {
3532
3536
  try {
@@ -3877,6 +3881,7 @@ var DatadogError = class extends Error {
3877
3881
  this.status = status;
3878
3882
  this.name = "DatadogError";
3879
3883
  }
3884
+ status;
3880
3885
  };
3881
3886
  function sanitizeUrl4(u) {
3882
3887
  try {
@@ -4240,6 +4245,7 @@ var PostHogError = class extends Error {
4240
4245
  this.status = status;
4241
4246
  this.name = "PostHogError";
4242
4247
  }
4248
+ status;
4243
4249
  };
4244
4250
  function sanitizeUrl5(u) {
4245
4251
  try {
@@ -4630,6 +4636,8 @@ var CloudflareError = class extends Error {
4630
4636
  this.op = op;
4631
4637
  this.name = "CloudflareError";
4632
4638
  }
4639
+ status;
4640
+ op;
4633
4641
  };
4634
4642
  function isTransient6(error) {
4635
4643
  if (error instanceof CloudflareError) {
@@ -10482,6 +10490,8 @@ var AudioProcessingError = class extends Error {
10482
10490
  this.cause = cause;
10483
10491
  this.name = "AudioProcessingError";
10484
10492
  }
10493
+ phase;
10494
+ cause;
10485
10495
  };
10486
10496
  var AudioTranscriptionUnavailableError = class extends Error {
10487
10497
  constructor() {
@@ -12964,12 +12974,12 @@ import fs10 from "fs";
12964
12974
  // package.json
12965
12975
  var package_default = {
12966
12976
  name: "crumbtrail-node",
12967
- version: "0.1.0",
12977
+ version: "0.2.0",
12968
12978
  description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
12969
12979
  license: "MIT",
12970
12980
  repository: {
12971
12981
  type: "git",
12972
- url: "https://github.com/oshabana/crumbtrail.git",
12982
+ url: "git+https://github.com/crumbtrail-dev/crumbtrail-js.git",
12973
12983
  directory: "packages/node"
12974
12984
  },
12975
12985
  keywords: [
@@ -13022,6 +13032,10 @@ var package_default = {
13022
13032
  "@types/node": "^25.5.0",
13023
13033
  express: "^5.2.1",
13024
13034
  typescript: "^5.7.0"
13035
+ },
13036
+ homepage: "https://github.com/crumbtrail-dev/crumbtrail-js/tree/main/packages/node#readme",
13037
+ bugs: {
13038
+ url: "https://github.com/crumbtrail-dev/crumbtrail-js/issues"
13025
13039
  }
13026
13040
  };
13027
13041
 
@@ -13299,6 +13313,10 @@ var RequestValidationError = class extends Error {
13299
13313
  this.retryable = retryable;
13300
13314
  this.name = "RequestValidationError";
13301
13315
  }
13316
+ status;
13317
+ publicMessage;
13318
+ code;
13319
+ retryable;
13302
13320
  };
13303
13321
  function serveStatic(res, staticDir, urlPath) {
13304
13322
  const resolved = resolveStaticPath(staticDir, urlPath);
@@ -14028,6 +14046,7 @@ var BoundedTraceSessionCache = class {
14028
14046
  constructor(maxEntries) {
14029
14047
  this.maxEntries = maxEntries;
14030
14048
  }
14049
+ maxEntries;
14031
14050
  entries = /* @__PURE__ */ new Map();
14032
14051
  get(traceId) {
14033
14052
  const existing = this.entries.get(traceId);
@@ -14726,6 +14745,20 @@ function createServer(config) {
14726
14745
  });
14727
14746
  server.on("close", () => sweeper.stop());
14728
14747
  }
14748
+ if (config.ai?.enabled && config.ai.backfillOnStart) {
14749
+ const sessionDirs = sessions.listSummaries().map((summary) => sessions.getExistingSessionDir(summary.id)).filter((dir) => dir !== void 0);
14750
+ const timer = setTimeout(() => {
14751
+ void backfillAiDiagnoses(
14752
+ sessionDirs,
14753
+ config.ai
14754
+ ).then(
14755
+ (result) => config.ai?.log?.(
14756
+ `Crumbtrail AI diagnosis backfill complete: ${JSON.stringify(result)}`
14757
+ )
14758
+ );
14759
+ }, 0);
14760
+ server.on("close", () => clearTimeout(timer));
14761
+ }
14729
14762
  if (fastFinalizer) {
14730
14763
  server.on("close", () => fastFinalizer.stop());
14731
14764
  }
@@ -14769,6 +14802,7 @@ var FixContextError = class extends Error {
14769
14802
  this.code = code;
14770
14803
  this.name = "FixContextError";
14771
14804
  }
14805
+ code;
14772
14806
  };
14773
14807
  function buildFixContext(sessionDirOrId, opts = {}) {
14774
14808
  const sessionDir = resolveSessionDir(sessionDirOrId, opts);
@@ -15701,16 +15735,85 @@ function diffEnvironment(a, b, rules, disabled) {
15701
15735
  addSuppressedRules(rules, beforeRules, afterRules, disabled);
15702
15736
  return [];
15703
15737
  }
15738
+ const envDelta = buildEnvDiff(a, b, disabled);
15704
15739
  return [
15705
15740
  {
15706
15741
  plane: "env",
15707
15742
  kind: "env.snapshot",
15708
15743
  before: a,
15709
15744
  after: b,
15710
- brief: "feature flag, config, release, or build snapshot changed between sessions"
15745
+ brief: envBrief(envDelta),
15746
+ envDelta
15711
15747
  }
15712
15748
  ];
15713
15749
  }
15750
+ function buildEnvDiff(a, b, disabled) {
15751
+ const delta = {
15752
+ flags: channelDelta(recordField(a.flags), recordField(b.flags), disabled),
15753
+ config: channelDelta(
15754
+ recordField(a.config),
15755
+ recordField(b.config),
15756
+ disabled
15757
+ )
15758
+ };
15759
+ const releaseBefore = stringOrUndefined(a.release);
15760
+ const releaseAfter = stringOrUndefined(b.release);
15761
+ if (releaseBefore !== releaseAfter) {
15762
+ delta.release = {
15763
+ ...releaseBefore !== void 0 ? { before: releaseBefore } : {},
15764
+ ...releaseAfter !== void 0 ? { after: releaseAfter } : {}
15765
+ };
15766
+ }
15767
+ const buildBefore = stringOrUndefined(a.build);
15768
+ const buildAfter = stringOrUndefined(b.build);
15769
+ if (buildBefore !== buildAfter) {
15770
+ delta.build = {
15771
+ ...buildBefore !== void 0 ? { before: buildBefore } : {},
15772
+ ...buildAfter !== void 0 ? { after: buildAfter } : {}
15773
+ };
15774
+ }
15775
+ return delta;
15776
+ }
15777
+ function recordField(value) {
15778
+ return isRecord14(value) ? value : {};
15779
+ }
15780
+ function channelDelta(a, b, disabled) {
15781
+ const added = [];
15782
+ const removed = [];
15783
+ const changed = [];
15784
+ for (const key of Object.keys(a).sort()) {
15785
+ if (!(key in b)) {
15786
+ removed.push({ key, before: a[key] });
15787
+ } else if (envValuesDiffer(a[key], b[key], disabled)) {
15788
+ changed.push({ key, before: a[key], after: b[key] });
15789
+ }
15790
+ }
15791
+ for (const key of Object.keys(b).sort()) {
15792
+ if (!(key in a)) added.push({ key, after: b[key] });
15793
+ }
15794
+ return { added, removed, changed };
15795
+ }
15796
+ function envValuesDiffer(before, after, disabled) {
15797
+ const beforeRules = /* @__PURE__ */ new Set();
15798
+ const afterRules = /* @__PURE__ */ new Set();
15799
+ if (canonicalize(before, beforeRules) !== canonicalize(after, afterRules))
15800
+ return true;
15801
+ const rawDiffers = stableStringify(before) !== stableStringify(after);
15802
+ return rawDiffers && hasDisabledRule(beforeRules, afterRules, disabled);
15803
+ }
15804
+ function envBrief(delta) {
15805
+ 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);
15806
+ const parts = [];
15807
+ const flagCount = delta.flags.added.length + delta.flags.removed.length + delta.flags.changed.length;
15808
+ const configCount = delta.config.added.length + delta.config.removed.length + delta.config.changed.length;
15809
+ if (flagCount > 0) parts.push(`${flagCount} flag(s)`);
15810
+ if (configCount > 0) parts.push(`${configCount} config value(s)`);
15811
+ if (delta.release) parts.push("release");
15812
+ if (delta.build) parts.push("build");
15813
+ if (counts === 0 || parts.length === 0)
15814
+ return "feature flag, config, release, or build snapshot changed between sessions";
15815
+ return `environment delta between sessions: ${parts.join(", ")} changed`;
15816
+ }
15714
15817
  function confidenceFor(divergences) {
15715
15818
  if (divergences.length === 0) return "high";
15716
15819
  if (divergences.some(
@@ -15737,6 +15840,9 @@ async function compareSessions(aDir, bDir, options = {}) {
15737
15840
  ...diffDb(a.dbWrites, b.dbWrites, rules, disabled),
15738
15841
  ...diffEnvironment(a.environment, b.environment, rules, disabled)
15739
15842
  ];
15843
+ const envDelta = divergences.find(
15844
+ (divergence) => divergence.kind === "env.snapshot"
15845
+ )?.envDelta;
15740
15846
  return {
15741
15847
  schemaVersion: SESSION_COMPARE_SCHEMA_VERSION,
15742
15848
  verdict: divergences.length > 0 ? "regression" : "clean",
@@ -15759,20 +15865,185 @@ async function compareSessions(aDir, bDir, options = {}) {
15759
15865
  divergences,
15760
15866
  noise: { suppressedCount: rules.size, rules: [...rules].sort() },
15761
15867
  evidence: divergencesToEvidence(divergences),
15762
- intent: []
15868
+ intent: [],
15869
+ ...envDelta ? { envDelta } : {}
15763
15870
  };
15764
15871
  }
15765
15872
 
15873
+ // src/compare/report.ts
15874
+ var PLANE_ORDER = ["flow", "network", "db", "env"];
15875
+ var PLANE_LABELS = {
15876
+ flow: "Flow steps",
15877
+ network: "Network calls",
15878
+ db: "Database rows",
15879
+ env: "Environment and flags"
15880
+ };
15881
+ function renderCompareReport(comparison) {
15882
+ const lines = [];
15883
+ const regressed = comparison.verdict === "regression";
15884
+ lines.push(
15885
+ `# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
15886
+ );
15887
+ lines.push("");
15888
+ lines.push(
15889
+ regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
15890
+ );
15891
+ lines.push("");
15892
+ lines.push(
15893
+ "A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
15894
+ );
15895
+ lines.push("");
15896
+ lines.push("## Aligned flow");
15897
+ lines.push("");
15898
+ const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
15899
+ lines.push(
15900
+ `${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
15901
+ );
15902
+ lines.push("");
15903
+ if (comparison.divergences.length > 0) {
15904
+ lines.push("| Plane | Kind | Component | Divergence |");
15905
+ lines.push("|---|---|---|---|");
15906
+ for (const d of comparison.divergences) {
15907
+ const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
15908
+ lines.push(
15909
+ `| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
15910
+ );
15911
+ }
15912
+ lines.push("");
15913
+ }
15914
+ for (const plane of PLANE_ORDER) {
15915
+ const planeDivergences = comparison.divergences.filter(
15916
+ (d) => d.plane === plane
15917
+ );
15918
+ if (planeDivergences.length === 0) continue;
15919
+ lines.push(`## ${PLANE_LABELS[plane]}`);
15920
+ lines.push("");
15921
+ for (const d of planeDivergences) {
15922
+ lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
15923
+ lines.push("");
15924
+ if (d.sig) lines.push(`- Component: \`${d.sig}\``);
15925
+ if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
15926
+ if (d.table)
15927
+ lines.push(
15928
+ `- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
15929
+ );
15930
+ lines.push("");
15931
+ if (d.envDelta) {
15932
+ lines.push(...renderEnvDelta(d.envDelta));
15933
+ } else {
15934
+ lines.push("```diff");
15935
+ lines.push(`- before: ${JSON.stringify(d.before)}`);
15936
+ lines.push(`+ after: ${JSON.stringify(d.after)}`);
15937
+ lines.push("```");
15938
+ }
15939
+ lines.push("");
15940
+ }
15941
+ }
15942
+ lines.push("---");
15943
+ lines.push("");
15944
+ lines.push(
15945
+ 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.*"
15946
+ );
15947
+ lines.push("");
15948
+ return lines.join("\n");
15949
+ }
15950
+ function formatComparisonSummary(comparison) {
15951
+ const lines = [];
15952
+ lines.push(
15953
+ `crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
15954
+ );
15955
+ lines.push(` Schema: ${comparison.schemaVersion}`);
15956
+ lines.push(
15957
+ ` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
15958
+ );
15959
+ lines.push(
15960
+ ` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
15961
+ );
15962
+ lines.push(` Divergences: ${comparison.divergences.length}`);
15963
+ for (const d of comparison.divergences.slice(0, 5)) {
15964
+ lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
15965
+ }
15966
+ if (comparison.divergences.length > 5) {
15967
+ lines.push(
15968
+ ` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
15969
+ );
15970
+ }
15971
+ const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
15972
+ lines.push(
15973
+ ` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
15974
+ );
15975
+ return lines.join("\n");
15976
+ }
15977
+ function escapeCell(text2) {
15978
+ return text2.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
15979
+ }
15980
+ function sessionRefLabel(ref) {
15981
+ return ref.release ?? ref.sessionId;
15982
+ }
15983
+ function comparisonTitle(comparison) {
15984
+ return `${sessionRefLabel(comparison.a)} vs ${sessionRefLabel(comparison.b)}`;
15985
+ }
15986
+ function renderEnvDelta(delta) {
15987
+ const lines = ["```diff"];
15988
+ renderEnvChannel(lines, "flags", delta.flags);
15989
+ renderEnvChannel(lines, "config", delta.config);
15990
+ if (delta.release) {
15991
+ lines.push(
15992
+ `~ release: ${JSON.stringify(delta.release.before ?? null)} -> ${JSON.stringify(delta.release.after ?? null)}`
15993
+ );
15994
+ }
15995
+ if (delta.build) {
15996
+ lines.push(
15997
+ `~ build: ${JSON.stringify(delta.build.before ?? null)} -> ${JSON.stringify(delta.build.after ?? null)}`
15998
+ );
15999
+ }
16000
+ if (lines.length === 1) lines.push(" (no field-level changes)");
16001
+ lines.push("```");
16002
+ return lines;
16003
+ }
16004
+ function renderEnvChannel(lines, channel, delta) {
16005
+ for (const change of delta.added) {
16006
+ lines.push(
16007
+ `+ ${channel}.${change.key}: ${JSON.stringify(change.after ?? null)}`
16008
+ );
16009
+ }
16010
+ for (const change of delta.removed) {
16011
+ lines.push(
16012
+ `- ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)}`
16013
+ );
16014
+ }
16015
+ for (const change of delta.changed) {
16016
+ lines.push(
16017
+ `~ ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)} -> ${JSON.stringify(change.after ?? null)}`
16018
+ );
16019
+ }
16020
+ }
16021
+ function formatSessionRef(ref) {
16022
+ if (ref.release) {
16023
+ const detail = [
16024
+ `session ${ref.sessionId}`,
16025
+ ref.build ? `build ${ref.build}` : void 0
16026
+ ].filter(Boolean);
16027
+ return `${ref.release} (${detail.join(", ")})`;
16028
+ }
16029
+ return ref.build ? `${ref.sessionId} (build ${ref.build})` : ref.sessionId;
16030
+ }
16031
+
15766
16032
  // src/compare/regression-context.ts
15767
16033
  import fs13 from "fs";
15768
16034
  import path13 from "path";
15769
16035
  var REGRESSION_CONTEXT_SCHEMA_VERSION = "regression-context.v1";
15770
16036
  function buildRegressionContext(comparison, bDir) {
15771
- const requestIds = unique(comparison.divergences.map((d) => d.requestId).filter(isString));
15772
- const times = requestIds.flatMap((requestId) => requestTimesFromIndex(bDir, requestId));
16037
+ const requestIds = unique(
16038
+ comparison.divergences.map((d) => d.requestId).filter(isString)
16039
+ );
16040
+ const times = requestIds.flatMap(
16041
+ (requestId) => requestTimesFromIndex(bDir, requestId)
16042
+ );
15773
16043
  const firstSig = comparison.divergences.map((d) => d.sig).find(isString);
15774
16044
  return {
15775
16045
  schemaVersion: REGRESSION_CONTEXT_SCHEMA_VERSION,
16046
+ title: comparisonTitle(comparison),
15776
16047
  comparison,
15777
16048
  divergent_interaction: firstSig ? interactionForSig(bDir, firstSig) : null,
15778
16049
  causal_window: requestIds.length > 0 ? {
@@ -15790,6 +16061,7 @@ function buildRegressionContext(comparison, bDir) {
15790
16061
  before: d.before,
15791
16062
  after: d.after
15792
16063
  })),
16064
+ env_delta: comparison.envDelta ?? null,
15793
16065
  repro_hint: reproHint(bDir)
15794
16066
  };
15795
16067
  }
@@ -15834,7 +16106,8 @@ function collectRefTimes(value, times) {
15834
16106
  const start = isRecord15(value.start) ? value.start : void 0;
15835
16107
  const end = isRecord15(value.end) ? value.end : void 0;
15836
16108
  for (const candidate of [ref, start, end]) {
15837
- if (typeof candidate?.t === "number" && Number.isFinite(candidate.t)) times.push(candidate.t);
16109
+ if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
16110
+ times.push(candidate.t);
15838
16111
  }
15839
16112
  }
15840
16113
  function reproHint(sessionDir) {
@@ -16892,12 +17165,29 @@ var McpServer = class {
16892
17165
  "sha"
16893
17166
  ]))
16894
17167
  continue;
16895
- sessions.push(meta);
17168
+ sessions.push(this.withReleaseBuild(meta));
16896
17169
  } catch {
16897
17170
  }
16898
17171
  }
16899
17172
  return textResult(sessions);
16900
17173
  }
17174
+ /**
17175
+ * Surfaces release/build as first-class list-row fields regardless of which
17176
+ * alias the app used (release/releaseId/version, build/buildId/commit/sha), so
17177
+ * an agent can label and group sessions by release without re-reading each
17178
+ * meta. Additive: the raw meta keys are preserved.
17179
+ */
17180
+ withReleaseBuild(meta) {
17181
+ const release = stringField5(meta.release ?? meta.releaseId ?? meta.version);
17182
+ const build = stringField5(
17183
+ meta.build ?? meta.buildId ?? meta.commit ?? meta.sha
17184
+ );
17185
+ return {
17186
+ ...meta,
17187
+ ...release !== void 0 ? { release } : {},
17188
+ ...build !== void 0 ? { build } : {}
17189
+ };
17190
+ }
16901
17191
  sessionMetadataMatches(meta, expected, keys) {
16902
17192
  return keys.some((key) => meta[key] === expected);
16903
17193
  }
@@ -18273,6 +18563,7 @@ var InspectError = class extends Error {
18273
18563
  this.code = code;
18274
18564
  this.name = "InspectError";
18275
18565
  }
18566
+ code;
18276
18567
  };
18277
18568
  function inspectSession(sessionDirOrId, opts = {}) {
18278
18569
  const sessionDir = resolveSessionDir2(sessionDirOrId, opts);
@@ -18400,117 +18691,6 @@ function isRecord17(value) {
18400
18691
  return value != null && typeof value === "object" && !Array.isArray(value);
18401
18692
  }
18402
18693
 
18403
- // src/compare/report.ts
18404
- var PLANE_ORDER = ["flow", "network", "db", "env"];
18405
- var PLANE_LABELS = {
18406
- flow: "Flow steps",
18407
- network: "Network calls",
18408
- db: "Database rows",
18409
- env: "Environment and flags"
18410
- };
18411
- function renderCompareReport(comparison) {
18412
- const lines = [];
18413
- const regressed = comparison.verdict === "regression";
18414
- lines.push(
18415
- `# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
18416
- );
18417
- lines.push("");
18418
- lines.push(
18419
- regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
18420
- );
18421
- lines.push("");
18422
- lines.push(
18423
- "A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
18424
- );
18425
- lines.push("");
18426
- lines.push("## Aligned flow");
18427
- lines.push("");
18428
- const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
18429
- lines.push(
18430
- `${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
18431
- );
18432
- lines.push("");
18433
- if (comparison.divergences.length > 0) {
18434
- lines.push("| Plane | Kind | Component | Divergence |");
18435
- lines.push("|---|---|---|---|");
18436
- for (const d of comparison.divergences) {
18437
- const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
18438
- lines.push(
18439
- `| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
18440
- );
18441
- }
18442
- lines.push("");
18443
- }
18444
- for (const plane of PLANE_ORDER) {
18445
- const planeDivergences = comparison.divergences.filter(
18446
- (d) => d.plane === plane
18447
- );
18448
- if (planeDivergences.length === 0) continue;
18449
- lines.push(`## ${PLANE_LABELS[plane]}`);
18450
- lines.push("");
18451
- for (const d of planeDivergences) {
18452
- lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
18453
- lines.push("");
18454
- if (d.sig) lines.push(`- Component: \`${d.sig}\``);
18455
- if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
18456
- if (d.table)
18457
- lines.push(
18458
- `- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
18459
- );
18460
- lines.push("");
18461
- lines.push("```diff");
18462
- lines.push(`- before: ${JSON.stringify(d.before)}`);
18463
- lines.push(`+ after: ${JSON.stringify(d.after)}`);
18464
- lines.push("```");
18465
- lines.push("");
18466
- }
18467
- }
18468
- lines.push("---");
18469
- lines.push("");
18470
- lines.push(
18471
- 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.*"
18472
- );
18473
- lines.push("");
18474
- return lines.join("\n");
18475
- }
18476
- function formatComparisonSummary(comparison) {
18477
- const lines = [];
18478
- lines.push(
18479
- `crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
18480
- );
18481
- lines.push(` Schema: ${comparison.schemaVersion}`);
18482
- lines.push(
18483
- ` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
18484
- );
18485
- lines.push(
18486
- ` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
18487
- );
18488
- lines.push(` Divergences: ${comparison.divergences.length}`);
18489
- for (const d of comparison.divergences.slice(0, 5)) {
18490
- lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
18491
- }
18492
- if (comparison.divergences.length > 5) {
18493
- lines.push(
18494
- ` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
18495
- );
18496
- }
18497
- const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
18498
- lines.push(
18499
- ` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
18500
- );
18501
- return lines.join("\n");
18502
- }
18503
- function escapeCell(text2) {
18504
- return text2.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
18505
- }
18506
- function formatSessionRef(ref) {
18507
- const tags = [
18508
- ref.release ? `release ${ref.release}` : void 0,
18509
- ref.build ? `build ${ref.build}` : void 0
18510
- ].filter(Boolean);
18511
- return tags.length > 0 ? `${ref.sessionId} (${tags.join(", ")})` : ref.sessionId;
18512
- }
18513
-
18514
18694
  // src/version.ts
18515
18695
  import fs15 from "fs";
18516
18696
  import path16 from "path";
@@ -18895,6 +19075,10 @@ export {
18895
19075
  SESSION_COMPARE_SCHEMA_VERSION,
18896
19076
  CompareError,
18897
19077
  compareSessions,
19078
+ renderCompareReport,
19079
+ formatComparisonSummary,
19080
+ sessionRefLabel,
19081
+ comparisonTitle,
18898
19082
  REGRESSION_CONTEXT_SCHEMA_VERSION,
18899
19083
  buildRegressionContext,
18900
19084
  McpServer,
@@ -18914,7 +19098,5 @@ export {
18914
19098
  InspectError,
18915
19099
  inspectSession,
18916
19100
  formatInspection,
18917
- renderCompareReport,
18918
- formatComparisonSummary,
18919
19101
  readPackageVersion
18920
19102
  };