crumbtrail-node 0.2.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,
@@ -12279,7 +12397,7 @@ function timeProximity(now, candidateTime) {
12279
12397
  const decay = Math.pow(0.5, ageMs / TIME_PROXIMITY_HALF_LIFE_MS);
12280
12398
  return { boost: TIME_PROXIMITY_WEIGHT * decay, recent: decay >= 0.5 };
12281
12399
  }
12282
- function clamp01(value) {
12400
+ function clamp012(value) {
12283
12401
  return Math.min(1, Math.max(0, value));
12284
12402
  }
12285
12403
  function locateIncident(symptom, store, opts = {}) {
@@ -12308,7 +12426,7 @@ function locateIncident(symptom, store, opts = {}) {
12308
12426
  candidates.push({
12309
12427
  sessionId: id,
12310
12428
  bugId: bug.bugId,
12311
- confidence: clamp01(base.score + time.boost + releaseBoost),
12429
+ confidence: clamp012(base.score + time.boost + releaseBoost),
12312
12430
  reasons,
12313
12431
  bug
12314
12432
  });
@@ -12454,7 +12572,20 @@ async function locateAndAssemble(symptom, store, opts = {}) {
12454
12572
  const adapter = await gatherAdapterEvidence(symptom, located, opts);
12455
12573
  const evidence = [...located.evidence, ...adapter.items];
12456
12574
  const gaps = located.evidence.length === 0 ? [NO_LOCATED_SESSION_GAP, ...adapter.gaps] : [...adapter.gaps];
12457
- 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
+ });
12458
12589
  return { bundle, match: located.match, sources: adapter.sources };
12459
12590
  }
12460
12591
 
@@ -12974,7 +13105,7 @@ import fs10 from "fs";
12974
13105
  // package.json
12975
13106
  var package_default = {
12976
13107
  name: "crumbtrail-node",
12977
- version: "0.2.0",
13108
+ version: "0.2.1",
12978
13109
  description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
12979
13110
  license: "MIT",
12980
13111
  repository: {
@@ -17591,6 +17722,7 @@ var McpServer = class {
17591
17722
  const currentSession = stringField5(args.currentSession);
17592
17723
  let evidence = [];
17593
17724
  let intent = [];
17725
+ let locatedDecision;
17594
17726
  const adapterGaps = [];
17595
17727
  let sessionlessAdapterBundle = false;
17596
17728
  if (baselineSession && currentSession) {
@@ -17606,6 +17738,13 @@ var McpServer = class {
17606
17738
  try {
17607
17739
  const located = locateEvidence(symptom, this.recallStore());
17608
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
+ };
17609
17748
  const adapter = await gatherAdapterEvidence(symptom, located, {
17610
17749
  sources: this.evidenceSources()
17611
17750
  });
@@ -17691,7 +17830,13 @@ var McpServer = class {
17691
17830
  }
17692
17831
  ] : []
17693
17832
  ];
17694
- const bundle = assembleBundle({ symptom, evidence, intent, gaps });
17833
+ const bundle = assembleBundle({
17834
+ symptom,
17835
+ evidence,
17836
+ intent,
17837
+ gaps,
17838
+ located: locatedDecision
17839
+ });
17695
17840
  if (budget.maxTokens === void 0) return textResult(bundle);
17696
17841
  return this.budgetedTextResult(
17697
17842
  bundle,
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,
@@ -218694,7 +218812,7 @@ function timeProximity(now, candidateTime) {
218694
218812
  const decay = Math.pow(0.5, ageMs / TIME_PROXIMITY_HALF_LIFE_MS);
218695
218813
  return { boost: TIME_PROXIMITY_WEIGHT * decay, recent: decay >= 0.5 };
218696
218814
  }
218697
- function clamp01(value) {
218815
+ function clamp012(value) {
218698
218816
  return Math.min(1, Math.max(0, value));
218699
218817
  }
218700
218818
  function locateIncident(symptom, store, opts = {}) {
@@ -218723,7 +218841,7 @@ function locateIncident(symptom, store, opts = {}) {
218723
218841
  candidates.push({
218724
218842
  sessionId: id,
218725
218843
  bugId: bug.bugId,
218726
- confidence: clamp01(base.score + time.boost + releaseBoost),
218844
+ confidence: clamp012(base.score + time.boost + releaseBoost),
218727
218845
  reasons,
218728
218846
  bug
218729
218847
  });
@@ -218869,7 +218987,20 @@ async function locateAndAssemble(symptom, store, opts = {}) {
218869
218987
  const adapter = await gatherAdapterEvidence(symptom, located, opts);
218870
218988
  const evidence = [...located.evidence, ...adapter.items];
218871
218989
  const gaps = located.evidence.length === 0 ? [NO_LOCATED_SESSION_GAP, ...adapter.gaps] : [...adapter.gaps];
218872
- 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
+ });
218873
219004
  return { bundle, match: located.match, sources: adapter.sources };
218874
219005
  }
218875
219006
 
@@ -225662,7 +225793,7 @@ var import_node_fs10 = __toESM(require("fs"), 1);
225662
225793
  // package.json
225663
225794
  var package_default = {
225664
225795
  name: "crumbtrail-node",
225665
- version: "0.2.0",
225796
+ version: "0.2.1",
225666
225797
  description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
225667
225798
  license: "MIT",
225668
225799
  repository: {
@@ -230654,6 +230785,7 @@ var McpServer = class {
230654
230785
  const currentSession = stringField5(args.currentSession);
230655
230786
  let evidence = [];
230656
230787
  let intent = [];
230788
+ let locatedDecision;
230657
230789
  const adapterGaps = [];
230658
230790
  let sessionlessAdapterBundle = false;
230659
230791
  if (baselineSession && currentSession) {
@@ -230669,6 +230801,13 @@ var McpServer = class {
230669
230801
  try {
230670
230802
  const located = locateEvidence(symptom, this.recallStore());
230671
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
+ };
230672
230811
  const adapter = await gatherAdapterEvidence(symptom, located, {
230673
230812
  sources: this.evidenceSources()
230674
230813
  });
@@ -230754,7 +230893,13 @@ var McpServer = class {
230754
230893
  }
230755
230894
  ] : []
230756
230895
  ];
230757
- const bundle = assembleBundle({ symptom, evidence, intent, gaps });
230896
+ const bundle = assembleBundle({
230897
+ symptom,
230898
+ evidence,
230899
+ intent,
230900
+ gaps,
230901
+ located: locatedDecision
230902
+ });
230758
230903
  if (budget.maxTokens === void 0) return textResult(bundle);
230759
230904
  return this.budgetedTextResult(
230760
230905
  bundle,
package/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  renderProviderCliOutput,
22
22
  resolveLatestIssue,
23
23
  resolveSessionDirById
24
- } from "./chunk-5HPWIOG3.js";
24
+ } from "./chunk-GHLXGRLJ.js";
25
25
  import {
26
26
  __commonJS,
27
27
  __require,
package/dist/index.cjs CHANGED
@@ -1300,21 +1300,139 @@ function inferIntent(evidence, commits) {
1300
1300
  var FUSION_SCHEMA_VERSION = "fusion.v1";
1301
1301
  function assembleBundle(input) {
1302
1302
  const evidence = rankEvidence(input.symptom, input.evidence);
1303
- const hypotheses = classifyHypotheses(
1303
+ const classified = classifyHypotheses(
1304
1304
  input.symptom,
1305
1305
  input.evidence,
1306
1306
  input.intent
1307
1307
  );
1308
1308
  const gaps = input.gaps ?? [];
1309
+ const located = input.located;
1310
+ const hypotheses = classified.map((hypothesis) => {
1311
+ const verification = deriveVerification(hypothesis, evidence);
1312
+ return verification.length > 0 ? { ...hypothesis, verification } : hypothesis;
1313
+ });
1314
+ const contextCompleteness = deriveContextCompleteness(
1315
+ evidence,
1316
+ gaps,
1317
+ hypotheses,
1318
+ located
1319
+ );
1320
+ const escalation = deriveEscalation(contextCompleteness, hypotheses);
1309
1321
  return {
1310
1322
  schemaVersion: FUSION_SCHEMA_VERSION,
1311
1323
  symptom: input.symptom,
1312
1324
  evidence,
1313
1325
  opinion: { stance: "advisory", hypotheses },
1314
1326
  gaps,
1315
- directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
1327
+ directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps),
1328
+ contextCompleteness,
1329
+ escalation,
1330
+ ...located ? { located } : {}
1316
1331
  };
1317
1332
  }
1333
+ function clamp01(value) {
1334
+ if (Number.isNaN(value)) return 0;
1335
+ return Math.max(0, Math.min(1, value));
1336
+ }
1337
+ var COMPLETENESS_LANES = [
1338
+ "network",
1339
+ "db",
1340
+ "flow",
1341
+ "browser",
1342
+ "env"
1343
+ ];
1344
+ function deriveContextCompleteness(evidence, gaps, hypotheses, located) {
1345
+ const lanesPresent = new Set(evidence.map((item) => item.lane));
1346
+ const informativePresent = COMPLETENESS_LANES.filter(
1347
+ (lane) => lanesPresent.has(lane)
1348
+ );
1349
+ const breadth = Math.min(1, informativePresent.length / 3);
1350
+ const volume = Math.min(1, evidence.length / 5);
1351
+ const top = hypotheses[0];
1352
+ const hypothesisStrength = !top || top.kind === "inconclusive" ? 0 : top.confidence;
1353
+ let score = 0.4 * breadth + 0.25 * volume + 0.35 * hypothesisStrength;
1354
+ if (located) {
1355
+ score = located.outcome === "matched" ? 0.85 * score + 0.15 * clamp01(located.confidence) : score * 0.6;
1356
+ }
1357
+ const hardGaps = gaps.filter((gap) => gap.kind === "source-unavailable").length;
1358
+ const softGaps = gaps.length - hardGaps;
1359
+ const gapPenalty = Math.min(0.5, softGaps * 0.1 + hardGaps * 0.3);
1360
+ score = clamp01(score - gapPenalty);
1361
+ const level = score < 0.34 ? "low" : score < 0.67 ? "medium" : "high";
1362
+ const reasons = [];
1363
+ const missingLanes = COMPLETENESS_LANES.filter(
1364
+ (lane) => !lanesPresent.has(lane)
1365
+ );
1366
+ if (informativePresent.length === 0) {
1367
+ reasons.push("no network/db/flow/browser/env evidence captured");
1368
+ } else if (missingLanes.length > 0) {
1369
+ reasons.push(`missing evidence lanes: ${missingLanes.join(", ")}`);
1370
+ }
1371
+ if (evidence.length > 0 && evidence.length < 3) {
1372
+ reasons.push(`thin evidence (${evidence.length} item(s))`);
1373
+ }
1374
+ if (hardGaps > 0) reasons.push(`source unavailable for ${hardGaps} lane(s)`);
1375
+ if (softGaps > 0) reasons.push(`${softGaps} evidence gap(s)`);
1376
+ if (located?.outcome === "inconclusive") {
1377
+ reasons.push("incident location inconclusive");
1378
+ }
1379
+ if (!top || top.kind === "inconclusive") {
1380
+ reasons.push("no distinguishing hypothesis");
1381
+ }
1382
+ return { score, level, reasons };
1383
+ }
1384
+ function pkString(pk) {
1385
+ return Object.keys(pk).sort().map((key) => `${key}=${String(pk[key])}`).join(", ");
1386
+ }
1387
+ function deriveVerification(hypothesis, evidence) {
1388
+ if (hypothesis.kind === "inconclusive") return [];
1389
+ const cited = new Set(hypothesis.evidenceIds);
1390
+ const out = [];
1391
+ for (const item of evidence) {
1392
+ if (!cited.has(item.id)) continue;
1393
+ const ref = item.ref;
1394
+ if (item.lane === "db" && ref.table && ref.pk && Object.keys(ref.pk).length > 0) {
1395
+ out.push({
1396
+ observation: `row in ${ref.table} (${pkString(ref.pk)}) matches the intended post-fix state`,
1397
+ evidenceIds: [item.id],
1398
+ how: "db"
1399
+ });
1400
+ } else if (ref.requestId) {
1401
+ const sigPart = ref.sig ? ` for signature ${ref.sig}` : "";
1402
+ out.push({
1403
+ observation: `request ${ref.requestId} succeeds${sigPart} on a fresh run (no error response)`,
1404
+ evidenceIds: [item.id],
1405
+ how: "request"
1406
+ });
1407
+ } else if (ref.sig) {
1408
+ out.push({
1409
+ observation: `error signature ${ref.sig} no longer appears in a fresh session over the same route`,
1410
+ evidenceIds: [item.id],
1411
+ how: "session"
1412
+ });
1413
+ }
1414
+ }
1415
+ return out;
1416
+ }
1417
+ function deriveEscalation(completeness, hypotheses) {
1418
+ const allInconclusive = hypotheses.length > 0 && hypotheses.every((hypothesis) => hypothesis.kind === "inconclusive");
1419
+ const recommended = completeness.level === "low" || allInconclusive;
1420
+ if (!recommended) return { recommended: false, when: [] };
1421
+ const when = [
1422
+ "if you cannot reproduce the symptom via the anchored request or session, stop and request human triage \u2014 do not widen the search"
1423
+ ];
1424
+ if (completeness.level === "low") {
1425
+ when.push(
1426
+ "context is thin: confirm the missing evidence lanes before acting on the top hypothesis"
1427
+ );
1428
+ }
1429
+ if (allInconclusive) {
1430
+ when.push(
1431
+ "no hypothesis is distinguished; treat the listed causes as equally unproven"
1432
+ );
1433
+ }
1434
+ return { recommended, when };
1435
+ }
1318
1436
  var LANE_PRIOR = {
1319
1437
  db: 0.2,
1320
1438
  network: 0.2,
@@ -5821,7 +5939,7 @@ function timeProximity(now, candidateTime) {
5821
5939
  const decay = Math.pow(0.5, ageMs / TIME_PROXIMITY_HALF_LIFE_MS);
5822
5940
  return { boost: TIME_PROXIMITY_WEIGHT * decay, recent: decay >= 0.5 };
5823
5941
  }
5824
- function clamp01(value) {
5942
+ function clamp012(value) {
5825
5943
  return Math.min(1, Math.max(0, value));
5826
5944
  }
5827
5945
  function locateIncident(symptom, store, opts = {}) {
@@ -5850,7 +5968,7 @@ function locateIncident(symptom, store, opts = {}) {
5850
5968
  candidates.push({
5851
5969
  sessionId: id,
5852
5970
  bugId: bug.bugId,
5853
- confidence: clamp01(base.score + time.boost + releaseBoost),
5971
+ confidence: clamp012(base.score + time.boost + releaseBoost),
5854
5972
  reasons,
5855
5973
  bug
5856
5974
  });
@@ -5996,7 +6114,20 @@ async function locateAndAssemble(symptom, store, opts = {}) {
5996
6114
  const adapter = await gatherAdapterEvidence(symptom, located, opts);
5997
6115
  const evidence = [...located.evidence, ...adapter.items];
5998
6116
  const gaps = located.evidence.length === 0 ? [NO_LOCATED_SESSION_GAP, ...adapter.gaps] : [...adapter.gaps];
5999
- const bundle = assembleBundle({ symptom, evidence, intent: [], gaps });
6117
+ const located_ = {
6118
+ outcome: located.match.outcome,
6119
+ confidence: located.match.confidence,
6120
+ method: "fuzzy",
6121
+ ...located.match.sessionId ? { sessionId: located.match.sessionId } : {},
6122
+ reasons: located.match.reasons
6123
+ };
6124
+ const bundle = assembleBundle({
6125
+ symptom,
6126
+ evidence,
6127
+ intent: [],
6128
+ gaps,
6129
+ located: located_
6130
+ });
6000
6131
  return { bundle, match: located.match, sources: adapter.sources };
6001
6132
  }
6002
6133
 
@@ -12789,7 +12920,7 @@ var import_node_fs10 = __toESM(require("fs"), 1);
12789
12920
  // package.json
12790
12921
  var package_default = {
12791
12922
  name: "crumbtrail-node",
12792
- version: "0.2.0",
12923
+ version: "0.2.1",
12793
12924
  description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
12794
12925
  license: "MIT",
12795
12926
  repository: {
@@ -17781,6 +17912,7 @@ var McpServer = class {
17781
17912
  const currentSession = stringField5(args.currentSession);
17782
17913
  let evidence = [];
17783
17914
  let intent = [];
17915
+ let locatedDecision;
17784
17916
  const adapterGaps = [];
17785
17917
  let sessionlessAdapterBundle = false;
17786
17918
  if (baselineSession && currentSession) {
@@ -17796,6 +17928,13 @@ var McpServer = class {
17796
17928
  try {
17797
17929
  const located = locateEvidence(symptom, this.recallStore());
17798
17930
  evidence = located.evidence;
17931
+ locatedDecision = {
17932
+ outcome: located.match.outcome,
17933
+ confidence: located.match.confidence,
17934
+ method: "fuzzy",
17935
+ ...located.match.sessionId ? { sessionId: located.match.sessionId } : {},
17936
+ reasons: located.match.reasons
17937
+ };
17799
17938
  const adapter = await gatherAdapterEvidence(symptom, located, {
17800
17939
  sources: this.evidenceSources()
17801
17940
  });
@@ -17881,7 +18020,13 @@ var McpServer = class {
17881
18020
  }
17882
18021
  ] : []
17883
18022
  ];
17884
- const bundle = assembleBundle({ symptom, evidence, intent, gaps });
18023
+ const bundle = assembleBundle({
18024
+ symptom,
18025
+ evidence,
18026
+ intent,
18027
+ gaps,
18028
+ located: locatedDecision
18029
+ });
17885
18030
  if (budget.maxTokens === void 0) return textResult(bundle);
17886
18031
  return this.budgetedTextResult(
17887
18032
  bundle,
package/dist/index.js CHANGED
@@ -142,7 +142,7 @@ import {
142
142
  startSessionSweeper,
143
143
  sweepIdleSessions,
144
144
  withBoundedRetry
145
- } from "./chunk-5HPWIOG3.js";
145
+ } from "./chunk-GHLXGRLJ.js";
146
146
  import "./chunk-QGM4M3NI.js";
147
147
 
148
148
  // src/db/columns.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crumbtrail-node",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "protobufjs": "^8.6.5",
45
- "crumbtrail-core": "^0.2.0"
45
+ "crumbtrail-core": "^0.2.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/express": "^5.0.6",