@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/neatd.cjs CHANGED
@@ -29,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
30
  mod
31
31
  ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
33
 
33
34
  // ../../node_modules/tsup/assets/cjs_shims.js
34
35
  var getImportMetaUrl, importMetaUrl;
@@ -41,13 +42,13 @@ var init_cjs_shims = __esm({
41
42
  });
42
43
 
43
44
  // src/auth.ts
44
- function isLoopbackHost(host) {
45
+ function isLoopbackHost2(host) {
45
46
  if (!host) return false;
46
47
  return LOOPBACK_HOSTS.has(host);
47
48
  }
48
49
  function assertBindAuthority(host, token) {
49
50
  if (token && token.length > 0) return;
50
- if (isLoopbackHost(host)) return;
51
+ if (isLoopbackHost2(host)) return;
51
52
  throw new BindAuthorityError(host);
52
53
  }
53
54
  function mountBearerAuth(app, opts) {
@@ -573,7 +574,23 @@ async function buildOtelReceiver(opts) {
573
574
  };
574
575
  return decorated;
575
576
  }
576
- var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
577
+ async function listenSteppingOtlp(app, requestedPort, host) {
578
+ let port = requestedPort;
579
+ for (let attempt = 0; ; attempt++) {
580
+ try {
581
+ return await app.listen({ port, host });
582
+ } catch (err) {
583
+ const code = err.code;
584
+ const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
585
+ if (!canStep) throw err;
586
+ console.warn(
587
+ `otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
588
+ );
589
+ port += OTLP_STEP_STRIDE;
590
+ }
591
+ }
592
+ }
593
+ var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
577
594
  var init_otel = __esm({
578
595
  "src/otel.ts"() {
579
596
  "use strict";
@@ -589,10 +606,17 @@ var init_otel = __esm({
589
606
  exportTraceServiceRequestType = null;
590
607
  exportTraceServiceResponseType = null;
591
608
  cachedProtobufResponseBody = null;
609
+ OTLP_STEP_ATTEMPTS = 8;
610
+ OTLP_STEP_STRIDE = 1;
592
611
  }
593
612
  });
594
613
 
595
614
  // src/neatd.ts
615
+ var neatd_exports = {};
616
+ __export(neatd_exports, {
617
+ installIngestFaultHandlers: () => installIngestFaultHandlers
618
+ });
619
+ module.exports = __toCommonJS(neatd_exports);
596
620
  init_cjs_shims();
597
621
  var import_node_fs27 = require("fs");
598
622
  var import_node_path44 = __toESM(require("path"), 1);
@@ -1229,22 +1253,164 @@ var rootCauseShapes = {
1229
1253
  [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1230
1254
  [import_types.NodeType.FileNode]: fileRootCauseShape
1231
1255
  };
1232
- function getRootCause(graph, errorNodeId, errorEvent) {
1256
+ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1233
1257
  if (!graph.hasNode(errorNodeId)) return null;
1234
1258
  const origin = graph.getNodeAttributes(errorNodeId);
1235
1259
  const shape = rootCauseShapes[origin.type];
1236
- if (!shape) return null;
1237
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1238
- const match = shape(graph, origin, walk);
1239
- if (!match) return null;
1240
- const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1260
+ if (shape) {
1261
+ const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1262
+ const match = shape(graph, origin, walk);
1263
+ if (match) {
1264
+ const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1265
+ return import_types.RootCauseResultSchema.parse({
1266
+ rootCauseNode: match.rootCauseNode,
1267
+ rootCauseReason: reason,
1268
+ traversalPath: walk.path,
1269
+ edgeProvenances: walk.edges.map((e) => e.provenance),
1270
+ confidence: confidenceFromMix(walk.edges),
1271
+ fixRecommendation: match.fixRecommendation
1272
+ });
1273
+ }
1274
+ }
1275
+ if (origin.type === import_types.NodeType.ServiceNode) {
1276
+ const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
1277
+ if (crossService) return crossService;
1278
+ }
1279
+ return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
1280
+ }
1281
+ var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
1282
+ function incidentMatchesNode(ev, nodeId) {
1283
+ return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
1284
+ }
1285
+ function localizeFromIncidents(nodeId, incidents, errorEvent) {
1286
+ const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
1287
+ const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
1288
+ if (relevant.length === 0) return null;
1289
+ const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
1290
+ const attrs = latest.attributes ?? {};
1291
+ const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
1292
+ const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
1293
+ const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
1294
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
1295
+ const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
1296
+ const count = sameMode.length;
1297
+ const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
1298
+ const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
1299
+ if (location) reasonParts.push(`surfaced at ${location}`);
1300
+ const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
1301
+ const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
1302
+ const fileNode = localizesToFile ? latest.affectedNode : void 0;
1303
+ const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
1304
+ return {
1305
+ rootCauseNode: fileNode ?? nodeId,
1306
+ rootCauseReason,
1307
+ ...fileNode ? { fileNode } : {},
1308
+ ...fixRecommendation ? { fixRecommendation } : {}
1309
+ };
1310
+ }
1311
+ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1312
+ const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
1313
+ if (!loc) return null;
1314
+ const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
1315
+ const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
1241
1316
  return import_types.RootCauseResultSchema.parse({
1242
- rootCauseNode: match.rootCauseNode,
1243
- rootCauseReason: reason,
1244
- traversalPath: walk.path,
1245
- edgeProvenances: walk.edges.map((e) => e.provenance),
1246
- confidence: confidenceFromMix(walk.edges),
1247
- fixRecommendation: match.fixRecommendation
1317
+ rootCauseNode: loc.rootCauseNode,
1318
+ rootCauseReason: loc.rootCauseReason,
1319
+ traversalPath,
1320
+ edgeProvenances,
1321
+ confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
1322
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1323
+ });
1324
+ }
1325
+ function isFailingCallEdge(e) {
1326
+ return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1327
+ }
1328
+ function callSourcesForService(graph, serviceId3) {
1329
+ const ids = [serviceId3];
1330
+ for (const edgeId of graph.outboundEdges(serviceId3)) {
1331
+ const e = graph.getEdgeAttributes(edgeId);
1332
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1333
+ const tgt = graph.getNodeAttributes(e.target);
1334
+ if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
1335
+ }
1336
+ return ids;
1337
+ }
1338
+ function failingCallDominates(e, id, curEdge, curId) {
1339
+ const ec = e.signal?.errorCount ?? 0;
1340
+ const cc = curEdge.signal?.errorCount ?? 0;
1341
+ if (ec !== cc) return ec > cc;
1342
+ if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
1343
+ return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
1344
+ }
1345
+ return id < curId;
1346
+ }
1347
+ function dominantFailingCall(graph, serviceId3, visited) {
1348
+ let best = null;
1349
+ for (const src of callSourcesForService(graph, serviceId3)) {
1350
+ for (const edgeId of graph.outboundEdges(src)) {
1351
+ const e = graph.getEdgeAttributes(edgeId);
1352
+ if (!isFailingCallEdge(e)) continue;
1353
+ if (isFrontierNode(graph, e.target)) continue;
1354
+ const owner = resolveOwningService(graph, e.target);
1355
+ if (!owner || visited.has(owner.id)) continue;
1356
+ if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
1357
+ best = { nextService: owner.id, edge: e };
1358
+ }
1359
+ }
1360
+ }
1361
+ return best;
1362
+ }
1363
+ function followFailingCallChain(graph, originServiceId, maxDepth) {
1364
+ const path45 = [originServiceId];
1365
+ const edges = [];
1366
+ const visited = /* @__PURE__ */ new Set([originServiceId]);
1367
+ let current = originServiceId;
1368
+ for (let depth = 0; depth < maxDepth; depth++) {
1369
+ const hop = dominantFailingCall(graph, current, visited);
1370
+ if (!hop) break;
1371
+ path45.push(hop.nextService);
1372
+ edges.push(hop.edge);
1373
+ visited.add(hop.nextService);
1374
+ current = hop.nextService;
1375
+ }
1376
+ if (edges.length === 0) return null;
1377
+ return { path: path45, edges, culprit: current };
1378
+ }
1379
+ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1380
+ const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1381
+ if (!chain) return null;
1382
+ const culprit = chain.culprit;
1383
+ const path45 = [...chain.path];
1384
+ const edgeProvenances = chain.edges.map((e) => e.provenance);
1385
+ const baseConfidence = confidenceFromMix(chain.edges);
1386
+ const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
1387
+ const loc = localizeFromIncidents(culprit, incidents, errorEvent);
1388
+ if (loc) {
1389
+ let rootCauseNode = culprit;
1390
+ if (loc.fileNode) {
1391
+ path45.push(loc.fileNode);
1392
+ edgeProvenances.push(import_types.Provenance.OBSERVED);
1393
+ rootCauseNode = loc.fileNode;
1394
+ }
1395
+ return import_types.RootCauseResultSchema.parse({
1396
+ rootCauseNode,
1397
+ rootCauseReason: loc.rootCauseReason,
1398
+ traversalPath: path45,
1399
+ edgeProvenances,
1400
+ confidence,
1401
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1402
+ });
1403
+ }
1404
+ const lastEdge = chain.edges[chain.edges.length - 1];
1405
+ const errs = lastEdge.signal?.errorCount ?? 0;
1406
+ const culpritName = culprit.replace(/^service:/, "");
1407
+ return import_types.RootCauseResultSchema.parse({
1408
+ rootCauseNode: culprit,
1409
+ rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1410
+ traversalPath: path45,
1411
+ edgeProvenances,
1412
+ confidence,
1413
+ fixRecommendation: `Inspect ${culpritName}'s failing handler`
1248
1414
  });
1249
1415
  }
1250
1416
  function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
@@ -1267,14 +1433,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
1267
1433
  });
1268
1434
  }
1269
1435
  if (frame.distance >= maxDepth) continue;
1270
- const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
1271
- for (const [tgtId, edge] of outgoing) {
1272
- if (enqueued.has(tgtId)) continue;
1273
- enqueued.add(tgtId);
1436
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
1437
+ for (const [srcId, edge] of incoming) {
1438
+ if (enqueued.has(srcId)) continue;
1439
+ enqueued.add(srcId);
1274
1440
  queue.push({
1275
- nodeId: tgtId,
1441
+ nodeId: srcId,
1276
1442
  distance: frame.distance + 1,
1277
- path: [...frame.path, tgtId],
1443
+ path: [...frame.path, srcId],
1278
1444
  pathEdges: [...frame.pathEdges, edge]
1279
1445
  });
1280
1446
  }
@@ -1330,6 +1496,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
1330
1496
  total: dependencies.length
1331
1497
  });
1332
1498
  }
1499
+ function getObservedDependencies(graph, nodeId) {
1500
+ if (!graph.hasNode(nodeId)) {
1501
+ return import_types.ObservedDependenciesResultSchema.parse({
1502
+ origin: nodeId,
1503
+ dependencies: [],
1504
+ observed: false,
1505
+ inboundObservedCount: 0,
1506
+ hasExtractedOutbound: false
1507
+ });
1508
+ }
1509
+ const attrs = graph.getNodeAttributes(nodeId);
1510
+ const scope = [nodeId];
1511
+ if (attrs.type === import_types.NodeType.ServiceNode) {
1512
+ for (const edgeId of graph.outboundEdges(nodeId)) {
1513
+ const e = graph.getEdgeAttributes(edgeId);
1514
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1515
+ const owned = graph.getNodeAttributes(e.target);
1516
+ if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
1517
+ }
1518
+ }
1519
+ const dependencies = [];
1520
+ const seenEdge = /* @__PURE__ */ new Set();
1521
+ let hasExtractedOutbound = false;
1522
+ for (const src of scope) {
1523
+ for (const edgeId of graph.outboundEdges(src)) {
1524
+ const e = graph.getEdgeAttributes(edgeId);
1525
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1526
+ if (e.provenance === import_types.Provenance.OBSERVED) {
1527
+ if (!seenEdge.has(e.id)) {
1528
+ seenEdge.add(e.id);
1529
+ dependencies.push(e);
1530
+ }
1531
+ } else if (e.provenance === import_types.Provenance.EXTRACTED) {
1532
+ hasExtractedOutbound = true;
1533
+ }
1534
+ }
1535
+ }
1536
+ let inboundObservedCount = 0;
1537
+ for (const tgt of scope) {
1538
+ for (const edgeId of graph.inboundEdges(tgt)) {
1539
+ const e = graph.getEdgeAttributes(edgeId);
1540
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1541
+ if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
1542
+ }
1543
+ }
1544
+ dependencies.sort(
1545
+ (a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
1546
+ );
1547
+ return import_types.ObservedDependenciesResultSchema.parse({
1548
+ origin: nodeId,
1549
+ dependencies,
1550
+ observed: dependencies.length > 0 || inboundObservedCount > 0,
1551
+ inboundObservedCount,
1552
+ hasExtractedOutbound
1553
+ });
1554
+ }
1333
1555
 
1334
1556
  // src/policy.ts
1335
1557
  var DEFAULT_ACTION_BY_SEVERITY = {
@@ -1599,6 +1821,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
1599
1821
  }
1600
1822
  return out;
1601
1823
  }
1824
+ function selectApplicablePolicies(graph, policies, nodeId) {
1825
+ if (!graph.hasNode(nodeId)) return [];
1826
+ const node = graph.getNodeAttributes(nodeId);
1827
+ const out = [];
1828
+ for (const policy of policies) {
1829
+ const m = matchPolicyToNode(graph, policy, nodeId, node);
1830
+ if (!m) continue;
1831
+ out.push({
1832
+ policyId: policy.id,
1833
+ policyName: policy.name,
1834
+ ...policy.description !== void 0 ? { description: policy.description } : {},
1835
+ severity: policy.severity,
1836
+ onViolation: resolveOnViolation(policy),
1837
+ ruleType: policy.rule.type,
1838
+ match: m.match,
1839
+ reason: m.reason
1840
+ });
1841
+ }
1842
+ return out;
1843
+ }
1844
+ function requiredProvenanceList(required) {
1845
+ if (Array.isArray(required)) return required.join(" | ");
1846
+ return String(required);
1847
+ }
1848
+ function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
1849
+ const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
1850
+ for (const edgeId of incident) {
1851
+ const e = graph.getEdgeAttributes(edgeId);
1852
+ if (e.type !== edgeType) continue;
1853
+ if (requiredOtherEnd === void 0) return true;
1854
+ if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
1855
+ }
1856
+ return false;
1857
+ }
1858
+ function blastRadiusSubjectReaching(graph, rule, nodeId) {
1859
+ let found = null;
1860
+ graph.forEachNode((subjId, attrs) => {
1861
+ if (found !== null) return;
1862
+ if (subjId === nodeId) return;
1863
+ if (attrs.type !== rule.nodeType) return;
1864
+ const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
1865
+ if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
1866
+ });
1867
+ return found;
1868
+ }
1869
+ function matchPolicyToNode(graph, policy, nodeId, node) {
1870
+ const rule = policy.rule;
1871
+ switch (rule.type) {
1872
+ case "structural": {
1873
+ if (node.type === rule.fromNodeType) {
1874
+ return {
1875
+ match: "subject",
1876
+ reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
1877
+ };
1878
+ }
1879
+ if (node.type === rule.toNodeType) {
1880
+ return {
1881
+ match: "region",
1882
+ reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
1883
+ };
1884
+ }
1885
+ return null;
1886
+ }
1887
+ case "ownership": {
1888
+ if (node.type === rule.nodeType) {
1889
+ return {
1890
+ match: "subject",
1891
+ reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
1892
+ };
1893
+ }
1894
+ return null;
1895
+ }
1896
+ case "blast-radius": {
1897
+ const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
1898
+ if (node.type === rule.nodeType) {
1899
+ return {
1900
+ match: "subject",
1901
+ reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
1902
+ };
1903
+ }
1904
+ const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
1905
+ if (subject) {
1906
+ return {
1907
+ match: "region",
1908
+ 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`
1909
+ };
1910
+ }
1911
+ return null;
1912
+ }
1913
+ case "compatibility": {
1914
+ const kindLabel = rule.kind ?? "all compat shapes";
1915
+ if (node.type === import_types2.NodeType.ServiceNode) {
1916
+ return {
1917
+ match: "subject",
1918
+ reason: `this service's dependencies are compatibility-checked (${kindLabel})`
1919
+ };
1920
+ }
1921
+ const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
1922
+ if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
1923
+ return {
1924
+ match: "region",
1925
+ reason: "services connecting to this database have their driver/engine compatibility checked against it"
1926
+ };
1927
+ }
1928
+ return null;
1929
+ }
1930
+ case "provenance": {
1931
+ const requiredList = requiredProvenanceList(rule.required);
1932
+ if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
1933
+ return {
1934
+ match: "subject",
1935
+ reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
1936
+ };
1937
+ }
1938
+ if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
1939
+ return {
1940
+ match: "region",
1941
+ 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`
1942
+ };
1943
+ }
1944
+ return null;
1945
+ }
1946
+ }
1947
+ }
1602
1948
  async function loadPolicyFile(policyPath) {
1603
1949
  let raw;
1604
1950
  try {
@@ -1685,9 +2031,9 @@ function loadIncidentThresholdsFromEnv() {
1685
2031
  return DEFAULT_INCIDENT_THRESHOLDS;
1686
2032
  }
1687
2033
  }
1688
- function httpResponseStatus(span) {
2034
+ function httpResponseStatusFromAttrs(attrs) {
1689
2035
  for (const key of ["http.response.status_code", "http.status_code"]) {
1690
- const v = span.attributes[key];
2036
+ const v = attrs[key];
1691
2037
  if (typeof v === "number" && Number.isFinite(v)) return v;
1692
2038
  if (typeof v === "string") {
1693
2039
  const n = Number(v);
@@ -1696,6 +2042,63 @@ function httpResponseStatus(span) {
1696
2042
  }
1697
2043
  return void 0;
1698
2044
  }
2045
+ function httpResponseStatus(span) {
2046
+ return httpResponseStatusFromAttrs(span.attributes);
2047
+ }
2048
+ function httpFailureMessageFromAttrs(attrs) {
2049
+ const status2 = httpResponseStatusFromAttrs(attrs);
2050
+ const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
2051
+ const method = pickAttrFrom(attrs, "http.request.method", "http.method");
2052
+ const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
2053
+ if (status2 !== void 0 && where) return `${status2} on ${where}`;
2054
+ if (status2 !== void 0) return `HTTP ${status2}`;
2055
+ if (where) return `error on ${where}`;
2056
+ return void 0;
2057
+ }
2058
+ var GRPC_STATUS_NAMES = {
2059
+ 1: "CANCELLED",
2060
+ 2: "UNKNOWN",
2061
+ 3: "INVALID_ARGUMENT",
2062
+ 4: "DEADLINE_EXCEEDED",
2063
+ 5: "NOT_FOUND",
2064
+ 6: "ALREADY_EXISTS",
2065
+ 7: "PERMISSION_DENIED",
2066
+ 8: "RESOURCE_EXHAUSTED",
2067
+ 9: "FAILED_PRECONDITION",
2068
+ 10: "ABORTED",
2069
+ 11: "OUT_OF_RANGE",
2070
+ 12: "UNIMPLEMENTED",
2071
+ 13: "INTERNAL",
2072
+ 14: "UNAVAILABLE",
2073
+ 15: "DATA_LOSS",
2074
+ 16: "UNAUTHENTICATED"
2075
+ };
2076
+ function grpcStatusCodeFromAttrs(attrs) {
2077
+ const v = attrs["rpc.grpc.status_code"];
2078
+ if (typeof v === "number" && Number.isFinite(v)) return v;
2079
+ if (typeof v === "string") {
2080
+ const n = Number(v);
2081
+ if (Number.isFinite(n)) return n;
2082
+ }
2083
+ return void 0;
2084
+ }
2085
+ function nonHttpFailureMessageFromAttrs(attrs) {
2086
+ const grpc2 = grpcStatusCodeFromAttrs(attrs);
2087
+ if (grpc2 !== void 0 && grpc2 !== 0) {
2088
+ const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
2089
+ const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
2090
+ return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
2091
+ }
2092
+ const errType = pickAttrFrom(attrs, "error.type");
2093
+ if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
2094
+ const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
2095
+ return peer ? `${errType} connecting to ${peer}` : errType;
2096
+ }
2097
+ return void 0;
2098
+ }
2099
+ function incidentMessage(span) {
2100
+ return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
2101
+ }
1699
2102
  function nowIso(ctx) {
1700
2103
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1701
2104
  }
@@ -1715,13 +2118,16 @@ function warnNoSourceMaps(serviceName) {
1715
2118
  `[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.`
1716
2119
  );
1717
2120
  }
1718
- function pickAttr(span, ...keys) {
2121
+ function pickAttrFrom(attrs, ...keys) {
1719
2122
  for (const k of keys) {
1720
- const v = span.attributes[k];
2123
+ const v = attrs[k];
1721
2124
  if (typeof v === "string" && v.length > 0) return v;
1722
2125
  }
1723
2126
  return void 0;
1724
2127
  }
2128
+ function pickAttr(span, ...keys) {
2129
+ return pickAttrFrom(span.attributes, ...keys);
2130
+ }
1725
2131
  function hostFromUrl(u) {
1726
2132
  if (!u) return void 0;
1727
2133
  try {
@@ -1733,6 +2139,10 @@ function hostFromUrl(u) {
1733
2139
  function pickAddress(span) {
1734
2140
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1735
2141
  }
2142
+ function isLoopbackHost(host) {
2143
+ const h = host.toLowerCase();
2144
+ return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
2145
+ }
1736
2146
  var CODE_FILEPATH_ATTR = "code.filepath";
1737
2147
  var CODE_LINENO_ATTR = "code.lineno";
1738
2148
  var CODE_FUNCTION_ATTR = "code.function";
@@ -1840,15 +2250,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
1840
2250
  ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1841
2251
  };
1842
2252
  }
2253
+ function reconcileObservedRelPath(graph, serviceName, relPath) {
2254
+ if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
2255
+ let best = null;
2256
+ graph.forEachNode((_id, attrs) => {
2257
+ const a = attrs;
2258
+ if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
2259
+ if (a.discoveredVia === "otel") return;
2260
+ const p = a.path;
2261
+ if (!p) return;
2262
+ if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
2263
+ best = p;
2264
+ }
2265
+ });
2266
+ return best ?? relPath;
2267
+ }
1843
2268
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1844
- const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
2269
+ const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
2270
+ const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
1845
2271
  if (!graph.hasNode(fileNodeId)) {
1846
- const language = languageForExt(callSite.relPath);
2272
+ const language = languageForExt(relPath);
1847
2273
  const node = {
1848
2274
  id: fileNodeId,
1849
2275
  type: import_types3.NodeType.FileNode,
1850
2276
  service: serviceName,
1851
- path: callSite.relPath,
2277
+ path: relPath,
1852
2278
  ...language ? { language } : {},
1853
2279
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1854
2280
  discoveredVia: "otel"
@@ -1876,6 +2302,11 @@ function makeInferredEdgeId(type, source, target) {
1876
2302
  }
1877
2303
  var INFERRED_CONFIDENCE = 0.6;
1878
2304
  var STITCH_MAX_DEPTH = 2;
2305
+ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2306
+ import_types3.EdgeType.CALLS,
2307
+ import_types3.EdgeType.CONNECTS_TO,
2308
+ import_types3.EdgeType.DEPENDS_ON
2309
+ ]);
1879
2310
  var WIRE_SPAN_KIND_CLIENT = 3;
1880
2311
  var WIRE_SPAN_KIND_PRODUCER = 4;
1881
2312
  function spanMintsObservedEdge(kind) {
@@ -2049,6 +2480,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
2049
2480
  for (const edgeId of outbound) {
2050
2481
  const edge = graph.getEdgeAttributes(edgeId);
2051
2482
  if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
2483
+ if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
2052
2484
  if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
2053
2485
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
2054
2486
  if (!visited.has(edge.target)) {
@@ -2081,6 +2513,16 @@ async function appendErrorEvent(ctx, ev) {
2081
2513
  await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
2082
2514
  await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
2083
2515
  }
2516
+ function incidentAffectedNode(span, graph, scanPath) {
2517
+ const sid = (0, import_types3.serviceId)(span.service, span.env);
2518
+ const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
2519
+ const callSite = callSiteFromSpan(span, serviceNode, scanPath);
2520
+ if (callSite) {
2521
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
2522
+ return (0, import_types3.fileId)(span.service, relPath);
2523
+ }
2524
+ return sid;
2525
+ }
2084
2526
  function sanitizeAttributes(attrs) {
2085
2527
  const out = {};
2086
2528
  for (const [k, v] of Object.entries(attrs)) {
@@ -2089,7 +2531,7 @@ function sanitizeAttributes(attrs) {
2089
2531
  }
2090
2532
  return out;
2091
2533
  }
2092
- function buildErrorEventForReceiver(span) {
2534
+ function buildErrorEventForReceiver(span, graph, scanPath) {
2093
2535
  if (span.statusCode !== 2) return null;
2094
2536
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
2095
2537
  const attrs = sanitizeAttributes(span.attributes);
@@ -2099,16 +2541,16 @@ function buildErrorEventForReceiver(span) {
2099
2541
  service: span.service,
2100
2542
  traceId: span.traceId,
2101
2543
  spanId: span.spanId,
2102
- errorMessage: span.exception?.message ?? "unknown error",
2544
+ errorMessage: incidentMessage(span),
2103
2545
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2104
2546
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2105
2547
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2106
- affectedNode: (0, import_types3.serviceId)(span.service, span.env)
2548
+ affectedNode: incidentAffectedNode(span, graph, scanPath)
2107
2549
  };
2108
2550
  }
2109
- function makeErrorSpanWriter(errorsPath) {
2551
+ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
2110
2552
  return async (span) => {
2111
- const ev = buildErrorEventForReceiver(span);
2553
+ const ev = buildErrorEventForReceiver(span, graph, scanPath);
2112
2554
  if (!ev) return;
2113
2555
  await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(errorsPath), { recursive: true });
2114
2556
  await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
@@ -2192,7 +2634,10 @@ async function handleSpan(ctx, span) {
2192
2634
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2193
2635
  cacheSpanService(span, nowMs, callSite);
2194
2636
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2195
- const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2637
+ const callSiteEvidence = callSite ? {
2638
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2639
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2640
+ } : void 0;
2196
2641
  let affectedNode = sourceId;
2197
2642
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2198
2643
  if (span.dbSystem) {
@@ -2214,7 +2659,7 @@ async function handleSpan(ctx, span) {
2214
2659
  } else {
2215
2660
  const host = pickAddress(span);
2216
2661
  let resolvedViaAddress = false;
2217
- if (mintsFromCallerSide && host && host !== span.service) {
2662
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
2218
2663
  const targetId = resolveServiceId(ctx.graph, host, env);
2219
2664
  if (targetId && targetId !== sourceId) {
2220
2665
  upsertObservedEdge(
@@ -2249,7 +2694,11 @@ async function handleSpan(ctx, span) {
2249
2694
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2250
2695
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2251
2696
  const fallbackEvidence = parent.callSite ? {
2252
- file: parent.callSite.relPath,
2697
+ file: reconcileObservedRelPath(
2698
+ ctx.graph,
2699
+ parent.service,
2700
+ parent.callSite.relPath
2701
+ ),
2253
2702
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2254
2703
  } : void 0;
2255
2704
  upsertObservedEdge(
@@ -2274,7 +2723,7 @@ async function handleSpan(ctx, span) {
2274
2723
  service: span.service,
2275
2724
  traceId: span.traceId,
2276
2725
  spanId: span.spanId,
2277
- errorMessage: span.exception?.message ?? "unknown error",
2726
+ errorMessage: incidentMessage(span),
2278
2727
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2279
2728
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2280
2729
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -2377,12 +2826,43 @@ async function readStaleEvents(staleEventsPath) {
2377
2826
  async function readErrorEvents(errorsPath) {
2378
2827
  try {
2379
2828
  const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
2380
- return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2829
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2830
+ return dedupeIncidents(events);
2381
2831
  } catch (err) {
2382
2832
  if (err.code === "ENOENT") return [];
2383
2833
  throw err;
2384
2834
  }
2385
2835
  }
2836
+ function isSynthesizedHttpIncident(ev) {
2837
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2838
+ if (ev.errorType) return false;
2839
+ if (!ev.attributes) return false;
2840
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2841
+ return synth !== void 0 && synth === ev.errorMessage;
2842
+ }
2843
+ function dedupeIncidents(events) {
2844
+ const seen = /* @__PURE__ */ new Set();
2845
+ const once = [];
2846
+ for (const ev of events) {
2847
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2848
+ if (key === void 0) {
2849
+ once.push(ev);
2850
+ continue;
2851
+ }
2852
+ if (seen.has(key)) continue;
2853
+ seen.add(key);
2854
+ once.push(ev);
2855
+ }
2856
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2857
+ const hasRealFailure = /* @__PURE__ */ new Set();
2858
+ for (const ev of once) {
2859
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2860
+ }
2861
+ return once.filter((ev) => {
2862
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
2863
+ return !hasRealFailure.has(groupKey(ev));
2864
+ });
2865
+ }
2386
2866
  function mergeSnapshot(graph, snapshot) {
2387
2867
  const exported = snapshot.graph;
2388
2868
  let nodesAdded = 0;
@@ -2463,10 +2943,17 @@ function isConfigFile(name) {
2463
2943
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2464
2944
  if (name === ".env" || name.startsWith(".env.")) {
2465
2945
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2946
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2466
2947
  return { match: true, fileType: "env" };
2467
2948
  }
2468
2949
  return { match: false, fileType: "" };
2469
2950
  }
2951
+ function isNeatAuthoredEnvFile(name) {
2952
+ return name === ".env.neat";
2953
+ }
2954
+ function isNeatAuthoredSourceFile(name) {
2955
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
2956
+ }
2470
2957
  function isTestPath(filePath) {
2471
2958
  const normalised = filePath.replace(/\\/g, "/");
2472
2959
  const segments = normalised.split("/");
@@ -3160,7 +3647,9 @@ async function walkSourceFiles(dir) {
3160
3647
  if (IGNORED_DIRS.has(entry2.name)) continue;
3161
3648
  if (await isPythonVenvDir(full)) continue;
3162
3649
  await walk(full);
3163
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name))) {
3650
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3651
+ // would attribute our instrumentation imports to the user's service.
3652
+ !isNeatAuthoredSourceFile(entry2.name)) {
3164
3653
  out.push(full);
3165
3654
  }
3166
3655
  }
@@ -3330,11 +3819,28 @@ function collectJsImports(node, out) {
3330
3819
  if (child) collectJsImports(child, out);
3331
3820
  }
3332
3821
  }
3822
+ function collectImportedNames(node, out) {
3823
+ if (node.type === "aliased_import") {
3824
+ const nameNode = node.childForFieldName("name");
3825
+ if (nameNode) out.push(nameNode.text);
3826
+ return;
3827
+ }
3828
+ if (node.type === "dotted_name") {
3829
+ out.push(node.text);
3830
+ return;
3831
+ }
3832
+ for (let i = 0; i < node.namedChildCount; i++) {
3833
+ const child = node.namedChild(i);
3834
+ if (child) collectImportedNames(child, out);
3835
+ }
3836
+ }
3333
3837
  function collectPyImports(node, out) {
3334
3838
  if (node.type === "import_from_statement") {
3335
3839
  let level = 0;
3336
3840
  let modulePath = "";
3841
+ const names = [];
3337
3842
  let pastFrom = false;
3843
+ let pastImport = false;
3338
3844
  for (let i = 0; i < node.childCount; i++) {
3339
3845
  const child = node.child(i);
3340
3846
  if (!child) continue;
@@ -3342,26 +3848,30 @@ function collectPyImports(node, out) {
3342
3848
  if (child.type === "from") pastFrom = true;
3343
3849
  continue;
3344
3850
  }
3345
- if (child.type === "import") break;
3346
- if (child.type === "relative_import") {
3347
- for (let j = 0; j < child.childCount; j++) {
3348
- const rc = child.child(j);
3349
- if (!rc) continue;
3350
- if (rc.type === "import_prefix") {
3351
- for (let k = 0; k < rc.childCount; k++) {
3352
- if (rc.child(k)?.type === ".") level++;
3353
- }
3354
- } else if (rc.type === "dotted_name") modulePath = rc.text;
3851
+ if (!pastImport) {
3852
+ if (child.type === "import") {
3853
+ pastImport = true;
3854
+ continue;
3355
3855
  }
3356
- break;
3357
- }
3358
- if (child.type === "dotted_name") {
3359
- modulePath = child.text;
3360
- break;
3856
+ if (child.type === "relative_import") {
3857
+ for (let j = 0; j < child.childCount; j++) {
3858
+ const rc = child.child(j);
3859
+ if (!rc) continue;
3860
+ if (rc.type === "import_prefix") {
3861
+ for (let k = 0; k < rc.childCount; k++) {
3862
+ if (rc.child(k)?.type === ".") level++;
3863
+ }
3864
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3865
+ }
3866
+ } else if (child.type === "dotted_name") {
3867
+ modulePath = child.text;
3868
+ }
3869
+ continue;
3361
3870
  }
3871
+ collectImportedNames(child, names);
3362
3872
  }
3363
3873
  if (level > 0 || modulePath) {
3364
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3874
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3365
3875
  }
3366
3876
  }
3367
3877
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3463,8 +3973,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3463
3973
  return null;
3464
3974
  }
3465
3975
  async function resolvePyImport(imp, importerPath, serviceDir) {
3466
- if (!imp.modulePath) return null;
3467
- const relPath = imp.modulePath.split(".").join("/");
3468
3976
  let baseDir;
3469
3977
  if (imp.level > 0) {
3470
3978
  baseDir = import_node_path12.default.dirname(importerPath);
@@ -3472,13 +3980,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
3472
3980
  } else {
3473
3981
  baseDir = serviceDir;
3474
3982
  }
3475
- const candidates = [import_node_path12.default.join(baseDir, `${relPath}.py`), import_node_path12.default.join(baseDir, relPath, "__init__.py")];
3476
- for (const candidate of candidates) {
3477
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3478
- return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
3983
+ const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
3984
+ const resolved = /* @__PURE__ */ new Set();
3985
+ let needModuleFile = imp.names.length === 0;
3986
+ for (const name of imp.names) {
3987
+ const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
3988
+ const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
3989
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
3990
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
3991
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
3992
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
3993
+ } else {
3994
+ needModuleFile = true;
3995
+ }
3996
+ }
3997
+ if (needModuleFile) {
3998
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
3999
+ for (const candidate of moduleFileCandidates) {
4000
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
4001
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
4002
+ break;
4003
+ }
3479
4004
  }
3480
4005
  }
3481
- return null;
4006
+ return [...resolved];
3482
4007
  }
3483
4008
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3484
4009
  const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
@@ -3519,17 +4044,18 @@ async function addImports(graph, services) {
3519
4044
  continue;
3520
4045
  }
3521
4046
  for (const imp of pyImports) {
3522
- const resolved = await resolvePyImport(imp, file.path, service.dir);
3523
- if (!resolved) continue;
3524
- edgesAdded += emitImportEdge(
3525
- graph,
3526
- service.pkg.name,
3527
- importerFileId,
3528
- relFile,
3529
- resolved,
3530
- imp.line,
3531
- imp.snippet
3532
- );
4047
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
4048
+ for (const resolved of resolvedPaths) {
4049
+ edgesAdded += emitImportEdge(
4050
+ graph,
4051
+ service.pkg.name,
4052
+ importerFileId,
4053
+ relFile,
4054
+ resolved,
4055
+ imp.line,
4056
+ imp.snippet
4057
+ );
4058
+ }
3533
4059
  }
3534
4060
  continue;
3535
4061
  }
@@ -4165,6 +4691,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4165
4691
  edgesAdded++;
4166
4692
  }
4167
4693
  }
4694
+ if (allConfigs.length === 1) {
4695
+ const primary = allConfigs[0];
4696
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
4697
+ const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
4698
+ const cfgId = (0, import_types9.configId)(relPath);
4699
+ if (!graph.hasNode(cfgId)) {
4700
+ const cfgNode = {
4701
+ id: cfgId,
4702
+ type: import_types9.NodeType.ConfigNode,
4703
+ name: import_node_path20.default.basename(primary.sourceFile),
4704
+ path: relPath,
4705
+ fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
4706
+ };
4707
+ graph.addNode(cfgId, cfgNode);
4708
+ nodesAdded++;
4709
+ }
4710
+ const cfgEdge = {
4711
+ id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
4712
+ source: service.node.id,
4713
+ target: cfgId,
4714
+ type: import_types9.EdgeType.CONFIGURED_BY,
4715
+ provenance: import_types9.Provenance.EXTRACTED,
4716
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
4717
+ evidence: { file: toPosix2(relPath) }
4718
+ };
4719
+ if (!graph.hasEdge(cfgEdge.id)) {
4720
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
4721
+ edgesAdded++;
4722
+ }
4723
+ }
4168
4724
  attachIncompatibilities(service, allConfigs);
4169
4725
  if (graph.hasNode(service.node.id)) {
4170
4726
  const current = graph.getNodeAttributes(service.node.id);
@@ -4176,6 +4732,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4176
4732
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
4177
4733
  delete updated.incompatibilities;
4178
4734
  }
4735
+ if (!service.node.dbConnectionTarget) {
4736
+ delete updated.dbConnectionTarget;
4737
+ }
4179
4738
  graph.replaceNodeAttributes(service.node.id, updated);
4180
4739
  }
4181
4740
  }
@@ -4353,7 +4912,7 @@ async function addHttpCallEdges(graph, services) {
4353
4912
  const dedupKey = `${relFile}|${targetId}`;
4354
4913
  if (seen.has(dedupKey)) continue;
4355
4914
  seen.add(dedupKey);
4356
- const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
4915
+ const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
4357
4916
  const ev = {
4358
4917
  file: relFile,
4359
4918
  line: site.line,
@@ -4373,7 +4932,7 @@ async function addHttpCallEdges(graph, services) {
4373
4932
  target: targetId,
4374
4933
  type: import_types11.EdgeType.CALLS,
4375
4934
  confidence,
4376
- confidenceKind: "hostname-shape-match",
4935
+ confidenceKind: "url-literal-service-target",
4377
4936
  evidence: ev
4378
4937
  });
4379
4938
  continue;
@@ -4879,19 +5438,29 @@ init_cjs_shims();
4879
5438
  var import_node_path29 = __toESM(require("path"), 1);
4880
5439
  var import_node_fs15 = require("fs");
4881
5440
  var import_types20 = require("@neat.is/types");
4882
- function runtimeImage(content) {
4883
- const lines = content.split("\n");
4884
- let last = null;
4885
- for (const raw of lines) {
5441
+ function readDockerfile(content) {
5442
+ let image = null;
5443
+ const ports = [];
5444
+ let cmd = null;
5445
+ let entrypoint = null;
5446
+ for (const raw of content.split("\n")) {
4886
5447
  const line = raw.trim();
4887
5448
  if (!line || line.startsWith("#")) continue;
4888
- if (!/^from\s+/i.test(line)) continue;
4889
- const tokens = line.split(/\s+/);
4890
- const image = tokens[1];
4891
- if (!image || image.toLowerCase() === "scratch") continue;
4892
- last = image;
5449
+ if (/^from\s+/i.test(line)) {
5450
+ const candidate = line.split(/\s+/)[1];
5451
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
5452
+ } else if (/^expose\s+/i.test(line)) {
5453
+ for (const token of line.split(/\s+/).slice(1)) {
5454
+ const port = Number.parseInt(token.split("/")[0], 10);
5455
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
5456
+ }
5457
+ } else if (/^entrypoint\s+/i.test(line)) {
5458
+ entrypoint = line;
5459
+ } else if (/^cmd\s+/i.test(line)) {
5460
+ cmd = line;
5461
+ }
4893
5462
  }
4894
- return last;
5463
+ return { image, ports, entrypoint: entrypoint ?? cmd };
4895
5464
  }
4896
5465
  async function addDockerfileRuntimes(graph, services, scanPath) {
4897
5466
  let nodesAdded = 0;
@@ -4910,14 +5479,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4910
5479
  );
4911
5480
  continue;
4912
5481
  }
4913
- const image = runtimeImage(content);
4914
- if (!image) continue;
4915
- const node = makeInfraNode("container-image", image);
5482
+ const facts = readDockerfile(content);
5483
+ if (!facts.image) continue;
5484
+ const node = makeInfraNode("container-image", facts.image);
4916
5485
  if (!graph.hasNode(node.id)) {
4917
5486
  graph.addNode(node.id, node);
4918
5487
  nodesAdded++;
4919
5488
  }
4920
5489
  const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5490
+ const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
4921
5491
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4922
5492
  graph,
4923
5493
  service.pkg.name,
@@ -4936,12 +5506,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4936
5506
  provenance: import_types20.Provenance.EXTRACTED,
4937
5507
  confidence: (0, import_types20.confidenceForExtracted)("structural"),
4938
5508
  evidence: {
4939
- file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
5509
+ file: evidenceFile,
5510
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
4940
5511
  }
4941
5512
  };
4942
5513
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
4943
5514
  edgesAdded++;
4944
5515
  }
5516
+ for (const port of facts.ports) {
5517
+ const portNode = makeInfraNode("port", String(port));
5518
+ if (!graph.hasNode(portNode.id)) {
5519
+ graph.addNode(portNode.id, portNode);
5520
+ nodesAdded++;
5521
+ }
5522
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
5523
+ if (graph.hasEdge(portEdgeId)) continue;
5524
+ const portEdge = {
5525
+ id: portEdgeId,
5526
+ source: fileNodeId,
5527
+ target: portNode.id,
5528
+ type: import_types20.EdgeType.CONNECTS_TO,
5529
+ provenance: import_types20.Provenance.EXTRACTED,
5530
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
5531
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5532
+ };
5533
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
5534
+ edgesAdded++;
5535
+ }
4945
5536
  }
4946
5537
  return { nodesAdded, edgesAdded };
4947
5538
  }
@@ -4950,7 +5541,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4950
5541
  init_cjs_shims();
4951
5542
  var import_node_fs16 = require("fs");
4952
5543
  var import_node_path30 = __toESM(require("path"), 1);
5544
+ var import_types21 = require("@neat.is/types");
4953
5545
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5546
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
4954
5547
  async function walkTfFiles(start, depth = 0, max = 5) {
4955
5548
  if (depth > max) return [];
4956
5549
  const out = [];
@@ -4967,24 +5560,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4967
5560
  }
4968
5561
  return out;
4969
5562
  }
5563
+ function blockBody(content, from) {
5564
+ const open = content.indexOf("{", from);
5565
+ if (open === -1) return null;
5566
+ let depth = 0;
5567
+ for (let i = open; i < content.length; i++) {
5568
+ const ch = content[i];
5569
+ if (ch === "{") depth++;
5570
+ else if (ch === "}") {
5571
+ depth--;
5572
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
5573
+ }
5574
+ }
5575
+ return null;
5576
+ }
5577
+ function lineAt(content, index) {
5578
+ let line = 1;
5579
+ for (let i = 0; i < index && i < content.length; i++) {
5580
+ if (content[i] === "\n") line++;
5581
+ }
5582
+ return line;
5583
+ }
4970
5584
  async function addTerraformResources(graph, scanPath) {
4971
5585
  let nodesAdded = 0;
5586
+ let edgesAdded = 0;
4972
5587
  const files = await walkTfFiles(scanPath);
4973
5588
  for (const file of files) {
4974
5589
  const content = await import_node_fs16.promises.readFile(file, "utf8");
5590
+ const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
5591
+ const resources = [];
5592
+ const byKey = /* @__PURE__ */ new Map();
4975
5593
  RESOURCE_RE.lastIndex = 0;
4976
5594
  let m;
4977
5595
  while ((m = RESOURCE_RE.exec(content)) !== null) {
4978
- const kind = m[1];
5596
+ const type = m[1];
4979
5597
  const name = m[2];
4980
- const node = makeInfraNode(kind, name, "aws");
5598
+ const node = makeInfraNode(type, name, "aws");
4981
5599
  if (!graph.hasNode(node.id)) {
4982
5600
  graph.addNode(node.id, node);
4983
5601
  nodesAdded++;
4984
5602
  }
5603
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
5604
+ const resource = {
5605
+ type,
5606
+ name,
5607
+ nodeId: node.id,
5608
+ body: span?.body ?? "",
5609
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
5610
+ };
5611
+ resources.push(resource);
5612
+ byKey.set(`${type}.${name}`, resource);
5613
+ }
5614
+ for (const resource of resources) {
5615
+ const seen = /* @__PURE__ */ new Set();
5616
+ REFERENCE_RE.lastIndex = 0;
5617
+ let ref;
5618
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
5619
+ const key = `${ref[1]}.${ref[2]}`;
5620
+ if (key === `${resource.type}.${resource.name}`) continue;
5621
+ const target = byKey.get(key);
5622
+ if (!target) continue;
5623
+ if (seen.has(target.nodeId)) continue;
5624
+ seen.add(target.nodeId);
5625
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
5626
+ if (graph.hasEdge(edgeId)) continue;
5627
+ const line = lineAt(content, resource.bodyOffset + ref.index);
5628
+ const edge = {
5629
+ id: edgeId,
5630
+ source: resource.nodeId,
5631
+ target: target.nodeId,
5632
+ type: import_types21.EdgeType.DEPENDS_ON,
5633
+ provenance: import_types21.Provenance.EXTRACTED,
5634
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
5635
+ evidence: { file: evidenceFile, line, snippet: key }
5636
+ };
5637
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5638
+ edgesAdded++;
5639
+ }
4985
5640
  }
4986
5641
  }
4987
- return { nodesAdded, edgesAdded: 0 };
5642
+ return { nodesAdded, edgesAdded };
4988
5643
  }
4989
5644
 
4990
5645
  // src/extract/infra/k8s.ts
@@ -5062,11 +5717,11 @@ var import_node_path33 = __toESM(require("path"), 1);
5062
5717
  init_cjs_shims();
5063
5718
  var import_node_fs18 = require("fs");
5064
5719
  var import_node_path32 = __toESM(require("path"), 1);
5065
- var import_types21 = require("@neat.is/types");
5720
+ var import_types22 = require("@neat.is/types");
5066
5721
  function dropOrphanedFileNodes(graph) {
5067
5722
  const orphans = [];
5068
5723
  graph.forEachNode((id, attrs) => {
5069
- if (attrs.type !== import_types21.NodeType.FileNode) return;
5724
+ if (attrs.type !== import_types22.NodeType.FileNode) return;
5070
5725
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5071
5726
  orphans.push(id);
5072
5727
  }
@@ -5079,7 +5734,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5079
5734
  const bases = [scanPath, ...serviceDirs];
5080
5735
  graph.forEachEdge((id, attrs) => {
5081
5736
  const edge = attrs;
5082
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5737
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5083
5738
  const evidenceFile = edge.evidence?.file;
5084
5739
  if (!evidenceFile) return;
5085
5740
  if (import_node_path32.default.isAbsolute(evidenceFile)) {
@@ -5162,7 +5817,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5162
5817
  init_cjs_shims();
5163
5818
  var import_node_fs19 = require("fs");
5164
5819
  var import_node_path34 = __toESM(require("path"), 1);
5165
- var import_types22 = require("@neat.is/types");
5820
+ var import_types23 = require("@neat.is/types");
5166
5821
  var SCHEMA_VERSION = 4;
5167
5822
  function migrateV1ToV2(payload) {
5168
5823
  const nodes = payload.graph.nodes;
@@ -5184,12 +5839,12 @@ function migrateV2ToV3(payload) {
5184
5839
  for (const edge of edges) {
5185
5840
  const attrs = edge.attributes;
5186
5841
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5187
- attrs.provenance = import_types22.Provenance.OBSERVED;
5842
+ attrs.provenance = import_types23.Provenance.OBSERVED;
5188
5843
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5189
5844
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5190
5845
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5191
5846
  if (type && source && target) {
5192
- const newId = (0, import_types22.observedEdgeId)(source, target, type);
5847
+ const newId = (0, import_types23.observedEdgeId)(source, target, type);
5193
5848
  attrs.id = newId;
5194
5849
  if (edge.key) edge.key = newId;
5195
5850
  }
@@ -5333,7 +5988,7 @@ var Projects = class {
5333
5988
  init_cjs_shims();
5334
5989
  var import_fastify = __toESM(require("fastify"), 1);
5335
5990
  var import_cors = __toESM(require("@fastify/cors"), 1);
5336
- var import_types25 = require("@neat.is/types");
5991
+ var import_types26 = require("@neat.is/types");
5337
5992
 
5338
5993
  // src/extend/index.ts
5339
5994
  init_cjs_shims();
@@ -5652,7 +6307,7 @@ async function rollbackExtension(ctx, args) {
5652
6307
 
5653
6308
  // src/divergences.ts
5654
6309
  init_cjs_shims();
5655
- var import_types23 = require("@neat.is/types");
6310
+ var import_types24 = require("@neat.is/types");
5656
6311
  function bucketKey(source, target, type) {
5657
6312
  return `${type}|${source}|${target}`;
5658
6313
  }
@@ -5660,22 +6315,22 @@ function bucketEdges(graph) {
5660
6315
  const buckets = /* @__PURE__ */ new Map();
5661
6316
  graph.forEachEdge((id, attrs) => {
5662
6317
  const e = attrs;
5663
- const parsed = (0, import_types23.parseEdgeId)(id);
6318
+ const parsed = (0, import_types24.parseEdgeId)(id);
5664
6319
  const provenance = parsed?.provenance ?? e.provenance;
5665
6320
  const key = bucketKey(e.source, e.target, e.type);
5666
6321
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5667
6322
  switch (provenance) {
5668
- case import_types23.Provenance.EXTRACTED:
6323
+ case import_types24.Provenance.EXTRACTED:
5669
6324
  cur.extracted = e;
5670
6325
  break;
5671
- case import_types23.Provenance.OBSERVED:
6326
+ case import_types24.Provenance.OBSERVED:
5672
6327
  cur.observed = e;
5673
6328
  break;
5674
- case import_types23.Provenance.INFERRED:
6329
+ case import_types24.Provenance.INFERRED:
5675
6330
  cur.inferred = e;
5676
6331
  break;
5677
6332
  default:
5678
- if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
6333
+ if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
5679
6334
  }
5680
6335
  buckets.set(key, cur);
5681
6336
  });
@@ -5684,7 +6339,7 @@ function bucketEdges(graph) {
5684
6339
  function nodeIsFrontier(graph, nodeId) {
5685
6340
  if (!graph.hasNode(nodeId)) return false;
5686
6341
  const attrs = graph.getNodeAttributes(nodeId);
5687
- return attrs.type === import_types23.NodeType.FrontierNode;
6342
+ return attrs.type === import_types24.NodeType.FrontierNode;
5688
6343
  }
5689
6344
  function clampConfidence(n) {
5690
6345
  if (!Number.isFinite(n)) return 0;
@@ -5703,10 +6358,16 @@ function gradedConfidence(edge) {
5703
6358
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
5704
6359
  return clampConfidence(confidenceForEdge(edge));
5705
6360
  }
6361
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6362
+ import_types24.EdgeType.CALLS,
6363
+ import_types24.EdgeType.CONNECTS_TO,
6364
+ import_types24.EdgeType.PUBLISHES_TO,
6365
+ import_types24.EdgeType.CONSUMES_FROM
6366
+ ]);
5706
6367
  function detectMissingDivergences(graph, bucket) {
5707
6368
  const out = [];
5708
- if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5709
- if (bucket.extracted && !bucket.observed) {
6369
+ if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
6370
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5710
6371
  if (!nodeIsFrontier(graph, bucket.target)) {
5711
6372
  out.push({
5712
6373
  type: "missing-observed",
@@ -5746,7 +6407,7 @@ function declaredHostFor(svc) {
5746
6407
  function hasExtractedConfiguredBy(graph, svcId) {
5747
6408
  for (const edgeId of graph.outboundEdges(svcId)) {
5748
6409
  const e = graph.getEdgeAttributes(edgeId);
5749
- if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
6410
+ if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
5750
6411
  return true;
5751
6412
  }
5752
6413
  }
@@ -5759,10 +6420,10 @@ function detectHostMismatch(graph, svcId, svc) {
5759
6420
  const out = [];
5760
6421
  for (const edgeId of graph.outboundEdges(svcId)) {
5761
6422
  const edge = graph.getEdgeAttributes(edgeId);
5762
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5763
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6423
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6424
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5764
6425
  const target = graph.getNodeAttributes(edge.target);
5765
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6426
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5766
6427
  const observedHost = target.host?.trim();
5767
6428
  if (!observedHost) continue;
5768
6429
  if (observedHost === declaredHost) continue;
@@ -5784,10 +6445,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5784
6445
  const deps = svc.dependencies ?? {};
5785
6446
  for (const edgeId of graph.outboundEdges(svcId)) {
5786
6447
  const edge = graph.getEdgeAttributes(edgeId);
5787
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5788
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6448
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6449
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5789
6450
  const target = graph.getNodeAttributes(edge.target);
5790
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6451
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5791
6452
  for (const pair of compatPairs()) {
5792
6453
  if (pair.engine !== target.engine) continue;
5793
6454
  const declared = deps[pair.driver];
@@ -5840,6 +6501,23 @@ function detectCompatDivergences(graph, svcId, svc) {
5840
6501
  function involvesNode(d, nodeId) {
5841
6502
  return d.source === nodeId || d.target === nodeId;
5842
6503
  }
6504
+ function suppressHostMismatchHalves(all) {
6505
+ const observedHalf = /* @__PURE__ */ new Set();
6506
+ const declaredHalf = /* @__PURE__ */ new Set();
6507
+ for (const d of all) {
6508
+ if (d.type !== "host-mismatch") continue;
6509
+ observedHalf.add(`${d.source}->${d.target}`);
6510
+ declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
6511
+ }
6512
+ if (observedHalf.size === 0) return all;
6513
+ return all.filter((d) => {
6514
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
6515
+ return false;
6516
+ }
6517
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
6518
+ return true;
6519
+ });
6520
+ }
5843
6521
  function computeDivergences(graph, opts = {}) {
5844
6522
  const all = [];
5845
6523
  const buckets = bucketEdges(graph);
@@ -5848,12 +6526,13 @@ function computeDivergences(graph, opts = {}) {
5848
6526
  }
5849
6527
  graph.forEachNode((nodeId, attrs) => {
5850
6528
  const n = attrs;
5851
- if (n.type !== import_types23.NodeType.ServiceNode) return;
6529
+ if (n.type !== import_types24.NodeType.ServiceNode) return;
5852
6530
  const svc = n;
5853
6531
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5854
6532
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
5855
6533
  });
5856
- let filtered = all;
6534
+ const reconciled = suppressHostMismatchHalves(all);
6535
+ let filtered = reconciled;
5857
6536
  if (opts.type) {
5858
6537
  const allowed = opts.type;
5859
6538
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -5881,7 +6560,7 @@ function computeDivergences(graph, opts = {}) {
5881
6560
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5882
6561
  return a.target.localeCompare(b.target);
5883
6562
  });
5884
- return import_types23.DivergenceResultSchema.parse({
6563
+ return import_types24.DivergenceResultSchema.parse({
5885
6564
  divergences: filtered,
5886
6565
  totalAffected: filtered.length,
5887
6566
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5970,7 +6649,7 @@ init_cjs_shims();
5970
6649
  var import_node_fs23 = require("fs");
5971
6650
  var import_node_os3 = __toESM(require("os"), 1);
5972
6651
  var import_node_path38 = __toESM(require("path"), 1);
5973
- var import_types24 = require("@neat.is/types");
6652
+ var import_types25 = require("@neat.is/types");
5974
6653
  var LOCK_TIMEOUT_MS = 5e3;
5975
6654
  var LOCK_RETRY_MS = 50;
5976
6655
  function neatHome() {
@@ -6111,10 +6790,10 @@ async function readRegistry() {
6111
6790
  throw err;
6112
6791
  }
6113
6792
  const parsed = JSON.parse(raw);
6114
- return import_types24.RegistryFileSchema.parse(parsed);
6793
+ return import_types25.RegistryFileSchema.parse(parsed);
6115
6794
  }
6116
6795
  async function writeRegistry(reg) {
6117
- const validated = import_types24.RegistryFileSchema.parse(reg);
6796
+ const validated = import_types25.RegistryFileSchema.parse(reg);
6118
6797
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6119
6798
  }
6120
6799
  async function getProject(name) {
@@ -6385,6 +7064,18 @@ function registerRoutes(scope, ctx) {
6385
7064
  }
6386
7065
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6387
7066
  });
7067
+ scope.get(
7068
+ "/graph/observed-dependencies/:nodeId",
7069
+ async (req2, reply) => {
7070
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
7071
+ if (!proj) return;
7072
+ const { nodeId } = req2.params;
7073
+ if (!proj.graph.hasNode(nodeId)) {
7074
+ return reply.code(404).send({ error: "node not found", id: nodeId });
7075
+ }
7076
+ return getObservedDependencies(proj.graph, nodeId);
7077
+ }
7078
+ );
6388
7079
  scope.get("/graph/divergences", async (req2, reply) => {
6389
7080
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
6390
7081
  if (!proj) return;
@@ -6393,11 +7084,11 @@ function registerRoutes(scope, ctx) {
6393
7084
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6394
7085
  const parsed = [];
6395
7086
  for (const c of candidates) {
6396
- const r = import_types25.DivergenceTypeSchema.safeParse(c);
7087
+ const r = import_types26.DivergenceTypeSchema.safeParse(c);
6397
7088
  if (!r.success) {
6398
7089
  return reply.code(400).send({
6399
7090
  error: `unknown divergence type "${c}"`,
6400
- allowed: import_types25.DivergenceTypeSchema.options
7091
+ allowed: import_types26.DivergenceTypeSchema.options
6401
7092
  });
6402
7093
  }
6403
7094
  parsed.push(r.data);
@@ -6445,23 +7136,29 @@ function registerRoutes(scope, ctx) {
6445
7136
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6446
7137
  return { count: sliced.length, total, events: sliced };
6447
7138
  });
7139
+ const incidentHistoryHandler = async (req2, reply) => {
7140
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
7141
+ if (!proj) return;
7142
+ const { nodeId } = req2.params;
7143
+ if (!proj.graph.hasNode(nodeId)) {
7144
+ reply.code(404).send({ error: "node not found", id: nodeId });
7145
+ return;
7146
+ }
7147
+ const epath = errorsPathFor(proj);
7148
+ if (!epath) return { count: 0, total: 0, events: [] };
7149
+ const events = await readErrorEvents(epath);
7150
+ const filtered = events.filter(
7151
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
7152
+ );
7153
+ return { count: filtered.length, total: filtered.length, events: filtered };
7154
+ };
6448
7155
  scope.get(
6449
7156
  "/incidents/:nodeId",
6450
- async (req2, reply) => {
6451
- const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
6452
- if (!proj) return;
6453
- const { nodeId } = req2.params;
6454
- if (!proj.graph.hasNode(nodeId)) {
6455
- return reply.code(404).send({ error: "node not found", id: nodeId });
6456
- }
6457
- const epath = errorsPathFor(proj);
6458
- if (!epath) return { count: 0, total: 0, events: [] };
6459
- const events = await readErrorEvents(epath);
6460
- const filtered = events.filter(
6461
- (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6462
- );
6463
- return { count: filtered.length, total: filtered.length, events: filtered };
6464
- }
7157
+ incidentHistoryHandler
7158
+ );
7159
+ scope.get(
7160
+ "/graph/incident-history/:nodeId",
7161
+ incidentHistoryHandler
6465
7162
  );
6466
7163
  scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
6467
7164
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
@@ -6470,16 +7167,16 @@ function registerRoutes(scope, ctx) {
6470
7167
  if (!proj.graph.hasNode(nodeId)) {
6471
7168
  return reply.code(404).send({ error: "node not found", id: nodeId });
6472
7169
  }
6473
- let errorEvent;
6474
7170
  const epath = errorsPathFor(proj);
6475
- if (req2.query.errorId && epath) {
6476
- const events = await readErrorEvents(epath);
6477
- errorEvent = events.find((e) => e.id === req2.query.errorId);
7171
+ const incidents = epath ? await readErrorEvents(epath) : [];
7172
+ let errorEvent;
7173
+ if (req2.query.errorId) {
7174
+ errorEvent = incidents.find((e) => e.id === req2.query.errorId);
6478
7175
  if (!errorEvent) {
6479
7176
  return reply.code(404).send({ error: "error event not found", id: req2.query.errorId });
6480
7177
  }
6481
7178
  }
6482
- const result = getRootCause(proj.graph, nodeId, errorEvent);
7179
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6483
7180
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6484
7181
  return result;
6485
7182
  });
@@ -6610,7 +7307,7 @@ function registerRoutes(scope, ctx) {
6610
7307
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6611
7308
  let violations = await log.readAll();
6612
7309
  if (req2.query.severity) {
6613
- const sev = import_types25.PolicySeveritySchema.safeParse(req2.query.severity);
7310
+ const sev = import_types26.PolicySeveritySchema.safeParse(req2.query.severity);
6614
7311
  if (!sev.success) {
6615
7312
  return reply.code(400).send({
6616
7313
  error: "invalid severity",
@@ -6624,10 +7321,32 @@ function registerRoutes(scope, ctx) {
6624
7321
  }
6625
7322
  return { violations };
6626
7323
  });
7324
+ scope.get("/policies/applicable", async (req2, reply) => {
7325
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
7326
+ if (!proj) return;
7327
+ const nodeId = req2.query.node;
7328
+ if (!nodeId) {
7329
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7330
+ }
7331
+ const policyPath = ctx.policyFilePathFor(proj);
7332
+ let policies = [];
7333
+ if (policyPath) {
7334
+ try {
7335
+ policies = await loadPolicyFile(policyPath);
7336
+ } catch (err) {
7337
+ return reply.code(400).send({
7338
+ error: "policy.json failed to parse",
7339
+ details: err.message
7340
+ });
7341
+ }
7342
+ }
7343
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7344
+ return { node: nodeId, applicable };
7345
+ });
6627
7346
  scope.post("/policies/check", async (req2, reply) => {
6628
7347
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
6629
7348
  if (!proj) return;
6630
- const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
7349
+ const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
6631
7350
  if (!parsed.success) {
6632
7351
  return reply.code(400).send({
6633
7352
  error: "invalid /policies/check body",
@@ -6909,6 +7628,7 @@ function unroutedErrorsPath(neatHome3) {
6909
7628
  }
6910
7629
 
6911
7630
  // src/daemon.ts
7631
+ var import_types27 = require("@neat.is/types");
6912
7632
  function daemonJsonPath(scanPath) {
6913
7633
  return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
6914
7634
  }
@@ -6962,6 +7682,20 @@ async function clearDaemonRecord(record, home) {
6962
7682
  } catch {
6963
7683
  }
6964
7684
  }
7685
+ function reconcileDaemonRecordSync(record, home) {
7686
+ try {
7687
+ const stopped = { ...record, status: "stopped" };
7688
+ const target = daemonJsonPath(record.projectPath);
7689
+ const tmp = `${target}.${process.pid}.tmp`;
7690
+ (0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
7691
+ (0, import_node_fs25.renameSync)(tmp, target);
7692
+ } catch {
7693
+ }
7694
+ try {
7695
+ (0, import_node_fs25.unlinkSync)(daemonDiscoveryPath(record.project, home));
7696
+ } catch {
7697
+ }
7698
+ }
6965
7699
  function teardownSlot(slot) {
6966
7700
  try {
6967
7701
  slot.stopPersist();
@@ -7011,6 +7745,19 @@ function isTokenContained(needle, haystack) {
7011
7745
  const tokens = haystack.split(/[-_]/);
7012
7746
  return tokens.includes(needle);
7013
7747
  }
7748
+ function serviceNameMatchesProject(serviceName, project) {
7749
+ if (serviceName === project) return true;
7750
+ if (isTokenPrefix(project, serviceName)) return true;
7751
+ if (isTokenContained(project, serviceName)) return true;
7752
+ return false;
7753
+ }
7754
+ function spanBelongsToSingleProject(graph, project, serviceName) {
7755
+ if (!serviceName) return true;
7756
+ if (serviceNameMatchesProject(serviceName, project)) return true;
7757
+ return graph.someNode(
7758
+ (_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
7759
+ );
7760
+ }
7014
7761
  async function bootstrapProject(entry2) {
7015
7762
  const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
7016
7763
  try {
@@ -7308,7 +8055,9 @@ async function startDaemon(opts = {}) {
7308
8055
  singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
7309
8056
  });
7310
8057
  restAddress = await restApp.listen({ port: restPort, host });
7311
- console.log(`neatd: REST listening on ${restAddress}`);
8058
+ console.log(
8059
+ `neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
8060
+ );
7312
8061
  } catch (err) {
7313
8062
  for (const slot of slots.values()) {
7314
8063
  teardownSlot(slot);
@@ -7339,6 +8088,10 @@ async function startDaemon(opts = {}) {
7339
8088
  warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
7340
8089
  return null;
7341
8090
  }
8091
+ if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
8092
+ await recordUnroutedSpan(serviceName, traceId);
8093
+ return null;
8094
+ }
7342
8095
  return slot2;
7343
8096
  }
7344
8097
  const liveEntries = await listProjects().catch(() => []);
@@ -7401,7 +8154,7 @@ async function startDaemon(opts = {}) {
7401
8154
  onErrorSpanSync: async (span) => {
7402
8155
  const slot = await resolveTargetSlot(span.service, span.traceId);
7403
8156
  if (!slot) return;
7404
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8157
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7405
8158
  },
7406
8159
  // Project-scoped route (issue #367) — the URL already named the
7407
8160
  // project. Resolution is a direct slot lookup; service.name resolves
@@ -7424,10 +8177,10 @@ async function startDaemon(opts = {}) {
7424
8177
  onProjectErrorSpanSync: async (project, span) => {
7425
8178
  const slot = await resolveSlotByName(project, span.service, span.traceId);
7426
8179
  if (!slot) return;
7427
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8180
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7428
8181
  }
7429
8182
  });
7430
- otlpAddress = await otlpApp.listen({ port: otlpPort, host });
8183
+ otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
7431
8184
  console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
7432
8185
  } catch (err) {
7433
8186
  for (const slot of slots.values()) {
@@ -7582,7 +8335,8 @@ async function startDaemon(opts = {}) {
7582
8335
  otlpAddress,
7583
8336
  bootstrap: tracker,
7584
8337
  initialBootstrap,
7585
- daemonRecord
8338
+ daemonRecord,
8339
+ neatHome: home
7586
8340
  };
7587
8341
  }
7588
8342
 
@@ -7911,6 +8665,20 @@ function webPortFromEnv() {
7911
8665
  const n = Number.parseInt(raw, 10);
7912
8666
  return Number.isFinite(n) ? n : void 0;
7913
8667
  }
8668
+ function installIngestFaultHandlers(onFatal = (code) => process.exit(code)) {
8669
+ const logFault = (kind, err) => {
8670
+ const e = err;
8671
+ console.error(`neatd: ${kind}. ${e?.stack ?? e?.message ?? String(err)}`);
8672
+ };
8673
+ process.on(
8674
+ "unhandledRejection",
8675
+ (reason) => logFault("unhandled rejection \u2014 daemon staying up", reason)
8676
+ );
8677
+ process.on("uncaughtException", (err) => {
8678
+ logFault("uncaught exception \u2014 exiting", err);
8679
+ onFatal(1);
8680
+ });
8681
+ }
7914
8682
  async function cmdStart() {
7915
8683
  const project = process.env.NEAT_PROJECT;
7916
8684
  const projectPath = process.env.NEAT_PROJECT_PATH;
@@ -7925,6 +8693,12 @@ async function cmdStart() {
7925
8693
  }
7926
8694
  throw err;
7927
8695
  }
8696
+ installIngestFaultHandlers();
8697
+ process.on("exit", () => {
8698
+ if (handle.daemonRecord) {
8699
+ reconcileDaemonRecordSync(handle.daemonRecord, handle.neatHome);
8700
+ }
8701
+ });
7928
8702
  console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
7929
8703
  console.log(`neatd: registry at ${registryPath()}`);
7930
8704
  if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
@@ -8026,4 +8800,8 @@ if (/[\\/]neatd\.(?:cjs|js)$/.test(entry) || entry.endsWith("/neatd")) {
8026
8800
  process.exit(1);
8027
8801
  });
8028
8802
  }
8803
+ // Annotate the CommonJS export names for ESM import in node:
8804
+ 0 && (module.exports = {
8805
+ installIngestFaultHandlers
8806
+ });
8029
8807
  //# sourceMappingURL=neatd.cjs.map