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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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");
@@ -2136,6 +2578,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
2136
2578
  };
2137
2579
  await appendErrorEvent(ctx, ev);
2138
2580
  }
2581
+ async function recordExceptionIncident(ctx, span, ts) {
2582
+ const attrs = sanitizeAttributes(span.attributes);
2583
+ const ev = {
2584
+ id: `${span.traceId}:${span.spanId}`,
2585
+ timestamp: ts,
2586
+ service: span.service,
2587
+ traceId: span.traceId,
2588
+ spanId: span.spanId,
2589
+ errorMessage: incidentMessage(span),
2590
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2591
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2592
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2593
+ affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
2594
+ };
2595
+ await appendErrorEvent(ctx, ev);
2596
+ }
2139
2597
  async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2140
2598
  const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2141
2599
  if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
@@ -2192,7 +2650,10 @@ async function handleSpan(ctx, span) {
2192
2650
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2193
2651
  cacheSpanService(span, nowMs, callSite);
2194
2652
  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;
2653
+ const callSiteEvidence = callSite ? {
2654
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2655
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2656
+ } : void 0;
2196
2657
  let affectedNode = sourceId;
2197
2658
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2198
2659
  if (span.dbSystem) {
@@ -2214,7 +2675,7 @@ async function handleSpan(ctx, span) {
2214
2675
  } else {
2215
2676
  const host = pickAddress(span);
2216
2677
  let resolvedViaAddress = false;
2217
- if (mintsFromCallerSide && host && host !== span.service) {
2678
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
2218
2679
  const targetId = resolveServiceId(ctx.graph, host, env);
2219
2680
  if (targetId && targetId !== sourceId) {
2220
2681
  upsertObservedEdge(
@@ -2249,7 +2710,11 @@ async function handleSpan(ctx, span) {
2249
2710
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2250
2711
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2251
2712
  const fallbackEvidence = parent.callSite ? {
2252
- file: parent.callSite.relPath,
2713
+ file: reconcileObservedRelPath(
2714
+ ctx.graph,
2715
+ parent.service,
2716
+ parent.callSite.relPath
2717
+ ),
2253
2718
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2254
2719
  } : void 0;
2255
2720
  upsertObservedEdge(
@@ -2274,7 +2739,7 @@ async function handleSpan(ctx, span) {
2274
2739
  service: span.service,
2275
2740
  traceId: span.traceId,
2276
2741
  spanId: span.spanId,
2277
- errorMessage: span.exception?.message ?? "unknown error",
2742
+ errorMessage: incidentMessage(span),
2278
2743
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2279
2744
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2280
2745
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -2285,7 +2750,9 @@ async function handleSpan(ctx, span) {
2285
2750
  }
2286
2751
  if (span.statusCode !== 2) {
2287
2752
  const status2 = httpResponseStatus(span);
2288
- if (status2 !== void 0 && status2 >= 500) {
2753
+ if (span.exception) {
2754
+ await recordExceptionIncident(ctx, span, ts);
2755
+ } else if (status2 !== void 0 && status2 >= 500) {
2289
2756
  await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2290
2757
  } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2291
2758
  await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
@@ -2377,12 +2844,43 @@ async function readStaleEvents(staleEventsPath) {
2377
2844
  async function readErrorEvents(errorsPath) {
2378
2845
  try {
2379
2846
  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));
2847
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2848
+ return dedupeIncidents(events);
2381
2849
  } catch (err) {
2382
2850
  if (err.code === "ENOENT") return [];
2383
2851
  throw err;
2384
2852
  }
2385
2853
  }
2854
+ function isSynthesizedHttpIncident(ev) {
2855
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2856
+ if (ev.errorType) return false;
2857
+ if (!ev.attributes) return false;
2858
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2859
+ return synth !== void 0 && synth === ev.errorMessage;
2860
+ }
2861
+ function dedupeIncidents(events) {
2862
+ const seen = /* @__PURE__ */ new Set();
2863
+ const once = [];
2864
+ for (const ev of events) {
2865
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2866
+ if (key === void 0) {
2867
+ once.push(ev);
2868
+ continue;
2869
+ }
2870
+ if (seen.has(key)) continue;
2871
+ seen.add(key);
2872
+ once.push(ev);
2873
+ }
2874
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2875
+ const hasRealFailure = /* @__PURE__ */ new Set();
2876
+ for (const ev of once) {
2877
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2878
+ }
2879
+ return once.filter((ev) => {
2880
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
2881
+ return !hasRealFailure.has(groupKey(ev));
2882
+ });
2883
+ }
2386
2884
  function mergeSnapshot(graph, snapshot) {
2387
2885
  const exported = snapshot.graph;
2388
2886
  let nodesAdded = 0;
@@ -2463,10 +2961,17 @@ function isConfigFile(name) {
2463
2961
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2464
2962
  if (name === ".env" || name.startsWith(".env.")) {
2465
2963
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2964
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2466
2965
  return { match: true, fileType: "env" };
2467
2966
  }
2468
2967
  return { match: false, fileType: "" };
2469
2968
  }
2969
+ function isNeatAuthoredEnvFile(name) {
2970
+ return name === ".env.neat";
2971
+ }
2972
+ function isNeatAuthoredSourceFile(name) {
2973
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
2974
+ }
2470
2975
  function isTestPath(filePath) {
2471
2976
  const normalised = filePath.replace(/\\/g, "/");
2472
2977
  const segments = normalised.split("/");
@@ -3160,7 +3665,9 @@ async function walkSourceFiles(dir) {
3160
3665
  if (IGNORED_DIRS.has(entry2.name)) continue;
3161
3666
  if (await isPythonVenvDir(full)) continue;
3162
3667
  await walk(full);
3163
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name))) {
3668
+ } 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
3669
+ // would attribute our instrumentation imports to the user's service.
3670
+ !isNeatAuthoredSourceFile(entry2.name)) {
3164
3671
  out.push(full);
3165
3672
  }
3166
3673
  }
@@ -3330,11 +3837,28 @@ function collectJsImports(node, out) {
3330
3837
  if (child) collectJsImports(child, out);
3331
3838
  }
3332
3839
  }
3840
+ function collectImportedNames(node, out) {
3841
+ if (node.type === "aliased_import") {
3842
+ const nameNode = node.childForFieldName("name");
3843
+ if (nameNode) out.push(nameNode.text);
3844
+ return;
3845
+ }
3846
+ if (node.type === "dotted_name") {
3847
+ out.push(node.text);
3848
+ return;
3849
+ }
3850
+ for (let i = 0; i < node.namedChildCount; i++) {
3851
+ const child = node.namedChild(i);
3852
+ if (child) collectImportedNames(child, out);
3853
+ }
3854
+ }
3333
3855
  function collectPyImports(node, out) {
3334
3856
  if (node.type === "import_from_statement") {
3335
3857
  let level = 0;
3336
3858
  let modulePath = "";
3859
+ const names = [];
3337
3860
  let pastFrom = false;
3861
+ let pastImport = false;
3338
3862
  for (let i = 0; i < node.childCount; i++) {
3339
3863
  const child = node.child(i);
3340
3864
  if (!child) continue;
@@ -3342,26 +3866,30 @@ function collectPyImports(node, out) {
3342
3866
  if (child.type === "from") pastFrom = true;
3343
3867
  continue;
3344
3868
  }
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;
3869
+ if (!pastImport) {
3870
+ if (child.type === "import") {
3871
+ pastImport = true;
3872
+ continue;
3355
3873
  }
3356
- break;
3357
- }
3358
- if (child.type === "dotted_name") {
3359
- modulePath = child.text;
3360
- break;
3874
+ if (child.type === "relative_import") {
3875
+ for (let j = 0; j < child.childCount; j++) {
3876
+ const rc = child.child(j);
3877
+ if (!rc) continue;
3878
+ if (rc.type === "import_prefix") {
3879
+ for (let k = 0; k < rc.childCount; k++) {
3880
+ if (rc.child(k)?.type === ".") level++;
3881
+ }
3882
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3883
+ }
3884
+ } else if (child.type === "dotted_name") {
3885
+ modulePath = child.text;
3886
+ }
3887
+ continue;
3361
3888
  }
3889
+ collectImportedNames(child, names);
3362
3890
  }
3363
3891
  if (level > 0 || modulePath) {
3364
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3892
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3365
3893
  }
3366
3894
  }
3367
3895
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3463,8 +3991,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3463
3991
  return null;
3464
3992
  }
3465
3993
  async function resolvePyImport(imp, importerPath, serviceDir) {
3466
- if (!imp.modulePath) return null;
3467
- const relPath = imp.modulePath.split(".").join("/");
3468
3994
  let baseDir;
3469
3995
  if (imp.level > 0) {
3470
3996
  baseDir = import_node_path12.default.dirname(importerPath);
@@ -3472,13 +3998,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
3472
3998
  } else {
3473
3999
  baseDir = serviceDir;
3474
4000
  }
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));
4001
+ const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
4002
+ const resolved = /* @__PURE__ */ new Set();
4003
+ let needModuleFile = imp.names.length === 0;
4004
+ for (const name of imp.names) {
4005
+ const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
4006
+ const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
4007
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
4008
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
4009
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
4010
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
4011
+ } else {
4012
+ needModuleFile = true;
4013
+ }
4014
+ }
4015
+ if (needModuleFile) {
4016
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
4017
+ for (const candidate of moduleFileCandidates) {
4018
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
4019
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
4020
+ break;
4021
+ }
3479
4022
  }
3480
4023
  }
3481
- return null;
4024
+ return [...resolved];
3482
4025
  }
3483
4026
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3484
4027
  const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
@@ -3519,17 +4062,18 @@ async function addImports(graph, services) {
3519
4062
  continue;
3520
4063
  }
3521
4064
  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
- );
4065
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
4066
+ for (const resolved of resolvedPaths) {
4067
+ edgesAdded += emitImportEdge(
4068
+ graph,
4069
+ service.pkg.name,
4070
+ importerFileId,
4071
+ relFile,
4072
+ resolved,
4073
+ imp.line,
4074
+ imp.snippet
4075
+ );
4076
+ }
3533
4077
  }
3534
4078
  continue;
3535
4079
  }
@@ -4165,6 +4709,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4165
4709
  edgesAdded++;
4166
4710
  }
4167
4711
  }
4712
+ if (allConfigs.length === 1) {
4713
+ const primary = allConfigs[0];
4714
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
4715
+ const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
4716
+ const cfgId = (0, import_types9.configId)(relPath);
4717
+ if (!graph.hasNode(cfgId)) {
4718
+ const cfgNode = {
4719
+ id: cfgId,
4720
+ type: import_types9.NodeType.ConfigNode,
4721
+ name: import_node_path20.default.basename(primary.sourceFile),
4722
+ path: relPath,
4723
+ fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
4724
+ };
4725
+ graph.addNode(cfgId, cfgNode);
4726
+ nodesAdded++;
4727
+ }
4728
+ const cfgEdge = {
4729
+ id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
4730
+ source: service.node.id,
4731
+ target: cfgId,
4732
+ type: import_types9.EdgeType.CONFIGURED_BY,
4733
+ provenance: import_types9.Provenance.EXTRACTED,
4734
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
4735
+ evidence: { file: toPosix2(relPath) }
4736
+ };
4737
+ if (!graph.hasEdge(cfgEdge.id)) {
4738
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
4739
+ edgesAdded++;
4740
+ }
4741
+ }
4168
4742
  attachIncompatibilities(service, allConfigs);
4169
4743
  if (graph.hasNode(service.node.id)) {
4170
4744
  const current = graph.getNodeAttributes(service.node.id);
@@ -4176,6 +4750,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4176
4750
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
4177
4751
  delete updated.incompatibilities;
4178
4752
  }
4753
+ if (!service.node.dbConnectionTarget) {
4754
+ delete updated.dbConnectionTarget;
4755
+ }
4179
4756
  graph.replaceNodeAttributes(service.node.id, updated);
4180
4757
  }
4181
4758
  }
@@ -4353,7 +4930,7 @@ async function addHttpCallEdges(graph, services) {
4353
4930
  const dedupKey = `${relFile}|${targetId}`;
4354
4931
  if (seen.has(dedupKey)) continue;
4355
4932
  seen.add(dedupKey);
4356
- const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
4933
+ const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
4357
4934
  const ev = {
4358
4935
  file: relFile,
4359
4936
  line: site.line,
@@ -4373,7 +4950,7 @@ async function addHttpCallEdges(graph, services) {
4373
4950
  target: targetId,
4374
4951
  type: import_types11.EdgeType.CALLS,
4375
4952
  confidence,
4376
- confidenceKind: "hostname-shape-match",
4953
+ confidenceKind: "url-literal-service-target",
4377
4954
  evidence: ev
4378
4955
  });
4379
4956
  continue;
@@ -4879,19 +5456,29 @@ init_cjs_shims();
4879
5456
  var import_node_path29 = __toESM(require("path"), 1);
4880
5457
  var import_node_fs15 = require("fs");
4881
5458
  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) {
5459
+ function readDockerfile(content) {
5460
+ let image = null;
5461
+ const ports = [];
5462
+ let cmd = null;
5463
+ let entrypoint = null;
5464
+ for (const raw of content.split("\n")) {
4886
5465
  const line = raw.trim();
4887
5466
  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;
5467
+ if (/^from\s+/i.test(line)) {
5468
+ const candidate = line.split(/\s+/)[1];
5469
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
5470
+ } else if (/^expose\s+/i.test(line)) {
5471
+ for (const token of line.split(/\s+/).slice(1)) {
5472
+ const port = Number.parseInt(token.split("/")[0], 10);
5473
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
5474
+ }
5475
+ } else if (/^entrypoint\s+/i.test(line)) {
5476
+ entrypoint = line;
5477
+ } else if (/^cmd\s+/i.test(line)) {
5478
+ cmd = line;
5479
+ }
4893
5480
  }
4894
- return last;
5481
+ return { image, ports, entrypoint: entrypoint ?? cmd };
4895
5482
  }
4896
5483
  async function addDockerfileRuntimes(graph, services, scanPath) {
4897
5484
  let nodesAdded = 0;
@@ -4910,14 +5497,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4910
5497
  );
4911
5498
  continue;
4912
5499
  }
4913
- const image = runtimeImage(content);
4914
- if (!image) continue;
4915
- const node = makeInfraNode("container-image", image);
5500
+ const facts = readDockerfile(content);
5501
+ if (!facts.image) continue;
5502
+ const node = makeInfraNode("container-image", facts.image);
4916
5503
  if (!graph.hasNode(node.id)) {
4917
5504
  graph.addNode(node.id, node);
4918
5505
  nodesAdded++;
4919
5506
  }
4920
5507
  const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5508
+ const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
4921
5509
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4922
5510
  graph,
4923
5511
  service.pkg.name,
@@ -4936,12 +5524,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4936
5524
  provenance: import_types20.Provenance.EXTRACTED,
4937
5525
  confidence: (0, import_types20.confidenceForExtracted)("structural"),
4938
5526
  evidence: {
4939
- file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
5527
+ file: evidenceFile,
5528
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
4940
5529
  }
4941
5530
  };
4942
5531
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
4943
5532
  edgesAdded++;
4944
5533
  }
5534
+ for (const port of facts.ports) {
5535
+ const portNode = makeInfraNode("port", String(port));
5536
+ if (!graph.hasNode(portNode.id)) {
5537
+ graph.addNode(portNode.id, portNode);
5538
+ nodesAdded++;
5539
+ }
5540
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
5541
+ if (graph.hasEdge(portEdgeId)) continue;
5542
+ const portEdge = {
5543
+ id: portEdgeId,
5544
+ source: fileNodeId,
5545
+ target: portNode.id,
5546
+ type: import_types20.EdgeType.CONNECTS_TO,
5547
+ provenance: import_types20.Provenance.EXTRACTED,
5548
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
5549
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5550
+ };
5551
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
5552
+ edgesAdded++;
5553
+ }
4945
5554
  }
4946
5555
  return { nodesAdded, edgesAdded };
4947
5556
  }
@@ -4950,7 +5559,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4950
5559
  init_cjs_shims();
4951
5560
  var import_node_fs16 = require("fs");
4952
5561
  var import_node_path30 = __toESM(require("path"), 1);
5562
+ var import_types21 = require("@neat.is/types");
4953
5563
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5564
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
4954
5565
  async function walkTfFiles(start, depth = 0, max = 5) {
4955
5566
  if (depth > max) return [];
4956
5567
  const out = [];
@@ -4967,24 +5578,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4967
5578
  }
4968
5579
  return out;
4969
5580
  }
5581
+ function blockBody(content, from) {
5582
+ const open = content.indexOf("{", from);
5583
+ if (open === -1) return null;
5584
+ let depth = 0;
5585
+ for (let i = open; i < content.length; i++) {
5586
+ const ch = content[i];
5587
+ if (ch === "{") depth++;
5588
+ else if (ch === "}") {
5589
+ depth--;
5590
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
5591
+ }
5592
+ }
5593
+ return null;
5594
+ }
5595
+ function lineAt(content, index) {
5596
+ let line = 1;
5597
+ for (let i = 0; i < index && i < content.length; i++) {
5598
+ if (content[i] === "\n") line++;
5599
+ }
5600
+ return line;
5601
+ }
4970
5602
  async function addTerraformResources(graph, scanPath) {
4971
5603
  let nodesAdded = 0;
5604
+ let edgesAdded = 0;
4972
5605
  const files = await walkTfFiles(scanPath);
4973
5606
  for (const file of files) {
4974
5607
  const content = await import_node_fs16.promises.readFile(file, "utf8");
5608
+ const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
5609
+ const resources = [];
5610
+ const byKey = /* @__PURE__ */ new Map();
4975
5611
  RESOURCE_RE.lastIndex = 0;
4976
5612
  let m;
4977
5613
  while ((m = RESOURCE_RE.exec(content)) !== null) {
4978
- const kind = m[1];
5614
+ const type = m[1];
4979
5615
  const name = m[2];
4980
- const node = makeInfraNode(kind, name, "aws");
5616
+ const node = makeInfraNode(type, name, "aws");
4981
5617
  if (!graph.hasNode(node.id)) {
4982
5618
  graph.addNode(node.id, node);
4983
5619
  nodesAdded++;
4984
5620
  }
5621
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
5622
+ const resource = {
5623
+ type,
5624
+ name,
5625
+ nodeId: node.id,
5626
+ body: span?.body ?? "",
5627
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
5628
+ };
5629
+ resources.push(resource);
5630
+ byKey.set(`${type}.${name}`, resource);
5631
+ }
5632
+ for (const resource of resources) {
5633
+ const seen = /* @__PURE__ */ new Set();
5634
+ REFERENCE_RE.lastIndex = 0;
5635
+ let ref;
5636
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
5637
+ const key = `${ref[1]}.${ref[2]}`;
5638
+ if (key === `${resource.type}.${resource.name}`) continue;
5639
+ const target = byKey.get(key);
5640
+ if (!target) continue;
5641
+ if (seen.has(target.nodeId)) continue;
5642
+ seen.add(target.nodeId);
5643
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
5644
+ if (graph.hasEdge(edgeId)) continue;
5645
+ const line = lineAt(content, resource.bodyOffset + ref.index);
5646
+ const edge = {
5647
+ id: edgeId,
5648
+ source: resource.nodeId,
5649
+ target: target.nodeId,
5650
+ type: import_types21.EdgeType.DEPENDS_ON,
5651
+ provenance: import_types21.Provenance.EXTRACTED,
5652
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
5653
+ evidence: { file: evidenceFile, line, snippet: key }
5654
+ };
5655
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5656
+ edgesAdded++;
5657
+ }
4985
5658
  }
4986
5659
  }
4987
- return { nodesAdded, edgesAdded: 0 };
5660
+ return { nodesAdded, edgesAdded };
4988
5661
  }
4989
5662
 
4990
5663
  // src/extract/infra/k8s.ts
@@ -5062,11 +5735,11 @@ var import_node_path33 = __toESM(require("path"), 1);
5062
5735
  init_cjs_shims();
5063
5736
  var import_node_fs18 = require("fs");
5064
5737
  var import_node_path32 = __toESM(require("path"), 1);
5065
- var import_types21 = require("@neat.is/types");
5738
+ var import_types22 = require("@neat.is/types");
5066
5739
  function dropOrphanedFileNodes(graph) {
5067
5740
  const orphans = [];
5068
5741
  graph.forEachNode((id, attrs) => {
5069
- if (attrs.type !== import_types21.NodeType.FileNode) return;
5742
+ if (attrs.type !== import_types22.NodeType.FileNode) return;
5070
5743
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5071
5744
  orphans.push(id);
5072
5745
  }
@@ -5079,7 +5752,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5079
5752
  const bases = [scanPath, ...serviceDirs];
5080
5753
  graph.forEachEdge((id, attrs) => {
5081
5754
  const edge = attrs;
5082
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5755
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5083
5756
  const evidenceFile = edge.evidence?.file;
5084
5757
  if (!evidenceFile) return;
5085
5758
  if (import_node_path32.default.isAbsolute(evidenceFile)) {
@@ -5162,7 +5835,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5162
5835
  init_cjs_shims();
5163
5836
  var import_node_fs19 = require("fs");
5164
5837
  var import_node_path34 = __toESM(require("path"), 1);
5165
- var import_types22 = require("@neat.is/types");
5838
+ var import_types23 = require("@neat.is/types");
5166
5839
  var SCHEMA_VERSION = 4;
5167
5840
  function migrateV1ToV2(payload) {
5168
5841
  const nodes = payload.graph.nodes;
@@ -5184,12 +5857,12 @@ function migrateV2ToV3(payload) {
5184
5857
  for (const edge of edges) {
5185
5858
  const attrs = edge.attributes;
5186
5859
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5187
- attrs.provenance = import_types22.Provenance.OBSERVED;
5860
+ attrs.provenance = import_types23.Provenance.OBSERVED;
5188
5861
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5189
5862
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5190
5863
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5191
5864
  if (type && source && target) {
5192
- const newId = (0, import_types22.observedEdgeId)(source, target, type);
5865
+ const newId = (0, import_types23.observedEdgeId)(source, target, type);
5193
5866
  attrs.id = newId;
5194
5867
  if (edge.key) edge.key = newId;
5195
5868
  }
@@ -5333,7 +6006,7 @@ var Projects = class {
5333
6006
  init_cjs_shims();
5334
6007
  var import_fastify = __toESM(require("fastify"), 1);
5335
6008
  var import_cors = __toESM(require("@fastify/cors"), 1);
5336
- var import_types25 = require("@neat.is/types");
6009
+ var import_types26 = require("@neat.is/types");
5337
6010
 
5338
6011
  // src/extend/index.ts
5339
6012
  init_cjs_shims();
@@ -5652,7 +6325,7 @@ async function rollbackExtension(ctx, args) {
5652
6325
 
5653
6326
  // src/divergences.ts
5654
6327
  init_cjs_shims();
5655
- var import_types23 = require("@neat.is/types");
6328
+ var import_types24 = require("@neat.is/types");
5656
6329
  function bucketKey(source, target, type) {
5657
6330
  return `${type}|${source}|${target}`;
5658
6331
  }
@@ -5660,22 +6333,22 @@ function bucketEdges(graph) {
5660
6333
  const buckets = /* @__PURE__ */ new Map();
5661
6334
  graph.forEachEdge((id, attrs) => {
5662
6335
  const e = attrs;
5663
- const parsed = (0, import_types23.parseEdgeId)(id);
6336
+ const parsed = (0, import_types24.parseEdgeId)(id);
5664
6337
  const provenance = parsed?.provenance ?? e.provenance;
5665
6338
  const key = bucketKey(e.source, e.target, e.type);
5666
6339
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5667
6340
  switch (provenance) {
5668
- case import_types23.Provenance.EXTRACTED:
6341
+ case import_types24.Provenance.EXTRACTED:
5669
6342
  cur.extracted = e;
5670
6343
  break;
5671
- case import_types23.Provenance.OBSERVED:
6344
+ case import_types24.Provenance.OBSERVED:
5672
6345
  cur.observed = e;
5673
6346
  break;
5674
- case import_types23.Provenance.INFERRED:
6347
+ case import_types24.Provenance.INFERRED:
5675
6348
  cur.inferred = e;
5676
6349
  break;
5677
6350
  default:
5678
- if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
6351
+ if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
5679
6352
  }
5680
6353
  buckets.set(key, cur);
5681
6354
  });
@@ -5684,7 +6357,7 @@ function bucketEdges(graph) {
5684
6357
  function nodeIsFrontier(graph, nodeId) {
5685
6358
  if (!graph.hasNode(nodeId)) return false;
5686
6359
  const attrs = graph.getNodeAttributes(nodeId);
5687
- return attrs.type === import_types23.NodeType.FrontierNode;
6360
+ return attrs.type === import_types24.NodeType.FrontierNode;
5688
6361
  }
5689
6362
  function clampConfidence(n) {
5690
6363
  if (!Number.isFinite(n)) return 0;
@@ -5703,10 +6376,16 @@ function gradedConfidence(edge) {
5703
6376
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
5704
6377
  return clampConfidence(confidenceForEdge(edge));
5705
6378
  }
6379
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6380
+ import_types24.EdgeType.CALLS,
6381
+ import_types24.EdgeType.CONNECTS_TO,
6382
+ import_types24.EdgeType.PUBLISHES_TO,
6383
+ import_types24.EdgeType.CONSUMES_FROM
6384
+ ]);
5706
6385
  function detectMissingDivergences(graph, bucket) {
5707
6386
  const out = [];
5708
- if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5709
- if (bucket.extracted && !bucket.observed) {
6387
+ if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
6388
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5710
6389
  if (!nodeIsFrontier(graph, bucket.target)) {
5711
6390
  out.push({
5712
6391
  type: "missing-observed",
@@ -5746,7 +6425,7 @@ function declaredHostFor(svc) {
5746
6425
  function hasExtractedConfiguredBy(graph, svcId) {
5747
6426
  for (const edgeId of graph.outboundEdges(svcId)) {
5748
6427
  const e = graph.getEdgeAttributes(edgeId);
5749
- if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
6428
+ if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
5750
6429
  return true;
5751
6430
  }
5752
6431
  }
@@ -5759,10 +6438,10 @@ function detectHostMismatch(graph, svcId, svc) {
5759
6438
  const out = [];
5760
6439
  for (const edgeId of graph.outboundEdges(svcId)) {
5761
6440
  const edge = graph.getEdgeAttributes(edgeId);
5762
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5763
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6441
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6442
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5764
6443
  const target = graph.getNodeAttributes(edge.target);
5765
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6444
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5766
6445
  const observedHost = target.host?.trim();
5767
6446
  if (!observedHost) continue;
5768
6447
  if (observedHost === declaredHost) continue;
@@ -5784,10 +6463,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5784
6463
  const deps = svc.dependencies ?? {};
5785
6464
  for (const edgeId of graph.outboundEdges(svcId)) {
5786
6465
  const edge = graph.getEdgeAttributes(edgeId);
5787
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5788
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6466
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6467
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5789
6468
  const target = graph.getNodeAttributes(edge.target);
5790
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6469
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5791
6470
  for (const pair of compatPairs()) {
5792
6471
  if (pair.engine !== target.engine) continue;
5793
6472
  const declared = deps[pair.driver];
@@ -5840,6 +6519,23 @@ function detectCompatDivergences(graph, svcId, svc) {
5840
6519
  function involvesNode(d, nodeId) {
5841
6520
  return d.source === nodeId || d.target === nodeId;
5842
6521
  }
6522
+ function suppressHostMismatchHalves(all) {
6523
+ const observedHalf = /* @__PURE__ */ new Set();
6524
+ const declaredHalf = /* @__PURE__ */ new Set();
6525
+ for (const d of all) {
6526
+ if (d.type !== "host-mismatch") continue;
6527
+ observedHalf.add(`${d.source}->${d.target}`);
6528
+ declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
6529
+ }
6530
+ if (observedHalf.size === 0) return all;
6531
+ return all.filter((d) => {
6532
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
6533
+ return false;
6534
+ }
6535
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
6536
+ return true;
6537
+ });
6538
+ }
5843
6539
  function computeDivergences(graph, opts = {}) {
5844
6540
  const all = [];
5845
6541
  const buckets = bucketEdges(graph);
@@ -5848,12 +6544,13 @@ function computeDivergences(graph, opts = {}) {
5848
6544
  }
5849
6545
  graph.forEachNode((nodeId, attrs) => {
5850
6546
  const n = attrs;
5851
- if (n.type !== import_types23.NodeType.ServiceNode) return;
6547
+ if (n.type !== import_types24.NodeType.ServiceNode) return;
5852
6548
  const svc = n;
5853
6549
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5854
6550
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
5855
6551
  });
5856
- let filtered = all;
6552
+ const reconciled = suppressHostMismatchHalves(all);
6553
+ let filtered = reconciled;
5857
6554
  if (opts.type) {
5858
6555
  const allowed = opts.type;
5859
6556
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -5881,7 +6578,7 @@ function computeDivergences(graph, opts = {}) {
5881
6578
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5882
6579
  return a.target.localeCompare(b.target);
5883
6580
  });
5884
- return import_types23.DivergenceResultSchema.parse({
6581
+ return import_types24.DivergenceResultSchema.parse({
5885
6582
  divergences: filtered,
5886
6583
  totalAffected: filtered.length,
5887
6584
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5970,7 +6667,7 @@ init_cjs_shims();
5970
6667
  var import_node_fs23 = require("fs");
5971
6668
  var import_node_os3 = __toESM(require("os"), 1);
5972
6669
  var import_node_path38 = __toESM(require("path"), 1);
5973
- var import_types24 = require("@neat.is/types");
6670
+ var import_types25 = require("@neat.is/types");
5974
6671
  var LOCK_TIMEOUT_MS = 5e3;
5975
6672
  var LOCK_RETRY_MS = 50;
5976
6673
  function neatHome() {
@@ -6111,10 +6808,10 @@ async function readRegistry() {
6111
6808
  throw err;
6112
6809
  }
6113
6810
  const parsed = JSON.parse(raw);
6114
- return import_types24.RegistryFileSchema.parse(parsed);
6811
+ return import_types25.RegistryFileSchema.parse(parsed);
6115
6812
  }
6116
6813
  async function writeRegistry(reg) {
6117
- const validated = import_types24.RegistryFileSchema.parse(reg);
6814
+ const validated = import_types25.RegistryFileSchema.parse(reg);
6118
6815
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6119
6816
  }
6120
6817
  async function getProject(name) {
@@ -6385,6 +7082,18 @@ function registerRoutes(scope, ctx) {
6385
7082
  }
6386
7083
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6387
7084
  });
7085
+ scope.get(
7086
+ "/graph/observed-dependencies/:nodeId",
7087
+ async (req2, reply) => {
7088
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
7089
+ if (!proj) return;
7090
+ const { nodeId } = req2.params;
7091
+ if (!proj.graph.hasNode(nodeId)) {
7092
+ return reply.code(404).send({ error: "node not found", id: nodeId });
7093
+ }
7094
+ return getObservedDependencies(proj.graph, nodeId);
7095
+ }
7096
+ );
6388
7097
  scope.get("/graph/divergences", async (req2, reply) => {
6389
7098
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
6390
7099
  if (!proj) return;
@@ -6393,11 +7102,11 @@ function registerRoutes(scope, ctx) {
6393
7102
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6394
7103
  const parsed = [];
6395
7104
  for (const c of candidates) {
6396
- const r = import_types25.DivergenceTypeSchema.safeParse(c);
7105
+ const r = import_types26.DivergenceTypeSchema.safeParse(c);
6397
7106
  if (!r.success) {
6398
7107
  return reply.code(400).send({
6399
7108
  error: `unknown divergence type "${c}"`,
6400
- allowed: import_types25.DivergenceTypeSchema.options
7109
+ allowed: import_types26.DivergenceTypeSchema.options
6401
7110
  });
6402
7111
  }
6403
7112
  parsed.push(r.data);
@@ -6445,23 +7154,29 @@ function registerRoutes(scope, ctx) {
6445
7154
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6446
7155
  return { count: sliced.length, total, events: sliced };
6447
7156
  });
7157
+ const incidentHistoryHandler = async (req2, reply) => {
7158
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
7159
+ if (!proj) return;
7160
+ const { nodeId } = req2.params;
7161
+ if (!proj.graph.hasNode(nodeId)) {
7162
+ reply.code(404).send({ error: "node not found", id: nodeId });
7163
+ return;
7164
+ }
7165
+ const epath = errorsPathFor(proj);
7166
+ if (!epath) return { count: 0, total: 0, events: [] };
7167
+ const events = await readErrorEvents(epath);
7168
+ const filtered = events.filter(
7169
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
7170
+ );
7171
+ return { count: filtered.length, total: filtered.length, events: filtered };
7172
+ };
6448
7173
  scope.get(
6449
7174
  "/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
- }
7175
+ incidentHistoryHandler
7176
+ );
7177
+ scope.get(
7178
+ "/graph/incident-history/:nodeId",
7179
+ incidentHistoryHandler
6465
7180
  );
6466
7181
  scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
6467
7182
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
@@ -6470,16 +7185,16 @@ function registerRoutes(scope, ctx) {
6470
7185
  if (!proj.graph.hasNode(nodeId)) {
6471
7186
  return reply.code(404).send({ error: "node not found", id: nodeId });
6472
7187
  }
6473
- let errorEvent;
6474
7188
  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);
7189
+ const incidents = epath ? await readErrorEvents(epath) : [];
7190
+ let errorEvent;
7191
+ if (req2.query.errorId) {
7192
+ errorEvent = incidents.find((e) => e.id === req2.query.errorId);
6478
7193
  if (!errorEvent) {
6479
7194
  return reply.code(404).send({ error: "error event not found", id: req2.query.errorId });
6480
7195
  }
6481
7196
  }
6482
- const result = getRootCause(proj.graph, nodeId, errorEvent);
7197
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6483
7198
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6484
7199
  return result;
6485
7200
  });
@@ -6610,7 +7325,7 @@ function registerRoutes(scope, ctx) {
6610
7325
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6611
7326
  let violations = await log.readAll();
6612
7327
  if (req2.query.severity) {
6613
- const sev = import_types25.PolicySeveritySchema.safeParse(req2.query.severity);
7328
+ const sev = import_types26.PolicySeveritySchema.safeParse(req2.query.severity);
6614
7329
  if (!sev.success) {
6615
7330
  return reply.code(400).send({
6616
7331
  error: "invalid severity",
@@ -6624,10 +7339,32 @@ function registerRoutes(scope, ctx) {
6624
7339
  }
6625
7340
  return { violations };
6626
7341
  });
7342
+ scope.get("/policies/applicable", async (req2, reply) => {
7343
+ const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
7344
+ if (!proj) return;
7345
+ const nodeId = req2.query.node;
7346
+ if (!nodeId) {
7347
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7348
+ }
7349
+ const policyPath = ctx.policyFilePathFor(proj);
7350
+ let policies = [];
7351
+ if (policyPath) {
7352
+ try {
7353
+ policies = await loadPolicyFile(policyPath);
7354
+ } catch (err) {
7355
+ return reply.code(400).send({
7356
+ error: "policy.json failed to parse",
7357
+ details: err.message
7358
+ });
7359
+ }
7360
+ }
7361
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7362
+ return { node: nodeId, applicable };
7363
+ });
6627
7364
  scope.post("/policies/check", async (req2, reply) => {
6628
7365
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
6629
7366
  if (!proj) return;
6630
- const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
7367
+ const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
6631
7368
  if (!parsed.success) {
6632
7369
  return reply.code(400).send({
6633
7370
  error: "invalid /policies/check body",
@@ -6909,6 +7646,7 @@ function unroutedErrorsPath(neatHome3) {
6909
7646
  }
6910
7647
 
6911
7648
  // src/daemon.ts
7649
+ var import_types27 = require("@neat.is/types");
6912
7650
  function daemonJsonPath(scanPath) {
6913
7651
  return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
6914
7652
  }
@@ -6962,6 +7700,20 @@ async function clearDaemonRecord(record, home) {
6962
7700
  } catch {
6963
7701
  }
6964
7702
  }
7703
+ function reconcileDaemonRecordSync(record, home) {
7704
+ try {
7705
+ const stopped = { ...record, status: "stopped" };
7706
+ const target = daemonJsonPath(record.projectPath);
7707
+ const tmp = `${target}.${process.pid}.tmp`;
7708
+ (0, import_node_fs25.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
7709
+ (0, import_node_fs25.renameSync)(tmp, target);
7710
+ } catch {
7711
+ }
7712
+ try {
7713
+ (0, import_node_fs25.unlinkSync)(daemonDiscoveryPath(record.project, home));
7714
+ } catch {
7715
+ }
7716
+ }
6965
7717
  function teardownSlot(slot) {
6966
7718
  try {
6967
7719
  slot.stopPersist();
@@ -7011,6 +7763,19 @@ function isTokenContained(needle, haystack) {
7011
7763
  const tokens = haystack.split(/[-_]/);
7012
7764
  return tokens.includes(needle);
7013
7765
  }
7766
+ function serviceNameMatchesProject(serviceName, project) {
7767
+ if (serviceName === project) return true;
7768
+ if (isTokenPrefix(project, serviceName)) return true;
7769
+ if (isTokenContained(project, serviceName)) return true;
7770
+ return false;
7771
+ }
7772
+ function spanBelongsToSingleProject(graph, project, serviceName) {
7773
+ if (!serviceName) return true;
7774
+ if (serviceNameMatchesProject(serviceName, project)) return true;
7775
+ return graph.someNode(
7776
+ (_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
7777
+ );
7778
+ }
7014
7779
  async function bootstrapProject(entry2) {
7015
7780
  const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
7016
7781
  try {
@@ -7308,7 +8073,9 @@ async function startDaemon(opts = {}) {
7308
8073
  singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
7309
8074
  });
7310
8075
  restAddress = await restApp.listen({ port: restPort, host });
7311
- console.log(`neatd: REST listening on ${restAddress}`);
8076
+ console.log(
8077
+ `neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
8078
+ );
7312
8079
  } catch (err) {
7313
8080
  for (const slot of slots.values()) {
7314
8081
  teardownSlot(slot);
@@ -7339,6 +8106,10 @@ async function startDaemon(opts = {}) {
7339
8106
  warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
7340
8107
  return null;
7341
8108
  }
8109
+ if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
8110
+ await recordUnroutedSpan(serviceName, traceId);
8111
+ return null;
8112
+ }
7342
8113
  return slot2;
7343
8114
  }
7344
8115
  const liveEntries = await listProjects().catch(() => []);
@@ -7401,7 +8172,7 @@ async function startDaemon(opts = {}) {
7401
8172
  onErrorSpanSync: async (span) => {
7402
8173
  const slot = await resolveTargetSlot(span.service, span.traceId);
7403
8174
  if (!slot) return;
7404
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8175
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7405
8176
  },
7406
8177
  // Project-scoped route (issue #367) — the URL already named the
7407
8178
  // project. Resolution is a direct slot lookup; service.name resolves
@@ -7424,10 +8195,10 @@ async function startDaemon(opts = {}) {
7424
8195
  onProjectErrorSpanSync: async (project, span) => {
7425
8196
  const slot = await resolveSlotByName(project, span.service, span.traceId);
7426
8197
  if (!slot) return;
7427
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8198
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7428
8199
  }
7429
8200
  });
7430
- otlpAddress = await otlpApp.listen({ port: otlpPort, host });
8201
+ otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
7431
8202
  console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
7432
8203
  } catch (err) {
7433
8204
  for (const slot of slots.values()) {
@@ -7582,7 +8353,8 @@ async function startDaemon(opts = {}) {
7582
8353
  otlpAddress,
7583
8354
  bootstrap: tracker,
7584
8355
  initialBootstrap,
7585
- daemonRecord
8356
+ daemonRecord,
8357
+ neatHome: home
7586
8358
  };
7587
8359
  }
7588
8360
 
@@ -7911,6 +8683,20 @@ function webPortFromEnv() {
7911
8683
  const n = Number.parseInt(raw, 10);
7912
8684
  return Number.isFinite(n) ? n : void 0;
7913
8685
  }
8686
+ function installIngestFaultHandlers(onFatal = (code) => process.exit(code)) {
8687
+ const logFault = (kind, err) => {
8688
+ const e = err;
8689
+ console.error(`neatd: ${kind}. ${e?.stack ?? e?.message ?? String(err)}`);
8690
+ };
8691
+ process.on(
8692
+ "unhandledRejection",
8693
+ (reason) => logFault("unhandled rejection \u2014 daemon staying up", reason)
8694
+ );
8695
+ process.on("uncaughtException", (err) => {
8696
+ logFault("uncaught exception \u2014 exiting", err);
8697
+ onFatal(1);
8698
+ });
8699
+ }
7914
8700
  async function cmdStart() {
7915
8701
  const project = process.env.NEAT_PROJECT;
7916
8702
  const projectPath = process.env.NEAT_PROJECT_PATH;
@@ -7925,6 +8711,12 @@ async function cmdStart() {
7925
8711
  }
7926
8712
  throw err;
7927
8713
  }
8714
+ installIngestFaultHandlers();
8715
+ process.on("exit", () => {
8716
+ if (handle.daemonRecord) {
8717
+ reconcileDaemonRecordSync(handle.daemonRecord, handle.neatHome);
8718
+ }
8719
+ });
7928
8720
  console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
7929
8721
  console.log(`neatd: registry at ${registryPath()}`);
7930
8722
  if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
@@ -8026,4 +8818,8 @@ if (/[\\/]neatd\.(?:cjs|js)$/.test(entry) || entry.endsWith("/neatd")) {
8026
8818
  process.exit(1);
8027
8819
  });
8028
8820
  }
8821
+ // Annotate the CommonJS export names for ESM import in node:
8822
+ 0 && (module.exports = {
8823
+ installIngestFaultHandlers
8824
+ });
8029
8825
  //# sourceMappingURL=neatd.cjs.map