@neat.is/core 0.4.20-dev.20260630 → 0.4.23

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountBearerAuth,
3
3
  readAuthEnv
4
- } from "./chunk-BGPWBRLU.js";
4
+ } from "./chunk-ZX7PCMGZ.js";
5
5
 
6
6
  // src/graph.ts
7
7
  import GraphDefault from "graphology";
@@ -344,7 +344,9 @@ import {
344
344
  BlastRadiusResultSchema,
345
345
  EdgeType,
346
346
  NodeType,
347
+ ObservedDependenciesResultSchema,
347
348
  PROV_RANK,
349
+ Provenance,
348
350
  RootCauseResultSchema,
349
351
  TransitiveDependenciesResultSchema
350
352
  } from "@neat.is/types";
@@ -552,22 +554,164 @@ var rootCauseShapes = {
552
554
  [NodeType.ServiceNode]: serviceRootCauseShape,
553
555
  [NodeType.FileNode]: fileRootCauseShape
554
556
  };
555
- function getRootCause(graph, errorNodeId, errorEvent) {
557
+ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
556
558
  if (!graph.hasNode(errorNodeId)) return null;
557
559
  const origin = graph.getNodeAttributes(errorNodeId);
558
560
  const shape = rootCauseShapes[origin.type];
559
- if (!shape) return null;
560
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
561
- const match = shape(graph, origin, walk);
562
- if (!match) return null;
563
- const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
561
+ if (shape) {
562
+ const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
563
+ const match = shape(graph, origin, walk);
564
+ if (match) {
565
+ const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
566
+ return RootCauseResultSchema.parse({
567
+ rootCauseNode: match.rootCauseNode,
568
+ rootCauseReason: reason,
569
+ traversalPath: walk.path,
570
+ edgeProvenances: walk.edges.map((e) => e.provenance),
571
+ confidence: confidenceFromMix(walk.edges),
572
+ fixRecommendation: match.fixRecommendation
573
+ });
574
+ }
575
+ }
576
+ if (origin.type === NodeType.ServiceNode) {
577
+ const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
578
+ if (crossService) return crossService;
579
+ }
580
+ return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
581
+ }
582
+ var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
583
+ function incidentMatchesNode(ev, nodeId) {
584
+ return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
585
+ }
586
+ function localizeFromIncidents(nodeId, incidents, errorEvent) {
587
+ const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
588
+ const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
589
+ if (relevant.length === 0) return null;
590
+ const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
591
+ const attrs = latest.attributes ?? {};
592
+ const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
593
+ const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
594
+ const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
595
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
596
+ const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
597
+ const count = sameMode.length;
598
+ const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
599
+ const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
600
+ if (location) reasonParts.push(`surfaced at ${location}`);
601
+ const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
602
+ const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
603
+ const fileNode = localizesToFile ? latest.affectedNode : void 0;
604
+ const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
605
+ return {
606
+ rootCauseNode: fileNode ?? nodeId,
607
+ rootCauseReason,
608
+ ...fileNode ? { fileNode } : {},
609
+ ...fixRecommendation ? { fixRecommendation } : {}
610
+ };
611
+ }
612
+ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
613
+ const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
614
+ if (!loc) return null;
615
+ const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
616
+ const edgeProvenances = loc.fileNode ? [Provenance.OBSERVED] : [];
564
617
  return RootCauseResultSchema.parse({
565
- rootCauseNode: match.rootCauseNode,
566
- rootCauseReason: reason,
567
- traversalPath: walk.path,
568
- edgeProvenances: walk.edges.map((e) => e.provenance),
569
- confidence: confidenceFromMix(walk.edges),
570
- fixRecommendation: match.fixRecommendation
618
+ rootCauseNode: loc.rootCauseNode,
619
+ rootCauseReason: loc.rootCauseReason,
620
+ traversalPath,
621
+ edgeProvenances,
622
+ confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
623
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
624
+ });
625
+ }
626
+ function isFailingCallEdge(e) {
627
+ return e.type === EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
628
+ }
629
+ function callSourcesForService(graph, serviceId3) {
630
+ const ids = [serviceId3];
631
+ for (const edgeId of graph.outboundEdges(serviceId3)) {
632
+ const e = graph.getEdgeAttributes(edgeId);
633
+ if (e.type !== EdgeType.CONTAINS) continue;
634
+ const tgt = graph.getNodeAttributes(e.target);
635
+ if (tgt.type === NodeType.FileNode) ids.push(e.target);
636
+ }
637
+ return ids;
638
+ }
639
+ function failingCallDominates(e, id, curEdge, curId) {
640
+ const ec = e.signal?.errorCount ?? 0;
641
+ const cc = curEdge.signal?.errorCount ?? 0;
642
+ if (ec !== cc) return ec > cc;
643
+ if (PROV_RANK[e.provenance] !== PROV_RANK[curEdge.provenance]) {
644
+ return PROV_RANK[e.provenance] > PROV_RANK[curEdge.provenance];
645
+ }
646
+ return id < curId;
647
+ }
648
+ function dominantFailingCall(graph, serviceId3, visited) {
649
+ let best = null;
650
+ for (const src of callSourcesForService(graph, serviceId3)) {
651
+ for (const edgeId of graph.outboundEdges(src)) {
652
+ const e = graph.getEdgeAttributes(edgeId);
653
+ if (!isFailingCallEdge(e)) continue;
654
+ if (isFrontierNode(graph, e.target)) continue;
655
+ const owner = resolveOwningService(graph, e.target);
656
+ if (!owner || visited.has(owner.id)) continue;
657
+ if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
658
+ best = { nextService: owner.id, edge: e };
659
+ }
660
+ }
661
+ }
662
+ return best;
663
+ }
664
+ function followFailingCallChain(graph, originServiceId, maxDepth) {
665
+ const path39 = [originServiceId];
666
+ const edges = [];
667
+ const visited = /* @__PURE__ */ new Set([originServiceId]);
668
+ let current = originServiceId;
669
+ for (let depth = 0; depth < maxDepth; depth++) {
670
+ const hop = dominantFailingCall(graph, current, visited);
671
+ if (!hop) break;
672
+ path39.push(hop.nextService);
673
+ edges.push(hop.edge);
674
+ visited.add(hop.nextService);
675
+ current = hop.nextService;
676
+ }
677
+ if (edges.length === 0) return null;
678
+ return { path: path39, edges, culprit: current };
679
+ }
680
+ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
681
+ const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
682
+ if (!chain) return null;
683
+ const culprit = chain.culprit;
684
+ const path39 = [...chain.path];
685
+ const edgeProvenances = chain.edges.map((e) => e.provenance);
686
+ const baseConfidence = confidenceFromMix(chain.edges);
687
+ const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
688
+ const loc = localizeFromIncidents(culprit, incidents, errorEvent);
689
+ if (loc) {
690
+ let rootCauseNode = culprit;
691
+ if (loc.fileNode) {
692
+ path39.push(loc.fileNode);
693
+ edgeProvenances.push(Provenance.OBSERVED);
694
+ rootCauseNode = loc.fileNode;
695
+ }
696
+ return RootCauseResultSchema.parse({
697
+ rootCauseNode,
698
+ rootCauseReason: loc.rootCauseReason,
699
+ traversalPath: path39,
700
+ edgeProvenances,
701
+ confidence,
702
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
703
+ });
704
+ }
705
+ const lastEdge = chain.edges[chain.edges.length - 1];
706
+ const errs = lastEdge.signal?.errorCount ?? 0;
707
+ const culpritName = culprit.replace(/^service:/, "");
708
+ return RootCauseResultSchema.parse({
709
+ rootCauseNode: culprit,
710
+ rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
711
+ traversalPath: path39,
712
+ edgeProvenances,
713
+ confidence,
714
+ fixRecommendation: `Inspect ${culpritName}'s failing handler`
571
715
  });
572
716
  }
573
717
  function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
@@ -590,14 +734,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
590
734
  });
591
735
  }
592
736
  if (frame.distance >= maxDepth) continue;
593
- const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
594
- for (const [tgtId, edge] of outgoing) {
595
- if (enqueued.has(tgtId)) continue;
596
- enqueued.add(tgtId);
737
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
738
+ for (const [srcId, edge] of incoming) {
739
+ if (enqueued.has(srcId)) continue;
740
+ enqueued.add(srcId);
597
741
  queue.push({
598
- nodeId: tgtId,
742
+ nodeId: srcId,
599
743
  distance: frame.distance + 1,
600
- path: [...frame.path, tgtId],
744
+ path: [...frame.path, srcId],
601
745
  pathEdges: [...frame.pathEdges, edge]
602
746
  });
603
747
  }
@@ -653,6 +797,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
653
797
  total: dependencies.length
654
798
  });
655
799
  }
800
+ function getObservedDependencies(graph, nodeId) {
801
+ if (!graph.hasNode(nodeId)) {
802
+ return ObservedDependenciesResultSchema.parse({
803
+ origin: nodeId,
804
+ dependencies: [],
805
+ observed: false,
806
+ inboundObservedCount: 0,
807
+ hasExtractedOutbound: false
808
+ });
809
+ }
810
+ const attrs = graph.getNodeAttributes(nodeId);
811
+ const scope = [nodeId];
812
+ if (attrs.type === NodeType.ServiceNode) {
813
+ for (const edgeId of graph.outboundEdges(nodeId)) {
814
+ const e = graph.getEdgeAttributes(edgeId);
815
+ if (e.type !== EdgeType.CONTAINS) continue;
816
+ const owned = graph.getNodeAttributes(e.target);
817
+ if (owned.type === NodeType.FileNode) scope.push(e.target);
818
+ }
819
+ }
820
+ const dependencies = [];
821
+ const seenEdge = /* @__PURE__ */ new Set();
822
+ let hasExtractedOutbound = false;
823
+ for (const src of scope) {
824
+ for (const edgeId of graph.outboundEdges(src)) {
825
+ const e = graph.getEdgeAttributes(edgeId);
826
+ if (e.type === EdgeType.CONTAINS) continue;
827
+ if (e.provenance === Provenance.OBSERVED) {
828
+ if (!seenEdge.has(e.id)) {
829
+ seenEdge.add(e.id);
830
+ dependencies.push(e);
831
+ }
832
+ } else if (e.provenance === Provenance.EXTRACTED) {
833
+ hasExtractedOutbound = true;
834
+ }
835
+ }
836
+ }
837
+ let inboundObservedCount = 0;
838
+ for (const tgt of scope) {
839
+ for (const edgeId of graph.inboundEdges(tgt)) {
840
+ const e = graph.getEdgeAttributes(edgeId);
841
+ if (e.type === EdgeType.CONTAINS) continue;
842
+ if (e.provenance === Provenance.OBSERVED) inboundObservedCount += 1;
843
+ }
844
+ }
845
+ dependencies.sort(
846
+ (a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
847
+ );
848
+ return ObservedDependenciesResultSchema.parse({
849
+ origin: nodeId,
850
+ dependencies,
851
+ observed: dependencies.length > 0 || inboundObservedCount > 0,
852
+ inboundObservedCount,
853
+ hasExtractedOutbound
854
+ });
855
+ }
656
856
 
657
857
  // src/ingest.ts
658
858
  import { promises as fs3, existsSync, readFileSync } from "fs";
@@ -997,6 +1197,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
997
1197
  }
998
1198
  return out;
999
1199
  }
1200
+ function selectApplicablePolicies(graph, policies, nodeId) {
1201
+ if (!graph.hasNode(nodeId)) return [];
1202
+ const node = graph.getNodeAttributes(nodeId);
1203
+ const out = [];
1204
+ for (const policy of policies) {
1205
+ const m = matchPolicyToNode(graph, policy, nodeId, node);
1206
+ if (!m) continue;
1207
+ out.push({
1208
+ policyId: policy.id,
1209
+ policyName: policy.name,
1210
+ ...policy.description !== void 0 ? { description: policy.description } : {},
1211
+ severity: policy.severity,
1212
+ onViolation: resolveOnViolation(policy),
1213
+ ruleType: policy.rule.type,
1214
+ match: m.match,
1215
+ reason: m.reason
1216
+ });
1217
+ }
1218
+ return out;
1219
+ }
1220
+ function requiredProvenanceList(required) {
1221
+ if (Array.isArray(required)) return required.join(" | ");
1222
+ return String(required);
1223
+ }
1224
+ function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
1225
+ const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
1226
+ for (const edgeId of incident) {
1227
+ const e = graph.getEdgeAttributes(edgeId);
1228
+ if (e.type !== edgeType) continue;
1229
+ if (requiredOtherEnd === void 0) return true;
1230
+ if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
1231
+ }
1232
+ return false;
1233
+ }
1234
+ function blastRadiusSubjectReaching(graph, rule, nodeId) {
1235
+ let found = null;
1236
+ graph.forEachNode((subjId, attrs) => {
1237
+ if (found !== null) return;
1238
+ if (subjId === nodeId) return;
1239
+ if (attrs.type !== rule.nodeType) return;
1240
+ const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
1241
+ if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
1242
+ });
1243
+ return found;
1244
+ }
1245
+ function matchPolicyToNode(graph, policy, nodeId, node) {
1246
+ const rule = policy.rule;
1247
+ switch (rule.type) {
1248
+ case "structural": {
1249
+ if (node.type === rule.fromNodeType) {
1250
+ return {
1251
+ match: "subject",
1252
+ reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
1253
+ };
1254
+ }
1255
+ if (node.type === rule.toNodeType) {
1256
+ return {
1257
+ match: "region",
1258
+ reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
1259
+ };
1260
+ }
1261
+ return null;
1262
+ }
1263
+ case "ownership": {
1264
+ if (node.type === rule.nodeType) {
1265
+ return {
1266
+ match: "subject",
1267
+ reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
1268
+ };
1269
+ }
1270
+ return null;
1271
+ }
1272
+ case "blast-radius": {
1273
+ const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
1274
+ if (node.type === rule.nodeType) {
1275
+ return {
1276
+ match: "subject",
1277
+ reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
1278
+ };
1279
+ }
1280
+ const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
1281
+ if (subject) {
1282
+ return {
1283
+ match: "region",
1284
+ reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
1285
+ };
1286
+ }
1287
+ return null;
1288
+ }
1289
+ case "compatibility": {
1290
+ const kindLabel = rule.kind ?? "all compat shapes";
1291
+ if (node.type === NodeType2.ServiceNode) {
1292
+ return {
1293
+ match: "subject",
1294
+ reason: `this service's dependencies are compatibility-checked (${kindLabel})`
1295
+ };
1296
+ }
1297
+ const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
1298
+ if (reachesDriverEngine && node.type === NodeType2.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, EdgeType2.CONNECTS_TO)) {
1299
+ return {
1300
+ match: "region",
1301
+ reason: "services connecting to this database have their driver/engine compatibility checked against it"
1302
+ };
1303
+ }
1304
+ return null;
1305
+ }
1306
+ case "provenance": {
1307
+ const requiredList = requiredProvenanceList(rule.required);
1308
+ if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
1309
+ return {
1310
+ match: "subject",
1311
+ reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
1312
+ };
1313
+ }
1314
+ if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
1315
+ return {
1316
+ match: "region",
1317
+ reason: rule.targetNodeId !== void 0 ? `this node sits on a ${rule.edgeType} edge to ${rule.targetNodeId}, which must carry ${requiredList} provenance` : `this node sits on a ${rule.edgeType} edge, which must carry ${requiredList} provenance`
1318
+ };
1319
+ }
1320
+ return null;
1321
+ }
1322
+ }
1323
+ }
1000
1324
  async function loadPolicyFile(policyPath) {
1001
1325
  let raw;
1002
1326
  try {
@@ -1050,7 +1374,7 @@ var PolicyViolationsLog = class {
1050
1374
  import {
1051
1375
  EdgeType as EdgeType3,
1052
1376
  NodeType as NodeType3,
1053
- Provenance,
1377
+ Provenance as Provenance2,
1054
1378
  confidenceForObservedSignal,
1055
1379
  databaseId,
1056
1380
  extractedEdgeId,
@@ -1117,9 +1441,9 @@ function loadIncidentThresholdsFromEnv() {
1117
1441
  return DEFAULT_INCIDENT_THRESHOLDS;
1118
1442
  }
1119
1443
  }
1120
- function httpResponseStatus(span) {
1444
+ function httpResponseStatusFromAttrs(attrs) {
1121
1445
  for (const key of ["http.response.status_code", "http.status_code"]) {
1122
- const v = span.attributes[key];
1446
+ const v = attrs[key];
1123
1447
  if (typeof v === "number" && Number.isFinite(v)) return v;
1124
1448
  if (typeof v === "string") {
1125
1449
  const n = Number(v);
@@ -1128,6 +1452,63 @@ function httpResponseStatus(span) {
1128
1452
  }
1129
1453
  return void 0;
1130
1454
  }
1455
+ function httpResponseStatus(span) {
1456
+ return httpResponseStatusFromAttrs(span.attributes);
1457
+ }
1458
+ function httpFailureMessageFromAttrs(attrs) {
1459
+ const status = httpResponseStatusFromAttrs(attrs);
1460
+ const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
1461
+ const method = pickAttrFrom(attrs, "http.request.method", "http.method");
1462
+ const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
1463
+ if (status !== void 0 && where) return `${status} on ${where}`;
1464
+ if (status !== void 0) return `HTTP ${status}`;
1465
+ if (where) return `error on ${where}`;
1466
+ return void 0;
1467
+ }
1468
+ var GRPC_STATUS_NAMES = {
1469
+ 1: "CANCELLED",
1470
+ 2: "UNKNOWN",
1471
+ 3: "INVALID_ARGUMENT",
1472
+ 4: "DEADLINE_EXCEEDED",
1473
+ 5: "NOT_FOUND",
1474
+ 6: "ALREADY_EXISTS",
1475
+ 7: "PERMISSION_DENIED",
1476
+ 8: "RESOURCE_EXHAUSTED",
1477
+ 9: "FAILED_PRECONDITION",
1478
+ 10: "ABORTED",
1479
+ 11: "OUT_OF_RANGE",
1480
+ 12: "UNIMPLEMENTED",
1481
+ 13: "INTERNAL",
1482
+ 14: "UNAVAILABLE",
1483
+ 15: "DATA_LOSS",
1484
+ 16: "UNAUTHENTICATED"
1485
+ };
1486
+ function grpcStatusCodeFromAttrs(attrs) {
1487
+ const v = attrs["rpc.grpc.status_code"];
1488
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1489
+ if (typeof v === "string") {
1490
+ const n = Number(v);
1491
+ if (Number.isFinite(n)) return n;
1492
+ }
1493
+ return void 0;
1494
+ }
1495
+ function nonHttpFailureMessageFromAttrs(attrs) {
1496
+ const grpc = grpcStatusCodeFromAttrs(attrs);
1497
+ if (grpc !== void 0 && grpc !== 0) {
1498
+ const name = GRPC_STATUS_NAMES[grpc] ?? `status ${grpc}`;
1499
+ const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
1500
+ return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
1501
+ }
1502
+ const errType = pickAttrFrom(attrs, "error.type");
1503
+ if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
1504
+ const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
1505
+ return peer ? `${errType} connecting to ${peer}` : errType;
1506
+ }
1507
+ return void 0;
1508
+ }
1509
+ function incidentMessage(span) {
1510
+ return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
1511
+ }
1131
1512
  function nowIso(ctx) {
1132
1513
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1133
1514
  }
@@ -1147,13 +1528,16 @@ function warnNoSourceMaps(serviceName) {
1147
1528
  `[neat] ${serviceName}: no .map files found under dist/; observed file edges will land on dist paths, not src. Set sourceMap: true in tsconfig to enable file-level reconciliation.`
1148
1529
  );
1149
1530
  }
1150
- function pickAttr(span, ...keys) {
1531
+ function pickAttrFrom(attrs, ...keys) {
1151
1532
  for (const k of keys) {
1152
- const v = span.attributes[k];
1533
+ const v = attrs[k];
1153
1534
  if (typeof v === "string" && v.length > 0) return v;
1154
1535
  }
1155
1536
  return void 0;
1156
1537
  }
1538
+ function pickAttr(span, ...keys) {
1539
+ return pickAttrFrom(span.attributes, ...keys);
1540
+ }
1157
1541
  function hostFromUrl(u) {
1158
1542
  if (!u) return void 0;
1159
1543
  try {
@@ -1165,6 +1549,10 @@ function hostFromUrl(u) {
1165
1549
  function pickAddress(span) {
1166
1550
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1167
1551
  }
1552
+ function isLoopbackHost(host) {
1553
+ const h = host.toLowerCase();
1554
+ return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
1555
+ }
1168
1556
  var CODE_FILEPATH_ATTR = "code.filepath";
1169
1557
  var CODE_LINENO_ATTR = "code.lineno";
1170
1558
  var CODE_FUNCTION_ATTR = "code.function";
@@ -1272,15 +1660,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
1272
1660
  ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1273
1661
  };
1274
1662
  }
1663
+ function reconcileObservedRelPath(graph, serviceName, relPath) {
1664
+ if (graph.hasNode(fileId(serviceName, relPath))) return relPath;
1665
+ let best = null;
1666
+ graph.forEachNode((_id, attrs) => {
1667
+ const a = attrs;
1668
+ if (a.type !== NodeType3.FileNode || a.service !== serviceName) return;
1669
+ if (a.discoveredVia === "otel") return;
1670
+ const p = a.path;
1671
+ if (!p) return;
1672
+ if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
1673
+ best = p;
1674
+ }
1675
+ });
1676
+ return best ?? relPath;
1677
+ }
1275
1678
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1276
- const fileNodeId = fileId(serviceName, callSite.relPath);
1679
+ const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
1680
+ const fileNodeId = fileId(serviceName, relPath);
1277
1681
  if (!graph.hasNode(fileNodeId)) {
1278
- const language = languageForExt(callSite.relPath);
1682
+ const language = languageForExt(relPath);
1279
1683
  const node = {
1280
1684
  id: fileNodeId,
1281
1685
  type: NodeType3.FileNode,
1282
1686
  service: serviceName,
1283
- path: callSite.relPath,
1687
+ path: relPath,
1284
1688
  ...language ? { language } : {},
1285
1689
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1286
1690
  discoveredVia: "otel"
@@ -1294,7 +1698,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1294
1698
  source: serviceNodeId,
1295
1699
  target: fileNodeId,
1296
1700
  type: EdgeType3.CONTAINS,
1297
- provenance: Provenance.OBSERVED
1701
+ provenance: Provenance2.OBSERVED
1298
1702
  };
1299
1703
  graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1300
1704
  }
@@ -1308,6 +1712,11 @@ function makeInferredEdgeId(type, source, target) {
1308
1712
  }
1309
1713
  var INFERRED_CONFIDENCE = 0.6;
1310
1714
  var STITCH_MAX_DEPTH = 2;
1715
+ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
1716
+ EdgeType3.CALLS,
1717
+ EdgeType3.CONNECTS_TO,
1718
+ EdgeType3.DEPENDS_ON
1719
+ ]);
1311
1720
  var WIRE_SPAN_KIND_CLIENT = 3;
1312
1721
  var WIRE_SPAN_KIND_PRODUCER = 4;
1313
1722
  function spanMintsObservedEdge(kind) {
@@ -1439,7 +1848,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
1439
1848
  };
1440
1849
  const updated = {
1441
1850
  ...existing,
1442
- provenance: Provenance.OBSERVED,
1851
+ provenance: Provenance2.OBSERVED,
1443
1852
  lastObserved: ts,
1444
1853
  callCount: newSpanCount,
1445
1854
  signal: newSignal,
@@ -1458,7 +1867,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
1458
1867
  source,
1459
1868
  target,
1460
1869
  type,
1461
- provenance: Provenance.OBSERVED,
1870
+ provenance: Provenance2.OBSERVED,
1462
1871
  confidence: confidenceForObservedSignal(signal),
1463
1872
  lastObserved: ts,
1464
1873
  callCount: 1,
@@ -1480,7 +1889,8 @@ function stitchTrace(graph, sourceServiceId, ts) {
1480
1889
  const outbound = graph.outboundEdges(nodeId);
1481
1890
  for (const edgeId of outbound) {
1482
1891
  const edge = graph.getEdgeAttributes(edgeId);
1483
- if (edge.provenance !== Provenance.EXTRACTED) continue;
1892
+ if (edge.provenance !== Provenance2.EXTRACTED) continue;
1893
+ if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
1484
1894
  if (graph.hasEdge(observedEdgeId(edge.source, edge.target, edge.type))) continue;
1485
1895
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
1486
1896
  if (!visited.has(edge.target)) {
@@ -1503,7 +1913,7 @@ function upsertInferredEdge(graph, type, source, target, ts) {
1503
1913
  source,
1504
1914
  target,
1505
1915
  type,
1506
- provenance: Provenance.INFERRED,
1916
+ provenance: Provenance2.INFERRED,
1507
1917
  confidence: INFERRED_CONFIDENCE,
1508
1918
  lastObserved: ts
1509
1919
  };
@@ -1513,6 +1923,16 @@ async function appendErrorEvent(ctx, ev) {
1513
1923
  await fs3.mkdir(path3.dirname(ctx.errorsPath), { recursive: true });
1514
1924
  await fs3.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
1515
1925
  }
1926
+ function incidentAffectedNode(span, graph, scanPath) {
1927
+ const sid = serviceId(span.service, span.env);
1928
+ const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
1929
+ const callSite = callSiteFromSpan(span, serviceNode, scanPath);
1930
+ if (callSite) {
1931
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
1932
+ return fileId(span.service, relPath);
1933
+ }
1934
+ return sid;
1935
+ }
1516
1936
  function sanitizeAttributes(attrs) {
1517
1937
  const out = {};
1518
1938
  for (const [k, v] of Object.entries(attrs)) {
@@ -1521,7 +1941,7 @@ function sanitizeAttributes(attrs) {
1521
1941
  }
1522
1942
  return out;
1523
1943
  }
1524
- function buildErrorEventForReceiver(span) {
1944
+ function buildErrorEventForReceiver(span, graph, scanPath) {
1525
1945
  if (span.statusCode !== 2) return null;
1526
1946
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
1527
1947
  const attrs = sanitizeAttributes(span.attributes);
@@ -1531,16 +1951,16 @@ function buildErrorEventForReceiver(span) {
1531
1951
  service: span.service,
1532
1952
  traceId: span.traceId,
1533
1953
  spanId: span.spanId,
1534
- errorMessage: span.exception?.message ?? "unknown error",
1954
+ errorMessage: incidentMessage(span),
1535
1955
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1536
1956
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1537
1957
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1538
- affectedNode: serviceId(span.service, span.env)
1958
+ affectedNode: incidentAffectedNode(span, graph, scanPath)
1539
1959
  };
1540
1960
  }
1541
- function makeErrorSpanWriter(errorsPath) {
1961
+ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
1542
1962
  return async (span) => {
1543
- const ev = buildErrorEventForReceiver(span);
1963
+ const ev = buildErrorEventForReceiver(span, graph, scanPath);
1544
1964
  if (!ev) return;
1545
1965
  await fs3.mkdir(path3.dirname(errorsPath), { recursive: true });
1546
1966
  await fs3.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
@@ -1568,6 +1988,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
1568
1988
  };
1569
1989
  await appendErrorEvent(ctx, ev);
1570
1990
  }
1991
+ async function recordExceptionIncident(ctx, span, ts) {
1992
+ const attrs = sanitizeAttributes(span.attributes);
1993
+ const ev = {
1994
+ id: `${span.traceId}:${span.spanId}`,
1995
+ timestamp: ts,
1996
+ service: span.service,
1997
+ traceId: span.traceId,
1998
+ spanId: span.spanId,
1999
+ errorMessage: incidentMessage(span),
2000
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2001
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2002
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2003
+ affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
2004
+ };
2005
+ await appendErrorEvent(ctx, ev);
2006
+ }
1571
2007
  async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status) {
1572
2008
  const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
1573
2009
  if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
@@ -1624,7 +2060,10 @@ async function handleSpan(ctx, span) {
1624
2060
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
1625
2061
  cacheSpanService(span, nowMs, callSite);
1626
2062
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1627
- const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2063
+ const callSiteEvidence = callSite ? {
2064
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2065
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2066
+ } : void 0;
1628
2067
  let affectedNode = sourceId;
1629
2068
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
1630
2069
  if (span.dbSystem) {
@@ -1646,7 +2085,7 @@ async function handleSpan(ctx, span) {
1646
2085
  } else {
1647
2086
  const host = pickAddress(span);
1648
2087
  let resolvedViaAddress = false;
1649
- if (mintsFromCallerSide && host && host !== span.service) {
2088
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
1650
2089
  const targetId = resolveServiceId(ctx.graph, host, env);
1651
2090
  if (targetId && targetId !== sourceId) {
1652
2091
  upsertObservedEdge(
@@ -1681,7 +2120,11 @@ async function handleSpan(ctx, span) {
1681
2120
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1682
2121
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
1683
2122
  const fallbackEvidence = parent.callSite ? {
1684
- file: parent.callSite.relPath,
2123
+ file: reconcileObservedRelPath(
2124
+ ctx.graph,
2125
+ parent.service,
2126
+ parent.callSite.relPath
2127
+ ),
1685
2128
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
1686
2129
  } : void 0;
1687
2130
  upsertObservedEdge(
@@ -1706,7 +2149,7 @@ async function handleSpan(ctx, span) {
1706
2149
  service: span.service,
1707
2150
  traceId: span.traceId,
1708
2151
  spanId: span.spanId,
1709
- errorMessage: span.exception?.message ?? "unknown error",
2152
+ errorMessage: incidentMessage(span),
1710
2153
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1711
2154
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1712
2155
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -1717,7 +2160,9 @@ async function handleSpan(ctx, span) {
1717
2160
  }
1718
2161
  if (span.statusCode !== 2) {
1719
2162
  const status = httpResponseStatus(span);
1720
- if (status !== void 0 && status >= 500) {
2163
+ if (span.exception) {
2164
+ await recordExceptionIncident(ctx, span, ts);
2165
+ } else if (status !== void 0 && status >= 500) {
1721
2166
  await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
1722
2167
  } else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
1723
2168
  await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
@@ -1773,7 +2218,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
1773
2218
  }
1774
2219
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1775
2220
  graph.dropEdge(oldEdgeId);
1776
- const newId = edge.provenance === Provenance.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId(newSource, newTarget, edge.type);
2221
+ const newId = edge.provenance === Provenance2.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance2.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId(newSource, newTarget, edge.type);
1777
2222
  if (graph.hasEdge(newId)) {
1778
2223
  const existing = graph.getEdgeAttributes(newId);
1779
2224
  const merged = {
@@ -1807,12 +2252,12 @@ async function markStaleEdges(graph, options = {}) {
1807
2252
  const project = options.project ?? DEFAULT_PROJECT;
1808
2253
  graph.forEachEdge((id, attrs) => {
1809
2254
  const e = attrs;
1810
- if (e.provenance !== Provenance.OBSERVED) return;
2255
+ if (e.provenance !== Provenance2.OBSERVED) return;
1811
2256
  if (!e.lastObserved) return;
1812
2257
  const threshold = thresholdForEdgeType(e.type, thresholds);
1813
2258
  const age = now - new Date(e.lastObserved).getTime();
1814
2259
  if (age > threshold) {
1815
- const updated = { ...e, provenance: Provenance.STALE, confidence: 0.3 };
2260
+ const updated = { ...e, provenance: Provenance2.STALE, confidence: 0.3 };
1816
2261
  graph.replaceEdgeAttributes(id, updated);
1817
2262
  events.push({
1818
2263
  edgeId: id,
@@ -1829,8 +2274,8 @@ async function markStaleEdges(graph, options = {}) {
1829
2274
  project,
1830
2275
  payload: {
1831
2276
  edgeId: id,
1832
- from: Provenance.OBSERVED,
1833
- to: Provenance.STALE
2277
+ from: Provenance2.OBSERVED,
2278
+ to: Provenance2.STALE
1834
2279
  }
1835
2280
  });
1836
2281
  }
@@ -1882,12 +2327,43 @@ function startStalenessLoop(graph, options = {}) {
1882
2327
  async function readErrorEvents(errorsPath) {
1883
2328
  try {
1884
2329
  const raw = await fs3.readFile(errorsPath, "utf8");
1885
- return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2330
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2331
+ return dedupeIncidents(events);
1886
2332
  } catch (err) {
1887
2333
  if (err.code === "ENOENT") return [];
1888
2334
  throw err;
1889
2335
  }
1890
2336
  }
2337
+ function isSynthesizedHttpIncident(ev) {
2338
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2339
+ if (ev.errorType) return false;
2340
+ if (!ev.attributes) return false;
2341
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2342
+ return synth !== void 0 && synth === ev.errorMessage;
2343
+ }
2344
+ function dedupeIncidents(events) {
2345
+ const seen = /* @__PURE__ */ new Set();
2346
+ const once = [];
2347
+ for (const ev of events) {
2348
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2349
+ if (key === void 0) {
2350
+ once.push(ev);
2351
+ continue;
2352
+ }
2353
+ if (seen.has(key)) continue;
2354
+ seen.add(key);
2355
+ once.push(ev);
2356
+ }
2357
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2358
+ const hasRealFailure = /* @__PURE__ */ new Set();
2359
+ for (const ev of once) {
2360
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2361
+ }
2362
+ return once.filter((ev) => {
2363
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
2364
+ return !hasRealFailure.has(groupKey(ev));
2365
+ });
2366
+ }
1891
2367
  function mergeSnapshot(graph, snapshot) {
1892
2368
  const exported = snapshot.graph;
1893
2369
  let nodesAdded = 0;
@@ -2021,10 +2497,17 @@ function isConfigFile(name) {
2021
2497
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2022
2498
  if (name === ".env" || name.startsWith(".env.")) {
2023
2499
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2500
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2024
2501
  return { match: true, fileType: "env" };
2025
2502
  }
2026
2503
  return { match: false, fileType: "" };
2027
2504
  }
2505
+ function isNeatAuthoredEnvFile(name) {
2506
+ return name === ".env.neat";
2507
+ }
2508
+ function isNeatAuthoredSourceFile(name) {
2509
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
2510
+ }
2028
2511
  function isTestPath(filePath) {
2029
2512
  const normalised = filePath.replace(/\\/g, "/");
2030
2513
  const segments = normalised.split("/");
@@ -2659,7 +3142,7 @@ import path10 from "path";
2659
3142
  import {
2660
3143
  EdgeType as EdgeType4,
2661
3144
  NodeType as NodeType6,
2662
- Provenance as Provenance2,
3145
+ Provenance as Provenance3,
2663
3146
  confidenceForExtracted,
2664
3147
  extractedEdgeId as extractedEdgeId3,
2665
3148
  fileId as fileId2
@@ -2674,7 +3157,9 @@ async function walkSourceFiles(dir) {
2674
3157
  if (IGNORED_DIRS.has(entry.name)) continue;
2675
3158
  if (await isPythonVenvDir(full)) continue;
2676
3159
  await walk(full);
2677
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))) {
3160
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3161
+ // would attribute our instrumentation imports to the user's service.
3162
+ !isNeatAuthoredSourceFile(entry.name)) {
2678
3163
  out.push(full);
2679
3164
  }
2680
3165
  }
@@ -2746,7 +3231,7 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
2746
3231
  source: serviceNodeId,
2747
3232
  target: fileNodeId,
2748
3233
  type: EdgeType4.CONTAINS,
2749
- provenance: Provenance2.EXTRACTED,
3234
+ provenance: Provenance3.EXTRACTED,
2750
3235
  confidence: confidenceForExtracted("structural"),
2751
3236
  evidence: { file: relPath }
2752
3237
  };
@@ -2786,7 +3271,7 @@ import JavaScript from "tree-sitter-javascript";
2786
3271
  import Python from "tree-sitter-python";
2787
3272
  import {
2788
3273
  EdgeType as EdgeType5,
2789
- Provenance as Provenance3,
3274
+ Provenance as Provenance4,
2790
3275
  confidenceForExtracted as confidenceForExtracted2,
2791
3276
  extractedEdgeId as extractedEdgeId4,
2792
3277
  fileId as fileId3
@@ -2849,11 +3334,28 @@ function collectJsImports(node, out) {
2849
3334
  if (child) collectJsImports(child, out);
2850
3335
  }
2851
3336
  }
3337
+ function collectImportedNames(node, out) {
3338
+ if (node.type === "aliased_import") {
3339
+ const nameNode = node.childForFieldName("name");
3340
+ if (nameNode) out.push(nameNode.text);
3341
+ return;
3342
+ }
3343
+ if (node.type === "dotted_name") {
3344
+ out.push(node.text);
3345
+ return;
3346
+ }
3347
+ for (let i = 0; i < node.namedChildCount; i++) {
3348
+ const child = node.namedChild(i);
3349
+ if (child) collectImportedNames(child, out);
3350
+ }
3351
+ }
2852
3352
  function collectPyImports(node, out) {
2853
3353
  if (node.type === "import_from_statement") {
2854
3354
  let level = 0;
2855
3355
  let modulePath = "";
3356
+ const names = [];
2856
3357
  let pastFrom = false;
3358
+ let pastImport = false;
2857
3359
  for (let i = 0; i < node.childCount; i++) {
2858
3360
  const child = node.child(i);
2859
3361
  if (!child) continue;
@@ -2861,26 +3363,30 @@ function collectPyImports(node, out) {
2861
3363
  if (child.type === "from") pastFrom = true;
2862
3364
  continue;
2863
3365
  }
2864
- if (child.type === "import") break;
2865
- if (child.type === "relative_import") {
2866
- for (let j = 0; j < child.childCount; j++) {
2867
- const rc = child.child(j);
2868
- if (!rc) continue;
2869
- if (rc.type === "import_prefix") {
2870
- for (let k = 0; k < rc.childCount; k++) {
2871
- if (rc.child(k)?.type === ".") level++;
2872
- }
2873
- } else if (rc.type === "dotted_name") modulePath = rc.text;
3366
+ if (!pastImport) {
3367
+ if (child.type === "import") {
3368
+ pastImport = true;
3369
+ continue;
2874
3370
  }
2875
- break;
2876
- }
2877
- if (child.type === "dotted_name") {
2878
- modulePath = child.text;
2879
- break;
3371
+ if (child.type === "relative_import") {
3372
+ for (let j = 0; j < child.childCount; j++) {
3373
+ const rc = child.child(j);
3374
+ if (!rc) continue;
3375
+ if (rc.type === "import_prefix") {
3376
+ for (let k = 0; k < rc.childCount; k++) {
3377
+ if (rc.child(k)?.type === ".") level++;
3378
+ }
3379
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3380
+ }
3381
+ } else if (child.type === "dotted_name") {
3382
+ modulePath = child.text;
3383
+ }
3384
+ continue;
2880
3385
  }
3386
+ collectImportedNames(child, names);
2881
3387
  }
2882
3388
  if (level > 0 || modulePath) {
2883
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3389
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2884
3390
  }
2885
3391
  }
2886
3392
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -2982,8 +3488,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
2982
3488
  return null;
2983
3489
  }
2984
3490
  async function resolvePyImport(imp, importerPath, serviceDir) {
2985
- if (!imp.modulePath) return null;
2986
- const relPath = imp.modulePath.split(".").join("/");
2987
3491
  let baseDir;
2988
3492
  if (imp.level > 0) {
2989
3493
  baseDir = path12.dirname(importerPath);
@@ -2991,13 +3495,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
2991
3495
  } else {
2992
3496
  baseDir = serviceDir;
2993
3497
  }
2994
- const candidates = [path12.join(baseDir, `${relPath}.py`), path12.join(baseDir, relPath, "__init__.py")];
2995
- for (const candidate of candidates) {
2996
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
2997
- return toPosix2(path12.relative(serviceDir, candidate));
3498
+ const moduleBase = imp.modulePath ? path12.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
3499
+ const resolved = /* @__PURE__ */ new Set();
3500
+ let needModuleFile = imp.names.length === 0;
3501
+ for (const name of imp.names) {
3502
+ const submoduleFile = path12.join(moduleBase, `${name}.py`);
3503
+ const subpackageInit = path12.join(moduleBase, name, "__init__.py");
3504
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
3505
+ resolved.add(toPosix2(path12.relative(serviceDir, submoduleFile)));
3506
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
3507
+ resolved.add(toPosix2(path12.relative(serviceDir, subpackageInit)));
3508
+ } else {
3509
+ needModuleFile = true;
3510
+ }
3511
+ }
3512
+ if (needModuleFile) {
3513
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, path12.join(moduleBase, "__init__.py")] : [path12.join(moduleBase, "__init__.py")];
3514
+ for (const candidate of moduleFileCandidates) {
3515
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3516
+ resolved.add(toPosix2(path12.relative(serviceDir, candidate)));
3517
+ break;
3518
+ }
2998
3519
  }
2999
3520
  }
3000
- return null;
3521
+ return [...resolved];
3001
3522
  }
3002
3523
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3003
3524
  const importeeFileId = fileId3(serviceName, importeeRelPath);
@@ -3009,7 +3530,7 @@ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, imp
3009
3530
  source: importerFileId,
3010
3531
  target: importeeFileId,
3011
3532
  type: EdgeType5.IMPORTS,
3012
- provenance: Provenance3.EXTRACTED,
3533
+ provenance: Provenance4.EXTRACTED,
3013
3534
  confidence: confidenceForExtracted2("structural"),
3014
3535
  evidence: { file: importerRelPath, line, snippet: snippet2 }
3015
3536
  };
@@ -3038,17 +3559,18 @@ async function addImports(graph, services) {
3038
3559
  continue;
3039
3560
  }
3040
3561
  for (const imp of pyImports) {
3041
- const resolved = await resolvePyImport(imp, file.path, service.dir);
3042
- if (!resolved) continue;
3043
- edgesAdded += emitImportEdge(
3044
- graph,
3045
- service.pkg.name,
3046
- importerFileId,
3047
- relFile,
3048
- resolved,
3049
- imp.line,
3050
- imp.snippet
3051
- );
3562
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
3563
+ for (const resolved of resolvedPaths) {
3564
+ edgesAdded += emitImportEdge(
3565
+ graph,
3566
+ service.pkg.name,
3567
+ importerFileId,
3568
+ relFile,
3569
+ resolved,
3570
+ imp.line,
3571
+ imp.snippet
3572
+ );
3573
+ }
3052
3574
  }
3053
3575
  continue;
3054
3576
  }
@@ -3083,7 +3605,8 @@ import path20 from "path";
3083
3605
  import {
3084
3606
  EdgeType as EdgeType6,
3085
3607
  NodeType as NodeType7,
3086
- Provenance as Provenance4,
3608
+ Provenance as Provenance5,
3609
+ configId,
3087
3610
  databaseId as databaseId2,
3088
3611
  confidenceForExtracted as confidenceForExtracted3
3089
3612
  } from "@neat.is/types";
@@ -3670,7 +4193,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3670
4193
  source: fileNodeId,
3671
4194
  target: dbNode.id,
3672
4195
  type: EdgeType6.CONNECTS_TO,
3673
- provenance: Provenance4.EXTRACTED,
4196
+ provenance: Provenance5.EXTRACTED,
3674
4197
  confidence: confidenceForExtracted3("structural"),
3675
4198
  evidence: { file: evidenceFile }
3676
4199
  };
@@ -3679,6 +4202,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3679
4202
  edgesAdded++;
3680
4203
  }
3681
4204
  }
4205
+ if (allConfigs.length === 1) {
4206
+ const primary = allConfigs[0];
4207
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
4208
+ const relPath = path20.relative(scanPath, primary.sourceFile);
4209
+ const cfgId = configId(relPath);
4210
+ if (!graph.hasNode(cfgId)) {
4211
+ const cfgNode = {
4212
+ id: cfgId,
4213
+ type: NodeType7.ConfigNode,
4214
+ name: path20.basename(primary.sourceFile),
4215
+ path: relPath,
4216
+ fileType: isConfigFile(path20.basename(primary.sourceFile)).fileType || "config"
4217
+ };
4218
+ graph.addNode(cfgId, cfgNode);
4219
+ nodesAdded++;
4220
+ }
4221
+ const cfgEdge = {
4222
+ id: extractedEdgeId2(service.node.id, cfgId, EdgeType6.CONFIGURED_BY),
4223
+ source: service.node.id,
4224
+ target: cfgId,
4225
+ type: EdgeType6.CONFIGURED_BY,
4226
+ provenance: Provenance5.EXTRACTED,
4227
+ confidence: confidenceForExtracted3("structural"),
4228
+ evidence: { file: toPosix2(relPath) }
4229
+ };
4230
+ if (!graph.hasEdge(cfgEdge.id)) {
4231
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
4232
+ edgesAdded++;
4233
+ }
4234
+ }
3682
4235
  attachIncompatibilities(service, allConfigs);
3683
4236
  if (graph.hasNode(service.node.id)) {
3684
4237
  const current = graph.getNodeAttributes(service.node.id);
@@ -3690,6 +4243,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3690
4243
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
3691
4244
  delete updated.incompatibilities;
3692
4245
  }
4246
+ if (!service.node.dbConnectionTarget) {
4247
+ delete updated.dbConnectionTarget;
4248
+ }
3693
4249
  graph.replaceNodeAttributes(service.node.id, updated);
3694
4250
  }
3695
4251
  }
@@ -3702,8 +4258,8 @@ import path21 from "path";
3702
4258
  import {
3703
4259
  EdgeType as EdgeType7,
3704
4260
  NodeType as NodeType8,
3705
- Provenance as Provenance5,
3706
- configId,
4261
+ Provenance as Provenance6,
4262
+ configId as configId2,
3707
4263
  confidenceForExtracted as confidenceForExtracted4
3708
4264
  } from "@neat.is/types";
3709
4265
  async function walkConfigFiles(dir) {
@@ -3732,7 +4288,7 @@ async function addConfigNodes(graph, services, scanPath) {
3732
4288
  for (const file of configFiles) {
3733
4289
  const relPath = path21.relative(scanPath, file);
3734
4290
  const node = {
3735
- id: configId(relPath),
4291
+ id: configId2(relPath),
3736
4292
  type: NodeType8.ConfigNode,
3737
4293
  name: path21.basename(file),
3738
4294
  path: relPath,
@@ -3756,7 +4312,7 @@ async function addConfigNodes(graph, services, scanPath) {
3756
4312
  source: fileNodeId,
3757
4313
  target: node.id,
3758
4314
  type: EdgeType7.CONFIGURED_BY,
3759
- provenance: Provenance5.EXTRACTED,
4315
+ provenance: Provenance6.EXTRACTED,
3760
4316
  confidence: confidenceForExtracted4("structural"),
3761
4317
  evidence: { file: relPath.split(path21.sep).join("/") }
3762
4318
  };
@@ -3773,7 +4329,7 @@ async function addConfigNodes(graph, services, scanPath) {
3773
4329
  import {
3774
4330
  EdgeType as EdgeType9,
3775
4331
  NodeType as NodeType9,
3776
- Provenance as Provenance7,
4332
+ Provenance as Provenance8,
3777
4333
  confidenceForExtracted as confidenceForExtracted6,
3778
4334
  passesExtractedFloor as passesExtractedFloor2
3779
4335
  } from "@neat.is/types";
@@ -3785,7 +4341,7 @@ import JavaScript2 from "tree-sitter-javascript";
3785
4341
  import Python2 from "tree-sitter-python";
3786
4342
  import {
3787
4343
  EdgeType as EdgeType8,
3788
- Provenance as Provenance6,
4344
+ Provenance as Provenance7,
3789
4345
  confidenceForExtracted as confidenceForExtracted5,
3790
4346
  passesExtractedFloor
3791
4347
  } from "@neat.is/types";
@@ -3881,7 +4437,7 @@ async function addHttpCallEdges(graph, services) {
3881
4437
  const dedupKey = `${relFile}|${targetId}`;
3882
4438
  if (seen.has(dedupKey)) continue;
3883
4439
  seen.add(dedupKey);
3884
- const confidence = confidenceForExtracted5("hostname-shape-match");
4440
+ const confidence = confidenceForExtracted5("url-literal-service-target");
3885
4441
  const ev = {
3886
4442
  file: relFile,
3887
4443
  line: site.line,
@@ -3901,7 +4457,7 @@ async function addHttpCallEdges(graph, services) {
3901
4457
  target: targetId,
3902
4458
  type: EdgeType8.CALLS,
3903
4459
  confidence,
3904
- confidenceKind: "hostname-shape-match",
4460
+ confidenceKind: "url-literal-service-target",
3905
4461
  evidence: ev
3906
4462
  });
3907
4463
  continue;
@@ -3913,7 +4469,7 @@ async function addHttpCallEdges(graph, services) {
3913
4469
  source: fileNodeId,
3914
4470
  target: targetId,
3915
4471
  type: EdgeType8.CALLS,
3916
- provenance: Provenance6.EXTRACTED,
4472
+ provenance: Provenance7.EXTRACTED,
3917
4473
  confidence,
3918
4474
  evidence: ev
3919
4475
  };
@@ -4266,7 +4822,7 @@ async function addExternalEndpointEdges(graph, services) {
4266
4822
  source: fileNodeId,
4267
4823
  target: ep.infraId,
4268
4824
  type: edgeType,
4269
- provenance: Provenance7.EXTRACTED,
4825
+ provenance: Provenance8.EXTRACTED,
4270
4826
  confidence,
4271
4827
  evidence: ep.evidence
4272
4828
  };
@@ -4288,7 +4844,7 @@ async function addCallEdges(graph, services) {
4288
4844
 
4289
4845
  // src/extract/infra/docker-compose.ts
4290
4846
  import path28 from "path";
4291
- import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
4847
+ import { EdgeType as EdgeType10, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
4292
4848
 
4293
4849
  // src/extract/infra/shared.ts
4294
4850
  import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
@@ -4381,7 +4937,7 @@ async function addComposeInfra(graph, scanPath, services) {
4381
4937
  source: sourceId,
4382
4938
  target: targetId,
4383
4939
  type: EdgeType10.DEPENDS_ON,
4384
- provenance: Provenance8.EXTRACTED,
4940
+ provenance: Provenance9.EXTRACTED,
4385
4941
  confidence: confidenceForExtracted7("structural"),
4386
4942
  evidence: { file: evidenceFile }
4387
4943
  };
@@ -4395,20 +4951,30 @@ async function addComposeInfra(graph, scanPath, services) {
4395
4951
  // src/extract/infra/dockerfile.ts
4396
4952
  import path29 from "path";
4397
4953
  import { promises as fs15 } from "fs";
4398
- import { EdgeType as EdgeType11, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
4399
- function runtimeImage(content) {
4400
- const lines = content.split("\n");
4401
- let last = null;
4402
- for (const raw of lines) {
4954
+ import { EdgeType as EdgeType11, Provenance as Provenance10, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
4955
+ function readDockerfile(content) {
4956
+ let image = null;
4957
+ const ports = [];
4958
+ let cmd = null;
4959
+ let entrypoint = null;
4960
+ for (const raw of content.split("\n")) {
4403
4961
  const line = raw.trim();
4404
4962
  if (!line || line.startsWith("#")) continue;
4405
- if (!/^from\s+/i.test(line)) continue;
4406
- const tokens = line.split(/\s+/);
4407
- const image = tokens[1];
4408
- if (!image || image.toLowerCase() === "scratch") continue;
4409
- last = image;
4963
+ if (/^from\s+/i.test(line)) {
4964
+ const candidate = line.split(/\s+/)[1];
4965
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
4966
+ } else if (/^expose\s+/i.test(line)) {
4967
+ for (const token of line.split(/\s+/).slice(1)) {
4968
+ const port = Number.parseInt(token.split("/")[0], 10);
4969
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
4970
+ }
4971
+ } else if (/^entrypoint\s+/i.test(line)) {
4972
+ entrypoint = line;
4973
+ } else if (/^cmd\s+/i.test(line)) {
4974
+ cmd = line;
4975
+ }
4410
4976
  }
4411
- return last;
4977
+ return { image, ports, entrypoint: entrypoint ?? cmd };
4412
4978
  }
4413
4979
  async function addDockerfileRuntimes(graph, services, scanPath) {
4414
4980
  let nodesAdded = 0;
@@ -4427,14 +4993,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4427
4993
  );
4428
4994
  continue;
4429
4995
  }
4430
- const image = runtimeImage(content);
4431
- if (!image) continue;
4432
- const node = makeInfraNode("container-image", image);
4996
+ const facts = readDockerfile(content);
4997
+ if (!facts.image) continue;
4998
+ const node = makeInfraNode("container-image", facts.image);
4433
4999
  if (!graph.hasNode(node.id)) {
4434
5000
  graph.addNode(node.id, node);
4435
5001
  nodesAdded++;
4436
5002
  }
4437
5003
  const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
5004
+ const evidenceFile = toPosix2(path29.relative(scanPath, dockerfilePath));
4438
5005
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4439
5006
  graph,
4440
5007
  service.pkg.name,
@@ -4450,15 +5017,36 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4450
5017
  source: fileNodeId,
4451
5018
  target: node.id,
4452
5019
  type: EdgeType11.RUNS_ON,
4453
- provenance: Provenance9.EXTRACTED,
5020
+ provenance: Provenance10.EXTRACTED,
4454
5021
  confidence: confidenceForExtracted8("structural"),
4455
5022
  evidence: {
4456
- file: toPosix2(path29.relative(scanPath, dockerfilePath))
5023
+ file: evidenceFile,
5024
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
4457
5025
  }
4458
5026
  };
4459
5027
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
4460
5028
  edgesAdded++;
4461
5029
  }
5030
+ for (const port of facts.ports) {
5031
+ const portNode = makeInfraNode("port", String(port));
5032
+ if (!graph.hasNode(portNode.id)) {
5033
+ graph.addNode(portNode.id, portNode);
5034
+ nodesAdded++;
5035
+ }
5036
+ const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType11.CONNECTS_TO);
5037
+ if (graph.hasEdge(portEdgeId)) continue;
5038
+ const portEdge = {
5039
+ id: portEdgeId,
5040
+ source: fileNodeId,
5041
+ target: portNode.id,
5042
+ type: EdgeType11.CONNECTS_TO,
5043
+ provenance: Provenance10.EXTRACTED,
5044
+ confidence: confidenceForExtracted8("structural"),
5045
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5046
+ };
5047
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
5048
+ edgesAdded++;
5049
+ }
4462
5050
  }
4463
5051
  return { nodesAdded, edgesAdded };
4464
5052
  }
@@ -4466,7 +5054,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4466
5054
  // src/extract/infra/terraform.ts
4467
5055
  import { promises as fs16 } from "fs";
4468
5056
  import path30 from "path";
5057
+ import { EdgeType as EdgeType12, Provenance as Provenance11, confidenceForExtracted as confidenceForExtracted9 } from "@neat.is/types";
4469
5058
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5059
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
4470
5060
  async function walkTfFiles(start, depth = 0, max = 5) {
4471
5061
  if (depth > max) return [];
4472
5062
  const out = [];
@@ -4483,24 +5073,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4483
5073
  }
4484
5074
  return out;
4485
5075
  }
5076
+ function blockBody(content, from) {
5077
+ const open = content.indexOf("{", from);
5078
+ if (open === -1) return null;
5079
+ let depth = 0;
5080
+ for (let i = open; i < content.length; i++) {
5081
+ const ch = content[i];
5082
+ if (ch === "{") depth++;
5083
+ else if (ch === "}") {
5084
+ depth--;
5085
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
5086
+ }
5087
+ }
5088
+ return null;
5089
+ }
5090
+ function lineAt(content, index) {
5091
+ let line = 1;
5092
+ for (let i = 0; i < index && i < content.length; i++) {
5093
+ if (content[i] === "\n") line++;
5094
+ }
5095
+ return line;
5096
+ }
4486
5097
  async function addTerraformResources(graph, scanPath) {
4487
5098
  let nodesAdded = 0;
5099
+ let edgesAdded = 0;
4488
5100
  const files = await walkTfFiles(scanPath);
4489
5101
  for (const file of files) {
4490
5102
  const content = await fs16.readFile(file, "utf8");
5103
+ const evidenceFile = toPosix2(path30.relative(scanPath, file));
5104
+ const resources = [];
5105
+ const byKey = /* @__PURE__ */ new Map();
4491
5106
  RESOURCE_RE.lastIndex = 0;
4492
5107
  let m;
4493
5108
  while ((m = RESOURCE_RE.exec(content)) !== null) {
4494
- const kind = m[1];
5109
+ const type = m[1];
4495
5110
  const name = m[2];
4496
- const node = makeInfraNode(kind, name, "aws");
5111
+ const node = makeInfraNode(type, name, "aws");
4497
5112
  if (!graph.hasNode(node.id)) {
4498
5113
  graph.addNode(node.id, node);
4499
5114
  nodesAdded++;
4500
5115
  }
5116
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
5117
+ const resource = {
5118
+ type,
5119
+ name,
5120
+ nodeId: node.id,
5121
+ body: span?.body ?? "",
5122
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
5123
+ };
5124
+ resources.push(resource);
5125
+ byKey.set(`${type}.${name}`, resource);
5126
+ }
5127
+ for (const resource of resources) {
5128
+ const seen = /* @__PURE__ */ new Set();
5129
+ REFERENCE_RE.lastIndex = 0;
5130
+ let ref;
5131
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
5132
+ const key = `${ref[1]}.${ref[2]}`;
5133
+ if (key === `${resource.type}.${resource.name}`) continue;
5134
+ const target = byKey.get(key);
5135
+ if (!target) continue;
5136
+ if (seen.has(target.nodeId)) continue;
5137
+ seen.add(target.nodeId);
5138
+ const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType12.DEPENDS_ON);
5139
+ if (graph.hasEdge(edgeId)) continue;
5140
+ const line = lineAt(content, resource.bodyOffset + ref.index);
5141
+ const edge = {
5142
+ id: edgeId,
5143
+ source: resource.nodeId,
5144
+ target: target.nodeId,
5145
+ type: EdgeType12.DEPENDS_ON,
5146
+ provenance: Provenance11.EXTRACTED,
5147
+ confidence: confidenceForExtracted9("structural"),
5148
+ evidence: { file: evidenceFile, line, snippet: key }
5149
+ };
5150
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5151
+ edgesAdded++;
5152
+ }
4501
5153
  }
4502
5154
  }
4503
- return { nodesAdded, edgesAdded: 0 };
5155
+ return { nodesAdded, edgesAdded };
4504
5156
  }
4505
5157
 
4506
5158
  // src/extract/infra/k8s.ts
@@ -4576,7 +5228,7 @@ import path33 from "path";
4576
5228
  // src/extract/retire.ts
4577
5229
  import { existsSync as existsSync2 } from "fs";
4578
5230
  import path32 from "path";
4579
- import { NodeType as NodeType11, Provenance as Provenance10 } from "@neat.is/types";
5231
+ import { NodeType as NodeType11, Provenance as Provenance12 } from "@neat.is/types";
4580
5232
  function dropOrphanedFileNodes(graph) {
4581
5233
  const orphans = [];
4582
5234
  graph.forEachNode((id, attrs) => {
@@ -4593,7 +5245,7 @@ function retireEdgesByFile(graph, file) {
4593
5245
  const toDrop = [];
4594
5246
  graph.forEachEdge((id, attrs) => {
4595
5247
  const edge = attrs;
4596
- if (edge.provenance !== Provenance10.EXTRACTED) return;
5248
+ if (edge.provenance !== Provenance12.EXTRACTED) return;
4597
5249
  if (!edge.evidence?.file) return;
4598
5250
  if (edge.evidence.file === normalized) toDrop.push(id);
4599
5251
  });
@@ -4606,7 +5258,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4606
5258
  const bases = [scanPath, ...serviceDirs];
4607
5259
  graph.forEachEdge((id, attrs) => {
4608
5260
  const edge = attrs;
4609
- if (edge.provenance !== Provenance10.EXTRACTED) return;
5261
+ if (edge.provenance !== Provenance12.EXTRACTED) return;
4610
5262
  const evidenceFile = edge.evidence?.file;
4611
5263
  if (!evidenceFile) return;
4612
5264
  if (path32.isAbsolute(evidenceFile)) {
@@ -4687,11 +5339,12 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4687
5339
 
4688
5340
  // src/divergences.ts
4689
5341
  import {
5342
+ databaseId as databaseId3,
4690
5343
  DivergenceResultSchema,
4691
- EdgeType as EdgeType12,
5344
+ EdgeType as EdgeType13,
4692
5345
  NodeType as NodeType12,
4693
5346
  parseEdgeId,
4694
- Provenance as Provenance11
5347
+ Provenance as Provenance13
4695
5348
  } from "@neat.is/types";
4696
5349
  function bucketKey(source, target, type) {
4697
5350
  return `${type}|${source}|${target}`;
@@ -4705,17 +5358,17 @@ function bucketEdges(graph) {
4705
5358
  const key = bucketKey(e.source, e.target, e.type);
4706
5359
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4707
5360
  switch (provenance) {
4708
- case Provenance11.EXTRACTED:
5361
+ case Provenance13.EXTRACTED:
4709
5362
  cur.extracted = e;
4710
5363
  break;
4711
- case Provenance11.OBSERVED:
5364
+ case Provenance13.OBSERVED:
4712
5365
  cur.observed = e;
4713
5366
  break;
4714
- case Provenance11.INFERRED:
5367
+ case Provenance13.INFERRED:
4715
5368
  cur.inferred = e;
4716
5369
  break;
4717
5370
  default:
4718
- if (e.provenance === Provenance11.STALE) cur.stale = e;
5371
+ if (e.provenance === Provenance13.STALE) cur.stale = e;
4719
5372
  }
4720
5373
  buckets.set(key, cur);
4721
5374
  });
@@ -4743,10 +5396,16 @@ function gradedConfidence(edge) {
4743
5396
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
4744
5397
  return clampConfidence(confidenceForEdge(edge));
4745
5398
  }
5399
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
5400
+ EdgeType13.CALLS,
5401
+ EdgeType13.CONNECTS_TO,
5402
+ EdgeType13.PUBLISHES_TO,
5403
+ EdgeType13.CONSUMES_FROM
5404
+ ]);
4746
5405
  function detectMissingDivergences(graph, bucket) {
4747
5406
  const out = [];
4748
- if (bucket.type === EdgeType12.CONTAINS) return out;
4749
- if (bucket.extracted && !bucket.observed) {
5407
+ if (bucket.type === EdgeType13.CONTAINS) return out;
5408
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
4750
5409
  if (!nodeIsFrontier(graph, bucket.target)) {
4751
5410
  out.push({
4752
5411
  type: "missing-observed",
@@ -4786,7 +5445,7 @@ function declaredHostFor(svc) {
4786
5445
  function hasExtractedConfiguredBy(graph, svcId) {
4787
5446
  for (const edgeId of graph.outboundEdges(svcId)) {
4788
5447
  const e = graph.getEdgeAttributes(edgeId);
4789
- if (e.type === EdgeType12.CONFIGURED_BY && e.provenance === Provenance11.EXTRACTED) {
5448
+ if (e.type === EdgeType13.CONFIGURED_BY && e.provenance === Provenance13.EXTRACTED) {
4790
5449
  return true;
4791
5450
  }
4792
5451
  }
@@ -4799,8 +5458,8 @@ function detectHostMismatch(graph, svcId, svc) {
4799
5458
  const out = [];
4800
5459
  for (const edgeId of graph.outboundEdges(svcId)) {
4801
5460
  const edge = graph.getEdgeAttributes(edgeId);
4802
- if (edge.type !== EdgeType12.CONNECTS_TO) continue;
4803
- if (edge.provenance !== Provenance11.OBSERVED) continue;
5461
+ if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5462
+ if (edge.provenance !== Provenance13.OBSERVED) continue;
4804
5463
  const target = graph.getNodeAttributes(edge.target);
4805
5464
  if (target.type !== NodeType12.DatabaseNode) continue;
4806
5465
  const observedHost = target.host?.trim();
@@ -4824,8 +5483,8 @@ function detectCompatDivergences(graph, svcId, svc) {
4824
5483
  const deps = svc.dependencies ?? {};
4825
5484
  for (const edgeId of graph.outboundEdges(svcId)) {
4826
5485
  const edge = graph.getEdgeAttributes(edgeId);
4827
- if (edge.type !== EdgeType12.CONNECTS_TO) continue;
4828
- if (edge.provenance !== Provenance11.OBSERVED) continue;
5486
+ if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5487
+ if (edge.provenance !== Provenance13.OBSERVED) continue;
4829
5488
  const target = graph.getNodeAttributes(edge.target);
4830
5489
  if (target.type !== NodeType12.DatabaseNode) continue;
4831
5490
  for (const pair of compatPairs()) {
@@ -4880,6 +5539,23 @@ function detectCompatDivergences(graph, svcId, svc) {
4880
5539
  function involvesNode(d, nodeId) {
4881
5540
  return d.source === nodeId || d.target === nodeId;
4882
5541
  }
5542
+ function suppressHostMismatchHalves(all) {
5543
+ const observedHalf = /* @__PURE__ */ new Set();
5544
+ const declaredHalf = /* @__PURE__ */ new Set();
5545
+ for (const d of all) {
5546
+ if (d.type !== "host-mismatch") continue;
5547
+ observedHalf.add(`${d.source}->${d.target}`);
5548
+ declaredHalf.add(databaseId3(d.extractedHost));
5549
+ }
5550
+ if (observedHalf.size === 0) return all;
5551
+ return all.filter((d) => {
5552
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
5553
+ return false;
5554
+ }
5555
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
5556
+ return true;
5557
+ });
5558
+ }
4883
5559
  function computeDivergences(graph, opts = {}) {
4884
5560
  const all = [];
4885
5561
  const buckets = bucketEdges(graph);
@@ -4893,7 +5569,8 @@ function computeDivergences(graph, opts = {}) {
4893
5569
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4894
5570
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
4895
5571
  });
4896
- let filtered = all;
5572
+ const reconciled = suppressHostMismatchHalves(all);
5573
+ let filtered = reconciled;
4897
5574
  if (opts.type) {
4898
5575
  const allowed = opts.type;
4899
5576
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -4931,7 +5608,7 @@ function computeDivergences(graph, opts = {}) {
4931
5608
  // src/persist.ts
4932
5609
  import { promises as fs18 } from "fs";
4933
5610
  import path34 from "path";
4934
- import { Provenance as Provenance12, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
5611
+ import { Provenance as Provenance14, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4935
5612
  var SCHEMA_VERSION = 4;
4936
5613
  function migrateV1ToV2(payload) {
4937
5614
  const nodes = payload.graph.nodes;
@@ -4953,7 +5630,7 @@ function migrateV2ToV3(payload) {
4953
5630
  for (const edge of edges) {
4954
5631
  const attrs = edge.attributes;
4955
5632
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4956
- attrs.provenance = Provenance12.OBSERVED;
5633
+ attrs.provenance = Provenance14.OBSERVED;
4957
5634
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4958
5635
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4959
5636
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -6077,6 +6754,18 @@ function registerRoutes(scope, ctx) {
6077
6754
  }
6078
6755
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6079
6756
  });
6757
+ scope.get(
6758
+ "/graph/observed-dependencies/:nodeId",
6759
+ async (req, reply) => {
6760
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6761
+ if (!proj) return;
6762
+ const { nodeId } = req.params;
6763
+ if (!proj.graph.hasNode(nodeId)) {
6764
+ return reply.code(404).send({ error: "node not found", id: nodeId });
6765
+ }
6766
+ return getObservedDependencies(proj.graph, nodeId);
6767
+ }
6768
+ );
6080
6769
  scope.get("/graph/divergences", async (req, reply) => {
6081
6770
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6082
6771
  if (!proj) return;
@@ -6137,23 +6826,29 @@ function registerRoutes(scope, ctx) {
6137
6826
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6138
6827
  return { count: sliced.length, total, events: sliced };
6139
6828
  });
6829
+ const incidentHistoryHandler = async (req, reply) => {
6830
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6831
+ if (!proj) return;
6832
+ const { nodeId } = req.params;
6833
+ if (!proj.graph.hasNode(nodeId)) {
6834
+ reply.code(404).send({ error: "node not found", id: nodeId });
6835
+ return;
6836
+ }
6837
+ const epath = errorsPathFor(proj);
6838
+ if (!epath) return { count: 0, total: 0, events: [] };
6839
+ const events = await readErrorEvents(epath);
6840
+ const filtered = events.filter(
6841
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6842
+ );
6843
+ return { count: filtered.length, total: filtered.length, events: filtered };
6844
+ };
6140
6845
  scope.get(
6141
6846
  "/incidents/:nodeId",
6142
- async (req, reply) => {
6143
- const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6144
- if (!proj) return;
6145
- const { nodeId } = req.params;
6146
- if (!proj.graph.hasNode(nodeId)) {
6147
- return reply.code(404).send({ error: "node not found", id: nodeId });
6148
- }
6149
- const epath = errorsPathFor(proj);
6150
- if (!epath) return { count: 0, total: 0, events: [] };
6151
- const events = await readErrorEvents(epath);
6152
- const filtered = events.filter(
6153
- (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6154
- );
6155
- return { count: filtered.length, total: filtered.length, events: filtered };
6156
- }
6847
+ incidentHistoryHandler
6848
+ );
6849
+ scope.get(
6850
+ "/graph/incident-history/:nodeId",
6851
+ incidentHistoryHandler
6157
6852
  );
6158
6853
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6159
6854
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -6162,16 +6857,16 @@ function registerRoutes(scope, ctx) {
6162
6857
  if (!proj.graph.hasNode(nodeId)) {
6163
6858
  return reply.code(404).send({ error: "node not found", id: nodeId });
6164
6859
  }
6165
- let errorEvent;
6166
6860
  const epath = errorsPathFor(proj);
6167
- if (req.query.errorId && epath) {
6168
- const events = await readErrorEvents(epath);
6169
- errorEvent = events.find((e) => e.id === req.query.errorId);
6861
+ const incidents = epath ? await readErrorEvents(epath) : [];
6862
+ let errorEvent;
6863
+ if (req.query.errorId) {
6864
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
6170
6865
  if (!errorEvent) {
6171
6866
  return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
6172
6867
  }
6173
6868
  }
6174
- const result = getRootCause(proj.graph, nodeId, errorEvent);
6869
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6175
6870
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6176
6871
  return result;
6177
6872
  });
@@ -6316,6 +7011,28 @@ function registerRoutes(scope, ctx) {
6316
7011
  }
6317
7012
  return { violations };
6318
7013
  });
7014
+ scope.get("/policies/applicable", async (req, reply) => {
7015
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7016
+ if (!proj) return;
7017
+ const nodeId = req.query.node;
7018
+ if (!nodeId) {
7019
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7020
+ }
7021
+ const policyPath = ctx.policyFilePathFor(proj);
7022
+ let policies = [];
7023
+ if (policyPath) {
7024
+ try {
7025
+ policies = await loadPolicyFile(policyPath);
7026
+ } catch (err) {
7027
+ return reply.code(400).send({
7028
+ error: "policy.json failed to parse",
7029
+ details: err.message
7030
+ });
7031
+ }
7032
+ }
7033
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7034
+ return { node: nodeId, applicable };
7035
+ });
6319
7036
  scope.post("/policies/check", async (req, reply) => {
6320
7037
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6321
7038
  if (!proj) return;
@@ -6606,6 +7323,7 @@ export {
6606
7323
  discoverServices,
6607
7324
  addServiceNodes,
6608
7325
  addServiceAliases,
7326
+ addFiles,
6609
7327
  addImports,
6610
7328
  addDatabasesAndCompat,
6611
7329
  addConfigNodes,
@@ -6644,4 +7362,4 @@ export {
6644
7362
  pruneRegistry,
6645
7363
  buildApi
6646
7364
  };
6647
- //# sourceMappingURL=chunk-GNAX2CBF.js.map
7365
+ //# sourceMappingURL=chunk-O25KZNZK.js.map