@neat.is/core 0.9.2-dev.20260822 → 0.9.3-dev.20260823
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-RQQUI3NQ.js → chunk-LRYPKC3B.js} +745 -50
- package/dist/chunk-LRYPKC3B.js.map +1 -0
- package/dist/{chunk-L4SZIIER.js → chunk-WJGZYEUG.js} +2 -2
- package/dist/cli.cjs +1203 -283
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +333 -110
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +788 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +788 -92
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +787 -91
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-RQQUI3NQ.js.map +0 -1
- /package/dist/{chunk-L4SZIIER.js.map → chunk-WJGZYEUG.js.map} +0 -0
|
@@ -3096,11 +3096,11 @@ function actixAttributeRoute(attrItem, out) {
|
|
|
3096
3096
|
if (!nameNode) return;
|
|
3097
3097
|
const macro = nameNode.type === "identifier" ? nameNode.text : nameNode.type === "scoped_identifier" ? nameNode.childForFieldName("name")?.text ?? null : null;
|
|
3098
3098
|
if (!macro) return;
|
|
3099
|
-
const
|
|
3100
|
-
if (!
|
|
3099
|
+
const tokens2 = attr.childForFieldName("arguments");
|
|
3100
|
+
if (!tokens2 || tokens2.type !== "token_tree") return;
|
|
3101
3101
|
const strings = [];
|
|
3102
|
-
for (let i = 0; i <
|
|
3103
|
-
const s = rustStringContent(
|
|
3102
|
+
for (let i = 0; i < tokens2.namedChildCount; i++) {
|
|
3103
|
+
const s = rustStringContent(tokens2.namedChild(i));
|
|
3104
3104
|
if (s !== null) strings.push(s);
|
|
3105
3105
|
}
|
|
3106
3106
|
const pathStr = strings[0];
|
|
@@ -4992,29 +4992,29 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
4992
4992
|
toPromote.push({ frontierId: id, serviceId: target });
|
|
4993
4993
|
});
|
|
4994
4994
|
let promoted = 0;
|
|
4995
|
-
for (const { frontierId: frontierId2, serviceId:
|
|
4995
|
+
for (const { frontierId: frontierId2, serviceId: serviceId16 } of toPromote) {
|
|
4996
4996
|
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
4997
4997
|
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
4998
4998
|
if (!gate.allowed) {
|
|
4999
4999
|
continue;
|
|
5000
5000
|
}
|
|
5001
5001
|
}
|
|
5002
|
-
rewireFrontierEdges(graph, frontierId2,
|
|
5002
|
+
rewireFrontierEdges(graph, frontierId2, serviceId16);
|
|
5003
5003
|
graph.dropNode(frontierId2);
|
|
5004
5004
|
promoted++;
|
|
5005
5005
|
}
|
|
5006
5006
|
return promoted;
|
|
5007
5007
|
}
|
|
5008
|
-
function rewireFrontierEdges(graph, frontierId2,
|
|
5008
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId16) {
|
|
5009
5009
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
5010
5010
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
5011
5011
|
for (const edgeId of inbound) {
|
|
5012
5012
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5013
|
-
rebuildEdge(graph, edge, edge.source,
|
|
5013
|
+
rebuildEdge(graph, edge, edge.source, serviceId16, edgeId);
|
|
5014
5014
|
}
|
|
5015
5015
|
for (const edgeId of outbound) {
|
|
5016
5016
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5017
|
-
rebuildEdge(graph, edge,
|
|
5017
|
+
rebuildEdge(graph, edge, serviceId16, edge.target, edgeId);
|
|
5018
5018
|
}
|
|
5019
5019
|
}
|
|
5020
5020
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
@@ -5373,7 +5373,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
|
|
|
5373
5373
|
for (const id of walk10.path) {
|
|
5374
5374
|
const owner = resolveOwningService(graph, id);
|
|
5375
5375
|
if (!owner) continue;
|
|
5376
|
-
const { id:
|
|
5376
|
+
const { id: serviceId16, svc } = owner;
|
|
5377
5377
|
const deps = svc.dependencies ?? {};
|
|
5378
5378
|
for (const pair of candidatePairs) {
|
|
5379
5379
|
const declared = deps[pair.driver];
|
|
@@ -5386,7 +5386,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
|
|
|
5386
5386
|
);
|
|
5387
5387
|
if (!result.compatible) {
|
|
5388
5388
|
return {
|
|
5389
|
-
rootCauseNode:
|
|
5389
|
+
rootCauseNode: serviceId16,
|
|
5390
5390
|
rootCauseReason: result.reason ?? "incompatible driver",
|
|
5391
5391
|
...result.minDriverVersion ? {
|
|
5392
5392
|
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
@@ -5401,7 +5401,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
|
|
|
5401
5401
|
for (const id of walk10.path) {
|
|
5402
5402
|
const owner = resolveOwningService(graph, id);
|
|
5403
5403
|
if (!owner) continue;
|
|
5404
|
-
const { id:
|
|
5404
|
+
const { id: serviceId16, svc } = owner;
|
|
5405
5405
|
const deps = svc.dependencies ?? {};
|
|
5406
5406
|
const serviceNodeEngine = svc.nodeEngine;
|
|
5407
5407
|
for (const constraint of nodeEngineConstraints()) {
|
|
@@ -5410,7 +5410,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
|
|
|
5410
5410
|
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
5411
5411
|
if (!result.compatible && result.reason) {
|
|
5412
5412
|
return {
|
|
5413
|
-
rootCauseNode:
|
|
5413
|
+
rootCauseNode: serviceId16,
|
|
5414
5414
|
rootCauseReason: result.reason,
|
|
5415
5415
|
...result.requiredNodeVersion ? {
|
|
5416
5416
|
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
@@ -5425,7 +5425,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
|
|
|
5425
5425
|
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
5426
5426
|
if (!result.compatible && result.reason) {
|
|
5427
5427
|
return {
|
|
5428
|
-
rootCauseNode:
|
|
5428
|
+
rootCauseNode: serviceId16,
|
|
5429
5429
|
rootCauseReason: result.reason,
|
|
5430
5430
|
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
5431
5431
|
};
|
|
@@ -5530,9 +5530,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
|
5530
5530
|
function isFailingCallEdge(e) {
|
|
5531
5531
|
return e.type === EdgeType6.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
5532
5532
|
}
|
|
5533
|
-
function callSourcesForService(graph,
|
|
5534
|
-
const ids = [
|
|
5535
|
-
for (const edgeId of graph.outboundEdges(
|
|
5533
|
+
function callSourcesForService(graph, serviceId16) {
|
|
5534
|
+
const ids = [serviceId16];
|
|
5535
|
+
for (const edgeId of graph.outboundEdges(serviceId16)) {
|
|
5536
5536
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5537
5537
|
if (e.type !== EdgeType6.CONTAINS) continue;
|
|
5538
5538
|
const tgt = graph.getNodeAttributes(e.target);
|
|
@@ -5556,9 +5556,9 @@ function failingCallDominates(e, id, curEdge, curId) {
|
|
|
5556
5556
|
}
|
|
5557
5557
|
return id < curId;
|
|
5558
5558
|
}
|
|
5559
|
-
function dominantFailingCall(graph,
|
|
5559
|
+
function dominantFailingCall(graph, serviceId16, visited) {
|
|
5560
5560
|
let best = null;
|
|
5561
|
-
for (const src of callSourcesForService(graph,
|
|
5561
|
+
for (const src of callSourcesForService(graph, serviceId16)) {
|
|
5562
5562
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
5563
5563
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5564
5564
|
if (!isFailingCallEdge(e)) continue;
|
|
@@ -5597,9 +5597,9 @@ function staleCallDominates(e, id, curEdge, curId) {
|
|
|
5597
5597
|
if (ev !== cv) return ev > cv;
|
|
5598
5598
|
return id < curId;
|
|
5599
5599
|
}
|
|
5600
|
-
function dominantStaleCall(graph,
|
|
5600
|
+
function dominantStaleCall(graph, serviceId16, visited) {
|
|
5601
5601
|
const bestByCallee = /* @__PURE__ */ new Map();
|
|
5602
|
-
for (const src of callSourcesForService(graph,
|
|
5602
|
+
for (const src of callSourcesForService(graph, serviceId16)) {
|
|
5603
5603
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
5604
5604
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5605
5605
|
if (e.type !== EdgeType6.CALLS) continue;
|
|
@@ -7059,9 +7059,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
7059
7059
|
"StatefulSet",
|
|
7060
7060
|
"DaemonSet"
|
|
7061
7061
|
]);
|
|
7062
|
-
function addAliases(graph,
|
|
7063
|
-
if (!graph.hasNode(
|
|
7064
|
-
const node = graph.getNodeAttributes(
|
|
7062
|
+
function addAliases(graph, serviceId16, candidates) {
|
|
7063
|
+
if (!graph.hasNode(serviceId16)) return;
|
|
7064
|
+
const node = graph.getNodeAttributes(serviceId16);
|
|
7065
7065
|
if (node.type !== NodeType16.ServiceNode) return;
|
|
7066
7066
|
const set = new Set(node.aliases ?? []);
|
|
7067
7067
|
for (const c of candidates) {
|
|
@@ -7071,7 +7071,7 @@ function addAliases(graph, serviceId15, candidates) {
|
|
|
7071
7071
|
}
|
|
7072
7072
|
if (set.size === 0) return;
|
|
7073
7073
|
const updated = { ...node, aliases: [...set].sort() };
|
|
7074
|
-
graph.replaceNodeAttributes(
|
|
7074
|
+
graph.replaceNodeAttributes(serviceId16, updated);
|
|
7075
7075
|
}
|
|
7076
7076
|
function indexServicesByName(services) {
|
|
7077
7077
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -7104,12 +7104,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
7104
7104
|
}
|
|
7105
7105
|
if (!compose?.services) return;
|
|
7106
7106
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
7107
|
-
const
|
|
7108
|
-
if (!
|
|
7107
|
+
const serviceId16 = serviceIndex.get(composeName);
|
|
7108
|
+
if (!serviceId16) continue;
|
|
7109
7109
|
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
7110
7110
|
if (svc.container_name) aliases.add(svc.container_name);
|
|
7111
7111
|
if (svc.hostname) aliases.add(svc.hostname);
|
|
7112
|
-
addAliases(graph,
|
|
7112
|
+
addAliases(graph, serviceId16, aliases);
|
|
7113
7113
|
}
|
|
7114
7114
|
}
|
|
7115
7115
|
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -9112,12 +9112,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
|
|
|
9112
9112
|
const value = await interpolateEnvRefs(raw, serviceDir);
|
|
9113
9113
|
if (!looksLike(value)) continue;
|
|
9114
9114
|
const parsed = parse11(value);
|
|
9115
|
-
if (parsed) out.push(parsed);
|
|
9115
|
+
if (parsed) out.push({ ...parsed, hostSource: "config" });
|
|
9116
9116
|
}
|
|
9117
9117
|
for (const lit of literals) {
|
|
9118
9118
|
if (!looksLike(lit)) continue;
|
|
9119
9119
|
const parsed = parse11(lit);
|
|
9120
|
-
if (parsed) out.push(parsed);
|
|
9120
|
+
if (parsed) out.push({ ...parsed, hostSource: "literal" });
|
|
9121
9121
|
}
|
|
9122
9122
|
return out;
|
|
9123
9123
|
}
|
|
@@ -9385,7 +9385,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
9385
9385
|
type: EdgeType10.CONNECTS_TO,
|
|
9386
9386
|
provenance: Provenance10.EXTRACTED,
|
|
9387
9387
|
confidence: confidenceForExtracted7("structural"),
|
|
9388
|
-
|
|
9388
|
+
// Carry how the host was recovered (ADR-213) so the divergence ranker
|
|
9389
|
+
// can tell a real declared store from a hardcoded fault-injection /
|
|
9390
|
+
// flag-gated probe. Only set when the parser distinguished the two.
|
|
9391
|
+
evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
|
|
9389
9392
|
};
|
|
9390
9393
|
if (!graph.hasEdge(edge.id)) {
|
|
9391
9394
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -15195,6 +15198,53 @@ function detectColumnDrift(node) {
|
|
|
15195
15198
|
function involvesNode(d, nodeId) {
|
|
15196
15199
|
return d.source === nodeId || d.target === nodeId;
|
|
15197
15200
|
}
|
|
15201
|
+
function datastoreEverObserved(graph, nodeId) {
|
|
15202
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
15203
|
+
const n = graph.getNodeAttributes(nodeId);
|
|
15204
|
+
if (n.type === NodeType30.DatabaseNode) {
|
|
15205
|
+
const via = n.discoveredVia;
|
|
15206
|
+
if (via === "otel" || via === "merged") return true;
|
|
15207
|
+
}
|
|
15208
|
+
for (const edgeId of graph.inboundEdges(nodeId)) {
|
|
15209
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
15210
|
+
if (e.provenance === Provenance24.OBSERVED || e.provenance === Provenance24.STALE) return true;
|
|
15211
|
+
}
|
|
15212
|
+
return false;
|
|
15213
|
+
}
|
|
15214
|
+
function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
|
|
15215
|
+
for (const bucket of buckets2.values()) {
|
|
15216
|
+
if (bucket.type !== EdgeType25.CONNECTS_TO) continue;
|
|
15217
|
+
if (bucket.source !== serviceId16) continue;
|
|
15218
|
+
if (bucket.target === excludeTarget) continue;
|
|
15219
|
+
if (!graph.hasNode(bucket.target)) continue;
|
|
15220
|
+
const target = graph.getNodeAttributes(bucket.target);
|
|
15221
|
+
if (target.type !== NodeType30.DatabaseNode) continue;
|
|
15222
|
+
if (target.engine !== engine) continue;
|
|
15223
|
+
if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
|
|
15224
|
+
}
|
|
15225
|
+
return false;
|
|
15226
|
+
}
|
|
15227
|
+
var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
|
|
15228
|
+
function dampenDeadCodeProbes(graph, buckets2, all) {
|
|
15229
|
+
return all.map((d) => {
|
|
15230
|
+
if (d.type !== "missing-observed") return d;
|
|
15231
|
+
if (!d.extracted || d.edgeType !== EdgeType25.CONNECTS_TO) return d;
|
|
15232
|
+
if (!graph.hasNode(d.target)) return d;
|
|
15233
|
+
const target = graph.getNodeAttributes(d.target);
|
|
15234
|
+
if (target.type !== NodeType30.DatabaseNode) return d;
|
|
15235
|
+
if (d.extracted.evidence?.hostSource !== "literal") return d;
|
|
15236
|
+
if (datastoreEverObserved(graph, d.target)) return d;
|
|
15237
|
+
const engine = target.engine;
|
|
15238
|
+
if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
|
|
15239
|
+
const host = target.host ?? target.name;
|
|
15240
|
+
return {
|
|
15241
|
+
...d,
|
|
15242
|
+
confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
|
|
15243
|
+
reason: `${d.source} declares a ${engine} connection to a hardcoded-literal host (${host}) that production has never observed, while it does observe another ${engine} store \u2014 this reads as a flag-gated or dead-code declaration (e.g. a fault-injection probe), not a real declared-vs-observed gap.`,
|
|
15244
|
+
recommendation: "Confirm this is an intentional dead alternate \u2014 a fault-injection probe or a flag-gated branch. If it is meant to run in production, check the feature flag or conditional that gates it; if not, it can be ignored or removed."
|
|
15245
|
+
};
|
|
15246
|
+
});
|
|
15247
|
+
}
|
|
15198
15248
|
function suppressHostMismatchHalves(all) {
|
|
15199
15249
|
const observedHalf = /* @__PURE__ */ new Set();
|
|
15200
15250
|
const declaredHalf = /* @__PURE__ */ new Set();
|
|
@@ -15231,7 +15281,8 @@ function computeDivergences(graph, opts = {}) {
|
|
|
15231
15281
|
}
|
|
15232
15282
|
});
|
|
15233
15283
|
const reconciled = suppressHostMismatchHalves(all);
|
|
15234
|
-
|
|
15284
|
+
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
15285
|
+
let filtered = dampened;
|
|
15235
15286
|
if (opts.type) {
|
|
15236
15287
|
const allowed = opts.type;
|
|
15237
15288
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -16331,6 +16382,638 @@ function queryLogEntries(opts) {
|
|
|
16331
16382
|
return merged;
|
|
16332
16383
|
}
|
|
16333
16384
|
|
|
16385
|
+
// src/ask.ts
|
|
16386
|
+
import { AskResultSchema, NodeType as NodeType32, Provenance as Provenance26, serviceId as serviceId14 } from "@neat.is/types";
|
|
16387
|
+
var DEFAULT_MAX_NODES = 3;
|
|
16388
|
+
var MAX_FACTS_PER_SECTION = 6;
|
|
16389
|
+
var INTENT_RULES = [
|
|
16390
|
+
{
|
|
16391
|
+
intent: "root-cause",
|
|
16392
|
+
test: /\b(why|root[\s-]?cause|failing|fail(?:s|ed)?|breaking|broke(?:n)?|crash(?:ing|ed)?|throw(?:ing|s)?|culprit|5\d\d)\b/
|
|
16393
|
+
},
|
|
16394
|
+
{
|
|
16395
|
+
intent: "blast-radius",
|
|
16396
|
+
test: /\b(blast|break[\s-]?if|breaks[\s-]?if|impact|downstream|dependents?|redeploy|who\s+(?:uses|calls|depends)|what\s+depends\s+on|affect(?:s|ed)?)\b/
|
|
16397
|
+
},
|
|
16398
|
+
{
|
|
16399
|
+
intent: "divergence",
|
|
16400
|
+
test: /\b(diverg\w*|weird|mismatch|drift|out\s+of\s+sync|inconsistent|disagree\w*|declared\s+vs|anything\s+wrong)\b/
|
|
16401
|
+
},
|
|
16402
|
+
{
|
|
16403
|
+
intent: "incidents",
|
|
16404
|
+
test: /\b(incidents?|recent\s+(?:errors?|failures?)|error\s+history|failure\s+history)\b/
|
|
16405
|
+
},
|
|
16406
|
+
{
|
|
16407
|
+
intent: "observed",
|
|
16408
|
+
test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually\s+call\w*|really\s+call\w*|observed|runtime\s+traffic)\b/
|
|
16409
|
+
},
|
|
16410
|
+
{
|
|
16411
|
+
intent: "dependencies",
|
|
16412
|
+
test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?)\b/
|
|
16413
|
+
}
|
|
16414
|
+
];
|
|
16415
|
+
function classifyIntent(question) {
|
|
16416
|
+
const q = question.toLowerCase();
|
|
16417
|
+
for (const rule of INTENT_RULES) {
|
|
16418
|
+
if (rule.test.test(q)) return rule.intent;
|
|
16419
|
+
}
|
|
16420
|
+
return "overview";
|
|
16421
|
+
}
|
|
16422
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
16423
|
+
"the",
|
|
16424
|
+
"a",
|
|
16425
|
+
"an",
|
|
16426
|
+
"is",
|
|
16427
|
+
"are",
|
|
16428
|
+
"was",
|
|
16429
|
+
"were",
|
|
16430
|
+
"be",
|
|
16431
|
+
"been",
|
|
16432
|
+
"being",
|
|
16433
|
+
"of",
|
|
16434
|
+
"to",
|
|
16435
|
+
"in",
|
|
16436
|
+
"on",
|
|
16437
|
+
"at",
|
|
16438
|
+
"by",
|
|
16439
|
+
"for",
|
|
16440
|
+
"with",
|
|
16441
|
+
"and",
|
|
16442
|
+
"or",
|
|
16443
|
+
"but",
|
|
16444
|
+
"if",
|
|
16445
|
+
"as",
|
|
16446
|
+
"this",
|
|
16447
|
+
"that",
|
|
16448
|
+
"these",
|
|
16449
|
+
"those",
|
|
16450
|
+
"it",
|
|
16451
|
+
"its",
|
|
16452
|
+
"do",
|
|
16453
|
+
"does",
|
|
16454
|
+
"did",
|
|
16455
|
+
"has",
|
|
16456
|
+
"have",
|
|
16457
|
+
"had",
|
|
16458
|
+
"what",
|
|
16459
|
+
"which",
|
|
16460
|
+
"who",
|
|
16461
|
+
"whom",
|
|
16462
|
+
"how",
|
|
16463
|
+
"when",
|
|
16464
|
+
"where",
|
|
16465
|
+
"my",
|
|
16466
|
+
"me",
|
|
16467
|
+
"i",
|
|
16468
|
+
"we",
|
|
16469
|
+
"you",
|
|
16470
|
+
"they",
|
|
16471
|
+
"them",
|
|
16472
|
+
"from",
|
|
16473
|
+
"into",
|
|
16474
|
+
"about",
|
|
16475
|
+
"would",
|
|
16476
|
+
"will",
|
|
16477
|
+
"can",
|
|
16478
|
+
"could",
|
|
16479
|
+
"should",
|
|
16480
|
+
"anything",
|
|
16481
|
+
"everything",
|
|
16482
|
+
"something",
|
|
16483
|
+
"all",
|
|
16484
|
+
"any",
|
|
16485
|
+
"some",
|
|
16486
|
+
"no",
|
|
16487
|
+
"not",
|
|
16488
|
+
"so",
|
|
16489
|
+
"up",
|
|
16490
|
+
"out",
|
|
16491
|
+
"over",
|
|
16492
|
+
"get",
|
|
16493
|
+
"got",
|
|
16494
|
+
"show",
|
|
16495
|
+
"tell",
|
|
16496
|
+
"explain",
|
|
16497
|
+
"find",
|
|
16498
|
+
"give",
|
|
16499
|
+
"happen",
|
|
16500
|
+
"happens",
|
|
16501
|
+
"happening",
|
|
16502
|
+
"here",
|
|
16503
|
+
"there",
|
|
16504
|
+
"system",
|
|
16505
|
+
"code",
|
|
16506
|
+
"service",
|
|
16507
|
+
"node",
|
|
16508
|
+
"graph"
|
|
16509
|
+
]);
|
|
16510
|
+
var INTENT_WORDS = /* @__PURE__ */ new Set([
|
|
16511
|
+
"why",
|
|
16512
|
+
"root",
|
|
16513
|
+
"cause",
|
|
16514
|
+
"failing",
|
|
16515
|
+
"fail",
|
|
16516
|
+
"fails",
|
|
16517
|
+
"failed",
|
|
16518
|
+
"breaking",
|
|
16519
|
+
"broke",
|
|
16520
|
+
"broken",
|
|
16521
|
+
"crash",
|
|
16522
|
+
"crashing",
|
|
16523
|
+
"crashed",
|
|
16524
|
+
"throw",
|
|
16525
|
+
"throwing",
|
|
16526
|
+
"throws",
|
|
16527
|
+
"culprit",
|
|
16528
|
+
"blast",
|
|
16529
|
+
"radius",
|
|
16530
|
+
"break",
|
|
16531
|
+
"breaks",
|
|
16532
|
+
"impact",
|
|
16533
|
+
"downstream",
|
|
16534
|
+
"dependent",
|
|
16535
|
+
"dependents",
|
|
16536
|
+
"redeploy",
|
|
16537
|
+
"affect",
|
|
16538
|
+
"affects",
|
|
16539
|
+
"affected",
|
|
16540
|
+
"diverge",
|
|
16541
|
+
"divergence",
|
|
16542
|
+
"divergences",
|
|
16543
|
+
"weird",
|
|
16544
|
+
"mismatch",
|
|
16545
|
+
"drift",
|
|
16546
|
+
"inconsistent",
|
|
16547
|
+
"disagree",
|
|
16548
|
+
"declared",
|
|
16549
|
+
"observed",
|
|
16550
|
+
"incident",
|
|
16551
|
+
"incidents",
|
|
16552
|
+
"recent",
|
|
16553
|
+
"errors",
|
|
16554
|
+
"error",
|
|
16555
|
+
"failures",
|
|
16556
|
+
"failure",
|
|
16557
|
+
"history",
|
|
16558
|
+
"runtime",
|
|
16559
|
+
"production",
|
|
16560
|
+
"prod",
|
|
16561
|
+
"traffic",
|
|
16562
|
+
"actually",
|
|
16563
|
+
"really",
|
|
16564
|
+
"depend",
|
|
16565
|
+
"depends",
|
|
16566
|
+
"dependency",
|
|
16567
|
+
"dependencies",
|
|
16568
|
+
"call",
|
|
16569
|
+
"calls",
|
|
16570
|
+
"use",
|
|
16571
|
+
"uses",
|
|
16572
|
+
"using",
|
|
16573
|
+
"import",
|
|
16574
|
+
"imports",
|
|
16575
|
+
"relies",
|
|
16576
|
+
"rely",
|
|
16577
|
+
"needs",
|
|
16578
|
+
"need",
|
|
16579
|
+
"sync"
|
|
16580
|
+
]);
|
|
16581
|
+
function tokens(input) {
|
|
16582
|
+
const spaced = input.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
16583
|
+
return (spaced.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((t) => t.length >= 2);
|
|
16584
|
+
}
|
|
16585
|
+
function queryTokens(question) {
|
|
16586
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16587
|
+
const out = [];
|
|
16588
|
+
for (const t of tokens(question)) {
|
|
16589
|
+
if (STOPWORDS.has(t) || INTENT_WORDS.has(t)) continue;
|
|
16590
|
+
if (seen.has(t)) continue;
|
|
16591
|
+
seen.add(t);
|
|
16592
|
+
out.push(t);
|
|
16593
|
+
}
|
|
16594
|
+
return out;
|
|
16595
|
+
}
|
|
16596
|
+
function nodeName(node) {
|
|
16597
|
+
return node.name ?? node.id;
|
|
16598
|
+
}
|
|
16599
|
+
function idBody(id) {
|
|
16600
|
+
const colon = id.indexOf(":");
|
|
16601
|
+
return colon >= 0 ? id.slice(colon + 1) : id;
|
|
16602
|
+
}
|
|
16603
|
+
var EMBED_MIN_SCORE = 0.35;
|
|
16604
|
+
async function resolveEntities(graph, question, searchIndex, maxNodes) {
|
|
16605
|
+
const qTokens = queryTokens(question);
|
|
16606
|
+
const normalized = question.toLowerCase();
|
|
16607
|
+
const best = /* @__PURE__ */ new Map();
|
|
16608
|
+
const consider = (cand) => {
|
|
16609
|
+
const cur = best.get(cand.nodeId);
|
|
16610
|
+
if (!cur || cand.score > cur.score) best.set(cand.nodeId, cand);
|
|
16611
|
+
};
|
|
16612
|
+
graph.forEachNode((id, attrs) => {
|
|
16613
|
+
const node = attrs;
|
|
16614
|
+
if (node.type === NodeType32.FrontierNode) return;
|
|
16615
|
+
const name = nodeName(node);
|
|
16616
|
+
const body = idBody(id);
|
|
16617
|
+
const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
|
|
16618
|
+
if (labelTokens.size === 0) return;
|
|
16619
|
+
let matched = 0;
|
|
16620
|
+
for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
|
|
16621
|
+
const named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
|
|
16622
|
+
if (matched === 0 && !named) return;
|
|
16623
|
+
const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
|
|
16624
|
+
const via = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
|
|
16625
|
+
const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
|
|
16626
|
+
consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
|
|
16627
|
+
});
|
|
16628
|
+
if (searchIndex) {
|
|
16629
|
+
try {
|
|
16630
|
+
const res = await searchIndex.search(question, 10);
|
|
16631
|
+
if (res.provider !== "substring") {
|
|
16632
|
+
for (const m of res.matches) {
|
|
16633
|
+
if (m.node.type === NodeType32.FrontierNode) continue;
|
|
16634
|
+
const already = best.get(m.node.id);
|
|
16635
|
+
if (!already && m.score < EMBED_MIN_SCORE) continue;
|
|
16636
|
+
consider({
|
|
16637
|
+
nodeId: m.node.id,
|
|
16638
|
+
label: nodeName(m.node),
|
|
16639
|
+
via: "embedding",
|
|
16640
|
+
score: Math.max(0, Math.min(1, m.score))
|
|
16641
|
+
});
|
|
16642
|
+
}
|
|
16643
|
+
}
|
|
16644
|
+
} catch {
|
|
16645
|
+
}
|
|
16646
|
+
}
|
|
16647
|
+
const viaRank = { id: 3, label: 2, token: 1, embedding: 0 };
|
|
16648
|
+
return [...best.values()].sort(
|
|
16649
|
+
(a, b) => b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
|
|
16650
|
+
).slice(0, maxNodes).map((s) => ({ nodeId: s.nodeId, label: s.label, via: s.via, score: Number(s.score.toFixed(3)) }));
|
|
16651
|
+
}
|
|
16652
|
+
function incidentsForNode(nodeId, incidents) {
|
|
16653
|
+
if (!incidents || incidents.length === 0) return [];
|
|
16654
|
+
const svc = nodeId.replace(/^service:/, "");
|
|
16655
|
+
return incidents.filter((e) => e.affectedNode === nodeId || e.service === svc);
|
|
16656
|
+
}
|
|
16657
|
+
function edgeSignalNote(e) {
|
|
16658
|
+
const bits = [];
|
|
16659
|
+
if (e.signal) {
|
|
16660
|
+
bits.push(`${e.signal.spanCount} spans`);
|
|
16661
|
+
if (e.signal.errorCount > 0) bits.push(`${e.signal.errorCount} errors`);
|
|
16662
|
+
if (e.signal.latencyMs?.p95 !== void 0) bits.push(`p95 ${Math.round(e.signal.latencyMs.p95)}ms`);
|
|
16663
|
+
} else if (e.callCount !== void 0) {
|
|
16664
|
+
bits.push(`${e.callCount} calls`);
|
|
16665
|
+
}
|
|
16666
|
+
return bits.length ? ` (${bits.join(", ")})` : "";
|
|
16667
|
+
}
|
|
16668
|
+
function buildRootCauseSection(graph, node, incidents, now) {
|
|
16669
|
+
const result = getRootCause(graph, node, void 0, incidents, { now });
|
|
16670
|
+
if (!result) return null;
|
|
16671
|
+
const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? Provenance26.OBSERVED;
|
|
16672
|
+
const facts = [
|
|
16673
|
+
{
|
|
16674
|
+
text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
|
|
16675
|
+
provenance: lastProv,
|
|
16676
|
+
confidence: result.confidence
|
|
16677
|
+
}
|
|
16678
|
+
];
|
|
16679
|
+
const candidates = result.candidates ?? [];
|
|
16680
|
+
for (const c of candidates.slice(0, 3)) {
|
|
16681
|
+
facts.push({
|
|
16682
|
+
text: `Candidate ${c.node} \u2014 ${c.classification}: ${c.reason}`,
|
|
16683
|
+
...c.provenance ? { provenance: c.provenance } : {},
|
|
16684
|
+
confidence: c.confidence
|
|
16685
|
+
});
|
|
16686
|
+
}
|
|
16687
|
+
if (result.traversalPath.length > 1) {
|
|
16688
|
+
facts.push({ text: `Traversal: ${result.traversalPath.join(" \u2190 ")}` });
|
|
16689
|
+
}
|
|
16690
|
+
if (result.fixRecommendation) {
|
|
16691
|
+
facts.push({ text: `Recommended fix: ${result.fixRecommendation}` });
|
|
16692
|
+
}
|
|
16693
|
+
return { heading: "Root cause (navigation)", facts: facts.slice(0, MAX_FACTS_PER_SECTION) };
|
|
16694
|
+
}
|
|
16695
|
+
function buildDependenciesSection(graph, node) {
|
|
16696
|
+
const result = getTransitiveDependencies(graph, node, 2);
|
|
16697
|
+
if (result.total === 0) return null;
|
|
16698
|
+
const facts = result.dependencies.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16699
|
+
text: `${d.nodeId} \u2014 ${d.edgeType} (distance ${d.distance})`,
|
|
16700
|
+
// Per-fact provenance: the transitive walk mixes EXTRACTED and OBSERVED
|
|
16701
|
+
// edges, so the tag is on the fact, not asserted for the whole section.
|
|
16702
|
+
provenance: d.provenance
|
|
16703
|
+
}));
|
|
16704
|
+
return { heading: "Dependencies", facts };
|
|
16705
|
+
}
|
|
16706
|
+
function buildObservedSection(graph, node) {
|
|
16707
|
+
const result = getObservedDependencies(graph, node);
|
|
16708
|
+
const facts = [];
|
|
16709
|
+
for (const e of result.dependencies.slice(0, MAX_FACTS_PER_SECTION)) {
|
|
16710
|
+
const via = e.source !== node ? ` via ${e.source}` : "";
|
|
16711
|
+
facts.push({
|
|
16712
|
+
text: `calls ${e.target}${via}${edgeSignalNote(e)}`,
|
|
16713
|
+
provenance: e.provenance,
|
|
16714
|
+
confidence: confidenceForEdge(e)
|
|
16715
|
+
});
|
|
16716
|
+
}
|
|
16717
|
+
if (facts.length === 0) {
|
|
16718
|
+
if (result.observed && result.inboundObservedCount > 0) {
|
|
16719
|
+
facts.push({
|
|
16720
|
+
text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
|
|
16721
|
+
provenance: Provenance26.OBSERVED
|
|
16722
|
+
});
|
|
16723
|
+
} else {
|
|
16724
|
+
return null;
|
|
16725
|
+
}
|
|
16726
|
+
}
|
|
16727
|
+
return { heading: "Runtime dependencies (OBSERVED)", facts };
|
|
16728
|
+
}
|
|
16729
|
+
function buildBlastSection(graph, node) {
|
|
16730
|
+
const result = getBlastRadius(graph, node);
|
|
16731
|
+
if (result.totalAffected === 0) return null;
|
|
16732
|
+
const facts = result.affectedNodes.slice(0, MAX_FACTS_PER_SECTION).map((n) => ({
|
|
16733
|
+
text: `${n.nodeId} (distance ${n.distance})`,
|
|
16734
|
+
provenance: n.edgeProvenance,
|
|
16735
|
+
confidence: n.confidence
|
|
16736
|
+
}));
|
|
16737
|
+
return {
|
|
16738
|
+
heading: `Blast radius \u2014 ${result.totalAffected} dependent${result.totalAffected === 1 ? "" : "s"}`,
|
|
16739
|
+
facts
|
|
16740
|
+
};
|
|
16741
|
+
}
|
|
16742
|
+
function buildIncidentsSection(node, incidents) {
|
|
16743
|
+
const relevant = incidentsForNode(node, incidents);
|
|
16744
|
+
if (relevant.length === 0) return null;
|
|
16745
|
+
const ordered = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 4);
|
|
16746
|
+
const facts = ordered.map((ev) => ({
|
|
16747
|
+
text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
|
|
16748
|
+
// ErrorEvents are observation records — OBSERVED by definition.
|
|
16749
|
+
provenance: Provenance26.OBSERVED
|
|
16750
|
+
}));
|
|
16751
|
+
return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
|
|
16752
|
+
}
|
|
16753
|
+
function divergenceLine(d) {
|
|
16754
|
+
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16755
|
+
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16756
|
+
}
|
|
16757
|
+
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16758
|
+
}
|
|
16759
|
+
function buildDivergenceSection(graph, node) {
|
|
16760
|
+
const result = computeDivergences(graph, { node });
|
|
16761
|
+
if (result.totalAffected === 0) return null;
|
|
16762
|
+
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16763
|
+
text: divergenceLine(d),
|
|
16764
|
+
confidence: d.confidence
|
|
16765
|
+
// A divergence is composite by construction (EXTRACTED vs OBSERVED), so it
|
|
16766
|
+
// carries no single provenance — the fact leaves it unset.
|
|
16767
|
+
}));
|
|
16768
|
+
return {
|
|
16769
|
+
heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
|
|
16770
|
+
facts
|
|
16771
|
+
};
|
|
16772
|
+
}
|
|
16773
|
+
function buildGlobalDivergenceSection(graph) {
|
|
16774
|
+
const result = computeDivergences(graph);
|
|
16775
|
+
if (result.totalAffected === 0) {
|
|
16776
|
+
return {
|
|
16777
|
+
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
16778
|
+
facts: [{ text: "None \u2014 declared code and observed runtime agree across the graph." }]
|
|
16779
|
+
};
|
|
16780
|
+
}
|
|
16781
|
+
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16782
|
+
text: divergenceLine(d),
|
|
16783
|
+
confidence: d.confidence
|
|
16784
|
+
// Composite by construction (EXTRACTED vs OBSERVED), so no single provenance.
|
|
16785
|
+
}));
|
|
16786
|
+
return {
|
|
16787
|
+
heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
|
|
16788
|
+
facts
|
|
16789
|
+
};
|
|
16790
|
+
}
|
|
16791
|
+
function buildGlobalIncidentsSection(incidents) {
|
|
16792
|
+
if (!incidents || incidents.length === 0) {
|
|
16793
|
+
return {
|
|
16794
|
+
heading: "Incidents across the system",
|
|
16795
|
+
facts: [{ text: "None recorded \u2014 the OBSERVED incident store is empty." }]
|
|
16796
|
+
};
|
|
16797
|
+
}
|
|
16798
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
16799
|
+
for (const ev of incidents) {
|
|
16800
|
+
const key = ev.affectedNode || serviceId14(ev.service);
|
|
16801
|
+
const cur = byKey.get(key);
|
|
16802
|
+
if (!cur) {
|
|
16803
|
+
byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
|
|
16804
|
+
} else {
|
|
16805
|
+
cur.count += 1;
|
|
16806
|
+
if (ev.timestamp > cur.latest) {
|
|
16807
|
+
cur.latest = ev.timestamp;
|
|
16808
|
+
cur.sampleMsg = ev.errorMessage;
|
|
16809
|
+
}
|
|
16810
|
+
}
|
|
16811
|
+
}
|
|
16812
|
+
const rows = [...byKey.values()].sort((a, b) => b.count - a.count || b.latest.localeCompare(a.latest) || a.key.localeCompare(b.key)).slice(0, MAX_FACTS_PER_SECTION);
|
|
16813
|
+
const facts = rows.map((r) => ({
|
|
16814
|
+
text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
|
|
16815
|
+
provenance: Provenance26.OBSERVED
|
|
16816
|
+
}));
|
|
16817
|
+
return {
|
|
16818
|
+
heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
|
|
16819
|
+
facts
|
|
16820
|
+
};
|
|
16821
|
+
}
|
|
16822
|
+
function buildOverviewSections(graph, incidents) {
|
|
16823
|
+
const nodeByType = /* @__PURE__ */ new Map();
|
|
16824
|
+
const services = [];
|
|
16825
|
+
graph.forEachNode((_id, attrs) => {
|
|
16826
|
+
const node = attrs;
|
|
16827
|
+
nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
|
|
16828
|
+
if (node.type === NodeType32.ServiceNode) services.push(node.id);
|
|
16829
|
+
});
|
|
16830
|
+
const edgeByProv = /* @__PURE__ */ new Map();
|
|
16831
|
+
graph.forEachEdge((_id, attrs) => {
|
|
16832
|
+
const p = attrs.provenance;
|
|
16833
|
+
edgeByProv.set(p, (edgeByProv.get(p) ?? 0) + 1);
|
|
16834
|
+
});
|
|
16835
|
+
const sections = [];
|
|
16836
|
+
const count = (t) => nodeByType.get(t) ?? 0;
|
|
16837
|
+
const shapeFacts = [
|
|
16838
|
+
{ text: `${graph.order} nodes, ${graph.size} edges` },
|
|
16839
|
+
{
|
|
16840
|
+
text: `${count(NodeType32.ServiceNode)} services, ${count(NodeType32.FileNode)} files, ${count(NodeType32.SymbolNode)} symbols, ${count(NodeType32.DatabaseNode)} databases`
|
|
16841
|
+
}
|
|
16842
|
+
];
|
|
16843
|
+
for (const p of [Provenance26.EXTRACTED, Provenance26.OBSERVED, Provenance26.INFERRED, Provenance26.STALE]) {
|
|
16844
|
+
const n = edgeByProv.get(p) ?? 0;
|
|
16845
|
+
if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
|
|
16846
|
+
}
|
|
16847
|
+
sections.push({ heading: "System shape", facts: shapeFacts });
|
|
16848
|
+
const depCounts = services.map((s) => ({ s, n: getTransitiveDependencies(graph, s, 1).total })).filter((r) => r.n > 0).sort((a, b) => b.n - a.n || a.s.localeCompare(b.s)).slice(0, MAX_FACTS_PER_SECTION);
|
|
16849
|
+
if (depCounts.length > 0) {
|
|
16850
|
+
sections.push({
|
|
16851
|
+
heading: "Busiest services (by direct dependencies)",
|
|
16852
|
+
facts: depCounts.map((r) => ({
|
|
16853
|
+
text: `${r.s} \u2014 ${r.n} direct dependenc${r.n === 1 ? "y" : "ies"}`
|
|
16854
|
+
}))
|
|
16855
|
+
});
|
|
16856
|
+
}
|
|
16857
|
+
if (incidents && incidents.length > 0) {
|
|
16858
|
+
const incCount = /* @__PURE__ */ new Map();
|
|
16859
|
+
for (const ev of incidents) {
|
|
16860
|
+
const key = ev.affectedNode || serviceId14(ev.service);
|
|
16861
|
+
incCount.set(key, (incCount.get(key) ?? 0) + 1);
|
|
16862
|
+
}
|
|
16863
|
+
const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
|
|
16864
|
+
sections.push({
|
|
16865
|
+
heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
|
|
16866
|
+
facts: top.map(([k, n]) => ({
|
|
16867
|
+
text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
|
|
16868
|
+
provenance: Provenance26.OBSERVED
|
|
16869
|
+
}))
|
|
16870
|
+
});
|
|
16871
|
+
}
|
|
16872
|
+
const div = computeDivergences(graph);
|
|
16873
|
+
sections.push({
|
|
16874
|
+
heading: "Divergences",
|
|
16875
|
+
facts: [
|
|
16876
|
+
{
|
|
16877
|
+
text: div.totalAffected === 0 ? "None \u2014 declared code and observed runtime agree across the graph." : `${div.totalAffected} divergence${div.totalAffected === 1 ? "" : "s"} between declared code and observed runtime \u2014 ask "are there any divergences?" for the list.`
|
|
16878
|
+
}
|
|
16879
|
+
]
|
|
16880
|
+
});
|
|
16881
|
+
return sections;
|
|
16882
|
+
}
|
|
16883
|
+
function buildGlobalSections(intent, graph, incidents) {
|
|
16884
|
+
switch (intent) {
|
|
16885
|
+
case "divergence":
|
|
16886
|
+
return [buildGlobalDivergenceSection(graph)];
|
|
16887
|
+
case "incidents":
|
|
16888
|
+
return [buildGlobalIncidentsSection(incidents)];
|
|
16889
|
+
case "overview":
|
|
16890
|
+
return buildOverviewSections(graph, incidents);
|
|
16891
|
+
default:
|
|
16892
|
+
return null;
|
|
16893
|
+
}
|
|
16894
|
+
}
|
|
16895
|
+
var SECTION_ORDER = {
|
|
16896
|
+
"root-cause": ["root-cause", "incidents", "blast", "observed", "divergence"],
|
|
16897
|
+
"blast-radius": ["blast", "dependencies", "observed", "incidents"],
|
|
16898
|
+
dependencies: ["dependencies", "observed", "blast"],
|
|
16899
|
+
observed: ["observed", "dependencies", "incidents"],
|
|
16900
|
+
incidents: ["incidents", "root-cause", "observed"],
|
|
16901
|
+
divergence: ["divergence", "observed", "dependencies", "incidents"],
|
|
16902
|
+
overview: ["dependencies", "observed", "blast", "incidents", "divergence"]
|
|
16903
|
+
};
|
|
16904
|
+
function buildSection(kind, graph, node, incidents, now) {
|
|
16905
|
+
switch (kind) {
|
|
16906
|
+
case "root-cause":
|
|
16907
|
+
return buildRootCauseSection(graph, node, incidents, now);
|
|
16908
|
+
case "dependencies":
|
|
16909
|
+
return buildDependenciesSection(graph, node);
|
|
16910
|
+
case "observed":
|
|
16911
|
+
return buildObservedSection(graph, node);
|
|
16912
|
+
case "blast":
|
|
16913
|
+
return buildBlastSection(graph, node);
|
|
16914
|
+
case "incidents":
|
|
16915
|
+
return buildIncidentsSection(node, incidents);
|
|
16916
|
+
case "divergence":
|
|
16917
|
+
return buildDivergenceSection(graph, node);
|
|
16918
|
+
}
|
|
16919
|
+
}
|
|
16920
|
+
function summarizeGlobal(intent, sections) {
|
|
16921
|
+
const lead = sections[0];
|
|
16922
|
+
switch (intent) {
|
|
16923
|
+
case "divergence": {
|
|
16924
|
+
const none = lead?.facts[0]?.text.startsWith("None") ?? true;
|
|
16925
|
+
return none ? "No divergences across the graph \u2014 declared code and observed runtime agree." : `${lead.heading} across the graph, highest-confidence first below.`;
|
|
16926
|
+
}
|
|
16927
|
+
case "incidents": {
|
|
16928
|
+
const none = lead?.facts[0]?.text.startsWith("None") ?? true;
|
|
16929
|
+
return none ? "No incidents recorded across the system \u2014 the OBSERVED incident store is empty." : `${lead.heading}, aggregated by node below.`;
|
|
16930
|
+
}
|
|
16931
|
+
case "overview":
|
|
16932
|
+
default:
|
|
16933
|
+
return `System overview across the whole graph: ${sections.length} view${sections.length === 1 ? "" : "s"} below \u2014 shape (nodes/edges by provenance), busiest services, incidents, and divergences.`;
|
|
16934
|
+
}
|
|
16935
|
+
}
|
|
16936
|
+
function summarize(question, intent, matched, primary, sections, scope) {
|
|
16937
|
+
if (scope === "global") {
|
|
16938
|
+
return summarizeGlobal(intent, sections);
|
|
16939
|
+
}
|
|
16940
|
+
if (!primary) {
|
|
16941
|
+
return `Nothing in "${question}" resolved to a node in the graph. Name a service, file, route, or table \u2014 e.g. \`ask "what does <service> depend on?"\`. For a graph-wide look, ask for an overview, divergences, or incidents.`;
|
|
16942
|
+
}
|
|
16943
|
+
const lead = sections[0];
|
|
16944
|
+
const others = matched.slice(1);
|
|
16945
|
+
const alsoNote = others.length ? ` Also matched: ${others.map((m) => m.nodeId).join(", ")}.` : "";
|
|
16946
|
+
let core;
|
|
16947
|
+
switch (intent) {
|
|
16948
|
+
case "root-cause": {
|
|
16949
|
+
const rc = sections.find((s) => s.heading === "Root cause (navigation)");
|
|
16950
|
+
if (rc) {
|
|
16951
|
+
core = rc.facts[0]?.text ?? "";
|
|
16952
|
+
} else if (lead) {
|
|
16953
|
+
core = `No root cause surfaced for ${primary} \u2014 it may be healthy, or the failure isn't recorded. Nearest context: ${lead.heading.toLowerCase()}.`;
|
|
16954
|
+
} else {
|
|
16955
|
+
core = `No root cause surfaced for ${primary} \u2014 it may be healthy.`;
|
|
16956
|
+
}
|
|
16957
|
+
break;
|
|
16958
|
+
}
|
|
16959
|
+
case "blast-radius":
|
|
16960
|
+
core = lead ? `${lead.heading} of ${primary}.` : `${primary} has no dependents \u2014 nothing else would break if it failed.`;
|
|
16961
|
+
break;
|
|
16962
|
+
case "dependencies":
|
|
16963
|
+
core = lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
|
|
16964
|
+
break;
|
|
16965
|
+
case "observed":
|
|
16966
|
+
core = lead ? `${primary} at runtime: ${lead.facts.length} OBSERVED fact${lead.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
|
|
16967
|
+
break;
|
|
16968
|
+
case "incidents":
|
|
16969
|
+
core = lead ? `${primary}: ${lead.heading.toLowerCase()}.` : `No incidents recorded against ${primary}.`;
|
|
16970
|
+
break;
|
|
16971
|
+
case "divergence":
|
|
16972
|
+
core = lead ? `${lead.heading} involving ${primary}.` : `No divergences involve ${primary} \u2014 declared and observed agree here.`;
|
|
16973
|
+
break;
|
|
16974
|
+
default:
|
|
16975
|
+
core = `${primary}: ${sections.length} view${sections.length === 1 ? "" : "s"} of its fused local context below.`;
|
|
16976
|
+
}
|
|
16977
|
+
return `${core}${alsoNote}`;
|
|
16978
|
+
}
|
|
16979
|
+
async function askGraph(graph, question, opts = {}) {
|
|
16980
|
+
const now = opts.now ?? Date.now();
|
|
16981
|
+
const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
|
|
16982
|
+
const intent = classifyIntent(question);
|
|
16983
|
+
const matched = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
|
|
16984
|
+
const primary = matched[0]?.nodeId;
|
|
16985
|
+
const sections = [];
|
|
16986
|
+
let scope;
|
|
16987
|
+
if (primary) {
|
|
16988
|
+
scope = "node";
|
|
16989
|
+
for (const kind of SECTION_ORDER[intent]) {
|
|
16990
|
+
const s = buildSection(kind, graph, primary, opts.incidents, now);
|
|
16991
|
+
if (s && s.facts.length > 0) sections.push(s);
|
|
16992
|
+
}
|
|
16993
|
+
} else {
|
|
16994
|
+
const global = buildGlobalSections(intent, graph, opts.incidents);
|
|
16995
|
+
if (global) {
|
|
16996
|
+
scope = "global";
|
|
16997
|
+
for (const s of global) if (s.facts.length > 0) sections.push(s);
|
|
16998
|
+
}
|
|
16999
|
+
}
|
|
17000
|
+
const answer = summarize(question, intent, matched, primary, sections, scope);
|
|
17001
|
+
const provSet = /* @__PURE__ */ new Set();
|
|
17002
|
+
for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
|
|
17003
|
+
const confidence = sections[0]?.facts[0]?.confidence;
|
|
17004
|
+
return AskResultSchema.parse({
|
|
17005
|
+
question,
|
|
17006
|
+
intent,
|
|
17007
|
+
matched,
|
|
17008
|
+
...primary ? { primaryNode: primary } : {},
|
|
17009
|
+
...scope ? { scope } : {},
|
|
17010
|
+
sections,
|
|
17011
|
+
answer,
|
|
17012
|
+
...confidence !== void 0 ? { confidence } : {},
|
|
17013
|
+
provenance: [...provSet]
|
|
17014
|
+
});
|
|
17015
|
+
}
|
|
17016
|
+
|
|
16334
17017
|
// src/streaming.ts
|
|
16335
17018
|
var SSE_HEARTBEAT_MS = 3e4;
|
|
16336
17019
|
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
@@ -16713,14 +17396,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
|
|
|
16713
17396
|
}
|
|
16714
17397
|
|
|
16715
17398
|
// src/connectors/index.ts
|
|
16716
|
-
import { NodeType as
|
|
17399
|
+
import { NodeType as NodeType33, parseFileId as parseFileId2, Provenance as Provenance27 } from "@neat.is/types";
|
|
16717
17400
|
var NO_ENV = "unknown";
|
|
16718
17401
|
function staticCallSiteFor(graph, serviceName, targetNodeId) {
|
|
16719
17402
|
if (!graph.hasNode(targetNodeId)) return void 0;
|
|
16720
17403
|
const sites = [];
|
|
16721
17404
|
for (const edgeId of graph.inboundEdges(targetNodeId)) {
|
|
16722
17405
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
16723
|
-
if (edge.provenance !==
|
|
17406
|
+
if (edge.provenance !== Provenance27.EXTRACTED) continue;
|
|
16724
17407
|
const parsed = parseFileId2(edge.source);
|
|
16725
17408
|
if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
|
|
16726
17409
|
const site = { relPath: edge.evidence.file };
|
|
@@ -16732,7 +17415,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
|
|
|
16732
17415
|
function routeCallSiteFor(graph, targetNodeId) {
|
|
16733
17416
|
if (!graph.hasNode(targetNodeId)) return void 0;
|
|
16734
17417
|
const attrs = graph.getNodeAttributes(targetNodeId);
|
|
16735
|
-
if (attrs.type !==
|
|
17418
|
+
if (attrs.type !== NodeType33.RouteNode || !attrs.path) return void 0;
|
|
16736
17419
|
const site = { relPath: attrs.path };
|
|
16737
17420
|
if (attrs.line !== void 0) site.line = attrs.line;
|
|
16738
17421
|
return site;
|
|
@@ -17437,7 +18120,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
|
|
|
17437
18120
|
}
|
|
17438
18121
|
|
|
17439
18122
|
// src/connectors/railway/index.ts
|
|
17440
|
-
import { EdgeType as EdgeType27, NodeType as
|
|
18123
|
+
import { EdgeType as EdgeType27, NodeType as NodeType34, serviceId as serviceId15 } from "@neat.is/types";
|
|
17441
18124
|
|
|
17442
18125
|
// src/connectors/railway/client.ts
|
|
17443
18126
|
var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
|
|
@@ -17587,7 +18270,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
|
|
|
17587
18270
|
const out = [];
|
|
17588
18271
|
graph.forEachNode((_id, attrs) => {
|
|
17589
18272
|
const node = attrs;
|
|
17590
|
-
if (node.type !==
|
|
18273
|
+
if (node.type !== NodeType34.RouteNode) return;
|
|
17591
18274
|
const route = attrs;
|
|
17592
18275
|
if (route.service !== serviceName) return;
|
|
17593
18276
|
out.push({
|
|
@@ -17696,7 +18379,7 @@ function createRailwayResolveTarget(config) {
|
|
|
17696
18379
|
if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
|
|
17697
18380
|
const peerName = config.serviceNameById[signal.targetName];
|
|
17698
18381
|
if (!peerName) return null;
|
|
17699
|
-
return { targetNodeId:
|
|
18382
|
+
return { targetNodeId: serviceId15(peerName), serviceName, edgeType: EdgeType27.CONNECTS_TO };
|
|
17700
18383
|
}
|
|
17701
18384
|
return null;
|
|
17702
18385
|
};
|
|
@@ -17878,7 +18561,7 @@ function mapLogEntriesToSignals(entries) {
|
|
|
17878
18561
|
}
|
|
17879
18562
|
|
|
17880
18563
|
// src/connectors/firebase/resolve.ts
|
|
17881
|
-
import { NodeType as
|
|
18564
|
+
import { NodeType as NodeType35, EdgeType as EdgeType28 } from "@neat.is/types";
|
|
17882
18565
|
function neatServiceNameFor(resourceType, resourceName, serviceMap) {
|
|
17883
18566
|
switch (resourceType) {
|
|
17884
18567
|
case "cloud_function":
|
|
@@ -17893,7 +18576,7 @@ function routeEntriesFor(graph, serviceName) {
|
|
|
17893
18576
|
const entries = [];
|
|
17894
18577
|
graph.forEachNode((_id, attrs) => {
|
|
17895
18578
|
const node = attrs;
|
|
17896
|
-
if (node.type !==
|
|
18579
|
+
if (node.type !== NodeType35.RouteNode) return;
|
|
17897
18580
|
const route = attrs;
|
|
17898
18581
|
if (route.service !== serviceName) return;
|
|
17899
18582
|
entries.push({
|
|
@@ -17948,7 +18631,7 @@ function createFirebaseConnector(graph, serviceMap) {
|
|
|
17948
18631
|
}
|
|
17949
18632
|
|
|
17950
18633
|
// src/connectors/cloudflare/connector.ts
|
|
17951
|
-
import { EdgeType as EdgeType29, NodeType as
|
|
18634
|
+
import { EdgeType as EdgeType29, NodeType as NodeType36, fileId as fileId5, infraId as infraId22 } from "@neat.is/types";
|
|
17952
18635
|
|
|
17953
18636
|
// src/connectors/cloudflare/client.ts
|
|
17954
18637
|
import { randomUUID } from "crypto";
|
|
@@ -18107,7 +18790,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
|
|
|
18107
18790
|
graph.forEachNode((id, attrs) => {
|
|
18108
18791
|
if (found) return;
|
|
18109
18792
|
const a = attrs;
|
|
18110
|
-
if (a.type ===
|
|
18793
|
+
if (a.type === NodeType36.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
|
|
18111
18794
|
found = id;
|
|
18112
18795
|
}
|
|
18113
18796
|
});
|
|
@@ -18119,7 +18802,7 @@ function findMatchingRouteNode(graph, serviceName, method, path72) {
|
|
|
18119
18802
|
graph.forEachNode((id, attrs) => {
|
|
18120
18803
|
if (found) return;
|
|
18121
18804
|
const a = attrs;
|
|
18122
|
-
if (a.type !==
|
|
18805
|
+
if (a.type !== NodeType36.RouteNode || a.service !== serviceName) return;
|
|
18123
18806
|
if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
|
|
18124
18807
|
const routeMethod = (a.method ?? "").toUpperCase();
|
|
18125
18808
|
if (routeMethod !== "ALL" && routeMethod !== method) return;
|
|
@@ -18518,14 +19201,14 @@ function mapLogEntriesToSignals2(entries) {
|
|
|
18518
19201
|
}
|
|
18519
19202
|
|
|
18520
19203
|
// src/connectors/cloud-run/resolve.ts
|
|
18521
|
-
import { EdgeType as EdgeType31, NodeType as
|
|
19204
|
+
import { EdgeType as EdgeType31, NodeType as NodeType37, infraId as infraId24 } from "@neat.is/types";
|
|
18522
19205
|
var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
|
|
18523
19206
|
function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
|
|
18524
19207
|
let found = null;
|
|
18525
19208
|
graph.forEachNode((_id, attrs) => {
|
|
18526
19209
|
if (found) return;
|
|
18527
19210
|
const node = attrs;
|
|
18528
|
-
if (node.type !==
|
|
19211
|
+
if (node.type !== NodeType37.RouteNode) return;
|
|
18529
19212
|
const route = attrs;
|
|
18530
19213
|
if (route.service !== serviceName || !route.pathTemplate) return;
|
|
18531
19214
|
if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
|
|
@@ -18596,7 +19279,7 @@ function createCloudRunConnector(graph, config = {}) {
|
|
|
18596
19279
|
}
|
|
18597
19280
|
|
|
18598
19281
|
// src/connectors/render/index.ts
|
|
18599
|
-
import { EdgeType as EdgeType32, NodeType as
|
|
19282
|
+
import { EdgeType as EdgeType32, NodeType as NodeType38 } from "@neat.is/types";
|
|
18600
19283
|
|
|
18601
19284
|
// src/connectors/render/types.ts
|
|
18602
19285
|
function readRenderToken(credentials) {
|
|
@@ -18672,7 +19355,7 @@ function buildRenderRouteIndex(graph, serviceName) {
|
|
|
18672
19355
|
const out = [];
|
|
18673
19356
|
graph.forEachNode((_id, attrs) => {
|
|
18674
19357
|
const node = attrs;
|
|
18675
|
-
if (node.type !==
|
|
19358
|
+
if (node.type !== NodeType38.RouteNode) return;
|
|
18676
19359
|
const route = attrs;
|
|
18677
19360
|
if (route.service !== serviceName) return;
|
|
18678
19361
|
out.push({
|
|
@@ -19156,7 +19839,7 @@ function mapBuildsToSignals(builds, serviceName) {
|
|
|
19156
19839
|
}
|
|
19157
19840
|
|
|
19158
19841
|
// src/connectors/eas/resolve.ts
|
|
19159
|
-
import { EdgeType as EdgeType34, NodeType as
|
|
19842
|
+
import { EdgeType as EdgeType34, NodeType as NodeType39, parseFileId as parseFileId3 } from "@neat.is/types";
|
|
19160
19843
|
var NO_ENV2 = "unknown";
|
|
19161
19844
|
var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
|
|
19162
19845
|
var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
|
|
@@ -19184,7 +19867,7 @@ function findConfigNode(graph, basenames, serviceName) {
|
|
|
19184
19867
|
graph.forEachNode((id, attrs) => {
|
|
19185
19868
|
if (scoped) return;
|
|
19186
19869
|
const node = attrs;
|
|
19187
|
-
if (node.type !==
|
|
19870
|
+
if (node.type !== NodeType39.ConfigNode) return;
|
|
19188
19871
|
if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
|
|
19189
19872
|
if (anyMatch === null) anyMatch = id;
|
|
19190
19873
|
if (configNodeService(graph, id) === serviceName) scoped = id;
|
|
@@ -20257,6 +20940,18 @@ function registerRoutes(scope, ctx) {
|
|
|
20257
20940
|
matches: matches.slice(0, safeLimit)
|
|
20258
20941
|
};
|
|
20259
20942
|
});
|
|
20943
|
+
scope.get("/graph/ask", async (req, reply) => {
|
|
20944
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
20945
|
+
if (!proj) return;
|
|
20946
|
+
const question = (req.query.q ?? "").trim();
|
|
20947
|
+
if (!question) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
20948
|
+
const epath = errorsPathFor(proj);
|
|
20949
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
20950
|
+
return askGraph(proj.graph, question, {
|
|
20951
|
+
...proj.searchIndex ? { searchIndex: proj.searchIndex } : {},
|
|
20952
|
+
incidents
|
|
20953
|
+
});
|
|
20954
|
+
});
|
|
20260
20955
|
scope.get(
|
|
20261
20956
|
"/graph/diff",
|
|
20262
20957
|
async (req, reply) => {
|
|
@@ -20796,4 +21491,4 @@ export {
|
|
|
20796
21491
|
deprovisionConnector,
|
|
20797
21492
|
buildApi
|
|
20798
21493
|
};
|
|
20799
|
-
//# sourceMappingURL=chunk-
|
|
21494
|
+
//# sourceMappingURL=chunk-LRYPKC3B.js.map
|