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