@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/index.cjs CHANGED
@@ -41,13 +41,13 @@ var init_cjs_shims = __esm({
41
41
  });
42
42
 
43
43
  // src/auth.ts
44
- function isLoopbackHost(host) {
44
+ function isLoopbackHost2(host) {
45
45
  if (!host) return false;
46
46
  return LOOPBACK_HOSTS.has(host);
47
47
  }
48
48
  function assertBindAuthority(host, token) {
49
49
  if (token && token.length > 0) return;
50
- if (isLoopbackHost(host)) return;
50
+ if (isLoopbackHost2(host)) return;
51
51
  throw new BindAuthorityError(host);
52
52
  }
53
53
  function mountBearerAuth(app, opts) {
@@ -573,6 +573,22 @@ async function buildOtelReceiver(opts) {
573
573
  };
574
574
  return decorated;
575
575
  }
576
+ async function listenSteppingOtlp(app, requestedPort, host) {
577
+ let port = requestedPort;
578
+ for (let attempt = 0; ; attempt++) {
579
+ try {
580
+ return await app.listen({ port, host });
581
+ } catch (err) {
582
+ const code = err.code;
583
+ const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
584
+ if (!canStep) throw err;
585
+ console.warn(
586
+ `otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
587
+ );
588
+ port += OTLP_STEP_STRIDE;
589
+ }
590
+ }
591
+ }
576
592
  function logSpanHandler(span) {
577
593
  const parent = span.parentSpanId ? span.parentSpanId.slice(0, 8) : "<root>";
578
594
  const status2 = span.statusCode === 2 ? "ERROR" : "OK";
@@ -581,7 +597,7 @@ function logSpanHandler(span) {
581
597
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
582
598
  );
583
599
  }
584
- var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
600
+ 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;
585
601
  var init_otel = __esm({
586
602
  "src/otel.ts"() {
587
603
  "use strict";
@@ -597,6 +613,8 @@ var init_otel = __esm({
597
613
  exportTraceServiceRequestType = null;
598
614
  exportTraceServiceResponseType = null;
599
615
  cachedProtobufResponseBody = null;
616
+ OTLP_STEP_ATTEMPTS = 8;
617
+ OTLP_STEP_STRIDE = 1;
600
618
  }
601
619
  });
602
620
 
@@ -1272,22 +1290,164 @@ var rootCauseShapes = {
1272
1290
  [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1273
1291
  [import_types.NodeType.FileNode]: fileRootCauseShape
1274
1292
  };
1275
- function getRootCause(graph, errorNodeId, errorEvent) {
1293
+ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1276
1294
  if (!graph.hasNode(errorNodeId)) return null;
1277
1295
  const origin = graph.getNodeAttributes(errorNodeId);
1278
1296
  const shape = rootCauseShapes[origin.type];
1279
- if (!shape) return null;
1280
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1281
- const match = shape(graph, origin, walk);
1282
- if (!match) return null;
1283
- const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1297
+ if (shape) {
1298
+ const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1299
+ const match = shape(graph, origin, walk);
1300
+ if (match) {
1301
+ const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1302
+ return import_types.RootCauseResultSchema.parse({
1303
+ rootCauseNode: match.rootCauseNode,
1304
+ rootCauseReason: reason,
1305
+ traversalPath: walk.path,
1306
+ edgeProvenances: walk.edges.map((e) => e.provenance),
1307
+ confidence: confidenceFromMix(walk.edges),
1308
+ fixRecommendation: match.fixRecommendation
1309
+ });
1310
+ }
1311
+ }
1312
+ if (origin.type === import_types.NodeType.ServiceNode) {
1313
+ const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
1314
+ if (crossService) return crossService;
1315
+ }
1316
+ return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
1317
+ }
1318
+ var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
1319
+ function incidentMatchesNode(ev, nodeId) {
1320
+ return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
1321
+ }
1322
+ function localizeFromIncidents(nodeId, incidents, errorEvent) {
1323
+ const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
1324
+ const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
1325
+ if (relevant.length === 0) return null;
1326
+ const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
1327
+ const attrs = latest.attributes ?? {};
1328
+ const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
1329
+ const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
1330
+ const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
1331
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
1332
+ const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
1333
+ const count = sameMode.length;
1334
+ const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
1335
+ const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
1336
+ if (location) reasonParts.push(`surfaced at ${location}`);
1337
+ const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
1338
+ const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
1339
+ const fileNode = localizesToFile ? latest.affectedNode : void 0;
1340
+ const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
1341
+ return {
1342
+ rootCauseNode: fileNode ?? nodeId,
1343
+ rootCauseReason,
1344
+ ...fileNode ? { fileNode } : {},
1345
+ ...fixRecommendation ? { fixRecommendation } : {}
1346
+ };
1347
+ }
1348
+ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1349
+ const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
1350
+ if (!loc) return null;
1351
+ const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
1352
+ const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
1284
1353
  return import_types.RootCauseResultSchema.parse({
1285
- rootCauseNode: match.rootCauseNode,
1286
- rootCauseReason: reason,
1287
- traversalPath: walk.path,
1288
- edgeProvenances: walk.edges.map((e) => e.provenance),
1289
- confidence: confidenceFromMix(walk.edges),
1290
- fixRecommendation: match.fixRecommendation
1354
+ rootCauseNode: loc.rootCauseNode,
1355
+ rootCauseReason: loc.rootCauseReason,
1356
+ traversalPath,
1357
+ edgeProvenances,
1358
+ confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
1359
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1360
+ });
1361
+ }
1362
+ function isFailingCallEdge(e) {
1363
+ return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1364
+ }
1365
+ function callSourcesForService(graph, serviceId3) {
1366
+ const ids = [serviceId3];
1367
+ for (const edgeId of graph.outboundEdges(serviceId3)) {
1368
+ const e = graph.getEdgeAttributes(edgeId);
1369
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1370
+ const tgt = graph.getNodeAttributes(e.target);
1371
+ if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
1372
+ }
1373
+ return ids;
1374
+ }
1375
+ function failingCallDominates(e, id, curEdge, curId) {
1376
+ const ec = e.signal?.errorCount ?? 0;
1377
+ const cc = curEdge.signal?.errorCount ?? 0;
1378
+ if (ec !== cc) return ec > cc;
1379
+ if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
1380
+ return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
1381
+ }
1382
+ return id < curId;
1383
+ }
1384
+ function dominantFailingCall(graph, serviceId3, visited) {
1385
+ let best = null;
1386
+ for (const src of callSourcesForService(graph, serviceId3)) {
1387
+ for (const edgeId of graph.outboundEdges(src)) {
1388
+ const e = graph.getEdgeAttributes(edgeId);
1389
+ if (!isFailingCallEdge(e)) continue;
1390
+ if (isFrontierNode(graph, e.target)) continue;
1391
+ const owner = resolveOwningService(graph, e.target);
1392
+ if (!owner || visited.has(owner.id)) continue;
1393
+ if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
1394
+ best = { nextService: owner.id, edge: e };
1395
+ }
1396
+ }
1397
+ }
1398
+ return best;
1399
+ }
1400
+ function followFailingCallChain(graph, originServiceId, maxDepth) {
1401
+ const path43 = [originServiceId];
1402
+ const edges = [];
1403
+ const visited = /* @__PURE__ */ new Set([originServiceId]);
1404
+ let current = originServiceId;
1405
+ for (let depth = 0; depth < maxDepth; depth++) {
1406
+ const hop = dominantFailingCall(graph, current, visited);
1407
+ if (!hop) break;
1408
+ path43.push(hop.nextService);
1409
+ edges.push(hop.edge);
1410
+ visited.add(hop.nextService);
1411
+ current = hop.nextService;
1412
+ }
1413
+ if (edges.length === 0) return null;
1414
+ return { path: path43, edges, culprit: current };
1415
+ }
1416
+ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1417
+ const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1418
+ if (!chain) return null;
1419
+ const culprit = chain.culprit;
1420
+ const path43 = [...chain.path];
1421
+ const edgeProvenances = chain.edges.map((e) => e.provenance);
1422
+ const baseConfidence = confidenceFromMix(chain.edges);
1423
+ const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
1424
+ const loc = localizeFromIncidents(culprit, incidents, errorEvent);
1425
+ if (loc) {
1426
+ let rootCauseNode = culprit;
1427
+ if (loc.fileNode) {
1428
+ path43.push(loc.fileNode);
1429
+ edgeProvenances.push(import_types.Provenance.OBSERVED);
1430
+ rootCauseNode = loc.fileNode;
1431
+ }
1432
+ return import_types.RootCauseResultSchema.parse({
1433
+ rootCauseNode,
1434
+ rootCauseReason: loc.rootCauseReason,
1435
+ traversalPath: path43,
1436
+ edgeProvenances,
1437
+ confidence,
1438
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1439
+ });
1440
+ }
1441
+ const lastEdge = chain.edges[chain.edges.length - 1];
1442
+ const errs = lastEdge.signal?.errorCount ?? 0;
1443
+ const culpritName = culprit.replace(/^service:/, "");
1444
+ return import_types.RootCauseResultSchema.parse({
1445
+ rootCauseNode: culprit,
1446
+ rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1447
+ traversalPath: path43,
1448
+ edgeProvenances,
1449
+ confidence,
1450
+ fixRecommendation: `Inspect ${culpritName}'s failing handler`
1291
1451
  });
1292
1452
  }
1293
1453
  function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
@@ -1310,14 +1470,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
1310
1470
  });
1311
1471
  }
1312
1472
  if (frame.distance >= maxDepth) continue;
1313
- const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
1314
- for (const [tgtId, edge] of outgoing) {
1315
- if (enqueued.has(tgtId)) continue;
1316
- enqueued.add(tgtId);
1473
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
1474
+ for (const [srcId, edge] of incoming) {
1475
+ if (enqueued.has(srcId)) continue;
1476
+ enqueued.add(srcId);
1317
1477
  queue.push({
1318
- nodeId: tgtId,
1478
+ nodeId: srcId,
1319
1479
  distance: frame.distance + 1,
1320
- path: [...frame.path, tgtId],
1480
+ path: [...frame.path, srcId],
1321
1481
  pathEdges: [...frame.pathEdges, edge]
1322
1482
  });
1323
1483
  }
@@ -1373,6 +1533,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
1373
1533
  total: dependencies.length
1374
1534
  });
1375
1535
  }
1536
+ function getObservedDependencies(graph, nodeId) {
1537
+ if (!graph.hasNode(nodeId)) {
1538
+ return import_types.ObservedDependenciesResultSchema.parse({
1539
+ origin: nodeId,
1540
+ dependencies: [],
1541
+ observed: false,
1542
+ inboundObservedCount: 0,
1543
+ hasExtractedOutbound: false
1544
+ });
1545
+ }
1546
+ const attrs = graph.getNodeAttributes(nodeId);
1547
+ const scope = [nodeId];
1548
+ if (attrs.type === import_types.NodeType.ServiceNode) {
1549
+ for (const edgeId of graph.outboundEdges(nodeId)) {
1550
+ const e = graph.getEdgeAttributes(edgeId);
1551
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1552
+ const owned = graph.getNodeAttributes(e.target);
1553
+ if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
1554
+ }
1555
+ }
1556
+ const dependencies = [];
1557
+ const seenEdge = /* @__PURE__ */ new Set();
1558
+ let hasExtractedOutbound = false;
1559
+ for (const src of scope) {
1560
+ for (const edgeId of graph.outboundEdges(src)) {
1561
+ const e = graph.getEdgeAttributes(edgeId);
1562
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1563
+ if (e.provenance === import_types.Provenance.OBSERVED) {
1564
+ if (!seenEdge.has(e.id)) {
1565
+ seenEdge.add(e.id);
1566
+ dependencies.push(e);
1567
+ }
1568
+ } else if (e.provenance === import_types.Provenance.EXTRACTED) {
1569
+ hasExtractedOutbound = true;
1570
+ }
1571
+ }
1572
+ }
1573
+ let inboundObservedCount = 0;
1574
+ for (const tgt of scope) {
1575
+ for (const edgeId of graph.inboundEdges(tgt)) {
1576
+ const e = graph.getEdgeAttributes(edgeId);
1577
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1578
+ if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
1579
+ }
1580
+ }
1581
+ dependencies.sort(
1582
+ (a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
1583
+ );
1584
+ return import_types.ObservedDependenciesResultSchema.parse({
1585
+ origin: nodeId,
1586
+ dependencies,
1587
+ observed: dependencies.length > 0 || inboundObservedCount > 0,
1588
+ inboundObservedCount,
1589
+ hasExtractedOutbound
1590
+ });
1591
+ }
1376
1592
 
1377
1593
  // src/policy.ts
1378
1594
  var DEFAULT_ACTION_BY_SEVERITY = {
@@ -1642,6 +1858,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
1642
1858
  }
1643
1859
  return out;
1644
1860
  }
1861
+ function selectApplicablePolicies(graph, policies, nodeId) {
1862
+ if (!graph.hasNode(nodeId)) return [];
1863
+ const node = graph.getNodeAttributes(nodeId);
1864
+ const out = [];
1865
+ for (const policy of policies) {
1866
+ const m = matchPolicyToNode(graph, policy, nodeId, node);
1867
+ if (!m) continue;
1868
+ out.push({
1869
+ policyId: policy.id,
1870
+ policyName: policy.name,
1871
+ ...policy.description !== void 0 ? { description: policy.description } : {},
1872
+ severity: policy.severity,
1873
+ onViolation: resolveOnViolation(policy),
1874
+ ruleType: policy.rule.type,
1875
+ match: m.match,
1876
+ reason: m.reason
1877
+ });
1878
+ }
1879
+ return out;
1880
+ }
1881
+ function requiredProvenanceList(required) {
1882
+ if (Array.isArray(required)) return required.join(" | ");
1883
+ return String(required);
1884
+ }
1885
+ function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
1886
+ const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
1887
+ for (const edgeId of incident) {
1888
+ const e = graph.getEdgeAttributes(edgeId);
1889
+ if (e.type !== edgeType) continue;
1890
+ if (requiredOtherEnd === void 0) return true;
1891
+ if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
1892
+ }
1893
+ return false;
1894
+ }
1895
+ function blastRadiusSubjectReaching(graph, rule, nodeId) {
1896
+ let found = null;
1897
+ graph.forEachNode((subjId, attrs) => {
1898
+ if (found !== null) return;
1899
+ if (subjId === nodeId) return;
1900
+ if (attrs.type !== rule.nodeType) return;
1901
+ const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
1902
+ if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
1903
+ });
1904
+ return found;
1905
+ }
1906
+ function matchPolicyToNode(graph, policy, nodeId, node) {
1907
+ const rule = policy.rule;
1908
+ switch (rule.type) {
1909
+ case "structural": {
1910
+ if (node.type === rule.fromNodeType) {
1911
+ return {
1912
+ match: "subject",
1913
+ reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
1914
+ };
1915
+ }
1916
+ if (node.type === rule.toNodeType) {
1917
+ return {
1918
+ match: "region",
1919
+ reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
1920
+ };
1921
+ }
1922
+ return null;
1923
+ }
1924
+ case "ownership": {
1925
+ if (node.type === rule.nodeType) {
1926
+ return {
1927
+ match: "subject",
1928
+ reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
1929
+ };
1930
+ }
1931
+ return null;
1932
+ }
1933
+ case "blast-radius": {
1934
+ const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
1935
+ if (node.type === rule.nodeType) {
1936
+ return {
1937
+ match: "subject",
1938
+ reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
1939
+ };
1940
+ }
1941
+ const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
1942
+ if (subject) {
1943
+ return {
1944
+ match: "region",
1945
+ 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`
1946
+ };
1947
+ }
1948
+ return null;
1949
+ }
1950
+ case "compatibility": {
1951
+ const kindLabel = rule.kind ?? "all compat shapes";
1952
+ if (node.type === import_types2.NodeType.ServiceNode) {
1953
+ return {
1954
+ match: "subject",
1955
+ reason: `this service's dependencies are compatibility-checked (${kindLabel})`
1956
+ };
1957
+ }
1958
+ const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
1959
+ if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
1960
+ return {
1961
+ match: "region",
1962
+ reason: "services connecting to this database have their driver/engine compatibility checked against it"
1963
+ };
1964
+ }
1965
+ return null;
1966
+ }
1967
+ case "provenance": {
1968
+ const requiredList = requiredProvenanceList(rule.required);
1969
+ if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
1970
+ return {
1971
+ match: "subject",
1972
+ reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
1973
+ };
1974
+ }
1975
+ if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
1976
+ return {
1977
+ match: "region",
1978
+ 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`
1979
+ };
1980
+ }
1981
+ return null;
1982
+ }
1983
+ }
1984
+ }
1645
1985
  async function loadPolicyFile(policyPath) {
1646
1986
  let raw;
1647
1987
  try {
@@ -1750,9 +2090,9 @@ function loadIncidentThresholdsFromEnv() {
1750
2090
  return DEFAULT_INCIDENT_THRESHOLDS;
1751
2091
  }
1752
2092
  }
1753
- function httpResponseStatus(span) {
2093
+ function httpResponseStatusFromAttrs(attrs) {
1754
2094
  for (const key of ["http.response.status_code", "http.status_code"]) {
1755
- const v = span.attributes[key];
2095
+ const v = attrs[key];
1756
2096
  if (typeof v === "number" && Number.isFinite(v)) return v;
1757
2097
  if (typeof v === "string") {
1758
2098
  const n = Number(v);
@@ -1761,6 +2101,63 @@ function httpResponseStatus(span) {
1761
2101
  }
1762
2102
  return void 0;
1763
2103
  }
2104
+ function httpResponseStatus(span) {
2105
+ return httpResponseStatusFromAttrs(span.attributes);
2106
+ }
2107
+ function httpFailureMessageFromAttrs(attrs) {
2108
+ const status2 = httpResponseStatusFromAttrs(attrs);
2109
+ const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
2110
+ const method = pickAttrFrom(attrs, "http.request.method", "http.method");
2111
+ const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
2112
+ if (status2 !== void 0 && where) return `${status2} on ${where}`;
2113
+ if (status2 !== void 0) return `HTTP ${status2}`;
2114
+ if (where) return `error on ${where}`;
2115
+ return void 0;
2116
+ }
2117
+ var GRPC_STATUS_NAMES = {
2118
+ 1: "CANCELLED",
2119
+ 2: "UNKNOWN",
2120
+ 3: "INVALID_ARGUMENT",
2121
+ 4: "DEADLINE_EXCEEDED",
2122
+ 5: "NOT_FOUND",
2123
+ 6: "ALREADY_EXISTS",
2124
+ 7: "PERMISSION_DENIED",
2125
+ 8: "RESOURCE_EXHAUSTED",
2126
+ 9: "FAILED_PRECONDITION",
2127
+ 10: "ABORTED",
2128
+ 11: "OUT_OF_RANGE",
2129
+ 12: "UNIMPLEMENTED",
2130
+ 13: "INTERNAL",
2131
+ 14: "UNAVAILABLE",
2132
+ 15: "DATA_LOSS",
2133
+ 16: "UNAUTHENTICATED"
2134
+ };
2135
+ function grpcStatusCodeFromAttrs(attrs) {
2136
+ const v = attrs["rpc.grpc.status_code"];
2137
+ if (typeof v === "number" && Number.isFinite(v)) return v;
2138
+ if (typeof v === "string") {
2139
+ const n = Number(v);
2140
+ if (Number.isFinite(n)) return n;
2141
+ }
2142
+ return void 0;
2143
+ }
2144
+ function nonHttpFailureMessageFromAttrs(attrs) {
2145
+ const grpc2 = grpcStatusCodeFromAttrs(attrs);
2146
+ if (grpc2 !== void 0 && grpc2 !== 0) {
2147
+ const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
2148
+ const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
2149
+ return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
2150
+ }
2151
+ const errType = pickAttrFrom(attrs, "error.type");
2152
+ if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
2153
+ const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
2154
+ return peer ? `${errType} connecting to ${peer}` : errType;
2155
+ }
2156
+ return void 0;
2157
+ }
2158
+ function incidentMessage(span) {
2159
+ return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
2160
+ }
1764
2161
  function nowIso(ctx) {
1765
2162
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1766
2163
  }
@@ -1780,13 +2177,16 @@ function warnNoSourceMaps(serviceName) {
1780
2177
  `[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.`
1781
2178
  );
1782
2179
  }
1783
- function pickAttr(span, ...keys) {
2180
+ function pickAttrFrom(attrs, ...keys) {
1784
2181
  for (const k of keys) {
1785
- const v = span.attributes[k];
2182
+ const v = attrs[k];
1786
2183
  if (typeof v === "string" && v.length > 0) return v;
1787
2184
  }
1788
2185
  return void 0;
1789
2186
  }
2187
+ function pickAttr(span, ...keys) {
2188
+ return pickAttrFrom(span.attributes, ...keys);
2189
+ }
1790
2190
  function hostFromUrl(u) {
1791
2191
  if (!u) return void 0;
1792
2192
  try {
@@ -1798,6 +2198,10 @@ function hostFromUrl(u) {
1798
2198
  function pickAddress(span) {
1799
2199
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1800
2200
  }
2201
+ function isLoopbackHost(host) {
2202
+ const h = host.toLowerCase();
2203
+ return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
2204
+ }
1801
2205
  var CODE_FILEPATH_ATTR = "code.filepath";
1802
2206
  var CODE_LINENO_ATTR = "code.lineno";
1803
2207
  var CODE_FUNCTION_ATTR = "code.function";
@@ -1905,15 +2309,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
1905
2309
  ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1906
2310
  };
1907
2311
  }
2312
+ function reconcileObservedRelPath(graph, serviceName, relPath) {
2313
+ if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
2314
+ let best = null;
2315
+ graph.forEachNode((_id, attrs) => {
2316
+ const a = attrs;
2317
+ if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
2318
+ if (a.discoveredVia === "otel") return;
2319
+ const p = a.path;
2320
+ if (!p) return;
2321
+ if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
2322
+ best = p;
2323
+ }
2324
+ });
2325
+ return best ?? relPath;
2326
+ }
1908
2327
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1909
- const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
2328
+ const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
2329
+ const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
1910
2330
  if (!graph.hasNode(fileNodeId)) {
1911
- const language = languageForExt(callSite.relPath);
2331
+ const language = languageForExt(relPath);
1912
2332
  const node = {
1913
2333
  id: fileNodeId,
1914
2334
  type: import_types3.NodeType.FileNode,
1915
2335
  service: serviceName,
1916
- path: callSite.relPath,
2336
+ path: relPath,
1917
2337
  ...language ? { language } : {},
1918
2338
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1919
2339
  discoveredVia: "otel"
@@ -1941,6 +2361,11 @@ function makeInferredEdgeId(type, source, target) {
1941
2361
  }
1942
2362
  var INFERRED_CONFIDENCE = 0.6;
1943
2363
  var STITCH_MAX_DEPTH = 2;
2364
+ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2365
+ import_types3.EdgeType.CALLS,
2366
+ import_types3.EdgeType.CONNECTS_TO,
2367
+ import_types3.EdgeType.DEPENDS_ON
2368
+ ]);
1944
2369
  var WIRE_SPAN_KIND_CLIENT = 3;
1945
2370
  var WIRE_SPAN_KIND_PRODUCER = 4;
1946
2371
  function spanMintsObservedEdge(kind) {
@@ -2114,6 +2539,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
2114
2539
  for (const edgeId of outbound) {
2115
2540
  const edge = graph.getEdgeAttributes(edgeId);
2116
2541
  if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
2542
+ if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
2117
2543
  if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
2118
2544
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
2119
2545
  if (!visited.has(edge.target)) {
@@ -2146,6 +2572,16 @@ async function appendErrorEvent(ctx, ev) {
2146
2572
  await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
2147
2573
  await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
2148
2574
  }
2575
+ function incidentAffectedNode(span, graph, scanPath) {
2576
+ const sid = (0, import_types3.serviceId)(span.service, span.env);
2577
+ const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
2578
+ const callSite = callSiteFromSpan(span, serviceNode, scanPath);
2579
+ if (callSite) {
2580
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
2581
+ return (0, import_types3.fileId)(span.service, relPath);
2582
+ }
2583
+ return sid;
2584
+ }
2149
2585
  function sanitizeAttributes(attrs) {
2150
2586
  const out = {};
2151
2587
  for (const [k, v] of Object.entries(attrs)) {
@@ -2154,7 +2590,7 @@ function sanitizeAttributes(attrs) {
2154
2590
  }
2155
2591
  return out;
2156
2592
  }
2157
- function buildErrorEventForReceiver(span) {
2593
+ function buildErrorEventForReceiver(span, graph, scanPath) {
2158
2594
  if (span.statusCode !== 2) return null;
2159
2595
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
2160
2596
  const attrs = sanitizeAttributes(span.attributes);
@@ -2164,16 +2600,16 @@ function buildErrorEventForReceiver(span) {
2164
2600
  service: span.service,
2165
2601
  traceId: span.traceId,
2166
2602
  spanId: span.spanId,
2167
- errorMessage: span.exception?.message ?? "unknown error",
2603
+ errorMessage: incidentMessage(span),
2168
2604
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2169
2605
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2170
2606
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2171
- affectedNode: (0, import_types3.serviceId)(span.service, span.env)
2607
+ affectedNode: incidentAffectedNode(span, graph, scanPath)
2172
2608
  };
2173
2609
  }
2174
- function makeErrorSpanWriter(errorsPath) {
2610
+ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
2175
2611
  return async (span) => {
2176
- const ev = buildErrorEventForReceiver(span);
2612
+ const ev = buildErrorEventForReceiver(span, graph, scanPath);
2177
2613
  if (!ev) return;
2178
2614
  await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(errorsPath), { recursive: true });
2179
2615
  await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
@@ -2201,6 +2637,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
2201
2637
  };
2202
2638
  await appendErrorEvent(ctx, ev);
2203
2639
  }
2640
+ async function recordExceptionIncident(ctx, span, ts) {
2641
+ const attrs = sanitizeAttributes(span.attributes);
2642
+ const ev = {
2643
+ id: `${span.traceId}:${span.spanId}`,
2644
+ timestamp: ts,
2645
+ service: span.service,
2646
+ traceId: span.traceId,
2647
+ spanId: span.spanId,
2648
+ errorMessage: incidentMessage(span),
2649
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2650
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2651
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2652
+ affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
2653
+ };
2654
+ await appendErrorEvent(ctx, ev);
2655
+ }
2204
2656
  async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2205
2657
  const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2206
2658
  if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
@@ -2257,7 +2709,10 @@ async function handleSpan(ctx, span) {
2257
2709
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2258
2710
  cacheSpanService(span, nowMs, callSite);
2259
2711
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2260
- const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2712
+ const callSiteEvidence = callSite ? {
2713
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2714
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2715
+ } : void 0;
2261
2716
  let affectedNode = sourceId;
2262
2717
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2263
2718
  if (span.dbSystem) {
@@ -2279,7 +2734,7 @@ async function handleSpan(ctx, span) {
2279
2734
  } else {
2280
2735
  const host = pickAddress(span);
2281
2736
  let resolvedViaAddress = false;
2282
- if (mintsFromCallerSide && host && host !== span.service) {
2737
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
2283
2738
  const targetId = resolveServiceId(ctx.graph, host, env);
2284
2739
  if (targetId && targetId !== sourceId) {
2285
2740
  upsertObservedEdge(
@@ -2314,7 +2769,11 @@ async function handleSpan(ctx, span) {
2314
2769
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2315
2770
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2316
2771
  const fallbackEvidence = parent.callSite ? {
2317
- file: parent.callSite.relPath,
2772
+ file: reconcileObservedRelPath(
2773
+ ctx.graph,
2774
+ parent.service,
2775
+ parent.callSite.relPath
2776
+ ),
2318
2777
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2319
2778
  } : void 0;
2320
2779
  upsertObservedEdge(
@@ -2339,7 +2798,7 @@ async function handleSpan(ctx, span) {
2339
2798
  service: span.service,
2340
2799
  traceId: span.traceId,
2341
2800
  spanId: span.spanId,
2342
- errorMessage: span.exception?.message ?? "unknown error",
2801
+ errorMessage: incidentMessage(span),
2343
2802
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2344
2803
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2345
2804
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -2350,7 +2809,9 @@ async function handleSpan(ctx, span) {
2350
2809
  }
2351
2810
  if (span.statusCode !== 2) {
2352
2811
  const status2 = httpResponseStatus(span);
2353
- if (status2 !== void 0 && status2 >= 500) {
2812
+ if (span.exception) {
2813
+ await recordExceptionIncident(ctx, span, ts);
2814
+ } else if (status2 !== void 0 && status2 >= 500) {
2354
2815
  await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2355
2816
  } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2356
2817
  await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
@@ -2515,12 +2976,43 @@ function startStalenessLoop(graph, options = {}) {
2515
2976
  async function readErrorEvents(errorsPath) {
2516
2977
  try {
2517
2978
  const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
2518
- return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2979
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2980
+ return dedupeIncidents(events);
2519
2981
  } catch (err) {
2520
2982
  if (err.code === "ENOENT") return [];
2521
2983
  throw err;
2522
2984
  }
2523
2985
  }
2986
+ function isSynthesizedHttpIncident(ev) {
2987
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2988
+ if (ev.errorType) return false;
2989
+ if (!ev.attributes) return false;
2990
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2991
+ return synth !== void 0 && synth === ev.errorMessage;
2992
+ }
2993
+ function dedupeIncidents(events) {
2994
+ const seen = /* @__PURE__ */ new Set();
2995
+ const once = [];
2996
+ for (const ev of events) {
2997
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2998
+ if (key === void 0) {
2999
+ once.push(ev);
3000
+ continue;
3001
+ }
3002
+ if (seen.has(key)) continue;
3003
+ seen.add(key);
3004
+ once.push(ev);
3005
+ }
3006
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
3007
+ const hasRealFailure = /* @__PURE__ */ new Set();
3008
+ for (const ev of once) {
3009
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
3010
+ }
3011
+ return once.filter((ev) => {
3012
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
3013
+ return !hasRealFailure.has(groupKey(ev));
3014
+ });
3015
+ }
2524
3016
  function mergeSnapshot(graph, snapshot) {
2525
3017
  const exported = snapshot.graph;
2526
3018
  let nodesAdded = 0;
@@ -2601,10 +3093,17 @@ function isConfigFile(name) {
2601
3093
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2602
3094
  if (name === ".env" || name.startsWith(".env.")) {
2603
3095
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
3096
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2604
3097
  return { match: true, fileType: "env" };
2605
3098
  }
2606
3099
  return { match: false, fileType: "" };
2607
3100
  }
3101
+ function isNeatAuthoredEnvFile(name) {
3102
+ return name === ".env.neat";
3103
+ }
3104
+ function isNeatAuthoredSourceFile(name) {
3105
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
3106
+ }
2608
3107
  function isTestPath(filePath) {
2609
3108
  const normalised = filePath.replace(/\\/g, "/");
2610
3109
  const segments = normalised.split("/");
@@ -3298,7 +3797,9 @@ async function walkSourceFiles(dir) {
3298
3797
  if (IGNORED_DIRS.has(entry.name)) continue;
3299
3798
  if (await isPythonVenvDir(full)) continue;
3300
3799
  await walk(full);
3301
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name))) {
3800
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3801
+ // would attribute our instrumentation imports to the user's service.
3802
+ !isNeatAuthoredSourceFile(entry.name)) {
3302
3803
  out.push(full);
3303
3804
  }
3304
3805
  }
@@ -3468,11 +3969,28 @@ function collectJsImports(node, out) {
3468
3969
  if (child) collectJsImports(child, out);
3469
3970
  }
3470
3971
  }
3972
+ function collectImportedNames(node, out) {
3973
+ if (node.type === "aliased_import") {
3974
+ const nameNode = node.childForFieldName("name");
3975
+ if (nameNode) out.push(nameNode.text);
3976
+ return;
3977
+ }
3978
+ if (node.type === "dotted_name") {
3979
+ out.push(node.text);
3980
+ return;
3981
+ }
3982
+ for (let i = 0; i < node.namedChildCount; i++) {
3983
+ const child = node.namedChild(i);
3984
+ if (child) collectImportedNames(child, out);
3985
+ }
3986
+ }
3471
3987
  function collectPyImports(node, out) {
3472
3988
  if (node.type === "import_from_statement") {
3473
3989
  let level = 0;
3474
3990
  let modulePath = "";
3991
+ const names = [];
3475
3992
  let pastFrom = false;
3993
+ let pastImport = false;
3476
3994
  for (let i = 0; i < node.childCount; i++) {
3477
3995
  const child = node.child(i);
3478
3996
  if (!child) continue;
@@ -3480,26 +3998,30 @@ function collectPyImports(node, out) {
3480
3998
  if (child.type === "from") pastFrom = true;
3481
3999
  continue;
3482
4000
  }
3483
- if (child.type === "import") break;
3484
- if (child.type === "relative_import") {
3485
- for (let j = 0; j < child.childCount; j++) {
3486
- const rc = child.child(j);
3487
- if (!rc) continue;
3488
- if (rc.type === "import_prefix") {
3489
- for (let k = 0; k < rc.childCount; k++) {
3490
- if (rc.child(k)?.type === ".") level++;
3491
- }
3492
- } else if (rc.type === "dotted_name") modulePath = rc.text;
4001
+ if (!pastImport) {
4002
+ if (child.type === "import") {
4003
+ pastImport = true;
4004
+ continue;
3493
4005
  }
3494
- break;
3495
- }
3496
- if (child.type === "dotted_name") {
3497
- modulePath = child.text;
3498
- break;
4006
+ if (child.type === "relative_import") {
4007
+ for (let j = 0; j < child.childCount; j++) {
4008
+ const rc = child.child(j);
4009
+ if (!rc) continue;
4010
+ if (rc.type === "import_prefix") {
4011
+ for (let k = 0; k < rc.childCount; k++) {
4012
+ if (rc.child(k)?.type === ".") level++;
4013
+ }
4014
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
4015
+ }
4016
+ } else if (child.type === "dotted_name") {
4017
+ modulePath = child.text;
4018
+ }
4019
+ continue;
3499
4020
  }
4021
+ collectImportedNames(child, names);
3500
4022
  }
3501
4023
  if (level > 0 || modulePath) {
3502
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
4024
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3503
4025
  }
3504
4026
  }
3505
4027
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3601,8 +4123,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3601
4123
  return null;
3602
4124
  }
3603
4125
  async function resolvePyImport(imp, importerPath, serviceDir) {
3604
- if (!imp.modulePath) return null;
3605
- const relPath = imp.modulePath.split(".").join("/");
3606
4126
  let baseDir;
3607
4127
  if (imp.level > 0) {
3608
4128
  baseDir = import_node_path12.default.dirname(importerPath);
@@ -3610,13 +4130,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
3610
4130
  } else {
3611
4131
  baseDir = serviceDir;
3612
4132
  }
3613
- const candidates = [import_node_path12.default.join(baseDir, `${relPath}.py`), import_node_path12.default.join(baseDir, relPath, "__init__.py")];
3614
- for (const candidate of candidates) {
3615
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3616
- return toPosix2(import_node_path12.default.relative(serviceDir, candidate));
4133
+ const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
4134
+ const resolved = /* @__PURE__ */ new Set();
4135
+ let needModuleFile = imp.names.length === 0;
4136
+ for (const name of imp.names) {
4137
+ const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
4138
+ const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
4139
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
4140
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
4141
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
4142
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
4143
+ } else {
4144
+ needModuleFile = true;
4145
+ }
4146
+ }
4147
+ if (needModuleFile) {
4148
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
4149
+ for (const candidate of moduleFileCandidates) {
4150
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
4151
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
4152
+ break;
4153
+ }
3617
4154
  }
3618
4155
  }
3619
- return null;
4156
+ return [...resolved];
3620
4157
  }
3621
4158
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3622
4159
  const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
@@ -3657,17 +4194,18 @@ async function addImports(graph, services) {
3657
4194
  continue;
3658
4195
  }
3659
4196
  for (const imp of pyImports) {
3660
- const resolved = await resolvePyImport(imp, file.path, service.dir);
3661
- if (!resolved) continue;
3662
- edgesAdded += emitImportEdge(
3663
- graph,
3664
- service.pkg.name,
3665
- importerFileId,
3666
- relFile,
3667
- resolved,
3668
- imp.line,
3669
- imp.snippet
3670
- );
4197
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
4198
+ for (const resolved of resolvedPaths) {
4199
+ edgesAdded += emitImportEdge(
4200
+ graph,
4201
+ service.pkg.name,
4202
+ importerFileId,
4203
+ relFile,
4204
+ resolved,
4205
+ imp.line,
4206
+ imp.snippet
4207
+ );
4208
+ }
3671
4209
  }
3672
4210
  continue;
3673
4211
  }
@@ -4303,6 +4841,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4303
4841
  edgesAdded++;
4304
4842
  }
4305
4843
  }
4844
+ if (allConfigs.length === 1) {
4845
+ const primary = allConfigs[0];
4846
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
4847
+ const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
4848
+ const cfgId = (0, import_types9.configId)(relPath);
4849
+ if (!graph.hasNode(cfgId)) {
4850
+ const cfgNode = {
4851
+ id: cfgId,
4852
+ type: import_types9.NodeType.ConfigNode,
4853
+ name: import_node_path20.default.basename(primary.sourceFile),
4854
+ path: relPath,
4855
+ fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
4856
+ };
4857
+ graph.addNode(cfgId, cfgNode);
4858
+ nodesAdded++;
4859
+ }
4860
+ const cfgEdge = {
4861
+ id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
4862
+ source: service.node.id,
4863
+ target: cfgId,
4864
+ type: import_types9.EdgeType.CONFIGURED_BY,
4865
+ provenance: import_types9.Provenance.EXTRACTED,
4866
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
4867
+ evidence: { file: toPosix2(relPath) }
4868
+ };
4869
+ if (!graph.hasEdge(cfgEdge.id)) {
4870
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
4871
+ edgesAdded++;
4872
+ }
4873
+ }
4306
4874
  attachIncompatibilities(service, allConfigs);
4307
4875
  if (graph.hasNode(service.node.id)) {
4308
4876
  const current = graph.getNodeAttributes(service.node.id);
@@ -4314,6 +4882,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4314
4882
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
4315
4883
  delete updated.incompatibilities;
4316
4884
  }
4885
+ if (!service.node.dbConnectionTarget) {
4886
+ delete updated.dbConnectionTarget;
4887
+ }
4317
4888
  graph.replaceNodeAttributes(service.node.id, updated);
4318
4889
  }
4319
4890
  }
@@ -4491,7 +5062,7 @@ async function addHttpCallEdges(graph, services) {
4491
5062
  const dedupKey = `${relFile}|${targetId}`;
4492
5063
  if (seen.has(dedupKey)) continue;
4493
5064
  seen.add(dedupKey);
4494
- const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
5065
+ const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
4495
5066
  const ev = {
4496
5067
  file: relFile,
4497
5068
  line: site.line,
@@ -4511,7 +5082,7 @@ async function addHttpCallEdges(graph, services) {
4511
5082
  target: targetId,
4512
5083
  type: import_types11.EdgeType.CALLS,
4513
5084
  confidence,
4514
- confidenceKind: "hostname-shape-match",
5085
+ confidenceKind: "url-literal-service-target",
4515
5086
  evidence: ev
4516
5087
  });
4517
5088
  continue;
@@ -5017,19 +5588,29 @@ init_cjs_shims();
5017
5588
  var import_node_path29 = __toESM(require("path"), 1);
5018
5589
  var import_node_fs15 = require("fs");
5019
5590
  var import_types20 = require("@neat.is/types");
5020
- function runtimeImage(content) {
5021
- const lines = content.split("\n");
5022
- let last = null;
5023
- for (const raw of lines) {
5591
+ function readDockerfile(content) {
5592
+ let image = null;
5593
+ const ports = [];
5594
+ let cmd = null;
5595
+ let entrypoint = null;
5596
+ for (const raw of content.split("\n")) {
5024
5597
  const line = raw.trim();
5025
5598
  if (!line || line.startsWith("#")) continue;
5026
- if (!/^from\s+/i.test(line)) continue;
5027
- const tokens = line.split(/\s+/);
5028
- const image = tokens[1];
5029
- if (!image || image.toLowerCase() === "scratch") continue;
5030
- last = image;
5599
+ if (/^from\s+/i.test(line)) {
5600
+ const candidate = line.split(/\s+/)[1];
5601
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
5602
+ } else if (/^expose\s+/i.test(line)) {
5603
+ for (const token of line.split(/\s+/).slice(1)) {
5604
+ const port = Number.parseInt(token.split("/")[0], 10);
5605
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
5606
+ }
5607
+ } else if (/^entrypoint\s+/i.test(line)) {
5608
+ entrypoint = line;
5609
+ } else if (/^cmd\s+/i.test(line)) {
5610
+ cmd = line;
5611
+ }
5031
5612
  }
5032
- return last;
5613
+ return { image, ports, entrypoint: entrypoint ?? cmd };
5033
5614
  }
5034
5615
  async function addDockerfileRuntimes(graph, services, scanPath) {
5035
5616
  let nodesAdded = 0;
@@ -5048,14 +5629,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5048
5629
  );
5049
5630
  continue;
5050
5631
  }
5051
- const image = runtimeImage(content);
5052
- if (!image) continue;
5053
- const node = makeInfraNode("container-image", image);
5632
+ const facts = readDockerfile(content);
5633
+ if (!facts.image) continue;
5634
+ const node = makeInfraNode("container-image", facts.image);
5054
5635
  if (!graph.hasNode(node.id)) {
5055
5636
  graph.addNode(node.id, node);
5056
5637
  nodesAdded++;
5057
5638
  }
5058
5639
  const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5640
+ const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
5059
5641
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5060
5642
  graph,
5061
5643
  service.pkg.name,
@@ -5074,12 +5656,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5074
5656
  provenance: import_types20.Provenance.EXTRACTED,
5075
5657
  confidence: (0, import_types20.confidenceForExtracted)("structural"),
5076
5658
  evidence: {
5077
- file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
5659
+ file: evidenceFile,
5660
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
5078
5661
  }
5079
5662
  };
5080
5663
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5081
5664
  edgesAdded++;
5082
5665
  }
5666
+ for (const port of facts.ports) {
5667
+ const portNode = makeInfraNode("port", String(port));
5668
+ if (!graph.hasNode(portNode.id)) {
5669
+ graph.addNode(portNode.id, portNode);
5670
+ nodesAdded++;
5671
+ }
5672
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
5673
+ if (graph.hasEdge(portEdgeId)) continue;
5674
+ const portEdge = {
5675
+ id: portEdgeId,
5676
+ source: fileNodeId,
5677
+ target: portNode.id,
5678
+ type: import_types20.EdgeType.CONNECTS_TO,
5679
+ provenance: import_types20.Provenance.EXTRACTED,
5680
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
5681
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5682
+ };
5683
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
5684
+ edgesAdded++;
5685
+ }
5083
5686
  }
5084
5687
  return { nodesAdded, edgesAdded };
5085
5688
  }
@@ -5088,7 +5691,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5088
5691
  init_cjs_shims();
5089
5692
  var import_node_fs16 = require("fs");
5090
5693
  var import_node_path30 = __toESM(require("path"), 1);
5694
+ var import_types21 = require("@neat.is/types");
5091
5695
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5696
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5092
5697
  async function walkTfFiles(start, depth = 0, max = 5) {
5093
5698
  if (depth > max) return [];
5094
5699
  const out = [];
@@ -5105,24 +5710,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
5105
5710
  }
5106
5711
  return out;
5107
5712
  }
5713
+ function blockBody(content, from) {
5714
+ const open = content.indexOf("{", from);
5715
+ if (open === -1) return null;
5716
+ let depth = 0;
5717
+ for (let i = open; i < content.length; i++) {
5718
+ const ch = content[i];
5719
+ if (ch === "{") depth++;
5720
+ else if (ch === "}") {
5721
+ depth--;
5722
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
5723
+ }
5724
+ }
5725
+ return null;
5726
+ }
5727
+ function lineAt(content, index) {
5728
+ let line = 1;
5729
+ for (let i = 0; i < index && i < content.length; i++) {
5730
+ if (content[i] === "\n") line++;
5731
+ }
5732
+ return line;
5733
+ }
5108
5734
  async function addTerraformResources(graph, scanPath) {
5109
5735
  let nodesAdded = 0;
5736
+ let edgesAdded = 0;
5110
5737
  const files = await walkTfFiles(scanPath);
5111
5738
  for (const file of files) {
5112
5739
  const content = await import_node_fs16.promises.readFile(file, "utf8");
5740
+ const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
5741
+ const resources = [];
5742
+ const byKey = /* @__PURE__ */ new Map();
5113
5743
  RESOURCE_RE.lastIndex = 0;
5114
5744
  let m;
5115
5745
  while ((m = RESOURCE_RE.exec(content)) !== null) {
5116
- const kind = m[1];
5746
+ const type = m[1];
5117
5747
  const name = m[2];
5118
- const node = makeInfraNode(kind, name, "aws");
5748
+ const node = makeInfraNode(type, name, "aws");
5119
5749
  if (!graph.hasNode(node.id)) {
5120
5750
  graph.addNode(node.id, node);
5121
5751
  nodesAdded++;
5122
5752
  }
5753
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
5754
+ const resource = {
5755
+ type,
5756
+ name,
5757
+ nodeId: node.id,
5758
+ body: span?.body ?? "",
5759
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
5760
+ };
5761
+ resources.push(resource);
5762
+ byKey.set(`${type}.${name}`, resource);
5763
+ }
5764
+ for (const resource of resources) {
5765
+ const seen = /* @__PURE__ */ new Set();
5766
+ REFERENCE_RE.lastIndex = 0;
5767
+ let ref;
5768
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
5769
+ const key = `${ref[1]}.${ref[2]}`;
5770
+ if (key === `${resource.type}.${resource.name}`) continue;
5771
+ const target = byKey.get(key);
5772
+ if (!target) continue;
5773
+ if (seen.has(target.nodeId)) continue;
5774
+ seen.add(target.nodeId);
5775
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
5776
+ if (graph.hasEdge(edgeId)) continue;
5777
+ const line = lineAt(content, resource.bodyOffset + ref.index);
5778
+ const edge = {
5779
+ id: edgeId,
5780
+ source: resource.nodeId,
5781
+ target: target.nodeId,
5782
+ type: import_types21.EdgeType.DEPENDS_ON,
5783
+ provenance: import_types21.Provenance.EXTRACTED,
5784
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
5785
+ evidence: { file: evidenceFile, line, snippet: key }
5786
+ };
5787
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5788
+ edgesAdded++;
5789
+ }
5123
5790
  }
5124
5791
  }
5125
- return { nodesAdded, edgesAdded: 0 };
5792
+ return { nodesAdded, edgesAdded };
5126
5793
  }
5127
5794
 
5128
5795
  // src/extract/infra/k8s.ts
@@ -5200,11 +5867,11 @@ var import_node_path33 = __toESM(require("path"), 1);
5200
5867
  init_cjs_shims();
5201
5868
  var import_node_fs18 = require("fs");
5202
5869
  var import_node_path32 = __toESM(require("path"), 1);
5203
- var import_types21 = require("@neat.is/types");
5870
+ var import_types22 = require("@neat.is/types");
5204
5871
  function dropOrphanedFileNodes(graph) {
5205
5872
  const orphans = [];
5206
5873
  graph.forEachNode((id, attrs) => {
5207
- if (attrs.type !== import_types21.NodeType.FileNode) return;
5874
+ if (attrs.type !== import_types22.NodeType.FileNode) return;
5208
5875
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5209
5876
  orphans.push(id);
5210
5877
  }
@@ -5217,7 +5884,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5217
5884
  const bases = [scanPath, ...serviceDirs];
5218
5885
  graph.forEachEdge((id, attrs) => {
5219
5886
  const edge = attrs;
5220
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5887
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5221
5888
  const evidenceFile = edge.evidence?.file;
5222
5889
  if (!evidenceFile) return;
5223
5890
  if (import_node_path32.default.isAbsolute(evidenceFile)) {
@@ -5300,7 +5967,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5300
5967
  init_cjs_shims();
5301
5968
  var import_node_fs19 = require("fs");
5302
5969
  var import_node_path34 = __toESM(require("path"), 1);
5303
- var import_types22 = require("@neat.is/types");
5970
+ var import_types23 = require("@neat.is/types");
5304
5971
  var SCHEMA_VERSION = 4;
5305
5972
  function migrateV1ToV2(payload) {
5306
5973
  const nodes = payload.graph.nodes;
@@ -5322,12 +5989,12 @@ function migrateV2ToV3(payload) {
5322
5989
  for (const edge of edges) {
5323
5990
  const attrs = edge.attributes;
5324
5991
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5325
- attrs.provenance = import_types22.Provenance.OBSERVED;
5992
+ attrs.provenance = import_types23.Provenance.OBSERVED;
5326
5993
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5327
5994
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5328
5995
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5329
5996
  if (type && source && target) {
5330
- const newId = (0, import_types22.observedEdgeId)(source, target, type);
5997
+ const newId = (0, import_types23.observedEdgeId)(source, target, type);
5331
5998
  attrs.id = newId;
5332
5999
  if (edge.key) edge.key = newId;
5333
6000
  }
@@ -5419,7 +6086,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
5419
6086
  init_cjs_shims();
5420
6087
  var import_fastify = __toESM(require("fastify"), 1);
5421
6088
  var import_cors = __toESM(require("@fastify/cors"), 1);
5422
- var import_types25 = require("@neat.is/types");
6089
+ var import_types26 = require("@neat.is/types");
5423
6090
 
5424
6091
  // src/extend/index.ts
5425
6092
  init_cjs_shims();
@@ -5738,7 +6405,7 @@ async function rollbackExtension(ctx, args) {
5738
6405
 
5739
6406
  // src/divergences.ts
5740
6407
  init_cjs_shims();
5741
- var import_types23 = require("@neat.is/types");
6408
+ var import_types24 = require("@neat.is/types");
5742
6409
  function bucketKey(source, target, type) {
5743
6410
  return `${type}|${source}|${target}`;
5744
6411
  }
@@ -5746,22 +6413,22 @@ function bucketEdges(graph) {
5746
6413
  const buckets = /* @__PURE__ */ new Map();
5747
6414
  graph.forEachEdge((id, attrs) => {
5748
6415
  const e = attrs;
5749
- const parsed = (0, import_types23.parseEdgeId)(id);
6416
+ const parsed = (0, import_types24.parseEdgeId)(id);
5750
6417
  const provenance = parsed?.provenance ?? e.provenance;
5751
6418
  const key = bucketKey(e.source, e.target, e.type);
5752
6419
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5753
6420
  switch (provenance) {
5754
- case import_types23.Provenance.EXTRACTED:
6421
+ case import_types24.Provenance.EXTRACTED:
5755
6422
  cur.extracted = e;
5756
6423
  break;
5757
- case import_types23.Provenance.OBSERVED:
6424
+ case import_types24.Provenance.OBSERVED:
5758
6425
  cur.observed = e;
5759
6426
  break;
5760
- case import_types23.Provenance.INFERRED:
6427
+ case import_types24.Provenance.INFERRED:
5761
6428
  cur.inferred = e;
5762
6429
  break;
5763
6430
  default:
5764
- if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
6431
+ if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
5765
6432
  }
5766
6433
  buckets.set(key, cur);
5767
6434
  });
@@ -5770,7 +6437,7 @@ function bucketEdges(graph) {
5770
6437
  function nodeIsFrontier(graph, nodeId) {
5771
6438
  if (!graph.hasNode(nodeId)) return false;
5772
6439
  const attrs = graph.getNodeAttributes(nodeId);
5773
- return attrs.type === import_types23.NodeType.FrontierNode;
6440
+ return attrs.type === import_types24.NodeType.FrontierNode;
5774
6441
  }
5775
6442
  function clampConfidence(n) {
5776
6443
  if (!Number.isFinite(n)) return 0;
@@ -5789,10 +6456,16 @@ function gradedConfidence(edge) {
5789
6456
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
5790
6457
  return clampConfidence(confidenceForEdge(edge));
5791
6458
  }
6459
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6460
+ import_types24.EdgeType.CALLS,
6461
+ import_types24.EdgeType.CONNECTS_TO,
6462
+ import_types24.EdgeType.PUBLISHES_TO,
6463
+ import_types24.EdgeType.CONSUMES_FROM
6464
+ ]);
5792
6465
  function detectMissingDivergences(graph, bucket) {
5793
6466
  const out = [];
5794
- if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5795
- if (bucket.extracted && !bucket.observed) {
6467
+ if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
6468
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5796
6469
  if (!nodeIsFrontier(graph, bucket.target)) {
5797
6470
  out.push({
5798
6471
  type: "missing-observed",
@@ -5832,7 +6505,7 @@ function declaredHostFor(svc) {
5832
6505
  function hasExtractedConfiguredBy(graph, svcId) {
5833
6506
  for (const edgeId of graph.outboundEdges(svcId)) {
5834
6507
  const e = graph.getEdgeAttributes(edgeId);
5835
- if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
6508
+ if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
5836
6509
  return true;
5837
6510
  }
5838
6511
  }
@@ -5845,10 +6518,10 @@ function detectHostMismatch(graph, svcId, svc) {
5845
6518
  const out = [];
5846
6519
  for (const edgeId of graph.outboundEdges(svcId)) {
5847
6520
  const edge = graph.getEdgeAttributes(edgeId);
5848
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5849
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6521
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6522
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5850
6523
  const target = graph.getNodeAttributes(edge.target);
5851
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6524
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5852
6525
  const observedHost = target.host?.trim();
5853
6526
  if (!observedHost) continue;
5854
6527
  if (observedHost === declaredHost) continue;
@@ -5870,10 +6543,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5870
6543
  const deps = svc.dependencies ?? {};
5871
6544
  for (const edgeId of graph.outboundEdges(svcId)) {
5872
6545
  const edge = graph.getEdgeAttributes(edgeId);
5873
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5874
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6546
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6547
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5875
6548
  const target = graph.getNodeAttributes(edge.target);
5876
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6549
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5877
6550
  for (const pair of compatPairs()) {
5878
6551
  if (pair.engine !== target.engine) continue;
5879
6552
  const declared = deps[pair.driver];
@@ -5926,6 +6599,23 @@ function detectCompatDivergences(graph, svcId, svc) {
5926
6599
  function involvesNode(d, nodeId) {
5927
6600
  return d.source === nodeId || d.target === nodeId;
5928
6601
  }
6602
+ function suppressHostMismatchHalves(all) {
6603
+ const observedHalf = /* @__PURE__ */ new Set();
6604
+ const declaredHalf = /* @__PURE__ */ new Set();
6605
+ for (const d of all) {
6606
+ if (d.type !== "host-mismatch") continue;
6607
+ observedHalf.add(`${d.source}->${d.target}`);
6608
+ declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
6609
+ }
6610
+ if (observedHalf.size === 0) return all;
6611
+ return all.filter((d) => {
6612
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
6613
+ return false;
6614
+ }
6615
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
6616
+ return true;
6617
+ });
6618
+ }
5929
6619
  function computeDivergences(graph, opts = {}) {
5930
6620
  const all = [];
5931
6621
  const buckets = bucketEdges(graph);
@@ -5934,12 +6624,13 @@ function computeDivergences(graph, opts = {}) {
5934
6624
  }
5935
6625
  graph.forEachNode((nodeId, attrs) => {
5936
6626
  const n = attrs;
5937
- if (n.type !== import_types23.NodeType.ServiceNode) return;
6627
+ if (n.type !== import_types24.NodeType.ServiceNode) return;
5938
6628
  const svc = n;
5939
6629
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5940
6630
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
5941
6631
  });
5942
- let filtered = all;
6632
+ const reconciled = suppressHostMismatchHalves(all);
6633
+ let filtered = reconciled;
5943
6634
  if (opts.type) {
5944
6635
  const allowed = opts.type;
5945
6636
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -5967,7 +6658,7 @@ function computeDivergences(graph, opts = {}) {
5967
6658
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5968
6659
  return a.target.localeCompare(b.target);
5969
6660
  });
5970
- return import_types23.DivergenceResultSchema.parse({
6661
+ return import_types24.DivergenceResultSchema.parse({
5971
6662
  divergences: filtered,
5972
6663
  totalAffected: filtered.length,
5973
6664
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6108,7 +6799,7 @@ init_cjs_shims();
6108
6799
  var import_node_fs23 = require("fs");
6109
6800
  var import_node_os3 = __toESM(require("os"), 1);
6110
6801
  var import_node_path38 = __toESM(require("path"), 1);
6111
- var import_types24 = require("@neat.is/types");
6802
+ var import_types25 = require("@neat.is/types");
6112
6803
  var LOCK_TIMEOUT_MS = 5e3;
6113
6804
  var LOCK_RETRY_MS = 50;
6114
6805
  function neatHome() {
@@ -6257,10 +6948,10 @@ async function readRegistry() {
6257
6948
  throw err;
6258
6949
  }
6259
6950
  const parsed = JSON.parse(raw);
6260
- return import_types24.RegistryFileSchema.parse(parsed);
6951
+ return import_types25.RegistryFileSchema.parse(parsed);
6261
6952
  }
6262
6953
  async function writeRegistry(reg) {
6263
- const validated = import_types24.RegistryFileSchema.parse(reg);
6954
+ const validated = import_types25.RegistryFileSchema.parse(reg);
6264
6955
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6265
6956
  }
6266
6957
  var ProjectNameCollisionError = class extends Error {
@@ -6581,6 +7272,18 @@ function registerRoutes(scope, ctx) {
6581
7272
  }
6582
7273
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6583
7274
  });
7275
+ scope.get(
7276
+ "/graph/observed-dependencies/:nodeId",
7277
+ async (req, reply) => {
7278
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7279
+ if (!proj) return;
7280
+ const { nodeId } = req.params;
7281
+ if (!proj.graph.hasNode(nodeId)) {
7282
+ return reply.code(404).send({ error: "node not found", id: nodeId });
7283
+ }
7284
+ return getObservedDependencies(proj.graph, nodeId);
7285
+ }
7286
+ );
6584
7287
  scope.get("/graph/divergences", async (req, reply) => {
6585
7288
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6586
7289
  if (!proj) return;
@@ -6589,11 +7292,11 @@ function registerRoutes(scope, ctx) {
6589
7292
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6590
7293
  const parsed = [];
6591
7294
  for (const c of candidates) {
6592
- const r = import_types25.DivergenceTypeSchema.safeParse(c);
7295
+ const r = import_types26.DivergenceTypeSchema.safeParse(c);
6593
7296
  if (!r.success) {
6594
7297
  return reply.code(400).send({
6595
7298
  error: `unknown divergence type "${c}"`,
6596
- allowed: import_types25.DivergenceTypeSchema.options
7299
+ allowed: import_types26.DivergenceTypeSchema.options
6597
7300
  });
6598
7301
  }
6599
7302
  parsed.push(r.data);
@@ -6641,23 +7344,29 @@ function registerRoutes(scope, ctx) {
6641
7344
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6642
7345
  return { count: sliced.length, total, events: sliced };
6643
7346
  });
7347
+ const incidentHistoryHandler = async (req, reply) => {
7348
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7349
+ if (!proj) return;
7350
+ const { nodeId } = req.params;
7351
+ if (!proj.graph.hasNode(nodeId)) {
7352
+ reply.code(404).send({ error: "node not found", id: nodeId });
7353
+ return;
7354
+ }
7355
+ const epath = errorsPathFor(proj);
7356
+ if (!epath) return { count: 0, total: 0, events: [] };
7357
+ const events = await readErrorEvents(epath);
7358
+ const filtered = events.filter(
7359
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
7360
+ );
7361
+ return { count: filtered.length, total: filtered.length, events: filtered };
7362
+ };
6644
7363
  scope.get(
6645
7364
  "/incidents/:nodeId",
6646
- async (req, reply) => {
6647
- const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6648
- if (!proj) return;
6649
- const { nodeId } = req.params;
6650
- if (!proj.graph.hasNode(nodeId)) {
6651
- return reply.code(404).send({ error: "node not found", id: nodeId });
6652
- }
6653
- const epath = errorsPathFor(proj);
6654
- if (!epath) return { count: 0, total: 0, events: [] };
6655
- const events = await readErrorEvents(epath);
6656
- const filtered = events.filter(
6657
- (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6658
- );
6659
- return { count: filtered.length, total: filtered.length, events: filtered };
6660
- }
7365
+ incidentHistoryHandler
7366
+ );
7367
+ scope.get(
7368
+ "/graph/incident-history/:nodeId",
7369
+ incidentHistoryHandler
6661
7370
  );
6662
7371
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6663
7372
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -6666,16 +7375,16 @@ function registerRoutes(scope, ctx) {
6666
7375
  if (!proj.graph.hasNode(nodeId)) {
6667
7376
  return reply.code(404).send({ error: "node not found", id: nodeId });
6668
7377
  }
6669
- let errorEvent;
6670
7378
  const epath = errorsPathFor(proj);
6671
- if (req.query.errorId && epath) {
6672
- const events = await readErrorEvents(epath);
6673
- errorEvent = events.find((e) => e.id === req.query.errorId);
7379
+ const incidents = epath ? await readErrorEvents(epath) : [];
7380
+ let errorEvent;
7381
+ if (req.query.errorId) {
7382
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
6674
7383
  if (!errorEvent) {
6675
7384
  return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
6676
7385
  }
6677
7386
  }
6678
- const result = getRootCause(proj.graph, nodeId, errorEvent);
7387
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6679
7388
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6680
7389
  return result;
6681
7390
  });
@@ -6806,7 +7515,7 @@ function registerRoutes(scope, ctx) {
6806
7515
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6807
7516
  let violations = await log.readAll();
6808
7517
  if (req.query.severity) {
6809
- const sev = import_types25.PolicySeveritySchema.safeParse(req.query.severity);
7518
+ const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
6810
7519
  if (!sev.success) {
6811
7520
  return reply.code(400).send({
6812
7521
  error: "invalid severity",
@@ -6820,10 +7529,32 @@ function registerRoutes(scope, ctx) {
6820
7529
  }
6821
7530
  return { violations };
6822
7531
  });
7532
+ scope.get("/policies/applicable", async (req, reply) => {
7533
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7534
+ if (!proj) return;
7535
+ const nodeId = req.query.node;
7536
+ if (!nodeId) {
7537
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7538
+ }
7539
+ const policyPath = ctx.policyFilePathFor(proj);
7540
+ let policies = [];
7541
+ if (policyPath) {
7542
+ try {
7543
+ policies = await loadPolicyFile(policyPath);
7544
+ } catch (err) {
7545
+ return reply.code(400).send({
7546
+ error: "policy.json failed to parse",
7547
+ details: err.message
7548
+ });
7549
+ }
7550
+ }
7551
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7552
+ return { node: nodeId, applicable };
7553
+ });
6823
7554
  scope.post("/policies/check", async (req, reply) => {
6824
7555
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6825
7556
  if (!proj) return;
6826
- const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7557
+ const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6827
7558
  if (!parsed.success) {
6828
7559
  return reply.code(400).send({
6829
7560
  error: "invalid /policies/check body",
@@ -7113,6 +7844,7 @@ function unroutedErrorsPath(neatHome2) {
7113
7844
  }
7114
7845
 
7115
7846
  // src/daemon.ts
7847
+ var import_types27 = require("@neat.is/types");
7116
7848
  function daemonJsonPath(scanPath) {
7117
7849
  return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
7118
7850
  }
@@ -7215,6 +7947,19 @@ function isTokenContained(needle, haystack) {
7215
7947
  const tokens = haystack.split(/[-_]/);
7216
7948
  return tokens.includes(needle);
7217
7949
  }
7950
+ function serviceNameMatchesProject(serviceName, project) {
7951
+ if (serviceName === project) return true;
7952
+ if (isTokenPrefix(project, serviceName)) return true;
7953
+ if (isTokenContained(project, serviceName)) return true;
7954
+ return false;
7955
+ }
7956
+ function spanBelongsToSingleProject(graph, project, serviceName) {
7957
+ if (!serviceName) return true;
7958
+ if (serviceNameMatchesProject(serviceName, project)) return true;
7959
+ return graph.someNode(
7960
+ (_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
7961
+ );
7962
+ }
7218
7963
  async function bootstrapProject(entry) {
7219
7964
  const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
7220
7965
  try {
@@ -7512,7 +8257,9 @@ async function startDaemon(opts = {}) {
7512
8257
  singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
7513
8258
  });
7514
8259
  restAddress = await restApp.listen({ port: restPort, host });
7515
- console.log(`neatd: REST listening on ${restAddress}`);
8260
+ console.log(
8261
+ `neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
8262
+ );
7516
8263
  } catch (err) {
7517
8264
  for (const slot of slots.values()) {
7518
8265
  teardownSlot(slot);
@@ -7543,6 +8290,10 @@ async function startDaemon(opts = {}) {
7543
8290
  warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
7544
8291
  return null;
7545
8292
  }
8293
+ if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
8294
+ await recordUnroutedSpan(serviceName, traceId);
8295
+ return null;
8296
+ }
7546
8297
  return slot2;
7547
8298
  }
7548
8299
  const liveEntries = await listProjects().catch(() => []);
@@ -7605,7 +8356,7 @@ async function startDaemon(opts = {}) {
7605
8356
  onErrorSpanSync: async (span) => {
7606
8357
  const slot = await resolveTargetSlot(span.service, span.traceId);
7607
8358
  if (!slot) return;
7608
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8359
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7609
8360
  },
7610
8361
  // Project-scoped route (issue #367) — the URL already named the
7611
8362
  // project. Resolution is a direct slot lookup; service.name resolves
@@ -7628,10 +8379,10 @@ async function startDaemon(opts = {}) {
7628
8379
  onProjectErrorSpanSync: async (project, span) => {
7629
8380
  const slot = await resolveSlotByName(project, span.service, span.traceId);
7630
8381
  if (!slot) return;
7631
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8382
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7632
8383
  }
7633
8384
  });
7634
- otlpAddress = await otlpApp.listen({ port: otlpPort, host });
8385
+ otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
7635
8386
  console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
7636
8387
  } catch (err) {
7637
8388
  for (const slot of slots.values()) {
@@ -7786,7 +8537,8 @@ async function startDaemon(opts = {}) {
7786
8537
  otlpAddress,
7787
8538
  bootstrap: tracker,
7788
8539
  initialBootstrap,
7789
- daemonRecord
8540
+ daemonRecord,
8541
+ neatHome: home
7790
8542
  };
7791
8543
  }
7792
8544
  // Annotate the CommonJS export names for ESM import in node: