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.
package/dist/cli.cjs CHANGED
@@ -214173,21 +214173,139 @@ function inferIntent(evidence, commits) {
214173
214173
  var FUSION_SCHEMA_VERSION = "fusion.v1";
214174
214174
  function assembleBundle(input) {
214175
214175
  const evidence = rankEvidence(input.symptom, input.evidence);
214176
- const hypotheses = classifyHypotheses(
214176
+ const classified = classifyHypotheses(
214177
214177
  input.symptom,
214178
214178
  input.evidence,
214179
214179
  input.intent
214180
214180
  );
214181
214181
  const gaps = input.gaps ?? [];
214182
+ const located = input.located;
214183
+ const hypotheses = classified.map((hypothesis) => {
214184
+ const verification = deriveVerification(hypothesis, evidence);
214185
+ return verification.length > 0 ? { ...hypothesis, verification } : hypothesis;
214186
+ });
214187
+ const contextCompleteness = deriveContextCompleteness(
214188
+ evidence,
214189
+ gaps,
214190
+ hypotheses,
214191
+ located
214192
+ );
214193
+ const escalation = deriveEscalation(contextCompleteness, hypotheses);
214182
214194
  return {
214183
214195
  schemaVersion: FUSION_SCHEMA_VERSION,
214184
214196
  symptom: input.symptom,
214185
214197
  evidence,
214186
214198
  opinion: { stance: "advisory", hypotheses },
214187
214199
  gaps,
214188
- directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
214200
+ directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps),
214201
+ contextCompleteness,
214202
+ escalation,
214203
+ ...located ? { located } : {}
214189
214204
  };
214190
214205
  }
214206
+ function clamp01(value) {
214207
+ if (Number.isNaN(value)) return 0;
214208
+ return Math.max(0, Math.min(1, value));
214209
+ }
214210
+ var COMPLETENESS_LANES = [
214211
+ "network",
214212
+ "db",
214213
+ "flow",
214214
+ "browser",
214215
+ "env"
214216
+ ];
214217
+ function deriveContextCompleteness(evidence, gaps, hypotheses, located) {
214218
+ const lanesPresent = new Set(evidence.map((item) => item.lane));
214219
+ const informativePresent = COMPLETENESS_LANES.filter(
214220
+ (lane) => lanesPresent.has(lane)
214221
+ );
214222
+ const breadth = Math.min(1, informativePresent.length / 3);
214223
+ const volume = Math.min(1, evidence.length / 5);
214224
+ const top = hypotheses[0];
214225
+ const hypothesisStrength = !top || top.kind === "inconclusive" ? 0 : top.confidence;
214226
+ let score = 0.4 * breadth + 0.25 * volume + 0.35 * hypothesisStrength;
214227
+ if (located) {
214228
+ score = located.outcome === "matched" ? 0.85 * score + 0.15 * clamp01(located.confidence) : score * 0.6;
214229
+ }
214230
+ const hardGaps = gaps.filter((gap) => gap.kind === "source-unavailable").length;
214231
+ const softGaps = gaps.length - hardGaps;
214232
+ const gapPenalty = Math.min(0.5, softGaps * 0.1 + hardGaps * 0.3);
214233
+ score = clamp01(score - gapPenalty);
214234
+ const level = score < 0.34 ? "low" : score < 0.67 ? "medium" : "high";
214235
+ const reasons = [];
214236
+ const missingLanes = COMPLETENESS_LANES.filter(
214237
+ (lane) => !lanesPresent.has(lane)
214238
+ );
214239
+ if (informativePresent.length === 0) {
214240
+ reasons.push("no network/db/flow/browser/env evidence captured");
214241
+ } else if (missingLanes.length > 0) {
214242
+ reasons.push(`missing evidence lanes: ${missingLanes.join(", ")}`);
214243
+ }
214244
+ if (evidence.length > 0 && evidence.length < 3) {
214245
+ reasons.push(`thin evidence (${evidence.length} item(s))`);
214246
+ }
214247
+ if (hardGaps > 0) reasons.push(`source unavailable for ${hardGaps} lane(s)`);
214248
+ if (softGaps > 0) reasons.push(`${softGaps} evidence gap(s)`);
214249
+ if (located?.outcome === "inconclusive") {
214250
+ reasons.push("incident location inconclusive");
214251
+ }
214252
+ if (!top || top.kind === "inconclusive") {
214253
+ reasons.push("no distinguishing hypothesis");
214254
+ }
214255
+ return { score, level, reasons };
214256
+ }
214257
+ function pkString(pk) {
214258
+ return Object.keys(pk).sort().map((key) => `${key}=${String(pk[key])}`).join(", ");
214259
+ }
214260
+ function deriveVerification(hypothesis, evidence) {
214261
+ if (hypothesis.kind === "inconclusive") return [];
214262
+ const cited = new Set(hypothesis.evidenceIds);
214263
+ const out = [];
214264
+ for (const item of evidence) {
214265
+ if (!cited.has(item.id)) continue;
214266
+ const ref = item.ref;
214267
+ if (item.lane === "db" && ref.table && ref.pk && Object.keys(ref.pk).length > 0) {
214268
+ out.push({
214269
+ observation: `row in ${ref.table} (${pkString(ref.pk)}) matches the intended post-fix state`,
214270
+ evidenceIds: [item.id],
214271
+ how: "db"
214272
+ });
214273
+ } else if (ref.requestId) {
214274
+ const sigPart = ref.sig ? ` for signature ${ref.sig}` : "";
214275
+ out.push({
214276
+ observation: `request ${ref.requestId} succeeds${sigPart} on a fresh run (no error response)`,
214277
+ evidenceIds: [item.id],
214278
+ how: "request"
214279
+ });
214280
+ } else if (ref.sig) {
214281
+ out.push({
214282
+ observation: `error signature ${ref.sig} no longer appears in a fresh session over the same route`,
214283
+ evidenceIds: [item.id],
214284
+ how: "session"
214285
+ });
214286
+ }
214287
+ }
214288
+ return out;
214289
+ }
214290
+ function deriveEscalation(completeness, hypotheses) {
214291
+ const allInconclusive = hypotheses.length > 0 && hypotheses.every((hypothesis) => hypothesis.kind === "inconclusive");
214292
+ const recommended = completeness.level === "low" || allInconclusive;
214293
+ if (!recommended) return { recommended: false, when: [] };
214294
+ const when = [
214295
+ "if you cannot reproduce the symptom via the anchored request or session, stop and request human triage \u2014 do not widen the search"
214296
+ ];
214297
+ if (completeness.level === "low") {
214298
+ when.push(
214299
+ "context is thin: confirm the missing evidence lanes before acting on the top hypothesis"
214300
+ );
214301
+ }
214302
+ if (allInconclusive) {
214303
+ when.push(
214304
+ "no hypothesis is distinguished; treat the listed causes as equally unproven"
214305
+ );
214306
+ }
214307
+ return { recommended, when };
214308
+ }
214191
214309
  var LANE_PRIOR = {
214192
214310
  db: 0.2,
214193
214311
  network: 0.2,
@@ -215956,6 +216074,7 @@ var SentryError = class extends Error {
215956
216074
  this.status = status;
215957
216075
  this.name = "SentryError";
215958
216076
  }
216077
+ status;
215959
216078
  };
215960
216079
  function sanitizeUrl2(u) {
215961
216080
  try {
@@ -216369,6 +216488,8 @@ var CloudWatchError = class extends Error {
216369
216488
  this.target = target;
216370
216489
  this.name = "CloudWatchError";
216371
216490
  }
216491
+ status;
216492
+ target;
216372
216493
  };
216373
216494
  function isTransient2(error) {
216374
216495
  if (error instanceof CloudWatchError) {
@@ -216781,6 +216902,7 @@ var SplunkError = class extends Error {
216781
216902
  this.status = status;
216782
216903
  this.name = "SplunkError";
216783
216904
  }
216905
+ status;
216784
216906
  };
216785
216907
  function sanitizeUrl3(u) {
216786
216908
  try {
@@ -217131,6 +217253,7 @@ var DatadogError = class extends Error {
217131
217253
  this.status = status;
217132
217254
  this.name = "DatadogError";
217133
217255
  }
217256
+ status;
217134
217257
  };
217135
217258
  function sanitizeUrl4(u) {
217136
217259
  try {
@@ -217494,6 +217617,7 @@ var PostHogError = class extends Error {
217494
217617
  this.status = status;
217495
217618
  this.name = "PostHogError";
217496
217619
  }
217620
+ status;
217497
217621
  };
217498
217622
  function sanitizeUrl5(u) {
217499
217623
  try {
@@ -217884,6 +218008,8 @@ var CloudflareError = class extends Error {
217884
218008
  this.op = op;
217885
218009
  this.name = "CloudflareError";
217886
218010
  }
218011
+ status;
218012
+ op;
217887
218013
  };
217888
218014
  function isTransient6(error) {
217889
218015
  if (error instanceof CloudflareError) {
@@ -218686,7 +218812,7 @@ function timeProximity(now, candidateTime) {
218686
218812
  const decay = Math.pow(0.5, ageMs / TIME_PROXIMITY_HALF_LIFE_MS);
218687
218813
  return { boost: TIME_PROXIMITY_WEIGHT * decay, recent: decay >= 0.5 };
218688
218814
  }
218689
- function clamp01(value) {
218815
+ function clamp012(value) {
218690
218816
  return Math.min(1, Math.max(0, value));
218691
218817
  }
218692
218818
  function locateIncident(symptom, store, opts = {}) {
@@ -218715,7 +218841,7 @@ function locateIncident(symptom, store, opts = {}) {
218715
218841
  candidates.push({
218716
218842
  sessionId: id,
218717
218843
  bugId: bug.bugId,
218718
- confidence: clamp01(base.score + time.boost + releaseBoost),
218844
+ confidence: clamp012(base.score + time.boost + releaseBoost),
218719
218845
  reasons,
218720
218846
  bug
218721
218847
  });
@@ -218861,7 +218987,20 @@ async function locateAndAssemble(symptom, store, opts = {}) {
218861
218987
  const adapter = await gatherAdapterEvidence(symptom, located, opts);
218862
218988
  const evidence = [...located.evidence, ...adapter.items];
218863
218989
  const gaps = located.evidence.length === 0 ? [NO_LOCATED_SESSION_GAP, ...adapter.gaps] : [...adapter.gaps];
218864
- const bundle = assembleBundle({ symptom, evidence, intent: [], gaps });
218990
+ const located_ = {
218991
+ outcome: located.match.outcome,
218992
+ confidence: located.match.confidence,
218993
+ method: "fuzzy",
218994
+ ...located.match.sessionId ? { sessionId: located.match.sessionId } : {},
218995
+ reasons: located.match.reasons
218996
+ };
218997
+ const bundle = assembleBundle({
218998
+ symptom,
218999
+ evidence,
219000
+ intent: [],
219001
+ gaps,
219002
+ located: located_
219003
+ });
218865
219004
  return { bundle, match: located.match, sources: adapter.sources };
218866
219005
  }
218867
219006
 
@@ -223902,6 +224041,8 @@ var AudioProcessingError = class extends Error {
223902
224041
  this.cause = cause;
223903
224042
  this.name = "AudioProcessingError";
223904
224043
  }
224044
+ phase;
224045
+ cause;
223905
224046
  };
223906
224047
  var AudioTranscriptionUnavailableError = class extends Error {
223907
224048
  constructor() {
@@ -225652,12 +225793,12 @@ var import_node_fs10 = __toESM(require("fs"), 1);
225652
225793
  // package.json
225653
225794
  var package_default = {
225654
225795
  name: "crumbtrail-node",
225655
- version: "0.1.0",
225796
+ version: "0.2.1",
225656
225797
  description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
225657
225798
  license: "MIT",
225658
225799
  repository: {
225659
225800
  type: "git",
225660
- url: "https://github.com/oshabana/crumbtrail.git",
225801
+ url: "git+https://github.com/crumbtrail-dev/crumbtrail-js.git",
225661
225802
  directory: "packages/node"
225662
225803
  },
225663
225804
  keywords: [
@@ -225710,6 +225851,10 @@ var package_default = {
225710
225851
  "@types/node": "^25.5.0",
225711
225852
  express: "^5.2.1",
225712
225853
  typescript: "^5.7.0"
225854
+ },
225855
+ homepage: "https://github.com/crumbtrail-dev/crumbtrail-js/tree/main/packages/node#readme",
225856
+ bugs: {
225857
+ url: "https://github.com/crumbtrail-dev/crumbtrail-js/issues"
225713
225858
  }
225714
225859
  };
225715
225860
 
@@ -226360,6 +226505,10 @@ var RequestValidationError = class extends Error {
226360
226505
  this.retryable = retryable;
226361
226506
  this.name = "RequestValidationError";
226362
226507
  }
226508
+ status;
226509
+ publicMessage;
226510
+ code;
226511
+ retryable;
226363
226512
  };
226364
226513
  function serveStatic(res, staticDir, urlPath) {
226365
226514
  const resolved = resolveStaticPath(staticDir, urlPath);
@@ -227089,6 +227238,7 @@ var BoundedTraceSessionCache = class {
227089
227238
  constructor(maxEntries) {
227090
227239
  this.maxEntries = maxEntries;
227091
227240
  }
227241
+ maxEntries;
227092
227242
  entries = /* @__PURE__ */ new Map();
227093
227243
  get(traceId) {
227094
227244
  const existing = this.entries.get(traceId);
@@ -227787,6 +227937,20 @@ function createServer(config) {
227787
227937
  });
227788
227938
  server2.on("close", () => sweeper.stop());
227789
227939
  }
227940
+ if (config.ai?.enabled && config.ai.backfillOnStart) {
227941
+ const sessionDirs = sessions.listSummaries().map((summary) => sessions.getExistingSessionDir(summary.id)).filter((dir) => dir !== void 0);
227942
+ const timer = setTimeout(() => {
227943
+ void backfillAiDiagnoses(
227944
+ sessionDirs,
227945
+ config.ai
227946
+ ).then(
227947
+ (result) => config.ai?.log?.(
227948
+ `Crumbtrail AI diagnosis backfill complete: ${JSON.stringify(result)}`
227949
+ )
227950
+ );
227951
+ }, 0);
227952
+ server2.on("close", () => clearTimeout(timer));
227953
+ }
227790
227954
  if (fastFinalizer) {
227791
227955
  server2.on("close", () => fastFinalizer.stop());
227792
227956
  }
@@ -227836,6 +228000,7 @@ var FixContextError = class extends Error {
227836
228000
  this.code = code;
227837
228001
  this.name = "FixContextError";
227838
228002
  }
228003
+ code;
227839
228004
  };
227840
228005
  function buildFixContext(sessionDirOrId, opts = {}) {
227841
228006
  const sessionDir = resolveSessionDir(sessionDirOrId, opts);
@@ -228895,16 +229060,85 @@ function diffEnvironment(a, b, rules, disabled) {
228895
229060
  addSuppressedRules(rules, beforeRules, afterRules, disabled);
228896
229061
  return [];
228897
229062
  }
229063
+ const envDelta = buildEnvDiff(a, b, disabled);
228898
229064
  return [
228899
229065
  {
228900
229066
  plane: "env",
228901
229067
  kind: "env.snapshot",
228902
229068
  before: a,
228903
229069
  after: b,
228904
- brief: "feature flag, config, release, or build snapshot changed between sessions"
229070
+ brief: envBrief(envDelta),
229071
+ envDelta
228905
229072
  }
228906
229073
  ];
228907
229074
  }
229075
+ function buildEnvDiff(a, b, disabled) {
229076
+ const delta = {
229077
+ flags: channelDelta(recordField(a.flags), recordField(b.flags), disabled),
229078
+ config: channelDelta(
229079
+ recordField(a.config),
229080
+ recordField(b.config),
229081
+ disabled
229082
+ )
229083
+ };
229084
+ const releaseBefore = stringOrUndefined(a.release);
229085
+ const releaseAfter = stringOrUndefined(b.release);
229086
+ if (releaseBefore !== releaseAfter) {
229087
+ delta.release = {
229088
+ ...releaseBefore !== void 0 ? { before: releaseBefore } : {},
229089
+ ...releaseAfter !== void 0 ? { after: releaseAfter } : {}
229090
+ };
229091
+ }
229092
+ const buildBefore = stringOrUndefined(a.build);
229093
+ const buildAfter = stringOrUndefined(b.build);
229094
+ if (buildBefore !== buildAfter) {
229095
+ delta.build = {
229096
+ ...buildBefore !== void 0 ? { before: buildBefore } : {},
229097
+ ...buildAfter !== void 0 ? { after: buildAfter } : {}
229098
+ };
229099
+ }
229100
+ return delta;
229101
+ }
229102
+ function recordField(value) {
229103
+ return isRecord14(value) ? value : {};
229104
+ }
229105
+ function channelDelta(a, b, disabled) {
229106
+ const added = [];
229107
+ const removed = [];
229108
+ const changed = [];
229109
+ for (const key of Object.keys(a).sort()) {
229110
+ if (!(key in b)) {
229111
+ removed.push({ key, before: a[key] });
229112
+ } else if (envValuesDiffer(a[key], b[key], disabled)) {
229113
+ changed.push({ key, before: a[key], after: b[key] });
229114
+ }
229115
+ }
229116
+ for (const key of Object.keys(b).sort()) {
229117
+ if (!(key in a)) added.push({ key, after: b[key] });
229118
+ }
229119
+ return { added, removed, changed };
229120
+ }
229121
+ function envValuesDiffer(before, after, disabled) {
229122
+ const beforeRules = /* @__PURE__ */ new Set();
229123
+ const afterRules = /* @__PURE__ */ new Set();
229124
+ if (canonicalize(before, beforeRules) !== canonicalize(after, afterRules))
229125
+ return true;
229126
+ const rawDiffers = stableStringify(before) !== stableStringify(after);
229127
+ return rawDiffers && hasDisabledRule(beforeRules, afterRules, disabled);
229128
+ }
229129
+ function envBrief(delta) {
229130
+ 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);
229131
+ const parts = [];
229132
+ const flagCount = delta.flags.added.length + delta.flags.removed.length + delta.flags.changed.length;
229133
+ const configCount = delta.config.added.length + delta.config.removed.length + delta.config.changed.length;
229134
+ if (flagCount > 0) parts.push(`${flagCount} flag(s)`);
229135
+ if (configCount > 0) parts.push(`${configCount} config value(s)`);
229136
+ if (delta.release) parts.push("release");
229137
+ if (delta.build) parts.push("build");
229138
+ if (counts === 0 || parts.length === 0)
229139
+ return "feature flag, config, release, or build snapshot changed between sessions";
229140
+ return `environment delta between sessions: ${parts.join(", ")} changed`;
229141
+ }
228908
229142
  function confidenceFor(divergences) {
228909
229143
  if (divergences.length === 0) return "high";
228910
229144
  if (divergences.some(
@@ -228931,6 +229165,9 @@ async function compareSessions(aDir, bDir, options = {}) {
228931
229165
  ...diffDb(a.dbWrites, b.dbWrites, rules, disabled),
228932
229166
  ...diffEnvironment(a.environment, b.environment, rules, disabled)
228933
229167
  ];
229168
+ const envDelta = divergences.find(
229169
+ (divergence) => divergence.kind === "env.snapshot"
229170
+ )?.envDelta;
228934
229171
  return {
228935
229172
  schemaVersion: SESSION_COMPARE_SCHEMA_VERSION,
228936
229173
  verdict: divergences.length > 0 ? "regression" : "clean",
@@ -228953,20 +229190,187 @@ async function compareSessions(aDir, bDir, options = {}) {
228953
229190
  divergences,
228954
229191
  noise: { suppressedCount: rules.size, rules: [...rules].sort() },
228955
229192
  evidence: divergencesToEvidence(divergences),
228956
- intent: []
229193
+ intent: [],
229194
+ ...envDelta ? { envDelta } : {}
228957
229195
  };
228958
229196
  }
228959
229197
 
228960
229198
  // src/compare/regression-context.ts
228961
229199
  var import_node_fs13 = __toESM(require("fs"), 1);
228962
229200
  var import_node_path13 = __toESM(require("path"), 1);
229201
+
229202
+ // src/compare/report.ts
229203
+ var PLANE_ORDER = ["flow", "network", "db", "env"];
229204
+ var PLANE_LABELS = {
229205
+ flow: "Flow steps",
229206
+ network: "Network calls",
229207
+ db: "Database rows",
229208
+ env: "Environment and flags"
229209
+ };
229210
+ function renderCompareReport(comparison) {
229211
+ const lines = [];
229212
+ const regressed = comparison.verdict === "regression";
229213
+ lines.push(
229214
+ `# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
229215
+ );
229216
+ lines.push("");
229217
+ lines.push(
229218
+ regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
229219
+ );
229220
+ lines.push("");
229221
+ lines.push(
229222
+ "A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
229223
+ );
229224
+ lines.push("");
229225
+ lines.push("## Aligned flow");
229226
+ lines.push("");
229227
+ const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
229228
+ lines.push(
229229
+ `${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
229230
+ );
229231
+ lines.push("");
229232
+ if (comparison.divergences.length > 0) {
229233
+ lines.push("| Plane | Kind | Component | Divergence |");
229234
+ lines.push("|---|---|---|---|");
229235
+ for (const d of comparison.divergences) {
229236
+ const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
229237
+ lines.push(
229238
+ `| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
229239
+ );
229240
+ }
229241
+ lines.push("");
229242
+ }
229243
+ for (const plane of PLANE_ORDER) {
229244
+ const planeDivergences = comparison.divergences.filter(
229245
+ (d) => d.plane === plane
229246
+ );
229247
+ if (planeDivergences.length === 0) continue;
229248
+ lines.push(`## ${PLANE_LABELS[plane]}`);
229249
+ lines.push("");
229250
+ for (const d of planeDivergences) {
229251
+ lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
229252
+ lines.push("");
229253
+ if (d.sig) lines.push(`- Component: \`${d.sig}\``);
229254
+ if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
229255
+ if (d.table)
229256
+ lines.push(
229257
+ `- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
229258
+ );
229259
+ lines.push("");
229260
+ if (d.envDelta) {
229261
+ lines.push(...renderEnvDelta(d.envDelta));
229262
+ } else {
229263
+ lines.push("```diff");
229264
+ lines.push(`- before: ${JSON.stringify(d.before)}`);
229265
+ lines.push(`+ after: ${JSON.stringify(d.after)}`);
229266
+ lines.push("```");
229267
+ }
229268
+ lines.push("");
229269
+ }
229270
+ }
229271
+ lines.push("---");
229272
+ lines.push("");
229273
+ lines.push(
229274
+ 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.*"
229275
+ );
229276
+ lines.push("");
229277
+ return lines.join("\n");
229278
+ }
229279
+ function formatComparisonSummary(comparison) {
229280
+ const lines = [];
229281
+ lines.push(
229282
+ `crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
229283
+ );
229284
+ lines.push(` Schema: ${comparison.schemaVersion}`);
229285
+ lines.push(
229286
+ ` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
229287
+ );
229288
+ lines.push(
229289
+ ` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
229290
+ );
229291
+ lines.push(` Divergences: ${comparison.divergences.length}`);
229292
+ for (const d of comparison.divergences.slice(0, 5)) {
229293
+ lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
229294
+ }
229295
+ if (comparison.divergences.length > 5) {
229296
+ lines.push(
229297
+ ` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
229298
+ );
229299
+ }
229300
+ const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
229301
+ lines.push(
229302
+ ` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
229303
+ );
229304
+ return lines.join("\n");
229305
+ }
229306
+ function escapeCell(text2) {
229307
+ return text2.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
229308
+ }
229309
+ function sessionRefLabel(ref) {
229310
+ return ref.release ?? ref.sessionId;
229311
+ }
229312
+ function comparisonTitle(comparison) {
229313
+ return `${sessionRefLabel(comparison.a)} vs ${sessionRefLabel(comparison.b)}`;
229314
+ }
229315
+ function renderEnvDelta(delta) {
229316
+ const lines = ["```diff"];
229317
+ renderEnvChannel(lines, "flags", delta.flags);
229318
+ renderEnvChannel(lines, "config", delta.config);
229319
+ if (delta.release) {
229320
+ lines.push(
229321
+ `~ release: ${JSON.stringify(delta.release.before ?? null)} -> ${JSON.stringify(delta.release.after ?? null)}`
229322
+ );
229323
+ }
229324
+ if (delta.build) {
229325
+ lines.push(
229326
+ `~ build: ${JSON.stringify(delta.build.before ?? null)} -> ${JSON.stringify(delta.build.after ?? null)}`
229327
+ );
229328
+ }
229329
+ if (lines.length === 1) lines.push(" (no field-level changes)");
229330
+ lines.push("```");
229331
+ return lines;
229332
+ }
229333
+ function renderEnvChannel(lines, channel, delta) {
229334
+ for (const change of delta.added) {
229335
+ lines.push(
229336
+ `+ ${channel}.${change.key}: ${JSON.stringify(change.after ?? null)}`
229337
+ );
229338
+ }
229339
+ for (const change of delta.removed) {
229340
+ lines.push(
229341
+ `- ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)}`
229342
+ );
229343
+ }
229344
+ for (const change of delta.changed) {
229345
+ lines.push(
229346
+ `~ ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)} -> ${JSON.stringify(change.after ?? null)}`
229347
+ );
229348
+ }
229349
+ }
229350
+ function formatSessionRef(ref) {
229351
+ if (ref.release) {
229352
+ const detail = [
229353
+ `session ${ref.sessionId}`,
229354
+ ref.build ? `build ${ref.build}` : void 0
229355
+ ].filter(Boolean);
229356
+ return `${ref.release} (${detail.join(", ")})`;
229357
+ }
229358
+ return ref.build ? `${ref.sessionId} (build ${ref.build})` : ref.sessionId;
229359
+ }
229360
+
229361
+ // src/compare/regression-context.ts
228963
229362
  var REGRESSION_CONTEXT_SCHEMA_VERSION = "regression-context.v1";
228964
229363
  function buildRegressionContext(comparison, bDir) {
228965
- const requestIds = unique(comparison.divergences.map((d) => d.requestId).filter(isString));
228966
- const times = requestIds.flatMap((requestId) => requestTimesFromIndex(bDir, requestId));
229364
+ const requestIds = unique(
229365
+ comparison.divergences.map((d) => d.requestId).filter(isString)
229366
+ );
229367
+ const times = requestIds.flatMap(
229368
+ (requestId) => requestTimesFromIndex(bDir, requestId)
229369
+ );
228967
229370
  const firstSig = comparison.divergences.map((d) => d.sig).find(isString);
228968
229371
  return {
228969
229372
  schemaVersion: REGRESSION_CONTEXT_SCHEMA_VERSION,
229373
+ title: comparisonTitle(comparison),
228970
229374
  comparison,
228971
229375
  divergent_interaction: firstSig ? interactionForSig(bDir, firstSig) : null,
228972
229376
  causal_window: requestIds.length > 0 ? {
@@ -228984,6 +229388,7 @@ function buildRegressionContext(comparison, bDir) {
228984
229388
  before: d.before,
228985
229389
  after: d.after
228986
229390
  })),
229391
+ env_delta: comparison.envDelta ?? null,
228987
229392
  repro_hint: reproHint(bDir)
228988
229393
  };
228989
229394
  }
@@ -229028,7 +229433,8 @@ function collectRefTimes(value, times) {
229028
229433
  const start = isRecord15(value.start) ? value.start : void 0;
229029
229434
  const end = isRecord15(value.end) ? value.end : void 0;
229030
229435
  for (const candidate of [ref, start, end]) {
229031
- if (typeof candidate?.t === "number" && Number.isFinite(candidate.t)) times.push(candidate.t);
229436
+ if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
229437
+ times.push(candidate.t);
229032
229438
  }
229033
229439
  }
229034
229440
  function reproHint(sessionDir) {
@@ -229953,12 +230359,29 @@ var McpServer = class {
229953
230359
  "sha"
229954
230360
  ]))
229955
230361
  continue;
229956
- sessions.push(meta);
230362
+ sessions.push(this.withReleaseBuild(meta));
229957
230363
  } catch {
229958
230364
  }
229959
230365
  }
229960
230366
  return textResult(sessions);
229961
230367
  }
230368
+ /**
230369
+ * Surfaces release/build as first-class list-row fields regardless of which
230370
+ * alias the app used (release/releaseId/version, build/buildId/commit/sha), so
230371
+ * an agent can label and group sessions by release without re-reading each
230372
+ * meta. Additive: the raw meta keys are preserved.
230373
+ */
230374
+ withReleaseBuild(meta) {
230375
+ const release = stringField5(meta.release ?? meta.releaseId ?? meta.version);
230376
+ const build = stringField5(
230377
+ meta.build ?? meta.buildId ?? meta.commit ?? meta.sha
230378
+ );
230379
+ return {
230380
+ ...meta,
230381
+ ...release !== void 0 ? { release } : {},
230382
+ ...build !== void 0 ? { build } : {}
230383
+ };
230384
+ }
229962
230385
  sessionMetadataMatches(meta, expected, keys) {
229963
230386
  return keys.some((key) => meta[key] === expected);
229964
230387
  }
@@ -230362,6 +230785,7 @@ var McpServer = class {
230362
230785
  const currentSession = stringField5(args.currentSession);
230363
230786
  let evidence = [];
230364
230787
  let intent = [];
230788
+ let locatedDecision;
230365
230789
  const adapterGaps = [];
230366
230790
  let sessionlessAdapterBundle = false;
230367
230791
  if (baselineSession && currentSession) {
@@ -230377,6 +230801,13 @@ var McpServer = class {
230377
230801
  try {
230378
230802
  const located = locateEvidence(symptom, this.recallStore());
230379
230803
  evidence = located.evidence;
230804
+ locatedDecision = {
230805
+ outcome: located.match.outcome,
230806
+ confidence: located.match.confidence,
230807
+ method: "fuzzy",
230808
+ ...located.match.sessionId ? { sessionId: located.match.sessionId } : {},
230809
+ reasons: located.match.reasons
230810
+ };
230380
230811
  const adapter = await gatherAdapterEvidence(symptom, located, {
230381
230812
  sources: this.evidenceSources()
230382
230813
  });
@@ -230462,7 +230893,13 @@ var McpServer = class {
230462
230893
  }
230463
230894
  ] : []
230464
230895
  ];
230465
- const bundle = assembleBundle({ symptom, evidence, intent, gaps });
230896
+ const bundle = assembleBundle({
230897
+ symptom,
230898
+ evidence,
230899
+ intent,
230900
+ gaps,
230901
+ located: locatedDecision
230902
+ });
230466
230903
  if (budget.maxTokens === void 0) return textResult(bundle);
230467
230904
  return this.budgetedTextResult(
230468
230905
  bundle,
@@ -231185,6 +231622,7 @@ var CliConfigError = class extends Error {
231185
231622
  this.code = code;
231186
231623
  this.name = "CliConfigError";
231187
231624
  }
231625
+ code;
231188
231626
  };
231189
231627
  function defaultCliConfig() {
231190
231628
  return {
@@ -232745,6 +233183,7 @@ var InspectError = class extends Error {
232745
233183
  this.code = code;
232746
233184
  this.name = "InspectError";
232747
233185
  }
233186
+ code;
232748
233187
  };
232749
233188
  function inspectSession(sessionDirOrId, opts = {}) {
232750
233189
  const sessionDir = resolveSessionDir2(sessionDirOrId, opts);
@@ -232916,119 +233355,6 @@ function resolveTarget2(target) {
232916
233355
  // src/run-compare.ts
232917
233356
  var import_node_fs20 = __toESM(require("fs"), 1);
232918
233357
  var import_node_path24 = __toESM(require("path"), 1);
232919
-
232920
- // src/compare/report.ts
232921
- var PLANE_ORDER = ["flow", "network", "db", "env"];
232922
- var PLANE_LABELS = {
232923
- flow: "Flow steps",
232924
- network: "Network calls",
232925
- db: "Database rows",
232926
- env: "Environment and flags"
232927
- };
232928
- function renderCompareReport(comparison) {
232929
- const lines = [];
232930
- const regressed = comparison.verdict === "regression";
232931
- lines.push(
232932
- `# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
232933
- );
232934
- lines.push("");
232935
- lines.push(
232936
- regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
232937
- );
232938
- lines.push("");
232939
- lines.push(
232940
- "A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
232941
- );
232942
- lines.push("");
232943
- lines.push("## Aligned flow");
232944
- lines.push("");
232945
- const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
232946
- lines.push(
232947
- `${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
232948
- );
232949
- lines.push("");
232950
- if (comparison.divergences.length > 0) {
232951
- lines.push("| Plane | Kind | Component | Divergence |");
232952
- lines.push("|---|---|---|---|");
232953
- for (const d of comparison.divergences) {
232954
- const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
232955
- lines.push(
232956
- `| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
232957
- );
232958
- }
232959
- lines.push("");
232960
- }
232961
- for (const plane of PLANE_ORDER) {
232962
- const planeDivergences = comparison.divergences.filter(
232963
- (d) => d.plane === plane
232964
- );
232965
- if (planeDivergences.length === 0) continue;
232966
- lines.push(`## ${PLANE_LABELS[plane]}`);
232967
- lines.push("");
232968
- for (const d of planeDivergences) {
232969
- lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
232970
- lines.push("");
232971
- if (d.sig) lines.push(`- Component: \`${d.sig}\``);
232972
- if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
232973
- if (d.table)
232974
- lines.push(
232975
- `- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
232976
- );
232977
- lines.push("");
232978
- lines.push("```diff");
232979
- lines.push(`- before: ${JSON.stringify(d.before)}`);
232980
- lines.push(`+ after: ${JSON.stringify(d.after)}`);
232981
- lines.push("```");
232982
- lines.push("");
232983
- }
232984
- }
232985
- lines.push("---");
232986
- lines.push("");
232987
- lines.push(
232988
- 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.*"
232989
- );
232990
- lines.push("");
232991
- return lines.join("\n");
232992
- }
232993
- function formatComparisonSummary(comparison) {
232994
- const lines = [];
232995
- lines.push(
232996
- `crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
232997
- );
232998
- lines.push(` Schema: ${comparison.schemaVersion}`);
232999
- lines.push(
233000
- ` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
233001
- );
233002
- lines.push(
233003
- ` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
233004
- );
233005
- lines.push(` Divergences: ${comparison.divergences.length}`);
233006
- for (const d of comparison.divergences.slice(0, 5)) {
233007
- lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
233008
- }
233009
- if (comparison.divergences.length > 5) {
233010
- lines.push(
233011
- ` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
233012
- );
233013
- }
233014
- const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
233015
- lines.push(
233016
- ` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
233017
- );
233018
- return lines.join("\n");
233019
- }
233020
- function escapeCell(text2) {
233021
- return text2.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
233022
- }
233023
- function formatSessionRef(ref) {
233024
- const tags = [
233025
- ref.release ? `release ${ref.release}` : void 0,
233026
- ref.build ? `build ${ref.build}` : void 0
233027
- ].filter(Boolean);
233028
- return tags.length > 0 ? `${ref.sessionId} (${tags.join(", ")})` : ref.sessionId;
233029
- }
233030
-
233031
- // src/run-compare.ts
233032
233358
  async function runCompare(rest) {
233033
233359
  const options = parseCompareArgs(rest);
233034
233360
  if (!options.a || !options.b) {