crumbtrail-node 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-EASSXKUJ.js → chunk-5HPWIOG3.js} +303 -121
- package/dist/cli.cjs +302 -121
- package/dist/cli.js +2 -1
- package/dist/index.cjs +305 -119
- 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,
|
|
@@ -3081,6 +3083,7 @@ var SentryError = class extends Error {
|
|
|
3081
3083
|
this.status = status;
|
|
3082
3084
|
this.name = "SentryError";
|
|
3083
3085
|
}
|
|
3086
|
+
status;
|
|
3084
3087
|
};
|
|
3085
3088
|
function sanitizeUrl2(u) {
|
|
3086
3089
|
try {
|
|
@@ -3494,6 +3497,8 @@ var CloudWatchError = class extends Error {
|
|
|
3494
3497
|
this.target = target;
|
|
3495
3498
|
this.name = "CloudWatchError";
|
|
3496
3499
|
}
|
|
3500
|
+
status;
|
|
3501
|
+
target;
|
|
3497
3502
|
};
|
|
3498
3503
|
function isTransient2(error) {
|
|
3499
3504
|
if (error instanceof CloudWatchError) {
|
|
@@ -3906,6 +3911,7 @@ var SplunkError = class extends Error {
|
|
|
3906
3911
|
this.status = status;
|
|
3907
3912
|
this.name = "SplunkError";
|
|
3908
3913
|
}
|
|
3914
|
+
status;
|
|
3909
3915
|
};
|
|
3910
3916
|
function sanitizeUrl3(u) {
|
|
3911
3917
|
try {
|
|
@@ -4256,6 +4262,7 @@ var DatadogError = class extends Error {
|
|
|
4256
4262
|
this.status = status;
|
|
4257
4263
|
this.name = "DatadogError";
|
|
4258
4264
|
}
|
|
4265
|
+
status;
|
|
4259
4266
|
};
|
|
4260
4267
|
function sanitizeUrl4(u) {
|
|
4261
4268
|
try {
|
|
@@ -4619,6 +4626,7 @@ var PostHogError = class extends Error {
|
|
|
4619
4626
|
this.status = status;
|
|
4620
4627
|
this.name = "PostHogError";
|
|
4621
4628
|
}
|
|
4629
|
+
status;
|
|
4622
4630
|
};
|
|
4623
4631
|
function sanitizeUrl5(u) {
|
|
4624
4632
|
try {
|
|
@@ -5009,6 +5017,8 @@ var CloudflareError = class extends Error {
|
|
|
5009
5017
|
this.op = op;
|
|
5010
5018
|
this.name = "CloudflareError";
|
|
5011
5019
|
}
|
|
5020
|
+
status;
|
|
5021
|
+
op;
|
|
5012
5022
|
};
|
|
5013
5023
|
function isTransient6(error) {
|
|
5014
5024
|
if (error instanceof CloudflareError) {
|
|
@@ -11027,6 +11037,8 @@ var AudioProcessingError = class extends Error {
|
|
|
11027
11037
|
this.cause = cause;
|
|
11028
11038
|
this.name = "AudioProcessingError";
|
|
11029
11039
|
}
|
|
11040
|
+
phase;
|
|
11041
|
+
cause;
|
|
11030
11042
|
};
|
|
11031
11043
|
var AudioTranscriptionUnavailableError = class extends Error {
|
|
11032
11044
|
constructor() {
|
|
@@ -12777,12 +12789,12 @@ var import_node_fs10 = __toESM(require("fs"), 1);
|
|
|
12777
12789
|
// package.json
|
|
12778
12790
|
var package_default = {
|
|
12779
12791
|
name: "crumbtrail-node",
|
|
12780
|
-
version: "0.
|
|
12792
|
+
version: "0.2.0",
|
|
12781
12793
|
description: "Local Crumbtrail server, MCP evidence tools, Express middleware, and CLI for self-hosted debugging sessions.",
|
|
12782
12794
|
license: "MIT",
|
|
12783
12795
|
repository: {
|
|
12784
12796
|
type: "git",
|
|
12785
|
-
url: "https://github.com/
|
|
12797
|
+
url: "git+https://github.com/crumbtrail-dev/crumbtrail-js.git",
|
|
12786
12798
|
directory: "packages/node"
|
|
12787
12799
|
},
|
|
12788
12800
|
keywords: [
|
|
@@ -12835,6 +12847,10 @@ var package_default = {
|
|
|
12835
12847
|
"@types/node": "^25.5.0",
|
|
12836
12848
|
express: "^5.2.1",
|
|
12837
12849
|
typescript: "^5.7.0"
|
|
12850
|
+
},
|
|
12851
|
+
homepage: "https://github.com/crumbtrail-dev/crumbtrail-js/tree/main/packages/node#readme",
|
|
12852
|
+
bugs: {
|
|
12853
|
+
url: "https://github.com/crumbtrail-dev/crumbtrail-js/issues"
|
|
12838
12854
|
}
|
|
12839
12855
|
};
|
|
12840
12856
|
|
|
@@ -13485,6 +13501,10 @@ var RequestValidationError = class extends Error {
|
|
|
13485
13501
|
this.retryable = retryable;
|
|
13486
13502
|
this.name = "RequestValidationError";
|
|
13487
13503
|
}
|
|
13504
|
+
status;
|
|
13505
|
+
publicMessage;
|
|
13506
|
+
code;
|
|
13507
|
+
retryable;
|
|
13488
13508
|
};
|
|
13489
13509
|
function serveStatic(res, staticDir, urlPath) {
|
|
13490
13510
|
const resolved = resolveStaticPath(staticDir, urlPath);
|
|
@@ -14214,6 +14234,7 @@ var BoundedTraceSessionCache = class {
|
|
|
14214
14234
|
constructor(maxEntries) {
|
|
14215
14235
|
this.maxEntries = maxEntries;
|
|
14216
14236
|
}
|
|
14237
|
+
maxEntries;
|
|
14217
14238
|
entries = /* @__PURE__ */ new Map();
|
|
14218
14239
|
get(traceId) {
|
|
14219
14240
|
const existing = this.entries.get(traceId);
|
|
@@ -14912,6 +14933,20 @@ function createServer(config) {
|
|
|
14912
14933
|
});
|
|
14913
14934
|
server.on("close", () => sweeper.stop());
|
|
14914
14935
|
}
|
|
14936
|
+
if (config.ai?.enabled && config.ai.backfillOnStart) {
|
|
14937
|
+
const sessionDirs = sessions.listSummaries().map((summary) => sessions.getExistingSessionDir(summary.id)).filter((dir) => dir !== void 0);
|
|
14938
|
+
const timer = setTimeout(() => {
|
|
14939
|
+
void backfillAiDiagnoses(
|
|
14940
|
+
sessionDirs,
|
|
14941
|
+
config.ai
|
|
14942
|
+
).then(
|
|
14943
|
+
(result) => config.ai?.log?.(
|
|
14944
|
+
`Crumbtrail AI diagnosis backfill complete: ${JSON.stringify(result)}`
|
|
14945
|
+
)
|
|
14946
|
+
);
|
|
14947
|
+
}, 0);
|
|
14948
|
+
server.on("close", () => clearTimeout(timer));
|
|
14949
|
+
}
|
|
14915
14950
|
if (fastFinalizer) {
|
|
14916
14951
|
server.on("close", () => fastFinalizer.stop());
|
|
14917
14952
|
}
|
|
@@ -14961,6 +14996,7 @@ var FixContextError = class extends Error {
|
|
|
14961
14996
|
this.code = code2;
|
|
14962
14997
|
this.name = "FixContextError";
|
|
14963
14998
|
}
|
|
14999
|
+
code;
|
|
14964
15000
|
};
|
|
14965
15001
|
function buildFixContext(sessionDirOrId, opts = {}) {
|
|
14966
15002
|
const sessionDir = resolveSessionDir(sessionDirOrId, opts);
|
|
@@ -16020,16 +16056,85 @@ function diffEnvironment(a, b, rules, disabled) {
|
|
|
16020
16056
|
addSuppressedRules(rules, beforeRules, afterRules, disabled);
|
|
16021
16057
|
return [];
|
|
16022
16058
|
}
|
|
16059
|
+
const envDelta = buildEnvDiff(a, b, disabled);
|
|
16023
16060
|
return [
|
|
16024
16061
|
{
|
|
16025
16062
|
plane: "env",
|
|
16026
16063
|
kind: "env.snapshot",
|
|
16027
16064
|
before: a,
|
|
16028
16065
|
after: b,
|
|
16029
|
-
brief:
|
|
16066
|
+
brief: envBrief(envDelta),
|
|
16067
|
+
envDelta
|
|
16030
16068
|
}
|
|
16031
16069
|
];
|
|
16032
16070
|
}
|
|
16071
|
+
function buildEnvDiff(a, b, disabled) {
|
|
16072
|
+
const delta = {
|
|
16073
|
+
flags: channelDelta(recordField(a.flags), recordField(b.flags), disabled),
|
|
16074
|
+
config: channelDelta(
|
|
16075
|
+
recordField(a.config),
|
|
16076
|
+
recordField(b.config),
|
|
16077
|
+
disabled
|
|
16078
|
+
)
|
|
16079
|
+
};
|
|
16080
|
+
const releaseBefore = stringOrUndefined(a.release);
|
|
16081
|
+
const releaseAfter = stringOrUndefined(b.release);
|
|
16082
|
+
if (releaseBefore !== releaseAfter) {
|
|
16083
|
+
delta.release = {
|
|
16084
|
+
...releaseBefore !== void 0 ? { before: releaseBefore } : {},
|
|
16085
|
+
...releaseAfter !== void 0 ? { after: releaseAfter } : {}
|
|
16086
|
+
};
|
|
16087
|
+
}
|
|
16088
|
+
const buildBefore = stringOrUndefined(a.build);
|
|
16089
|
+
const buildAfter = stringOrUndefined(b.build);
|
|
16090
|
+
if (buildBefore !== buildAfter) {
|
|
16091
|
+
delta.build = {
|
|
16092
|
+
...buildBefore !== void 0 ? { before: buildBefore } : {},
|
|
16093
|
+
...buildAfter !== void 0 ? { after: buildAfter } : {}
|
|
16094
|
+
};
|
|
16095
|
+
}
|
|
16096
|
+
return delta;
|
|
16097
|
+
}
|
|
16098
|
+
function recordField(value) {
|
|
16099
|
+
return isRecord14(value) ? value : {};
|
|
16100
|
+
}
|
|
16101
|
+
function channelDelta(a, b, disabled) {
|
|
16102
|
+
const added = [];
|
|
16103
|
+
const removed = [];
|
|
16104
|
+
const changed = [];
|
|
16105
|
+
for (const key of Object.keys(a).sort()) {
|
|
16106
|
+
if (!(key in b)) {
|
|
16107
|
+
removed.push({ key, before: a[key] });
|
|
16108
|
+
} else if (envValuesDiffer(a[key], b[key], disabled)) {
|
|
16109
|
+
changed.push({ key, before: a[key], after: b[key] });
|
|
16110
|
+
}
|
|
16111
|
+
}
|
|
16112
|
+
for (const key of Object.keys(b).sort()) {
|
|
16113
|
+
if (!(key in a)) added.push({ key, after: b[key] });
|
|
16114
|
+
}
|
|
16115
|
+
return { added, removed, changed };
|
|
16116
|
+
}
|
|
16117
|
+
function envValuesDiffer(before, after, disabled) {
|
|
16118
|
+
const beforeRules = /* @__PURE__ */ new Set();
|
|
16119
|
+
const afterRules = /* @__PURE__ */ new Set();
|
|
16120
|
+
if (canonicalize(before, beforeRules) !== canonicalize(after, afterRules))
|
|
16121
|
+
return true;
|
|
16122
|
+
const rawDiffers = stableStringify(before) !== stableStringify(after);
|
|
16123
|
+
return rawDiffers && hasDisabledRule(beforeRules, afterRules, disabled);
|
|
16124
|
+
}
|
|
16125
|
+
function envBrief(delta) {
|
|
16126
|
+
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);
|
|
16127
|
+
const parts = [];
|
|
16128
|
+
const flagCount = delta.flags.added.length + delta.flags.removed.length + delta.flags.changed.length;
|
|
16129
|
+
const configCount = delta.config.added.length + delta.config.removed.length + delta.config.changed.length;
|
|
16130
|
+
if (flagCount > 0) parts.push(`${flagCount} flag(s)`);
|
|
16131
|
+
if (configCount > 0) parts.push(`${configCount} config value(s)`);
|
|
16132
|
+
if (delta.release) parts.push("release");
|
|
16133
|
+
if (delta.build) parts.push("build");
|
|
16134
|
+
if (counts === 0 || parts.length === 0)
|
|
16135
|
+
return "feature flag, config, release, or build snapshot changed between sessions";
|
|
16136
|
+
return `environment delta between sessions: ${parts.join(", ")} changed`;
|
|
16137
|
+
}
|
|
16033
16138
|
function confidenceFor(divergences) {
|
|
16034
16139
|
if (divergences.length === 0) return "high";
|
|
16035
16140
|
if (divergences.some(
|
|
@@ -16056,6 +16161,9 @@ async function compareSessions(aDir, bDir, options = {}) {
|
|
|
16056
16161
|
...diffDb(a.dbWrites, b.dbWrites, rules, disabled),
|
|
16057
16162
|
...diffEnvironment(a.environment, b.environment, rules, disabled)
|
|
16058
16163
|
];
|
|
16164
|
+
const envDelta = divergences.find(
|
|
16165
|
+
(divergence) => divergence.kind === "env.snapshot"
|
|
16166
|
+
)?.envDelta;
|
|
16059
16167
|
return {
|
|
16060
16168
|
schemaVersion: SESSION_COMPARE_SCHEMA_VERSION,
|
|
16061
16169
|
verdict: divergences.length > 0 ? "regression" : "clean",
|
|
@@ -16078,20 +16186,187 @@ async function compareSessions(aDir, bDir, options = {}) {
|
|
|
16078
16186
|
divergences,
|
|
16079
16187
|
noise: { suppressedCount: rules.size, rules: [...rules].sort() },
|
|
16080
16188
|
evidence: divergencesToEvidence(divergences),
|
|
16081
|
-
intent: []
|
|
16189
|
+
intent: [],
|
|
16190
|
+
...envDelta ? { envDelta } : {}
|
|
16082
16191
|
};
|
|
16083
16192
|
}
|
|
16084
16193
|
|
|
16085
16194
|
// src/compare/regression-context.ts
|
|
16086
16195
|
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
16087
16196
|
var import_node_path13 = __toESM(require("path"), 1);
|
|
16197
|
+
|
|
16198
|
+
// src/compare/report.ts
|
|
16199
|
+
var PLANE_ORDER = ["flow", "network", "db", "env"];
|
|
16200
|
+
var PLANE_LABELS = {
|
|
16201
|
+
flow: "Flow steps",
|
|
16202
|
+
network: "Network calls",
|
|
16203
|
+
db: "Database rows",
|
|
16204
|
+
env: "Environment and flags"
|
|
16205
|
+
};
|
|
16206
|
+
function renderCompareReport(comparison) {
|
|
16207
|
+
const lines = [];
|
|
16208
|
+
const regressed = comparison.verdict === "regression";
|
|
16209
|
+
lines.push(
|
|
16210
|
+
`# Session comparison - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
|
|
16211
|
+
);
|
|
16212
|
+
lines.push("");
|
|
16213
|
+
lines.push(
|
|
16214
|
+
regressed ? `> **Verdict: REGRESSION** (confidence: ${comparison.confidence}) - recorded behavior changed between these sessions.` : `> **Verdict: CLEAN** (confidence: ${comparison.confidence}) - the recorded sessions show no behavioral divergence.`
|
|
16215
|
+
);
|
|
16216
|
+
lines.push("");
|
|
16217
|
+
lines.push(
|
|
16218
|
+
"A is the baseline session; B is the candidate release/build. Every row below is grounded in the recorded evidence of both sessions."
|
|
16219
|
+
);
|
|
16220
|
+
lines.push("");
|
|
16221
|
+
lines.push("## Aligned flow");
|
|
16222
|
+
lines.push("");
|
|
16223
|
+
const { matchedSteps, unmatchedA, unmatchedB } = comparison.alignment;
|
|
16224
|
+
lines.push(
|
|
16225
|
+
`${matchedSteps} step(s) matched by component identity \xB7 ${unmatchedA} only in A \xB7 ${unmatchedB} only in B`
|
|
16226
|
+
);
|
|
16227
|
+
lines.push("");
|
|
16228
|
+
if (comparison.divergences.length > 0) {
|
|
16229
|
+
lines.push("| Plane | Kind | Component | Divergence |");
|
|
16230
|
+
lines.push("|---|---|---|---|");
|
|
16231
|
+
for (const d of comparison.divergences) {
|
|
16232
|
+
const component = d.sig ? `\`${escapeCell(d.sig)}\`` : "\u2014";
|
|
16233
|
+
lines.push(
|
|
16234
|
+
`| **${d.plane}** | \`${d.kind}\` | ${component} | ${escapeCell(d.brief)} |`
|
|
16235
|
+
);
|
|
16236
|
+
}
|
|
16237
|
+
lines.push("");
|
|
16238
|
+
}
|
|
16239
|
+
for (const plane of PLANE_ORDER) {
|
|
16240
|
+
const planeDivergences = comparison.divergences.filter(
|
|
16241
|
+
(d) => d.plane === plane
|
|
16242
|
+
);
|
|
16243
|
+
if (planeDivergences.length === 0) continue;
|
|
16244
|
+
lines.push(`## ${PLANE_LABELS[plane]}`);
|
|
16245
|
+
lines.push("");
|
|
16246
|
+
for (const d of planeDivergences) {
|
|
16247
|
+
lines.push(`### \`${d.kind}\` - ${escapeCell(d.brief)}`);
|
|
16248
|
+
lines.push("");
|
|
16249
|
+
if (d.sig) lines.push(`- Component: \`${d.sig}\``);
|
|
16250
|
+
if (d.requestId) lines.push(`- Request: \`${d.requestId}\``);
|
|
16251
|
+
if (d.table)
|
|
16252
|
+
lines.push(
|
|
16253
|
+
`- Table: \`${d.table}\`${d.pk ? ` \xB7 pk \`${JSON.stringify(d.pk)}\`` : ""}`
|
|
16254
|
+
);
|
|
16255
|
+
lines.push("");
|
|
16256
|
+
if (d.envDelta) {
|
|
16257
|
+
lines.push(...renderEnvDelta(d.envDelta));
|
|
16258
|
+
} else {
|
|
16259
|
+
lines.push("```diff");
|
|
16260
|
+
lines.push(`- before: ${JSON.stringify(d.before)}`);
|
|
16261
|
+
lines.push(`+ after: ${JSON.stringify(d.after)}`);
|
|
16262
|
+
lines.push("```");
|
|
16263
|
+
}
|
|
16264
|
+
lines.push("");
|
|
16265
|
+
}
|
|
16266
|
+
}
|
|
16267
|
+
lines.push("---");
|
|
16268
|
+
lines.push("");
|
|
16269
|
+
lines.push(
|
|
16270
|
+
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.*"
|
|
16271
|
+
);
|
|
16272
|
+
lines.push("");
|
|
16273
|
+
return lines.join("\n");
|
|
16274
|
+
}
|
|
16275
|
+
function formatComparisonSummary(comparison) {
|
|
16276
|
+
const lines = [];
|
|
16277
|
+
lines.push(
|
|
16278
|
+
`crumbtrail-server compare - ${formatSessionRef(comparison.a)} vs ${formatSessionRef(comparison.b)}`
|
|
16279
|
+
);
|
|
16280
|
+
lines.push(` Schema: ${comparison.schemaVersion}`);
|
|
16281
|
+
lines.push(
|
|
16282
|
+
` Verdict: ${comparison.verdict.toUpperCase()} (confidence ${comparison.confidence})`
|
|
16283
|
+
);
|
|
16284
|
+
lines.push(
|
|
16285
|
+
` Alignment: ${comparison.alignment.matchedSteps} matched \xB7 ${comparison.alignment.unmatchedA} only in A \xB7 ${comparison.alignment.unmatchedB} only in B`
|
|
16286
|
+
);
|
|
16287
|
+
lines.push(` Divergences: ${comparison.divergences.length}`);
|
|
16288
|
+
for (const d of comparison.divergences.slice(0, 5)) {
|
|
16289
|
+
lines.push(` [${d.plane}] ${d.kind} - ${d.brief}`);
|
|
16290
|
+
}
|
|
16291
|
+
if (comparison.divergences.length > 5) {
|
|
16292
|
+
lines.push(
|
|
16293
|
+
` ... and ${comparison.divergences.length - 5} more (use --json or --report for the complete list)`
|
|
16294
|
+
);
|
|
16295
|
+
}
|
|
16296
|
+
const rules = comparison.noise.rules.length > 0 ? ` (${comparison.noise.rules.join(", ")})` : "";
|
|
16297
|
+
lines.push(
|
|
16298
|
+
` Noise: ${comparison.noise.suppressedCount} suppressed${rules}`
|
|
16299
|
+
);
|
|
16300
|
+
return lines.join("\n");
|
|
16301
|
+
}
|
|
16302
|
+
function escapeCell(text3) {
|
|
16303
|
+
return text3.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
|
|
16304
|
+
}
|
|
16305
|
+
function sessionRefLabel(ref) {
|
|
16306
|
+
return ref.release ?? ref.sessionId;
|
|
16307
|
+
}
|
|
16308
|
+
function comparisonTitle(comparison) {
|
|
16309
|
+
return `${sessionRefLabel(comparison.a)} vs ${sessionRefLabel(comparison.b)}`;
|
|
16310
|
+
}
|
|
16311
|
+
function renderEnvDelta(delta) {
|
|
16312
|
+
const lines = ["```diff"];
|
|
16313
|
+
renderEnvChannel(lines, "flags", delta.flags);
|
|
16314
|
+
renderEnvChannel(lines, "config", delta.config);
|
|
16315
|
+
if (delta.release) {
|
|
16316
|
+
lines.push(
|
|
16317
|
+
`~ release: ${JSON.stringify(delta.release.before ?? null)} -> ${JSON.stringify(delta.release.after ?? null)}`
|
|
16318
|
+
);
|
|
16319
|
+
}
|
|
16320
|
+
if (delta.build) {
|
|
16321
|
+
lines.push(
|
|
16322
|
+
`~ build: ${JSON.stringify(delta.build.before ?? null)} -> ${JSON.stringify(delta.build.after ?? null)}`
|
|
16323
|
+
);
|
|
16324
|
+
}
|
|
16325
|
+
if (lines.length === 1) lines.push(" (no field-level changes)");
|
|
16326
|
+
lines.push("```");
|
|
16327
|
+
return lines;
|
|
16328
|
+
}
|
|
16329
|
+
function renderEnvChannel(lines, channel, delta) {
|
|
16330
|
+
for (const change of delta.added) {
|
|
16331
|
+
lines.push(
|
|
16332
|
+
`+ ${channel}.${change.key}: ${JSON.stringify(change.after ?? null)}`
|
|
16333
|
+
);
|
|
16334
|
+
}
|
|
16335
|
+
for (const change of delta.removed) {
|
|
16336
|
+
lines.push(
|
|
16337
|
+
`- ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)}`
|
|
16338
|
+
);
|
|
16339
|
+
}
|
|
16340
|
+
for (const change of delta.changed) {
|
|
16341
|
+
lines.push(
|
|
16342
|
+
`~ ${channel}.${change.key}: ${JSON.stringify(change.before ?? null)} -> ${JSON.stringify(change.after ?? null)}`
|
|
16343
|
+
);
|
|
16344
|
+
}
|
|
16345
|
+
}
|
|
16346
|
+
function formatSessionRef(ref) {
|
|
16347
|
+
if (ref.release) {
|
|
16348
|
+
const detail = [
|
|
16349
|
+
`session ${ref.sessionId}`,
|
|
16350
|
+
ref.build ? `build ${ref.build}` : void 0
|
|
16351
|
+
].filter(Boolean);
|
|
16352
|
+
return `${ref.release} (${detail.join(", ")})`;
|
|
16353
|
+
}
|
|
16354
|
+
return ref.build ? `${ref.sessionId} (build ${ref.build})` : ref.sessionId;
|
|
16355
|
+
}
|
|
16356
|
+
|
|
16357
|
+
// src/compare/regression-context.ts
|
|
16088
16358
|
var REGRESSION_CONTEXT_SCHEMA_VERSION = "regression-context.v1";
|
|
16089
16359
|
function buildRegressionContext(comparison, bDir) {
|
|
16090
|
-
const requestIds = unique(
|
|
16091
|
-
|
|
16360
|
+
const requestIds = unique(
|
|
16361
|
+
comparison.divergences.map((d) => d.requestId).filter(isString)
|
|
16362
|
+
);
|
|
16363
|
+
const times = requestIds.flatMap(
|
|
16364
|
+
(requestId) => requestTimesFromIndex(bDir, requestId)
|
|
16365
|
+
);
|
|
16092
16366
|
const firstSig = comparison.divergences.map((d) => d.sig).find(isString);
|
|
16093
16367
|
return {
|
|
16094
16368
|
schemaVersion: REGRESSION_CONTEXT_SCHEMA_VERSION,
|
|
16369
|
+
title: comparisonTitle(comparison),
|
|
16095
16370
|
comparison,
|
|
16096
16371
|
divergent_interaction: firstSig ? interactionForSig(bDir, firstSig) : null,
|
|
16097
16372
|
causal_window: requestIds.length > 0 ? {
|
|
@@ -16109,6 +16384,7 @@ function buildRegressionContext(comparison, bDir) {
|
|
|
16109
16384
|
before: d.before,
|
|
16110
16385
|
after: d.after
|
|
16111
16386
|
})),
|
|
16387
|
+
env_delta: comparison.envDelta ?? null,
|
|
16112
16388
|
repro_hint: reproHint(bDir)
|
|
16113
16389
|
};
|
|
16114
16390
|
}
|
|
@@ -16153,7 +16429,8 @@ function collectRefTimes(value, times) {
|
|
|
16153
16429
|
const start = isRecord15(value.start) ? value.start : void 0;
|
|
16154
16430
|
const end = isRecord15(value.end) ? value.end : void 0;
|
|
16155
16431
|
for (const candidate of [ref, start, end]) {
|
|
16156
|
-
if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
|
|
16432
|
+
if (typeof candidate?.t === "number" && Number.isFinite(candidate.t))
|
|
16433
|
+
times.push(candidate.t);
|
|
16157
16434
|
}
|
|
16158
16435
|
}
|
|
16159
16436
|
function reproHint(sessionDir) {
|
|
@@ -17078,12 +17355,29 @@ var McpServer = class {
|
|
|
17078
17355
|
"sha"
|
|
17079
17356
|
]))
|
|
17080
17357
|
continue;
|
|
17081
|
-
sessions.push(meta);
|
|
17358
|
+
sessions.push(this.withReleaseBuild(meta));
|
|
17082
17359
|
} catch {
|
|
17083
17360
|
}
|
|
17084
17361
|
}
|
|
17085
17362
|
return textResult(sessions);
|
|
17086
17363
|
}
|
|
17364
|
+
/**
|
|
17365
|
+
* Surfaces release/build as first-class list-row fields regardless of which
|
|
17366
|
+
* alias the app used (release/releaseId/version, build/buildId/commit/sha), so
|
|
17367
|
+
* an agent can label and group sessions by release without re-reading each
|
|
17368
|
+
* meta. Additive: the raw meta keys are preserved.
|
|
17369
|
+
*/
|
|
17370
|
+
withReleaseBuild(meta) {
|
|
17371
|
+
const release = stringField5(meta.release ?? meta.releaseId ?? meta.version);
|
|
17372
|
+
const build = stringField5(
|
|
17373
|
+
meta.build ?? meta.buildId ?? meta.commit ?? meta.sha
|
|
17374
|
+
);
|
|
17375
|
+
return {
|
|
17376
|
+
...meta,
|
|
17377
|
+
...release !== void 0 ? { release } : {},
|
|
17378
|
+
...build !== void 0 ? { build } : {}
|
|
17379
|
+
};
|
|
17380
|
+
}
|
|
17087
17381
|
sessionMetadataMatches(meta, expected, keys) {
|
|
17088
17382
|
return keys.some((key) => meta[key] === expected);
|
|
17089
17383
|
}
|
|
@@ -20089,6 +20383,7 @@ var InspectError = class extends Error {
|
|
|
20089
20383
|
this.code = code2;
|
|
20090
20384
|
this.name = "InspectError";
|
|
20091
20385
|
}
|
|
20386
|
+
code;
|
|
20092
20387
|
};
|
|
20093
20388
|
function inspectSession(sessionDirOrId, opts = {}) {
|
|
20094
20389
|
const sessionDir = resolveSessionDir2(sessionDirOrId, opts);
|
|
@@ -20715,117 +21010,6 @@ function isRecord21(value) {
|
|
|
20715
21010
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
20716
21011
|
}
|
|
20717
21012
|
|
|
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
21013
|
// src/ticket/comment.ts
|
|
20830
21014
|
var REASON_PHRASES = {
|
|
20831
21015
|
semantic: "wording overlap with the captured incident",
|
|
@@ -21276,6 +21460,7 @@ function bounded(value, max) {
|
|
|
21276
21460
|
cloudWatchEvidenceProvider,
|
|
21277
21461
|
cloudflareEvidenceProvider,
|
|
21278
21462
|
compareSessions,
|
|
21463
|
+
comparisonTitle,
|
|
21279
21464
|
createCrumbtrailExpressErrorMiddleware,
|
|
21280
21465
|
createCrumbtrailExpressMiddleware,
|
|
21281
21466
|
createServer,
|
|
@@ -21325,6 +21510,7 @@ function bounded(value, max) {
|
|
|
21325
21510
|
renderProviderReadme,
|
|
21326
21511
|
resolveDbRequestContext,
|
|
21327
21512
|
sentryEvidenceProvider,
|
|
21513
|
+
sessionRefLabel,
|
|
21328
21514
|
signSigV4,
|
|
21329
21515
|
splunkEvidenceProvider,
|
|
21330
21516
|
splunkSearchDeepLink,
|