crumbtrail-node 0.1.0 → 0.2.1

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.
@@ -1105,21 +1105,139 @@ function inferIntent(evidence, commits) {
1105
1105
  var FUSION_SCHEMA_VERSION = "fusion.v1";
1106
1106
  function assembleBundle(input) {
1107
1107
  const evidence = rankEvidence(input.symptom, input.evidence);
1108
- const hypotheses = classifyHypotheses(
1108
+ const classified = classifyHypotheses(
1109
1109
  input.symptom,
1110
1110
  input.evidence,
1111
1111
  input.intent
1112
1112
  );
1113
1113
  const gaps = input.gaps ?? [];
1114
+ const located = input.located;
1115
+ const hypotheses = classified.map((hypothesis) => {
1116
+ const verification = deriveVerification(hypothesis, evidence);
1117
+ return verification.length > 0 ? { ...hypothesis, verification } : hypothesis;
1118
+ });
1119
+ const contextCompleteness = deriveContextCompleteness(
1120
+ evidence,
1121
+ gaps,
1122
+ hypotheses,
1123
+ located
1124
+ );
1125
+ const escalation = deriveEscalation(contextCompleteness, hypotheses);
1114
1126
  return {
1115
1127
  schemaVersion: FUSION_SCHEMA_VERSION,
1116
1128
  symptom: input.symptom,
1117
1129
  evidence,
1118
1130
  opinion: { stance: "advisory", hypotheses },
1119
1131
  gaps,
1120
- directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
1132
+ directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps),
1133
+ contextCompleteness,
1134
+ escalation,
1135
+ ...located ? { located } : {}
1121
1136
  };
1122
1137
  }
1138
+ function clamp01(value) {
1139
+ if (Number.isNaN(value)) return 0;
1140
+ return Math.max(0, Math.min(1, value));
1141
+ }
1142
+ var COMPLETENESS_LANES = [
1143
+ "network",
1144
+ "db",
1145
+ "flow",
1146
+ "browser",
1147
+ "env"
1148
+ ];
1149
+ function deriveContextCompleteness(evidence, gaps, hypotheses, located) {
1150
+ const lanesPresent = new Set(evidence.map((item) => item.lane));
1151
+ const informativePresent = COMPLETENESS_LANES.filter(
1152
+ (lane) => lanesPresent.has(lane)
1153
+ );
1154
+ const breadth = Math.min(1, informativePresent.length / 3);
1155
+ const volume = Math.min(1, evidence.length / 5);
1156
+ const top = hypotheses[0];
1157
+ const hypothesisStrength = !top || top.kind === "inconclusive" ? 0 : top.confidence;
1158
+ let score = 0.4 * breadth + 0.25 * volume + 0.35 * hypothesisStrength;
1159
+ if (located) {
1160
+ score = located.outcome === "matched" ? 0.85 * score + 0.15 * clamp01(located.confidence) : score * 0.6;
1161
+ }
1162
+ const hardGaps = gaps.filter((gap) => gap.kind === "source-unavailable").length;
1163
+ const softGaps = gaps.length - hardGaps;
1164
+ const gapPenalty = Math.min(0.5, softGaps * 0.1 + hardGaps * 0.3);
1165
+ score = clamp01(score - gapPenalty);
1166
+ const level = score < 0.34 ? "low" : score < 0.67 ? "medium" : "high";
1167
+ const reasons = [];
1168
+ const missingLanes = COMPLETENESS_LANES.filter(
1169
+ (lane) => !lanesPresent.has(lane)
1170
+ );
1171
+ if (informativePresent.length === 0) {
1172
+ reasons.push("no network/db/flow/browser/env evidence captured");
1173
+ } else if (missingLanes.length > 0) {
1174
+ reasons.push(`missing evidence lanes: ${missingLanes.join(", ")}`);
1175
+ }
1176
+ if (evidence.length > 0 && evidence.length < 3) {
1177
+ reasons.push(`thin evidence (${evidence.length} item(s))`);
1178
+ }
1179
+ if (hardGaps > 0) reasons.push(`source unavailable for ${hardGaps} lane(s)`);
1180
+ if (softGaps > 0) reasons.push(`${softGaps} evidence gap(s)`);
1181
+ if (located?.outcome === "inconclusive") {
1182
+ reasons.push("incident location inconclusive");
1183
+ }
1184
+ if (!top || top.kind === "inconclusive") {
1185
+ reasons.push("no distinguishing hypothesis");
1186
+ }
1187
+ return { score, level, reasons };
1188
+ }
1189
+ function pkString(pk) {
1190
+ return Object.keys(pk).sort().map((key) => `${key}=${String(pk[key])}`).join(", ");
1191
+ }
1192
+ function deriveVerification(hypothesis, evidence) {
1193
+ if (hypothesis.kind === "inconclusive") return [];
1194
+ const cited = new Set(hypothesis.evidenceIds);
1195
+ const out = [];
1196
+ for (const item of evidence) {
1197
+ if (!cited.has(item.id)) continue;
1198
+ const ref = item.ref;
1199
+ if (item.lane === "db" && ref.table && ref.pk && Object.keys(ref.pk).length > 0) {
1200
+ out.push({
1201
+ observation: `row in ${ref.table} (${pkString(ref.pk)}) matches the intended post-fix state`,
1202
+ evidenceIds: [item.id],
1203
+ how: "db"
1204
+ });
1205
+ } else if (ref.requestId) {
1206
+ const sigPart = ref.sig ? ` for signature ${ref.sig}` : "";
1207
+ out.push({
1208
+ observation: `request ${ref.requestId} succeeds${sigPart} on a fresh run (no error response)`,
1209
+ evidenceIds: [item.id],
1210
+ how: "request"
1211
+ });
1212
+ } else if (ref.sig) {
1213
+ out.push({
1214
+ observation: `error signature ${ref.sig} no longer appears in a fresh session over the same route`,
1215
+ evidenceIds: [item.id],
1216
+ how: "session"
1217
+ });
1218
+ }
1219
+ }
1220
+ return out;
1221
+ }
1222
+ function deriveEscalation(completeness, hypotheses) {
1223
+ const allInconclusive = hypotheses.length > 0 && hypotheses.every((hypothesis) => hypothesis.kind === "inconclusive");
1224
+ const recommended = completeness.level === "low" || allInconclusive;
1225
+ if (!recommended) return { recommended: false, when: [] };
1226
+ const when = [
1227
+ "if you cannot reproduce the symptom via the anchored request or session, stop and request human triage \u2014 do not widen the search"
1228
+ ];
1229
+ if (completeness.level === "low") {
1230
+ when.push(
1231
+ "context is thin: confirm the missing evidence lanes before acting on the top hypothesis"
1232
+ );
1233
+ }
1234
+ if (allInconclusive) {
1235
+ when.push(
1236
+ "no hypothesis is distinguished; treat the listed causes as equally unproven"
1237
+ );
1238
+ }
1239
+ return { recommended, when };
1240
+ }
1123
1241
  var LANE_PRIOR = {
1124
1242
  db: 0.2,
1125
1243
  network: 0.2,
@@ -2702,6 +2820,7 @@ var SentryError = class extends Error {
2702
2820
  this.status = status;
2703
2821
  this.name = "SentryError";
2704
2822
  }
2823
+ status;
2705
2824
  };
2706
2825
  function sanitizeUrl2(u) {
2707
2826
  try {
@@ -3115,6 +3234,8 @@ var CloudWatchError = class extends Error {
3115
3234
  this.target = target;
3116
3235
  this.name = "CloudWatchError";
3117
3236
  }
3237
+ status;
3238
+ target;
3118
3239
  };
3119
3240
  function isTransient2(error) {
3120
3241
  if (error instanceof CloudWatchError) {
@@ -3527,6 +3648,7 @@ var SplunkError = class extends Error {
3527
3648
  this.status = status;
3528
3649
  this.name = "SplunkError";
3529
3650
  }
3651
+ status;
3530
3652
  };
3531
3653
  function sanitizeUrl3(u) {
3532
3654
  try {
@@ -3877,6 +3999,7 @@ var DatadogError = class extends Error {
3877
3999
  this.status = status;
3878
4000
  this.name = "DatadogError";
3879
4001
  }
4002
+ status;
3880
4003
  };
3881
4004
  function sanitizeUrl4(u) {
3882
4005
  try {
@@ -4240,6 +4363,7 @@ var PostHogError = class extends Error {
4240
4363
  this.status = status;
4241
4364
  this.name = "PostHogError";
4242
4365
  }
4366
+ status;
4243
4367
  };
4244
4368
  function sanitizeUrl5(u) {
4245
4369
  try {
@@ -4630,6 +4754,8 @@ var CloudflareError = class extends Error {
4630
4754
  this.op = op;
4631
4755
  this.name = "CloudflareError";
4632
4756
  }
4757
+ status;
4758
+ op;
4633
4759
  };
4634
4760
  function isTransient6(error) {
4635
4761
  if (error instanceof CloudflareError) {
@@ -10482,6 +10608,8 @@ var AudioProcessingError = class extends Error {
10482
10608
  this.cause = cause;
10483
10609
  this.name = "AudioProcessingError";
10484
10610
  }
10611
+ phase;
10612
+ cause;
10485
10613
  };
10486
10614
  var AudioTranscriptionUnavailableError = class extends Error {
10487
10615
  constructor() {
@@ -12269,7 +12397,7 @@ function timeProximity(now, candidateTime) {
12269
12397
  const decay = Math.pow(0.5, ageMs / TIME_PROXIMITY_HALF_LIFE_MS);
12270
12398
  return { boost: TIME_PROXIMITY_WEIGHT * decay, recent: decay >= 0.5 };
12271
12399
  }
12272
- function clamp01(value) {
12400
+ function clamp012(value) {
12273
12401
  return Math.min(1, Math.max(0, value));
12274
12402
  }
12275
12403
  function locateIncident(symptom, store, opts = {}) {
@@ -12298,7 +12426,7 @@ function locateIncident(symptom, store, opts = {}) {
12298
12426
  candidates.push({
12299
12427
  sessionId: id,
12300
12428
  bugId: bug.bugId,
12301
- confidence: clamp01(base.score + time.boost + releaseBoost),
12429
+ confidence: clamp012(base.score + time.boost + releaseBoost),
12302
12430
  reasons,
12303
12431
  bug
12304
12432
  });
@@ -12444,7 +12572,20 @@ async function locateAndAssemble(symptom, store, opts = {}) {
12444
12572
  const adapter = await gatherAdapterEvidence(symptom, located, opts);
12445
12573
  const evidence = [...located.evidence, ...adapter.items];
12446
12574
  const gaps = located.evidence.length === 0 ? [NO_LOCATED_SESSION_GAP, ...adapter.gaps] : [...adapter.gaps];
12447
- const bundle = assembleBundle({ symptom, evidence, intent: [], gaps });
12575
+ const located_ = {
12576
+ outcome: located.match.outcome,
12577
+ confidence: located.match.confidence,
12578
+ method: "fuzzy",
12579
+ ...located.match.sessionId ? { sessionId: located.match.sessionId } : {},
12580
+ reasons: located.match.reasons
12581
+ };
12582
+ const bundle = assembleBundle({
12583
+ symptom,
12584
+ evidence,
12585
+ intent: [],
12586
+ gaps,
12587
+ located: located_
12588
+ });
12448
12589
  return { bundle, match: located.match, sources: adapter.sources };
12449
12590
  }
12450
12591
 
@@ -12964,12 +13105,12 @@ import fs10 from "fs";
12964
13105
  // package.json
12965
13106
  var package_default = {
12966
13107
  name: "crumbtrail-node",
12967
- version: "0.1.0",
13108
+ version: "0.2.1",
12968
13109
  description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
12969
13110
  license: "MIT",
12970
13111
  repository: {
12971
13112
  type: "git",
12972
- url: "https://github.com/oshabana/crumbtrail.git",
13113
+ url: "git+https://github.com/crumbtrail-dev/crumbtrail-js.git",
12973
13114
  directory: "packages/node"
12974
13115
  },
12975
13116
  keywords: [
@@ -13022,6 +13163,10 @@ var package_default = {
13022
13163
  "@types/node": "^25.5.0",
13023
13164
  express: "^5.2.1",
13024
13165
  typescript: "^5.7.0"
13166
+ },
13167
+ homepage: "https://github.com/crumbtrail-dev/crumbtrail-js/tree/main/packages/node#readme",
13168
+ bugs: {
13169
+ url: "https://github.com/crumbtrail-dev/crumbtrail-js/issues"
13025
13170
  }
13026
13171
  };
13027
13172
 
@@ -13299,6 +13444,10 @@ var RequestValidationError = class extends Error {
13299
13444
  this.retryable = retryable;
13300
13445
  this.name = "RequestValidationError";
13301
13446
  }
13447
+ status;
13448
+ publicMessage;
13449
+ code;
13450
+ retryable;
13302
13451
  };
13303
13452
  function serveStatic(res, staticDir, urlPath) {
13304
13453
  const resolved = resolveStaticPath(staticDir, urlPath);
@@ -14028,6 +14177,7 @@ var BoundedTraceSessionCache = class {
14028
14177
  constructor(maxEntries) {
14029
14178
  this.maxEntries = maxEntries;
14030
14179
  }
14180
+ maxEntries;
14031
14181
  entries = /* @__PURE__ */ new Map();
14032
14182
  get(traceId) {
14033
14183
  const existing = this.entries.get(traceId);
@@ -14726,6 +14876,20 @@ function createServer(config) {
14726
14876
  });
14727
14877
  server.on("close", () => sweeper.stop());
14728
14878
  }
14879
+ if (config.ai?.enabled && config.ai.backfillOnStart) {
14880
+ const sessionDirs = sessions.listSummaries().map((summary) => sessions.getExistingSessionDir(summary.id)).filter((dir) => dir !== void 0);
14881
+ const timer = setTimeout(() => {
14882
+ void backfillAiDiagnoses(
14883
+ sessionDirs,
14884
+ config.ai
14885
+ ).then(
14886
+ (result) => config.ai?.log?.(
14887
+ `Crumbtrail AI diagnosis backfill complete: ${JSON.stringify(result)}`
14888
+ )
14889
+ );
14890
+ }, 0);
14891
+ server.on("close", () => clearTimeout(timer));
14892
+ }
14729
14893
  if (fastFinalizer) {
14730
14894
  server.on("close", () => fastFinalizer.stop());
14731
14895
  }
@@ -14769,6 +14933,7 @@ var FixContextError = class extends Error {
14769
14933
  this.code = code;
14770
14934
  this.name = "FixContextError";
14771
14935
  }
14936
+ code;
14772
14937
  };
14773
14938
  function buildFixContext(sessionDirOrId, opts = {}) {
14774
14939
  const sessionDir = resolveSessionDir(sessionDirOrId, opts);
@@ -15701,16 +15866,85 @@ function diffEnvironment(a, b, rules, disabled) {
15701
15866
  addSuppressedRules(rules, beforeRules, afterRules, disabled);
15702
15867
  return [];
15703
15868
  }
15869
+ const envDelta = buildEnvDiff(a, b, disabled);
15704
15870
  return [
15705
15871
  {
15706
15872
  plane: "env",
15707
15873
  kind: "env.snapshot",
15708
15874
  before: a,
15709
15875
  after: b,
15710
- brief: "feature flag, config, release, or build snapshot changed between sessions"
15876
+ brief: envBrief(envDelta),
15877
+ envDelta
15711
15878
  }
15712
15879
  ];
15713
15880
  }
15881
+ function buildEnvDiff(a, b, disabled) {
15882
+ const delta = {
15883
+ flags: channelDelta(recordField(a.flags), recordField(b.flags), disabled),
15884
+ config: channelDelta(
15885
+ recordField(a.config),
15886
+ recordField(b.config),
15887
+ disabled
15888
+ )
15889
+ };
15890
+ const releaseBefore = stringOrUndefined(a.release);
15891
+ const releaseAfter = stringOrUndefined(b.release);
15892
+ if (releaseBefore !== releaseAfter) {
15893
+ delta.release = {
15894
+ ...releaseBefore !== void 0 ? { before: releaseBefore } : {},
15895
+ ...releaseAfter !== void 0 ? { after: releaseAfter } : {}
15896
+ };
15897
+ }
15898
+ const buildBefore = stringOrUndefined(a.build);
15899
+ const buildAfter = stringOrUndefined(b.build);
15900
+ if (buildBefore !== buildAfter) {
15901
+ delta.build = {
15902
+ ...buildBefore !== void 0 ? { before: buildBefore } : {},
15903
+ ...buildAfter !== void 0 ? { after: buildAfter } : {}
15904
+ };
15905
+ }
15906
+ return delta;
15907
+ }
15908
+ function recordField(value) {
15909
+ return isRecord14(value) ? value : {};
15910
+ }
15911
+ function channelDelta(a, b, disabled) {
15912
+ const added = [];
15913
+ const removed = [];
15914
+ const changed = [];
15915
+ for (const key of Object.keys(a).sort()) {
15916
+ if (!(key in b)) {
15917
+ removed.push({ key, before: a[key] });
15918
+ } else if (envValuesDiffer(a[key], b[key], disabled)) {
15919
+ changed.push({ key, before: a[key], after: b[key] });
15920
+ }
15921
+ }
15922
+ for (const key of Object.keys(b).sort()) {
15923
+ if (!(key in a)) added.push({ key, after: b[key] });
15924
+ }
15925
+ return { added, removed, changed };
15926
+ }
15927
+ function envValuesDiffer(before, after, disabled) {
15928
+ const beforeRules = /* @__PURE__ */ new Set();
15929
+ const afterRules = /* @__PURE__ */ new Set();
15930
+ if (canonicalize(before, beforeRules) !== canonicalize(after, afterRules))
15931
+ return true;
15932
+ const rawDiffers = stableStringify(before) !== stableStringify(after);
15933
+ return rawDiffers && hasDisabledRule(beforeRules, afterRules, disabled);
15934
+ }
15935
+ function envBrief(delta) {
15936
+ 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);
15937
+ const parts = [];
15938
+ const flagCount = delta.flags.added.length + delta.flags.removed.length + delta.flags.changed.length;
15939
+ const configCount = delta.config.added.length + delta.config.removed.length + delta.config.changed.length;
15940
+ if (flagCount > 0) parts.push(`${flagCount} flag(s)`);
15941
+ if (configCount > 0) parts.push(`${configCount} config value(s)`);
15942
+ if (delta.release) parts.push("release");
15943
+ if (delta.build) parts.push("build");
15944
+ if (counts === 0 || parts.length === 0)
15945
+ return "feature flag, config, release, or build snapshot changed between sessions";
15946
+ return `environment delta between sessions: ${parts.join(", ")} changed`;
15947
+ }
15714
15948
  function confidenceFor(divergences) {
15715
15949
  if (divergences.length === 0) return "high";
15716
15950
  if (divergences.some(
@@ -15737,6 +15971,9 @@ async function compareSessions(aDir, bDir, options = {}) {
15737
15971
  ...diffDb(a.dbWrites, b.dbWrites, rules, disabled),
15738
15972
  ...diffEnvironment(a.environment, b.environment, rules, disabled)
15739
15973
  ];
15974
+ const envDelta = divergences.find(
15975
+ (divergence) => divergence.kind === "env.snapshot"
15976
+ )?.envDelta;
15740
15977
  return {
15741
15978
  schemaVersion: SESSION_COMPARE_SCHEMA_VERSION,
15742
15979
  verdict: divergences.length > 0 ? "regression" : "clean",
@@ -15759,20 +15996,185 @@ async function compareSessions(aDir, bDir, options = {}) {
15759
15996
  divergences,
15760
15997
  noise: { suppressedCount: rules.size, rules: [...rules].sort() },
15761
15998
  evidence: divergencesToEvidence(divergences),
15762
- intent: []
15999
+ intent: [],
16000
+ ...envDelta ? { envDelta } : {}
15763
16001
  };
15764
16002
  }
15765
16003
 
16004
+ // src/compare/report.ts
16005
+ var PLANE_ORDER = ["flow", "network", "db", "env"];
16006
+ var PLANE_LABELS = {
16007
+ flow: "Flow steps",
16008
+ network: "Network calls",
16009
+ db: "Database rows",
16010
+ env: "Environment and flags"
16011
+ };
16012
+ function renderCompareReport(comparison) {
16013
+ const lines = [];
16014
+ const regressed = comparison.verdict === "regression";
16015
+ lines.push(
16016
+ `# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
16017
+ );
16018
+ lines.push("");
16019
+ lines.push(
16020
+ regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
16021
+ );
16022
+ lines.push("");
16023
+ lines.push(
16024
+ "A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
16025
+ );
16026
+ lines.push("");
16027
+ lines.push("## Aligned flow");
16028
+ lines.push("");
16029
+ const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
16030
+ lines.push(
16031
+ `${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
16032
+ );
16033
+ lines.push("");
16034
+ if (comparison.divergences.length > 0) {
16035
+ lines.push("| Plane | Kind | Component | Divergence |");
16036
+ lines.push("|---|---|---|---|");
16037
+ for (const d of comparison.divergences) {
16038
+ const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
16039
+ lines.push(
16040
+ `| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
16041
+ );
16042
+ }
16043
+ lines.push("");
16044
+ }
16045
+ for (const plane of PLANE_ORDER) {
16046
+ const planeDivergences = comparison.divergences.filter(
16047
+ (d) => d.plane === plane
16048
+ );
16049
+ if (planeDivergences.length === 0) continue;
16050
+ lines.push(`## ${PLANE_LABELS[plane]}`);
16051
+ lines.push("");
16052
+ for (const d of planeDivergences) {
16053
+ lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
16054
+ lines.push("");
16055
+ if (d.sig) lines.push(`- Component: \`${d.sig}\``);
16056
+ if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
16057
+ if (d.table)
16058
+ lines.push(
16059
+ `- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
16060
+ );
16061
+ lines.push("");
16062
+ if (d.envDelta) {
16063
+ lines.push(...renderEnvDelta(d.envDelta));
16064
+ } else {
16065
+ lines.push("```diff");
16066
+ lines.push(`- before: ${JSON.stringify(d.before)}`);
16067
+ lines.push(`+ after: ${JSON.stringify(d.after)}`);
16068
+ lines.push("```");
16069
+ }
16070
+ lines.push("");
16071
+ }
16072
+ }
16073
+ lines.push("---");
16074
+ lines.push("");
16075
+ lines.push(
16076
+ 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.*"
16077
+ );
16078
+ lines.push("");
16079
+ return lines.join("\n");
16080
+ }
16081
+ function formatComparisonSummary(comparison) {
16082
+ const lines = [];
16083
+ lines.push(
16084
+ `crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
16085
+ );
16086
+ lines.push(` Schema: ${comparison.schemaVersion}`);
16087
+ lines.push(
16088
+ ` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
16089
+ );
16090
+ lines.push(
16091
+ ` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
16092
+ );
16093
+ lines.push(` Divergences: ${comparison.divergences.length}`);
16094
+ for (const d of comparison.divergences.slice(0, 5)) {
16095
+ lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
16096
+ }
16097
+ if (comparison.divergences.length > 5) {
16098
+ lines.push(
16099
+ ` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
16100
+ );
16101
+ }
16102
+ const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
16103
+ lines.push(
16104
+ ` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
16105
+ );
16106
+ return lines.join("\n");
16107
+ }
16108
+ function escapeCell(text2) {
16109
+ return text2.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
16110
+ }
16111
+ function sessionRefLabel(ref) {
16112
+ return ref.release ?? ref.sessionId;
16113
+ }
16114
+ function comparisonTitle(comparison) {
16115
+ return `${sessionRefLabel(comparison.a)} vs ${sessionRefLabel(comparison.b)}`;
16116
+ }
16117
+ function renderEnvDelta(delta) {
16118
+ const lines = ["```diff"];
16119
+ renderEnvChannel(lines, "flags", delta.flags);
16120
+ renderEnvChannel(lines, "config", delta.config);
16121
+ if (delta.release) {
16122
+ lines.push(
16123
+ `~ release: ${JSON.stringify(delta.release.before ?? null)} -> ${JSON.stringify(delta.release.after ?? null)}`
16124
+ );
16125
+ }
16126
+ if (delta.build) {
16127
+ lines.push(
16128
+ `~ build: ${JSON.stringify(delta.build.before ?? null)} -> ${JSON.stringify(delta.build.after ?? null)}`
16129
+ );
16130
+ }
16131
+ if (lines.length === 1) lines.push(" (no field-level changes)");
16132
+ lines.push("```");
16133
+ return lines;
16134
+ }
16135
+ function renderEnvChannel(lines, channel, delta) {
16136
+ for (const change of delta.added) {
16137
+ lines.push(
16138
+ `+ ${channel}.${change.key}: ${JSON.stringify(change.after ?? null)}`
16139
+ );
16140
+ }
16141
+ for (const change of delta.removed) {
16142
+ lines.push(
16143
+ `- ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)}`
16144
+ );
16145
+ }
16146
+ for (const change of delta.changed) {
16147
+ lines.push(
16148
+ `~ ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)} -> ${JSON.stringify(change.after ?? null)}`
16149
+ );
16150
+ }
16151
+ }
16152
+ function formatSessionRef(ref) {
16153
+ if (ref.release) {
16154
+ const detail = [
16155
+ `session ${ref.sessionId}`,
16156
+ ref.build ? `build ${ref.build}` : void 0
16157
+ ].filter(Boolean);
16158
+ return `${ref.release} (${detail.join(", ")})`;
16159
+ }
16160
+ return ref.build ? `${ref.sessionId} (build ${ref.build})` : ref.sessionId;
16161
+ }
16162
+
15766
16163
  // src/compare/regression-context.ts
15767
16164
  import fs13 from "fs";
15768
16165
  import path13 from "path";
15769
16166
  var REGRESSION_CONTEXT_SCHEMA_VERSION = "regression-context.v1";
15770
16167
  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));
16168
+ const requestIds = unique(
16169
+ comparison.divergences.map((d) => d.requestId).filter(isString)
16170
+ );
16171
+ const times = requestIds.flatMap(
16172
+ (requestId) => requestTimesFromIndex(bDir, requestId)
16173
+ );
15773
16174
  const firstSig = comparison.divergences.map((d) => d.sig).find(isString);
15774
16175
  return {
15775
16176
  schemaVersion: REGRESSION_CONTEXT_SCHEMA_VERSION,
16177
+ title: comparisonTitle(comparison),
15776
16178
  comparison,
15777
16179
  divergent_interaction: firstSig ? interactionForSig(bDir, firstSig) : null,
15778
16180
  causal_window: requestIds.length > 0 ? {
@@ -15790,6 +16192,7 @@ function buildRegressionContext(comparison, bDir) {
15790
16192
  before: d.before,
15791
16193
  after: d.after
15792
16194
  })),
16195
+ env_delta: comparison.envDelta ?? null,
15793
16196
  repro_hint: reproHint(bDir)
15794
16197
  };
15795
16198
  }
@@ -15834,7 +16237,8 @@ function collectRefTimes(value, times) {
15834
16237
  const start = isRecord15(value.start) ? value.start : void 0;
15835
16238
  const end = isRecord15(value.end) ? value.end : void 0;
15836
16239
  for (const candidate of [ref, start, end]) {
15837
- if (typeof candidate?.t === "number" && Number.isFinite(candidate.t)) times.push(candidate.t);
16240
+ if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
16241
+ times.push(candidate.t);
15838
16242
  }
15839
16243
  }
15840
16244
  function reproHint(sessionDir) {
@@ -16892,12 +17296,29 @@ var McpServer = class {
16892
17296
  "sha"
16893
17297
  ]))
16894
17298
  continue;
16895
- sessions.push(meta);
17299
+ sessions.push(this.withReleaseBuild(meta));
16896
17300
  } catch {
16897
17301
  }
16898
17302
  }
16899
17303
  return textResult(sessions);
16900
17304
  }
17305
+ /**
17306
+ * Surfaces release/build as first-class list-row fields regardless of which
17307
+ * alias the app used (release/releaseId/version, build/buildId/commit/sha), so
17308
+ * an agent can label and group sessions by release without re-reading each
17309
+ * meta. Additive: the raw meta keys are preserved.
17310
+ */
17311
+ withReleaseBuild(meta) {
17312
+ const release = stringField5(meta.release ?? meta.releaseId ?? meta.version);
17313
+ const build = stringField5(
17314
+ meta.build ?? meta.buildId ?? meta.commit ?? meta.sha
17315
+ );
17316
+ return {
17317
+ ...meta,
17318
+ ...release !== void 0 ? { release } : {},
17319
+ ...build !== void 0 ? { build } : {}
17320
+ };
17321
+ }
16901
17322
  sessionMetadataMatches(meta, expected, keys) {
16902
17323
  return keys.some((key) => meta[key] === expected);
16903
17324
  }
@@ -17301,6 +17722,7 @@ var McpServer = class {
17301
17722
  const currentSession = stringField5(args.currentSession);
17302
17723
  let evidence = [];
17303
17724
  let intent = [];
17725
+ let locatedDecision;
17304
17726
  const adapterGaps = [];
17305
17727
  let sessionlessAdapterBundle = false;
17306
17728
  if (baselineSession && currentSession) {
@@ -17316,6 +17738,13 @@ var McpServer = class {
17316
17738
  try {
17317
17739
  const located = locateEvidence(symptom, this.recallStore());
17318
17740
  evidence = located.evidence;
17741
+ locatedDecision = {
17742
+ outcome: located.match.outcome,
17743
+ confidence: located.match.confidence,
17744
+ method: "fuzzy",
17745
+ ...located.match.sessionId ? { sessionId: located.match.sessionId } : {},
17746
+ reasons: located.match.reasons
17747
+ };
17319
17748
  const adapter = await gatherAdapterEvidence(symptom, located, {
17320
17749
  sources: this.evidenceSources()
17321
17750
  });
@@ -17401,7 +17830,13 @@ var McpServer = class {
17401
17830
  }
17402
17831
  ] : []
17403
17832
  ];
17404
- const bundle = assembleBundle({ symptom, evidence, intent, gaps });
17833
+ const bundle = assembleBundle({
17834
+ symptom,
17835
+ evidence,
17836
+ intent,
17837
+ gaps,
17838
+ located: locatedDecision
17839
+ });
17405
17840
  if (budget.maxTokens === void 0) return textResult(bundle);
17406
17841
  return this.budgetedTextResult(
17407
17842
  bundle,
@@ -18273,6 +18708,7 @@ var InspectError = class extends Error {
18273
18708
  this.code = code;
18274
18709
  this.name = "InspectError";
18275
18710
  }
18711
+ code;
18276
18712
  };
18277
18713
  function inspectSession(sessionDirOrId, opts = {}) {
18278
18714
  const sessionDir = resolveSessionDir2(sessionDirOrId, opts);
@@ -18400,117 +18836,6 @@ function isRecord17(value) {
18400
18836
  return value != null && typeof value === "object" && !Array.isArray(value);
18401
18837
  }
18402
18838
 
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
18839
  // src/version.ts
18515
18840
  import fs15 from "fs";
18516
18841
  import path16 from "path";
@@ -18895,6 +19220,10 @@ export {
18895
19220
  SESSION_COMPARE_SCHEMA_VERSION,
18896
19221
  CompareError,
18897
19222
  compareSessions,
19223
+ renderCompareReport,
19224
+ formatComparisonSummary,
19225
+ sessionRefLabel,
19226
+ comparisonTitle,
18898
19227
  REGRESSION_CONTEXT_SCHEMA_VERSION,
18899
19228
  buildRegressionContext,
18900
19229
  McpServer,
@@ -18914,7 +19243,5 @@ export {
18914
19243
  InspectError,
18915
19244
  inspectSession,
18916
19245
  formatInspection,
18917
- renderCompareReport,
18918
- formatComparisonSummary,
18919
19246
  readPackageVersion
18920
19247
  };