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/{chunk-EASSXKUJ.js → chunk-GHLXGRLJ.js} +454 -127
- package/dist/cli.cjs +453 -127
- package/dist/cli.js +2 -1
- package/dist/index.cjs +456 -125
- package/dist/index.d.cts +79 -4
- package/dist/index.d.ts +79 -4
- package/dist/index.js +5 -1
- package/package.json +7 -3
package/dist/index.cjs
CHANGED
|
@@ -123,6 +123,7 @@ __export(index_exports, {
|
|
|
123
123
|
cloudWatchEvidenceProvider: () => cloudWatchEvidenceProvider,
|
|
124
124
|
cloudflareEvidenceProvider: () => cloudflareEvidenceProvider,
|
|
125
125
|
compareSessions: () => compareSessions,
|
|
126
|
+
comparisonTitle: () => comparisonTitle,
|
|
126
127
|
createCrumbtrailExpressErrorMiddleware: () => createCrumbtrailExpressErrorMiddleware,
|
|
127
128
|
createCrumbtrailExpressMiddleware: () => createCrumbtrailExpressMiddleware,
|
|
128
129
|
createServer: () => createServer,
|
|
@@ -172,6 +173,7 @@ __export(index_exports, {
|
|
|
172
173
|
renderProviderReadme: () => renderProviderReadme,
|
|
173
174
|
resolveDbRequestContext: () => resolveDbRequestContext,
|
|
174
175
|
sentryEvidenceProvider: () => sentryEvidenceProvider,
|
|
176
|
+
sessionRefLabel: () => sessionRefLabel,
|
|
175
177
|
signSigV4: () => signSigV4,
|
|
176
178
|
splunkEvidenceProvider: () => splunkEvidenceProvider,
|
|
177
179
|
splunkSearchDeepLink: () => splunkSearchDeepLink,
|
|
@@ -1298,21 +1300,139 @@ function inferIntent(evidence, commits) {
|
|
|
1298
1300
|
var FUSION_SCHEMA_VERSION = "fusion.v1";
|
|
1299
1301
|
function assembleBundle(input) {
|
|
1300
1302
|
const evidence = rankEvidence(input.symptom, input.evidence);
|
|
1301
|
-
const
|
|
1303
|
+
const classified = classifyHypotheses(
|
|
1302
1304
|
input.symptom,
|
|
1303
1305
|
input.evidence,
|
|
1304
1306
|
input.intent
|
|
1305
1307
|
);
|
|
1306
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);
|
|
1307
1321
|
return {
|
|
1308
1322
|
schemaVersion: FUSION_SCHEMA_VERSION,
|
|
1309
1323
|
symptom: input.symptom,
|
|
1310
1324
|
evidence,
|
|
1311
1325
|
opinion: { stance: "advisory", hypotheses },
|
|
1312
1326
|
gaps,
|
|
1313
|
-
directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps)
|
|
1327
|
+
directives: suggestCaptureDirectives(input.symptom, input.evidence, gaps),
|
|
1328
|
+
contextCompleteness,
|
|
1329
|
+
escalation,
|
|
1330
|
+
...located ? { located } : {}
|
|
1314
1331
|
};
|
|
1315
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
|
+
}
|
|
1316
1436
|
var LANE_PRIOR = {
|
|
1317
1437
|
db: 0.2,
|
|
1318
1438
|
network: 0.2,
|
|
@@ -3081,6 +3201,7 @@ var SentryError = class extends Error {
|
|
|
3081
3201
|
this.status = status;
|
|
3082
3202
|
this.name = "SentryError";
|
|
3083
3203
|
}
|
|
3204
|
+
status;
|
|
3084
3205
|
};
|
|
3085
3206
|
function sanitizeUrl2(u) {
|
|
3086
3207
|
try {
|
|
@@ -3494,6 +3615,8 @@ var CloudWatchError = class extends Error {
|
|
|
3494
3615
|
this.target = target;
|
|
3495
3616
|
this.name = "CloudWatchError";
|
|
3496
3617
|
}
|
|
3618
|
+
status;
|
|
3619
|
+
target;
|
|
3497
3620
|
};
|
|
3498
3621
|
function isTransient2(error) {
|
|
3499
3622
|
if (error instanceof CloudWatchError) {
|
|
@@ -3906,6 +4029,7 @@ var SplunkError = class extends Error {
|
|
|
3906
4029
|
this.status = status;
|
|
3907
4030
|
this.name = "SplunkError";
|
|
3908
4031
|
}
|
|
4032
|
+
status;
|
|
3909
4033
|
};
|
|
3910
4034
|
function sanitizeUrl3(u) {
|
|
3911
4035
|
try {
|
|
@@ -4256,6 +4380,7 @@ var DatadogError = class extends Error {
|
|
|
4256
4380
|
this.status = status;
|
|
4257
4381
|
this.name = "DatadogError";
|
|
4258
4382
|
}
|
|
4383
|
+
status;
|
|
4259
4384
|
};
|
|
4260
4385
|
function sanitizeUrl4(u) {
|
|
4261
4386
|
try {
|
|
@@ -4619,6 +4744,7 @@ var PostHogError = class extends Error {
|
|
|
4619
4744
|
this.status = status;
|
|
4620
4745
|
this.name = "PostHogError";
|
|
4621
4746
|
}
|
|
4747
|
+
status;
|
|
4622
4748
|
};
|
|
4623
4749
|
function sanitizeUrl5(u) {
|
|
4624
4750
|
try {
|
|
@@ -5009,6 +5135,8 @@ var CloudflareError = class extends Error {
|
|
|
5009
5135
|
this.op = op;
|
|
5010
5136
|
this.name = "CloudflareError";
|
|
5011
5137
|
}
|
|
5138
|
+
status;
|
|
5139
|
+
op;
|
|
5012
5140
|
};
|
|
5013
5141
|
function isTransient6(error) {
|
|
5014
5142
|
if (error instanceof CloudflareError) {
|
|
@@ -5811,7 +5939,7 @@ function timeProximity(now, candidateTime) {
|
|
|
5811
5939
|
const decay = Math.pow(0.5, ageMs / TIME_PROXIMITY_HALF_LIFE_MS);
|
|
5812
5940
|
return { boost: TIME_PROXIMITY_WEIGHT * decay, recent: decay >= 0.5 };
|
|
5813
5941
|
}
|
|
5814
|
-
function
|
|
5942
|
+
function clamp012(value) {
|
|
5815
5943
|
return Math.min(1, Math.max(0, value));
|
|
5816
5944
|
}
|
|
5817
5945
|
function locateIncident(symptom, store, opts = {}) {
|
|
@@ -5840,7 +5968,7 @@ function locateIncident(symptom, store, opts = {}) {
|
|
|
5840
5968
|
candidates.push({
|
|
5841
5969
|
sessionId: id,
|
|
5842
5970
|
bugId: bug.bugId,
|
|
5843
|
-
confidence:
|
|
5971
|
+
confidence: clamp012(base.score + time.boost + releaseBoost),
|
|
5844
5972
|
reasons,
|
|
5845
5973
|
bug
|
|
5846
5974
|
});
|
|
@@ -5986,7 +6114,20 @@ async function locateAndAssemble(symptom, store, opts = {}) {
|
|
|
5986
6114
|
const adapter = await gatherAdapterEvidence(symptom, located, opts);
|
|
5987
6115
|
const evidence = [...located.evidence, ...adapter.items];
|
|
5988
6116
|
const gaps = located.evidence.length === 0 ? [NO_LOCATED_SESSION_GAP, ...adapter.gaps] : [...adapter.gaps];
|
|
5989
|
-
const
|
|
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
|
+
});
|
|
5990
6131
|
return { bundle, match: located.match, sources: adapter.sources };
|
|
5991
6132
|
}
|
|
5992
6133
|
|
|
@@ -11027,6 +11168,8 @@ var AudioProcessingError = class extends Error {
|
|
|
11027
11168
|
this.cause = cause;
|
|
11028
11169
|
this.name = "AudioProcessingError";
|
|
11029
11170
|
}
|
|
11171
|
+
phase;
|
|
11172
|
+
cause;
|
|
11030
11173
|
};
|
|
11031
11174
|
var AudioTranscriptionUnavailableError = class extends Error {
|
|
11032
11175
|
constructor() {
|
|
@@ -12777,12 +12920,12 @@ var import_node_fs10 = __toESM(require("fs"), 1);
|
|
|
12777
12920
|
// package.json
|
|
12778
12921
|
var package_default = {
|
|
12779
12922
|
name: "crumbtrail-node",
|
|
12780
|
-
version: "0.1
|
|
12923
|
+
version: "0.2.1",
|
|
12781
12924
|
description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
|
|
12782
12925
|
license: "MIT",
|
|
12783
12926
|
repository: {
|
|
12784
12927
|
type: "git",
|
|
12785
|
-
url: "https://github.com/
|
|
12928
|
+
url: "git+https://github.com/crumbtrail-dev/crumbtrail-js.git",
|
|
12786
12929
|
directory: "packages/node"
|
|
12787
12930
|
},
|
|
12788
12931
|
keywords: [
|
|
@@ -12835,6 +12978,10 @@ var package_default = {
|
|
|
12835
12978
|
"@types/node": "^25.5.0",
|
|
12836
12979
|
express: "^5.2.1",
|
|
12837
12980
|
typescript: "^5.7.0"
|
|
12981
|
+
},
|
|
12982
|
+
homepage: "https://github.com/crumbtrail-dev/crumbtrail-js/tree/main/packages/node#readme",
|
|
12983
|
+
bugs: {
|
|
12984
|
+
url: "https://github.com/crumbtrail-dev/crumbtrail-js/issues"
|
|
12838
12985
|
}
|
|
12839
12986
|
};
|
|
12840
12987
|
|
|
@@ -13485,6 +13632,10 @@ var RequestValidationError = class extends Error {
|
|
|
13485
13632
|
this.retryable = retryable;
|
|
13486
13633
|
this.name = "RequestValidationError";
|
|
13487
13634
|
}
|
|
13635
|
+
status;
|
|
13636
|
+
publicMessage;
|
|
13637
|
+
code;
|
|
13638
|
+
retryable;
|
|
13488
13639
|
};
|
|
13489
13640
|
function serveStatic(res, staticDir, urlPath) {
|
|
13490
13641
|
const resolved = resolveStaticPath(staticDir, urlPath);
|
|
@@ -14214,6 +14365,7 @@ var BoundedTraceSessionCache = class {
|
|
|
14214
14365
|
constructor(maxEntries) {
|
|
14215
14366
|
this.maxEntries = maxEntries;
|
|
14216
14367
|
}
|
|
14368
|
+
maxEntries;
|
|
14217
14369
|
entries = /* @__PURE__ */ new Map();
|
|
14218
14370
|
get(traceId) {
|
|
14219
14371
|
const existing = this.entries.get(traceId);
|
|
@@ -14912,6 +15064,20 @@ function createServer(config) {
|
|
|
14912
15064
|
});
|
|
14913
15065
|
server.on("close", () => sweeper.stop());
|
|
14914
15066
|
}
|
|
15067
|
+
if (config.ai?.enabled && config.ai.backfillOnStart) {
|
|
15068
|
+
const sessionDirs = sessions.listSummaries().map((summary) => sessions.getExistingSessionDir(summary.id)).filter((dir) => dir !== void 0);
|
|
15069
|
+
const timer = setTimeout(() => {
|
|
15070
|
+
void backfillAiDiagnoses(
|
|
15071
|
+
sessionDirs,
|
|
15072
|
+
config.ai
|
|
15073
|
+
).then(
|
|
15074
|
+
(result) => config.ai?.log?.(
|
|
15075
|
+
`Crumbtrail AI diagnosis backfill complete: ${JSON.stringify(result)}`
|
|
15076
|
+
)
|
|
15077
|
+
);
|
|
15078
|
+
}, 0);
|
|
15079
|
+
server.on("close", () => clearTimeout(timer));
|
|
15080
|
+
}
|
|
14915
15081
|
if (fastFinalizer) {
|
|
14916
15082
|
server.on("close", () => fastFinalizer.stop());
|
|
14917
15083
|
}
|
|
@@ -14961,6 +15127,7 @@ var FixContextError = class extends Error {
|
|
|
14961
15127
|
this.code = code2;
|
|
14962
15128
|
this.name = "FixContextError";
|
|
14963
15129
|
}
|
|
15130
|
+
code;
|
|
14964
15131
|
};
|
|
14965
15132
|
function buildFixContext(sessionDirOrId, opts = {}) {
|
|
14966
15133
|
const sessionDir = resolveSessionDir(sessionDirOrId, opts);
|
|
@@ -16020,16 +16187,85 @@ function diffEnvironment(a, b, rules, disabled) {
|
|
|
16020
16187
|
addSuppressedRules(rules, beforeRules, afterRules, disabled);
|
|
16021
16188
|
return [];
|
|
16022
16189
|
}
|
|
16190
|
+
const envDelta = buildEnvDiff(a, b, disabled);
|
|
16023
16191
|
return [
|
|
16024
16192
|
{
|
|
16025
16193
|
plane: "env",
|
|
16026
16194
|
kind: "env.snapshot",
|
|
16027
16195
|
before: a,
|
|
16028
16196
|
after: b,
|
|
16029
|
-
brief:
|
|
16197
|
+
brief: envBrief(envDelta),
|
|
16198
|
+
envDelta
|
|
16030
16199
|
}
|
|
16031
16200
|
];
|
|
16032
16201
|
}
|
|
16202
|
+
function buildEnvDiff(a, b, disabled) {
|
|
16203
|
+
const delta = {
|
|
16204
|
+
flags: channelDelta(recordField(a.flags), recordField(b.flags), disabled),
|
|
16205
|
+
config: channelDelta(
|
|
16206
|
+
recordField(a.config),
|
|
16207
|
+
recordField(b.config),
|
|
16208
|
+
disabled
|
|
16209
|
+
)
|
|
16210
|
+
};
|
|
16211
|
+
const releaseBefore = stringOrUndefined(a.release);
|
|
16212
|
+
const releaseAfter = stringOrUndefined(b.release);
|
|
16213
|
+
if (releaseBefore !== releaseAfter) {
|
|
16214
|
+
delta.release = {
|
|
16215
|
+
...releaseBefore !== void 0 ? { before: releaseBefore } : {},
|
|
16216
|
+
...releaseAfter !== void 0 ? { after: releaseAfter } : {}
|
|
16217
|
+
};
|
|
16218
|
+
}
|
|
16219
|
+
const buildBefore = stringOrUndefined(a.build);
|
|
16220
|
+
const buildAfter = stringOrUndefined(b.build);
|
|
16221
|
+
if (buildBefore !== buildAfter) {
|
|
16222
|
+
delta.build = {
|
|
16223
|
+
...buildBefore !== void 0 ? { before: buildBefore } : {},
|
|
16224
|
+
...buildAfter !== void 0 ? { after: buildAfter } : {}
|
|
16225
|
+
};
|
|
16226
|
+
}
|
|
16227
|
+
return delta;
|
|
16228
|
+
}
|
|
16229
|
+
function recordField(value) {
|
|
16230
|
+
return isRecord14(value) ? value : {};
|
|
16231
|
+
}
|
|
16232
|
+
function channelDelta(a, b, disabled) {
|
|
16233
|
+
const added = [];
|
|
16234
|
+
const removed = [];
|
|
16235
|
+
const changed = [];
|
|
16236
|
+
for (const key of Object.keys(a).sort()) {
|
|
16237
|
+
if (!(key in b)) {
|
|
16238
|
+
removed.push({ key, before: a[key] });
|
|
16239
|
+
} else if (envValuesDiffer(a[key], b[key], disabled)) {
|
|
16240
|
+
changed.push({ key, before: a[key], after: b[key] });
|
|
16241
|
+
}
|
|
16242
|
+
}
|
|
16243
|
+
for (const key of Object.keys(b).sort()) {
|
|
16244
|
+
if (!(key in a)) added.push({ key, after: b[key] });
|
|
16245
|
+
}
|
|
16246
|
+
return { added, removed, changed };
|
|
16247
|
+
}
|
|
16248
|
+
function envValuesDiffer(before, after, disabled) {
|
|
16249
|
+
const beforeRules = /* @__PURE__ */ new Set();
|
|
16250
|
+
const afterRules = /* @__PURE__ */ new Set();
|
|
16251
|
+
if (canonicalize(before, beforeRules) !== canonicalize(after, afterRules))
|
|
16252
|
+
return true;
|
|
16253
|
+
const rawDiffers = stableStringify(before) !== stableStringify(after);
|
|
16254
|
+
return rawDiffers && hasDisabledRule(beforeRules, afterRules, disabled);
|
|
16255
|
+
}
|
|
16256
|
+
function envBrief(delta) {
|
|
16257
|
+
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);
|
|
16258
|
+
const parts = [];
|
|
16259
|
+
const flagCount = delta.flags.added.length + delta.flags.removed.length + delta.flags.changed.length;
|
|
16260
|
+
const configCount = delta.config.added.length + delta.config.removed.length + delta.config.changed.length;
|
|
16261
|
+
if (flagCount > 0) parts.push(`${flagCount} flag(s)`);
|
|
16262
|
+
if (configCount > 0) parts.push(`${configCount} config value(s)`);
|
|
16263
|
+
if (delta.release) parts.push("release");
|
|
16264
|
+
if (delta.build) parts.push("build");
|
|
16265
|
+
if (counts === 0 || parts.length === 0)
|
|
16266
|
+
return "feature flag, config, release, or build snapshot changed between sessions";
|
|
16267
|
+
return `environment delta between sessions: ${parts.join(", ")} changed`;
|
|
16268
|
+
}
|
|
16033
16269
|
function confidenceFor(divergences) {
|
|
16034
16270
|
if (divergences.length === 0) return "high";
|
|
16035
16271
|
if (divergences.some(
|
|
@@ -16056,6 +16292,9 @@ async function compareSessions(aDir, bDir, options = {}) {
|
|
|
16056
16292
|
...diffDb(a.dbWrites, b.dbWrites, rules, disabled),
|
|
16057
16293
|
...diffEnvironment(a.environment, b.environment, rules, disabled)
|
|
16058
16294
|
];
|
|
16295
|
+
const envDelta = divergences.find(
|
|
16296
|
+
(divergence) => divergence.kind === "env.snapshot"
|
|
16297
|
+
)?.envDelta;
|
|
16059
16298
|
return {
|
|
16060
16299
|
schemaVersion: SESSION_COMPARE_SCHEMA_VERSION,
|
|
16061
16300
|
verdict: divergences.length > 0 ? "regression" : "clean",
|
|
@@ -16078,20 +16317,187 @@ async function compareSessions(aDir, bDir, options = {}) {
|
|
|
16078
16317
|
divergences,
|
|
16079
16318
|
noise: { suppressedCount: rules.size, rules: [...rules].sort() },
|
|
16080
16319
|
evidence: divergencesToEvidence(divergences),
|
|
16081
|
-
intent: []
|
|
16320
|
+
intent: [],
|
|
16321
|
+
...envDelta ? { envDelta } : {}
|
|
16082
16322
|
};
|
|
16083
16323
|
}
|
|
16084
16324
|
|
|
16085
16325
|
// src/compare/regression-context.ts
|
|
16086
16326
|
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
16087
16327
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
16328
|
+
|
|
16329
|
+
// src/compare/report.ts
|
|
16330
|
+
var PLANE_ORDER = ["flow", "network", "db", "env"];
|
|
16331
|
+
var PLANE_LABELS = {
|
|
16332
|
+
flow: "Flow steps",
|
|
16333
|
+
network: "Network calls",
|
|
16334
|
+
db: "Database rows",
|
|
16335
|
+
env: "Environment and flags"
|
|
16336
|
+
};
|
|
16337
|
+
function renderCompareReport(comparison) {
|
|
16338
|
+
const lines = [];
|
|
16339
|
+
const regressed = comparison.verdict === "regression";
|
|
16340
|
+
lines.push(
|
|
16341
|
+
`# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
|
|
16342
|
+
);
|
|
16343
|
+
lines.push("");
|
|
16344
|
+
lines.push(
|
|
16345
|
+
regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
|
|
16346
|
+
);
|
|
16347
|
+
lines.push("");
|
|
16348
|
+
lines.push(
|
|
16349
|
+
"A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
|
|
16350
|
+
);
|
|
16351
|
+
lines.push("");
|
|
16352
|
+
lines.push("## Aligned flow");
|
|
16353
|
+
lines.push("");
|
|
16354
|
+
const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
|
|
16355
|
+
lines.push(
|
|
16356
|
+
`${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
|
|
16357
|
+
);
|
|
16358
|
+
lines.push("");
|
|
16359
|
+
if (comparison.divergences.length > 0) {
|
|
16360
|
+
lines.push("| Plane | Kind | Component | Divergence |");
|
|
16361
|
+
lines.push("|---|---|---|---|");
|
|
16362
|
+
for (const d of comparison.divergences) {
|
|
16363
|
+
const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
|
|
16364
|
+
lines.push(
|
|
16365
|
+
`| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
|
|
16366
|
+
);
|
|
16367
|
+
}
|
|
16368
|
+
lines.push("");
|
|
16369
|
+
}
|
|
16370
|
+
for (const plane of PLANE_ORDER) {
|
|
16371
|
+
const planeDivergences = comparison.divergences.filter(
|
|
16372
|
+
(d) => d.plane === plane
|
|
16373
|
+
);
|
|
16374
|
+
if (planeDivergences.length === 0) continue;
|
|
16375
|
+
lines.push(`## ${PLANE_LABELS[plane]}`);
|
|
16376
|
+
lines.push("");
|
|
16377
|
+
for (const d of planeDivergences) {
|
|
16378
|
+
lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
|
|
16379
|
+
lines.push("");
|
|
16380
|
+
if (d.sig) lines.push(`- Component: \`${d.sig}\``);
|
|
16381
|
+
if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
|
|
16382
|
+
if (d.table)
|
|
16383
|
+
lines.push(
|
|
16384
|
+
`- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
|
|
16385
|
+
);
|
|
16386
|
+
lines.push("");
|
|
16387
|
+
if (d.envDelta) {
|
|
16388
|
+
lines.push(...renderEnvDelta(d.envDelta));
|
|
16389
|
+
} else {
|
|
16390
|
+
lines.push("```diff");
|
|
16391
|
+
lines.push(`- before: ${JSON.stringify(d.before)}`);
|
|
16392
|
+
lines.push(`+ after: ${JSON.stringify(d.after)}`);
|
|
16393
|
+
lines.push("```");
|
|
16394
|
+
}
|
|
16395
|
+
lines.push("");
|
|
16396
|
+
}
|
|
16397
|
+
}
|
|
16398
|
+
lines.push("---");
|
|
16399
|
+
lines.push("");
|
|
16400
|
+
lines.push(
|
|
16401
|
+
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.*"
|
|
16402
|
+
);
|
|
16403
|
+
lines.push("");
|
|
16404
|
+
return lines.join("\n");
|
|
16405
|
+
}
|
|
16406
|
+
function formatComparisonSummary(comparison) {
|
|
16407
|
+
const lines = [];
|
|
16408
|
+
lines.push(
|
|
16409
|
+
`crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
|
|
16410
|
+
);
|
|
16411
|
+
lines.push(` Schema: ${comparison.schemaVersion}`);
|
|
16412
|
+
lines.push(
|
|
16413
|
+
` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
|
|
16414
|
+
);
|
|
16415
|
+
lines.push(
|
|
16416
|
+
` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
|
|
16417
|
+
);
|
|
16418
|
+
lines.push(` Divergences: ${comparison.divergences.length}`);
|
|
16419
|
+
for (const d of comparison.divergences.slice(0, 5)) {
|
|
16420
|
+
lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
|
|
16421
|
+
}
|
|
16422
|
+
if (comparison.divergences.length > 5) {
|
|
16423
|
+
lines.push(
|
|
16424
|
+
` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
|
|
16425
|
+
);
|
|
16426
|
+
}
|
|
16427
|
+
const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
|
|
16428
|
+
lines.push(
|
|
16429
|
+
` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
|
|
16430
|
+
);
|
|
16431
|
+
return lines.join("\n");
|
|
16432
|
+
}
|
|
16433
|
+
function escapeCell(text3) {
|
|
16434
|
+
return text3.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
16435
|
+
}
|
|
16436
|
+
function sessionRefLabel(ref) {
|
|
16437
|
+
return ref.release ?? ref.sessionId;
|
|
16438
|
+
}
|
|
16439
|
+
function comparisonTitle(comparison) {
|
|
16440
|
+
return `${sessionRefLabel(comparison.a)} vs ${sessionRefLabel(comparison.b)}`;
|
|
16441
|
+
}
|
|
16442
|
+
function renderEnvDelta(delta) {
|
|
16443
|
+
const lines = ["```diff"];
|
|
16444
|
+
renderEnvChannel(lines, "flags", delta.flags);
|
|
16445
|
+
renderEnvChannel(lines, "config", delta.config);
|
|
16446
|
+
if (delta.release) {
|
|
16447
|
+
lines.push(
|
|
16448
|
+
`~ release: ${JSON.stringify(delta.release.before ?? null)} -> ${JSON.stringify(delta.release.after ?? null)}`
|
|
16449
|
+
);
|
|
16450
|
+
}
|
|
16451
|
+
if (delta.build) {
|
|
16452
|
+
lines.push(
|
|
16453
|
+
`~ build: ${JSON.stringify(delta.build.before ?? null)} -> ${JSON.stringify(delta.build.after ?? null)}`
|
|
16454
|
+
);
|
|
16455
|
+
}
|
|
16456
|
+
if (lines.length === 1) lines.push(" (no field-level changes)");
|
|
16457
|
+
lines.push("```");
|
|
16458
|
+
return lines;
|
|
16459
|
+
}
|
|
16460
|
+
function renderEnvChannel(lines, channel, delta) {
|
|
16461
|
+
for (const change of delta.added) {
|
|
16462
|
+
lines.push(
|
|
16463
|
+
`+ ${channel}.${change.key}: ${JSON.stringify(change.after ?? null)}`
|
|
16464
|
+
);
|
|
16465
|
+
}
|
|
16466
|
+
for (const change of delta.removed) {
|
|
16467
|
+
lines.push(
|
|
16468
|
+
`- ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)}`
|
|
16469
|
+
);
|
|
16470
|
+
}
|
|
16471
|
+
for (const change of delta.changed) {
|
|
16472
|
+
lines.push(
|
|
16473
|
+
`~ ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)} -> ${JSON.stringify(change.after ?? null)}`
|
|
16474
|
+
);
|
|
16475
|
+
}
|
|
16476
|
+
}
|
|
16477
|
+
function formatSessionRef(ref) {
|
|
16478
|
+
if (ref.release) {
|
|
16479
|
+
const detail = [
|
|
16480
|
+
`session ${ref.sessionId}`,
|
|
16481
|
+
ref.build ? `build ${ref.build}` : void 0
|
|
16482
|
+
].filter(Boolean);
|
|
16483
|
+
return `${ref.release} (${detail.join(", ")})`;
|
|
16484
|
+
}
|
|
16485
|
+
return ref.build ? `${ref.sessionId} (build ${ref.build})` : ref.sessionId;
|
|
16486
|
+
}
|
|
16487
|
+
|
|
16488
|
+
// src/compare/regression-context.ts
|
|
16088
16489
|
var REGRESSION_CONTEXT_SCHEMA_VERSION = "regression-context.v1";
|
|
16089
16490
|
function buildRegressionContext(comparison, bDir) {
|
|
16090
|
-
const requestIds = unique(
|
|
16091
|
-
|
|
16491
|
+
const requestIds = unique(
|
|
16492
|
+
comparison.divergences.map((d) => d.requestId).filter(isString)
|
|
16493
|
+
);
|
|
16494
|
+
const times = requestIds.flatMap(
|
|
16495
|
+
(requestId) => requestTimesFromIndex(bDir, requestId)
|
|
16496
|
+
);
|
|
16092
16497
|
const firstSig = comparison.divergences.map((d) => d.sig).find(isString);
|
|
16093
16498
|
return {
|
|
16094
16499
|
schemaVersion: REGRESSION_CONTEXT_SCHEMA_VERSION,
|
|
16500
|
+
title: comparisonTitle(comparison),
|
|
16095
16501
|
comparison,
|
|
16096
16502
|
divergent_interaction: firstSig ? interactionForSig(bDir, firstSig) : null,
|
|
16097
16503
|
causal_window: requestIds.length > 0 ? {
|
|
@@ -16109,6 +16515,7 @@ function buildRegressionContext(comparison, bDir) {
|
|
|
16109
16515
|
before: d.before,
|
|
16110
16516
|
after: d.after
|
|
16111
16517
|
})),
|
|
16518
|
+
env_delta: comparison.envDelta ?? null,
|
|
16112
16519
|
repro_hint: reproHint(bDir)
|
|
16113
16520
|
};
|
|
16114
16521
|
}
|
|
@@ -16153,7 +16560,8 @@ function collectRefTimes(value, times) {
|
|
|
16153
16560
|
const start = isRecord15(value.start) ? value.start : void 0;
|
|
16154
16561
|
const end = isRecord15(value.end) ? value.end : void 0;
|
|
16155
16562
|
for (const candidate of [ref, start, end]) {
|
|
16156
|
-
if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
|
|
16563
|
+
if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
|
|
16564
|
+
times.push(candidate.t);
|
|
16157
16565
|
}
|
|
16158
16566
|
}
|
|
16159
16567
|
function reproHint(sessionDir) {
|
|
@@ -17078,12 +17486,29 @@ var McpServer = class {
|
|
|
17078
17486
|
"sha"
|
|
17079
17487
|
]))
|
|
17080
17488
|
continue;
|
|
17081
|
-
sessions.push(meta);
|
|
17489
|
+
sessions.push(this.withReleaseBuild(meta));
|
|
17082
17490
|
} catch {
|
|
17083
17491
|
}
|
|
17084
17492
|
}
|
|
17085
17493
|
return textResult(sessions);
|
|
17086
17494
|
}
|
|
17495
|
+
/**
|
|
17496
|
+
* Surfaces release/build as first-class list-row fields regardless of which
|
|
17497
|
+
* alias the app used (release/releaseId/version, build/buildId/commit/sha), so
|
|
17498
|
+
* an agent can label and group sessions by release without re-reading each
|
|
17499
|
+
* meta. Additive: the raw meta keys are preserved.
|
|
17500
|
+
*/
|
|
17501
|
+
withReleaseBuild(meta) {
|
|
17502
|
+
const release = stringField5(meta.release ?? meta.releaseId ?? meta.version);
|
|
17503
|
+
const build = stringField5(
|
|
17504
|
+
meta.build ?? meta.buildId ?? meta.commit ?? meta.sha
|
|
17505
|
+
);
|
|
17506
|
+
return {
|
|
17507
|
+
...meta,
|
|
17508
|
+
...release !== void 0 ? { release } : {},
|
|
17509
|
+
...build !== void 0 ? { build } : {}
|
|
17510
|
+
};
|
|
17511
|
+
}
|
|
17087
17512
|
sessionMetadataMatches(meta, expected, keys) {
|
|
17088
17513
|
return keys.some((key) => meta[key] === expected);
|
|
17089
17514
|
}
|
|
@@ -17487,6 +17912,7 @@ var McpServer = class {
|
|
|
17487
17912
|
const currentSession = stringField5(args.currentSession);
|
|
17488
17913
|
let evidence = [];
|
|
17489
17914
|
let intent = [];
|
|
17915
|
+
let locatedDecision;
|
|
17490
17916
|
const adapterGaps = [];
|
|
17491
17917
|
let sessionlessAdapterBundle = false;
|
|
17492
17918
|
if (baselineSession && currentSession) {
|
|
@@ -17502,6 +17928,13 @@ var McpServer = class {
|
|
|
17502
17928
|
try {
|
|
17503
17929
|
const located = locateEvidence(symptom, this.recallStore());
|
|
17504
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
|
+
};
|
|
17505
17938
|
const adapter = await gatherAdapterEvidence(symptom, located, {
|
|
17506
17939
|
sources: this.evidenceSources()
|
|
17507
17940
|
});
|
|
@@ -17587,7 +18020,13 @@ var McpServer = class {
|
|
|
17587
18020
|
}
|
|
17588
18021
|
] : []
|
|
17589
18022
|
];
|
|
17590
|
-
const bundle = assembleBundle({
|
|
18023
|
+
const bundle = assembleBundle({
|
|
18024
|
+
symptom,
|
|
18025
|
+
evidence,
|
|
18026
|
+
intent,
|
|
18027
|
+
gaps,
|
|
18028
|
+
located: locatedDecision
|
|
18029
|
+
});
|
|
17591
18030
|
if (budget.maxTokens === void 0) return textResult(bundle);
|
|
17592
18031
|
return this.budgetedTextResult(
|
|
17593
18032
|
bundle,
|
|
@@ -20089,6 +20528,7 @@ var InspectError = class extends Error {
|
|
|
20089
20528
|
this.code = code2;
|
|
20090
20529
|
this.name = "InspectError";
|
|
20091
20530
|
}
|
|
20531
|
+
code;
|
|
20092
20532
|
};
|
|
20093
20533
|
function inspectSession(sessionDirOrId, opts = {}) {
|
|
20094
20534
|
const sessionDir = resolveSessionDir2(sessionDirOrId, opts);
|
|
@@ -20715,117 +21155,6 @@ function isRecord21(value) {
|
|
|
20715
21155
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20716
21156
|
}
|
|
20717
21157
|
|
|
20718
|
-
// src/compare/report.ts
|
|
20719
|
-
var PLANE_ORDER = ["flow", "network", "db", "env"];
|
|
20720
|
-
var PLANE_LABELS = {
|
|
20721
|
-
flow: "Flow steps",
|
|
20722
|
-
network: "Network calls",
|
|
20723
|
-
db: "Database rows",
|
|
20724
|
-
env: "Environment and flags"
|
|
20725
|
-
};
|
|
20726
|
-
function renderCompareReport(comparison) {
|
|
20727
|
-
const lines = [];
|
|
20728
|
-
const regressed = comparison.verdict === "regression";
|
|
20729
|
-
lines.push(
|
|
20730
|
-
`# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
|
|
20731
|
-
);
|
|
20732
|
-
lines.push("");
|
|
20733
|
-
lines.push(
|
|
20734
|
-
regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
|
|
20735
|
-
);
|
|
20736
|
-
lines.push("");
|
|
20737
|
-
lines.push(
|
|
20738
|
-
"A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
|
|
20739
|
-
);
|
|
20740
|
-
lines.push("");
|
|
20741
|
-
lines.push("## Aligned flow");
|
|
20742
|
-
lines.push("");
|
|
20743
|
-
const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
|
|
20744
|
-
lines.push(
|
|
20745
|
-
`${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
|
|
20746
|
-
);
|
|
20747
|
-
lines.push("");
|
|
20748
|
-
if (comparison.divergences.length > 0) {
|
|
20749
|
-
lines.push("| Plane | Kind | Component | Divergence |");
|
|
20750
|
-
lines.push("|---|---|---|---|");
|
|
20751
|
-
for (const d of comparison.divergences) {
|
|
20752
|
-
const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
|
|
20753
|
-
lines.push(
|
|
20754
|
-
`| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
|
|
20755
|
-
);
|
|
20756
|
-
}
|
|
20757
|
-
lines.push("");
|
|
20758
|
-
}
|
|
20759
|
-
for (const plane of PLANE_ORDER) {
|
|
20760
|
-
const planeDivergences = comparison.divergences.filter(
|
|
20761
|
-
(d) => d.plane === plane
|
|
20762
|
-
);
|
|
20763
|
-
if (planeDivergences.length === 0) continue;
|
|
20764
|
-
lines.push(`## ${PLANE_LABELS[plane]}`);
|
|
20765
|
-
lines.push("");
|
|
20766
|
-
for (const d of planeDivergences) {
|
|
20767
|
-
lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
|
|
20768
|
-
lines.push("");
|
|
20769
|
-
if (d.sig) lines.push(`- Component: \`${d.sig}\``);
|
|
20770
|
-
if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
|
|
20771
|
-
if (d.table)
|
|
20772
|
-
lines.push(
|
|
20773
|
-
`- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
|
|
20774
|
-
);
|
|
20775
|
-
lines.push("");
|
|
20776
|
-
lines.push("```diff");
|
|
20777
|
-
lines.push(`- before: ${JSON.stringify(d.before)}`);
|
|
20778
|
-
lines.push(`+ after: ${JSON.stringify(d.after)}`);
|
|
20779
|
-
lines.push("```");
|
|
20780
|
-
lines.push("");
|
|
20781
|
-
}
|
|
20782
|
-
}
|
|
20783
|
-
lines.push("---");
|
|
20784
|
-
lines.push("");
|
|
20785
|
-
lines.push(
|
|
20786
|
-
comparison.noise.suppressedCount > 0 ? `*${comparison.noise.suppressedCount} expected-variance difference(s) suppressed by the noise model (${comparison.noise.rules.join(", ")}).*` : "*No differences were suppressed by the noise model.*"
|
|
20787
|
-
);
|
|
20788
|
-
lines.push("");
|
|
20789
|
-
return lines.join("\n");
|
|
20790
|
-
}
|
|
20791
|
-
function formatComparisonSummary(comparison) {
|
|
20792
|
-
const lines = [];
|
|
20793
|
-
lines.push(
|
|
20794
|
-
`crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
|
|
20795
|
-
);
|
|
20796
|
-
lines.push(` Schema: ${comparison.schemaVersion}`);
|
|
20797
|
-
lines.push(
|
|
20798
|
-
` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
|
|
20799
|
-
);
|
|
20800
|
-
lines.push(
|
|
20801
|
-
` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
|
|
20802
|
-
);
|
|
20803
|
-
lines.push(` Divergences: ${comparison.divergences.length}`);
|
|
20804
|
-
for (const d of comparison.divergences.slice(0, 5)) {
|
|
20805
|
-
lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
|
|
20806
|
-
}
|
|
20807
|
-
if (comparison.divergences.length > 5) {
|
|
20808
|
-
lines.push(
|
|
20809
|
-
` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
|
|
20810
|
-
);
|
|
20811
|
-
}
|
|
20812
|
-
const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
|
|
20813
|
-
lines.push(
|
|
20814
|
-
` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
|
|
20815
|
-
);
|
|
20816
|
-
return lines.join("\n");
|
|
20817
|
-
}
|
|
20818
|
-
function escapeCell(text3) {
|
|
20819
|
-
return text3.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
20820
|
-
}
|
|
20821
|
-
function formatSessionRef(ref) {
|
|
20822
|
-
const tags = [
|
|
20823
|
-
ref.release ? `release ${ref.release}` : void 0,
|
|
20824
|
-
ref.build ? `build ${ref.build}` : void 0
|
|
20825
|
-
].filter(Boolean);
|
|
20826
|
-
return tags.length > 0 ? `${ref.sessionId} (${tags.join(", ")})` : ref.sessionId;
|
|
20827
|
-
}
|
|
20828
|
-
|
|
20829
21158
|
// src/ticket/comment.ts
|
|
20830
21159
|
var REASON_PHRASES = {
|
|
20831
21160
|
semantic: "wording overlap with the captured incident",
|
|
@@ -21276,6 +21605,7 @@ function bounded(value, max) {
|
|
|
21276
21605
|
cloudWatchEvidenceProvider,
|
|
21277
21606
|
cloudflareEvidenceProvider,
|
|
21278
21607
|
compareSessions,
|
|
21608
|
+
comparisonTitle,
|
|
21279
21609
|
createCrumbtrailExpressErrorMiddleware,
|
|
21280
21610
|
createCrumbtrailExpressMiddleware,
|
|
21281
21611
|
createServer,
|
|
@@ -21325,6 +21655,7 @@ function bounded(value, max) {
|
|
|
21325
21655
|
renderProviderReadme,
|
|
21326
21656
|
resolveDbRequestContext,
|
|
21327
21657
|
sentryEvidenceProvider,
|
|
21658
|
+
sessionRefLabel,
|
|
21328
21659
|
signSigV4,
|
|
21329
21660
|
splunkEvidenceProvider,
|
|
21330
21661
|
splunkSearchDeepLink,
|