@neat.is/core 0.4.20-dev.20260629 → 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.
package/dist/server.cjs CHANGED
@@ -40,13 +40,13 @@ var init_cjs_shims = __esm({
40
40
  });
41
41
 
42
42
  // src/auth.ts
43
- function isLoopbackHost(host) {
43
+ function isLoopbackHost2(host) {
44
44
  if (!host) return false;
45
45
  return LOOPBACK_HOSTS.has(host);
46
46
  }
47
47
  function assertBindAuthority(host, token) {
48
48
  if (token && token.length > 0) return;
49
- if (isLoopbackHost(host)) return;
49
+ if (isLoopbackHost2(host)) return;
50
50
  throw new BindAuthorityError(host);
51
51
  }
52
52
  function mountBearerAuth(app, opts) {
@@ -617,7 +617,7 @@ function getGraph(project = DEFAULT_PROJECT) {
617
617
  init_cjs_shims();
618
618
  var import_fastify = __toESM(require("fastify"), 1);
619
619
  var import_cors = __toESM(require("@fastify/cors"), 1);
620
- var import_types25 = require("@neat.is/types");
620
+ var import_types26 = require("@neat.is/types");
621
621
 
622
622
  // src/extend/index.ts
623
623
  init_cjs_shims();
@@ -1458,22 +1458,164 @@ var rootCauseShapes = {
1458
1458
  [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1459
1459
  [import_types.NodeType.FileNode]: fileRootCauseShape
1460
1460
  };
1461
- function getRootCause(graph, errorNodeId, errorEvent) {
1461
+ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1462
1462
  if (!graph.hasNode(errorNodeId)) return null;
1463
1463
  const origin = graph.getNodeAttributes(errorNodeId);
1464
1464
  const shape = rootCauseShapes[origin.type];
1465
- if (!shape) return null;
1466
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1467
- const match = shape(graph, origin, walk);
1468
- if (!match) return null;
1469
- const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1465
+ if (shape) {
1466
+ const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1467
+ const match = shape(graph, origin, walk);
1468
+ if (match) {
1469
+ const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1470
+ return import_types.RootCauseResultSchema.parse({
1471
+ rootCauseNode: match.rootCauseNode,
1472
+ rootCauseReason: reason,
1473
+ traversalPath: walk.path,
1474
+ edgeProvenances: walk.edges.map((e) => e.provenance),
1475
+ confidence: confidenceFromMix(walk.edges),
1476
+ fixRecommendation: match.fixRecommendation
1477
+ });
1478
+ }
1479
+ }
1480
+ if (origin.type === import_types.NodeType.ServiceNode) {
1481
+ const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
1482
+ if (crossService) return crossService;
1483
+ }
1484
+ return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
1485
+ }
1486
+ var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
1487
+ function incidentMatchesNode(ev, nodeId) {
1488
+ return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
1489
+ }
1490
+ function localizeFromIncidents(nodeId, incidents, errorEvent) {
1491
+ const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
1492
+ const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
1493
+ if (relevant.length === 0) return null;
1494
+ const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
1495
+ const attrs = latest.attributes ?? {};
1496
+ const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
1497
+ const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
1498
+ const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
1499
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
1500
+ const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
1501
+ const count = sameMode.length;
1502
+ const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
1503
+ const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
1504
+ if (location) reasonParts.push(`surfaced at ${location}`);
1505
+ const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
1506
+ const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
1507
+ const fileNode = localizesToFile ? latest.affectedNode : void 0;
1508
+ const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
1509
+ return {
1510
+ rootCauseNode: fileNode ?? nodeId,
1511
+ rootCauseReason,
1512
+ ...fileNode ? { fileNode } : {},
1513
+ ...fixRecommendation ? { fixRecommendation } : {}
1514
+ };
1515
+ }
1516
+ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1517
+ const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
1518
+ if (!loc) return null;
1519
+ const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
1520
+ const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
1521
+ return import_types.RootCauseResultSchema.parse({
1522
+ rootCauseNode: loc.rootCauseNode,
1523
+ rootCauseReason: loc.rootCauseReason,
1524
+ traversalPath,
1525
+ edgeProvenances,
1526
+ confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
1527
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1528
+ });
1529
+ }
1530
+ function isFailingCallEdge(e) {
1531
+ return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1532
+ }
1533
+ function callSourcesForService(graph, serviceId3) {
1534
+ const ids = [serviceId3];
1535
+ for (const edgeId of graph.outboundEdges(serviceId3)) {
1536
+ const e = graph.getEdgeAttributes(edgeId);
1537
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1538
+ const tgt = graph.getNodeAttributes(e.target);
1539
+ if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
1540
+ }
1541
+ return ids;
1542
+ }
1543
+ function failingCallDominates(e, id, curEdge, curId) {
1544
+ const ec = e.signal?.errorCount ?? 0;
1545
+ const cc = curEdge.signal?.errorCount ?? 0;
1546
+ if (ec !== cc) return ec > cc;
1547
+ if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
1548
+ return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
1549
+ }
1550
+ return id < curId;
1551
+ }
1552
+ function dominantFailingCall(graph, serviceId3, visited) {
1553
+ let best = null;
1554
+ for (const src of callSourcesForService(graph, serviceId3)) {
1555
+ for (const edgeId of graph.outboundEdges(src)) {
1556
+ const e = graph.getEdgeAttributes(edgeId);
1557
+ if (!isFailingCallEdge(e)) continue;
1558
+ if (isFrontierNode(graph, e.target)) continue;
1559
+ const owner = resolveOwningService(graph, e.target);
1560
+ if (!owner || visited.has(owner.id)) continue;
1561
+ if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
1562
+ best = { nextService: owner.id, edge: e };
1563
+ }
1564
+ }
1565
+ }
1566
+ return best;
1567
+ }
1568
+ function followFailingCallChain(graph, originServiceId, maxDepth) {
1569
+ const path43 = [originServiceId];
1570
+ const edges = [];
1571
+ const visited = /* @__PURE__ */ new Set([originServiceId]);
1572
+ let current = originServiceId;
1573
+ for (let depth = 0; depth < maxDepth; depth++) {
1574
+ const hop = dominantFailingCall(graph, current, visited);
1575
+ if (!hop) break;
1576
+ path43.push(hop.nextService);
1577
+ edges.push(hop.edge);
1578
+ visited.add(hop.nextService);
1579
+ current = hop.nextService;
1580
+ }
1581
+ if (edges.length === 0) return null;
1582
+ return { path: path43, edges, culprit: current };
1583
+ }
1584
+ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1585
+ const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1586
+ if (!chain) return null;
1587
+ const culprit = chain.culprit;
1588
+ const path43 = [...chain.path];
1589
+ const edgeProvenances = chain.edges.map((e) => e.provenance);
1590
+ const baseConfidence = confidenceFromMix(chain.edges);
1591
+ const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
1592
+ const loc = localizeFromIncidents(culprit, incidents, errorEvent);
1593
+ if (loc) {
1594
+ let rootCauseNode = culprit;
1595
+ if (loc.fileNode) {
1596
+ path43.push(loc.fileNode);
1597
+ edgeProvenances.push(import_types.Provenance.OBSERVED);
1598
+ rootCauseNode = loc.fileNode;
1599
+ }
1600
+ return import_types.RootCauseResultSchema.parse({
1601
+ rootCauseNode,
1602
+ rootCauseReason: loc.rootCauseReason,
1603
+ traversalPath: path43,
1604
+ edgeProvenances,
1605
+ confidence,
1606
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1607
+ });
1608
+ }
1609
+ const lastEdge = chain.edges[chain.edges.length - 1];
1610
+ const errs = lastEdge.signal?.errorCount ?? 0;
1611
+ const culpritName = culprit.replace(/^service:/, "");
1470
1612
  return import_types.RootCauseResultSchema.parse({
1471
- rootCauseNode: match.rootCauseNode,
1472
- rootCauseReason: reason,
1473
- traversalPath: walk.path,
1474
- edgeProvenances: walk.edges.map((e) => e.provenance),
1475
- confidence: confidenceFromMix(walk.edges),
1476
- fixRecommendation: match.fixRecommendation
1613
+ rootCauseNode: culprit,
1614
+ rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1615
+ traversalPath: path43,
1616
+ edgeProvenances,
1617
+ confidence,
1618
+ fixRecommendation: `Inspect ${culpritName}'s failing handler`
1477
1619
  });
1478
1620
  }
1479
1621
  function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
@@ -1496,14 +1638,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
1496
1638
  });
1497
1639
  }
1498
1640
  if (frame.distance >= maxDepth) continue;
1499
- const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
1500
- for (const [tgtId, edge] of outgoing) {
1501
- if (enqueued.has(tgtId)) continue;
1502
- enqueued.add(tgtId);
1641
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
1642
+ for (const [srcId, edge] of incoming) {
1643
+ if (enqueued.has(srcId)) continue;
1644
+ enqueued.add(srcId);
1503
1645
  queue.push({
1504
- nodeId: tgtId,
1646
+ nodeId: srcId,
1505
1647
  distance: frame.distance + 1,
1506
- path: [...frame.path, tgtId],
1648
+ path: [...frame.path, srcId],
1507
1649
  pathEdges: [...frame.pathEdges, edge]
1508
1650
  });
1509
1651
  }
@@ -1559,6 +1701,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
1559
1701
  total: dependencies.length
1560
1702
  });
1561
1703
  }
1704
+ function getObservedDependencies(graph, nodeId) {
1705
+ if (!graph.hasNode(nodeId)) {
1706
+ return import_types.ObservedDependenciesResultSchema.parse({
1707
+ origin: nodeId,
1708
+ dependencies: [],
1709
+ observed: false,
1710
+ inboundObservedCount: 0,
1711
+ hasExtractedOutbound: false
1712
+ });
1713
+ }
1714
+ const attrs = graph.getNodeAttributes(nodeId);
1715
+ const scope = [nodeId];
1716
+ if (attrs.type === import_types.NodeType.ServiceNode) {
1717
+ for (const edgeId of graph.outboundEdges(nodeId)) {
1718
+ const e = graph.getEdgeAttributes(edgeId);
1719
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1720
+ const owned = graph.getNodeAttributes(e.target);
1721
+ if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
1722
+ }
1723
+ }
1724
+ const dependencies = [];
1725
+ const seenEdge = /* @__PURE__ */ new Set();
1726
+ let hasExtractedOutbound = false;
1727
+ for (const src of scope) {
1728
+ for (const edgeId of graph.outboundEdges(src)) {
1729
+ const e = graph.getEdgeAttributes(edgeId);
1730
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1731
+ if (e.provenance === import_types.Provenance.OBSERVED) {
1732
+ if (!seenEdge.has(e.id)) {
1733
+ seenEdge.add(e.id);
1734
+ dependencies.push(e);
1735
+ }
1736
+ } else if (e.provenance === import_types.Provenance.EXTRACTED) {
1737
+ hasExtractedOutbound = true;
1738
+ }
1739
+ }
1740
+ }
1741
+ let inboundObservedCount = 0;
1742
+ for (const tgt of scope) {
1743
+ for (const edgeId of graph.inboundEdges(tgt)) {
1744
+ const e = graph.getEdgeAttributes(edgeId);
1745
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1746
+ if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
1747
+ }
1748
+ }
1749
+ dependencies.sort(
1750
+ (a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
1751
+ );
1752
+ return import_types.ObservedDependenciesResultSchema.parse({
1753
+ origin: nodeId,
1754
+ dependencies,
1755
+ observed: dependencies.length > 0 || inboundObservedCount > 0,
1756
+ inboundObservedCount,
1757
+ hasExtractedOutbound
1758
+ });
1759
+ }
1562
1760
 
1563
1761
  // src/divergences.ts
1564
1762
  function bucketKey(source, target, type) {
@@ -1611,10 +1809,16 @@ function gradedConfidence(edge) {
1611
1809
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
1612
1810
  return clampConfidence(confidenceForEdge(edge));
1613
1811
  }
1812
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
1813
+ import_types2.EdgeType.CALLS,
1814
+ import_types2.EdgeType.CONNECTS_TO,
1815
+ import_types2.EdgeType.PUBLISHES_TO,
1816
+ import_types2.EdgeType.CONSUMES_FROM
1817
+ ]);
1614
1818
  function detectMissingDivergences(graph, bucket) {
1615
1819
  const out = [];
1616
1820
  if (bucket.type === import_types2.EdgeType.CONTAINS) return out;
1617
- if (bucket.extracted && !bucket.observed) {
1821
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
1618
1822
  if (!nodeIsFrontier(graph, bucket.target)) {
1619
1823
  out.push({
1620
1824
  type: "missing-observed",
@@ -1748,6 +1952,23 @@ function detectCompatDivergences(graph, svcId, svc) {
1748
1952
  function involvesNode(d, nodeId) {
1749
1953
  return d.source === nodeId || d.target === nodeId;
1750
1954
  }
1955
+ function suppressHostMismatchHalves(all) {
1956
+ const observedHalf = /* @__PURE__ */ new Set();
1957
+ const declaredHalf = /* @__PURE__ */ new Set();
1958
+ for (const d of all) {
1959
+ if (d.type !== "host-mismatch") continue;
1960
+ observedHalf.add(`${d.source}->${d.target}`);
1961
+ declaredHalf.add((0, import_types2.databaseId)(d.extractedHost));
1962
+ }
1963
+ if (observedHalf.size === 0) return all;
1964
+ return all.filter((d) => {
1965
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
1966
+ return false;
1967
+ }
1968
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
1969
+ return true;
1970
+ });
1971
+ }
1751
1972
  function computeDivergences(graph, opts = {}) {
1752
1973
  const all = [];
1753
1974
  const buckets = bucketEdges(graph);
@@ -1761,7 +1982,8 @@ function computeDivergences(graph, opts = {}) {
1761
1982
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
1762
1983
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
1763
1984
  });
1764
- let filtered = all;
1985
+ const reconciled = suppressHostMismatchHalves(all);
1986
+ let filtered = reconciled;
1765
1987
  if (opts.type) {
1766
1988
  const allowed = opts.type;
1767
1989
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -2082,6 +2304,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
2082
2304
  }
2083
2305
  return out;
2084
2306
  }
2307
+ function selectApplicablePolicies(graph, policies, nodeId) {
2308
+ if (!graph.hasNode(nodeId)) return [];
2309
+ const node = graph.getNodeAttributes(nodeId);
2310
+ const out = [];
2311
+ for (const policy of policies) {
2312
+ const m = matchPolicyToNode(graph, policy, nodeId, node);
2313
+ if (!m) continue;
2314
+ out.push({
2315
+ policyId: policy.id,
2316
+ policyName: policy.name,
2317
+ ...policy.description !== void 0 ? { description: policy.description } : {},
2318
+ severity: policy.severity,
2319
+ onViolation: resolveOnViolation(policy),
2320
+ ruleType: policy.rule.type,
2321
+ match: m.match,
2322
+ reason: m.reason
2323
+ });
2324
+ }
2325
+ return out;
2326
+ }
2327
+ function requiredProvenanceList(required) {
2328
+ if (Array.isArray(required)) return required.join(" | ");
2329
+ return String(required);
2330
+ }
2331
+ function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
2332
+ const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
2333
+ for (const edgeId of incident) {
2334
+ const e = graph.getEdgeAttributes(edgeId);
2335
+ if (e.type !== edgeType) continue;
2336
+ if (requiredOtherEnd === void 0) return true;
2337
+ if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
2338
+ }
2339
+ return false;
2340
+ }
2341
+ function blastRadiusSubjectReaching(graph, rule, nodeId) {
2342
+ let found = null;
2343
+ graph.forEachNode((subjId, attrs) => {
2344
+ if (found !== null) return;
2345
+ if (subjId === nodeId) return;
2346
+ if (attrs.type !== rule.nodeType) return;
2347
+ const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
2348
+ if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
2349
+ });
2350
+ return found;
2351
+ }
2352
+ function matchPolicyToNode(graph, policy, nodeId, node) {
2353
+ const rule = policy.rule;
2354
+ switch (rule.type) {
2355
+ case "structural": {
2356
+ if (node.type === rule.fromNodeType) {
2357
+ return {
2358
+ match: "subject",
2359
+ reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
2360
+ };
2361
+ }
2362
+ if (node.type === rule.toNodeType) {
2363
+ return {
2364
+ match: "region",
2365
+ reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
2366
+ };
2367
+ }
2368
+ return null;
2369
+ }
2370
+ case "ownership": {
2371
+ if (node.type === rule.nodeType) {
2372
+ return {
2373
+ match: "subject",
2374
+ reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
2375
+ };
2376
+ }
2377
+ return null;
2378
+ }
2379
+ case "blast-radius": {
2380
+ const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
2381
+ if (node.type === rule.nodeType) {
2382
+ return {
2383
+ match: "subject",
2384
+ reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
2385
+ };
2386
+ }
2387
+ const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
2388
+ if (subject) {
2389
+ return {
2390
+ match: "region",
2391
+ 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`
2392
+ };
2393
+ }
2394
+ return null;
2395
+ }
2396
+ case "compatibility": {
2397
+ const kindLabel = rule.kind ?? "all compat shapes";
2398
+ if (node.type === import_types3.NodeType.ServiceNode) {
2399
+ return {
2400
+ match: "subject",
2401
+ reason: `this service's dependencies are compatibility-checked (${kindLabel})`
2402
+ };
2403
+ }
2404
+ const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
2405
+ if (reachesDriverEngine && node.type === import_types3.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types3.EdgeType.CONNECTS_TO)) {
2406
+ return {
2407
+ match: "region",
2408
+ reason: "services connecting to this database have their driver/engine compatibility checked against it"
2409
+ };
2410
+ }
2411
+ return null;
2412
+ }
2413
+ case "provenance": {
2414
+ const requiredList = requiredProvenanceList(rule.required);
2415
+ if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
2416
+ return {
2417
+ match: "subject",
2418
+ reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
2419
+ };
2420
+ }
2421
+ if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
2422
+ return {
2423
+ match: "region",
2424
+ 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`
2425
+ };
2426
+ }
2427
+ return null;
2428
+ }
2429
+ }
2430
+ }
2085
2431
  async function loadPolicyFile(policyPath) {
2086
2432
  let raw;
2087
2433
  try {
@@ -2200,9 +2546,9 @@ function loadIncidentThresholdsFromEnv() {
2200
2546
  return DEFAULT_INCIDENT_THRESHOLDS;
2201
2547
  }
2202
2548
  }
2203
- function httpResponseStatus(span) {
2549
+ function httpResponseStatusFromAttrs(attrs) {
2204
2550
  for (const key of ["http.response.status_code", "http.status_code"]) {
2205
- const v = span.attributes[key];
2551
+ const v = attrs[key];
2206
2552
  if (typeof v === "number" && Number.isFinite(v)) return v;
2207
2553
  if (typeof v === "string") {
2208
2554
  const n = Number(v);
@@ -2211,6 +2557,63 @@ function httpResponseStatus(span) {
2211
2557
  }
2212
2558
  return void 0;
2213
2559
  }
2560
+ function httpResponseStatus(span) {
2561
+ return httpResponseStatusFromAttrs(span.attributes);
2562
+ }
2563
+ function httpFailureMessageFromAttrs(attrs) {
2564
+ const status2 = httpResponseStatusFromAttrs(attrs);
2565
+ const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
2566
+ const method = pickAttrFrom(attrs, "http.request.method", "http.method");
2567
+ const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
2568
+ if (status2 !== void 0 && where) return `${status2} on ${where}`;
2569
+ if (status2 !== void 0) return `HTTP ${status2}`;
2570
+ if (where) return `error on ${where}`;
2571
+ return void 0;
2572
+ }
2573
+ var GRPC_STATUS_NAMES = {
2574
+ 1: "CANCELLED",
2575
+ 2: "UNKNOWN",
2576
+ 3: "INVALID_ARGUMENT",
2577
+ 4: "DEADLINE_EXCEEDED",
2578
+ 5: "NOT_FOUND",
2579
+ 6: "ALREADY_EXISTS",
2580
+ 7: "PERMISSION_DENIED",
2581
+ 8: "RESOURCE_EXHAUSTED",
2582
+ 9: "FAILED_PRECONDITION",
2583
+ 10: "ABORTED",
2584
+ 11: "OUT_OF_RANGE",
2585
+ 12: "UNIMPLEMENTED",
2586
+ 13: "INTERNAL",
2587
+ 14: "UNAVAILABLE",
2588
+ 15: "DATA_LOSS",
2589
+ 16: "UNAUTHENTICATED"
2590
+ };
2591
+ function grpcStatusCodeFromAttrs(attrs) {
2592
+ const v = attrs["rpc.grpc.status_code"];
2593
+ if (typeof v === "number" && Number.isFinite(v)) return v;
2594
+ if (typeof v === "string") {
2595
+ const n = Number(v);
2596
+ if (Number.isFinite(n)) return n;
2597
+ }
2598
+ return void 0;
2599
+ }
2600
+ function nonHttpFailureMessageFromAttrs(attrs) {
2601
+ const grpc2 = grpcStatusCodeFromAttrs(attrs);
2602
+ if (grpc2 !== void 0 && grpc2 !== 0) {
2603
+ const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
2604
+ const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
2605
+ return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
2606
+ }
2607
+ const errType = pickAttrFrom(attrs, "error.type");
2608
+ if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
2609
+ const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
2610
+ return peer ? `${errType} connecting to ${peer}` : errType;
2611
+ }
2612
+ return void 0;
2613
+ }
2614
+ function incidentMessage(span) {
2615
+ return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
2616
+ }
2214
2617
  function nowIso(ctx) {
2215
2618
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
2216
2619
  }
@@ -2230,13 +2633,16 @@ function warnNoSourceMaps(serviceName) {
2230
2633
  `[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.`
2231
2634
  );
2232
2635
  }
2233
- function pickAttr(span, ...keys) {
2636
+ function pickAttrFrom(attrs, ...keys) {
2234
2637
  for (const k of keys) {
2235
- const v = span.attributes[k];
2638
+ const v = attrs[k];
2236
2639
  if (typeof v === "string" && v.length > 0) return v;
2237
2640
  }
2238
2641
  return void 0;
2239
2642
  }
2643
+ function pickAttr(span, ...keys) {
2644
+ return pickAttrFrom(span.attributes, ...keys);
2645
+ }
2240
2646
  function hostFromUrl(u) {
2241
2647
  if (!u) return void 0;
2242
2648
  try {
@@ -2248,6 +2654,10 @@ function hostFromUrl(u) {
2248
2654
  function pickAddress(span) {
2249
2655
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
2250
2656
  }
2657
+ function isLoopbackHost(host) {
2658
+ const h = host.toLowerCase();
2659
+ return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
2660
+ }
2251
2661
  var CODE_FILEPATH_ATTR = "code.filepath";
2252
2662
  var CODE_LINENO_ATTR = "code.lineno";
2253
2663
  var CODE_FUNCTION_ATTR = "code.function";
@@ -2355,15 +2765,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
2355
2765
  ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
2356
2766
  };
2357
2767
  }
2768
+ function reconcileObservedRelPath(graph, serviceName, relPath) {
2769
+ if (graph.hasNode((0, import_types4.fileId)(serviceName, relPath))) return relPath;
2770
+ let best = null;
2771
+ graph.forEachNode((_id, attrs) => {
2772
+ const a = attrs;
2773
+ if (a.type !== import_types4.NodeType.FileNode || a.service !== serviceName) return;
2774
+ if (a.discoveredVia === "otel") return;
2775
+ const p = a.path;
2776
+ if (!p) return;
2777
+ if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
2778
+ best = p;
2779
+ }
2780
+ });
2781
+ return best ?? relPath;
2782
+ }
2358
2783
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
2359
- const fileNodeId = (0, import_types4.fileId)(serviceName, callSite.relPath);
2784
+ const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
2785
+ const fileNodeId = (0, import_types4.fileId)(serviceName, relPath);
2360
2786
  if (!graph.hasNode(fileNodeId)) {
2361
- const language = languageForExt(callSite.relPath);
2787
+ const language = languageForExt(relPath);
2362
2788
  const node = {
2363
2789
  id: fileNodeId,
2364
2790
  type: import_types4.NodeType.FileNode,
2365
2791
  service: serviceName,
2366
- path: callSite.relPath,
2792
+ path: relPath,
2367
2793
  ...language ? { language } : {},
2368
2794
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
2369
2795
  discoveredVia: "otel"
@@ -2391,6 +2817,11 @@ function makeInferredEdgeId(type, source, target) {
2391
2817
  }
2392
2818
  var INFERRED_CONFIDENCE = 0.6;
2393
2819
  var STITCH_MAX_DEPTH = 2;
2820
+ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2821
+ import_types4.EdgeType.CALLS,
2822
+ import_types4.EdgeType.CONNECTS_TO,
2823
+ import_types4.EdgeType.DEPENDS_ON
2824
+ ]);
2394
2825
  var WIRE_SPAN_KIND_CLIENT = 3;
2395
2826
  var WIRE_SPAN_KIND_PRODUCER = 4;
2396
2827
  function spanMintsObservedEdge(kind) {
@@ -2564,6 +2995,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
2564
2995
  for (const edgeId of outbound) {
2565
2996
  const edge = graph.getEdgeAttributes(edgeId);
2566
2997
  if (edge.provenance !== import_types4.Provenance.EXTRACTED) continue;
2998
+ if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
2567
2999
  if (graph.hasEdge((0, import_types4.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
2568
3000
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
2569
3001
  if (!visited.has(edge.target)) {
@@ -2682,7 +3114,10 @@ async function handleSpan(ctx, span) {
2682
3114
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2683
3115
  cacheSpanService(span, nowMs, callSite);
2684
3116
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2685
- const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
3117
+ const callSiteEvidence = callSite ? {
3118
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
3119
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
3120
+ } : void 0;
2686
3121
  let affectedNode = sourceId;
2687
3122
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2688
3123
  if (span.dbSystem) {
@@ -2704,7 +3139,7 @@ async function handleSpan(ctx, span) {
2704
3139
  } else {
2705
3140
  const host = pickAddress(span);
2706
3141
  let resolvedViaAddress = false;
2707
- if (mintsFromCallerSide && host && host !== span.service) {
3142
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
2708
3143
  const targetId = resolveServiceId(ctx.graph, host, env);
2709
3144
  if (targetId && targetId !== sourceId) {
2710
3145
  upsertObservedEdge(
@@ -2739,7 +3174,11 @@ async function handleSpan(ctx, span) {
2739
3174
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2740
3175
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2741
3176
  const fallbackEvidence = parent.callSite ? {
2742
- file: parent.callSite.relPath,
3177
+ file: reconcileObservedRelPath(
3178
+ ctx.graph,
3179
+ parent.service,
3180
+ parent.callSite.relPath
3181
+ ),
2743
3182
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2744
3183
  } : void 0;
2745
3184
  upsertObservedEdge(
@@ -2764,7 +3203,7 @@ async function handleSpan(ctx, span) {
2764
3203
  service: span.service,
2765
3204
  traceId: span.traceId,
2766
3205
  spanId: span.spanId,
2767
- errorMessage: span.exception?.message ?? "unknown error",
3206
+ errorMessage: incidentMessage(span),
2768
3207
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2769
3208
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2770
3209
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -2940,12 +3379,43 @@ function startStalenessLoop(graph, options = {}) {
2940
3379
  async function readErrorEvents(errorsPath) {
2941
3380
  try {
2942
3381
  const raw = await import_node_fs5.promises.readFile(errorsPath, "utf8");
2943
- return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
3382
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
3383
+ return dedupeIncidents(events);
2944
3384
  } catch (err) {
2945
3385
  if (err.code === "ENOENT") return [];
2946
3386
  throw err;
2947
3387
  }
2948
3388
  }
3389
+ function isSynthesizedHttpIncident(ev) {
3390
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
3391
+ if (ev.errorType) return false;
3392
+ if (!ev.attributes) return false;
3393
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
3394
+ return synth !== void 0 && synth === ev.errorMessage;
3395
+ }
3396
+ function dedupeIncidents(events) {
3397
+ const seen = /* @__PURE__ */ new Set();
3398
+ const once = [];
3399
+ for (const ev of events) {
3400
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
3401
+ if (key === void 0) {
3402
+ once.push(ev);
3403
+ continue;
3404
+ }
3405
+ if (seen.has(key)) continue;
3406
+ seen.add(key);
3407
+ once.push(ev);
3408
+ }
3409
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
3410
+ const hasRealFailure = /* @__PURE__ */ new Set();
3411
+ for (const ev of once) {
3412
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
3413
+ }
3414
+ return once.filter((ev) => {
3415
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
3416
+ return !hasRealFailure.has(groupKey(ev));
3417
+ });
3418
+ }
2949
3419
  function mergeSnapshot(graph, snapshot) {
2950
3420
  const exported = snapshot.graph;
2951
3421
  let nodesAdded = 0;
@@ -3026,10 +3496,17 @@ function isConfigFile(name) {
3026
3496
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
3027
3497
  if (name === ".env" || name.startsWith(".env.")) {
3028
3498
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
3499
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
3029
3500
  return { match: true, fileType: "env" };
3030
3501
  }
3031
3502
  return { match: false, fileType: "" };
3032
3503
  }
3504
+ function isNeatAuthoredEnvFile(name) {
3505
+ return name === ".env.neat";
3506
+ }
3507
+ function isNeatAuthoredSourceFile(name) {
3508
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
3509
+ }
3033
3510
  function isTestPath(filePath) {
3034
3511
  const normalised = filePath.replace(/\\/g, "/");
3035
3512
  const segments = normalised.split("/");
@@ -3723,7 +4200,9 @@ async function walkSourceFiles(dir) {
3723
4200
  if (IGNORED_DIRS.has(entry.name)) continue;
3724
4201
  if (await isPythonVenvDir(full)) continue;
3725
4202
  await walk(full);
3726
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name))) {
4203
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
4204
+ // would attribute our instrumentation imports to the user's service.
4205
+ !isNeatAuthoredSourceFile(entry.name)) {
3727
4206
  out.push(full);
3728
4207
  }
3729
4208
  }
@@ -3893,11 +4372,28 @@ function collectJsImports(node, out) {
3893
4372
  if (child) collectJsImports(child, out);
3894
4373
  }
3895
4374
  }
4375
+ function collectImportedNames(node, out) {
4376
+ if (node.type === "aliased_import") {
4377
+ const nameNode = node.childForFieldName("name");
4378
+ if (nameNode) out.push(nameNode.text);
4379
+ return;
4380
+ }
4381
+ if (node.type === "dotted_name") {
4382
+ out.push(node.text);
4383
+ return;
4384
+ }
4385
+ for (let i = 0; i < node.namedChildCount; i++) {
4386
+ const child = node.namedChild(i);
4387
+ if (child) collectImportedNames(child, out);
4388
+ }
4389
+ }
3896
4390
  function collectPyImports(node, out) {
3897
4391
  if (node.type === "import_from_statement") {
3898
4392
  let level = 0;
3899
4393
  let modulePath = "";
4394
+ const names = [];
3900
4395
  let pastFrom = false;
4396
+ let pastImport = false;
3901
4397
  for (let i = 0; i < node.childCount; i++) {
3902
4398
  const child = node.child(i);
3903
4399
  if (!child) continue;
@@ -3905,26 +4401,30 @@ function collectPyImports(node, out) {
3905
4401
  if (child.type === "from") pastFrom = true;
3906
4402
  continue;
3907
4403
  }
3908
- if (child.type === "import") break;
3909
- if (child.type === "relative_import") {
3910
- for (let j = 0; j < child.childCount; j++) {
3911
- const rc = child.child(j);
3912
- if (!rc) continue;
3913
- if (rc.type === "import_prefix") {
3914
- for (let k = 0; k < rc.childCount; k++) {
3915
- if (rc.child(k)?.type === ".") level++;
3916
- }
3917
- } else if (rc.type === "dotted_name") modulePath = rc.text;
4404
+ if (!pastImport) {
4405
+ if (child.type === "import") {
4406
+ pastImport = true;
4407
+ continue;
3918
4408
  }
3919
- break;
3920
- }
3921
- if (child.type === "dotted_name") {
3922
- modulePath = child.text;
3923
- break;
4409
+ if (child.type === "relative_import") {
4410
+ for (let j = 0; j < child.childCount; j++) {
4411
+ const rc = child.child(j);
4412
+ if (!rc) continue;
4413
+ if (rc.type === "import_prefix") {
4414
+ for (let k = 0; k < rc.childCount; k++) {
4415
+ if (rc.child(k)?.type === ".") level++;
4416
+ }
4417
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
4418
+ }
4419
+ } else if (child.type === "dotted_name") {
4420
+ modulePath = child.text;
4421
+ }
4422
+ continue;
3924
4423
  }
4424
+ collectImportedNames(child, names);
3925
4425
  }
3926
4426
  if (level > 0 || modulePath) {
3927
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
4427
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3928
4428
  }
3929
4429
  }
3930
4430
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -4026,8 +4526,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
4026
4526
  return null;
4027
4527
  }
4028
4528
  async function resolvePyImport(imp, importerPath, serviceDir) {
4029
- if (!imp.modulePath) return null;
4030
- const relPath = imp.modulePath.split(".").join("/");
4031
4529
  let baseDir;
4032
4530
  if (imp.level > 0) {
4033
4531
  baseDir = import_node_path14.default.dirname(importerPath);
@@ -4035,13 +4533,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
4035
4533
  } else {
4036
4534
  baseDir = serviceDir;
4037
4535
  }
4038
- const candidates = [import_node_path14.default.join(baseDir, `${relPath}.py`), import_node_path14.default.join(baseDir, relPath, "__init__.py")];
4039
- for (const candidate of candidates) {
4040
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists2(candidate)) {
4041
- return toPosix2(import_node_path14.default.relative(serviceDir, candidate));
4536
+ const moduleBase = imp.modulePath ? import_node_path14.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
4537
+ const resolved = /* @__PURE__ */ new Set();
4538
+ let needModuleFile = imp.names.length === 0;
4539
+ for (const name of imp.names) {
4540
+ const submoduleFile = import_node_path14.default.join(moduleBase, `${name}.py`);
4541
+ const subpackageInit = import_node_path14.default.join(moduleBase, name, "__init__.py");
4542
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists2(submoduleFile)) {
4543
+ resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, submoduleFile)));
4544
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists2(subpackageInit)) {
4545
+ resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, subpackageInit)));
4546
+ } else {
4547
+ needModuleFile = true;
4548
+ }
4549
+ }
4550
+ if (needModuleFile) {
4551
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path14.default.join(moduleBase, "__init__.py")] : [import_node_path14.default.join(moduleBase, "__init__.py")];
4552
+ for (const candidate of moduleFileCandidates) {
4553
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists2(candidate)) {
4554
+ resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, candidate)));
4555
+ break;
4556
+ }
4042
4557
  }
4043
4558
  }
4044
- return null;
4559
+ return [...resolved];
4045
4560
  }
4046
4561
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
4047
4562
  const importeeFileId = (0, import_types9.fileId)(serviceName, importeeRelPath);
@@ -4082,17 +4597,18 @@ async function addImports(graph, services) {
4082
4597
  continue;
4083
4598
  }
4084
4599
  for (const imp of pyImports) {
4085
- const resolved = await resolvePyImport(imp, file.path, service.dir);
4086
- if (!resolved) continue;
4087
- edgesAdded += emitImportEdge(
4088
- graph,
4089
- service.pkg.name,
4090
- importerFileId,
4091
- relFile,
4092
- resolved,
4093
- imp.line,
4094
- imp.snippet
4095
- );
4600
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
4601
+ for (const resolved of resolvedPaths) {
4602
+ edgesAdded += emitImportEdge(
4603
+ graph,
4604
+ service.pkg.name,
4605
+ importerFileId,
4606
+ relFile,
4607
+ resolved,
4608
+ imp.line,
4609
+ imp.snippet
4610
+ );
4611
+ }
4096
4612
  }
4097
4613
  continue;
4098
4614
  }
@@ -4728,6 +5244,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4728
5244
  edgesAdded++;
4729
5245
  }
4730
5246
  }
5247
+ if (allConfigs.length === 1) {
5248
+ const primary = allConfigs[0];
5249
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
5250
+ const relPath = import_node_path22.default.relative(scanPath, primary.sourceFile);
5251
+ const cfgId = (0, import_types10.configId)(relPath);
5252
+ if (!graph.hasNode(cfgId)) {
5253
+ const cfgNode = {
5254
+ id: cfgId,
5255
+ type: import_types10.NodeType.ConfigNode,
5256
+ name: import_node_path22.default.basename(primary.sourceFile),
5257
+ path: relPath,
5258
+ fileType: isConfigFile(import_node_path22.default.basename(primary.sourceFile)).fileType || "config"
5259
+ };
5260
+ graph.addNode(cfgId, cfgNode);
5261
+ nodesAdded++;
5262
+ }
5263
+ const cfgEdge = {
5264
+ id: (0, import_types5.extractedEdgeId)(service.node.id, cfgId, import_types10.EdgeType.CONFIGURED_BY),
5265
+ source: service.node.id,
5266
+ target: cfgId,
5267
+ type: import_types10.EdgeType.CONFIGURED_BY,
5268
+ provenance: import_types10.Provenance.EXTRACTED,
5269
+ confidence: (0, import_types10.confidenceForExtracted)("structural"),
5270
+ evidence: { file: toPosix2(relPath) }
5271
+ };
5272
+ if (!graph.hasEdge(cfgEdge.id)) {
5273
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
5274
+ edgesAdded++;
5275
+ }
5276
+ }
4731
5277
  attachIncompatibilities(service, allConfigs);
4732
5278
  if (graph.hasNode(service.node.id)) {
4733
5279
  const current = graph.getNodeAttributes(service.node.id);
@@ -4739,6 +5285,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4739
5285
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
4740
5286
  delete updated.incompatibilities;
4741
5287
  }
5288
+ if (!service.node.dbConnectionTarget) {
5289
+ delete updated.dbConnectionTarget;
5290
+ }
4742
5291
  graph.replaceNodeAttributes(service.node.id, updated);
4743
5292
  }
4744
5293
  }
@@ -4916,7 +5465,7 @@ async function addHttpCallEdges(graph, services) {
4916
5465
  const dedupKey = `${relFile}|${targetId}`;
4917
5466
  if (seen.has(dedupKey)) continue;
4918
5467
  seen.add(dedupKey);
4919
- const confidence = (0, import_types12.confidenceForExtracted)("hostname-shape-match");
5468
+ const confidence = (0, import_types12.confidenceForExtracted)("url-literal-service-target");
4920
5469
  const ev = {
4921
5470
  file: relFile,
4922
5471
  line: site.line,
@@ -4936,7 +5485,7 @@ async function addHttpCallEdges(graph, services) {
4936
5485
  target: targetId,
4937
5486
  type: import_types12.EdgeType.CALLS,
4938
5487
  confidence,
4939
- confidenceKind: "hostname-shape-match",
5488
+ confidenceKind: "url-literal-service-target",
4940
5489
  evidence: ev
4941
5490
  });
4942
5491
  continue;
@@ -5442,19 +5991,29 @@ init_cjs_shims();
5442
5991
  var import_node_path31 = __toESM(require("path"), 1);
5443
5992
  var import_node_fs17 = require("fs");
5444
5993
  var import_types21 = require("@neat.is/types");
5445
- function runtimeImage(content) {
5446
- const lines = content.split("\n");
5447
- let last = null;
5448
- for (const raw of lines) {
5994
+ function readDockerfile(content) {
5995
+ let image = null;
5996
+ const ports = [];
5997
+ let cmd = null;
5998
+ let entrypoint = null;
5999
+ for (const raw of content.split("\n")) {
5449
6000
  const line = raw.trim();
5450
6001
  if (!line || line.startsWith("#")) continue;
5451
- if (!/^from\s+/i.test(line)) continue;
5452
- const tokens = line.split(/\s+/);
5453
- const image = tokens[1];
5454
- if (!image || image.toLowerCase() === "scratch") continue;
5455
- last = image;
6002
+ if (/^from\s+/i.test(line)) {
6003
+ const candidate = line.split(/\s+/)[1];
6004
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
6005
+ } else if (/^expose\s+/i.test(line)) {
6006
+ for (const token of line.split(/\s+/).slice(1)) {
6007
+ const port = Number.parseInt(token.split("/")[0], 10);
6008
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
6009
+ }
6010
+ } else if (/^entrypoint\s+/i.test(line)) {
6011
+ entrypoint = line;
6012
+ } else if (/^cmd\s+/i.test(line)) {
6013
+ cmd = line;
6014
+ }
5456
6015
  }
5457
- return last;
6016
+ return { image, ports, entrypoint: entrypoint ?? cmd };
5458
6017
  }
5459
6018
  async function addDockerfileRuntimes(graph, services, scanPath) {
5460
6019
  let nodesAdded = 0;
@@ -5473,14 +6032,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5473
6032
  );
5474
6033
  continue;
5475
6034
  }
5476
- const image = runtimeImage(content);
5477
- if (!image) continue;
5478
- const node = makeInfraNode("container-image", image);
6035
+ const facts = readDockerfile(content);
6036
+ if (!facts.image) continue;
6037
+ const node = makeInfraNode("container-image", facts.image);
5479
6038
  if (!graph.hasNode(node.id)) {
5480
6039
  graph.addNode(node.id, node);
5481
6040
  nodesAdded++;
5482
6041
  }
5483
6042
  const relDockerfile = toPosix2(import_node_path31.default.relative(service.dir, dockerfilePath));
6043
+ const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, dockerfilePath));
5484
6044
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5485
6045
  graph,
5486
6046
  service.pkg.name,
@@ -5499,12 +6059,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5499
6059
  provenance: import_types21.Provenance.EXTRACTED,
5500
6060
  confidence: (0, import_types21.confidenceForExtracted)("structural"),
5501
6061
  evidence: {
5502
- file: toPosix2(import_node_path31.default.relative(scanPath, dockerfilePath))
6062
+ file: evidenceFile,
6063
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
5503
6064
  }
5504
6065
  };
5505
6066
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5506
6067
  edgesAdded++;
5507
6068
  }
6069
+ for (const port of facts.ports) {
6070
+ const portNode = makeInfraNode("port", String(port));
6071
+ if (!graph.hasNode(portNode.id)) {
6072
+ graph.addNode(portNode.id, portNode);
6073
+ nodesAdded++;
6074
+ }
6075
+ const portEdgeId = (0, import_types5.extractedEdgeId)(fileNodeId, portNode.id, import_types21.EdgeType.CONNECTS_TO);
6076
+ if (graph.hasEdge(portEdgeId)) continue;
6077
+ const portEdge = {
6078
+ id: portEdgeId,
6079
+ source: fileNodeId,
6080
+ target: portNode.id,
6081
+ type: import_types21.EdgeType.CONNECTS_TO,
6082
+ provenance: import_types21.Provenance.EXTRACTED,
6083
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
6084
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
6085
+ };
6086
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
6087
+ edgesAdded++;
6088
+ }
5508
6089
  }
5509
6090
  return { nodesAdded, edgesAdded };
5510
6091
  }
@@ -5513,7 +6094,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5513
6094
  init_cjs_shims();
5514
6095
  var import_node_fs18 = require("fs");
5515
6096
  var import_node_path32 = __toESM(require("path"), 1);
6097
+ var import_types22 = require("@neat.is/types");
5516
6098
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
6099
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5517
6100
  async function walkTfFiles(start, depth = 0, max = 5) {
5518
6101
  if (depth > max) return [];
5519
6102
  const out = [];
@@ -5530,24 +6113,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
5530
6113
  }
5531
6114
  return out;
5532
6115
  }
6116
+ function blockBody(content, from) {
6117
+ const open = content.indexOf("{", from);
6118
+ if (open === -1) return null;
6119
+ let depth = 0;
6120
+ for (let i = open; i < content.length; i++) {
6121
+ const ch = content[i];
6122
+ if (ch === "{") depth++;
6123
+ else if (ch === "}") {
6124
+ depth--;
6125
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
6126
+ }
6127
+ }
6128
+ return null;
6129
+ }
6130
+ function lineAt(content, index) {
6131
+ let line = 1;
6132
+ for (let i = 0; i < index && i < content.length; i++) {
6133
+ if (content[i] === "\n") line++;
6134
+ }
6135
+ return line;
6136
+ }
5533
6137
  async function addTerraformResources(graph, scanPath) {
5534
6138
  let nodesAdded = 0;
6139
+ let edgesAdded = 0;
5535
6140
  const files = await walkTfFiles(scanPath);
5536
6141
  for (const file of files) {
5537
6142
  const content = await import_node_fs18.promises.readFile(file, "utf8");
6143
+ const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, file));
6144
+ const resources = [];
6145
+ const byKey = /* @__PURE__ */ new Map();
5538
6146
  RESOURCE_RE.lastIndex = 0;
5539
6147
  let m;
5540
6148
  while ((m = RESOURCE_RE.exec(content)) !== null) {
5541
- const kind = m[1];
6149
+ const type = m[1];
5542
6150
  const name = m[2];
5543
- const node = makeInfraNode(kind, name, "aws");
6151
+ const node = makeInfraNode(type, name, "aws");
5544
6152
  if (!graph.hasNode(node.id)) {
5545
6153
  graph.addNode(node.id, node);
5546
6154
  nodesAdded++;
5547
6155
  }
6156
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
6157
+ const resource = {
6158
+ type,
6159
+ name,
6160
+ nodeId: node.id,
6161
+ body: span?.body ?? "",
6162
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
6163
+ };
6164
+ resources.push(resource);
6165
+ byKey.set(`${type}.${name}`, resource);
6166
+ }
6167
+ for (const resource of resources) {
6168
+ const seen = /* @__PURE__ */ new Set();
6169
+ REFERENCE_RE.lastIndex = 0;
6170
+ let ref;
6171
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
6172
+ const key = `${ref[1]}.${ref[2]}`;
6173
+ if (key === `${resource.type}.${resource.name}`) continue;
6174
+ const target = byKey.get(key);
6175
+ if (!target) continue;
6176
+ if (seen.has(target.nodeId)) continue;
6177
+ seen.add(target.nodeId);
6178
+ const edgeId = (0, import_types5.extractedEdgeId)(resource.nodeId, target.nodeId, import_types22.EdgeType.DEPENDS_ON);
6179
+ if (graph.hasEdge(edgeId)) continue;
6180
+ const line = lineAt(content, resource.bodyOffset + ref.index);
6181
+ const edge = {
6182
+ id: edgeId,
6183
+ source: resource.nodeId,
6184
+ target: target.nodeId,
6185
+ type: import_types22.EdgeType.DEPENDS_ON,
6186
+ provenance: import_types22.Provenance.EXTRACTED,
6187
+ confidence: (0, import_types22.confidenceForExtracted)("structural"),
6188
+ evidence: { file: evidenceFile, line, snippet: key }
6189
+ };
6190
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
6191
+ edgesAdded++;
6192
+ }
5548
6193
  }
5549
6194
  }
5550
- return { nodesAdded, edgesAdded: 0 };
6195
+ return { nodesAdded, edgesAdded };
5551
6196
  }
5552
6197
 
5553
6198
  // src/extract/infra/k8s.ts
@@ -5625,11 +6270,11 @@ var import_node_path35 = __toESM(require("path"), 1);
5625
6270
  init_cjs_shims();
5626
6271
  var import_node_fs20 = require("fs");
5627
6272
  var import_node_path34 = __toESM(require("path"), 1);
5628
- var import_types22 = require("@neat.is/types");
6273
+ var import_types23 = require("@neat.is/types");
5629
6274
  function dropOrphanedFileNodes(graph) {
5630
6275
  const orphans = [];
5631
6276
  graph.forEachNode((id, attrs) => {
5632
- if (attrs.type !== import_types22.NodeType.FileNode) return;
6277
+ if (attrs.type !== import_types23.NodeType.FileNode) return;
5633
6278
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5634
6279
  orphans.push(id);
5635
6280
  }
@@ -5642,7 +6287,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5642
6287
  const bases = [scanPath, ...serviceDirs];
5643
6288
  graph.forEachEdge((id, attrs) => {
5644
6289
  const edge = attrs;
5645
- if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
6290
+ if (edge.provenance !== import_types23.Provenance.EXTRACTED) return;
5646
6291
  const evidenceFile = edge.evidence?.file;
5647
6292
  if (!evidenceFile) return;
5648
6293
  if (import_node_path34.default.isAbsolute(evidenceFile)) {
@@ -5802,7 +6447,7 @@ function canonicalJson(value) {
5802
6447
  init_cjs_shims();
5803
6448
  var import_node_fs22 = require("fs");
5804
6449
  var import_node_path36 = __toESM(require("path"), 1);
5805
- var import_types23 = require("@neat.is/types");
6450
+ var import_types24 = require("@neat.is/types");
5806
6451
  var SCHEMA_VERSION = 4;
5807
6452
  function migrateV1ToV2(payload) {
5808
6453
  const nodes = payload.graph.nodes;
@@ -5824,12 +6469,12 @@ function migrateV2ToV3(payload) {
5824
6469
  for (const edge of edges) {
5825
6470
  const attrs = edge.attributes;
5826
6471
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5827
- attrs.provenance = import_types23.Provenance.OBSERVED;
6472
+ attrs.provenance = import_types24.Provenance.OBSERVED;
5828
6473
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5829
6474
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5830
6475
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5831
6476
  if (type && source && target) {
5832
- const newId = (0, import_types23.observedEdgeId)(source, target, type);
6477
+ const newId = (0, import_types24.observedEdgeId)(source, target, type);
5833
6478
  attrs.id = newId;
5834
6479
  if (edge.key) edge.key = newId;
5835
6480
  }
@@ -5978,7 +6623,7 @@ init_cjs_shims();
5978
6623
  var import_node_fs23 = require("fs");
5979
6624
  var import_node_os3 = __toESM(require("os"), 1);
5980
6625
  var import_node_path38 = __toESM(require("path"), 1);
5981
- var import_types24 = require("@neat.is/types");
6626
+ var import_types25 = require("@neat.is/types");
5982
6627
  function neatHome() {
5983
6628
  const override = process.env.NEAT_HOME;
5984
6629
  if (override && override.length > 0) return import_node_path38.default.resolve(override);
@@ -5999,7 +6644,7 @@ async function readRegistry() {
5999
6644
  throw err;
6000
6645
  }
6001
6646
  const parsed = JSON.parse(raw);
6002
- return import_types24.RegistryFileSchema.parse(parsed);
6647
+ return import_types25.RegistryFileSchema.parse(parsed);
6003
6648
  }
6004
6649
  async function getProject(name) {
6005
6650
  const reg = await readRegistry();
@@ -6200,6 +6845,18 @@ function registerRoutes(scope, ctx) {
6200
6845
  }
6201
6846
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6202
6847
  });
6848
+ scope.get(
6849
+ "/graph/observed-dependencies/:nodeId",
6850
+ async (req, reply) => {
6851
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6852
+ if (!proj) return;
6853
+ const { nodeId } = req.params;
6854
+ if (!proj.graph.hasNode(nodeId)) {
6855
+ return reply.code(404).send({ error: "node not found", id: nodeId });
6856
+ }
6857
+ return getObservedDependencies(proj.graph, nodeId);
6858
+ }
6859
+ );
6203
6860
  scope.get("/graph/divergences", async (req, reply) => {
6204
6861
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6205
6862
  if (!proj) return;
@@ -6208,11 +6865,11 @@ function registerRoutes(scope, ctx) {
6208
6865
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6209
6866
  const parsed = [];
6210
6867
  for (const c of candidates) {
6211
- const r = import_types25.DivergenceTypeSchema.safeParse(c);
6868
+ const r = import_types26.DivergenceTypeSchema.safeParse(c);
6212
6869
  if (!r.success) {
6213
6870
  return reply.code(400).send({
6214
6871
  error: `unknown divergence type "${c}"`,
6215
- allowed: import_types25.DivergenceTypeSchema.options
6872
+ allowed: import_types26.DivergenceTypeSchema.options
6216
6873
  });
6217
6874
  }
6218
6875
  parsed.push(r.data);
@@ -6260,23 +6917,29 @@ function registerRoutes(scope, ctx) {
6260
6917
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6261
6918
  return { count: sliced.length, total, events: sliced };
6262
6919
  });
6920
+ const incidentHistoryHandler = async (req, reply) => {
6921
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6922
+ if (!proj) return;
6923
+ const { nodeId } = req.params;
6924
+ if (!proj.graph.hasNode(nodeId)) {
6925
+ reply.code(404).send({ error: "node not found", id: nodeId });
6926
+ return;
6927
+ }
6928
+ const epath = errorsPathFor(proj);
6929
+ if (!epath) return { count: 0, total: 0, events: [] };
6930
+ const events = await readErrorEvents(epath);
6931
+ const filtered = events.filter(
6932
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6933
+ );
6934
+ return { count: filtered.length, total: filtered.length, events: filtered };
6935
+ };
6263
6936
  scope.get(
6264
6937
  "/incidents/:nodeId",
6265
- async (req, reply) => {
6266
- const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6267
- if (!proj) return;
6268
- const { nodeId } = req.params;
6269
- if (!proj.graph.hasNode(nodeId)) {
6270
- return reply.code(404).send({ error: "node not found", id: nodeId });
6271
- }
6272
- const epath = errorsPathFor(proj);
6273
- if (!epath) return { count: 0, total: 0, events: [] };
6274
- const events = await readErrorEvents(epath);
6275
- const filtered = events.filter(
6276
- (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6277
- );
6278
- return { count: filtered.length, total: filtered.length, events: filtered };
6279
- }
6938
+ incidentHistoryHandler
6939
+ );
6940
+ scope.get(
6941
+ "/graph/incident-history/:nodeId",
6942
+ incidentHistoryHandler
6280
6943
  );
6281
6944
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6282
6945
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -6285,16 +6948,16 @@ function registerRoutes(scope, ctx) {
6285
6948
  if (!proj.graph.hasNode(nodeId)) {
6286
6949
  return reply.code(404).send({ error: "node not found", id: nodeId });
6287
6950
  }
6288
- let errorEvent;
6289
6951
  const epath = errorsPathFor(proj);
6290
- if (req.query.errorId && epath) {
6291
- const events = await readErrorEvents(epath);
6292
- errorEvent = events.find((e) => e.id === req.query.errorId);
6952
+ const incidents = epath ? await readErrorEvents(epath) : [];
6953
+ let errorEvent;
6954
+ if (req.query.errorId) {
6955
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
6293
6956
  if (!errorEvent) {
6294
6957
  return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
6295
6958
  }
6296
6959
  }
6297
- const result = getRootCause(proj.graph, nodeId, errorEvent);
6960
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6298
6961
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6299
6962
  return result;
6300
6963
  });
@@ -6425,7 +7088,7 @@ function registerRoutes(scope, ctx) {
6425
7088
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6426
7089
  let violations = await log.readAll();
6427
7090
  if (req.query.severity) {
6428
- const sev = import_types25.PolicySeveritySchema.safeParse(req.query.severity);
7091
+ const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
6429
7092
  if (!sev.success) {
6430
7093
  return reply.code(400).send({
6431
7094
  error: "invalid severity",
@@ -6439,10 +7102,32 @@ function registerRoutes(scope, ctx) {
6439
7102
  }
6440
7103
  return { violations };
6441
7104
  });
7105
+ scope.get("/policies/applicable", async (req, reply) => {
7106
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7107
+ if (!proj) return;
7108
+ const nodeId = req.query.node;
7109
+ if (!nodeId) {
7110
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7111
+ }
7112
+ const policyPath = ctx.policyFilePathFor(proj);
7113
+ let policies = [];
7114
+ if (policyPath) {
7115
+ try {
7116
+ policies = await loadPolicyFile(policyPath);
7117
+ } catch (err) {
7118
+ return reply.code(400).send({
7119
+ error: "policy.json failed to parse",
7120
+ details: err.message
7121
+ });
7122
+ }
7123
+ }
7124
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7125
+ return { node: nodeId, applicable };
7126
+ });
6442
7127
  scope.post("/policies/check", async (req, reply) => {
6443
7128
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6444
7129
  if (!proj) return;
6445
- const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7130
+ const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6446
7131
  if (!parsed.success) {
6447
7132
  return reply.code(400).send({
6448
7133
  error: "invalid /policies/check body",