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

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] : [];
617
+ return RootCauseResultSchema.parse({
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:/, "");
564
708
  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
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");
@@ -1624,7 +2044,10 @@ async function handleSpan(ctx, span) {
1624
2044
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
1625
2045
  cacheSpanService(span, nowMs, callSite);
1626
2046
  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;
2047
+ const callSiteEvidence = callSite ? {
2048
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2049
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2050
+ } : void 0;
1628
2051
  let affectedNode = sourceId;
1629
2052
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
1630
2053
  if (span.dbSystem) {
@@ -1646,7 +2069,7 @@ async function handleSpan(ctx, span) {
1646
2069
  } else {
1647
2070
  const host = pickAddress(span);
1648
2071
  let resolvedViaAddress = false;
1649
- if (mintsFromCallerSide && host && host !== span.service) {
2072
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
1650
2073
  const targetId = resolveServiceId(ctx.graph, host, env);
1651
2074
  if (targetId && targetId !== sourceId) {
1652
2075
  upsertObservedEdge(
@@ -1681,7 +2104,11 @@ async function handleSpan(ctx, span) {
1681
2104
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1682
2105
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
1683
2106
  const fallbackEvidence = parent.callSite ? {
1684
- file: parent.callSite.relPath,
2107
+ file: reconcileObservedRelPath(
2108
+ ctx.graph,
2109
+ parent.service,
2110
+ parent.callSite.relPath
2111
+ ),
1685
2112
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
1686
2113
  } : void 0;
1687
2114
  upsertObservedEdge(
@@ -1706,7 +2133,7 @@ async function handleSpan(ctx, span) {
1706
2133
  service: span.service,
1707
2134
  traceId: span.traceId,
1708
2135
  spanId: span.spanId,
1709
- errorMessage: span.exception?.message ?? "unknown error",
2136
+ errorMessage: incidentMessage(span),
1710
2137
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1711
2138
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1712
2139
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -1773,7 +2200,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
1773
2200
  }
1774
2201
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1775
2202
  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);
2203
+ 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
2204
  if (graph.hasEdge(newId)) {
1778
2205
  const existing = graph.getEdgeAttributes(newId);
1779
2206
  const merged = {
@@ -1807,12 +2234,12 @@ async function markStaleEdges(graph, options = {}) {
1807
2234
  const project = options.project ?? DEFAULT_PROJECT;
1808
2235
  graph.forEachEdge((id, attrs) => {
1809
2236
  const e = attrs;
1810
- if (e.provenance !== Provenance.OBSERVED) return;
2237
+ if (e.provenance !== Provenance2.OBSERVED) return;
1811
2238
  if (!e.lastObserved) return;
1812
2239
  const threshold = thresholdForEdgeType(e.type, thresholds);
1813
2240
  const age = now - new Date(e.lastObserved).getTime();
1814
2241
  if (age > threshold) {
1815
- const updated = { ...e, provenance: Provenance.STALE, confidence: 0.3 };
2242
+ const updated = { ...e, provenance: Provenance2.STALE, confidence: 0.3 };
1816
2243
  graph.replaceEdgeAttributes(id, updated);
1817
2244
  events.push({
1818
2245
  edgeId: id,
@@ -1829,8 +2256,8 @@ async function markStaleEdges(graph, options = {}) {
1829
2256
  project,
1830
2257
  payload: {
1831
2258
  edgeId: id,
1832
- from: Provenance.OBSERVED,
1833
- to: Provenance.STALE
2259
+ from: Provenance2.OBSERVED,
2260
+ to: Provenance2.STALE
1834
2261
  }
1835
2262
  });
1836
2263
  }
@@ -1882,12 +2309,43 @@ function startStalenessLoop(graph, options = {}) {
1882
2309
  async function readErrorEvents(errorsPath) {
1883
2310
  try {
1884
2311
  const raw = await fs3.readFile(errorsPath, "utf8");
1885
- return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2312
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2313
+ return dedupeIncidents(events);
1886
2314
  } catch (err) {
1887
2315
  if (err.code === "ENOENT") return [];
1888
2316
  throw err;
1889
2317
  }
1890
2318
  }
2319
+ function isSynthesizedHttpIncident(ev) {
2320
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2321
+ if (ev.errorType) return false;
2322
+ if (!ev.attributes) return false;
2323
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2324
+ return synth !== void 0 && synth === ev.errorMessage;
2325
+ }
2326
+ function dedupeIncidents(events) {
2327
+ const seen = /* @__PURE__ */ new Set();
2328
+ const once = [];
2329
+ for (const ev of events) {
2330
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2331
+ if (key === void 0) {
2332
+ once.push(ev);
2333
+ continue;
2334
+ }
2335
+ if (seen.has(key)) continue;
2336
+ seen.add(key);
2337
+ once.push(ev);
2338
+ }
2339
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2340
+ const hasRealFailure = /* @__PURE__ */ new Set();
2341
+ for (const ev of once) {
2342
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2343
+ }
2344
+ return once.filter((ev) => {
2345
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
2346
+ return !hasRealFailure.has(groupKey(ev));
2347
+ });
2348
+ }
1891
2349
  function mergeSnapshot(graph, snapshot) {
1892
2350
  const exported = snapshot.graph;
1893
2351
  let nodesAdded = 0;
@@ -2021,10 +2479,17 @@ function isConfigFile(name) {
2021
2479
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2022
2480
  if (name === ".env" || name.startsWith(".env.")) {
2023
2481
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2482
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2024
2483
  return { match: true, fileType: "env" };
2025
2484
  }
2026
2485
  return { match: false, fileType: "" };
2027
2486
  }
2487
+ function isNeatAuthoredEnvFile(name) {
2488
+ return name === ".env.neat";
2489
+ }
2490
+ function isNeatAuthoredSourceFile(name) {
2491
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
2492
+ }
2028
2493
  function isTestPath(filePath) {
2029
2494
  const normalised = filePath.replace(/\\/g, "/");
2030
2495
  const segments = normalised.split("/");
@@ -2659,7 +3124,7 @@ import path10 from "path";
2659
3124
  import {
2660
3125
  EdgeType as EdgeType4,
2661
3126
  NodeType as NodeType6,
2662
- Provenance as Provenance2,
3127
+ Provenance as Provenance3,
2663
3128
  confidenceForExtracted,
2664
3129
  extractedEdgeId as extractedEdgeId3,
2665
3130
  fileId as fileId2
@@ -2674,7 +3139,9 @@ async function walkSourceFiles(dir) {
2674
3139
  if (IGNORED_DIRS.has(entry.name)) continue;
2675
3140
  if (await isPythonVenvDir(full)) continue;
2676
3141
  await walk(full);
2677
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))) {
3142
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3143
+ // would attribute our instrumentation imports to the user's service.
3144
+ !isNeatAuthoredSourceFile(entry.name)) {
2678
3145
  out.push(full);
2679
3146
  }
2680
3147
  }
@@ -2746,7 +3213,7 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
2746
3213
  source: serviceNodeId,
2747
3214
  target: fileNodeId,
2748
3215
  type: EdgeType4.CONTAINS,
2749
- provenance: Provenance2.EXTRACTED,
3216
+ provenance: Provenance3.EXTRACTED,
2750
3217
  confidence: confidenceForExtracted("structural"),
2751
3218
  evidence: { file: relPath }
2752
3219
  };
@@ -2786,7 +3253,7 @@ import JavaScript from "tree-sitter-javascript";
2786
3253
  import Python from "tree-sitter-python";
2787
3254
  import {
2788
3255
  EdgeType as EdgeType5,
2789
- Provenance as Provenance3,
3256
+ Provenance as Provenance4,
2790
3257
  confidenceForExtracted as confidenceForExtracted2,
2791
3258
  extractedEdgeId as extractedEdgeId4,
2792
3259
  fileId as fileId3
@@ -2849,11 +3316,28 @@ function collectJsImports(node, out) {
2849
3316
  if (child) collectJsImports(child, out);
2850
3317
  }
2851
3318
  }
3319
+ function collectImportedNames(node, out) {
3320
+ if (node.type === "aliased_import") {
3321
+ const nameNode = node.childForFieldName("name");
3322
+ if (nameNode) out.push(nameNode.text);
3323
+ return;
3324
+ }
3325
+ if (node.type === "dotted_name") {
3326
+ out.push(node.text);
3327
+ return;
3328
+ }
3329
+ for (let i = 0; i < node.namedChildCount; i++) {
3330
+ const child = node.namedChild(i);
3331
+ if (child) collectImportedNames(child, out);
3332
+ }
3333
+ }
2852
3334
  function collectPyImports(node, out) {
2853
3335
  if (node.type === "import_from_statement") {
2854
3336
  let level = 0;
2855
3337
  let modulePath = "";
3338
+ const names = [];
2856
3339
  let pastFrom = false;
3340
+ let pastImport = false;
2857
3341
  for (let i = 0; i < node.childCount; i++) {
2858
3342
  const child = node.child(i);
2859
3343
  if (!child) continue;
@@ -2861,26 +3345,30 @@ function collectPyImports(node, out) {
2861
3345
  if (child.type === "from") pastFrom = true;
2862
3346
  continue;
2863
3347
  }
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;
3348
+ if (!pastImport) {
3349
+ if (child.type === "import") {
3350
+ pastImport = true;
3351
+ continue;
2874
3352
  }
2875
- break;
2876
- }
2877
- if (child.type === "dotted_name") {
2878
- modulePath = child.text;
2879
- break;
3353
+ if (child.type === "relative_import") {
3354
+ for (let j = 0; j < child.childCount; j++) {
3355
+ const rc = child.child(j);
3356
+ if (!rc) continue;
3357
+ if (rc.type === "import_prefix") {
3358
+ for (let k = 0; k < rc.childCount; k++) {
3359
+ if (rc.child(k)?.type === ".") level++;
3360
+ }
3361
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3362
+ }
3363
+ } else if (child.type === "dotted_name") {
3364
+ modulePath = child.text;
3365
+ }
3366
+ continue;
2880
3367
  }
3368
+ collectImportedNames(child, names);
2881
3369
  }
2882
3370
  if (level > 0 || modulePath) {
2883
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3371
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
2884
3372
  }
2885
3373
  }
2886
3374
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -2982,8 +3470,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
2982
3470
  return null;
2983
3471
  }
2984
3472
  async function resolvePyImport(imp, importerPath, serviceDir) {
2985
- if (!imp.modulePath) return null;
2986
- const relPath = imp.modulePath.split(".").join("/");
2987
3473
  let baseDir;
2988
3474
  if (imp.level > 0) {
2989
3475
  baseDir = path12.dirname(importerPath);
@@ -2991,13 +3477,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
2991
3477
  } else {
2992
3478
  baseDir = serviceDir;
2993
3479
  }
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));
3480
+ const moduleBase = imp.modulePath ? path12.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
3481
+ const resolved = /* @__PURE__ */ new Set();
3482
+ let needModuleFile = imp.names.length === 0;
3483
+ for (const name of imp.names) {
3484
+ const submoduleFile = path12.join(moduleBase, `${name}.py`);
3485
+ const subpackageInit = path12.join(moduleBase, name, "__init__.py");
3486
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
3487
+ resolved.add(toPosix2(path12.relative(serviceDir, submoduleFile)));
3488
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
3489
+ resolved.add(toPosix2(path12.relative(serviceDir, subpackageInit)));
3490
+ } else {
3491
+ needModuleFile = true;
3492
+ }
3493
+ }
3494
+ if (needModuleFile) {
3495
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, path12.join(moduleBase, "__init__.py")] : [path12.join(moduleBase, "__init__.py")];
3496
+ for (const candidate of moduleFileCandidates) {
3497
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3498
+ resolved.add(toPosix2(path12.relative(serviceDir, candidate)));
3499
+ break;
3500
+ }
2998
3501
  }
2999
3502
  }
3000
- return null;
3503
+ return [...resolved];
3001
3504
  }
3002
3505
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3003
3506
  const importeeFileId = fileId3(serviceName, importeeRelPath);
@@ -3009,7 +3512,7 @@ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, imp
3009
3512
  source: importerFileId,
3010
3513
  target: importeeFileId,
3011
3514
  type: EdgeType5.IMPORTS,
3012
- provenance: Provenance3.EXTRACTED,
3515
+ provenance: Provenance4.EXTRACTED,
3013
3516
  confidence: confidenceForExtracted2("structural"),
3014
3517
  evidence: { file: importerRelPath, line, snippet: snippet2 }
3015
3518
  };
@@ -3038,17 +3541,18 @@ async function addImports(graph, services) {
3038
3541
  continue;
3039
3542
  }
3040
3543
  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
- );
3544
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
3545
+ for (const resolved of resolvedPaths) {
3546
+ edgesAdded += emitImportEdge(
3547
+ graph,
3548
+ service.pkg.name,
3549
+ importerFileId,
3550
+ relFile,
3551
+ resolved,
3552
+ imp.line,
3553
+ imp.snippet
3554
+ );
3555
+ }
3052
3556
  }
3053
3557
  continue;
3054
3558
  }
@@ -3083,7 +3587,8 @@ import path20 from "path";
3083
3587
  import {
3084
3588
  EdgeType as EdgeType6,
3085
3589
  NodeType as NodeType7,
3086
- Provenance as Provenance4,
3590
+ Provenance as Provenance5,
3591
+ configId,
3087
3592
  databaseId as databaseId2,
3088
3593
  confidenceForExtracted as confidenceForExtracted3
3089
3594
  } from "@neat.is/types";
@@ -3670,7 +4175,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3670
4175
  source: fileNodeId,
3671
4176
  target: dbNode.id,
3672
4177
  type: EdgeType6.CONNECTS_TO,
3673
- provenance: Provenance4.EXTRACTED,
4178
+ provenance: Provenance5.EXTRACTED,
3674
4179
  confidence: confidenceForExtracted3("structural"),
3675
4180
  evidence: { file: evidenceFile }
3676
4181
  };
@@ -3679,6 +4184,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3679
4184
  edgesAdded++;
3680
4185
  }
3681
4186
  }
4187
+ if (allConfigs.length === 1) {
4188
+ const primary = allConfigs[0];
4189
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
4190
+ const relPath = path20.relative(scanPath, primary.sourceFile);
4191
+ const cfgId = configId(relPath);
4192
+ if (!graph.hasNode(cfgId)) {
4193
+ const cfgNode = {
4194
+ id: cfgId,
4195
+ type: NodeType7.ConfigNode,
4196
+ name: path20.basename(primary.sourceFile),
4197
+ path: relPath,
4198
+ fileType: isConfigFile(path20.basename(primary.sourceFile)).fileType || "config"
4199
+ };
4200
+ graph.addNode(cfgId, cfgNode);
4201
+ nodesAdded++;
4202
+ }
4203
+ const cfgEdge = {
4204
+ id: extractedEdgeId2(service.node.id, cfgId, EdgeType6.CONFIGURED_BY),
4205
+ source: service.node.id,
4206
+ target: cfgId,
4207
+ type: EdgeType6.CONFIGURED_BY,
4208
+ provenance: Provenance5.EXTRACTED,
4209
+ confidence: confidenceForExtracted3("structural"),
4210
+ evidence: { file: toPosix2(relPath) }
4211
+ };
4212
+ if (!graph.hasEdge(cfgEdge.id)) {
4213
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
4214
+ edgesAdded++;
4215
+ }
4216
+ }
3682
4217
  attachIncompatibilities(service, allConfigs);
3683
4218
  if (graph.hasNode(service.node.id)) {
3684
4219
  const current = graph.getNodeAttributes(service.node.id);
@@ -3690,6 +4225,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3690
4225
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
3691
4226
  delete updated.incompatibilities;
3692
4227
  }
4228
+ if (!service.node.dbConnectionTarget) {
4229
+ delete updated.dbConnectionTarget;
4230
+ }
3693
4231
  graph.replaceNodeAttributes(service.node.id, updated);
3694
4232
  }
3695
4233
  }
@@ -3702,8 +4240,8 @@ import path21 from "path";
3702
4240
  import {
3703
4241
  EdgeType as EdgeType7,
3704
4242
  NodeType as NodeType8,
3705
- Provenance as Provenance5,
3706
- configId,
4243
+ Provenance as Provenance6,
4244
+ configId as configId2,
3707
4245
  confidenceForExtracted as confidenceForExtracted4
3708
4246
  } from "@neat.is/types";
3709
4247
  async function walkConfigFiles(dir) {
@@ -3732,7 +4270,7 @@ async function addConfigNodes(graph, services, scanPath) {
3732
4270
  for (const file of configFiles) {
3733
4271
  const relPath = path21.relative(scanPath, file);
3734
4272
  const node = {
3735
- id: configId(relPath),
4273
+ id: configId2(relPath),
3736
4274
  type: NodeType8.ConfigNode,
3737
4275
  name: path21.basename(file),
3738
4276
  path: relPath,
@@ -3756,7 +4294,7 @@ async function addConfigNodes(graph, services, scanPath) {
3756
4294
  source: fileNodeId,
3757
4295
  target: node.id,
3758
4296
  type: EdgeType7.CONFIGURED_BY,
3759
- provenance: Provenance5.EXTRACTED,
4297
+ provenance: Provenance6.EXTRACTED,
3760
4298
  confidence: confidenceForExtracted4("structural"),
3761
4299
  evidence: { file: relPath.split(path21.sep).join("/") }
3762
4300
  };
@@ -3773,7 +4311,7 @@ async function addConfigNodes(graph, services, scanPath) {
3773
4311
  import {
3774
4312
  EdgeType as EdgeType9,
3775
4313
  NodeType as NodeType9,
3776
- Provenance as Provenance7,
4314
+ Provenance as Provenance8,
3777
4315
  confidenceForExtracted as confidenceForExtracted6,
3778
4316
  passesExtractedFloor as passesExtractedFloor2
3779
4317
  } from "@neat.is/types";
@@ -3785,7 +4323,7 @@ import JavaScript2 from "tree-sitter-javascript";
3785
4323
  import Python2 from "tree-sitter-python";
3786
4324
  import {
3787
4325
  EdgeType as EdgeType8,
3788
- Provenance as Provenance6,
4326
+ Provenance as Provenance7,
3789
4327
  confidenceForExtracted as confidenceForExtracted5,
3790
4328
  passesExtractedFloor
3791
4329
  } from "@neat.is/types";
@@ -3881,7 +4419,7 @@ async function addHttpCallEdges(graph, services) {
3881
4419
  const dedupKey = `${relFile}|${targetId}`;
3882
4420
  if (seen.has(dedupKey)) continue;
3883
4421
  seen.add(dedupKey);
3884
- const confidence = confidenceForExtracted5("hostname-shape-match");
4422
+ const confidence = confidenceForExtracted5("url-literal-service-target");
3885
4423
  const ev = {
3886
4424
  file: relFile,
3887
4425
  line: site.line,
@@ -3901,7 +4439,7 @@ async function addHttpCallEdges(graph, services) {
3901
4439
  target: targetId,
3902
4440
  type: EdgeType8.CALLS,
3903
4441
  confidence,
3904
- confidenceKind: "hostname-shape-match",
4442
+ confidenceKind: "url-literal-service-target",
3905
4443
  evidence: ev
3906
4444
  });
3907
4445
  continue;
@@ -3913,7 +4451,7 @@ async function addHttpCallEdges(graph, services) {
3913
4451
  source: fileNodeId,
3914
4452
  target: targetId,
3915
4453
  type: EdgeType8.CALLS,
3916
- provenance: Provenance6.EXTRACTED,
4454
+ provenance: Provenance7.EXTRACTED,
3917
4455
  confidence,
3918
4456
  evidence: ev
3919
4457
  };
@@ -4266,7 +4804,7 @@ async function addExternalEndpointEdges(graph, services) {
4266
4804
  source: fileNodeId,
4267
4805
  target: ep.infraId,
4268
4806
  type: edgeType,
4269
- provenance: Provenance7.EXTRACTED,
4807
+ provenance: Provenance8.EXTRACTED,
4270
4808
  confidence,
4271
4809
  evidence: ep.evidence
4272
4810
  };
@@ -4288,7 +4826,7 @@ async function addCallEdges(graph, services) {
4288
4826
 
4289
4827
  // src/extract/infra/docker-compose.ts
4290
4828
  import path28 from "path";
4291
- import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
4829
+ import { EdgeType as EdgeType10, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
4292
4830
 
4293
4831
  // src/extract/infra/shared.ts
4294
4832
  import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
@@ -4381,7 +4919,7 @@ async function addComposeInfra(graph, scanPath, services) {
4381
4919
  source: sourceId,
4382
4920
  target: targetId,
4383
4921
  type: EdgeType10.DEPENDS_ON,
4384
- provenance: Provenance8.EXTRACTED,
4922
+ provenance: Provenance9.EXTRACTED,
4385
4923
  confidence: confidenceForExtracted7("structural"),
4386
4924
  evidence: { file: evidenceFile }
4387
4925
  };
@@ -4395,20 +4933,30 @@ async function addComposeInfra(graph, scanPath, services) {
4395
4933
  // src/extract/infra/dockerfile.ts
4396
4934
  import path29 from "path";
4397
4935
  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) {
4936
+ import { EdgeType as EdgeType11, Provenance as Provenance10, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
4937
+ function readDockerfile(content) {
4938
+ let image = null;
4939
+ const ports = [];
4940
+ let cmd = null;
4941
+ let entrypoint = null;
4942
+ for (const raw of content.split("\n")) {
4403
4943
  const line = raw.trim();
4404
4944
  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;
4945
+ if (/^from\s+/i.test(line)) {
4946
+ const candidate = line.split(/\s+/)[1];
4947
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
4948
+ } else if (/^expose\s+/i.test(line)) {
4949
+ for (const token of line.split(/\s+/).slice(1)) {
4950
+ const port = Number.parseInt(token.split("/")[0], 10);
4951
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
4952
+ }
4953
+ } else if (/^entrypoint\s+/i.test(line)) {
4954
+ entrypoint = line;
4955
+ } else if (/^cmd\s+/i.test(line)) {
4956
+ cmd = line;
4957
+ }
4410
4958
  }
4411
- return last;
4959
+ return { image, ports, entrypoint: entrypoint ?? cmd };
4412
4960
  }
4413
4961
  async function addDockerfileRuntimes(graph, services, scanPath) {
4414
4962
  let nodesAdded = 0;
@@ -4427,14 +4975,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4427
4975
  );
4428
4976
  continue;
4429
4977
  }
4430
- const image = runtimeImage(content);
4431
- if (!image) continue;
4432
- const node = makeInfraNode("container-image", image);
4978
+ const facts = readDockerfile(content);
4979
+ if (!facts.image) continue;
4980
+ const node = makeInfraNode("container-image", facts.image);
4433
4981
  if (!graph.hasNode(node.id)) {
4434
4982
  graph.addNode(node.id, node);
4435
4983
  nodesAdded++;
4436
4984
  }
4437
4985
  const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
4986
+ const evidenceFile = toPosix2(path29.relative(scanPath, dockerfilePath));
4438
4987
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4439
4988
  graph,
4440
4989
  service.pkg.name,
@@ -4450,15 +4999,36 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4450
4999
  source: fileNodeId,
4451
5000
  target: node.id,
4452
5001
  type: EdgeType11.RUNS_ON,
4453
- provenance: Provenance9.EXTRACTED,
5002
+ provenance: Provenance10.EXTRACTED,
4454
5003
  confidence: confidenceForExtracted8("structural"),
4455
5004
  evidence: {
4456
- file: toPosix2(path29.relative(scanPath, dockerfilePath))
5005
+ file: evidenceFile,
5006
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
4457
5007
  }
4458
5008
  };
4459
5009
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
4460
5010
  edgesAdded++;
4461
5011
  }
5012
+ for (const port of facts.ports) {
5013
+ const portNode = makeInfraNode("port", String(port));
5014
+ if (!graph.hasNode(portNode.id)) {
5015
+ graph.addNode(portNode.id, portNode);
5016
+ nodesAdded++;
5017
+ }
5018
+ const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType11.CONNECTS_TO);
5019
+ if (graph.hasEdge(portEdgeId)) continue;
5020
+ const portEdge = {
5021
+ id: portEdgeId,
5022
+ source: fileNodeId,
5023
+ target: portNode.id,
5024
+ type: EdgeType11.CONNECTS_TO,
5025
+ provenance: Provenance10.EXTRACTED,
5026
+ confidence: confidenceForExtracted8("structural"),
5027
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5028
+ };
5029
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
5030
+ edgesAdded++;
5031
+ }
4462
5032
  }
4463
5033
  return { nodesAdded, edgesAdded };
4464
5034
  }
@@ -4466,7 +5036,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4466
5036
  // src/extract/infra/terraform.ts
4467
5037
  import { promises as fs16 } from "fs";
4468
5038
  import path30 from "path";
5039
+ import { EdgeType as EdgeType12, Provenance as Provenance11, confidenceForExtracted as confidenceForExtracted9 } from "@neat.is/types";
4469
5040
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5041
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
4470
5042
  async function walkTfFiles(start, depth = 0, max = 5) {
4471
5043
  if (depth > max) return [];
4472
5044
  const out = [];
@@ -4483,24 +5055,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4483
5055
  }
4484
5056
  return out;
4485
5057
  }
5058
+ function blockBody(content, from) {
5059
+ const open = content.indexOf("{", from);
5060
+ if (open === -1) return null;
5061
+ let depth = 0;
5062
+ for (let i = open; i < content.length; i++) {
5063
+ const ch = content[i];
5064
+ if (ch === "{") depth++;
5065
+ else if (ch === "}") {
5066
+ depth--;
5067
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
5068
+ }
5069
+ }
5070
+ return null;
5071
+ }
5072
+ function lineAt(content, index) {
5073
+ let line = 1;
5074
+ for (let i = 0; i < index && i < content.length; i++) {
5075
+ if (content[i] === "\n") line++;
5076
+ }
5077
+ return line;
5078
+ }
4486
5079
  async function addTerraformResources(graph, scanPath) {
4487
5080
  let nodesAdded = 0;
5081
+ let edgesAdded = 0;
4488
5082
  const files = await walkTfFiles(scanPath);
4489
5083
  for (const file of files) {
4490
5084
  const content = await fs16.readFile(file, "utf8");
5085
+ const evidenceFile = toPosix2(path30.relative(scanPath, file));
5086
+ const resources = [];
5087
+ const byKey = /* @__PURE__ */ new Map();
4491
5088
  RESOURCE_RE.lastIndex = 0;
4492
5089
  let m;
4493
5090
  while ((m = RESOURCE_RE.exec(content)) !== null) {
4494
- const kind = m[1];
5091
+ const type = m[1];
4495
5092
  const name = m[2];
4496
- const node = makeInfraNode(kind, name, "aws");
5093
+ const node = makeInfraNode(type, name, "aws");
4497
5094
  if (!graph.hasNode(node.id)) {
4498
5095
  graph.addNode(node.id, node);
4499
5096
  nodesAdded++;
4500
5097
  }
5098
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
5099
+ const resource = {
5100
+ type,
5101
+ name,
5102
+ nodeId: node.id,
5103
+ body: span?.body ?? "",
5104
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
5105
+ };
5106
+ resources.push(resource);
5107
+ byKey.set(`${type}.${name}`, resource);
5108
+ }
5109
+ for (const resource of resources) {
5110
+ const seen = /* @__PURE__ */ new Set();
5111
+ REFERENCE_RE.lastIndex = 0;
5112
+ let ref;
5113
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
5114
+ const key = `${ref[1]}.${ref[2]}`;
5115
+ if (key === `${resource.type}.${resource.name}`) continue;
5116
+ const target = byKey.get(key);
5117
+ if (!target) continue;
5118
+ if (seen.has(target.nodeId)) continue;
5119
+ seen.add(target.nodeId);
5120
+ const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType12.DEPENDS_ON);
5121
+ if (graph.hasEdge(edgeId)) continue;
5122
+ const line = lineAt(content, resource.bodyOffset + ref.index);
5123
+ const edge = {
5124
+ id: edgeId,
5125
+ source: resource.nodeId,
5126
+ target: target.nodeId,
5127
+ type: EdgeType12.DEPENDS_ON,
5128
+ provenance: Provenance11.EXTRACTED,
5129
+ confidence: confidenceForExtracted9("structural"),
5130
+ evidence: { file: evidenceFile, line, snippet: key }
5131
+ };
5132
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5133
+ edgesAdded++;
5134
+ }
4501
5135
  }
4502
5136
  }
4503
- return { nodesAdded, edgesAdded: 0 };
5137
+ return { nodesAdded, edgesAdded };
4504
5138
  }
4505
5139
 
4506
5140
  // src/extract/infra/k8s.ts
@@ -4576,7 +5210,7 @@ import path33 from "path";
4576
5210
  // src/extract/retire.ts
4577
5211
  import { existsSync as existsSync2 } from "fs";
4578
5212
  import path32 from "path";
4579
- import { NodeType as NodeType11, Provenance as Provenance10 } from "@neat.is/types";
5213
+ import { NodeType as NodeType11, Provenance as Provenance12 } from "@neat.is/types";
4580
5214
  function dropOrphanedFileNodes(graph) {
4581
5215
  const orphans = [];
4582
5216
  graph.forEachNode((id, attrs) => {
@@ -4593,7 +5227,7 @@ function retireEdgesByFile(graph, file) {
4593
5227
  const toDrop = [];
4594
5228
  graph.forEachEdge((id, attrs) => {
4595
5229
  const edge = attrs;
4596
- if (edge.provenance !== Provenance10.EXTRACTED) return;
5230
+ if (edge.provenance !== Provenance12.EXTRACTED) return;
4597
5231
  if (!edge.evidence?.file) return;
4598
5232
  if (edge.evidence.file === normalized) toDrop.push(id);
4599
5233
  });
@@ -4606,7 +5240,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4606
5240
  const bases = [scanPath, ...serviceDirs];
4607
5241
  graph.forEachEdge((id, attrs) => {
4608
5242
  const edge = attrs;
4609
- if (edge.provenance !== Provenance10.EXTRACTED) return;
5243
+ if (edge.provenance !== Provenance12.EXTRACTED) return;
4610
5244
  const evidenceFile = edge.evidence?.file;
4611
5245
  if (!evidenceFile) return;
4612
5246
  if (path32.isAbsolute(evidenceFile)) {
@@ -4687,11 +5321,12 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4687
5321
 
4688
5322
  // src/divergences.ts
4689
5323
  import {
5324
+ databaseId as databaseId3,
4690
5325
  DivergenceResultSchema,
4691
- EdgeType as EdgeType12,
5326
+ EdgeType as EdgeType13,
4692
5327
  NodeType as NodeType12,
4693
5328
  parseEdgeId,
4694
- Provenance as Provenance11
5329
+ Provenance as Provenance13
4695
5330
  } from "@neat.is/types";
4696
5331
  function bucketKey(source, target, type) {
4697
5332
  return `${type}|${source}|${target}`;
@@ -4705,17 +5340,17 @@ function bucketEdges(graph) {
4705
5340
  const key = bucketKey(e.source, e.target, e.type);
4706
5341
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4707
5342
  switch (provenance) {
4708
- case Provenance11.EXTRACTED:
5343
+ case Provenance13.EXTRACTED:
4709
5344
  cur.extracted = e;
4710
5345
  break;
4711
- case Provenance11.OBSERVED:
5346
+ case Provenance13.OBSERVED:
4712
5347
  cur.observed = e;
4713
5348
  break;
4714
- case Provenance11.INFERRED:
5349
+ case Provenance13.INFERRED:
4715
5350
  cur.inferred = e;
4716
5351
  break;
4717
5352
  default:
4718
- if (e.provenance === Provenance11.STALE) cur.stale = e;
5353
+ if (e.provenance === Provenance13.STALE) cur.stale = e;
4719
5354
  }
4720
5355
  buckets.set(key, cur);
4721
5356
  });
@@ -4743,10 +5378,16 @@ function gradedConfidence(edge) {
4743
5378
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
4744
5379
  return clampConfidence(confidenceForEdge(edge));
4745
5380
  }
5381
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
5382
+ EdgeType13.CALLS,
5383
+ EdgeType13.CONNECTS_TO,
5384
+ EdgeType13.PUBLISHES_TO,
5385
+ EdgeType13.CONSUMES_FROM
5386
+ ]);
4746
5387
  function detectMissingDivergences(graph, bucket) {
4747
5388
  const out = [];
4748
- if (bucket.type === EdgeType12.CONTAINS) return out;
4749
- if (bucket.extracted && !bucket.observed) {
5389
+ if (bucket.type === EdgeType13.CONTAINS) return out;
5390
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
4750
5391
  if (!nodeIsFrontier(graph, bucket.target)) {
4751
5392
  out.push({
4752
5393
  type: "missing-observed",
@@ -4786,7 +5427,7 @@ function declaredHostFor(svc) {
4786
5427
  function hasExtractedConfiguredBy(graph, svcId) {
4787
5428
  for (const edgeId of graph.outboundEdges(svcId)) {
4788
5429
  const e = graph.getEdgeAttributes(edgeId);
4789
- if (e.type === EdgeType12.CONFIGURED_BY && e.provenance === Provenance11.EXTRACTED) {
5430
+ if (e.type === EdgeType13.CONFIGURED_BY && e.provenance === Provenance13.EXTRACTED) {
4790
5431
  return true;
4791
5432
  }
4792
5433
  }
@@ -4799,8 +5440,8 @@ function detectHostMismatch(graph, svcId, svc) {
4799
5440
  const out = [];
4800
5441
  for (const edgeId of graph.outboundEdges(svcId)) {
4801
5442
  const edge = graph.getEdgeAttributes(edgeId);
4802
- if (edge.type !== EdgeType12.CONNECTS_TO) continue;
4803
- if (edge.provenance !== Provenance11.OBSERVED) continue;
5443
+ if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5444
+ if (edge.provenance !== Provenance13.OBSERVED) continue;
4804
5445
  const target = graph.getNodeAttributes(edge.target);
4805
5446
  if (target.type !== NodeType12.DatabaseNode) continue;
4806
5447
  const observedHost = target.host?.trim();
@@ -4824,8 +5465,8 @@ function detectCompatDivergences(graph, svcId, svc) {
4824
5465
  const deps = svc.dependencies ?? {};
4825
5466
  for (const edgeId of graph.outboundEdges(svcId)) {
4826
5467
  const edge = graph.getEdgeAttributes(edgeId);
4827
- if (edge.type !== EdgeType12.CONNECTS_TO) continue;
4828
- if (edge.provenance !== Provenance11.OBSERVED) continue;
5468
+ if (edge.type !== EdgeType13.CONNECTS_TO) continue;
5469
+ if (edge.provenance !== Provenance13.OBSERVED) continue;
4829
5470
  const target = graph.getNodeAttributes(edge.target);
4830
5471
  if (target.type !== NodeType12.DatabaseNode) continue;
4831
5472
  for (const pair of compatPairs()) {
@@ -4880,6 +5521,23 @@ function detectCompatDivergences(graph, svcId, svc) {
4880
5521
  function involvesNode(d, nodeId) {
4881
5522
  return d.source === nodeId || d.target === nodeId;
4882
5523
  }
5524
+ function suppressHostMismatchHalves(all) {
5525
+ const observedHalf = /* @__PURE__ */ new Set();
5526
+ const declaredHalf = /* @__PURE__ */ new Set();
5527
+ for (const d of all) {
5528
+ if (d.type !== "host-mismatch") continue;
5529
+ observedHalf.add(`${d.source}->${d.target}`);
5530
+ declaredHalf.add(databaseId3(d.extractedHost));
5531
+ }
5532
+ if (observedHalf.size === 0) return all;
5533
+ return all.filter((d) => {
5534
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
5535
+ return false;
5536
+ }
5537
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
5538
+ return true;
5539
+ });
5540
+ }
4883
5541
  function computeDivergences(graph, opts = {}) {
4884
5542
  const all = [];
4885
5543
  const buckets = bucketEdges(graph);
@@ -4893,7 +5551,8 @@ function computeDivergences(graph, opts = {}) {
4893
5551
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4894
5552
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
4895
5553
  });
4896
- let filtered = all;
5554
+ const reconciled = suppressHostMismatchHalves(all);
5555
+ let filtered = reconciled;
4897
5556
  if (opts.type) {
4898
5557
  const allowed = opts.type;
4899
5558
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -4931,7 +5590,7 @@ function computeDivergences(graph, opts = {}) {
4931
5590
  // src/persist.ts
4932
5591
  import { promises as fs18 } from "fs";
4933
5592
  import path34 from "path";
4934
- import { Provenance as Provenance12, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
5593
+ import { Provenance as Provenance14, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4935
5594
  var SCHEMA_VERSION = 4;
4936
5595
  function migrateV1ToV2(payload) {
4937
5596
  const nodes = payload.graph.nodes;
@@ -4953,7 +5612,7 @@ function migrateV2ToV3(payload) {
4953
5612
  for (const edge of edges) {
4954
5613
  const attrs = edge.attributes;
4955
5614
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4956
- attrs.provenance = Provenance12.OBSERVED;
5615
+ attrs.provenance = Provenance14.OBSERVED;
4957
5616
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4958
5617
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4959
5618
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -6077,6 +6736,18 @@ function registerRoutes(scope, ctx) {
6077
6736
  }
6078
6737
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6079
6738
  });
6739
+ scope.get(
6740
+ "/graph/observed-dependencies/:nodeId",
6741
+ async (req, reply) => {
6742
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6743
+ if (!proj) return;
6744
+ const { nodeId } = req.params;
6745
+ if (!proj.graph.hasNode(nodeId)) {
6746
+ return reply.code(404).send({ error: "node not found", id: nodeId });
6747
+ }
6748
+ return getObservedDependencies(proj.graph, nodeId);
6749
+ }
6750
+ );
6080
6751
  scope.get("/graph/divergences", async (req, reply) => {
6081
6752
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6082
6753
  if (!proj) return;
@@ -6137,23 +6808,29 @@ function registerRoutes(scope, ctx) {
6137
6808
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6138
6809
  return { count: sliced.length, total, events: sliced };
6139
6810
  });
6811
+ const incidentHistoryHandler = async (req, reply) => {
6812
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6813
+ if (!proj) return;
6814
+ const { nodeId } = req.params;
6815
+ if (!proj.graph.hasNode(nodeId)) {
6816
+ reply.code(404).send({ error: "node not found", id: nodeId });
6817
+ return;
6818
+ }
6819
+ const epath = errorsPathFor(proj);
6820
+ if (!epath) return { count: 0, total: 0, events: [] };
6821
+ const events = await readErrorEvents(epath);
6822
+ const filtered = events.filter(
6823
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6824
+ );
6825
+ return { count: filtered.length, total: filtered.length, events: filtered };
6826
+ };
6140
6827
  scope.get(
6141
6828
  "/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
- }
6829
+ incidentHistoryHandler
6830
+ );
6831
+ scope.get(
6832
+ "/graph/incident-history/:nodeId",
6833
+ incidentHistoryHandler
6157
6834
  );
6158
6835
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6159
6836
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -6162,16 +6839,16 @@ function registerRoutes(scope, ctx) {
6162
6839
  if (!proj.graph.hasNode(nodeId)) {
6163
6840
  return reply.code(404).send({ error: "node not found", id: nodeId });
6164
6841
  }
6165
- let errorEvent;
6166
6842
  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);
6843
+ const incidents = epath ? await readErrorEvents(epath) : [];
6844
+ let errorEvent;
6845
+ if (req.query.errorId) {
6846
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
6170
6847
  if (!errorEvent) {
6171
6848
  return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
6172
6849
  }
6173
6850
  }
6174
- const result = getRootCause(proj.graph, nodeId, errorEvent);
6851
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6175
6852
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6176
6853
  return result;
6177
6854
  });
@@ -6316,6 +6993,28 @@ function registerRoutes(scope, ctx) {
6316
6993
  }
6317
6994
  return { violations };
6318
6995
  });
6996
+ scope.get("/policies/applicable", async (req, reply) => {
6997
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6998
+ if (!proj) return;
6999
+ const nodeId = req.query.node;
7000
+ if (!nodeId) {
7001
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7002
+ }
7003
+ const policyPath = ctx.policyFilePathFor(proj);
7004
+ let policies = [];
7005
+ if (policyPath) {
7006
+ try {
7007
+ policies = await loadPolicyFile(policyPath);
7008
+ } catch (err) {
7009
+ return reply.code(400).send({
7010
+ error: "policy.json failed to parse",
7011
+ details: err.message
7012
+ });
7013
+ }
7014
+ }
7015
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7016
+ return { node: nodeId, applicable };
7017
+ });
6319
7018
  scope.post("/policies/check", async (req, reply) => {
6320
7019
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6321
7020
  if (!proj) return;
@@ -6606,6 +7305,7 @@ export {
6606
7305
  discoverServices,
6607
7306
  addServiceNodes,
6608
7307
  addServiceAliases,
7308
+ addFiles,
6609
7309
  addImports,
6610
7310
  addDatabasesAndCompat,
6611
7311
  addConfigNodes,
@@ -6644,4 +7344,4 @@ export {
6644
7344
  pruneRegistry,
6645
7345
  buildApi
6646
7346
  };
6647
- //# sourceMappingURL=chunk-GNAX2CBF.js.map
7347
+ //# sourceMappingURL=chunk-OQ7CHOY2.js.map