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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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");
@@ -2257,7 +2693,10 @@ async function handleSpan(ctx, span) {
2257
2693
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2258
2694
  cacheSpanService(span, nowMs, callSite);
2259
2695
  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;
2696
+ const callSiteEvidence = callSite ? {
2697
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2698
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2699
+ } : void 0;
2261
2700
  let affectedNode = sourceId;
2262
2701
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2263
2702
  if (span.dbSystem) {
@@ -2279,7 +2718,7 @@ async function handleSpan(ctx, span) {
2279
2718
  } else {
2280
2719
  const host = pickAddress(span);
2281
2720
  let resolvedViaAddress = false;
2282
- if (mintsFromCallerSide && host && host !== span.service) {
2721
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
2283
2722
  const targetId = resolveServiceId(ctx.graph, host, env);
2284
2723
  if (targetId && targetId !== sourceId) {
2285
2724
  upsertObservedEdge(
@@ -2314,7 +2753,11 @@ async function handleSpan(ctx, span) {
2314
2753
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2315
2754
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2316
2755
  const fallbackEvidence = parent.callSite ? {
2317
- file: parent.callSite.relPath,
2756
+ file: reconcileObservedRelPath(
2757
+ ctx.graph,
2758
+ parent.service,
2759
+ parent.callSite.relPath
2760
+ ),
2318
2761
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2319
2762
  } : void 0;
2320
2763
  upsertObservedEdge(
@@ -2339,7 +2782,7 @@ async function handleSpan(ctx, span) {
2339
2782
  service: span.service,
2340
2783
  traceId: span.traceId,
2341
2784
  spanId: span.spanId,
2342
- errorMessage: span.exception?.message ?? "unknown error",
2785
+ errorMessage: incidentMessage(span),
2343
2786
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2344
2787
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2345
2788
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -2515,12 +2958,43 @@ function startStalenessLoop(graph, options = {}) {
2515
2958
  async function readErrorEvents(errorsPath) {
2516
2959
  try {
2517
2960
  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));
2961
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2962
+ return dedupeIncidents(events);
2519
2963
  } catch (err) {
2520
2964
  if (err.code === "ENOENT") return [];
2521
2965
  throw err;
2522
2966
  }
2523
2967
  }
2968
+ function isSynthesizedHttpIncident(ev) {
2969
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2970
+ if (ev.errorType) return false;
2971
+ if (!ev.attributes) return false;
2972
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2973
+ return synth !== void 0 && synth === ev.errorMessage;
2974
+ }
2975
+ function dedupeIncidents(events) {
2976
+ const seen = /* @__PURE__ */ new Set();
2977
+ const once = [];
2978
+ for (const ev of events) {
2979
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2980
+ if (key === void 0) {
2981
+ once.push(ev);
2982
+ continue;
2983
+ }
2984
+ if (seen.has(key)) continue;
2985
+ seen.add(key);
2986
+ once.push(ev);
2987
+ }
2988
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2989
+ const hasRealFailure = /* @__PURE__ */ new Set();
2990
+ for (const ev of once) {
2991
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2992
+ }
2993
+ return once.filter((ev) => {
2994
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
2995
+ return !hasRealFailure.has(groupKey(ev));
2996
+ });
2997
+ }
2524
2998
  function mergeSnapshot(graph, snapshot) {
2525
2999
  const exported = snapshot.graph;
2526
3000
  let nodesAdded = 0;
@@ -2601,10 +3075,17 @@ function isConfigFile(name) {
2601
3075
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2602
3076
  if (name === ".env" || name.startsWith(".env.")) {
2603
3077
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
3078
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2604
3079
  return { match: true, fileType: "env" };
2605
3080
  }
2606
3081
  return { match: false, fileType: "" };
2607
3082
  }
3083
+ function isNeatAuthoredEnvFile(name) {
3084
+ return name === ".env.neat";
3085
+ }
3086
+ function isNeatAuthoredSourceFile(name) {
3087
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
3088
+ }
2608
3089
  function isTestPath(filePath) {
2609
3090
  const normalised = filePath.replace(/\\/g, "/");
2610
3091
  const segments = normalised.split("/");
@@ -3298,7 +3779,9 @@ async function walkSourceFiles(dir) {
3298
3779
  if (IGNORED_DIRS.has(entry.name)) continue;
3299
3780
  if (await isPythonVenvDir(full)) continue;
3300
3781
  await walk(full);
3301
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name))) {
3782
+ } 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
3783
+ // would attribute our instrumentation imports to the user's service.
3784
+ !isNeatAuthoredSourceFile(entry.name)) {
3302
3785
  out.push(full);
3303
3786
  }
3304
3787
  }
@@ -3468,11 +3951,28 @@ function collectJsImports(node, out) {
3468
3951
  if (child) collectJsImports(child, out);
3469
3952
  }
3470
3953
  }
3954
+ function collectImportedNames(node, out) {
3955
+ if (node.type === "aliased_import") {
3956
+ const nameNode = node.childForFieldName("name");
3957
+ if (nameNode) out.push(nameNode.text);
3958
+ return;
3959
+ }
3960
+ if (node.type === "dotted_name") {
3961
+ out.push(node.text);
3962
+ return;
3963
+ }
3964
+ for (let i = 0; i < node.namedChildCount; i++) {
3965
+ const child = node.namedChild(i);
3966
+ if (child) collectImportedNames(child, out);
3967
+ }
3968
+ }
3471
3969
  function collectPyImports(node, out) {
3472
3970
  if (node.type === "import_from_statement") {
3473
3971
  let level = 0;
3474
3972
  let modulePath = "";
3973
+ const names = [];
3475
3974
  let pastFrom = false;
3975
+ let pastImport = false;
3476
3976
  for (let i = 0; i < node.childCount; i++) {
3477
3977
  const child = node.child(i);
3478
3978
  if (!child) continue;
@@ -3480,26 +3980,30 @@ function collectPyImports(node, out) {
3480
3980
  if (child.type === "from") pastFrom = true;
3481
3981
  continue;
3482
3982
  }
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;
3983
+ if (!pastImport) {
3984
+ if (child.type === "import") {
3985
+ pastImport = true;
3986
+ continue;
3493
3987
  }
3494
- break;
3495
- }
3496
- if (child.type === "dotted_name") {
3497
- modulePath = child.text;
3498
- break;
3988
+ if (child.type === "relative_import") {
3989
+ for (let j = 0; j < child.childCount; j++) {
3990
+ const rc = child.child(j);
3991
+ if (!rc) continue;
3992
+ if (rc.type === "import_prefix") {
3993
+ for (let k = 0; k < rc.childCount; k++) {
3994
+ if (rc.child(k)?.type === ".") level++;
3995
+ }
3996
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3997
+ }
3998
+ } else if (child.type === "dotted_name") {
3999
+ modulePath = child.text;
4000
+ }
4001
+ continue;
3499
4002
  }
4003
+ collectImportedNames(child, names);
3500
4004
  }
3501
4005
  if (level > 0 || modulePath) {
3502
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
4006
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3503
4007
  }
3504
4008
  }
3505
4009
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3601,8 +4105,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3601
4105
  return null;
3602
4106
  }
3603
4107
  async function resolvePyImport(imp, importerPath, serviceDir) {
3604
- if (!imp.modulePath) return null;
3605
- const relPath = imp.modulePath.split(".").join("/");
3606
4108
  let baseDir;
3607
4109
  if (imp.level > 0) {
3608
4110
  baseDir = import_node_path12.default.dirname(importerPath);
@@ -3610,13 +4112,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
3610
4112
  } else {
3611
4113
  baseDir = serviceDir;
3612
4114
  }
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));
4115
+ const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
4116
+ const resolved = /* @__PURE__ */ new Set();
4117
+ let needModuleFile = imp.names.length === 0;
4118
+ for (const name of imp.names) {
4119
+ const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
4120
+ const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
4121
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
4122
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
4123
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
4124
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
4125
+ } else {
4126
+ needModuleFile = true;
4127
+ }
4128
+ }
4129
+ if (needModuleFile) {
4130
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
4131
+ for (const candidate of moduleFileCandidates) {
4132
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
4133
+ resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
4134
+ break;
4135
+ }
3617
4136
  }
3618
4137
  }
3619
- return null;
4138
+ return [...resolved];
3620
4139
  }
3621
4140
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3622
4141
  const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
@@ -3657,17 +4176,18 @@ async function addImports(graph, services) {
3657
4176
  continue;
3658
4177
  }
3659
4178
  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
- );
4179
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
4180
+ for (const resolved of resolvedPaths) {
4181
+ edgesAdded += emitImportEdge(
4182
+ graph,
4183
+ service.pkg.name,
4184
+ importerFileId,
4185
+ relFile,
4186
+ resolved,
4187
+ imp.line,
4188
+ imp.snippet
4189
+ );
4190
+ }
3671
4191
  }
3672
4192
  continue;
3673
4193
  }
@@ -4303,6 +4823,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4303
4823
  edgesAdded++;
4304
4824
  }
4305
4825
  }
4826
+ if (allConfigs.length === 1) {
4827
+ const primary = allConfigs[0];
4828
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
4829
+ const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
4830
+ const cfgId = (0, import_types9.configId)(relPath);
4831
+ if (!graph.hasNode(cfgId)) {
4832
+ const cfgNode = {
4833
+ id: cfgId,
4834
+ type: import_types9.NodeType.ConfigNode,
4835
+ name: import_node_path20.default.basename(primary.sourceFile),
4836
+ path: relPath,
4837
+ fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
4838
+ };
4839
+ graph.addNode(cfgId, cfgNode);
4840
+ nodesAdded++;
4841
+ }
4842
+ const cfgEdge = {
4843
+ id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
4844
+ source: service.node.id,
4845
+ target: cfgId,
4846
+ type: import_types9.EdgeType.CONFIGURED_BY,
4847
+ provenance: import_types9.Provenance.EXTRACTED,
4848
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
4849
+ evidence: { file: toPosix2(relPath) }
4850
+ };
4851
+ if (!graph.hasEdge(cfgEdge.id)) {
4852
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
4853
+ edgesAdded++;
4854
+ }
4855
+ }
4306
4856
  attachIncompatibilities(service, allConfigs);
4307
4857
  if (graph.hasNode(service.node.id)) {
4308
4858
  const current = graph.getNodeAttributes(service.node.id);
@@ -4314,6 +4864,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4314
4864
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
4315
4865
  delete updated.incompatibilities;
4316
4866
  }
4867
+ if (!service.node.dbConnectionTarget) {
4868
+ delete updated.dbConnectionTarget;
4869
+ }
4317
4870
  graph.replaceNodeAttributes(service.node.id, updated);
4318
4871
  }
4319
4872
  }
@@ -4491,7 +5044,7 @@ async function addHttpCallEdges(graph, services) {
4491
5044
  const dedupKey = `${relFile}|${targetId}`;
4492
5045
  if (seen.has(dedupKey)) continue;
4493
5046
  seen.add(dedupKey);
4494
- const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
5047
+ const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
4495
5048
  const ev = {
4496
5049
  file: relFile,
4497
5050
  line: site.line,
@@ -4511,7 +5064,7 @@ async function addHttpCallEdges(graph, services) {
4511
5064
  target: targetId,
4512
5065
  type: import_types11.EdgeType.CALLS,
4513
5066
  confidence,
4514
- confidenceKind: "hostname-shape-match",
5067
+ confidenceKind: "url-literal-service-target",
4515
5068
  evidence: ev
4516
5069
  });
4517
5070
  continue;
@@ -5017,19 +5570,29 @@ init_cjs_shims();
5017
5570
  var import_node_path29 = __toESM(require("path"), 1);
5018
5571
  var import_node_fs15 = require("fs");
5019
5572
  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) {
5573
+ function readDockerfile(content) {
5574
+ let image = null;
5575
+ const ports = [];
5576
+ let cmd = null;
5577
+ let entrypoint = null;
5578
+ for (const raw of content.split("\n")) {
5024
5579
  const line = raw.trim();
5025
5580
  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;
5581
+ if (/^from\s+/i.test(line)) {
5582
+ const candidate = line.split(/\s+/)[1];
5583
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
5584
+ } else if (/^expose\s+/i.test(line)) {
5585
+ for (const token of line.split(/\s+/).slice(1)) {
5586
+ const port = Number.parseInt(token.split("/")[0], 10);
5587
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
5588
+ }
5589
+ } else if (/^entrypoint\s+/i.test(line)) {
5590
+ entrypoint = line;
5591
+ } else if (/^cmd\s+/i.test(line)) {
5592
+ cmd = line;
5593
+ }
5031
5594
  }
5032
- return last;
5595
+ return { image, ports, entrypoint: entrypoint ?? cmd };
5033
5596
  }
5034
5597
  async function addDockerfileRuntimes(graph, services, scanPath) {
5035
5598
  let nodesAdded = 0;
@@ -5048,14 +5611,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5048
5611
  );
5049
5612
  continue;
5050
5613
  }
5051
- const image = runtimeImage(content);
5052
- if (!image) continue;
5053
- const node = makeInfraNode("container-image", image);
5614
+ const facts = readDockerfile(content);
5615
+ if (!facts.image) continue;
5616
+ const node = makeInfraNode("container-image", facts.image);
5054
5617
  if (!graph.hasNode(node.id)) {
5055
5618
  graph.addNode(node.id, node);
5056
5619
  nodesAdded++;
5057
5620
  }
5058
5621
  const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5622
+ const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
5059
5623
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5060
5624
  graph,
5061
5625
  service.pkg.name,
@@ -5074,12 +5638,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5074
5638
  provenance: import_types20.Provenance.EXTRACTED,
5075
5639
  confidence: (0, import_types20.confidenceForExtracted)("structural"),
5076
5640
  evidence: {
5077
- file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
5641
+ file: evidenceFile,
5642
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
5078
5643
  }
5079
5644
  };
5080
5645
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5081
5646
  edgesAdded++;
5082
5647
  }
5648
+ for (const port of facts.ports) {
5649
+ const portNode = makeInfraNode("port", String(port));
5650
+ if (!graph.hasNode(portNode.id)) {
5651
+ graph.addNode(portNode.id, portNode);
5652
+ nodesAdded++;
5653
+ }
5654
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
5655
+ if (graph.hasEdge(portEdgeId)) continue;
5656
+ const portEdge = {
5657
+ id: portEdgeId,
5658
+ source: fileNodeId,
5659
+ target: portNode.id,
5660
+ type: import_types20.EdgeType.CONNECTS_TO,
5661
+ provenance: import_types20.Provenance.EXTRACTED,
5662
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
5663
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5664
+ };
5665
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
5666
+ edgesAdded++;
5667
+ }
5083
5668
  }
5084
5669
  return { nodesAdded, edgesAdded };
5085
5670
  }
@@ -5088,7 +5673,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5088
5673
  init_cjs_shims();
5089
5674
  var import_node_fs16 = require("fs");
5090
5675
  var import_node_path30 = __toESM(require("path"), 1);
5676
+ var import_types21 = require("@neat.is/types");
5091
5677
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5678
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5092
5679
  async function walkTfFiles(start, depth = 0, max = 5) {
5093
5680
  if (depth > max) return [];
5094
5681
  const out = [];
@@ -5105,24 +5692,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
5105
5692
  }
5106
5693
  return out;
5107
5694
  }
5695
+ function blockBody(content, from) {
5696
+ const open = content.indexOf("{", from);
5697
+ if (open === -1) return null;
5698
+ let depth = 0;
5699
+ for (let i = open; i < content.length; i++) {
5700
+ const ch = content[i];
5701
+ if (ch === "{") depth++;
5702
+ else if (ch === "}") {
5703
+ depth--;
5704
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
5705
+ }
5706
+ }
5707
+ return null;
5708
+ }
5709
+ function lineAt(content, index) {
5710
+ let line = 1;
5711
+ for (let i = 0; i < index && i < content.length; i++) {
5712
+ if (content[i] === "\n") line++;
5713
+ }
5714
+ return line;
5715
+ }
5108
5716
  async function addTerraformResources(graph, scanPath) {
5109
5717
  let nodesAdded = 0;
5718
+ let edgesAdded = 0;
5110
5719
  const files = await walkTfFiles(scanPath);
5111
5720
  for (const file of files) {
5112
5721
  const content = await import_node_fs16.promises.readFile(file, "utf8");
5722
+ const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
5723
+ const resources = [];
5724
+ const byKey = /* @__PURE__ */ new Map();
5113
5725
  RESOURCE_RE.lastIndex = 0;
5114
5726
  let m;
5115
5727
  while ((m = RESOURCE_RE.exec(content)) !== null) {
5116
- const kind = m[1];
5728
+ const type = m[1];
5117
5729
  const name = m[2];
5118
- const node = makeInfraNode(kind, name, "aws");
5730
+ const node = makeInfraNode(type, name, "aws");
5119
5731
  if (!graph.hasNode(node.id)) {
5120
5732
  graph.addNode(node.id, node);
5121
5733
  nodesAdded++;
5122
5734
  }
5735
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
5736
+ const resource = {
5737
+ type,
5738
+ name,
5739
+ nodeId: node.id,
5740
+ body: span?.body ?? "",
5741
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
5742
+ };
5743
+ resources.push(resource);
5744
+ byKey.set(`${type}.${name}`, resource);
5745
+ }
5746
+ for (const resource of resources) {
5747
+ const seen = /* @__PURE__ */ new Set();
5748
+ REFERENCE_RE.lastIndex = 0;
5749
+ let ref;
5750
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
5751
+ const key = `${ref[1]}.${ref[2]}`;
5752
+ if (key === `${resource.type}.${resource.name}`) continue;
5753
+ const target = byKey.get(key);
5754
+ if (!target) continue;
5755
+ if (seen.has(target.nodeId)) continue;
5756
+ seen.add(target.nodeId);
5757
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
5758
+ if (graph.hasEdge(edgeId)) continue;
5759
+ const line = lineAt(content, resource.bodyOffset + ref.index);
5760
+ const edge = {
5761
+ id: edgeId,
5762
+ source: resource.nodeId,
5763
+ target: target.nodeId,
5764
+ type: import_types21.EdgeType.DEPENDS_ON,
5765
+ provenance: import_types21.Provenance.EXTRACTED,
5766
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
5767
+ evidence: { file: evidenceFile, line, snippet: key }
5768
+ };
5769
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5770
+ edgesAdded++;
5771
+ }
5123
5772
  }
5124
5773
  }
5125
- return { nodesAdded, edgesAdded: 0 };
5774
+ return { nodesAdded, edgesAdded };
5126
5775
  }
5127
5776
 
5128
5777
  // src/extract/infra/k8s.ts
@@ -5200,11 +5849,11 @@ var import_node_path33 = __toESM(require("path"), 1);
5200
5849
  init_cjs_shims();
5201
5850
  var import_node_fs18 = require("fs");
5202
5851
  var import_node_path32 = __toESM(require("path"), 1);
5203
- var import_types21 = require("@neat.is/types");
5852
+ var import_types22 = require("@neat.is/types");
5204
5853
  function dropOrphanedFileNodes(graph) {
5205
5854
  const orphans = [];
5206
5855
  graph.forEachNode((id, attrs) => {
5207
- if (attrs.type !== import_types21.NodeType.FileNode) return;
5856
+ if (attrs.type !== import_types22.NodeType.FileNode) return;
5208
5857
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5209
5858
  orphans.push(id);
5210
5859
  }
@@ -5217,7 +5866,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5217
5866
  const bases = [scanPath, ...serviceDirs];
5218
5867
  graph.forEachEdge((id, attrs) => {
5219
5868
  const edge = attrs;
5220
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5869
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5221
5870
  const evidenceFile = edge.evidence?.file;
5222
5871
  if (!evidenceFile) return;
5223
5872
  if (import_node_path32.default.isAbsolute(evidenceFile)) {
@@ -5300,7 +5949,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5300
5949
  init_cjs_shims();
5301
5950
  var import_node_fs19 = require("fs");
5302
5951
  var import_node_path34 = __toESM(require("path"), 1);
5303
- var import_types22 = require("@neat.is/types");
5952
+ var import_types23 = require("@neat.is/types");
5304
5953
  var SCHEMA_VERSION = 4;
5305
5954
  function migrateV1ToV2(payload) {
5306
5955
  const nodes = payload.graph.nodes;
@@ -5322,12 +5971,12 @@ function migrateV2ToV3(payload) {
5322
5971
  for (const edge of edges) {
5323
5972
  const attrs = edge.attributes;
5324
5973
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5325
- attrs.provenance = import_types22.Provenance.OBSERVED;
5974
+ attrs.provenance = import_types23.Provenance.OBSERVED;
5326
5975
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5327
5976
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5328
5977
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5329
5978
  if (type && source && target) {
5330
- const newId = (0, import_types22.observedEdgeId)(source, target, type);
5979
+ const newId = (0, import_types23.observedEdgeId)(source, target, type);
5331
5980
  attrs.id = newId;
5332
5981
  if (edge.key) edge.key = newId;
5333
5982
  }
@@ -5419,7 +6068,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
5419
6068
  init_cjs_shims();
5420
6069
  var import_fastify = __toESM(require("fastify"), 1);
5421
6070
  var import_cors = __toESM(require("@fastify/cors"), 1);
5422
- var import_types25 = require("@neat.is/types");
6071
+ var import_types26 = require("@neat.is/types");
5423
6072
 
5424
6073
  // src/extend/index.ts
5425
6074
  init_cjs_shims();
@@ -5738,7 +6387,7 @@ async function rollbackExtension(ctx, args) {
5738
6387
 
5739
6388
  // src/divergences.ts
5740
6389
  init_cjs_shims();
5741
- var import_types23 = require("@neat.is/types");
6390
+ var import_types24 = require("@neat.is/types");
5742
6391
  function bucketKey(source, target, type) {
5743
6392
  return `${type}|${source}|${target}`;
5744
6393
  }
@@ -5746,22 +6395,22 @@ function bucketEdges(graph) {
5746
6395
  const buckets = /* @__PURE__ */ new Map();
5747
6396
  graph.forEachEdge((id, attrs) => {
5748
6397
  const e = attrs;
5749
- const parsed = (0, import_types23.parseEdgeId)(id);
6398
+ const parsed = (0, import_types24.parseEdgeId)(id);
5750
6399
  const provenance = parsed?.provenance ?? e.provenance;
5751
6400
  const key = bucketKey(e.source, e.target, e.type);
5752
6401
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5753
6402
  switch (provenance) {
5754
- case import_types23.Provenance.EXTRACTED:
6403
+ case import_types24.Provenance.EXTRACTED:
5755
6404
  cur.extracted = e;
5756
6405
  break;
5757
- case import_types23.Provenance.OBSERVED:
6406
+ case import_types24.Provenance.OBSERVED:
5758
6407
  cur.observed = e;
5759
6408
  break;
5760
- case import_types23.Provenance.INFERRED:
6409
+ case import_types24.Provenance.INFERRED:
5761
6410
  cur.inferred = e;
5762
6411
  break;
5763
6412
  default:
5764
- if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
6413
+ if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
5765
6414
  }
5766
6415
  buckets.set(key, cur);
5767
6416
  });
@@ -5770,7 +6419,7 @@ function bucketEdges(graph) {
5770
6419
  function nodeIsFrontier(graph, nodeId) {
5771
6420
  if (!graph.hasNode(nodeId)) return false;
5772
6421
  const attrs = graph.getNodeAttributes(nodeId);
5773
- return attrs.type === import_types23.NodeType.FrontierNode;
6422
+ return attrs.type === import_types24.NodeType.FrontierNode;
5774
6423
  }
5775
6424
  function clampConfidence(n) {
5776
6425
  if (!Number.isFinite(n)) return 0;
@@ -5789,10 +6438,16 @@ function gradedConfidence(edge) {
5789
6438
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
5790
6439
  return clampConfidence(confidenceForEdge(edge));
5791
6440
  }
6441
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6442
+ import_types24.EdgeType.CALLS,
6443
+ import_types24.EdgeType.CONNECTS_TO,
6444
+ import_types24.EdgeType.PUBLISHES_TO,
6445
+ import_types24.EdgeType.CONSUMES_FROM
6446
+ ]);
5792
6447
  function detectMissingDivergences(graph, bucket) {
5793
6448
  const out = [];
5794
- if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5795
- if (bucket.extracted && !bucket.observed) {
6449
+ if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
6450
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5796
6451
  if (!nodeIsFrontier(graph, bucket.target)) {
5797
6452
  out.push({
5798
6453
  type: "missing-observed",
@@ -5832,7 +6487,7 @@ function declaredHostFor(svc) {
5832
6487
  function hasExtractedConfiguredBy(graph, svcId) {
5833
6488
  for (const edgeId of graph.outboundEdges(svcId)) {
5834
6489
  const e = graph.getEdgeAttributes(edgeId);
5835
- if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
6490
+ if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
5836
6491
  return true;
5837
6492
  }
5838
6493
  }
@@ -5845,10 +6500,10 @@ function detectHostMismatch(graph, svcId, svc) {
5845
6500
  const out = [];
5846
6501
  for (const edgeId of graph.outboundEdges(svcId)) {
5847
6502
  const edge = graph.getEdgeAttributes(edgeId);
5848
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5849
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6503
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6504
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5850
6505
  const target = graph.getNodeAttributes(edge.target);
5851
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6506
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5852
6507
  const observedHost = target.host?.trim();
5853
6508
  if (!observedHost) continue;
5854
6509
  if (observedHost === declaredHost) continue;
@@ -5870,10 +6525,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5870
6525
  const deps = svc.dependencies ?? {};
5871
6526
  for (const edgeId of graph.outboundEdges(svcId)) {
5872
6527
  const edge = graph.getEdgeAttributes(edgeId);
5873
- if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5874
- if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
6528
+ if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
6529
+ if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
5875
6530
  const target = graph.getNodeAttributes(edge.target);
5876
- if (target.type !== import_types23.NodeType.DatabaseNode) continue;
6531
+ if (target.type !== import_types24.NodeType.DatabaseNode) continue;
5877
6532
  for (const pair of compatPairs()) {
5878
6533
  if (pair.engine !== target.engine) continue;
5879
6534
  const declared = deps[pair.driver];
@@ -5926,6 +6581,23 @@ function detectCompatDivergences(graph, svcId, svc) {
5926
6581
  function involvesNode(d, nodeId) {
5927
6582
  return d.source === nodeId || d.target === nodeId;
5928
6583
  }
6584
+ function suppressHostMismatchHalves(all) {
6585
+ const observedHalf = /* @__PURE__ */ new Set();
6586
+ const declaredHalf = /* @__PURE__ */ new Set();
6587
+ for (const d of all) {
6588
+ if (d.type !== "host-mismatch") continue;
6589
+ observedHalf.add(`${d.source}->${d.target}`);
6590
+ declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
6591
+ }
6592
+ if (observedHalf.size === 0) return all;
6593
+ return all.filter((d) => {
6594
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
6595
+ return false;
6596
+ }
6597
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
6598
+ return true;
6599
+ });
6600
+ }
5929
6601
  function computeDivergences(graph, opts = {}) {
5930
6602
  const all = [];
5931
6603
  const buckets = bucketEdges(graph);
@@ -5934,12 +6606,13 @@ function computeDivergences(graph, opts = {}) {
5934
6606
  }
5935
6607
  graph.forEachNode((nodeId, attrs) => {
5936
6608
  const n = attrs;
5937
- if (n.type !== import_types23.NodeType.ServiceNode) return;
6609
+ if (n.type !== import_types24.NodeType.ServiceNode) return;
5938
6610
  const svc = n;
5939
6611
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5940
6612
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
5941
6613
  });
5942
- let filtered = all;
6614
+ const reconciled = suppressHostMismatchHalves(all);
6615
+ let filtered = reconciled;
5943
6616
  if (opts.type) {
5944
6617
  const allowed = opts.type;
5945
6618
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -5967,7 +6640,7 @@ function computeDivergences(graph, opts = {}) {
5967
6640
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5968
6641
  return a.target.localeCompare(b.target);
5969
6642
  });
5970
- return import_types23.DivergenceResultSchema.parse({
6643
+ return import_types24.DivergenceResultSchema.parse({
5971
6644
  divergences: filtered,
5972
6645
  totalAffected: filtered.length,
5973
6646
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6108,7 +6781,7 @@ init_cjs_shims();
6108
6781
  var import_node_fs23 = require("fs");
6109
6782
  var import_node_os3 = __toESM(require("os"), 1);
6110
6783
  var import_node_path38 = __toESM(require("path"), 1);
6111
- var import_types24 = require("@neat.is/types");
6784
+ var import_types25 = require("@neat.is/types");
6112
6785
  var LOCK_TIMEOUT_MS = 5e3;
6113
6786
  var LOCK_RETRY_MS = 50;
6114
6787
  function neatHome() {
@@ -6257,10 +6930,10 @@ async function readRegistry() {
6257
6930
  throw err;
6258
6931
  }
6259
6932
  const parsed = JSON.parse(raw);
6260
- return import_types24.RegistryFileSchema.parse(parsed);
6933
+ return import_types25.RegistryFileSchema.parse(parsed);
6261
6934
  }
6262
6935
  async function writeRegistry(reg) {
6263
- const validated = import_types24.RegistryFileSchema.parse(reg);
6936
+ const validated = import_types25.RegistryFileSchema.parse(reg);
6264
6937
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6265
6938
  }
6266
6939
  var ProjectNameCollisionError = class extends Error {
@@ -6581,6 +7254,18 @@ function registerRoutes(scope, ctx) {
6581
7254
  }
6582
7255
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6583
7256
  });
7257
+ scope.get(
7258
+ "/graph/observed-dependencies/:nodeId",
7259
+ async (req, reply) => {
7260
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7261
+ if (!proj) return;
7262
+ const { nodeId } = req.params;
7263
+ if (!proj.graph.hasNode(nodeId)) {
7264
+ return reply.code(404).send({ error: "node not found", id: nodeId });
7265
+ }
7266
+ return getObservedDependencies(proj.graph, nodeId);
7267
+ }
7268
+ );
6584
7269
  scope.get("/graph/divergences", async (req, reply) => {
6585
7270
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6586
7271
  if (!proj) return;
@@ -6589,11 +7274,11 @@ function registerRoutes(scope, ctx) {
6589
7274
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6590
7275
  const parsed = [];
6591
7276
  for (const c of candidates) {
6592
- const r = import_types25.DivergenceTypeSchema.safeParse(c);
7277
+ const r = import_types26.DivergenceTypeSchema.safeParse(c);
6593
7278
  if (!r.success) {
6594
7279
  return reply.code(400).send({
6595
7280
  error: `unknown divergence type "${c}"`,
6596
- allowed: import_types25.DivergenceTypeSchema.options
7281
+ allowed: import_types26.DivergenceTypeSchema.options
6597
7282
  });
6598
7283
  }
6599
7284
  parsed.push(r.data);
@@ -6641,23 +7326,29 @@ function registerRoutes(scope, ctx) {
6641
7326
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6642
7327
  return { count: sliced.length, total, events: sliced };
6643
7328
  });
7329
+ const incidentHistoryHandler = async (req, reply) => {
7330
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7331
+ if (!proj) return;
7332
+ const { nodeId } = req.params;
7333
+ if (!proj.graph.hasNode(nodeId)) {
7334
+ reply.code(404).send({ error: "node not found", id: nodeId });
7335
+ return;
7336
+ }
7337
+ const epath = errorsPathFor(proj);
7338
+ if (!epath) return { count: 0, total: 0, events: [] };
7339
+ const events = await readErrorEvents(epath);
7340
+ const filtered = events.filter(
7341
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
7342
+ );
7343
+ return { count: filtered.length, total: filtered.length, events: filtered };
7344
+ };
6644
7345
  scope.get(
6645
7346
  "/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
- }
7347
+ incidentHistoryHandler
7348
+ );
7349
+ scope.get(
7350
+ "/graph/incident-history/:nodeId",
7351
+ incidentHistoryHandler
6661
7352
  );
6662
7353
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6663
7354
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -6666,16 +7357,16 @@ function registerRoutes(scope, ctx) {
6666
7357
  if (!proj.graph.hasNode(nodeId)) {
6667
7358
  return reply.code(404).send({ error: "node not found", id: nodeId });
6668
7359
  }
6669
- let errorEvent;
6670
7360
  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);
7361
+ const incidents = epath ? await readErrorEvents(epath) : [];
7362
+ let errorEvent;
7363
+ if (req.query.errorId) {
7364
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
6674
7365
  if (!errorEvent) {
6675
7366
  return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
6676
7367
  }
6677
7368
  }
6678
- const result = getRootCause(proj.graph, nodeId, errorEvent);
7369
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6679
7370
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6680
7371
  return result;
6681
7372
  });
@@ -6806,7 +7497,7 @@ function registerRoutes(scope, ctx) {
6806
7497
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6807
7498
  let violations = await log.readAll();
6808
7499
  if (req.query.severity) {
6809
- const sev = import_types25.PolicySeveritySchema.safeParse(req.query.severity);
7500
+ const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
6810
7501
  if (!sev.success) {
6811
7502
  return reply.code(400).send({
6812
7503
  error: "invalid severity",
@@ -6820,10 +7511,32 @@ function registerRoutes(scope, ctx) {
6820
7511
  }
6821
7512
  return { violations };
6822
7513
  });
7514
+ scope.get("/policies/applicable", async (req, reply) => {
7515
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7516
+ if (!proj) return;
7517
+ const nodeId = req.query.node;
7518
+ if (!nodeId) {
7519
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7520
+ }
7521
+ const policyPath = ctx.policyFilePathFor(proj);
7522
+ let policies = [];
7523
+ if (policyPath) {
7524
+ try {
7525
+ policies = await loadPolicyFile(policyPath);
7526
+ } catch (err) {
7527
+ return reply.code(400).send({
7528
+ error: "policy.json failed to parse",
7529
+ details: err.message
7530
+ });
7531
+ }
7532
+ }
7533
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7534
+ return { node: nodeId, applicable };
7535
+ });
6823
7536
  scope.post("/policies/check", async (req, reply) => {
6824
7537
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6825
7538
  if (!proj) return;
6826
- const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7539
+ const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6827
7540
  if (!parsed.success) {
6828
7541
  return reply.code(400).send({
6829
7542
  error: "invalid /policies/check body",
@@ -7113,6 +7826,7 @@ function unroutedErrorsPath(neatHome2) {
7113
7826
  }
7114
7827
 
7115
7828
  // src/daemon.ts
7829
+ var import_types27 = require("@neat.is/types");
7116
7830
  function daemonJsonPath(scanPath) {
7117
7831
  return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
7118
7832
  }
@@ -7215,6 +7929,19 @@ function isTokenContained(needle, haystack) {
7215
7929
  const tokens = haystack.split(/[-_]/);
7216
7930
  return tokens.includes(needle);
7217
7931
  }
7932
+ function serviceNameMatchesProject(serviceName, project) {
7933
+ if (serviceName === project) return true;
7934
+ if (isTokenPrefix(project, serviceName)) return true;
7935
+ if (isTokenContained(project, serviceName)) return true;
7936
+ return false;
7937
+ }
7938
+ function spanBelongsToSingleProject(graph, project, serviceName) {
7939
+ if (!serviceName) return true;
7940
+ if (serviceNameMatchesProject(serviceName, project)) return true;
7941
+ return graph.someNode(
7942
+ (_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
7943
+ );
7944
+ }
7218
7945
  async function bootstrapProject(entry) {
7219
7946
  const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
7220
7947
  try {
@@ -7512,7 +8239,9 @@ async function startDaemon(opts = {}) {
7512
8239
  singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
7513
8240
  });
7514
8241
  restAddress = await restApp.listen({ port: restPort, host });
7515
- console.log(`neatd: REST listening on ${restAddress}`);
8242
+ console.log(
8243
+ `neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
8244
+ );
7516
8245
  } catch (err) {
7517
8246
  for (const slot of slots.values()) {
7518
8247
  teardownSlot(slot);
@@ -7543,6 +8272,10 @@ async function startDaemon(opts = {}) {
7543
8272
  warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
7544
8273
  return null;
7545
8274
  }
8275
+ if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
8276
+ await recordUnroutedSpan(serviceName, traceId);
8277
+ return null;
8278
+ }
7546
8279
  return slot2;
7547
8280
  }
7548
8281
  const liveEntries = await listProjects().catch(() => []);
@@ -7605,7 +8338,7 @@ async function startDaemon(opts = {}) {
7605
8338
  onErrorSpanSync: async (span) => {
7606
8339
  const slot = await resolveTargetSlot(span.service, span.traceId);
7607
8340
  if (!slot) return;
7608
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8341
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7609
8342
  },
7610
8343
  // Project-scoped route (issue #367) — the URL already named the
7611
8344
  // project. Resolution is a direct slot lookup; service.name resolves
@@ -7628,10 +8361,10 @@ async function startDaemon(opts = {}) {
7628
8361
  onProjectErrorSpanSync: async (project, span) => {
7629
8362
  const slot = await resolveSlotByName(project, span.service, span.traceId);
7630
8363
  if (!slot) return;
7631
- await makeErrorSpanWriter(slot.paths.errorsPath)(span);
8364
+ await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
7632
8365
  }
7633
8366
  });
7634
- otlpAddress = await otlpApp.listen({ port: otlpPort, host });
8367
+ otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
7635
8368
  console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
7636
8369
  } catch (err) {
7637
8370
  for (const slot of slots.values()) {
@@ -7786,7 +8519,8 @@ async function startDaemon(opts = {}) {
7786
8519
  otlpAddress,
7787
8520
  bootstrap: tracker,
7788
8521
  initialBootstrap,
7789
- daemonRecord
8522
+ daemonRecord,
8523
+ neatHome: home
7790
8524
  };
7791
8525
  }
7792
8526
  // Annotate the CommonJS export names for ESM import in node: