@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/cli.cjs CHANGED
@@ -42,13 +42,13 @@ var init_cjs_shims = __esm({
42
42
  });
43
43
 
44
44
  // src/auth.ts
45
- function isLoopbackHost(host) {
45
+ function isLoopbackHost2(host) {
46
46
  if (!host) return false;
47
47
  return LOOPBACK_HOSTS.has(host);
48
48
  }
49
49
  function assertBindAuthority(host, token) {
50
50
  if (token && token.length > 0) return;
51
- if (isLoopbackHost(host)) return;
51
+ if (isLoopbackHost2(host)) return;
52
52
  throw new BindAuthorityError(host);
53
53
  }
54
54
  function mountBearerAuth(app, opts) {
@@ -574,7 +574,23 @@ async function buildOtelReceiver(opts) {
574
574
  };
575
575
  return decorated;
576
576
  }
577
- var import_node_path42, import_node_url3, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
577
+ async function listenSteppingOtlp(app, requestedPort, host) {
578
+ let port = requestedPort;
579
+ for (let attempt = 0; ; attempt++) {
580
+ try {
581
+ return await app.listen({ port, host });
582
+ } catch (err) {
583
+ const code = err.code;
584
+ const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
585
+ if (!canStep) throw err;
586
+ console.warn(
587
+ `otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
588
+ );
589
+ port += OTLP_STEP_STRIDE;
590
+ }
591
+ }
592
+ }
593
+ var import_node_path42, import_node_url3, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
578
594
  var init_otel = __esm({
579
595
  "src/otel.ts"() {
580
596
  "use strict";
@@ -590,6 +606,8 @@ var init_otel = __esm({
590
606
  exportTraceServiceRequestType = null;
591
607
  exportTraceServiceResponseType = null;
592
608
  cachedProtobufResponseBody = null;
609
+ OTLP_STEP_ATTEMPTS = 8;
610
+ OTLP_STEP_STRIDE = 1;
593
611
  }
594
612
  });
595
613
 
@@ -605,6 +623,7 @@ __export(cli_exports, {
605
623
  parseArgs: () => parseArgs,
606
624
  printBanner: () => printBanner,
607
625
  readPackageVersion: () => readPackageVersion,
626
+ resolveDaemonUrl: () => resolveDaemonUrl,
608
627
  resolveProjectForVerb: () => resolveProjectForVerb,
609
628
  runInit: () => runInit,
610
629
  runQueryVerb: () => runQueryVerb,
@@ -1277,22 +1296,164 @@ var rootCauseShapes = {
1277
1296
  [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1278
1297
  [import_types.NodeType.FileNode]: fileRootCauseShape
1279
1298
  };
1280
- function getRootCause(graph, errorNodeId, errorEvent) {
1299
+ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1281
1300
  if (!graph.hasNode(errorNodeId)) return null;
1282
1301
  const origin = graph.getNodeAttributes(errorNodeId);
1283
1302
  const shape = rootCauseShapes[origin.type];
1284
- if (!shape) return null;
1285
- const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1286
- const match = shape(graph, origin, walk);
1287
- if (!match) return null;
1288
- const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1303
+ if (shape) {
1304
+ const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
1305
+ const match = shape(graph, origin, walk);
1306
+ if (match) {
1307
+ const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
1308
+ return import_types.RootCauseResultSchema.parse({
1309
+ rootCauseNode: match.rootCauseNode,
1310
+ rootCauseReason: reason,
1311
+ traversalPath: walk.path,
1312
+ edgeProvenances: walk.edges.map((e) => e.provenance),
1313
+ confidence: confidenceFromMix(walk.edges),
1314
+ fixRecommendation: match.fixRecommendation
1315
+ });
1316
+ }
1317
+ }
1318
+ if (origin.type === import_types.NodeType.ServiceNode) {
1319
+ const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
1320
+ if (crossService) return crossService;
1321
+ }
1322
+ return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
1323
+ }
1324
+ var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
1325
+ function incidentMatchesNode(ev, nodeId) {
1326
+ return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
1327
+ }
1328
+ function localizeFromIncidents(nodeId, incidents, errorEvent) {
1329
+ const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
1330
+ const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
1331
+ if (relevant.length === 0) return null;
1332
+ const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
1333
+ const attrs = latest.attributes ?? {};
1334
+ const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
1335
+ const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
1336
+ const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
1337
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
1338
+ const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
1339
+ const count = sameMode.length;
1340
+ const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
1341
+ const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
1342
+ if (location) reasonParts.push(`surfaced at ${location}`);
1343
+ const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
1344
+ const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
1345
+ const fileNode = localizesToFile ? latest.affectedNode : void 0;
1346
+ const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
1347
+ return {
1348
+ rootCauseNode: fileNode ?? nodeId,
1349
+ rootCauseReason,
1350
+ ...fileNode ? { fileNode } : {},
1351
+ ...fixRecommendation ? { fixRecommendation } : {}
1352
+ };
1353
+ }
1354
+ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1355
+ const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
1356
+ if (!loc) return null;
1357
+ const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
1358
+ const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
1289
1359
  return import_types.RootCauseResultSchema.parse({
1290
- rootCauseNode: match.rootCauseNode,
1291
- rootCauseReason: reason,
1292
- traversalPath: walk.path,
1293
- edgeProvenances: walk.edges.map((e) => e.provenance),
1294
- confidence: confidenceFromMix(walk.edges),
1295
- fixRecommendation: match.fixRecommendation
1360
+ rootCauseNode: loc.rootCauseNode,
1361
+ rootCauseReason: loc.rootCauseReason,
1362
+ traversalPath,
1363
+ edgeProvenances,
1364
+ confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
1365
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1366
+ });
1367
+ }
1368
+ function isFailingCallEdge(e) {
1369
+ return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1370
+ }
1371
+ function callSourcesForService(graph, serviceId3) {
1372
+ const ids = [serviceId3];
1373
+ for (const edgeId of graph.outboundEdges(serviceId3)) {
1374
+ const e = graph.getEdgeAttributes(edgeId);
1375
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1376
+ const tgt = graph.getNodeAttributes(e.target);
1377
+ if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
1378
+ }
1379
+ return ids;
1380
+ }
1381
+ function failingCallDominates(e, id, curEdge, curId) {
1382
+ const ec = e.signal?.errorCount ?? 0;
1383
+ const cc = curEdge.signal?.errorCount ?? 0;
1384
+ if (ec !== cc) return ec > cc;
1385
+ if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
1386
+ return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
1387
+ }
1388
+ return id < curId;
1389
+ }
1390
+ function dominantFailingCall(graph, serviceId3, visited) {
1391
+ let best = null;
1392
+ for (const src of callSourcesForService(graph, serviceId3)) {
1393
+ for (const edgeId of graph.outboundEdges(src)) {
1394
+ const e = graph.getEdgeAttributes(edgeId);
1395
+ if (!isFailingCallEdge(e)) continue;
1396
+ if (isFrontierNode(graph, e.target)) continue;
1397
+ const owner = resolveOwningService(graph, e.target);
1398
+ if (!owner || visited.has(owner.id)) continue;
1399
+ if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
1400
+ best = { nextService: owner.id, edge: e };
1401
+ }
1402
+ }
1403
+ }
1404
+ return best;
1405
+ }
1406
+ function followFailingCallChain(graph, originServiceId, maxDepth) {
1407
+ const path53 = [originServiceId];
1408
+ const edges = [];
1409
+ const visited = /* @__PURE__ */ new Set([originServiceId]);
1410
+ let current = originServiceId;
1411
+ for (let depth = 0; depth < maxDepth; depth++) {
1412
+ const hop = dominantFailingCall(graph, current, visited);
1413
+ if (!hop) break;
1414
+ path53.push(hop.nextService);
1415
+ edges.push(hop.edge);
1416
+ visited.add(hop.nextService);
1417
+ current = hop.nextService;
1418
+ }
1419
+ if (edges.length === 0) return null;
1420
+ return { path: path53, edges, culprit: current };
1421
+ }
1422
+ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1423
+ const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1424
+ if (!chain) return null;
1425
+ const culprit = chain.culprit;
1426
+ const path53 = [...chain.path];
1427
+ const edgeProvenances = chain.edges.map((e) => e.provenance);
1428
+ const baseConfidence = confidenceFromMix(chain.edges);
1429
+ const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
1430
+ const loc = localizeFromIncidents(culprit, incidents, errorEvent);
1431
+ if (loc) {
1432
+ let rootCauseNode = culprit;
1433
+ if (loc.fileNode) {
1434
+ path53.push(loc.fileNode);
1435
+ edgeProvenances.push(import_types.Provenance.OBSERVED);
1436
+ rootCauseNode = loc.fileNode;
1437
+ }
1438
+ return import_types.RootCauseResultSchema.parse({
1439
+ rootCauseNode,
1440
+ rootCauseReason: loc.rootCauseReason,
1441
+ traversalPath: path53,
1442
+ edgeProvenances,
1443
+ confidence,
1444
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
1445
+ });
1446
+ }
1447
+ const lastEdge = chain.edges[chain.edges.length - 1];
1448
+ const errs = lastEdge.signal?.errorCount ?? 0;
1449
+ const culpritName = culprit.replace(/^service:/, "");
1450
+ return import_types.RootCauseResultSchema.parse({
1451
+ rootCauseNode: culprit,
1452
+ rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1453
+ traversalPath: path53,
1454
+ edgeProvenances,
1455
+ confidence,
1456
+ fixRecommendation: `Inspect ${culpritName}'s failing handler`
1296
1457
  });
1297
1458
  }
1298
1459
  function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
@@ -1315,14 +1476,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
1315
1476
  });
1316
1477
  }
1317
1478
  if (frame.distance >= maxDepth) continue;
1318
- const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
1319
- for (const [tgtId, edge] of outgoing) {
1320
- if (enqueued.has(tgtId)) continue;
1321
- enqueued.add(tgtId);
1479
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
1480
+ for (const [srcId, edge] of incoming) {
1481
+ if (enqueued.has(srcId)) continue;
1482
+ enqueued.add(srcId);
1322
1483
  queue.push({
1323
- nodeId: tgtId,
1484
+ nodeId: srcId,
1324
1485
  distance: frame.distance + 1,
1325
- path: [...frame.path, tgtId],
1486
+ path: [...frame.path, srcId],
1326
1487
  pathEdges: [...frame.pathEdges, edge]
1327
1488
  });
1328
1489
  }
@@ -1378,6 +1539,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
1378
1539
  total: dependencies.length
1379
1540
  });
1380
1541
  }
1542
+ function getObservedDependencies(graph, nodeId) {
1543
+ if (!graph.hasNode(nodeId)) {
1544
+ return import_types.ObservedDependenciesResultSchema.parse({
1545
+ origin: nodeId,
1546
+ dependencies: [],
1547
+ observed: false,
1548
+ inboundObservedCount: 0,
1549
+ hasExtractedOutbound: false
1550
+ });
1551
+ }
1552
+ const attrs = graph.getNodeAttributes(nodeId);
1553
+ const scope = [nodeId];
1554
+ if (attrs.type === import_types.NodeType.ServiceNode) {
1555
+ for (const edgeId of graph.outboundEdges(nodeId)) {
1556
+ const e = graph.getEdgeAttributes(edgeId);
1557
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1558
+ const owned = graph.getNodeAttributes(e.target);
1559
+ if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
1560
+ }
1561
+ }
1562
+ const dependencies = [];
1563
+ const seenEdge = /* @__PURE__ */ new Set();
1564
+ let hasExtractedOutbound = false;
1565
+ for (const src of scope) {
1566
+ for (const edgeId of graph.outboundEdges(src)) {
1567
+ const e = graph.getEdgeAttributes(edgeId);
1568
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1569
+ if (e.provenance === import_types.Provenance.OBSERVED) {
1570
+ if (!seenEdge.has(e.id)) {
1571
+ seenEdge.add(e.id);
1572
+ dependencies.push(e);
1573
+ }
1574
+ } else if (e.provenance === import_types.Provenance.EXTRACTED) {
1575
+ hasExtractedOutbound = true;
1576
+ }
1577
+ }
1578
+ }
1579
+ let inboundObservedCount = 0;
1580
+ for (const tgt of scope) {
1581
+ for (const edgeId of graph.inboundEdges(tgt)) {
1582
+ const e = graph.getEdgeAttributes(edgeId);
1583
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1584
+ if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
1585
+ }
1586
+ }
1587
+ dependencies.sort(
1588
+ (a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
1589
+ );
1590
+ return import_types.ObservedDependenciesResultSchema.parse({
1591
+ origin: nodeId,
1592
+ dependencies,
1593
+ observed: dependencies.length > 0 || inboundObservedCount > 0,
1594
+ inboundObservedCount,
1595
+ hasExtractedOutbound
1596
+ });
1597
+ }
1381
1598
 
1382
1599
  // src/policy.ts
1383
1600
  var DEFAULT_ACTION_BY_SEVERITY = {
@@ -1647,6 +1864,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
1647
1864
  }
1648
1865
  return out;
1649
1866
  }
1867
+ function selectApplicablePolicies(graph, policies, nodeId) {
1868
+ if (!graph.hasNode(nodeId)) return [];
1869
+ const node = graph.getNodeAttributes(nodeId);
1870
+ const out = [];
1871
+ for (const policy of policies) {
1872
+ const m = matchPolicyToNode(graph, policy, nodeId, node);
1873
+ if (!m) continue;
1874
+ out.push({
1875
+ policyId: policy.id,
1876
+ policyName: policy.name,
1877
+ ...policy.description !== void 0 ? { description: policy.description } : {},
1878
+ severity: policy.severity,
1879
+ onViolation: resolveOnViolation(policy),
1880
+ ruleType: policy.rule.type,
1881
+ match: m.match,
1882
+ reason: m.reason
1883
+ });
1884
+ }
1885
+ return out;
1886
+ }
1887
+ function requiredProvenanceList(required) {
1888
+ if (Array.isArray(required)) return required.join(" | ");
1889
+ return String(required);
1890
+ }
1891
+ function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
1892
+ const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
1893
+ for (const edgeId of incident) {
1894
+ const e = graph.getEdgeAttributes(edgeId);
1895
+ if (e.type !== edgeType) continue;
1896
+ if (requiredOtherEnd === void 0) return true;
1897
+ if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
1898
+ }
1899
+ return false;
1900
+ }
1901
+ function blastRadiusSubjectReaching(graph, rule, nodeId) {
1902
+ let found = null;
1903
+ graph.forEachNode((subjId, attrs) => {
1904
+ if (found !== null) return;
1905
+ if (subjId === nodeId) return;
1906
+ if (attrs.type !== rule.nodeType) return;
1907
+ const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
1908
+ if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
1909
+ });
1910
+ return found;
1911
+ }
1912
+ function matchPolicyToNode(graph, policy, nodeId, node) {
1913
+ const rule = policy.rule;
1914
+ switch (rule.type) {
1915
+ case "structural": {
1916
+ if (node.type === rule.fromNodeType) {
1917
+ return {
1918
+ match: "subject",
1919
+ reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
1920
+ };
1921
+ }
1922
+ if (node.type === rule.toNodeType) {
1923
+ return {
1924
+ match: "region",
1925
+ reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
1926
+ };
1927
+ }
1928
+ return null;
1929
+ }
1930
+ case "ownership": {
1931
+ if (node.type === rule.nodeType) {
1932
+ return {
1933
+ match: "subject",
1934
+ reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
1935
+ };
1936
+ }
1937
+ return null;
1938
+ }
1939
+ case "blast-radius": {
1940
+ const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
1941
+ if (node.type === rule.nodeType) {
1942
+ return {
1943
+ match: "subject",
1944
+ reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
1945
+ };
1946
+ }
1947
+ const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
1948
+ if (subject) {
1949
+ return {
1950
+ match: "region",
1951
+ 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`
1952
+ };
1953
+ }
1954
+ return null;
1955
+ }
1956
+ case "compatibility": {
1957
+ const kindLabel = rule.kind ?? "all compat shapes";
1958
+ if (node.type === import_types2.NodeType.ServiceNode) {
1959
+ return {
1960
+ match: "subject",
1961
+ reason: `this service's dependencies are compatibility-checked (${kindLabel})`
1962
+ };
1963
+ }
1964
+ const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
1965
+ if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
1966
+ return {
1967
+ match: "region",
1968
+ reason: "services connecting to this database have their driver/engine compatibility checked against it"
1969
+ };
1970
+ }
1971
+ return null;
1972
+ }
1973
+ case "provenance": {
1974
+ const requiredList = requiredProvenanceList(rule.required);
1975
+ if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
1976
+ return {
1977
+ match: "subject",
1978
+ reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
1979
+ };
1980
+ }
1981
+ if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
1982
+ return {
1983
+ match: "region",
1984
+ 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`
1985
+ };
1986
+ }
1987
+ return null;
1988
+ }
1989
+ }
1990
+ }
1650
1991
  async function loadPolicyFile(policyPath) {
1651
1992
  let raw;
1652
1993
  try {
@@ -1755,9 +2096,9 @@ function loadIncidentThresholdsFromEnv() {
1755
2096
  return DEFAULT_INCIDENT_THRESHOLDS;
1756
2097
  }
1757
2098
  }
1758
- function httpResponseStatus(span) {
2099
+ function httpResponseStatusFromAttrs(attrs) {
1759
2100
  for (const key of ["http.response.status_code", "http.status_code"]) {
1760
- const v = span.attributes[key];
2101
+ const v = attrs[key];
1761
2102
  if (typeof v === "number" && Number.isFinite(v)) return v;
1762
2103
  if (typeof v === "string") {
1763
2104
  const n = Number(v);
@@ -1766,6 +2107,63 @@ function httpResponseStatus(span) {
1766
2107
  }
1767
2108
  return void 0;
1768
2109
  }
2110
+ function httpResponseStatus(span) {
2111
+ return httpResponseStatusFromAttrs(span.attributes);
2112
+ }
2113
+ function httpFailureMessageFromAttrs(attrs) {
2114
+ const status2 = httpResponseStatusFromAttrs(attrs);
2115
+ const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
2116
+ const method = pickAttrFrom(attrs, "http.request.method", "http.method");
2117
+ const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
2118
+ if (status2 !== void 0 && where) return `${status2} on ${where}`;
2119
+ if (status2 !== void 0) return `HTTP ${status2}`;
2120
+ if (where) return `error on ${where}`;
2121
+ return void 0;
2122
+ }
2123
+ var GRPC_STATUS_NAMES = {
2124
+ 1: "CANCELLED",
2125
+ 2: "UNKNOWN",
2126
+ 3: "INVALID_ARGUMENT",
2127
+ 4: "DEADLINE_EXCEEDED",
2128
+ 5: "NOT_FOUND",
2129
+ 6: "ALREADY_EXISTS",
2130
+ 7: "PERMISSION_DENIED",
2131
+ 8: "RESOURCE_EXHAUSTED",
2132
+ 9: "FAILED_PRECONDITION",
2133
+ 10: "ABORTED",
2134
+ 11: "OUT_OF_RANGE",
2135
+ 12: "UNIMPLEMENTED",
2136
+ 13: "INTERNAL",
2137
+ 14: "UNAVAILABLE",
2138
+ 15: "DATA_LOSS",
2139
+ 16: "UNAUTHENTICATED"
2140
+ };
2141
+ function grpcStatusCodeFromAttrs(attrs) {
2142
+ const v = attrs["rpc.grpc.status_code"];
2143
+ if (typeof v === "number" && Number.isFinite(v)) return v;
2144
+ if (typeof v === "string") {
2145
+ const n = Number(v);
2146
+ if (Number.isFinite(n)) return n;
2147
+ }
2148
+ return void 0;
2149
+ }
2150
+ function nonHttpFailureMessageFromAttrs(attrs) {
2151
+ const grpc2 = grpcStatusCodeFromAttrs(attrs);
2152
+ if (grpc2 !== void 0 && grpc2 !== 0) {
2153
+ const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
2154
+ const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
2155
+ return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
2156
+ }
2157
+ const errType = pickAttrFrom(attrs, "error.type");
2158
+ if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
2159
+ const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
2160
+ return peer ? `${errType} connecting to ${peer}` : errType;
2161
+ }
2162
+ return void 0;
2163
+ }
2164
+ function incidentMessage(span) {
2165
+ return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
2166
+ }
1769
2167
  function nowIso(ctx) {
1770
2168
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1771
2169
  }
@@ -1785,13 +2183,16 @@ function warnNoSourceMaps(serviceName) {
1785
2183
  `[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.`
1786
2184
  );
1787
2185
  }
1788
- function pickAttr(span, ...keys) {
2186
+ function pickAttrFrom(attrs, ...keys) {
1789
2187
  for (const k of keys) {
1790
- const v = span.attributes[k];
2188
+ const v = attrs[k];
1791
2189
  if (typeof v === "string" && v.length > 0) return v;
1792
2190
  }
1793
2191
  return void 0;
1794
2192
  }
2193
+ function pickAttr(span, ...keys) {
2194
+ return pickAttrFrom(span.attributes, ...keys);
2195
+ }
1795
2196
  function hostFromUrl(u) {
1796
2197
  if (!u) return void 0;
1797
2198
  try {
@@ -1803,6 +2204,10 @@ function hostFromUrl(u) {
1803
2204
  function pickAddress(span) {
1804
2205
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1805
2206
  }
2207
+ function isLoopbackHost(host) {
2208
+ const h = host.toLowerCase();
2209
+ return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
2210
+ }
1806
2211
  var CODE_FILEPATH_ATTR = "code.filepath";
1807
2212
  var CODE_LINENO_ATTR = "code.lineno";
1808
2213
  var CODE_FUNCTION_ATTR = "code.function";
@@ -1910,15 +2315,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
1910
2315
  ...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
1911
2316
  };
1912
2317
  }
2318
+ function reconcileObservedRelPath(graph, serviceName, relPath) {
2319
+ if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
2320
+ let best = null;
2321
+ graph.forEachNode((_id, attrs) => {
2322
+ const a = attrs;
2323
+ if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
2324
+ if (a.discoveredVia === "otel") return;
2325
+ const p = a.path;
2326
+ if (!p) return;
2327
+ if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
2328
+ best = p;
2329
+ }
2330
+ });
2331
+ return best ?? relPath;
2332
+ }
1913
2333
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1914
- const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
2334
+ const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
2335
+ const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
1915
2336
  if (!graph.hasNode(fileNodeId)) {
1916
- const language = languageForExt(callSite.relPath);
2337
+ const language = languageForExt(relPath);
1917
2338
  const node = {
1918
2339
  id: fileNodeId,
1919
2340
  type: import_types3.NodeType.FileNode,
1920
2341
  service: serviceName,
1921
- path: callSite.relPath,
2342
+ path: relPath,
1922
2343
  ...language ? { language } : {},
1923
2344
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
1924
2345
  discoveredVia: "otel"
@@ -1946,6 +2367,11 @@ function makeInferredEdgeId(type, source, target) {
1946
2367
  }
1947
2368
  var INFERRED_CONFIDENCE = 0.6;
1948
2369
  var STITCH_MAX_DEPTH = 2;
2370
+ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
2371
+ import_types3.EdgeType.CALLS,
2372
+ import_types3.EdgeType.CONNECTS_TO,
2373
+ import_types3.EdgeType.DEPENDS_ON
2374
+ ]);
1949
2375
  var WIRE_SPAN_KIND_CLIENT = 3;
1950
2376
  var WIRE_SPAN_KIND_PRODUCER = 4;
1951
2377
  function spanMintsObservedEdge(kind) {
@@ -2119,6 +2545,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
2119
2545
  for (const edgeId of outbound) {
2120
2546
  const edge = graph.getEdgeAttributes(edgeId);
2121
2547
  if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
2548
+ if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
2122
2549
  if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
2123
2550
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
2124
2551
  if (!visited.has(edge.target)) {
@@ -2151,6 +2578,16 @@ async function appendErrorEvent(ctx, ev) {
2151
2578
  await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(ctx.errorsPath), { recursive: true });
2152
2579
  await import_node_fs4.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
2153
2580
  }
2581
+ function incidentAffectedNode(span, graph, scanPath) {
2582
+ const sid = (0, import_types3.serviceId)(span.service, span.env);
2583
+ const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
2584
+ const callSite = callSiteFromSpan(span, serviceNode, scanPath);
2585
+ if (callSite) {
2586
+ const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
2587
+ return (0, import_types3.fileId)(span.service, relPath);
2588
+ }
2589
+ return sid;
2590
+ }
2154
2591
  function sanitizeAttributes(attrs) {
2155
2592
  const out = {};
2156
2593
  for (const [k, v] of Object.entries(attrs)) {
@@ -2159,7 +2596,7 @@ function sanitizeAttributes(attrs) {
2159
2596
  }
2160
2597
  return out;
2161
2598
  }
2162
- function buildErrorEventForReceiver(span) {
2599
+ function buildErrorEventForReceiver(span, graph, scanPath) {
2163
2600
  if (span.statusCode !== 2) return null;
2164
2601
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
2165
2602
  const attrs = sanitizeAttributes(span.attributes);
@@ -2169,16 +2606,16 @@ function buildErrorEventForReceiver(span) {
2169
2606
  service: span.service,
2170
2607
  traceId: span.traceId,
2171
2608
  spanId: span.spanId,
2172
- errorMessage: span.exception?.message ?? "unknown error",
2609
+ errorMessage: incidentMessage(span),
2173
2610
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2174
2611
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2175
2612
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2176
- affectedNode: (0, import_types3.serviceId)(span.service, span.env)
2613
+ affectedNode: incidentAffectedNode(span, graph, scanPath)
2177
2614
  };
2178
2615
  }
2179
- function makeErrorSpanWriter(errorsPath) {
2616
+ function makeErrorSpanWriter(errorsPath, graph, scanPath) {
2180
2617
  return async (span) => {
2181
- const ev = buildErrorEventForReceiver(span);
2618
+ const ev = buildErrorEventForReceiver(span, graph, scanPath);
2182
2619
  if (!ev) return;
2183
2620
  await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(errorsPath), { recursive: true });
2184
2621
  await import_node_fs4.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
@@ -2262,7 +2699,10 @@ async function handleSpan(ctx, span) {
2262
2699
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2263
2700
  cacheSpanService(span, nowMs, callSite);
2264
2701
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2265
- const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2702
+ const callSiteEvidence = callSite ? {
2703
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2704
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2705
+ } : void 0;
2266
2706
  let affectedNode = sourceId;
2267
2707
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2268
2708
  if (span.dbSystem) {
@@ -2284,7 +2724,7 @@ async function handleSpan(ctx, span) {
2284
2724
  } else {
2285
2725
  const host = pickAddress(span);
2286
2726
  let resolvedViaAddress = false;
2287
- if (mintsFromCallerSide && host && host !== span.service) {
2727
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
2288
2728
  const targetId = resolveServiceId(ctx.graph, host, env);
2289
2729
  if (targetId && targetId !== sourceId) {
2290
2730
  upsertObservedEdge(
@@ -2319,7 +2759,11 @@ async function handleSpan(ctx, span) {
2319
2759
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2320
2760
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2321
2761
  const fallbackEvidence = parent.callSite ? {
2322
- file: parent.callSite.relPath,
2762
+ file: reconcileObservedRelPath(
2763
+ ctx.graph,
2764
+ parent.service,
2765
+ parent.callSite.relPath
2766
+ ),
2323
2767
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2324
2768
  } : void 0;
2325
2769
  upsertObservedEdge(
@@ -2344,7 +2788,7 @@ async function handleSpan(ctx, span) {
2344
2788
  service: span.service,
2345
2789
  traceId: span.traceId,
2346
2790
  spanId: span.spanId,
2347
- errorMessage: span.exception?.message ?? "unknown error",
2791
+ errorMessage: incidentMessage(span),
2348
2792
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2349
2793
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2350
2794
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -2520,12 +2964,43 @@ function startStalenessLoop(graph, options = {}) {
2520
2964
  async function readErrorEvents(errorsPath) {
2521
2965
  try {
2522
2966
  const raw = await import_node_fs4.promises.readFile(errorsPath, "utf8");
2523
- return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2967
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2968
+ return dedupeIncidents(events);
2524
2969
  } catch (err) {
2525
2970
  if (err.code === "ENOENT") return [];
2526
2971
  throw err;
2527
2972
  }
2528
2973
  }
2974
+ function isSynthesizedHttpIncident(ev) {
2975
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2976
+ if (ev.errorType) return false;
2977
+ if (!ev.attributes) return false;
2978
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2979
+ return synth !== void 0 && synth === ev.errorMessage;
2980
+ }
2981
+ function dedupeIncidents(events) {
2982
+ const seen = /* @__PURE__ */ new Set();
2983
+ const once = [];
2984
+ for (const ev of events) {
2985
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2986
+ if (key === void 0) {
2987
+ once.push(ev);
2988
+ continue;
2989
+ }
2990
+ if (seen.has(key)) continue;
2991
+ seen.add(key);
2992
+ once.push(ev);
2993
+ }
2994
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2995
+ const hasRealFailure = /* @__PURE__ */ new Set();
2996
+ for (const ev of once) {
2997
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2998
+ }
2999
+ return once.filter((ev) => {
3000
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
3001
+ return !hasRealFailure.has(groupKey(ev));
3002
+ });
3003
+ }
2529
3004
  function mergeSnapshot(graph, snapshot) {
2530
3005
  const exported = snapshot.graph;
2531
3006
  let nodesAdded = 0;
@@ -2606,10 +3081,17 @@ function isConfigFile(name) {
2606
3081
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2607
3082
  if (name === ".env" || name.startsWith(".env.")) {
2608
3083
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
3084
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2609
3085
  return { match: true, fileType: "env" };
2610
3086
  }
2611
3087
  return { match: false, fileType: "" };
2612
3088
  }
3089
+ function isNeatAuthoredEnvFile(name) {
3090
+ return name === ".env.neat";
3091
+ }
3092
+ function isNeatAuthoredSourceFile(name) {
3093
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
3094
+ }
2613
3095
  function isTestPath(filePath) {
2614
3096
  const normalised = filePath.replace(/\\/g, "/");
2615
3097
  const segments = normalised.split("/");
@@ -3315,7 +3797,9 @@ async function walkSourceFiles(dir) {
3315
3797
  if (IGNORED_DIRS.has(entry2.name)) continue;
3316
3798
  if (await isPythonVenvDir(full)) continue;
3317
3799
  await walk(full);
3318
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry2.name))) {
3800
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry2.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3801
+ // would attribute our instrumentation imports to the user's service.
3802
+ !isNeatAuthoredSourceFile(entry2.name)) {
3319
3803
  out.push(full);
3320
3804
  }
3321
3805
  }
@@ -3485,11 +3969,28 @@ function collectJsImports(node, out) {
3485
3969
  if (child) collectJsImports(child, out);
3486
3970
  }
3487
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
+ }
3488
3987
  function collectPyImports(node, out) {
3489
3988
  if (node.type === "import_from_statement") {
3490
3989
  let level = 0;
3491
3990
  let modulePath = "";
3991
+ const names = [];
3492
3992
  let pastFrom = false;
3993
+ let pastImport = false;
3493
3994
  for (let i = 0; i < node.childCount; i++) {
3494
3995
  const child = node.child(i);
3495
3996
  if (!child) continue;
@@ -3497,26 +3998,30 @@ function collectPyImports(node, out) {
3497
3998
  if (child.type === "from") pastFrom = true;
3498
3999
  continue;
3499
4000
  }
3500
- if (child.type === "import") break;
3501
- if (child.type === "relative_import") {
3502
- for (let j = 0; j < child.childCount; j++) {
3503
- const rc = child.child(j);
3504
- if (!rc) continue;
3505
- if (rc.type === "import_prefix") {
3506
- for (let k = 0; k < rc.childCount; k++) {
3507
- if (rc.child(k)?.type === ".") level++;
3508
- }
3509
- } else if (rc.type === "dotted_name") modulePath = rc.text;
4001
+ if (!pastImport) {
4002
+ if (child.type === "import") {
4003
+ pastImport = true;
4004
+ continue;
3510
4005
  }
3511
- break;
3512
- }
3513
- if (child.type === "dotted_name") {
3514
- modulePath = child.text;
3515
- 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;
3516
4020
  }
4021
+ collectImportedNames(child, names);
3517
4022
  }
3518
4023
  if (level > 0 || modulePath) {
3519
- 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) });
3520
4025
  }
3521
4026
  }
3522
4027
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3618,8 +4123,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3618
4123
  return null;
3619
4124
  }
3620
4125
  async function resolvePyImport(imp, importerPath, serviceDir) {
3621
- if (!imp.modulePath) return null;
3622
- const relPath = imp.modulePath.split(".").join("/");
3623
4126
  let baseDir;
3624
4127
  if (imp.level > 0) {
3625
4128
  baseDir = import_node_path13.default.dirname(importerPath);
@@ -3627,13 +4130,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
3627
4130
  } else {
3628
4131
  baseDir = serviceDir;
3629
4132
  }
3630
- const candidates = [import_node_path13.default.join(baseDir, `${relPath}.py`), import_node_path13.default.join(baseDir, relPath, "__init__.py")];
3631
- for (const candidate of candidates) {
3632
- if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
3633
- return toPosix2(import_node_path13.default.relative(serviceDir, candidate));
4133
+ const moduleBase = imp.modulePath ? import_node_path13.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_path13.default.join(moduleBase, `${name}.py`);
4138
+ const subpackageInit = import_node_path13.default.join(moduleBase, name, "__init__.py");
4139
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
4140
+ resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, submoduleFile)));
4141
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
4142
+ resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, subpackageInit)));
4143
+ } else {
4144
+ needModuleFile = true;
3634
4145
  }
3635
4146
  }
3636
- return null;
4147
+ if (needModuleFile) {
4148
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path13.default.join(moduleBase, "__init__.py")] : [import_node_path13.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_path13.default.relative(serviceDir, candidate)));
4152
+ break;
4153
+ }
4154
+ }
4155
+ }
4156
+ return [...resolved];
3637
4157
  }
3638
4158
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3639
4159
  const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
@@ -3674,17 +4194,18 @@ async function addImports(graph, services) {
3674
4194
  continue;
3675
4195
  }
3676
4196
  for (const imp of pyImports) {
3677
- const resolved = await resolvePyImport(imp, file.path, service.dir);
3678
- if (!resolved) continue;
3679
- edgesAdded += emitImportEdge(
3680
- graph,
3681
- service.pkg.name,
3682
- importerFileId,
3683
- relFile,
3684
- resolved,
3685
- imp.line,
3686
- imp.snippet
3687
- );
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
+ }
3688
4209
  }
3689
4210
  continue;
3690
4211
  }
@@ -4320,6 +4841,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4320
4841
  edgesAdded++;
4321
4842
  }
4322
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_path21.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_path21.default.basename(primary.sourceFile),
4854
+ path: relPath,
4855
+ fileType: isConfigFile(import_node_path21.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
+ }
4323
4874
  attachIncompatibilities(service, allConfigs);
4324
4875
  if (graph.hasNode(service.node.id)) {
4325
4876
  const current = graph.getNodeAttributes(service.node.id);
@@ -4331,6 +4882,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4331
4882
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
4332
4883
  delete updated.incompatibilities;
4333
4884
  }
4885
+ if (!service.node.dbConnectionTarget) {
4886
+ delete updated.dbConnectionTarget;
4887
+ }
4334
4888
  graph.replaceNodeAttributes(service.node.id, updated);
4335
4889
  }
4336
4890
  }
@@ -4508,7 +5062,7 @@ async function addHttpCallEdges(graph, services) {
4508
5062
  const dedupKey = `${relFile}|${targetId}`;
4509
5063
  if (seen.has(dedupKey)) continue;
4510
5064
  seen.add(dedupKey);
4511
- const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
5065
+ const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
4512
5066
  const ev = {
4513
5067
  file: relFile,
4514
5068
  line: site.line,
@@ -4528,7 +5082,7 @@ async function addHttpCallEdges(graph, services) {
4528
5082
  target: targetId,
4529
5083
  type: import_types11.EdgeType.CALLS,
4530
5084
  confidence,
4531
- confidenceKind: "hostname-shape-match",
5085
+ confidenceKind: "url-literal-service-target",
4532
5086
  evidence: ev
4533
5087
  });
4534
5088
  continue;
@@ -5034,19 +5588,29 @@ init_cjs_shims();
5034
5588
  var import_node_path30 = __toESM(require("path"), 1);
5035
5589
  var import_node_fs16 = require("fs");
5036
5590
  var import_types20 = require("@neat.is/types");
5037
- function runtimeImage(content) {
5038
- const lines = content.split("\n");
5039
- let last = null;
5040
- 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")) {
5041
5597
  const line = raw.trim();
5042
5598
  if (!line || line.startsWith("#")) continue;
5043
- if (!/^from\s+/i.test(line)) continue;
5044
- const tokens = line.split(/\s+/);
5045
- const image = tokens[1];
5046
- if (!image || image.toLowerCase() === "scratch") continue;
5047
- 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
+ }
5048
5612
  }
5049
- return last;
5613
+ return { image, ports, entrypoint: entrypoint ?? cmd };
5050
5614
  }
5051
5615
  async function addDockerfileRuntimes(graph, services, scanPath) {
5052
5616
  let nodesAdded = 0;
@@ -5065,14 +5629,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5065
5629
  );
5066
5630
  continue;
5067
5631
  }
5068
- const image = runtimeImage(content);
5069
- if (!image) continue;
5070
- const node = makeInfraNode("container-image", image);
5632
+ const facts = readDockerfile(content);
5633
+ if (!facts.image) continue;
5634
+ const node = makeInfraNode("container-image", facts.image);
5071
5635
  if (!graph.hasNode(node.id)) {
5072
5636
  graph.addNode(node.id, node);
5073
5637
  nodesAdded++;
5074
5638
  }
5075
5639
  const relDockerfile = toPosix2(import_node_path30.default.relative(service.dir, dockerfilePath));
5640
+ const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath));
5076
5641
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5077
5642
  graph,
5078
5643
  service.pkg.name,
@@ -5091,12 +5656,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5091
5656
  provenance: import_types20.Provenance.EXTRACTED,
5092
5657
  confidence: (0, import_types20.confidenceForExtracted)("structural"),
5093
5658
  evidence: {
5094
- file: toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath))
5659
+ file: evidenceFile,
5660
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
5095
5661
  }
5096
5662
  };
5097
5663
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5098
5664
  edgesAdded++;
5099
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
+ }
5100
5686
  }
5101
5687
  return { nodesAdded, edgesAdded };
5102
5688
  }
@@ -5105,7 +5691,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5105
5691
  init_cjs_shims();
5106
5692
  var import_node_fs17 = require("fs");
5107
5693
  var import_node_path31 = __toESM(require("path"), 1);
5694
+ var import_types21 = require("@neat.is/types");
5108
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;
5109
5697
  async function walkTfFiles(start, depth = 0, max = 5) {
5110
5698
  if (depth > max) return [];
5111
5699
  const out = [];
@@ -5122,24 +5710,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
5122
5710
  }
5123
5711
  return out;
5124
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
+ }
5125
5734
  async function addTerraformResources(graph, scanPath) {
5126
5735
  let nodesAdded = 0;
5736
+ let edgesAdded = 0;
5127
5737
  const files = await walkTfFiles(scanPath);
5128
5738
  for (const file of files) {
5129
5739
  const content = await import_node_fs17.promises.readFile(file, "utf8");
5740
+ const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, file));
5741
+ const resources = [];
5742
+ const byKey = /* @__PURE__ */ new Map();
5130
5743
  RESOURCE_RE.lastIndex = 0;
5131
5744
  let m;
5132
5745
  while ((m = RESOURCE_RE.exec(content)) !== null) {
5133
- const kind = m[1];
5746
+ const type = m[1];
5134
5747
  const name = m[2];
5135
- const node = makeInfraNode(kind, name, "aws");
5748
+ const node = makeInfraNode(type, name, "aws");
5136
5749
  if (!graph.hasNode(node.id)) {
5137
5750
  graph.addNode(node.id, node);
5138
5751
  nodesAdded++;
5139
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
+ }
5140
5790
  }
5141
5791
  }
5142
- return { nodesAdded, edgesAdded: 0 };
5792
+ return { nodesAdded, edgesAdded };
5143
5793
  }
5144
5794
 
5145
5795
  // src/extract/infra/k8s.ts
@@ -5217,11 +5867,11 @@ var import_node_path34 = __toESM(require("path"), 1);
5217
5867
  init_cjs_shims();
5218
5868
  var import_node_fs19 = require("fs");
5219
5869
  var import_node_path33 = __toESM(require("path"), 1);
5220
- var import_types21 = require("@neat.is/types");
5870
+ var import_types22 = require("@neat.is/types");
5221
5871
  function dropOrphanedFileNodes(graph) {
5222
5872
  const orphans = [];
5223
5873
  graph.forEachNode((id, attrs) => {
5224
- if (attrs.type !== import_types21.NodeType.FileNode) return;
5874
+ if (attrs.type !== import_types22.NodeType.FileNode) return;
5225
5875
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5226
5876
  orphans.push(id);
5227
5877
  }
@@ -5234,7 +5884,7 @@ function retireEdgesByFile(graph, file) {
5234
5884
  const toDrop = [];
5235
5885
  graph.forEachEdge((id, attrs) => {
5236
5886
  const edge = attrs;
5237
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5887
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5238
5888
  if (!edge.evidence?.file) return;
5239
5889
  if (edge.evidence.file === normalized) toDrop.push(id);
5240
5890
  });
@@ -5247,7 +5897,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5247
5897
  const bases = [scanPath, ...serviceDirs];
5248
5898
  graph.forEachEdge((id, attrs) => {
5249
5899
  const edge = attrs;
5250
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5900
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5251
5901
  const evidenceFile = edge.evidence?.file;
5252
5902
  if (!evidenceFile) return;
5253
5903
  if (import_node_path33.default.isAbsolute(evidenceFile)) {
@@ -5328,7 +5978,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5328
5978
 
5329
5979
  // src/divergences.ts
5330
5980
  init_cjs_shims();
5331
- var import_types22 = require("@neat.is/types");
5981
+ var import_types23 = require("@neat.is/types");
5332
5982
  function bucketKey(source, target, type) {
5333
5983
  return `${type}|${source}|${target}`;
5334
5984
  }
@@ -5336,22 +5986,22 @@ function bucketEdges(graph) {
5336
5986
  const buckets = /* @__PURE__ */ new Map();
5337
5987
  graph.forEachEdge((id, attrs) => {
5338
5988
  const e = attrs;
5339
- const parsed = (0, import_types22.parseEdgeId)(id);
5989
+ const parsed = (0, import_types23.parseEdgeId)(id);
5340
5990
  const provenance = parsed?.provenance ?? e.provenance;
5341
5991
  const key = bucketKey(e.source, e.target, e.type);
5342
5992
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5343
5993
  switch (provenance) {
5344
- case import_types22.Provenance.EXTRACTED:
5994
+ case import_types23.Provenance.EXTRACTED:
5345
5995
  cur.extracted = e;
5346
5996
  break;
5347
- case import_types22.Provenance.OBSERVED:
5997
+ case import_types23.Provenance.OBSERVED:
5348
5998
  cur.observed = e;
5349
5999
  break;
5350
- case import_types22.Provenance.INFERRED:
6000
+ case import_types23.Provenance.INFERRED:
5351
6001
  cur.inferred = e;
5352
6002
  break;
5353
6003
  default:
5354
- if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
6004
+ if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
5355
6005
  }
5356
6006
  buckets.set(key, cur);
5357
6007
  });
@@ -5360,7 +6010,7 @@ function bucketEdges(graph) {
5360
6010
  function nodeIsFrontier(graph, nodeId) {
5361
6011
  if (!graph.hasNode(nodeId)) return false;
5362
6012
  const attrs = graph.getNodeAttributes(nodeId);
5363
- return attrs.type === import_types22.NodeType.FrontierNode;
6013
+ return attrs.type === import_types23.NodeType.FrontierNode;
5364
6014
  }
5365
6015
  function clampConfidence(n) {
5366
6016
  if (!Number.isFinite(n)) return 0;
@@ -5379,10 +6029,16 @@ function gradedConfidence(edge) {
5379
6029
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
5380
6030
  return clampConfidence(confidenceForEdge(edge));
5381
6031
  }
6032
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6033
+ import_types23.EdgeType.CALLS,
6034
+ import_types23.EdgeType.CONNECTS_TO,
6035
+ import_types23.EdgeType.PUBLISHES_TO,
6036
+ import_types23.EdgeType.CONSUMES_FROM
6037
+ ]);
5382
6038
  function detectMissingDivergences(graph, bucket) {
5383
6039
  const out = [];
5384
- if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
5385
- if (bucket.extracted && !bucket.observed) {
6040
+ if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
6041
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5386
6042
  if (!nodeIsFrontier(graph, bucket.target)) {
5387
6043
  out.push({
5388
6044
  type: "missing-observed",
@@ -5422,7 +6078,7 @@ function declaredHostFor(svc) {
5422
6078
  function hasExtractedConfiguredBy(graph, svcId) {
5423
6079
  for (const edgeId of graph.outboundEdges(svcId)) {
5424
6080
  const e = graph.getEdgeAttributes(edgeId);
5425
- if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
6081
+ if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
5426
6082
  return true;
5427
6083
  }
5428
6084
  }
@@ -5435,10 +6091,10 @@ function detectHostMismatch(graph, svcId, svc) {
5435
6091
  const out = [];
5436
6092
  for (const edgeId of graph.outboundEdges(svcId)) {
5437
6093
  const edge = graph.getEdgeAttributes(edgeId);
5438
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5439
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
6094
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
6095
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5440
6096
  const target = graph.getNodeAttributes(edge.target);
5441
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
6097
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5442
6098
  const observedHost = target.host?.trim();
5443
6099
  if (!observedHost) continue;
5444
6100
  if (observedHost === declaredHost) continue;
@@ -5460,10 +6116,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5460
6116
  const deps = svc.dependencies ?? {};
5461
6117
  for (const edgeId of graph.outboundEdges(svcId)) {
5462
6118
  const edge = graph.getEdgeAttributes(edgeId);
5463
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5464
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
6119
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
6120
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5465
6121
  const target = graph.getNodeAttributes(edge.target);
5466
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
6122
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5467
6123
  for (const pair of compatPairs()) {
5468
6124
  if (pair.engine !== target.engine) continue;
5469
6125
  const declared = deps[pair.driver];
@@ -5516,6 +6172,23 @@ function detectCompatDivergences(graph, svcId, svc) {
5516
6172
  function involvesNode(d, nodeId) {
5517
6173
  return d.source === nodeId || d.target === nodeId;
5518
6174
  }
6175
+ function suppressHostMismatchHalves(all) {
6176
+ const observedHalf = /* @__PURE__ */ new Set();
6177
+ const declaredHalf = /* @__PURE__ */ new Set();
6178
+ for (const d of all) {
6179
+ if (d.type !== "host-mismatch") continue;
6180
+ observedHalf.add(`${d.source}->${d.target}`);
6181
+ declaredHalf.add((0, import_types23.databaseId)(d.extractedHost));
6182
+ }
6183
+ if (observedHalf.size === 0) return all;
6184
+ return all.filter((d) => {
6185
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
6186
+ return false;
6187
+ }
6188
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
6189
+ return true;
6190
+ });
6191
+ }
5519
6192
  function computeDivergences(graph, opts = {}) {
5520
6193
  const all = [];
5521
6194
  const buckets = bucketEdges(graph);
@@ -5524,12 +6197,13 @@ function computeDivergences(graph, opts = {}) {
5524
6197
  }
5525
6198
  graph.forEachNode((nodeId, attrs) => {
5526
6199
  const n = attrs;
5527
- if (n.type !== import_types22.NodeType.ServiceNode) return;
6200
+ if (n.type !== import_types23.NodeType.ServiceNode) return;
5528
6201
  const svc = n;
5529
6202
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5530
6203
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
5531
6204
  });
5532
- let filtered = all;
6205
+ const reconciled = suppressHostMismatchHalves(all);
6206
+ let filtered = reconciled;
5533
6207
  if (opts.type) {
5534
6208
  const allowed = opts.type;
5535
6209
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -5557,7 +6231,7 @@ function computeDivergences(graph, opts = {}) {
5557
6231
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5558
6232
  return a.target.localeCompare(b.target);
5559
6233
  });
5560
- return import_types22.DivergenceResultSchema.parse({
6234
+ return import_types23.DivergenceResultSchema.parse({
5561
6235
  divergences: filtered,
5562
6236
  totalAffected: filtered.length,
5563
6237
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5568,7 +6242,7 @@ function computeDivergences(graph, opts = {}) {
5568
6242
  init_cjs_shims();
5569
6243
  var import_node_fs20 = require("fs");
5570
6244
  var import_node_path35 = __toESM(require("path"), 1);
5571
- var import_types23 = require("@neat.is/types");
6245
+ var import_types24 = require("@neat.is/types");
5572
6246
  var SCHEMA_VERSION = 4;
5573
6247
  function migrateV1ToV2(payload) {
5574
6248
  const nodes = payload.graph.nodes;
@@ -5590,12 +6264,12 @@ function migrateV2ToV3(payload) {
5590
6264
  for (const edge of edges) {
5591
6265
  const attrs = edge.attributes;
5592
6266
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5593
- attrs.provenance = import_types23.Provenance.OBSERVED;
6267
+ attrs.provenance = import_types24.Provenance.OBSERVED;
5594
6268
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5595
6269
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5596
6270
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5597
6271
  if (type && source && target) {
5598
- const newId = (0, import_types23.observedEdgeId)(source, target, type);
6272
+ const newId = (0, import_types24.observedEdgeId)(source, target, type);
5599
6273
  attrs.id = newId;
5600
6274
  if (edge.key) edge.key = newId;
5601
6275
  }
@@ -5721,7 +6395,7 @@ ${NEAT_OUT_LINE}
5721
6395
 
5722
6396
  // src/summary.ts
5723
6397
  init_cjs_shims();
5724
- var import_types24 = require("@neat.is/types");
6398
+ var import_types25 = require("@neat.is/types");
5725
6399
  function renderOtelEnvBlock() {
5726
6400
  return [
5727
6401
  "for prod OTel routing, set these in your deploy platform's env:",
@@ -5731,19 +6405,19 @@ function renderOtelEnvBlock() {
5731
6405
  }
5732
6406
  function findIncompatServices(nodes) {
5733
6407
  return nodes.filter(
5734
- (n) => n.type === import_types24.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
6408
+ (n) => n.type === import_types25.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
5735
6409
  );
5736
6410
  }
5737
6411
  function servicesWithoutObserved(nodes, edges) {
5738
6412
  const seen = /* @__PURE__ */ new Set();
5739
6413
  for (const e of edges) {
5740
- if (e.provenance === import_types24.Provenance.OBSERVED) {
6414
+ if (e.provenance === import_types25.Provenance.OBSERVED) {
5741
6415
  seen.add(e.source);
5742
6416
  seen.add(e.target);
5743
6417
  }
5744
6418
  }
5745
6419
  return nodes.filter(
5746
- (n) => n.type === import_types24.NodeType.ServiceNode && !seen.has(n.id)
6420
+ (n) => n.type === import_types25.NodeType.ServiceNode && !seen.has(n.id)
5747
6421
  );
5748
6422
  }
5749
6423
  function formatDivergence(d) {
@@ -5817,15 +6491,15 @@ function formatIncompat(inc) {
5817
6491
 
5818
6492
  // src/watch.ts
5819
6493
  init_cjs_shims();
5820
- var import_node_fs27 = __toESM(require("fs"), 1);
5821
- var import_node_path44 = __toESM(require("path"), 1);
6494
+ var import_node_fs29 = __toESM(require("fs"), 1);
6495
+ var import_node_path46 = __toESM(require("path"), 1);
5822
6496
  var import_chokidar = __toESM(require("chokidar"), 1);
5823
6497
 
5824
6498
  // src/api.ts
5825
6499
  init_cjs_shims();
5826
6500
  var import_fastify = __toESM(require("fastify"), 1);
5827
6501
  var import_cors = __toESM(require("@fastify/cors"), 1);
5828
- var import_types26 = require("@neat.is/types");
6502
+ var import_types27 = require("@neat.is/types");
5829
6503
 
5830
6504
  // src/extend/index.ts
5831
6505
  init_cjs_shims();
@@ -6276,7 +6950,7 @@ init_cjs_shims();
6276
6950
  var import_node_fs25 = require("fs");
6277
6951
  var import_node_os3 = __toESM(require("os"), 1);
6278
6952
  var import_node_path40 = __toESM(require("path"), 1);
6279
- var import_types25 = require("@neat.is/types");
6953
+ var import_types26 = require("@neat.is/types");
6280
6954
  var LOCK_TIMEOUT_MS = 5e3;
6281
6955
  var LOCK_RETRY_MS = 50;
6282
6956
  function neatHome() {
@@ -6530,10 +7204,10 @@ async function readRegistry() {
6530
7204
  throw err;
6531
7205
  }
6532
7206
  const parsed = JSON.parse(raw);
6533
- return import_types25.RegistryFileSchema.parse(parsed);
7207
+ return import_types26.RegistryFileSchema.parse(parsed);
6534
7208
  }
6535
7209
  async function writeRegistry(reg) {
6536
- const validated = import_types25.RegistryFileSchema.parse(reg);
7210
+ const validated = import_types26.RegistryFileSchema.parse(reg);
6537
7211
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6538
7212
  }
6539
7213
  var ProjectNameCollisionError = class extends Error {
@@ -6845,6 +7519,18 @@ function registerRoutes(scope, ctx) {
6845
7519
  }
6846
7520
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6847
7521
  });
7522
+ scope.get(
7523
+ "/graph/observed-dependencies/:nodeId",
7524
+ async (req, reply) => {
7525
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7526
+ if (!proj) return;
7527
+ const { nodeId } = req.params;
7528
+ if (!proj.graph.hasNode(nodeId)) {
7529
+ return reply.code(404).send({ error: "node not found", id: nodeId });
7530
+ }
7531
+ return getObservedDependencies(proj.graph, nodeId);
7532
+ }
7533
+ );
6848
7534
  scope.get("/graph/divergences", async (req, reply) => {
6849
7535
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6850
7536
  if (!proj) return;
@@ -6853,11 +7539,11 @@ function registerRoutes(scope, ctx) {
6853
7539
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6854
7540
  const parsed = [];
6855
7541
  for (const c of candidates) {
6856
- const r = import_types26.DivergenceTypeSchema.safeParse(c);
7542
+ const r = import_types27.DivergenceTypeSchema.safeParse(c);
6857
7543
  if (!r.success) {
6858
7544
  return reply.code(400).send({
6859
7545
  error: `unknown divergence type "${c}"`,
6860
- allowed: import_types26.DivergenceTypeSchema.options
7546
+ allowed: import_types27.DivergenceTypeSchema.options
6861
7547
  });
6862
7548
  }
6863
7549
  parsed.push(r.data);
@@ -6905,23 +7591,29 @@ function registerRoutes(scope, ctx) {
6905
7591
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6906
7592
  return { count: sliced.length, total, events: sliced };
6907
7593
  });
7594
+ const incidentHistoryHandler = async (req, reply) => {
7595
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7596
+ if (!proj) return;
7597
+ const { nodeId } = req.params;
7598
+ if (!proj.graph.hasNode(nodeId)) {
7599
+ reply.code(404).send({ error: "node not found", id: nodeId });
7600
+ return;
7601
+ }
7602
+ const epath = errorsPathFor(proj);
7603
+ if (!epath) return { count: 0, total: 0, events: [] };
7604
+ const events = await readErrorEvents(epath);
7605
+ const filtered = events.filter(
7606
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
7607
+ );
7608
+ return { count: filtered.length, total: filtered.length, events: filtered };
7609
+ };
6908
7610
  scope.get(
6909
7611
  "/incidents/:nodeId",
6910
- async (req, reply) => {
6911
- const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6912
- if (!proj) return;
6913
- const { nodeId } = req.params;
6914
- if (!proj.graph.hasNode(nodeId)) {
6915
- return reply.code(404).send({ error: "node not found", id: nodeId });
6916
- }
6917
- const epath = errorsPathFor(proj);
6918
- if (!epath) return { count: 0, total: 0, events: [] };
6919
- const events = await readErrorEvents(epath);
6920
- const filtered = events.filter(
6921
- (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
6922
- );
6923
- return { count: filtered.length, total: filtered.length, events: filtered };
6924
- }
7612
+ incidentHistoryHandler
7613
+ );
7614
+ scope.get(
7615
+ "/graph/incident-history/:nodeId",
7616
+ incidentHistoryHandler
6925
7617
  );
6926
7618
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6927
7619
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -6930,16 +7622,16 @@ function registerRoutes(scope, ctx) {
6930
7622
  if (!proj.graph.hasNode(nodeId)) {
6931
7623
  return reply.code(404).send({ error: "node not found", id: nodeId });
6932
7624
  }
6933
- let errorEvent;
6934
7625
  const epath = errorsPathFor(proj);
6935
- if (req.query.errorId && epath) {
6936
- const events = await readErrorEvents(epath);
6937
- errorEvent = events.find((e) => e.id === req.query.errorId);
7626
+ const incidents = epath ? await readErrorEvents(epath) : [];
7627
+ let errorEvent;
7628
+ if (req.query.errorId) {
7629
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
6938
7630
  if (!errorEvent) {
6939
7631
  return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
6940
7632
  }
6941
7633
  }
6942
- const result = getRootCause(proj.graph, nodeId, errorEvent);
7634
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6943
7635
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6944
7636
  return result;
6945
7637
  });
@@ -7070,7 +7762,7 @@ function registerRoutes(scope, ctx) {
7070
7762
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
7071
7763
  let violations = await log.readAll();
7072
7764
  if (req.query.severity) {
7073
- const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
7765
+ const sev = import_types27.PolicySeveritySchema.safeParse(req.query.severity);
7074
7766
  if (!sev.success) {
7075
7767
  return reply.code(400).send({
7076
7768
  error: "invalid severity",
@@ -7084,10 +7776,32 @@ function registerRoutes(scope, ctx) {
7084
7776
  }
7085
7777
  return { violations };
7086
7778
  });
7779
+ scope.get("/policies/applicable", async (req, reply) => {
7780
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7781
+ if (!proj) return;
7782
+ const nodeId = req.query.node;
7783
+ if (!nodeId) {
7784
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7785
+ }
7786
+ const policyPath = ctx.policyFilePathFor(proj);
7787
+ let policies = [];
7788
+ if (policyPath) {
7789
+ try {
7790
+ policies = await loadPolicyFile(policyPath);
7791
+ } catch (err) {
7792
+ return reply.code(400).send({
7793
+ error: "policy.json failed to parse",
7794
+ details: err.message
7795
+ });
7796
+ }
7797
+ }
7798
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7799
+ return { node: nodeId, applicable };
7800
+ });
7087
7801
  scope.post("/policies/check", async (req, reply) => {
7088
7802
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7089
7803
  if (!proj) return;
7090
- const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7804
+ const parsed = import_types27.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7091
7805
  if (!parsed.success) {
7092
7806
  return reply.code(400).send({
7093
7807
  error: "invalid /policies/check body",
@@ -7346,12 +8060,111 @@ async function buildApi(opts) {
7346
8060
  // src/watch.ts
7347
8061
  init_auth();
7348
8062
  init_otel();
7349
- init_otel_grpc();
7350
8063
 
7351
- // src/search.ts
8064
+ // src/daemon.ts
8065
+ init_cjs_shims();
8066
+ var import_node_fs27 = require("fs");
8067
+ var import_node_path44 = __toESM(require("path"), 1);
8068
+ var import_node_module = require("module");
8069
+ init_otel();
8070
+ init_auth();
8071
+
8072
+ // src/unrouted.ts
7352
8073
  init_cjs_shims();
7353
8074
  var import_node_fs26 = require("fs");
7354
8075
  var import_node_path43 = __toESM(require("path"), 1);
8076
+
8077
+ // src/daemon.ts
8078
+ var import_types28 = require("@neat.is/types");
8079
+ function daemonJsonPath(scanPath) {
8080
+ return import_node_path44.default.join(scanPath, "neat-out", "daemon.json");
8081
+ }
8082
+ function daemonsDiscoveryDir(home) {
8083
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
8084
+ return import_node_path44.default.join(base, "daemons");
8085
+ }
8086
+ function daemonDiscoveryPath(project, home) {
8087
+ return import_node_path44.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
8088
+ }
8089
+ function sanitizeDiscoveryName(project) {
8090
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
8091
+ }
8092
+ function neatHomeFromEnv() {
8093
+ const env = process.env.NEAT_HOME;
8094
+ if (env && env.length > 0) return import_node_path44.default.resolve(env);
8095
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8096
+ return import_node_path44.default.join(home, ".neat");
8097
+ }
8098
+ async function readDaemonRecord(scanPath) {
8099
+ try {
8100
+ const raw = await import_node_fs27.promises.readFile(daemonJsonPath(scanPath), "utf8");
8101
+ const parsed = JSON.parse(raw);
8102
+ if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
8103
+ return parsed;
8104
+ }
8105
+ return null;
8106
+ } catch {
8107
+ return null;
8108
+ }
8109
+ }
8110
+ function resolveNeatVersion() {
8111
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
8112
+ return process.env.NEAT_LOCAL_VERSION;
8113
+ }
8114
+ try {
8115
+ const req = (0, import_node_module.createRequire)(importMetaUrl);
8116
+ const pkg = req("../package.json");
8117
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8118
+ } catch {
8119
+ return "0.0.0";
8120
+ }
8121
+ }
8122
+ async function writeDaemonRecord(record, home) {
8123
+ const body = JSON.stringify(record, null, 2) + "\n";
8124
+ await writeAtomically(daemonJsonPath(record.projectPath), body);
8125
+ try {
8126
+ await writeAtomically(daemonDiscoveryPath(record.project, home), body);
8127
+ } catch (err) {
8128
+ console.warn(
8129
+ `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
8130
+ );
8131
+ }
8132
+ }
8133
+ async function clearDaemonRecord(record, home) {
8134
+ try {
8135
+ const stopped = { ...record, status: "stopped" };
8136
+ await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
8137
+ } catch {
8138
+ }
8139
+ try {
8140
+ await import_node_fs27.promises.unlink(daemonDiscoveryPath(record.project, home));
8141
+ } catch {
8142
+ }
8143
+ }
8144
+ function portFromListenAddress(address, fallback) {
8145
+ try {
8146
+ const port = new URL(address).port;
8147
+ const n = Number.parseInt(port, 10);
8148
+ if (Number.isFinite(n) && n > 0) return n;
8149
+ } catch {
8150
+ }
8151
+ return fallback;
8152
+ }
8153
+ function resolveHost(opts, authTokenSet) {
8154
+ if (opts.host && opts.host.length > 0) return opts.host;
8155
+ const env = process.env.HOST;
8156
+ if (env && env.length > 0) return env;
8157
+ if (!authTokenSet) return "127.0.0.1";
8158
+ return "0.0.0.0";
8159
+ }
8160
+
8161
+ // src/watch.ts
8162
+ init_otel_grpc();
8163
+
8164
+ // src/search.ts
8165
+ init_cjs_shims();
8166
+ var import_node_fs28 = require("fs");
8167
+ var import_node_path45 = __toESM(require("path"), 1);
7355
8168
  var import_node_crypto3 = require("crypto");
7356
8169
  var DEFAULT_LIMIT = 10;
7357
8170
  var NOMIC_DIM = 768;
@@ -7481,7 +8294,7 @@ async function pickEmbedder() {
7481
8294
  }
7482
8295
  async function readCache(cachePath) {
7483
8296
  try {
7484
- const raw = await import_node_fs26.promises.readFile(cachePath, "utf8");
8297
+ const raw = await import_node_fs28.promises.readFile(cachePath, "utf8");
7485
8298
  const parsed = JSON.parse(raw);
7486
8299
  if (parsed.version !== 1) return null;
7487
8300
  return parsed;
@@ -7490,8 +8303,8 @@ async function readCache(cachePath) {
7490
8303
  }
7491
8304
  }
7492
8305
  async function writeCache(cachePath, cache) {
7493
- await import_node_fs26.promises.mkdir(import_node_path43.default.dirname(cachePath), { recursive: true });
7494
- await import_node_fs26.promises.writeFile(cachePath, JSON.stringify(cache));
8306
+ await import_node_fs28.promises.mkdir(import_node_path45.default.dirname(cachePath), { recursive: true });
8307
+ await import_node_fs28.promises.writeFile(cachePath, JSON.stringify(cache));
7495
8308
  }
7496
8309
  var VectorIndex = class {
7497
8310
  constructor(embedder, cachePath) {
@@ -7639,6 +8452,7 @@ async function buildSearchIndex(graph, options = {}) {
7639
8452
  var ALL_PHASES = [
7640
8453
  "services",
7641
8454
  "aliases",
8455
+ "files",
7642
8456
  "imports",
7643
8457
  "databases",
7644
8458
  "configs",
@@ -7647,8 +8461,8 @@ var ALL_PHASES = [
7647
8461
  ];
7648
8462
  function classifyChange(relPath) {
7649
8463
  const phases = /* @__PURE__ */ new Set();
7650
- const base = import_node_path44.default.basename(relPath).toLowerCase();
7651
- const segments = relPath.split(import_node_path44.default.sep).map((s) => s.toLowerCase());
8464
+ const base = import_node_path46.default.basename(relPath).toLowerCase();
8465
+ const segments = relPath.split(import_node_path46.default.sep).map((s) => s.toLowerCase());
7652
8466
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
7653
8467
  phases.add("services");
7654
8468
  phases.add("aliases");
@@ -7663,6 +8477,7 @@ function classifyChange(relPath) {
7663
8477
  phases.add("aliases");
7664
8478
  }
7665
8479
  if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
8480
+ phases.add("files");
7666
8481
  phases.add("imports");
7667
8482
  phases.add("calls");
7668
8483
  }
@@ -7685,6 +8500,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
7685
8500
  if (phases.has("aliases")) {
7686
8501
  await addServiceAliases(graph, scanPath, services);
7687
8502
  }
8503
+ if (phases.has("files")) {
8504
+ const r = await addFiles(graph, services);
8505
+ nodesAdded += r.nodesAdded;
8506
+ edgesAdded += r.edgesAdded;
8507
+ }
7688
8508
  if (phases.has("imports")) {
7689
8509
  const r = await addImports(graph, services);
7690
8510
  nodesAdded += r.nodesAdded;
@@ -7719,6 +8539,7 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
7719
8539
  durationMs: Date.now() - started
7720
8540
  };
7721
8541
  }
8542
+ var DEFAULT_WATCH_WEB_PORT = 6328;
7722
8543
  var IGNORED_WATCH_GLOBS = [
7723
8544
  "**/node_modules/**",
7724
8545
  "**/.git/**",
@@ -7762,16 +8583,16 @@ function countWatchableDirs(scanPath, limit) {
7762
8583
  if (count >= limit) return;
7763
8584
  let entries;
7764
8585
  try {
7765
- entries = import_node_fs27.default.readdirSync(dir, { withFileTypes: true });
8586
+ entries = import_node_fs29.default.readdirSync(dir, { withFileTypes: true });
7766
8587
  } catch {
7767
8588
  return;
7768
8589
  }
7769
8590
  for (const e of entries) {
7770
8591
  if (count >= limit) return;
7771
8592
  if (!e.isDirectory()) continue;
7772
- if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path44.default.join(dir, e.name) + import_node_path44.default.sep))) continue;
8593
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path46.default.join(dir, e.name) + import_node_path46.default.sep))) continue;
7773
8594
  count++;
7774
- if (depth < 2) visit(import_node_path44.default.join(dir, e.name), depth + 1);
8595
+ if (depth < 2) visit(import_node_path46.default.join(dir, e.name), depth + 1);
7775
8596
  }
7776
8597
  };
7777
8598
  visit(scanPath, 0);
@@ -7789,8 +8610,8 @@ async function startWatch(graph, opts) {
7789
8610
  const projectName = opts.project ?? DEFAULT_PROJECT;
7790
8611
  await loadGraphFromDisk(graph, opts.outPath);
7791
8612
  const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
7792
- const policyFilePath = import_node_path44.default.join(opts.scanPath, "policy.json");
7793
- const policyViolationsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), "policy-violations.ndjson");
8613
+ const policyFilePath = import_node_path46.default.join(opts.scanPath, "policy.json");
8614
+ const policyViolationsPath = import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "policy-violations.ndjson");
7794
8615
  let policies = [];
7795
8616
  try {
7796
8617
  policies = await loadPolicyFile(policyFilePath);
@@ -7841,7 +8662,7 @@ async function startWatch(graph, opts) {
7841
8662
  assertBindAuthority(host, auth.authToken);
7842
8663
  const port = opts.port ?? 8080;
7843
8664
  const otelPort = opts.otelPort ?? 4318;
7844
- const cachePath = opts.embeddingsCachePath ?? import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), "embeddings.json");
8665
+ const cachePath = opts.embeddingsCachePath ?? import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "embeddings.json");
7845
8666
  let searchIndex;
7846
8667
  try {
7847
8668
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -7859,7 +8680,7 @@ async function startWatch(graph, opts) {
7859
8680
  // Paths are derived from the explicit options the watch caller passes
7860
8681
  // — pathsForProject is only used to fill in the embeddings/snapshot
7861
8682
  // fields so the registry shape is complete.
7862
- ...pathsForProject(projectName, import_node_path44.default.dirname(opts.outPath)),
8683
+ ...pathsForProject(projectName, import_node_path46.default.dirname(opts.outPath)),
7863
8684
  snapshotPath: opts.outPath,
7864
8685
  errorsPath: opts.errorsPath,
7865
8686
  staleEventsPath: opts.staleEventsPath
@@ -7867,7 +8688,7 @@ async function startWatch(graph, opts) {
7867
8688
  searchIndex
7868
8689
  });
7869
8690
  const api = await buildApi({ projects: registry });
7870
- await api.listen({ port, host });
8691
+ const restAddress = await api.listen({ port, host });
7871
8692
  console.log(`neat-core listening on http://${host}:${port}`);
7872
8693
  console.log(` scan path: ${opts.scanPath} (watching for changes)`);
7873
8694
  console.log(` snapshot path: ${opts.outPath}`);
@@ -7880,10 +8701,41 @@ async function startWatch(graph, opts) {
7880
8701
  writeErrorEventInline: false,
7881
8702
  onPolicyTrigger
7882
8703
  });
7883
- const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath);
8704
+ const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath, graph, opts.scanPath);
7884
8705
  const otelHttp = await buildOtelReceiver({ onSpan, onErrorSpanSync });
7885
- await otelHttp.listen({ port: otelPort, host });
7886
- console.log(`neat-core OTLP receiver on http://${host}:${otelPort}/v1/traces`);
8706
+ const otelAddress = await listenSteppingOtlp(otelHttp, otelPort, host);
8707
+ const boundOtelPort = portFromListenAddress(otelAddress, otelPort);
8708
+ console.log(`neat-core OTLP receiver on ${otelAddress}/v1/traces`);
8709
+ const boundRestPort = portFromListenAddress(restAddress, port);
8710
+ const daemonRecord = {
8711
+ project: projectName,
8712
+ projectPath: opts.scanPath,
8713
+ pid: process.pid,
8714
+ status: "running",
8715
+ ports: {
8716
+ rest: boundRestPort,
8717
+ otlp: boundOtelPort,
8718
+ // watch serves no dashboard of its own; record the canonical web port so
8719
+ // the record shape is valid. Only ports.otlp is load-bearing here.
8720
+ web: DEFAULT_WATCH_WEB_PORT
8721
+ },
8722
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8723
+ neatVersion: resolveNeatVersion()
8724
+ };
8725
+ try {
8726
+ await writeDaemonRecord(daemonRecord);
8727
+ console.log(
8728
+ `neat watch: wrote daemon.json (REST ${boundRestPort} / OTLP ${boundOtelPort})`
8729
+ );
8730
+ } catch (err) {
8731
+ await api.close().catch(() => {
8732
+ });
8733
+ await otelHttp.close().catch(() => {
8734
+ });
8735
+ throw new Error(
8736
+ `neat watch: failed to write daemon.json \u2014 ${err.message}`
8737
+ );
8738
+ }
7887
8739
  let grpcReceiver = null;
7888
8740
  if (opts.otelGrpc) {
7889
8741
  const grpcPort = opts.otelGrpcPort ?? 4317;
@@ -7946,9 +8798,9 @@ async function startWatch(graph, opts) {
7946
8798
  };
7947
8799
  const onPath = (absPath) => {
7948
8800
  if (shouldIgnore(absPath)) return;
7949
- const rel = import_node_path44.default.relative(opts.scanPath, absPath);
8801
+ const rel = import_node_path46.default.relative(opts.scanPath, absPath);
7950
8802
  if (!rel || rel.startsWith("..")) return;
7951
- pendingPaths.add(rel.split(import_node_path44.default.sep).join("/"));
8803
+ pendingPaths.add(rel.split(import_node_path46.default.sep).join("/"));
7952
8804
  const phases = classifyChange(rel);
7953
8805
  if (phases.size === 0) {
7954
8806
  for (const p of ALL_PHASES) pending.add(p);
@@ -7996,14 +8848,16 @@ async function startWatch(graph, opts) {
7996
8848
  await api.close();
7997
8849
  await otelHttp.close();
7998
8850
  if (grpcReceiver) await grpcReceiver.stop();
8851
+ await clearDaemonRecord(daemonRecord).catch(() => {
8852
+ });
7999
8853
  };
8000
8854
  return { api, stop };
8001
8855
  }
8002
8856
 
8003
8857
  // src/deploy/detect.ts
8004
8858
  init_cjs_shims();
8005
- var import_node_fs28 = require("fs");
8006
- var import_node_path45 = __toESM(require("path"), 1);
8859
+ var import_node_fs30 = require("fs");
8860
+ var import_node_path47 = __toESM(require("path"), 1);
8007
8861
  var import_node_child_process2 = require("child_process");
8008
8862
  var import_node_crypto4 = require("crypto");
8009
8863
  function generateToken() {
@@ -8103,21 +8957,21 @@ async function runDeploy(opts = {}) {
8103
8957
  const token = generateToken();
8104
8958
  switch (substrate) {
8105
8959
  case "docker-compose": {
8106
- const artifactPath = import_node_path45.default.join(cwd, "docker-compose.neat.yml");
8960
+ const artifactPath = import_node_path47.default.join(cwd, "docker-compose.neat.yml");
8107
8961
  const contents = emitDockerCompose(cwd);
8108
- await import_node_fs28.promises.writeFile(artifactPath, contents, "utf8");
8962
+ await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
8109
8963
  return {
8110
8964
  substrate,
8111
8965
  artifactPath,
8112
8966
  token,
8113
8967
  contents,
8114
- startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path45.default.basename(artifactPath)} up -d`
8968
+ startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path47.default.basename(artifactPath)} up -d`
8115
8969
  };
8116
8970
  }
8117
8971
  case "systemd": {
8118
- const artifactPath = import_node_path45.default.join(cwd, "neat.service");
8972
+ const artifactPath = import_node_path47.default.join(cwd, "neat.service");
8119
8973
  const contents = emitSystemdUnit(cwd);
8120
- await import_node_fs28.promises.writeFile(artifactPath, contents, "utf8");
8974
+ await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
8121
8975
  return {
8122
8976
  substrate,
8123
8977
  artifactPath,
@@ -8150,8 +9004,8 @@ init_cjs_shims();
8150
9004
 
8151
9005
  // src/installers/javascript.ts
8152
9006
  init_cjs_shims();
8153
- var import_node_fs29 = require("fs");
8154
- var import_node_path46 = __toESM(require("path"), 1);
9007
+ var import_node_fs31 = require("fs");
9008
+ var import_node_path48 = __toESM(require("path"), 1);
8155
9009
  var import_semver2 = __toESM(require("semver"), 1);
8156
9010
 
8157
9011
  // src/installers/templates.ts
@@ -8747,15 +9601,15 @@ var OTEL_ENV = {
8747
9601
  value: "http://localhost:4318/projects/<project>/v1/traces"
8748
9602
  };
8749
9603
  function serviceNodeName(pkg, serviceDir) {
8750
- return pkg.name ?? import_node_path46.default.basename(serviceDir);
9604
+ return pkg.name ?? import_node_path48.default.basename(serviceDir);
8751
9605
  }
8752
9606
  function projectToken(pkg, serviceDir, project) {
8753
9607
  if (project && project.length > 0) return project;
8754
- return pkg.name ?? import_node_path46.default.basename(serviceDir);
9608
+ return pkg.name ?? import_node_path48.default.basename(serviceDir);
8755
9609
  }
8756
9610
  async function readJsonFile(p) {
8757
9611
  try {
8758
- const raw = await import_node_fs29.promises.readFile(p, "utf8");
9612
+ const raw = await import_node_fs31.promises.readFile(p, "utf8");
8759
9613
  return JSON.parse(raw);
8760
9614
  } catch {
8761
9615
  return null;
@@ -8764,16 +9618,16 @@ async function readJsonFile(p) {
8764
9618
  async function detectRuntimeKind(pkgRoot, pkg) {
8765
9619
  const deps = allDeps(pkg);
8766
9620
  if ("react-native" in deps || "expo" in deps) return "react-native";
8767
- const appJson = await readJsonFile(import_node_path46.default.join(pkgRoot, "app.json"));
9621
+ const appJson = await readJsonFile(import_node_path48.default.join(pkgRoot, "app.json"));
8768
9622
  if (appJson && typeof appJson === "object" && "expo" in appJson) {
8769
9623
  return "react-native";
8770
9624
  }
8771
- if (await exists3(import_node_path46.default.join(pkgRoot, "vite.config.js")) || await exists3(import_node_path46.default.join(pkgRoot, "vite.config.ts")) || await exists3(import_node_path46.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
9625
+ if (await exists3(import_node_path48.default.join(pkgRoot, "vite.config.js")) || await exists3(import_node_path48.default.join(pkgRoot, "vite.config.ts")) || await exists3(import_node_path48.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
8772
9626
  return "browser-bundle";
8773
9627
  }
8774
- if (await exists3(import_node_path46.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
8775
- if (await exists3(import_node_path46.default.join(pkgRoot, "bun.lockb"))) return "bun";
8776
- if (await exists3(import_node_path46.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path46.default.join(pkgRoot, "deno.lock"))) {
9628
+ if (await exists3(import_node_path48.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
9629
+ if (await exists3(import_node_path48.default.join(pkgRoot, "bun.lockb"))) return "bun";
9630
+ if (await exists3(import_node_path48.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path48.default.join(pkgRoot, "deno.lock"))) {
8777
9631
  return "deno";
8778
9632
  }
8779
9633
  const engines = pkg.engines ?? {};
@@ -8782,7 +9636,7 @@ async function detectRuntimeKind(pkgRoot, pkg) {
8782
9636
  }
8783
9637
  async function readPackageJson2(serviceDir) {
8784
9638
  try {
8785
- const raw = await import_node_fs29.promises.readFile(import_node_path46.default.join(serviceDir, "package.json"), "utf8");
9639
+ const raw = await import_node_fs31.promises.readFile(import_node_path48.default.join(serviceDir, "package.json"), "utf8");
8786
9640
  return JSON.parse(raw);
8787
9641
  } catch {
8788
9642
  return null;
@@ -8790,7 +9644,7 @@ async function readPackageJson2(serviceDir) {
8790
9644
  }
8791
9645
  async function exists3(p) {
8792
9646
  try {
8793
- await import_node_fs29.promises.stat(p);
9647
+ await import_node_fs31.promises.stat(p);
8794
9648
  return true;
8795
9649
  } catch {
8796
9650
  return false;
@@ -8798,7 +9652,7 @@ async function exists3(p) {
8798
9652
  }
8799
9653
  async function readFileMaybe(p) {
8800
9654
  try {
8801
- return await import_node_fs29.promises.readFile(p, "utf8");
9655
+ return await import_node_fs31.promises.readFile(p, "utf8");
8802
9656
  } catch {
8803
9657
  return null;
8804
9658
  }
@@ -8826,7 +9680,7 @@ function needsVersionUpgrade(installed, expected) {
8826
9680
  var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
8827
9681
  async function findNextConfig(serviceDir) {
8828
9682
  for (const name of NEXT_CONFIG_CANDIDATES) {
8829
- const candidate = import_node_path46.default.join(serviceDir, name);
9683
+ const candidate = import_node_path48.default.join(serviceDir, name);
8830
9684
  if (await exists3(candidate)) return candidate;
8831
9685
  }
8832
9686
  return null;
@@ -8852,9 +9706,23 @@ var WEB_FRAMEWORK_DEPS = [
8852
9706
  "polka",
8853
9707
  "micro"
8854
9708
  ];
8855
- function looksLikeWebApp(pkg) {
9709
+ var WORKER_FRAMEWORK_DEPS = [
9710
+ "bullmq",
9711
+ "bull",
9712
+ "bee-queue",
9713
+ "agenda",
9714
+ "kafkajs",
9715
+ "amqplib",
9716
+ "amqp-connection-manager",
9717
+ "nats",
9718
+ "node-resque",
9719
+ "pg-boss",
9720
+ "graphile-worker"
9721
+ ];
9722
+ var APP_FRAMEWORK_DEPS = [...WEB_FRAMEWORK_DEPS, ...WORKER_FRAMEWORK_DEPS];
9723
+ function appFrameworkDependencies(pkg) {
8856
9724
  const deps = allDeps(pkg);
8857
- return WEB_FRAMEWORK_DEPS.some((name) => name in deps);
9725
+ return APP_FRAMEWORK_DEPS.filter((name) => name in deps);
8858
9726
  }
8859
9727
  function uninstrumentedLibraries(pkg) {
8860
9728
  const deps = allDeps(pkg);
@@ -8881,7 +9749,7 @@ function hasRemixDependency(pkg) {
8881
9749
  }
8882
9750
  async function findRemixEntry(serviceDir) {
8883
9751
  for (const rel of REMIX_ENTRY_CANDIDATES) {
8884
- const candidate = import_node_path46.default.join(serviceDir, rel);
9752
+ const candidate = import_node_path48.default.join(serviceDir, rel);
8885
9753
  if (await exists3(candidate)) return candidate;
8886
9754
  }
8887
9755
  return null;
@@ -8893,14 +9761,14 @@ function hasSvelteKitDependency(pkg) {
8893
9761
  }
8894
9762
  async function findSvelteKitHooks(serviceDir) {
8895
9763
  for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
8896
- const candidate = import_node_path46.default.join(serviceDir, rel);
9764
+ const candidate = import_node_path48.default.join(serviceDir, rel);
8897
9765
  if (await exists3(candidate)) return candidate;
8898
9766
  }
8899
9767
  return null;
8900
9768
  }
8901
9769
  async function findSvelteKitConfig(serviceDir) {
8902
9770
  for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
8903
- const candidate = import_node_path46.default.join(serviceDir, rel);
9771
+ const candidate = import_node_path48.default.join(serviceDir, rel);
8904
9772
  if (await exists3(candidate)) return candidate;
8905
9773
  }
8906
9774
  return null;
@@ -8911,7 +9779,7 @@ function hasNuxtDependency(pkg) {
8911
9779
  }
8912
9780
  async function findNuxtConfig(serviceDir) {
8913
9781
  for (const name of NUXT_CONFIG_CANDIDATES) {
8914
- const candidate = import_node_path46.default.join(serviceDir, name);
9782
+ const candidate = import_node_path48.default.join(serviceDir, name);
8915
9783
  if (await exists3(candidate)) return candidate;
8916
9784
  }
8917
9785
  return null;
@@ -8922,7 +9790,7 @@ function hasAstroDependency(pkg) {
8922
9790
  }
8923
9791
  async function findAstroConfig(serviceDir) {
8924
9792
  for (const name of ASTRO_CONFIG_CANDIDATES) {
8925
- const candidate = import_node_path46.default.join(serviceDir, name);
9793
+ const candidate = import_node_path48.default.join(serviceDir, name);
8926
9794
  if (await exists3(candidate)) return candidate;
8927
9795
  }
8928
9796
  return null;
@@ -8936,15 +9804,15 @@ function parseNextMajor(range) {
8936
9804
  return Number.isFinite(n) ? n : null;
8937
9805
  }
8938
9806
  async function isTypeScriptProject(serviceDir) {
8939
- return exists3(import_node_path46.default.join(serviceDir, "tsconfig.json"));
9807
+ return exists3(import_node_path48.default.join(serviceDir, "tsconfig.json"));
8940
9808
  }
8941
9809
  var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
8942
9810
  var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
8943
9811
  var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
8944
- var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
9812
+ var SRC_NAMED_CANDIDATES = ["server", "main", "app", "worker"].flatMap(
8945
9813
  (name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
8946
9814
  );
8947
- var ROOT_NAMED_CANDIDATES = ["server", "app", "main"].flatMap(
9815
+ var ROOT_NAMED_CANDIDATES = ["server", "app", "main", "worker"].flatMap(
8948
9816
  (name) => INDEX_EXTENSIONS.map((ext) => `${name}${ext}`)
8949
9817
  );
8950
9818
  var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
@@ -8985,7 +9853,7 @@ function entryFromScript(script) {
8985
9853
  }
8986
9854
  async function resolveEntry(serviceDir, pkg) {
8987
9855
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
8988
- const candidate = import_node_path46.default.resolve(serviceDir, pkg.main);
9856
+ const candidate = import_node_path48.default.resolve(serviceDir, pkg.main);
8989
9857
  if (await exists3(candidate)) return candidate;
8990
9858
  }
8991
9859
  if (pkg.bin) {
@@ -8999,40 +9867,40 @@ async function resolveEntry(serviceDir, pkg) {
8999
9867
  if (typeof first === "string") binEntry = first;
9000
9868
  }
9001
9869
  if (binEntry) {
9002
- const candidate = import_node_path46.default.resolve(serviceDir, binEntry);
9870
+ const candidate = import_node_path48.default.resolve(serviceDir, binEntry);
9003
9871
  if (await exists3(candidate)) return candidate;
9004
9872
  }
9005
9873
  }
9006
9874
  const startEntry = entryFromScript(pkg.scripts?.start);
9007
9875
  if (startEntry) {
9008
- const candidate = import_node_path46.default.resolve(serviceDir, startEntry);
9876
+ const candidate = import_node_path48.default.resolve(serviceDir, startEntry);
9009
9877
  if (await exists3(candidate)) return candidate;
9010
9878
  }
9011
9879
  const devEntry = entryFromScript(pkg.scripts?.dev);
9012
9880
  if (devEntry) {
9013
- const candidate = import_node_path46.default.resolve(serviceDir, devEntry);
9881
+ const candidate = import_node_path48.default.resolve(serviceDir, devEntry);
9014
9882
  if (await exists3(candidate)) return candidate;
9015
9883
  }
9016
9884
  for (const rel of SRC_INDEX_CANDIDATES) {
9017
- const candidate = import_node_path46.default.join(serviceDir, rel);
9885
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9018
9886
  if (await exists3(candidate)) return candidate;
9019
9887
  }
9020
9888
  for (const rel of SRC_NAMED_CANDIDATES) {
9021
- const candidate = import_node_path46.default.join(serviceDir, rel);
9889
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9022
9890
  if (await exists3(candidate)) return candidate;
9023
9891
  }
9024
9892
  for (const rel of ROOT_NAMED_CANDIDATES) {
9025
- const candidate = import_node_path46.default.join(serviceDir, rel);
9893
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9026
9894
  if (await exists3(candidate)) return candidate;
9027
9895
  }
9028
9896
  for (const name of INDEX_CANDIDATES) {
9029
- const candidate = import_node_path46.default.join(serviceDir, name);
9897
+ const candidate = import_node_path48.default.join(serviceDir, name);
9030
9898
  if (await exists3(candidate)) return candidate;
9031
9899
  }
9032
9900
  return null;
9033
9901
  }
9034
9902
  function dispatchEntry(entryFile, pkg) {
9035
- const ext = import_node_path46.default.extname(entryFile).toLowerCase();
9903
+ const ext = import_node_path48.default.extname(entryFile).toLowerCase();
9036
9904
  if (ext === ".ts" || ext === ".tsx") return "ts";
9037
9905
  if (ext === ".mjs") return "esm";
9038
9906
  if (ext === ".cjs") return "cjs";
@@ -9049,9 +9917,9 @@ function otelInitContents(flavor) {
9049
9917
  return OTEL_INIT_CJS;
9050
9918
  }
9051
9919
  function injectionLine(flavor, entryFile, otelInitFile) {
9052
- let rel = import_node_path46.default.relative(import_node_path46.default.dirname(entryFile), otelInitFile);
9920
+ let rel = import_node_path48.default.relative(import_node_path48.default.dirname(entryFile), otelInitFile);
9053
9921
  if (!rel.startsWith(".")) rel = `./${rel}`;
9054
- rel = rel.split(import_node_path46.default.sep).join("/");
9922
+ rel = rel.split(import_node_path48.default.sep).join("/");
9055
9923
  if (flavor === "cjs") return `require('${rel}')`;
9056
9924
  if (flavor === "esm") return `import '${rel}'`;
9057
9925
  const tsRel = rel.replace(/\.ts$/, "");
@@ -9064,23 +9932,23 @@ function lineIsOtelInjection(line) {
9064
9932
  }
9065
9933
  async function detectsSrcLayout(serviceDir) {
9066
9934
  const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
9067
- exists3(import_node_path46.default.join(serviceDir, "src", "app")),
9068
- exists3(import_node_path46.default.join(serviceDir, "src", "pages")),
9069
- exists3(import_node_path46.default.join(serviceDir, "app")),
9070
- exists3(import_node_path46.default.join(serviceDir, "pages"))
9935
+ exists3(import_node_path48.default.join(serviceDir, "src", "app")),
9936
+ exists3(import_node_path48.default.join(serviceDir, "src", "pages")),
9937
+ exists3(import_node_path48.default.join(serviceDir, "app")),
9938
+ exists3(import_node_path48.default.join(serviceDir, "pages"))
9071
9939
  ]);
9072
9940
  return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
9073
9941
  }
9074
9942
  async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
9075
9943
  const useTs = await isTypeScriptProject(serviceDir);
9076
9944
  const srcLayout = await detectsSrcLayout(serviceDir);
9077
- const baseDir = srcLayout ? import_node_path46.default.join(serviceDir, "src") : serviceDir;
9078
- const instrumentationFile = import_node_path46.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
9079
- const instrumentationNodeFile = import_node_path46.default.join(
9945
+ const baseDir = srcLayout ? import_node_path48.default.join(serviceDir, "src") : serviceDir;
9946
+ const instrumentationFile = import_node_path48.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
9947
+ const instrumentationNodeFile = import_node_path48.default.join(
9080
9948
  baseDir,
9081
9949
  useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
9082
9950
  );
9083
- const envNeatFile = import_node_path46.default.join(baseDir, ".env.neat");
9951
+ const envNeatFile = import_node_path48.default.join(baseDir, ".env.neat");
9084
9952
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
9085
9953
  const dependencyEdits = [];
9086
9954
  for (const sdk of SDK_PACKAGES) {
@@ -9137,7 +10005,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
9137
10005
  const nextMajor = parseNextMajor(nextRange);
9138
10006
  if (nextMajor !== null && nextMajor < 15) {
9139
10007
  try {
9140
- const raw = await import_node_fs29.promises.readFile(nextConfigPath, "utf8");
10008
+ const raw = await import_node_fs31.promises.readFile(nextConfigPath, "utf8");
9141
10009
  if (!raw.includes("instrumentationHook")) {
9142
10010
  nextConfigEdit = {
9143
10011
  file: nextConfigPath,
@@ -9185,7 +10053,7 @@ function buildDependencyEdits(pkg, manifestPath) {
9185
10053
  return edits;
9186
10054
  }
9187
10055
  async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
9188
- const envNeatFile = import_node_path46.default.join(serviceDir, ".env.neat");
10056
+ const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
9189
10057
  if (!await exists3(envNeatFile)) {
9190
10058
  generatedFiles.push({
9191
10059
  file: envNeatFile,
@@ -9220,7 +10088,7 @@ function fileImportsOtelHook(raw, specifiers) {
9220
10088
  }
9221
10089
  async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
9222
10090
  const useTs = await isTypeScriptProject(serviceDir);
9223
- const otelServerFile = import_node_path46.default.join(
10091
+ const otelServerFile = import_node_path48.default.join(
9224
10092
  serviceDir,
9225
10093
  useTs ? "app/otel.server.ts" : "app/otel.server.js"
9226
10094
  );
@@ -9241,7 +10109,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
9241
10109
  await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
9242
10110
  const entrypointEdits = [];
9243
10111
  try {
9244
- const raw = await import_node_fs29.promises.readFile(entryFile, "utf8");
10112
+ const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
9245
10113
  if (!fileImportsOtelHook(raw, ["./otel.server"])) {
9246
10114
  const lines = raw.split(/\r?\n/);
9247
10115
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
@@ -9277,11 +10145,11 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
9277
10145
  }
9278
10146
  async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
9279
10147
  const useTs = await isTypeScriptProject(serviceDir);
9280
- const otelInitFile = import_node_path46.default.join(
10148
+ const otelInitFile = import_node_path48.default.join(
9281
10149
  serviceDir,
9282
10150
  useTs ? "src/otel-init.ts" : "src/otel-init.js"
9283
10151
  );
9284
- const resolvedHooksFile = hooksFile ?? import_node_path46.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
10152
+ const resolvedHooksFile = hooksFile ?? import_node_path48.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
9285
10153
  const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
9286
10154
  const generatedFiles = [];
9287
10155
  const entrypointEdits = [];
@@ -9306,7 +10174,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
9306
10174
  });
9307
10175
  } else {
9308
10176
  try {
9309
- const raw = await import_node_fs29.promises.readFile(hooksFile, "utf8");
10177
+ const raw = await import_node_fs31.promises.readFile(hooksFile, "utf8");
9310
10178
  if (!fileImportsOtelHook(raw, ["./otel-init"])) {
9311
10179
  const lines = raw.split(/\r?\n/);
9312
10180
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
@@ -9343,11 +10211,11 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
9343
10211
  }
9344
10212
  async function planNuxt(serviceDir, pkg, manifestPath, project) {
9345
10213
  const useTs = await isTypeScriptProject(serviceDir);
9346
- const otelPluginFile = import_node_path46.default.join(
10214
+ const otelPluginFile = import_node_path48.default.join(
9347
10215
  serviceDir,
9348
10216
  useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
9349
10217
  );
9350
- const otelInitFile = import_node_path46.default.join(
10218
+ const otelInitFile = import_node_path48.default.join(
9351
10219
  serviceDir,
9352
10220
  useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
9353
10221
  );
@@ -9398,19 +10266,19 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
9398
10266
  var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
9399
10267
  async function findAstroMiddleware(serviceDir) {
9400
10268
  for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
9401
- const candidate = import_node_path46.default.join(serviceDir, rel);
10269
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9402
10270
  if (await exists3(candidate)) return candidate;
9403
10271
  }
9404
10272
  return null;
9405
10273
  }
9406
10274
  async function planAstro(serviceDir, pkg, manifestPath, project) {
9407
10275
  const useTs = await isTypeScriptProject(serviceDir);
9408
- const otelInitFile = import_node_path46.default.join(
10276
+ const otelInitFile = import_node_path48.default.join(
9409
10277
  serviceDir,
9410
10278
  useTs ? "src/otel-init.ts" : "src/otel-init.js"
9411
10279
  );
9412
10280
  const existingMiddleware = await findAstroMiddleware(serviceDir);
9413
- const middlewareFile = existingMiddleware ?? import_node_path46.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
10281
+ const middlewareFile = existingMiddleware ?? import_node_path48.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
9414
10282
  const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
9415
10283
  const generatedFiles = [];
9416
10284
  const entrypointEdits = [];
@@ -9435,7 +10303,7 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
9435
10303
  });
9436
10304
  } else {
9437
10305
  try {
9438
- const raw = await import_node_fs29.promises.readFile(existingMiddleware, "utf8");
10306
+ const raw = await import_node_fs31.promises.readFile(existingMiddleware, "utf8");
9439
10307
  if (!fileImportsOtelHook(raw, ["./otel-init"])) {
9440
10308
  const lines = raw.split(/\r?\n/);
9441
10309
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
@@ -9506,7 +10374,7 @@ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
9506
10374
  }
9507
10375
  async function plan(serviceDir, opts) {
9508
10376
  const pkg = await readPackageJson2(serviceDir);
9509
- const manifestPath = import_node_path46.default.join(serviceDir, "package.json");
10377
+ const manifestPath = import_node_path48.default.join(serviceDir, "package.json");
9510
10378
  const project = opts?.project;
9511
10379
  const empty = {
9512
10380
  language: "javascript",
@@ -9541,8 +10409,8 @@ async function plan(serviceDir, opts) {
9541
10409
  return { ...empty, libOnly: true };
9542
10410
  }
9543
10411
  const flavor = dispatchEntry(entryFile, pkg);
9544
- const otelInitFile = import_node_path46.default.join(import_node_path46.default.dirname(entryFile), otelInitFilename(flavor));
9545
- const envNeatFile = import_node_path46.default.join(serviceDir, ".env.neat");
10412
+ const otelInitFile = import_node_path48.default.join(import_node_path48.default.dirname(entryFile), otelInitFilename(flavor));
10413
+ const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
9546
10414
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
9547
10415
  const dependencyEdits = [];
9548
10416
  for (const sdk of SDK_PACKAGES) {
@@ -9566,7 +10434,7 @@ async function plan(serviceDir, opts) {
9566
10434
  }
9567
10435
  const entrypointEdits = [];
9568
10436
  try {
9569
- const raw = await import_node_fs29.promises.readFile(entryFile, "utf8");
10437
+ const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
9570
10438
  const lines = raw.split(/\r?\n/);
9571
10439
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
9572
10440
  if (!lineIsOtelInjection(firstReal)) {
@@ -9611,13 +10479,13 @@ async function plan(serviceDir, opts) {
9611
10479
  };
9612
10480
  }
9613
10481
  function isAllowedWritePath(serviceDir, target) {
9614
- const rel = import_node_path46.default.relative(serviceDir, target);
10482
+ const rel = import_node_path48.default.relative(serviceDir, target);
9615
10483
  if (rel.startsWith("..")) return false;
9616
- const base = import_node_path46.default.basename(target);
10484
+ const base = import_node_path48.default.basename(target);
9617
10485
  if (base === "package.json") return true;
9618
10486
  if (base === ".env.neat") return true;
9619
10487
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
9620
- const relPosix = rel.split(import_node_path46.default.sep).join("/");
10488
+ const relPosix = rel.split(import_node_path48.default.sep).join("/");
9621
10489
  if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
9622
10490
  if (relPosix === base) return true;
9623
10491
  if (relPosix === `src/${base}`) return true;
@@ -9634,10 +10502,10 @@ function isAllowedWritePath(serviceDir, target) {
9634
10502
  return false;
9635
10503
  }
9636
10504
  async function writeAtomic(file, contents) {
9637
- await import_node_fs29.promises.mkdir(import_node_path46.default.dirname(file), { recursive: true });
10505
+ await import_node_fs31.promises.mkdir(import_node_path48.default.dirname(file), { recursive: true });
9638
10506
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
9639
- await import_node_fs29.promises.writeFile(tmp, contents, "utf8");
9640
- await import_node_fs29.promises.rename(tmp, file);
10507
+ await import_node_fs31.promises.writeFile(tmp, contents, "utf8");
10508
+ await import_node_fs31.promises.rename(tmp, file);
9641
10509
  }
9642
10510
  async function apply(installPlan) {
9643
10511
  const { serviceDir } = installPlan;
@@ -9713,7 +10581,7 @@ async function apply(installPlan) {
9713
10581
  for (const target of allTargets) {
9714
10582
  if (await exists3(target)) {
9715
10583
  try {
9716
- originals.set(target, await import_node_fs29.promises.readFile(target, "utf8"));
10584
+ originals.set(target, await import_node_fs31.promises.readFile(target, "utf8"));
9717
10585
  } catch {
9718
10586
  }
9719
10587
  }
@@ -9796,14 +10664,14 @@ async function rollback(installPlan, originals, createdFiles) {
9796
10664
  const removed = [];
9797
10665
  for (const [file, raw] of originals.entries()) {
9798
10666
  try {
9799
- await import_node_fs29.promises.writeFile(file, raw, "utf8");
10667
+ await import_node_fs31.promises.writeFile(file, raw, "utf8");
9800
10668
  restored.push(file);
9801
10669
  } catch {
9802
10670
  }
9803
10671
  }
9804
10672
  for (const file of createdFiles) {
9805
10673
  try {
9806
- await import_node_fs29.promises.unlink(file);
10674
+ await import_node_fs31.promises.unlink(file);
9807
10675
  removed.push(file);
9808
10676
  } catch {
9809
10677
  }
@@ -9818,8 +10686,8 @@ async function rollback(installPlan, originals, createdFiles) {
9818
10686
  ...removed.map((f) => `removed: ${f}`),
9819
10687
  ""
9820
10688
  ];
9821
- const rollbackPath = import_node_path46.default.join(installPlan.serviceDir, "neat-rollback.patch");
9822
- await import_node_fs29.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
10689
+ const rollbackPath = import_node_path48.default.join(installPlan.serviceDir, "neat-rollback.patch");
10690
+ await import_node_fs31.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
9823
10691
  }
9824
10692
  function injectInstrumentationHook(raw) {
9825
10693
  if (raw.includes("instrumentationHook")) return raw;
@@ -9848,8 +10716,8 @@ var javascriptInstaller = {
9848
10716
 
9849
10717
  // src/installers/python.ts
9850
10718
  init_cjs_shims();
9851
- var import_node_fs30 = require("fs");
9852
- var import_node_path47 = __toESM(require("path"), 1);
10719
+ var import_node_fs32 = require("fs");
10720
+ var import_node_path49 = __toESM(require("path"), 1);
9853
10721
  var SDK_PACKAGES2 = [
9854
10722
  { name: "opentelemetry-distro", version: ">=0.49b0" },
9855
10723
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -9861,7 +10729,7 @@ var OTEL_ENV2 = {
9861
10729
  };
9862
10730
  async function exists4(p) {
9863
10731
  try {
9864
- await import_node_fs30.promises.stat(p);
10732
+ await import_node_fs32.promises.stat(p);
9865
10733
  return true;
9866
10734
  } catch {
9867
10735
  return false;
@@ -9870,7 +10738,7 @@ async function exists4(p) {
9870
10738
  async function detect2(serviceDir) {
9871
10739
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
9872
10740
  for (const m of markers) {
9873
- if (await exists4(import_node_path47.default.join(serviceDir, m))) return true;
10741
+ if (await exists4(import_node_path49.default.join(serviceDir, m))) return true;
9874
10742
  }
9875
10743
  return false;
9876
10744
  }
@@ -9880,9 +10748,9 @@ function reqPackageName(line) {
9880
10748
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
9881
10749
  }
9882
10750
  async function planRequirementsTxtEdits(serviceDir) {
9883
- const file = import_node_path47.default.join(serviceDir, "requirements.txt");
10751
+ const file = import_node_path49.default.join(serviceDir, "requirements.txt");
9884
10752
  if (!await exists4(file)) return null;
9885
- const raw = await import_node_fs30.promises.readFile(file, "utf8");
10753
+ const raw = await import_node_fs32.promises.readFile(file, "utf8");
9886
10754
  const presentNames = new Set(
9887
10755
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
9888
10756
  );
@@ -9890,9 +10758,9 @@ async function planRequirementsTxtEdits(serviceDir) {
9890
10758
  return { manifest: file, missing: [...missing] };
9891
10759
  }
9892
10760
  async function planProcfileEdits(serviceDir) {
9893
- const procfile = import_node_path47.default.join(serviceDir, "Procfile");
10761
+ const procfile = import_node_path49.default.join(serviceDir, "Procfile");
9894
10762
  if (!await exists4(procfile)) return [];
9895
- const raw = await import_node_fs30.promises.readFile(procfile, "utf8");
10763
+ const raw = await import_node_fs32.promises.readFile(procfile, "utf8");
9896
10764
  const edits = [];
9897
10765
  for (const line of raw.split(/\r?\n/)) {
9898
10766
  if (line.length === 0) continue;
@@ -9944,8 +10812,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
9944
10812
  const next = `${original}${trailing}${newlines.join("\n")}
9945
10813
  `;
9946
10814
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
9947
- await import_node_fs30.promises.writeFile(tmp, next, "utf8");
9948
- await import_node_fs30.promises.rename(tmp, manifest);
10815
+ await import_node_fs32.promises.writeFile(tmp, next, "utf8");
10816
+ await import_node_fs32.promises.rename(tmp, manifest);
9949
10817
  }
9950
10818
  async function applyProcfile(procfile, edits, original) {
9951
10819
  let next = original;
@@ -9954,8 +10822,8 @@ async function applyProcfile(procfile, edits, original) {
9954
10822
  next = next.replace(e.before, e.after);
9955
10823
  }
9956
10824
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
9957
- await import_node_fs30.promises.writeFile(tmp, next, "utf8");
9958
- await import_node_fs30.promises.rename(tmp, procfile);
10825
+ await import_node_fs32.promises.writeFile(tmp, next, "utf8");
10826
+ await import_node_fs32.promises.rename(tmp, procfile);
9959
10827
  }
9960
10828
  async function apply2(installPlan) {
9961
10829
  const { serviceDir } = installPlan;
@@ -9968,7 +10836,7 @@ async function apply2(installPlan) {
9968
10836
  const originals = /* @__PURE__ */ new Map();
9969
10837
  for (const file of touched) {
9970
10838
  try {
9971
- originals.set(file, await import_node_fs30.promises.readFile(file, "utf8"));
10839
+ originals.set(file, await import_node_fs32.promises.readFile(file, "utf8"));
9972
10840
  } catch {
9973
10841
  }
9974
10842
  }
@@ -9979,7 +10847,7 @@ async function apply2(installPlan) {
9979
10847
  if (raw === void 0) {
9980
10848
  throw new Error(`python installer: cannot read ${file} during apply`);
9981
10849
  }
9982
- const base = import_node_path47.default.basename(file);
10850
+ const base = import_node_path49.default.basename(file);
9983
10851
  if (base === "requirements.txt") {
9984
10852
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
9985
10853
  if (edits.length > 0) {
@@ -10004,7 +10872,7 @@ async function rollback2(installPlan, originals) {
10004
10872
  const restored = [];
10005
10873
  for (const [file, raw] of originals.entries()) {
10006
10874
  try {
10007
- await import_node_fs30.promises.writeFile(file, raw, "utf8");
10875
+ await import_node_fs32.promises.writeFile(file, raw, "utf8");
10008
10876
  restored.push(file);
10009
10877
  } catch {
10010
10878
  }
@@ -10018,8 +10886,8 @@ async function rollback2(installPlan, originals) {
10018
10886
  ...restored.map((f) => `restored: ${f}`),
10019
10887
  ""
10020
10888
  ];
10021
- const rollbackPath = import_node_path47.default.join(installPlan.serviceDir, "neat-rollback.patch");
10022
- await import_node_fs30.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
10889
+ const rollbackPath = import_node_path49.default.join(installPlan.serviceDir, "neat-rollback.patch");
10890
+ await import_node_fs32.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
10023
10891
  }
10024
10892
  var pythonInstaller = {
10025
10893
  name: "python",
@@ -10148,38 +11016,6 @@ var import_node_net = __toESM(require("net"), 1);
10148
11016
  var import_node_path50 = __toESM(require("path"), 1);
10149
11017
  var import_node_child_process3 = require("child_process");
10150
11018
  var import_node_readline = __toESM(require("readline"), 1);
10151
-
10152
- // src/daemon.ts
10153
- init_cjs_shims();
10154
- var import_node_fs32 = require("fs");
10155
- var import_node_path49 = __toESM(require("path"), 1);
10156
- var import_node_module = require("module");
10157
- init_otel();
10158
- init_auth();
10159
-
10160
- // src/unrouted.ts
10161
- init_cjs_shims();
10162
- var import_node_fs31 = require("fs");
10163
- var import_node_path48 = __toESM(require("path"), 1);
10164
-
10165
- // src/daemon.ts
10166
- function daemonJsonPath(scanPath) {
10167
- return import_node_path49.default.join(scanPath, "neat-out", "daemon.json");
10168
- }
10169
- async function readDaemonRecord(scanPath) {
10170
- try {
10171
- const raw = await import_node_fs32.promises.readFile(daemonJsonPath(scanPath), "utf8");
10172
- const parsed = JSON.parse(raw);
10173
- if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
10174
- return parsed;
10175
- }
10176
- return null;
10177
- } catch {
10178
- return null;
10179
- }
10180
- }
10181
-
10182
- // src/orchestrator.ts
10183
11019
  async function extractAndPersist(opts) {
10184
11020
  const services = await discoverServices(opts.scanPath);
10185
11021
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
@@ -10236,11 +11072,13 @@ async function applyInstallersOver(services, project, options = {}) {
10236
11072
  } else if (outcome.outcome === "already-instrumented") already++;
10237
11073
  else if (outcome.outcome === "lib-only") {
10238
11074
  libOnly++;
10239
- if (svc.pkg && looksLikeWebApp(svc.pkg)) {
11075
+ const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
11076
+ if (appDeps.length > 0) {
10240
11077
  const svcName = import_node_path50.default.basename(svc.dir);
11078
+ const list = appDeps.join(", ");
10241
11079
  console.warn(
10242
11080
  `neat: runtime layer won't engage for ${svcName}: no entry point found.
10243
- ${svc.dir} depends on a web framework but neat couldn't resolve an entry to instrument.
11081
+ ${svc.dir} depends on ${list} \u2014 a runnable app \u2014 but neat couldn't resolve an entry to instrument.
10244
11082
  Add a "start" script to package.json, or point neat at the entry file directly.`
10245
11083
  );
10246
11084
  }
@@ -10308,10 +11146,11 @@ async function applyInstallersOver(services, project, options = {}) {
10308
11146
  if (gaps.length > 0) {
10309
11147
  const svcName = import_node_path50.default.basename(svc.dir);
10310
11148
  const list = gaps.join(", ");
10311
- const verb = gaps.length === 1 ? "this library" : "these libraries";
11149
+ const subject = gaps.length === 1 ? "this library" : "these libraries";
11150
+ const aux = gaps.length === 1 ? "isn't" : "aren't";
10312
11151
  console.warn(
10313
11152
  `neat: calls to ${list} won't be observed by default in ${svcName}.
10314
- ${verb} aren't in the default instrumentation set, so they produce no OBSERVED edges.
11153
+ ${subject} ${aux} in the default instrumentation set, so they produce no OBSERVED edges.
10315
11154
  Run \`neat list-uninstrumented\` to review them, then \`neat extend\` to capture them.`
10316
11155
  );
10317
11156
  }
@@ -10405,29 +11244,22 @@ async function probeProjectHealth(restPort, name) {
10405
11244
  });
10406
11245
  });
10407
11246
  }
10408
- async function snapshotProjectStatus(restPort, body) {
11247
+ async function snapshotProjectStatus(restPort, project, body) {
10409
11248
  if (body.projects && body.projects.length > 0) {
10410
- return body.projects.map((p) => ({
10411
- name: p.name,
10412
- status: p.status ?? "active"
10413
- }));
10414
- }
10415
- const entries = await listProjects().catch(() => []);
10416
- if (entries.length === 0) return [];
10417
- return Promise.all(
10418
- entries.map(async (entry2) => ({
10419
- name: entry2.name,
10420
- status: await probeProjectHealth(restPort, entry2.name)
10421
- }))
10422
- );
11249
+ const mine = body.projects.filter((p) => p.name === project);
11250
+ if (mine.length > 0) {
11251
+ return mine.map((p) => ({ name: p.name, status: p.status ?? "active" }));
11252
+ }
11253
+ }
11254
+ return [{ name: project, status: await probeProjectHealth(restPort, project) }];
10423
11255
  }
10424
- async function waitForDaemonReady(restPort, timeoutMs) {
11256
+ async function waitForDaemonReady(restPort, project, timeoutMs) {
10425
11257
  const deadline = Date.now() + timeoutMs;
10426
11258
  let lastBootstrapping = [];
10427
11259
  while (Date.now() < deadline) {
10428
11260
  const body = await fetchDaemonHealth(restPort);
10429
11261
  if (body !== null) {
10430
- const projects2 = await snapshotProjectStatus(restPort, body);
11262
+ const projects2 = await snapshotProjectStatus(restPort, project, body);
10431
11263
  const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
10432
11264
  const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
10433
11265
  if (bootstrapping.length === 0) {
@@ -10446,7 +11278,7 @@ async function waitForDaemonReady(restPort, timeoutMs) {
10446
11278
  await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
10447
11279
  }
10448
11280
  const final = await fetchDaemonHealth(restPort);
10449
- const projects = final ? await snapshotProjectStatus(restPort, final) : [];
11281
+ const projects = final ? await snapshotProjectStatus(restPort, project, final) : [];
10450
11282
  return {
10451
11283
  ready: false,
10452
11284
  brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
@@ -10454,14 +11286,37 @@ async function waitForDaemonReady(restPort, timeoutMs) {
10454
11286
  };
10455
11287
  }
10456
11288
  var NEAT_PORTS = [8080, 4318, 6328];
10457
- async function isPortFree(port) {
11289
+ function probeBind(port, host) {
10458
11290
  return new Promise((resolve) => {
10459
11291
  const server = import_node_net.default.createServer();
10460
- server.once("error", () => resolve(false));
10461
- server.once("listening", () => server.close(() => resolve(true)));
10462
- server.listen(port, "127.0.0.1");
11292
+ server.once("error", (err) => {
11293
+ resolve(err.code === "EADDRINUSE" ? "in-use" : "unavailable");
11294
+ });
11295
+ server.once("listening", () => server.close(() => resolve("free")));
11296
+ server.listen(port, host);
10463
11297
  });
10464
11298
  }
11299
+ function crossFamilyHost(host) {
11300
+ switch (host) {
11301
+ case "127.0.0.1":
11302
+ case "localhost":
11303
+ return "::1";
11304
+ case "::1":
11305
+ return "127.0.0.1";
11306
+ case "0.0.0.0":
11307
+ return "::";
11308
+ case "::":
11309
+ return "0.0.0.0";
11310
+ default:
11311
+ return null;
11312
+ }
11313
+ }
11314
+ async function isPortFree(port, host = "127.0.0.1") {
11315
+ if (await probeBind(port, host) !== "free") return false;
11316
+ const sibling = crossFamilyHost(host);
11317
+ if (sibling && await probeBind(port, sibling) === "in-use") return false;
11318
+ return true;
11319
+ }
10465
11320
  function formatPortCollisionMessage(port) {
10466
11321
  return [
10467
11322
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -10471,13 +11326,13 @@ function formatPortCollisionMessage(port) {
10471
11326
  }
10472
11327
  var PORT_ALLOCATION_ATTEMPTS = 8;
10473
11328
  var PORT_STRIDE = 1;
10474
- async function tripleFree(ports) {
11329
+ async function tripleFree(ports, host = "127.0.0.1") {
10475
11330
  for (const p of [ports.rest, ports.otlp, ports.web]) {
10476
- if (!await isPortFree(p)) return false;
11331
+ if (!await isPortFree(p, host)) return false;
10477
11332
  }
10478
11333
  return true;
10479
11334
  }
10480
- async function allocatePorts() {
11335
+ async function allocatePorts(host = "127.0.0.1") {
10481
11336
  const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
10482
11337
  for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
10483
11338
  const candidate = {
@@ -10485,7 +11340,7 @@ async function allocatePorts() {
10485
11340
  otlp: baseOtlp + i * PORT_STRIDE,
10486
11341
  web: baseWeb + i * PORT_STRIDE
10487
11342
  };
10488
- if (await tripleFree(candidate)) return candidate;
11343
+ if (await tripleFree(candidate, host)) return candidate;
10489
11344
  }
10490
11345
  return null;
10491
11346
  }
@@ -10691,16 +11546,20 @@ async function runOrchestrator(opts) {
10691
11546
  }
10692
11547
  }
10693
11548
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
11549
+ const bindHost = resolveHost(
11550
+ {},
11551
+ typeof process.env.NEAT_AUTH_TOKEN === "string" && process.env.NEAT_AUTH_TOKEN.length > 0
11552
+ );
10694
11553
  const persistedPorts = await persistedPortsFor(opts.scanPath);
10695
11554
  let allocated = null;
10696
11555
  if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
10697
11556
  result.steps.daemon = "already-running";
10698
11557
  allocated = persistedPorts;
10699
11558
  } else {
10700
- if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
11559
+ if (persistedPorts && await isPortFree(persistedPorts.rest, bindHost) && await tripleFree(persistedPorts, bindHost)) {
10701
11560
  allocated = persistedPorts;
10702
11561
  } else {
10703
- allocated = await allocatePorts();
11562
+ allocated = await allocatePorts(bindHost);
10704
11563
  }
10705
11564
  if (!allocated) {
10706
11565
  for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
@@ -10729,7 +11588,7 @@ async function runOrchestrator(opts) {
10729
11588
  projectPath: opts.scanPath,
10730
11589
  ports: allocated
10731
11590
  });
10732
- const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
11591
+ const ready = await waitForDaemonReady(allocated.rest, currentProjectName, timeoutMs);
10733
11592
  result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10734
11593
  if (!ready.ready) {
10735
11594
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
@@ -10797,7 +11656,7 @@ var import_node_path51 = __toESM(require("path"), 1);
10797
11656
 
10798
11657
  // src/cli-client.ts
10799
11658
  init_cjs_shims();
10800
- var import_types27 = require("@neat.is/types");
11659
+ var import_types29 = require("@neat.is/types");
10801
11660
  var HttpError = class extends Error {
10802
11661
  constructor(status2, message, responseBody = "") {
10803
11662
  super(message);
@@ -10913,7 +11772,7 @@ async function runBlastRadius(client, input) {
10913
11772
  const result = await client.get(path53);
10914
11773
  if (result.totalAffected === 0) {
10915
11774
  return {
10916
- summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
11775
+ summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
10917
11776
  };
10918
11777
  }
10919
11778
  const sorted = [...result.affectedNodes].sort(
@@ -10926,7 +11785,7 @@ async function runBlastRadius(client, input) {
10926
11785
  );
10927
11786
  const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
10928
11787
  return {
10929
- summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
11788
+ summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
10930
11789
  block: blockLines.join("\n"),
10931
11790
  confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
10932
11791
  provenance: provenances.length ? provenances : void 0
@@ -10939,7 +11798,7 @@ async function runBlastRadius(client, input) {
10939
11798
  }
10940
11799
  }
10941
11800
  function formatBlastEntry(n) {
10942
- const tag = n.edgeProvenance === import_types27.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
11801
+ const tag = n.edgeProvenance === import_types29.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
10943
11802
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
10944
11803
  }
10945
11804
  async function runDependencies(client, input) {
@@ -10980,22 +11839,33 @@ async function runDependencies(client, input) {
10980
11839
  throw err;
10981
11840
  }
10982
11841
  }
11842
+ function observedDepLine(nodeId, e) {
11843
+ const via = e.source !== nodeId ? ` (via ${e.source})` : "";
11844
+ return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
11845
+ }
10983
11846
  async function runObservedDependencies(client, input) {
10984
11847
  try {
10985
- const edges = await client.get(
10986
- projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
11848
+ const result = await client.get(
11849
+ projectPath(
11850
+ input.project,
11851
+ `/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
11852
+ )
10987
11853
  );
10988
- const observed = edges.outbound.filter((e) => e.provenance === import_types27.Provenance.OBSERVED);
10989
- if (observed.length === 0) {
10990
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types27.Provenance.EXTRACTED);
10991
- const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
11854
+ if (result.dependencies.length === 0) {
11855
+ if (result.observed) {
11856
+ return {
11857
+ summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
11858
+ provenance: import_types29.Provenance.OBSERVED
11859
+ };
11860
+ }
11861
+ const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
10992
11862
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
10993
11863
  }
10994
- const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
11864
+ const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
10995
11865
  return {
10996
- summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
11866
+ summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
10997
11867
  block: blockLines.join("\n"),
10998
- provenance: import_types27.Provenance.OBSERVED
11868
+ provenance: import_types29.Provenance.OBSERVED
10999
11869
  };
11000
11870
  } catch (err) {
11001
11871
  if (err instanceof HttpError && err.status === 404) {
@@ -11049,7 +11919,7 @@ async function runIncidents(client, input) {
11049
11919
  return {
11050
11920
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
11051
11921
  block: blockLines.join("\n"),
11052
- provenance: import_types27.Provenance.OBSERVED
11922
+ provenance: import_types29.Provenance.OBSERVED
11053
11923
  };
11054
11924
  } catch (err) {
11055
11925
  if (err instanceof HttpError && err.status === 404) {
@@ -11158,7 +12028,7 @@ async function runStaleEdges(client, input) {
11158
12028
  return {
11159
12029
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
11160
12030
  block: blockLines.join("\n"),
11161
- provenance: import_types27.Provenance.STALE
12031
+ provenance: import_types29.Provenance.STALE
11162
12032
  };
11163
12033
  }
11164
12034
  async function runPolicies(client, input) {
@@ -11475,7 +12345,7 @@ async function runSync(opts) {
11475
12345
  }
11476
12346
 
11477
12347
  // src/cli.ts
11478
- var import_types28 = require("@neat.is/types");
12348
+ var import_types30 = require("@neat.is/types");
11479
12349
  function isNpxInvocation() {
11480
12350
  if (process.env.npm_command === "exec") return true;
11481
12351
  const execpath = process.env.npm_execpath ?? "";
@@ -11540,7 +12410,7 @@ function usage() {
11540
12410
  console.log("query commands (mirror the MCP tools, ADR-050):");
11541
12411
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
11542
12412
  console.log(` example: ${neat} root-cause service:<name>`);
11543
- console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
12413
+ console.log(" blast-radius <node-id> BFS inbound \u2014 the dependents that break if this dies.");
11544
12414
  console.log(` example: ${neat} blast-radius database:<host>`);
11545
12415
  console.log(" dependencies <node-id> Transitive outbound dependencies.");
11546
12416
  console.log(" Flags: --depth N (default 3, max 10)");
@@ -12267,11 +13137,18 @@ Pass --project <name> to choose:
12267
13137
  ${names}`
12268
13138
  );
12269
13139
  }
12270
- function resolveDaemonUrl() {
12271
- return process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL ?? "http://localhost:8080";
13140
+ async function resolveDaemonUrl(project) {
13141
+ const explicit = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
13142
+ if (explicit) return explicit;
13143
+ if (project) {
13144
+ const daemon = await findDaemonByProject(project);
13145
+ if (daemon) return `http://localhost:${daemon.record.ports.rest}`;
13146
+ }
13147
+ return "http://localhost:8080";
12272
13148
  }
12273
13149
  async function runQueryVerb(cmd, parsed) {
12274
- const baseUrl = resolveDaemonUrl();
13150
+ const requestedProject = resolveProjectFlag(parsed);
13151
+ const baseUrl = await resolveDaemonUrl(requestedProject);
12275
13152
  const client = createHttpClient(baseUrl, resolveAuthToken());
12276
13153
  const positional = parsed.positional;
12277
13154
  let makeWork;
@@ -12389,10 +13266,10 @@ async function runQueryVerb(cmd, parsed) {
12389
13266
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
12390
13267
  const out = [];
12391
13268
  for (const p of parts) {
12392
- const r = import_types28.DivergenceTypeSchema.safeParse(p);
13269
+ const r = import_types30.DivergenceTypeSchema.safeParse(p);
12393
13270
  if (!r.success) {
12394
13271
  console.error(
12395
- `neat divergences: unknown --type "${p}". allowed: ${import_types28.DivergenceTypeSchema.options.join(", ")}`
13272
+ `neat divergences: unknown --type "${p}". allowed: ${import_types30.DivergenceTypeSchema.options.join(", ")}`
12396
13273
  );
12397
13274
  return 2;
12398
13275
  }
@@ -12427,7 +13304,7 @@ async function runQueryVerb(cmd, parsed) {
12427
13304
  const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
12428
13305
  console.error(`neat ${cmd}: ${detail.trim()}`);
12429
13306
  } else if (err instanceof TransportError) {
12430
- console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${resolveDaemonUrl()})`);
13307
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${baseUrl})`);
12431
13308
  } else {
12432
13309
  console.error(`neat ${cmd}: ${err.message}`);
12433
13310
  }
@@ -12452,6 +13329,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
12452
13329
  parseArgs,
12453
13330
  printBanner,
12454
13331
  readPackageVersion,
13332
+ resolveDaemonUrl,
12455
13333
  resolveProjectForVerb,
12456
13334
  runInit,
12457
13335
  runQueryVerb,