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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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");
@@ -2206,6 +2643,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
2206
2643
  };
2207
2644
  await appendErrorEvent(ctx, ev);
2208
2645
  }
2646
+ async function recordExceptionIncident(ctx, span, ts) {
2647
+ const attrs = sanitizeAttributes(span.attributes);
2648
+ const ev = {
2649
+ id: `${span.traceId}:${span.spanId}`,
2650
+ timestamp: ts,
2651
+ service: span.service,
2652
+ traceId: span.traceId,
2653
+ spanId: span.spanId,
2654
+ errorMessage: incidentMessage(span),
2655
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2656
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2657
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2658
+ affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
2659
+ };
2660
+ await appendErrorEvent(ctx, ev);
2661
+ }
2209
2662
  async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2210
2663
  const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2211
2664
  if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
@@ -2262,7 +2715,10 @@ async function handleSpan(ctx, span) {
2262
2715
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2263
2716
  cacheSpanService(span, nowMs, callSite);
2264
2717
  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;
2718
+ const callSiteEvidence = callSite ? {
2719
+ file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
2720
+ ...callSite.line !== void 0 ? { line: callSite.line } : {}
2721
+ } : void 0;
2266
2722
  let affectedNode = sourceId;
2267
2723
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2268
2724
  if (span.dbSystem) {
@@ -2284,7 +2740,7 @@ async function handleSpan(ctx, span) {
2284
2740
  } else {
2285
2741
  const host = pickAddress(span);
2286
2742
  let resolvedViaAddress = false;
2287
- if (mintsFromCallerSide && host && host !== span.service) {
2743
+ if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
2288
2744
  const targetId = resolveServiceId(ctx.graph, host, env);
2289
2745
  if (targetId && targetId !== sourceId) {
2290
2746
  upsertObservedEdge(
@@ -2319,7 +2775,11 @@ async function handleSpan(ctx, span) {
2319
2775
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2320
2776
  const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2321
2777
  const fallbackEvidence = parent.callSite ? {
2322
- file: parent.callSite.relPath,
2778
+ file: reconcileObservedRelPath(
2779
+ ctx.graph,
2780
+ parent.service,
2781
+ parent.callSite.relPath
2782
+ ),
2323
2783
  ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2324
2784
  } : void 0;
2325
2785
  upsertObservedEdge(
@@ -2344,7 +2804,7 @@ async function handleSpan(ctx, span) {
2344
2804
  service: span.service,
2345
2805
  traceId: span.traceId,
2346
2806
  spanId: span.spanId,
2347
- errorMessage: span.exception?.message ?? "unknown error",
2807
+ errorMessage: incidentMessage(span),
2348
2808
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2349
2809
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2350
2810
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -2355,7 +2815,9 @@ async function handleSpan(ctx, span) {
2355
2815
  }
2356
2816
  if (span.statusCode !== 2) {
2357
2817
  const status2 = httpResponseStatus(span);
2358
- if (status2 !== void 0 && status2 >= 500) {
2818
+ if (span.exception) {
2819
+ await recordExceptionIncident(ctx, span, ts);
2820
+ } else if (status2 !== void 0 && status2 >= 500) {
2359
2821
  await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2360
2822
  } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2361
2823
  await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
@@ -2520,12 +2982,43 @@ function startStalenessLoop(graph, options = {}) {
2520
2982
  async function readErrorEvents(errorsPath) {
2521
2983
  try {
2522
2984
  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));
2985
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2986
+ return dedupeIncidents(events);
2524
2987
  } catch (err) {
2525
2988
  if (err.code === "ENOENT") return [];
2526
2989
  throw err;
2527
2990
  }
2528
2991
  }
2992
+ function isSynthesizedHttpIncident(ev) {
2993
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2994
+ if (ev.errorType) return false;
2995
+ if (!ev.attributes) return false;
2996
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2997
+ return synth !== void 0 && synth === ev.errorMessage;
2998
+ }
2999
+ function dedupeIncidents(events) {
3000
+ const seen = /* @__PURE__ */ new Set();
3001
+ const once = [];
3002
+ for (const ev of events) {
3003
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
3004
+ if (key === void 0) {
3005
+ once.push(ev);
3006
+ continue;
3007
+ }
3008
+ if (seen.has(key)) continue;
3009
+ seen.add(key);
3010
+ once.push(ev);
3011
+ }
3012
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
3013
+ const hasRealFailure = /* @__PURE__ */ new Set();
3014
+ for (const ev of once) {
3015
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
3016
+ }
3017
+ return once.filter((ev) => {
3018
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
3019
+ return !hasRealFailure.has(groupKey(ev));
3020
+ });
3021
+ }
2529
3022
  function mergeSnapshot(graph, snapshot) {
2530
3023
  const exported = snapshot.graph;
2531
3024
  let nodesAdded = 0;
@@ -2606,10 +3099,17 @@ function isConfigFile(name) {
2606
3099
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2607
3100
  if (name === ".env" || name.startsWith(".env.")) {
2608
3101
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
3102
+ if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
2609
3103
  return { match: true, fileType: "env" };
2610
3104
  }
2611
3105
  return { match: false, fileType: "" };
2612
3106
  }
3107
+ function isNeatAuthoredEnvFile(name) {
3108
+ return name === ".env.neat";
3109
+ }
3110
+ function isNeatAuthoredSourceFile(name) {
3111
+ return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
3112
+ }
2613
3113
  function isTestPath(filePath) {
2614
3114
  const normalised = filePath.replace(/\\/g, "/");
2615
3115
  const segments = normalised.split("/");
@@ -3315,7 +3815,9 @@ async function walkSourceFiles(dir) {
3315
3815
  if (IGNORED_DIRS.has(entry2.name)) continue;
3316
3816
  if (await isPythonVenvDir(full)) continue;
3317
3817
  await walk(full);
3318
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry2.name))) {
3818
+ } 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
3819
+ // would attribute our instrumentation imports to the user's service.
3820
+ !isNeatAuthoredSourceFile(entry2.name)) {
3319
3821
  out.push(full);
3320
3822
  }
3321
3823
  }
@@ -3485,11 +3987,28 @@ function collectJsImports(node, out) {
3485
3987
  if (child) collectJsImports(child, out);
3486
3988
  }
3487
3989
  }
3990
+ function collectImportedNames(node, out) {
3991
+ if (node.type === "aliased_import") {
3992
+ const nameNode = node.childForFieldName("name");
3993
+ if (nameNode) out.push(nameNode.text);
3994
+ return;
3995
+ }
3996
+ if (node.type === "dotted_name") {
3997
+ out.push(node.text);
3998
+ return;
3999
+ }
4000
+ for (let i = 0; i < node.namedChildCount; i++) {
4001
+ const child = node.namedChild(i);
4002
+ if (child) collectImportedNames(child, out);
4003
+ }
4004
+ }
3488
4005
  function collectPyImports(node, out) {
3489
4006
  if (node.type === "import_from_statement") {
3490
4007
  let level = 0;
3491
4008
  let modulePath = "";
4009
+ const names = [];
3492
4010
  let pastFrom = false;
4011
+ let pastImport = false;
3493
4012
  for (let i = 0; i < node.childCount; i++) {
3494
4013
  const child = node.child(i);
3495
4014
  if (!child) continue;
@@ -3497,26 +4016,30 @@ function collectPyImports(node, out) {
3497
4016
  if (child.type === "from") pastFrom = true;
3498
4017
  continue;
3499
4018
  }
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;
4019
+ if (!pastImport) {
4020
+ if (child.type === "import") {
4021
+ pastImport = true;
4022
+ continue;
3510
4023
  }
3511
- break;
3512
- }
3513
- if (child.type === "dotted_name") {
3514
- modulePath = child.text;
3515
- break;
4024
+ if (child.type === "relative_import") {
4025
+ for (let j = 0; j < child.childCount; j++) {
4026
+ const rc = child.child(j);
4027
+ if (!rc) continue;
4028
+ if (rc.type === "import_prefix") {
4029
+ for (let k = 0; k < rc.childCount; k++) {
4030
+ if (rc.child(k)?.type === ".") level++;
4031
+ }
4032
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
4033
+ }
4034
+ } else if (child.type === "dotted_name") {
4035
+ modulePath = child.text;
4036
+ }
4037
+ continue;
3516
4038
  }
4039
+ collectImportedNames(child, names);
3517
4040
  }
3518
4041
  if (level > 0 || modulePath) {
3519
- out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
4042
+ out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3520
4043
  }
3521
4044
  }
3522
4045
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3618,8 +4141,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3618
4141
  return null;
3619
4142
  }
3620
4143
  async function resolvePyImport(imp, importerPath, serviceDir) {
3621
- if (!imp.modulePath) return null;
3622
- const relPath = imp.modulePath.split(".").join("/");
3623
4144
  let baseDir;
3624
4145
  if (imp.level > 0) {
3625
4146
  baseDir = import_node_path13.default.dirname(importerPath);
@@ -3627,13 +4148,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
3627
4148
  } else {
3628
4149
  baseDir = serviceDir;
3629
4150
  }
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));
4151
+ const moduleBase = imp.modulePath ? import_node_path13.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
4152
+ const resolved = /* @__PURE__ */ new Set();
4153
+ let needModuleFile = imp.names.length === 0;
4154
+ for (const name of imp.names) {
4155
+ const submoduleFile = import_node_path13.default.join(moduleBase, `${name}.py`);
4156
+ const subpackageInit = import_node_path13.default.join(moduleBase, name, "__init__.py");
4157
+ if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
4158
+ resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, submoduleFile)));
4159
+ } else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
4160
+ resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, subpackageInit)));
4161
+ } else {
4162
+ needModuleFile = true;
3634
4163
  }
3635
4164
  }
3636
- return null;
4165
+ if (needModuleFile) {
4166
+ const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path13.default.join(moduleBase, "__init__.py")] : [import_node_path13.default.join(moduleBase, "__init__.py")];
4167
+ for (const candidate of moduleFileCandidates) {
4168
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
4169
+ resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, candidate)));
4170
+ break;
4171
+ }
4172
+ }
4173
+ }
4174
+ return [...resolved];
3637
4175
  }
3638
4176
  function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3639
4177
  const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
@@ -3674,17 +4212,18 @@ async function addImports(graph, services) {
3674
4212
  continue;
3675
4213
  }
3676
4214
  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
- );
4215
+ const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
4216
+ for (const resolved of resolvedPaths) {
4217
+ edgesAdded += emitImportEdge(
4218
+ graph,
4219
+ service.pkg.name,
4220
+ importerFileId,
4221
+ relFile,
4222
+ resolved,
4223
+ imp.line,
4224
+ imp.snippet
4225
+ );
4226
+ }
3688
4227
  }
3689
4228
  continue;
3690
4229
  }
@@ -4320,6 +4859,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4320
4859
  edgesAdded++;
4321
4860
  }
4322
4861
  }
4862
+ if (allConfigs.length === 1) {
4863
+ const primary = allConfigs[0];
4864
+ service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
4865
+ const relPath = import_node_path21.default.relative(scanPath, primary.sourceFile);
4866
+ const cfgId = (0, import_types9.configId)(relPath);
4867
+ if (!graph.hasNode(cfgId)) {
4868
+ const cfgNode = {
4869
+ id: cfgId,
4870
+ type: import_types9.NodeType.ConfigNode,
4871
+ name: import_node_path21.default.basename(primary.sourceFile),
4872
+ path: relPath,
4873
+ fileType: isConfigFile(import_node_path21.default.basename(primary.sourceFile)).fileType || "config"
4874
+ };
4875
+ graph.addNode(cfgId, cfgNode);
4876
+ nodesAdded++;
4877
+ }
4878
+ const cfgEdge = {
4879
+ id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
4880
+ source: service.node.id,
4881
+ target: cfgId,
4882
+ type: import_types9.EdgeType.CONFIGURED_BY,
4883
+ provenance: import_types9.Provenance.EXTRACTED,
4884
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
4885
+ evidence: { file: toPosix2(relPath) }
4886
+ };
4887
+ if (!graph.hasEdge(cfgEdge.id)) {
4888
+ graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
4889
+ edgesAdded++;
4890
+ }
4891
+ }
4323
4892
  attachIncompatibilities(service, allConfigs);
4324
4893
  if (graph.hasNode(service.node.id)) {
4325
4894
  const current = graph.getNodeAttributes(service.node.id);
@@ -4331,6 +4900,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
4331
4900
  if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
4332
4901
  delete updated.incompatibilities;
4333
4902
  }
4903
+ if (!service.node.dbConnectionTarget) {
4904
+ delete updated.dbConnectionTarget;
4905
+ }
4334
4906
  graph.replaceNodeAttributes(service.node.id, updated);
4335
4907
  }
4336
4908
  }
@@ -4508,7 +5080,7 @@ async function addHttpCallEdges(graph, services) {
4508
5080
  const dedupKey = `${relFile}|${targetId}`;
4509
5081
  if (seen.has(dedupKey)) continue;
4510
5082
  seen.add(dedupKey);
4511
- const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
5083
+ const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
4512
5084
  const ev = {
4513
5085
  file: relFile,
4514
5086
  line: site.line,
@@ -4528,7 +5100,7 @@ async function addHttpCallEdges(graph, services) {
4528
5100
  target: targetId,
4529
5101
  type: import_types11.EdgeType.CALLS,
4530
5102
  confidence,
4531
- confidenceKind: "hostname-shape-match",
5103
+ confidenceKind: "url-literal-service-target",
4532
5104
  evidence: ev
4533
5105
  });
4534
5106
  continue;
@@ -5034,19 +5606,29 @@ init_cjs_shims();
5034
5606
  var import_node_path30 = __toESM(require("path"), 1);
5035
5607
  var import_node_fs16 = require("fs");
5036
5608
  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) {
5609
+ function readDockerfile(content) {
5610
+ let image = null;
5611
+ const ports = [];
5612
+ let cmd = null;
5613
+ let entrypoint = null;
5614
+ for (const raw of content.split("\n")) {
5041
5615
  const line = raw.trim();
5042
5616
  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;
5617
+ if (/^from\s+/i.test(line)) {
5618
+ const candidate = line.split(/\s+/)[1];
5619
+ if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
5620
+ } else if (/^expose\s+/i.test(line)) {
5621
+ for (const token of line.split(/\s+/).slice(1)) {
5622
+ const port = Number.parseInt(token.split("/")[0], 10);
5623
+ if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
5624
+ }
5625
+ } else if (/^entrypoint\s+/i.test(line)) {
5626
+ entrypoint = line;
5627
+ } else if (/^cmd\s+/i.test(line)) {
5628
+ cmd = line;
5629
+ }
5048
5630
  }
5049
- return last;
5631
+ return { image, ports, entrypoint: entrypoint ?? cmd };
5050
5632
  }
5051
5633
  async function addDockerfileRuntimes(graph, services, scanPath) {
5052
5634
  let nodesAdded = 0;
@@ -5065,14 +5647,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5065
5647
  );
5066
5648
  continue;
5067
5649
  }
5068
- const image = runtimeImage(content);
5069
- if (!image) continue;
5070
- const node = makeInfraNode("container-image", image);
5650
+ const facts = readDockerfile(content);
5651
+ if (!facts.image) continue;
5652
+ const node = makeInfraNode("container-image", facts.image);
5071
5653
  if (!graph.hasNode(node.id)) {
5072
5654
  graph.addNode(node.id, node);
5073
5655
  nodesAdded++;
5074
5656
  }
5075
5657
  const relDockerfile = toPosix2(import_node_path30.default.relative(service.dir, dockerfilePath));
5658
+ const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath));
5076
5659
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5077
5660
  graph,
5078
5661
  service.pkg.name,
@@ -5091,12 +5674,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5091
5674
  provenance: import_types20.Provenance.EXTRACTED,
5092
5675
  confidence: (0, import_types20.confidenceForExtracted)("structural"),
5093
5676
  evidence: {
5094
- file: toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath))
5677
+ file: evidenceFile,
5678
+ ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
5095
5679
  }
5096
5680
  };
5097
5681
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5098
5682
  edgesAdded++;
5099
5683
  }
5684
+ for (const port of facts.ports) {
5685
+ const portNode = makeInfraNode("port", String(port));
5686
+ if (!graph.hasNode(portNode.id)) {
5687
+ graph.addNode(portNode.id, portNode);
5688
+ nodesAdded++;
5689
+ }
5690
+ const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
5691
+ if (graph.hasEdge(portEdgeId)) continue;
5692
+ const portEdge = {
5693
+ id: portEdgeId,
5694
+ source: fileNodeId,
5695
+ target: portNode.id,
5696
+ type: import_types20.EdgeType.CONNECTS_TO,
5697
+ provenance: import_types20.Provenance.EXTRACTED,
5698
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
5699
+ evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
5700
+ };
5701
+ graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
5702
+ edgesAdded++;
5703
+ }
5100
5704
  }
5101
5705
  return { nodesAdded, edgesAdded };
5102
5706
  }
@@ -5105,7 +5709,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
5105
5709
  init_cjs_shims();
5106
5710
  var import_node_fs17 = require("fs");
5107
5711
  var import_node_path31 = __toESM(require("path"), 1);
5712
+ var import_types21 = require("@neat.is/types");
5108
5713
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
5714
+ var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
5109
5715
  async function walkTfFiles(start, depth = 0, max = 5) {
5110
5716
  if (depth > max) return [];
5111
5717
  const out = [];
@@ -5122,24 +5728,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
5122
5728
  }
5123
5729
  return out;
5124
5730
  }
5731
+ function blockBody(content, from) {
5732
+ const open = content.indexOf("{", from);
5733
+ if (open === -1) return null;
5734
+ let depth = 0;
5735
+ for (let i = open; i < content.length; i++) {
5736
+ const ch = content[i];
5737
+ if (ch === "{") depth++;
5738
+ else if (ch === "}") {
5739
+ depth--;
5740
+ if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
5741
+ }
5742
+ }
5743
+ return null;
5744
+ }
5745
+ function lineAt(content, index) {
5746
+ let line = 1;
5747
+ for (let i = 0; i < index && i < content.length; i++) {
5748
+ if (content[i] === "\n") line++;
5749
+ }
5750
+ return line;
5751
+ }
5125
5752
  async function addTerraformResources(graph, scanPath) {
5126
5753
  let nodesAdded = 0;
5754
+ let edgesAdded = 0;
5127
5755
  const files = await walkTfFiles(scanPath);
5128
5756
  for (const file of files) {
5129
5757
  const content = await import_node_fs17.promises.readFile(file, "utf8");
5758
+ const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, file));
5759
+ const resources = [];
5760
+ const byKey = /* @__PURE__ */ new Map();
5130
5761
  RESOURCE_RE.lastIndex = 0;
5131
5762
  let m;
5132
5763
  while ((m = RESOURCE_RE.exec(content)) !== null) {
5133
- const kind = m[1];
5764
+ const type = m[1];
5134
5765
  const name = m[2];
5135
- const node = makeInfraNode(kind, name, "aws");
5766
+ const node = makeInfraNode(type, name, "aws");
5136
5767
  if (!graph.hasNode(node.id)) {
5137
5768
  graph.addNode(node.id, node);
5138
5769
  nodesAdded++;
5139
5770
  }
5771
+ const span = blockBody(content, RESOURCE_RE.lastIndex);
5772
+ const resource = {
5773
+ type,
5774
+ name,
5775
+ nodeId: node.id,
5776
+ body: span?.body ?? "",
5777
+ bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
5778
+ };
5779
+ resources.push(resource);
5780
+ byKey.set(`${type}.${name}`, resource);
5781
+ }
5782
+ for (const resource of resources) {
5783
+ const seen = /* @__PURE__ */ new Set();
5784
+ REFERENCE_RE.lastIndex = 0;
5785
+ let ref;
5786
+ while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
5787
+ const key = `${ref[1]}.${ref[2]}`;
5788
+ if (key === `${resource.type}.${resource.name}`) continue;
5789
+ const target = byKey.get(key);
5790
+ if (!target) continue;
5791
+ if (seen.has(target.nodeId)) continue;
5792
+ seen.add(target.nodeId);
5793
+ const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
5794
+ if (graph.hasEdge(edgeId)) continue;
5795
+ const line = lineAt(content, resource.bodyOffset + ref.index);
5796
+ const edge = {
5797
+ id: edgeId,
5798
+ source: resource.nodeId,
5799
+ target: target.nodeId,
5800
+ type: import_types21.EdgeType.DEPENDS_ON,
5801
+ provenance: import_types21.Provenance.EXTRACTED,
5802
+ confidence: (0, import_types21.confidenceForExtracted)("structural"),
5803
+ evidence: { file: evidenceFile, line, snippet: key }
5804
+ };
5805
+ graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
5806
+ edgesAdded++;
5807
+ }
5140
5808
  }
5141
5809
  }
5142
- return { nodesAdded, edgesAdded: 0 };
5810
+ return { nodesAdded, edgesAdded };
5143
5811
  }
5144
5812
 
5145
5813
  // src/extract/infra/k8s.ts
@@ -5217,11 +5885,11 @@ var import_node_path34 = __toESM(require("path"), 1);
5217
5885
  init_cjs_shims();
5218
5886
  var import_node_fs19 = require("fs");
5219
5887
  var import_node_path33 = __toESM(require("path"), 1);
5220
- var import_types21 = require("@neat.is/types");
5888
+ var import_types22 = require("@neat.is/types");
5221
5889
  function dropOrphanedFileNodes(graph) {
5222
5890
  const orphans = [];
5223
5891
  graph.forEachNode((id, attrs) => {
5224
- if (attrs.type !== import_types21.NodeType.FileNode) return;
5892
+ if (attrs.type !== import_types22.NodeType.FileNode) return;
5225
5893
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5226
5894
  orphans.push(id);
5227
5895
  }
@@ -5234,7 +5902,7 @@ function retireEdgesByFile(graph, file) {
5234
5902
  const toDrop = [];
5235
5903
  graph.forEachEdge((id, attrs) => {
5236
5904
  const edge = attrs;
5237
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5905
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5238
5906
  if (!edge.evidence?.file) return;
5239
5907
  if (edge.evidence.file === normalized) toDrop.push(id);
5240
5908
  });
@@ -5247,7 +5915,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5247
5915
  const bases = [scanPath, ...serviceDirs];
5248
5916
  graph.forEachEdge((id, attrs) => {
5249
5917
  const edge = attrs;
5250
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5918
+ if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
5251
5919
  const evidenceFile = edge.evidence?.file;
5252
5920
  if (!evidenceFile) return;
5253
5921
  if (import_node_path33.default.isAbsolute(evidenceFile)) {
@@ -5328,7 +5996,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5328
5996
 
5329
5997
  // src/divergences.ts
5330
5998
  init_cjs_shims();
5331
- var import_types22 = require("@neat.is/types");
5999
+ var import_types23 = require("@neat.is/types");
5332
6000
  function bucketKey(source, target, type) {
5333
6001
  return `${type}|${source}|${target}`;
5334
6002
  }
@@ -5336,22 +6004,22 @@ function bucketEdges(graph) {
5336
6004
  const buckets = /* @__PURE__ */ new Map();
5337
6005
  graph.forEachEdge((id, attrs) => {
5338
6006
  const e = attrs;
5339
- const parsed = (0, import_types22.parseEdgeId)(id);
6007
+ const parsed = (0, import_types23.parseEdgeId)(id);
5340
6008
  const provenance = parsed?.provenance ?? e.provenance;
5341
6009
  const key = bucketKey(e.source, e.target, e.type);
5342
6010
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5343
6011
  switch (provenance) {
5344
- case import_types22.Provenance.EXTRACTED:
6012
+ case import_types23.Provenance.EXTRACTED:
5345
6013
  cur.extracted = e;
5346
6014
  break;
5347
- case import_types22.Provenance.OBSERVED:
6015
+ case import_types23.Provenance.OBSERVED:
5348
6016
  cur.observed = e;
5349
6017
  break;
5350
- case import_types22.Provenance.INFERRED:
6018
+ case import_types23.Provenance.INFERRED:
5351
6019
  cur.inferred = e;
5352
6020
  break;
5353
6021
  default:
5354
- if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
6022
+ if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
5355
6023
  }
5356
6024
  buckets.set(key, cur);
5357
6025
  });
@@ -5360,7 +6028,7 @@ function bucketEdges(graph) {
5360
6028
  function nodeIsFrontier(graph, nodeId) {
5361
6029
  if (!graph.hasNode(nodeId)) return false;
5362
6030
  const attrs = graph.getNodeAttributes(nodeId);
5363
- return attrs.type === import_types22.NodeType.FrontierNode;
6031
+ return attrs.type === import_types23.NodeType.FrontierNode;
5364
6032
  }
5365
6033
  function clampConfidence(n) {
5366
6034
  if (!Number.isFinite(n)) return 0;
@@ -5379,10 +6047,16 @@ function gradedConfidence(edge) {
5379
6047
  if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
5380
6048
  return clampConfidence(confidenceForEdge(edge));
5381
6049
  }
6050
+ var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
6051
+ import_types23.EdgeType.CALLS,
6052
+ import_types23.EdgeType.CONNECTS_TO,
6053
+ import_types23.EdgeType.PUBLISHES_TO,
6054
+ import_types23.EdgeType.CONSUMES_FROM
6055
+ ]);
5382
6056
  function detectMissingDivergences(graph, bucket) {
5383
6057
  const out = [];
5384
- if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
5385
- if (bucket.extracted && !bucket.observed) {
6058
+ if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
6059
+ if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
5386
6060
  if (!nodeIsFrontier(graph, bucket.target)) {
5387
6061
  out.push({
5388
6062
  type: "missing-observed",
@@ -5422,7 +6096,7 @@ function declaredHostFor(svc) {
5422
6096
  function hasExtractedConfiguredBy(graph, svcId) {
5423
6097
  for (const edgeId of graph.outboundEdges(svcId)) {
5424
6098
  const e = graph.getEdgeAttributes(edgeId);
5425
- if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
6099
+ if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
5426
6100
  return true;
5427
6101
  }
5428
6102
  }
@@ -5435,10 +6109,10 @@ function detectHostMismatch(graph, svcId, svc) {
5435
6109
  const out = [];
5436
6110
  for (const edgeId of graph.outboundEdges(svcId)) {
5437
6111
  const edge = graph.getEdgeAttributes(edgeId);
5438
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5439
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
6112
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
6113
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5440
6114
  const target = graph.getNodeAttributes(edge.target);
5441
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
6115
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5442
6116
  const observedHost = target.host?.trim();
5443
6117
  if (!observedHost) continue;
5444
6118
  if (observedHost === declaredHost) continue;
@@ -5460,10 +6134,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5460
6134
  const deps = svc.dependencies ?? {};
5461
6135
  for (const edgeId of graph.outboundEdges(svcId)) {
5462
6136
  const edge = graph.getEdgeAttributes(edgeId);
5463
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5464
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
6137
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
6138
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5465
6139
  const target = graph.getNodeAttributes(edge.target);
5466
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
6140
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5467
6141
  for (const pair of compatPairs()) {
5468
6142
  if (pair.engine !== target.engine) continue;
5469
6143
  const declared = deps[pair.driver];
@@ -5516,6 +6190,23 @@ function detectCompatDivergences(graph, svcId, svc) {
5516
6190
  function involvesNode(d, nodeId) {
5517
6191
  return d.source === nodeId || d.target === nodeId;
5518
6192
  }
6193
+ function suppressHostMismatchHalves(all) {
6194
+ const observedHalf = /* @__PURE__ */ new Set();
6195
+ const declaredHalf = /* @__PURE__ */ new Set();
6196
+ for (const d of all) {
6197
+ if (d.type !== "host-mismatch") continue;
6198
+ observedHalf.add(`${d.source}->${d.target}`);
6199
+ declaredHalf.add((0, import_types23.databaseId)(d.extractedHost));
6200
+ }
6201
+ if (observedHalf.size === 0) return all;
6202
+ return all.filter((d) => {
6203
+ if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
6204
+ return false;
6205
+ }
6206
+ if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
6207
+ return true;
6208
+ });
6209
+ }
5519
6210
  function computeDivergences(graph, opts = {}) {
5520
6211
  const all = [];
5521
6212
  const buckets = bucketEdges(graph);
@@ -5524,12 +6215,13 @@ function computeDivergences(graph, opts = {}) {
5524
6215
  }
5525
6216
  graph.forEachNode((nodeId, attrs) => {
5526
6217
  const n = attrs;
5527
- if (n.type !== import_types22.NodeType.ServiceNode) return;
6218
+ if (n.type !== import_types23.NodeType.ServiceNode) return;
5528
6219
  const svc = n;
5529
6220
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5530
6221
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
5531
6222
  });
5532
- let filtered = all;
6223
+ const reconciled = suppressHostMismatchHalves(all);
6224
+ let filtered = reconciled;
5533
6225
  if (opts.type) {
5534
6226
  const allowed = opts.type;
5535
6227
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -5557,7 +6249,7 @@ function computeDivergences(graph, opts = {}) {
5557
6249
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5558
6250
  return a.target.localeCompare(b.target);
5559
6251
  });
5560
- return import_types22.DivergenceResultSchema.parse({
6252
+ return import_types23.DivergenceResultSchema.parse({
5561
6253
  divergences: filtered,
5562
6254
  totalAffected: filtered.length,
5563
6255
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5568,7 +6260,7 @@ function computeDivergences(graph, opts = {}) {
5568
6260
  init_cjs_shims();
5569
6261
  var import_node_fs20 = require("fs");
5570
6262
  var import_node_path35 = __toESM(require("path"), 1);
5571
- var import_types23 = require("@neat.is/types");
6263
+ var import_types24 = require("@neat.is/types");
5572
6264
  var SCHEMA_VERSION = 4;
5573
6265
  function migrateV1ToV2(payload) {
5574
6266
  const nodes = payload.graph.nodes;
@@ -5590,12 +6282,12 @@ function migrateV2ToV3(payload) {
5590
6282
  for (const edge of edges) {
5591
6283
  const attrs = edge.attributes;
5592
6284
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5593
- attrs.provenance = import_types23.Provenance.OBSERVED;
6285
+ attrs.provenance = import_types24.Provenance.OBSERVED;
5594
6286
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5595
6287
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5596
6288
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5597
6289
  if (type && source && target) {
5598
- const newId = (0, import_types23.observedEdgeId)(source, target, type);
6290
+ const newId = (0, import_types24.observedEdgeId)(source, target, type);
5599
6291
  attrs.id = newId;
5600
6292
  if (edge.key) edge.key = newId;
5601
6293
  }
@@ -5721,7 +6413,7 @@ ${NEAT_OUT_LINE}
5721
6413
 
5722
6414
  // src/summary.ts
5723
6415
  init_cjs_shims();
5724
- var import_types24 = require("@neat.is/types");
6416
+ var import_types25 = require("@neat.is/types");
5725
6417
  function renderOtelEnvBlock() {
5726
6418
  return [
5727
6419
  "for prod OTel routing, set these in your deploy platform's env:",
@@ -5731,19 +6423,19 @@ function renderOtelEnvBlock() {
5731
6423
  }
5732
6424
  function findIncompatServices(nodes) {
5733
6425
  return nodes.filter(
5734
- (n) => n.type === import_types24.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
6426
+ (n) => n.type === import_types25.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
5735
6427
  );
5736
6428
  }
5737
6429
  function servicesWithoutObserved(nodes, edges) {
5738
6430
  const seen = /* @__PURE__ */ new Set();
5739
6431
  for (const e of edges) {
5740
- if (e.provenance === import_types24.Provenance.OBSERVED) {
6432
+ if (e.provenance === import_types25.Provenance.OBSERVED) {
5741
6433
  seen.add(e.source);
5742
6434
  seen.add(e.target);
5743
6435
  }
5744
6436
  }
5745
6437
  return nodes.filter(
5746
- (n) => n.type === import_types24.NodeType.ServiceNode && !seen.has(n.id)
6438
+ (n) => n.type === import_types25.NodeType.ServiceNode && !seen.has(n.id)
5747
6439
  );
5748
6440
  }
5749
6441
  function formatDivergence(d) {
@@ -5817,15 +6509,15 @@ function formatIncompat(inc) {
5817
6509
 
5818
6510
  // src/watch.ts
5819
6511
  init_cjs_shims();
5820
- var import_node_fs27 = __toESM(require("fs"), 1);
5821
- var import_node_path44 = __toESM(require("path"), 1);
6512
+ var import_node_fs29 = __toESM(require("fs"), 1);
6513
+ var import_node_path46 = __toESM(require("path"), 1);
5822
6514
  var import_chokidar = __toESM(require("chokidar"), 1);
5823
6515
 
5824
6516
  // src/api.ts
5825
6517
  init_cjs_shims();
5826
6518
  var import_fastify = __toESM(require("fastify"), 1);
5827
6519
  var import_cors = __toESM(require("@fastify/cors"), 1);
5828
- var import_types26 = require("@neat.is/types");
6520
+ var import_types27 = require("@neat.is/types");
5829
6521
 
5830
6522
  // src/extend/index.ts
5831
6523
  init_cjs_shims();
@@ -6276,7 +6968,7 @@ init_cjs_shims();
6276
6968
  var import_node_fs25 = require("fs");
6277
6969
  var import_node_os3 = __toESM(require("os"), 1);
6278
6970
  var import_node_path40 = __toESM(require("path"), 1);
6279
- var import_types25 = require("@neat.is/types");
6971
+ var import_types26 = require("@neat.is/types");
6280
6972
  var LOCK_TIMEOUT_MS = 5e3;
6281
6973
  var LOCK_RETRY_MS = 50;
6282
6974
  function neatHome() {
@@ -6530,10 +7222,10 @@ async function readRegistry() {
6530
7222
  throw err;
6531
7223
  }
6532
7224
  const parsed = JSON.parse(raw);
6533
- return import_types25.RegistryFileSchema.parse(parsed);
7225
+ return import_types26.RegistryFileSchema.parse(parsed);
6534
7226
  }
6535
7227
  async function writeRegistry(reg) {
6536
- const validated = import_types25.RegistryFileSchema.parse(reg);
7228
+ const validated = import_types26.RegistryFileSchema.parse(reg);
6537
7229
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6538
7230
  }
6539
7231
  var ProjectNameCollisionError = class extends Error {
@@ -6845,6 +7537,18 @@ function registerRoutes(scope, ctx) {
6845
7537
  }
6846
7538
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6847
7539
  });
7540
+ scope.get(
7541
+ "/graph/observed-dependencies/:nodeId",
7542
+ async (req, reply) => {
7543
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7544
+ if (!proj) return;
7545
+ const { nodeId } = req.params;
7546
+ if (!proj.graph.hasNode(nodeId)) {
7547
+ return reply.code(404).send({ error: "node not found", id: nodeId });
7548
+ }
7549
+ return getObservedDependencies(proj.graph, nodeId);
7550
+ }
7551
+ );
6848
7552
  scope.get("/graph/divergences", async (req, reply) => {
6849
7553
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6850
7554
  if (!proj) return;
@@ -6853,11 +7557,11 @@ function registerRoutes(scope, ctx) {
6853
7557
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6854
7558
  const parsed = [];
6855
7559
  for (const c of candidates) {
6856
- const r = import_types26.DivergenceTypeSchema.safeParse(c);
7560
+ const r = import_types27.DivergenceTypeSchema.safeParse(c);
6857
7561
  if (!r.success) {
6858
7562
  return reply.code(400).send({
6859
7563
  error: `unknown divergence type "${c}"`,
6860
- allowed: import_types26.DivergenceTypeSchema.options
7564
+ allowed: import_types27.DivergenceTypeSchema.options
6861
7565
  });
6862
7566
  }
6863
7567
  parsed.push(r.data);
@@ -6905,23 +7609,29 @@ function registerRoutes(scope, ctx) {
6905
7609
  const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
6906
7610
  return { count: sliced.length, total, events: sliced };
6907
7611
  });
7612
+ const incidentHistoryHandler = async (req, reply) => {
7613
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7614
+ if (!proj) return;
7615
+ const { nodeId } = req.params;
7616
+ if (!proj.graph.hasNode(nodeId)) {
7617
+ reply.code(404).send({ error: "node not found", id: nodeId });
7618
+ return;
7619
+ }
7620
+ const epath = errorsPathFor(proj);
7621
+ if (!epath) return { count: 0, total: 0, events: [] };
7622
+ const events = await readErrorEvents(epath);
7623
+ const filtered = events.filter(
7624
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
7625
+ );
7626
+ return { count: filtered.length, total: filtered.length, events: filtered };
7627
+ };
6908
7628
  scope.get(
6909
7629
  "/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
- }
7630
+ incidentHistoryHandler
7631
+ );
7632
+ scope.get(
7633
+ "/graph/incident-history/:nodeId",
7634
+ incidentHistoryHandler
6925
7635
  );
6926
7636
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6927
7637
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
@@ -6930,16 +7640,16 @@ function registerRoutes(scope, ctx) {
6930
7640
  if (!proj.graph.hasNode(nodeId)) {
6931
7641
  return reply.code(404).send({ error: "node not found", id: nodeId });
6932
7642
  }
6933
- let errorEvent;
6934
7643
  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);
7644
+ const incidents = epath ? await readErrorEvents(epath) : [];
7645
+ let errorEvent;
7646
+ if (req.query.errorId) {
7647
+ errorEvent = incidents.find((e) => e.id === req.query.errorId);
6938
7648
  if (!errorEvent) {
6939
7649
  return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
6940
7650
  }
6941
7651
  }
6942
- const result = getRootCause(proj.graph, nodeId, errorEvent);
7652
+ const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
6943
7653
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
6944
7654
  return result;
6945
7655
  });
@@ -7070,7 +7780,7 @@ function registerRoutes(scope, ctx) {
7070
7780
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
7071
7781
  let violations = await log.readAll();
7072
7782
  if (req.query.severity) {
7073
- const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
7783
+ const sev = import_types27.PolicySeveritySchema.safeParse(req.query.severity);
7074
7784
  if (!sev.success) {
7075
7785
  return reply.code(400).send({
7076
7786
  error: "invalid severity",
@@ -7084,10 +7794,32 @@ function registerRoutes(scope, ctx) {
7084
7794
  }
7085
7795
  return { violations };
7086
7796
  });
7797
+ scope.get("/policies/applicable", async (req, reply) => {
7798
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7799
+ if (!proj) return;
7800
+ const nodeId = req.query.node;
7801
+ if (!nodeId) {
7802
+ return reply.code(400).send({ error: 'missing required query param "node"' });
7803
+ }
7804
+ const policyPath = ctx.policyFilePathFor(proj);
7805
+ let policies = [];
7806
+ if (policyPath) {
7807
+ try {
7808
+ policies = await loadPolicyFile(policyPath);
7809
+ } catch (err) {
7810
+ return reply.code(400).send({
7811
+ error: "policy.json failed to parse",
7812
+ details: err.message
7813
+ });
7814
+ }
7815
+ }
7816
+ const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
7817
+ return { node: nodeId, applicable };
7818
+ });
7087
7819
  scope.post("/policies/check", async (req, reply) => {
7088
7820
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
7089
7821
  if (!proj) return;
7090
- const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7822
+ const parsed = import_types27.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7091
7823
  if (!parsed.success) {
7092
7824
  return reply.code(400).send({
7093
7825
  error: "invalid /policies/check body",
@@ -7346,12 +8078,111 @@ async function buildApi(opts) {
7346
8078
  // src/watch.ts
7347
8079
  init_auth();
7348
8080
  init_otel();
7349
- init_otel_grpc();
7350
8081
 
7351
- // src/search.ts
8082
+ // src/daemon.ts
8083
+ init_cjs_shims();
8084
+ var import_node_fs27 = require("fs");
8085
+ var import_node_path44 = __toESM(require("path"), 1);
8086
+ var import_node_module = require("module");
8087
+ init_otel();
8088
+ init_auth();
8089
+
8090
+ // src/unrouted.ts
7352
8091
  init_cjs_shims();
7353
8092
  var import_node_fs26 = require("fs");
7354
8093
  var import_node_path43 = __toESM(require("path"), 1);
8094
+
8095
+ // src/daemon.ts
8096
+ var import_types28 = require("@neat.is/types");
8097
+ function daemonJsonPath(scanPath) {
8098
+ return import_node_path44.default.join(scanPath, "neat-out", "daemon.json");
8099
+ }
8100
+ function daemonsDiscoveryDir(home) {
8101
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
8102
+ return import_node_path44.default.join(base, "daemons");
8103
+ }
8104
+ function daemonDiscoveryPath(project, home) {
8105
+ return import_node_path44.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
8106
+ }
8107
+ function sanitizeDiscoveryName(project) {
8108
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
8109
+ }
8110
+ function neatHomeFromEnv() {
8111
+ const env = process.env.NEAT_HOME;
8112
+ if (env && env.length > 0) return import_node_path44.default.resolve(env);
8113
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8114
+ return import_node_path44.default.join(home, ".neat");
8115
+ }
8116
+ async function readDaemonRecord(scanPath) {
8117
+ try {
8118
+ const raw = await import_node_fs27.promises.readFile(daemonJsonPath(scanPath), "utf8");
8119
+ const parsed = JSON.parse(raw);
8120
+ if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
8121
+ return parsed;
8122
+ }
8123
+ return null;
8124
+ } catch {
8125
+ return null;
8126
+ }
8127
+ }
8128
+ function resolveNeatVersion() {
8129
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
8130
+ return process.env.NEAT_LOCAL_VERSION;
8131
+ }
8132
+ try {
8133
+ const req = (0, import_node_module.createRequire)(importMetaUrl);
8134
+ const pkg = req("../package.json");
8135
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8136
+ } catch {
8137
+ return "0.0.0";
8138
+ }
8139
+ }
8140
+ async function writeDaemonRecord(record, home) {
8141
+ const body = JSON.stringify(record, null, 2) + "\n";
8142
+ await writeAtomically(daemonJsonPath(record.projectPath), body);
8143
+ try {
8144
+ await writeAtomically(daemonDiscoveryPath(record.project, home), body);
8145
+ } catch (err) {
8146
+ console.warn(
8147
+ `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
8148
+ );
8149
+ }
8150
+ }
8151
+ async function clearDaemonRecord(record, home) {
8152
+ try {
8153
+ const stopped = { ...record, status: "stopped" };
8154
+ await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
8155
+ } catch {
8156
+ }
8157
+ try {
8158
+ await import_node_fs27.promises.unlink(daemonDiscoveryPath(record.project, home));
8159
+ } catch {
8160
+ }
8161
+ }
8162
+ function portFromListenAddress(address, fallback) {
8163
+ try {
8164
+ const port = new URL(address).port;
8165
+ const n = Number.parseInt(port, 10);
8166
+ if (Number.isFinite(n) && n > 0) return n;
8167
+ } catch {
8168
+ }
8169
+ return fallback;
8170
+ }
8171
+ function resolveHost(opts, authTokenSet) {
8172
+ if (opts.host && opts.host.length > 0) return opts.host;
8173
+ const env = process.env.HOST;
8174
+ if (env && env.length > 0) return env;
8175
+ if (!authTokenSet) return "127.0.0.1";
8176
+ return "0.0.0.0";
8177
+ }
8178
+
8179
+ // src/watch.ts
8180
+ init_otel_grpc();
8181
+
8182
+ // src/search.ts
8183
+ init_cjs_shims();
8184
+ var import_node_fs28 = require("fs");
8185
+ var import_node_path45 = __toESM(require("path"), 1);
7355
8186
  var import_node_crypto3 = require("crypto");
7356
8187
  var DEFAULT_LIMIT = 10;
7357
8188
  var NOMIC_DIM = 768;
@@ -7481,7 +8312,7 @@ async function pickEmbedder() {
7481
8312
  }
7482
8313
  async function readCache(cachePath) {
7483
8314
  try {
7484
- const raw = await import_node_fs26.promises.readFile(cachePath, "utf8");
8315
+ const raw = await import_node_fs28.promises.readFile(cachePath, "utf8");
7485
8316
  const parsed = JSON.parse(raw);
7486
8317
  if (parsed.version !== 1) return null;
7487
8318
  return parsed;
@@ -7490,8 +8321,8 @@ async function readCache(cachePath) {
7490
8321
  }
7491
8322
  }
7492
8323
  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));
8324
+ await import_node_fs28.promises.mkdir(import_node_path45.default.dirname(cachePath), { recursive: true });
8325
+ await import_node_fs28.promises.writeFile(cachePath, JSON.stringify(cache));
7495
8326
  }
7496
8327
  var VectorIndex = class {
7497
8328
  constructor(embedder, cachePath) {
@@ -7639,6 +8470,7 @@ async function buildSearchIndex(graph, options = {}) {
7639
8470
  var ALL_PHASES = [
7640
8471
  "services",
7641
8472
  "aliases",
8473
+ "files",
7642
8474
  "imports",
7643
8475
  "databases",
7644
8476
  "configs",
@@ -7647,8 +8479,8 @@ var ALL_PHASES = [
7647
8479
  ];
7648
8480
  function classifyChange(relPath) {
7649
8481
  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());
8482
+ const base = import_node_path46.default.basename(relPath).toLowerCase();
8483
+ const segments = relPath.split(import_node_path46.default.sep).map((s) => s.toLowerCase());
7652
8484
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
7653
8485
  phases.add("services");
7654
8486
  phases.add("aliases");
@@ -7663,6 +8495,7 @@ function classifyChange(relPath) {
7663
8495
  phases.add("aliases");
7664
8496
  }
7665
8497
  if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
8498
+ phases.add("files");
7666
8499
  phases.add("imports");
7667
8500
  phases.add("calls");
7668
8501
  }
@@ -7685,6 +8518,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
7685
8518
  if (phases.has("aliases")) {
7686
8519
  await addServiceAliases(graph, scanPath, services);
7687
8520
  }
8521
+ if (phases.has("files")) {
8522
+ const r = await addFiles(graph, services);
8523
+ nodesAdded += r.nodesAdded;
8524
+ edgesAdded += r.edgesAdded;
8525
+ }
7688
8526
  if (phases.has("imports")) {
7689
8527
  const r = await addImports(graph, services);
7690
8528
  nodesAdded += r.nodesAdded;
@@ -7719,6 +8557,7 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
7719
8557
  durationMs: Date.now() - started
7720
8558
  };
7721
8559
  }
8560
+ var DEFAULT_WATCH_WEB_PORT = 6328;
7722
8561
  var IGNORED_WATCH_GLOBS = [
7723
8562
  "**/node_modules/**",
7724
8563
  "**/.git/**",
@@ -7762,16 +8601,16 @@ function countWatchableDirs(scanPath, limit) {
7762
8601
  if (count >= limit) return;
7763
8602
  let entries;
7764
8603
  try {
7765
- entries = import_node_fs27.default.readdirSync(dir, { withFileTypes: true });
8604
+ entries = import_node_fs29.default.readdirSync(dir, { withFileTypes: true });
7766
8605
  } catch {
7767
8606
  return;
7768
8607
  }
7769
8608
  for (const e of entries) {
7770
8609
  if (count >= limit) return;
7771
8610
  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;
8611
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path46.default.join(dir, e.name) + import_node_path46.default.sep))) continue;
7773
8612
  count++;
7774
- if (depth < 2) visit(import_node_path44.default.join(dir, e.name), depth + 1);
8613
+ if (depth < 2) visit(import_node_path46.default.join(dir, e.name), depth + 1);
7775
8614
  }
7776
8615
  };
7777
8616
  visit(scanPath, 0);
@@ -7789,8 +8628,8 @@ async function startWatch(graph, opts) {
7789
8628
  const projectName = opts.project ?? DEFAULT_PROJECT;
7790
8629
  await loadGraphFromDisk(graph, opts.outPath);
7791
8630
  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");
8631
+ const policyFilePath = import_node_path46.default.join(opts.scanPath, "policy.json");
8632
+ const policyViolationsPath = import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "policy-violations.ndjson");
7794
8633
  let policies = [];
7795
8634
  try {
7796
8635
  policies = await loadPolicyFile(policyFilePath);
@@ -7841,7 +8680,7 @@ async function startWatch(graph, opts) {
7841
8680
  assertBindAuthority(host, auth.authToken);
7842
8681
  const port = opts.port ?? 8080;
7843
8682
  const otelPort = opts.otelPort ?? 4318;
7844
- const cachePath = opts.embeddingsCachePath ?? import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), "embeddings.json");
8683
+ const cachePath = opts.embeddingsCachePath ?? import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "embeddings.json");
7845
8684
  let searchIndex;
7846
8685
  try {
7847
8686
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -7859,7 +8698,7 @@ async function startWatch(graph, opts) {
7859
8698
  // Paths are derived from the explicit options the watch caller passes
7860
8699
  // — pathsForProject is only used to fill in the embeddings/snapshot
7861
8700
  // fields so the registry shape is complete.
7862
- ...pathsForProject(projectName, import_node_path44.default.dirname(opts.outPath)),
8701
+ ...pathsForProject(projectName, import_node_path46.default.dirname(opts.outPath)),
7863
8702
  snapshotPath: opts.outPath,
7864
8703
  errorsPath: opts.errorsPath,
7865
8704
  staleEventsPath: opts.staleEventsPath
@@ -7867,7 +8706,7 @@ async function startWatch(graph, opts) {
7867
8706
  searchIndex
7868
8707
  });
7869
8708
  const api = await buildApi({ projects: registry });
7870
- await api.listen({ port, host });
8709
+ const restAddress = await api.listen({ port, host });
7871
8710
  console.log(`neat-core listening on http://${host}:${port}`);
7872
8711
  console.log(` scan path: ${opts.scanPath} (watching for changes)`);
7873
8712
  console.log(` snapshot path: ${opts.outPath}`);
@@ -7880,10 +8719,41 @@ async function startWatch(graph, opts) {
7880
8719
  writeErrorEventInline: false,
7881
8720
  onPolicyTrigger
7882
8721
  });
7883
- const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath);
8722
+ const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath, graph, opts.scanPath);
7884
8723
  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`);
8724
+ const otelAddress = await listenSteppingOtlp(otelHttp, otelPort, host);
8725
+ const boundOtelPort = portFromListenAddress(otelAddress, otelPort);
8726
+ console.log(`neat-core OTLP receiver on ${otelAddress}/v1/traces`);
8727
+ const boundRestPort = portFromListenAddress(restAddress, port);
8728
+ const daemonRecord = {
8729
+ project: projectName,
8730
+ projectPath: opts.scanPath,
8731
+ pid: process.pid,
8732
+ status: "running",
8733
+ ports: {
8734
+ rest: boundRestPort,
8735
+ otlp: boundOtelPort,
8736
+ // watch serves no dashboard of its own; record the canonical web port so
8737
+ // the record shape is valid. Only ports.otlp is load-bearing here.
8738
+ web: DEFAULT_WATCH_WEB_PORT
8739
+ },
8740
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8741
+ neatVersion: resolveNeatVersion()
8742
+ };
8743
+ try {
8744
+ await writeDaemonRecord(daemonRecord);
8745
+ console.log(
8746
+ `neat watch: wrote daemon.json (REST ${boundRestPort} / OTLP ${boundOtelPort})`
8747
+ );
8748
+ } catch (err) {
8749
+ await api.close().catch(() => {
8750
+ });
8751
+ await otelHttp.close().catch(() => {
8752
+ });
8753
+ throw new Error(
8754
+ `neat watch: failed to write daemon.json \u2014 ${err.message}`
8755
+ );
8756
+ }
7887
8757
  let grpcReceiver = null;
7888
8758
  if (opts.otelGrpc) {
7889
8759
  const grpcPort = opts.otelGrpcPort ?? 4317;
@@ -7946,9 +8816,9 @@ async function startWatch(graph, opts) {
7946
8816
  };
7947
8817
  const onPath = (absPath) => {
7948
8818
  if (shouldIgnore(absPath)) return;
7949
- const rel = import_node_path44.default.relative(opts.scanPath, absPath);
8819
+ const rel = import_node_path46.default.relative(opts.scanPath, absPath);
7950
8820
  if (!rel || rel.startsWith("..")) return;
7951
- pendingPaths.add(rel.split(import_node_path44.default.sep).join("/"));
8821
+ pendingPaths.add(rel.split(import_node_path46.default.sep).join("/"));
7952
8822
  const phases = classifyChange(rel);
7953
8823
  if (phases.size === 0) {
7954
8824
  for (const p of ALL_PHASES) pending.add(p);
@@ -7996,14 +8866,16 @@ async function startWatch(graph, opts) {
7996
8866
  await api.close();
7997
8867
  await otelHttp.close();
7998
8868
  if (grpcReceiver) await grpcReceiver.stop();
8869
+ await clearDaemonRecord(daemonRecord).catch(() => {
8870
+ });
7999
8871
  };
8000
8872
  return { api, stop };
8001
8873
  }
8002
8874
 
8003
8875
  // src/deploy/detect.ts
8004
8876
  init_cjs_shims();
8005
- var import_node_fs28 = require("fs");
8006
- var import_node_path45 = __toESM(require("path"), 1);
8877
+ var import_node_fs30 = require("fs");
8878
+ var import_node_path47 = __toESM(require("path"), 1);
8007
8879
  var import_node_child_process2 = require("child_process");
8008
8880
  var import_node_crypto4 = require("crypto");
8009
8881
  function generateToken() {
@@ -8103,21 +8975,21 @@ async function runDeploy(opts = {}) {
8103
8975
  const token = generateToken();
8104
8976
  switch (substrate) {
8105
8977
  case "docker-compose": {
8106
- const artifactPath = import_node_path45.default.join(cwd, "docker-compose.neat.yml");
8978
+ const artifactPath = import_node_path47.default.join(cwd, "docker-compose.neat.yml");
8107
8979
  const contents = emitDockerCompose(cwd);
8108
- await import_node_fs28.promises.writeFile(artifactPath, contents, "utf8");
8980
+ await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
8109
8981
  return {
8110
8982
  substrate,
8111
8983
  artifactPath,
8112
8984
  token,
8113
8985
  contents,
8114
- startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path45.default.basename(artifactPath)} up -d`
8986
+ startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path47.default.basename(artifactPath)} up -d`
8115
8987
  };
8116
8988
  }
8117
8989
  case "systemd": {
8118
- const artifactPath = import_node_path45.default.join(cwd, "neat.service");
8990
+ const artifactPath = import_node_path47.default.join(cwd, "neat.service");
8119
8991
  const contents = emitSystemdUnit(cwd);
8120
- await import_node_fs28.promises.writeFile(artifactPath, contents, "utf8");
8992
+ await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
8121
8993
  return {
8122
8994
  substrate,
8123
8995
  artifactPath,
@@ -8150,8 +9022,8 @@ init_cjs_shims();
8150
9022
 
8151
9023
  // src/installers/javascript.ts
8152
9024
  init_cjs_shims();
8153
- var import_node_fs29 = require("fs");
8154
- var import_node_path46 = __toESM(require("path"), 1);
9025
+ var import_node_fs31 = require("fs");
9026
+ var import_node_path48 = __toESM(require("path"), 1);
8155
9027
  var import_semver2 = __toESM(require("semver"), 1);
8156
9028
 
8157
9029
  // src/installers/templates.ts
@@ -8747,15 +9619,15 @@ var OTEL_ENV = {
8747
9619
  value: "http://localhost:4318/projects/<project>/v1/traces"
8748
9620
  };
8749
9621
  function serviceNodeName(pkg, serviceDir) {
8750
- return pkg.name ?? import_node_path46.default.basename(serviceDir);
9622
+ return pkg.name ?? import_node_path48.default.basename(serviceDir);
8751
9623
  }
8752
9624
  function projectToken(pkg, serviceDir, project) {
8753
9625
  if (project && project.length > 0) return project;
8754
- return pkg.name ?? import_node_path46.default.basename(serviceDir);
9626
+ return pkg.name ?? import_node_path48.default.basename(serviceDir);
8755
9627
  }
8756
9628
  async function readJsonFile(p) {
8757
9629
  try {
8758
- const raw = await import_node_fs29.promises.readFile(p, "utf8");
9630
+ const raw = await import_node_fs31.promises.readFile(p, "utf8");
8759
9631
  return JSON.parse(raw);
8760
9632
  } catch {
8761
9633
  return null;
@@ -8764,16 +9636,16 @@ async function readJsonFile(p) {
8764
9636
  async function detectRuntimeKind(pkgRoot, pkg) {
8765
9637
  const deps = allDeps(pkg);
8766
9638
  if ("react-native" in deps || "expo" in deps) return "react-native";
8767
- const appJson = await readJsonFile(import_node_path46.default.join(pkgRoot, "app.json"));
9639
+ const appJson = await readJsonFile(import_node_path48.default.join(pkgRoot, "app.json"));
8768
9640
  if (appJson && typeof appJson === "object" && "expo" in appJson) {
8769
9641
  return "react-native";
8770
9642
  }
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) {
9643
+ 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
9644
  return "browser-bundle";
8773
9645
  }
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"))) {
9646
+ if (await exists3(import_node_path48.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
9647
+ if (await exists3(import_node_path48.default.join(pkgRoot, "bun.lockb"))) return "bun";
9648
+ if (await exists3(import_node_path48.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path48.default.join(pkgRoot, "deno.lock"))) {
8777
9649
  return "deno";
8778
9650
  }
8779
9651
  const engines = pkg.engines ?? {};
@@ -8782,7 +9654,7 @@ async function detectRuntimeKind(pkgRoot, pkg) {
8782
9654
  }
8783
9655
  async function readPackageJson2(serviceDir) {
8784
9656
  try {
8785
- const raw = await import_node_fs29.promises.readFile(import_node_path46.default.join(serviceDir, "package.json"), "utf8");
9657
+ const raw = await import_node_fs31.promises.readFile(import_node_path48.default.join(serviceDir, "package.json"), "utf8");
8786
9658
  return JSON.parse(raw);
8787
9659
  } catch {
8788
9660
  return null;
@@ -8790,7 +9662,7 @@ async function readPackageJson2(serviceDir) {
8790
9662
  }
8791
9663
  async function exists3(p) {
8792
9664
  try {
8793
- await import_node_fs29.promises.stat(p);
9665
+ await import_node_fs31.promises.stat(p);
8794
9666
  return true;
8795
9667
  } catch {
8796
9668
  return false;
@@ -8798,7 +9670,7 @@ async function exists3(p) {
8798
9670
  }
8799
9671
  async function readFileMaybe(p) {
8800
9672
  try {
8801
- return await import_node_fs29.promises.readFile(p, "utf8");
9673
+ return await import_node_fs31.promises.readFile(p, "utf8");
8802
9674
  } catch {
8803
9675
  return null;
8804
9676
  }
@@ -8826,7 +9698,7 @@ function needsVersionUpgrade(installed, expected) {
8826
9698
  var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
8827
9699
  async function findNextConfig(serviceDir) {
8828
9700
  for (const name of NEXT_CONFIG_CANDIDATES) {
8829
- const candidate = import_node_path46.default.join(serviceDir, name);
9701
+ const candidate = import_node_path48.default.join(serviceDir, name);
8830
9702
  if (await exists3(candidate)) return candidate;
8831
9703
  }
8832
9704
  return null;
@@ -8852,9 +9724,23 @@ var WEB_FRAMEWORK_DEPS = [
8852
9724
  "polka",
8853
9725
  "micro"
8854
9726
  ];
8855
- function looksLikeWebApp(pkg) {
9727
+ var WORKER_FRAMEWORK_DEPS = [
9728
+ "bullmq",
9729
+ "bull",
9730
+ "bee-queue",
9731
+ "agenda",
9732
+ "kafkajs",
9733
+ "amqplib",
9734
+ "amqp-connection-manager",
9735
+ "nats",
9736
+ "node-resque",
9737
+ "pg-boss",
9738
+ "graphile-worker"
9739
+ ];
9740
+ var APP_FRAMEWORK_DEPS = [...WEB_FRAMEWORK_DEPS, ...WORKER_FRAMEWORK_DEPS];
9741
+ function appFrameworkDependencies(pkg) {
8856
9742
  const deps = allDeps(pkg);
8857
- return WEB_FRAMEWORK_DEPS.some((name) => name in deps);
9743
+ return APP_FRAMEWORK_DEPS.filter((name) => name in deps);
8858
9744
  }
8859
9745
  function uninstrumentedLibraries(pkg) {
8860
9746
  const deps = allDeps(pkg);
@@ -8881,7 +9767,7 @@ function hasRemixDependency(pkg) {
8881
9767
  }
8882
9768
  async function findRemixEntry(serviceDir) {
8883
9769
  for (const rel of REMIX_ENTRY_CANDIDATES) {
8884
- const candidate = import_node_path46.default.join(serviceDir, rel);
9770
+ const candidate = import_node_path48.default.join(serviceDir, rel);
8885
9771
  if (await exists3(candidate)) return candidate;
8886
9772
  }
8887
9773
  return null;
@@ -8893,14 +9779,14 @@ function hasSvelteKitDependency(pkg) {
8893
9779
  }
8894
9780
  async function findSvelteKitHooks(serviceDir) {
8895
9781
  for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
8896
- const candidate = import_node_path46.default.join(serviceDir, rel);
9782
+ const candidate = import_node_path48.default.join(serviceDir, rel);
8897
9783
  if (await exists3(candidate)) return candidate;
8898
9784
  }
8899
9785
  return null;
8900
9786
  }
8901
9787
  async function findSvelteKitConfig(serviceDir) {
8902
9788
  for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
8903
- const candidate = import_node_path46.default.join(serviceDir, rel);
9789
+ const candidate = import_node_path48.default.join(serviceDir, rel);
8904
9790
  if (await exists3(candidate)) return candidate;
8905
9791
  }
8906
9792
  return null;
@@ -8911,7 +9797,7 @@ function hasNuxtDependency(pkg) {
8911
9797
  }
8912
9798
  async function findNuxtConfig(serviceDir) {
8913
9799
  for (const name of NUXT_CONFIG_CANDIDATES) {
8914
- const candidate = import_node_path46.default.join(serviceDir, name);
9800
+ const candidate = import_node_path48.default.join(serviceDir, name);
8915
9801
  if (await exists3(candidate)) return candidate;
8916
9802
  }
8917
9803
  return null;
@@ -8922,7 +9808,7 @@ function hasAstroDependency(pkg) {
8922
9808
  }
8923
9809
  async function findAstroConfig(serviceDir) {
8924
9810
  for (const name of ASTRO_CONFIG_CANDIDATES) {
8925
- const candidate = import_node_path46.default.join(serviceDir, name);
9811
+ const candidate = import_node_path48.default.join(serviceDir, name);
8926
9812
  if (await exists3(candidate)) return candidate;
8927
9813
  }
8928
9814
  return null;
@@ -8936,15 +9822,15 @@ function parseNextMajor(range) {
8936
9822
  return Number.isFinite(n) ? n : null;
8937
9823
  }
8938
9824
  async function isTypeScriptProject(serviceDir) {
8939
- return exists3(import_node_path46.default.join(serviceDir, "tsconfig.json"));
9825
+ return exists3(import_node_path48.default.join(serviceDir, "tsconfig.json"));
8940
9826
  }
8941
9827
  var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
8942
9828
  var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
8943
9829
  var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
8944
- var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
9830
+ var SRC_NAMED_CANDIDATES = ["server", "main", "app", "worker"].flatMap(
8945
9831
  (name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
8946
9832
  );
8947
- var ROOT_NAMED_CANDIDATES = ["server", "app", "main"].flatMap(
9833
+ var ROOT_NAMED_CANDIDATES = ["server", "app", "main", "worker"].flatMap(
8948
9834
  (name) => INDEX_EXTENSIONS.map((ext) => `${name}${ext}`)
8949
9835
  );
8950
9836
  var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
@@ -8985,7 +9871,7 @@ function entryFromScript(script) {
8985
9871
  }
8986
9872
  async function resolveEntry(serviceDir, pkg) {
8987
9873
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
8988
- const candidate = import_node_path46.default.resolve(serviceDir, pkg.main);
9874
+ const candidate = import_node_path48.default.resolve(serviceDir, pkg.main);
8989
9875
  if (await exists3(candidate)) return candidate;
8990
9876
  }
8991
9877
  if (pkg.bin) {
@@ -8999,40 +9885,40 @@ async function resolveEntry(serviceDir, pkg) {
8999
9885
  if (typeof first === "string") binEntry = first;
9000
9886
  }
9001
9887
  if (binEntry) {
9002
- const candidate = import_node_path46.default.resolve(serviceDir, binEntry);
9888
+ const candidate = import_node_path48.default.resolve(serviceDir, binEntry);
9003
9889
  if (await exists3(candidate)) return candidate;
9004
9890
  }
9005
9891
  }
9006
9892
  const startEntry = entryFromScript(pkg.scripts?.start);
9007
9893
  if (startEntry) {
9008
- const candidate = import_node_path46.default.resolve(serviceDir, startEntry);
9894
+ const candidate = import_node_path48.default.resolve(serviceDir, startEntry);
9009
9895
  if (await exists3(candidate)) return candidate;
9010
9896
  }
9011
9897
  const devEntry = entryFromScript(pkg.scripts?.dev);
9012
9898
  if (devEntry) {
9013
- const candidate = import_node_path46.default.resolve(serviceDir, devEntry);
9899
+ const candidate = import_node_path48.default.resolve(serviceDir, devEntry);
9014
9900
  if (await exists3(candidate)) return candidate;
9015
9901
  }
9016
9902
  for (const rel of SRC_INDEX_CANDIDATES) {
9017
- const candidate = import_node_path46.default.join(serviceDir, rel);
9903
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9018
9904
  if (await exists3(candidate)) return candidate;
9019
9905
  }
9020
9906
  for (const rel of SRC_NAMED_CANDIDATES) {
9021
- const candidate = import_node_path46.default.join(serviceDir, rel);
9907
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9022
9908
  if (await exists3(candidate)) return candidate;
9023
9909
  }
9024
9910
  for (const rel of ROOT_NAMED_CANDIDATES) {
9025
- const candidate = import_node_path46.default.join(serviceDir, rel);
9911
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9026
9912
  if (await exists3(candidate)) return candidate;
9027
9913
  }
9028
9914
  for (const name of INDEX_CANDIDATES) {
9029
- const candidate = import_node_path46.default.join(serviceDir, name);
9915
+ const candidate = import_node_path48.default.join(serviceDir, name);
9030
9916
  if (await exists3(candidate)) return candidate;
9031
9917
  }
9032
9918
  return null;
9033
9919
  }
9034
9920
  function dispatchEntry(entryFile, pkg) {
9035
- const ext = import_node_path46.default.extname(entryFile).toLowerCase();
9921
+ const ext = import_node_path48.default.extname(entryFile).toLowerCase();
9036
9922
  if (ext === ".ts" || ext === ".tsx") return "ts";
9037
9923
  if (ext === ".mjs") return "esm";
9038
9924
  if (ext === ".cjs") return "cjs";
@@ -9049,9 +9935,9 @@ function otelInitContents(flavor) {
9049
9935
  return OTEL_INIT_CJS;
9050
9936
  }
9051
9937
  function injectionLine(flavor, entryFile, otelInitFile) {
9052
- let rel = import_node_path46.default.relative(import_node_path46.default.dirname(entryFile), otelInitFile);
9938
+ let rel = import_node_path48.default.relative(import_node_path48.default.dirname(entryFile), otelInitFile);
9053
9939
  if (!rel.startsWith(".")) rel = `./${rel}`;
9054
- rel = rel.split(import_node_path46.default.sep).join("/");
9940
+ rel = rel.split(import_node_path48.default.sep).join("/");
9055
9941
  if (flavor === "cjs") return `require('${rel}')`;
9056
9942
  if (flavor === "esm") return `import '${rel}'`;
9057
9943
  const tsRel = rel.replace(/\.ts$/, "");
@@ -9064,23 +9950,23 @@ function lineIsOtelInjection(line) {
9064
9950
  }
9065
9951
  async function detectsSrcLayout(serviceDir) {
9066
9952
  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"))
9953
+ exists3(import_node_path48.default.join(serviceDir, "src", "app")),
9954
+ exists3(import_node_path48.default.join(serviceDir, "src", "pages")),
9955
+ exists3(import_node_path48.default.join(serviceDir, "app")),
9956
+ exists3(import_node_path48.default.join(serviceDir, "pages"))
9071
9957
  ]);
9072
9958
  return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
9073
9959
  }
9074
9960
  async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
9075
9961
  const useTs = await isTypeScriptProject(serviceDir);
9076
9962
  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(
9963
+ const baseDir = srcLayout ? import_node_path48.default.join(serviceDir, "src") : serviceDir;
9964
+ const instrumentationFile = import_node_path48.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
9965
+ const instrumentationNodeFile = import_node_path48.default.join(
9080
9966
  baseDir,
9081
9967
  useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
9082
9968
  );
9083
- const envNeatFile = import_node_path46.default.join(baseDir, ".env.neat");
9969
+ const envNeatFile = import_node_path48.default.join(baseDir, ".env.neat");
9084
9970
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
9085
9971
  const dependencyEdits = [];
9086
9972
  for (const sdk of SDK_PACKAGES) {
@@ -9137,7 +10023,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
9137
10023
  const nextMajor = parseNextMajor(nextRange);
9138
10024
  if (nextMajor !== null && nextMajor < 15) {
9139
10025
  try {
9140
- const raw = await import_node_fs29.promises.readFile(nextConfigPath, "utf8");
10026
+ const raw = await import_node_fs31.promises.readFile(nextConfigPath, "utf8");
9141
10027
  if (!raw.includes("instrumentationHook")) {
9142
10028
  nextConfigEdit = {
9143
10029
  file: nextConfigPath,
@@ -9185,7 +10071,7 @@ function buildDependencyEdits(pkg, manifestPath) {
9185
10071
  return edits;
9186
10072
  }
9187
10073
  async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
9188
- const envNeatFile = import_node_path46.default.join(serviceDir, ".env.neat");
10074
+ const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
9189
10075
  if (!await exists3(envNeatFile)) {
9190
10076
  generatedFiles.push({
9191
10077
  file: envNeatFile,
@@ -9220,7 +10106,7 @@ function fileImportsOtelHook(raw, specifiers) {
9220
10106
  }
9221
10107
  async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
9222
10108
  const useTs = await isTypeScriptProject(serviceDir);
9223
- const otelServerFile = import_node_path46.default.join(
10109
+ const otelServerFile = import_node_path48.default.join(
9224
10110
  serviceDir,
9225
10111
  useTs ? "app/otel.server.ts" : "app/otel.server.js"
9226
10112
  );
@@ -9241,7 +10127,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
9241
10127
  await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
9242
10128
  const entrypointEdits = [];
9243
10129
  try {
9244
- const raw = await import_node_fs29.promises.readFile(entryFile, "utf8");
10130
+ const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
9245
10131
  if (!fileImportsOtelHook(raw, ["./otel.server"])) {
9246
10132
  const lines = raw.split(/\r?\n/);
9247
10133
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
@@ -9277,11 +10163,11 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
9277
10163
  }
9278
10164
  async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
9279
10165
  const useTs = await isTypeScriptProject(serviceDir);
9280
- const otelInitFile = import_node_path46.default.join(
10166
+ const otelInitFile = import_node_path48.default.join(
9281
10167
  serviceDir,
9282
10168
  useTs ? "src/otel-init.ts" : "src/otel-init.js"
9283
10169
  );
9284
- const resolvedHooksFile = hooksFile ?? import_node_path46.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
10170
+ const resolvedHooksFile = hooksFile ?? import_node_path48.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
9285
10171
  const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
9286
10172
  const generatedFiles = [];
9287
10173
  const entrypointEdits = [];
@@ -9306,7 +10192,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
9306
10192
  });
9307
10193
  } else {
9308
10194
  try {
9309
- const raw = await import_node_fs29.promises.readFile(hooksFile, "utf8");
10195
+ const raw = await import_node_fs31.promises.readFile(hooksFile, "utf8");
9310
10196
  if (!fileImportsOtelHook(raw, ["./otel-init"])) {
9311
10197
  const lines = raw.split(/\r?\n/);
9312
10198
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
@@ -9343,11 +10229,11 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
9343
10229
  }
9344
10230
  async function planNuxt(serviceDir, pkg, manifestPath, project) {
9345
10231
  const useTs = await isTypeScriptProject(serviceDir);
9346
- const otelPluginFile = import_node_path46.default.join(
10232
+ const otelPluginFile = import_node_path48.default.join(
9347
10233
  serviceDir,
9348
10234
  useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
9349
10235
  );
9350
- const otelInitFile = import_node_path46.default.join(
10236
+ const otelInitFile = import_node_path48.default.join(
9351
10237
  serviceDir,
9352
10238
  useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
9353
10239
  );
@@ -9398,19 +10284,19 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
9398
10284
  var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
9399
10285
  async function findAstroMiddleware(serviceDir) {
9400
10286
  for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
9401
- const candidate = import_node_path46.default.join(serviceDir, rel);
10287
+ const candidate = import_node_path48.default.join(serviceDir, rel);
9402
10288
  if (await exists3(candidate)) return candidate;
9403
10289
  }
9404
10290
  return null;
9405
10291
  }
9406
10292
  async function planAstro(serviceDir, pkg, manifestPath, project) {
9407
10293
  const useTs = await isTypeScriptProject(serviceDir);
9408
- const otelInitFile = import_node_path46.default.join(
10294
+ const otelInitFile = import_node_path48.default.join(
9409
10295
  serviceDir,
9410
10296
  useTs ? "src/otel-init.ts" : "src/otel-init.js"
9411
10297
  );
9412
10298
  const existingMiddleware = await findAstroMiddleware(serviceDir);
9413
- const middlewareFile = existingMiddleware ?? import_node_path46.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
10299
+ const middlewareFile = existingMiddleware ?? import_node_path48.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
9414
10300
  const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
9415
10301
  const generatedFiles = [];
9416
10302
  const entrypointEdits = [];
@@ -9435,7 +10321,7 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
9435
10321
  });
9436
10322
  } else {
9437
10323
  try {
9438
- const raw = await import_node_fs29.promises.readFile(existingMiddleware, "utf8");
10324
+ const raw = await import_node_fs31.promises.readFile(existingMiddleware, "utf8");
9439
10325
  if (!fileImportsOtelHook(raw, ["./otel-init"])) {
9440
10326
  const lines = raw.split(/\r?\n/);
9441
10327
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
@@ -9506,7 +10392,7 @@ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
9506
10392
  }
9507
10393
  async function plan(serviceDir, opts) {
9508
10394
  const pkg = await readPackageJson2(serviceDir);
9509
- const manifestPath = import_node_path46.default.join(serviceDir, "package.json");
10395
+ const manifestPath = import_node_path48.default.join(serviceDir, "package.json");
9510
10396
  const project = opts?.project;
9511
10397
  const empty = {
9512
10398
  language: "javascript",
@@ -9541,8 +10427,8 @@ async function plan(serviceDir, opts) {
9541
10427
  return { ...empty, libOnly: true };
9542
10428
  }
9543
10429
  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");
10430
+ const otelInitFile = import_node_path48.default.join(import_node_path48.default.dirname(entryFile), otelInitFilename(flavor));
10431
+ const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
9546
10432
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
9547
10433
  const dependencyEdits = [];
9548
10434
  for (const sdk of SDK_PACKAGES) {
@@ -9566,7 +10452,7 @@ async function plan(serviceDir, opts) {
9566
10452
  }
9567
10453
  const entrypointEdits = [];
9568
10454
  try {
9569
- const raw = await import_node_fs29.promises.readFile(entryFile, "utf8");
10455
+ const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
9570
10456
  const lines = raw.split(/\r?\n/);
9571
10457
  const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
9572
10458
  if (!lineIsOtelInjection(firstReal)) {
@@ -9611,13 +10497,13 @@ async function plan(serviceDir, opts) {
9611
10497
  };
9612
10498
  }
9613
10499
  function isAllowedWritePath(serviceDir, target) {
9614
- const rel = import_node_path46.default.relative(serviceDir, target);
10500
+ const rel = import_node_path48.default.relative(serviceDir, target);
9615
10501
  if (rel.startsWith("..")) return false;
9616
- const base = import_node_path46.default.basename(target);
10502
+ const base = import_node_path48.default.basename(target);
9617
10503
  if (base === "package.json") return true;
9618
10504
  if (base === ".env.neat") return true;
9619
10505
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
9620
- const relPosix = rel.split(import_node_path46.default.sep).join("/");
10506
+ const relPosix = rel.split(import_node_path48.default.sep).join("/");
9621
10507
  if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
9622
10508
  if (relPosix === base) return true;
9623
10509
  if (relPosix === `src/${base}`) return true;
@@ -9634,10 +10520,10 @@ function isAllowedWritePath(serviceDir, target) {
9634
10520
  return false;
9635
10521
  }
9636
10522
  async function writeAtomic(file, contents) {
9637
- await import_node_fs29.promises.mkdir(import_node_path46.default.dirname(file), { recursive: true });
10523
+ await import_node_fs31.promises.mkdir(import_node_path48.default.dirname(file), { recursive: true });
9638
10524
  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);
10525
+ await import_node_fs31.promises.writeFile(tmp, contents, "utf8");
10526
+ await import_node_fs31.promises.rename(tmp, file);
9641
10527
  }
9642
10528
  async function apply(installPlan) {
9643
10529
  const { serviceDir } = installPlan;
@@ -9713,7 +10599,7 @@ async function apply(installPlan) {
9713
10599
  for (const target of allTargets) {
9714
10600
  if (await exists3(target)) {
9715
10601
  try {
9716
- originals.set(target, await import_node_fs29.promises.readFile(target, "utf8"));
10602
+ originals.set(target, await import_node_fs31.promises.readFile(target, "utf8"));
9717
10603
  } catch {
9718
10604
  }
9719
10605
  }
@@ -9796,14 +10682,14 @@ async function rollback(installPlan, originals, createdFiles) {
9796
10682
  const removed = [];
9797
10683
  for (const [file, raw] of originals.entries()) {
9798
10684
  try {
9799
- await import_node_fs29.promises.writeFile(file, raw, "utf8");
10685
+ await import_node_fs31.promises.writeFile(file, raw, "utf8");
9800
10686
  restored.push(file);
9801
10687
  } catch {
9802
10688
  }
9803
10689
  }
9804
10690
  for (const file of createdFiles) {
9805
10691
  try {
9806
- await import_node_fs29.promises.unlink(file);
10692
+ await import_node_fs31.promises.unlink(file);
9807
10693
  removed.push(file);
9808
10694
  } catch {
9809
10695
  }
@@ -9818,8 +10704,8 @@ async function rollback(installPlan, originals, createdFiles) {
9818
10704
  ...removed.map((f) => `removed: ${f}`),
9819
10705
  ""
9820
10706
  ];
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");
10707
+ const rollbackPath = import_node_path48.default.join(installPlan.serviceDir, "neat-rollback.patch");
10708
+ await import_node_fs31.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
9823
10709
  }
9824
10710
  function injectInstrumentationHook(raw) {
9825
10711
  if (raw.includes("instrumentationHook")) return raw;
@@ -9848,8 +10734,8 @@ var javascriptInstaller = {
9848
10734
 
9849
10735
  // src/installers/python.ts
9850
10736
  init_cjs_shims();
9851
- var import_node_fs30 = require("fs");
9852
- var import_node_path47 = __toESM(require("path"), 1);
10737
+ var import_node_fs32 = require("fs");
10738
+ var import_node_path49 = __toESM(require("path"), 1);
9853
10739
  var SDK_PACKAGES2 = [
9854
10740
  { name: "opentelemetry-distro", version: ">=0.49b0" },
9855
10741
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -9861,7 +10747,7 @@ var OTEL_ENV2 = {
9861
10747
  };
9862
10748
  async function exists4(p) {
9863
10749
  try {
9864
- await import_node_fs30.promises.stat(p);
10750
+ await import_node_fs32.promises.stat(p);
9865
10751
  return true;
9866
10752
  } catch {
9867
10753
  return false;
@@ -9870,7 +10756,7 @@ async function exists4(p) {
9870
10756
  async function detect2(serviceDir) {
9871
10757
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
9872
10758
  for (const m of markers) {
9873
- if (await exists4(import_node_path47.default.join(serviceDir, m))) return true;
10759
+ if (await exists4(import_node_path49.default.join(serviceDir, m))) return true;
9874
10760
  }
9875
10761
  return false;
9876
10762
  }
@@ -9880,9 +10766,9 @@ function reqPackageName(line) {
9880
10766
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
9881
10767
  }
9882
10768
  async function planRequirementsTxtEdits(serviceDir) {
9883
- const file = import_node_path47.default.join(serviceDir, "requirements.txt");
10769
+ const file = import_node_path49.default.join(serviceDir, "requirements.txt");
9884
10770
  if (!await exists4(file)) return null;
9885
- const raw = await import_node_fs30.promises.readFile(file, "utf8");
10771
+ const raw = await import_node_fs32.promises.readFile(file, "utf8");
9886
10772
  const presentNames = new Set(
9887
10773
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
9888
10774
  );
@@ -9890,9 +10776,9 @@ async function planRequirementsTxtEdits(serviceDir) {
9890
10776
  return { manifest: file, missing: [...missing] };
9891
10777
  }
9892
10778
  async function planProcfileEdits(serviceDir) {
9893
- const procfile = import_node_path47.default.join(serviceDir, "Procfile");
10779
+ const procfile = import_node_path49.default.join(serviceDir, "Procfile");
9894
10780
  if (!await exists4(procfile)) return [];
9895
- const raw = await import_node_fs30.promises.readFile(procfile, "utf8");
10781
+ const raw = await import_node_fs32.promises.readFile(procfile, "utf8");
9896
10782
  const edits = [];
9897
10783
  for (const line of raw.split(/\r?\n/)) {
9898
10784
  if (line.length === 0) continue;
@@ -9944,8 +10830,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
9944
10830
  const next = `${original}${trailing}${newlines.join("\n")}
9945
10831
  `;
9946
10832
  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);
10833
+ await import_node_fs32.promises.writeFile(tmp, next, "utf8");
10834
+ await import_node_fs32.promises.rename(tmp, manifest);
9949
10835
  }
9950
10836
  async function applyProcfile(procfile, edits, original) {
9951
10837
  let next = original;
@@ -9954,8 +10840,8 @@ async function applyProcfile(procfile, edits, original) {
9954
10840
  next = next.replace(e.before, e.after);
9955
10841
  }
9956
10842
  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);
10843
+ await import_node_fs32.promises.writeFile(tmp, next, "utf8");
10844
+ await import_node_fs32.promises.rename(tmp, procfile);
9959
10845
  }
9960
10846
  async function apply2(installPlan) {
9961
10847
  const { serviceDir } = installPlan;
@@ -9968,7 +10854,7 @@ async function apply2(installPlan) {
9968
10854
  const originals = /* @__PURE__ */ new Map();
9969
10855
  for (const file of touched) {
9970
10856
  try {
9971
- originals.set(file, await import_node_fs30.promises.readFile(file, "utf8"));
10857
+ originals.set(file, await import_node_fs32.promises.readFile(file, "utf8"));
9972
10858
  } catch {
9973
10859
  }
9974
10860
  }
@@ -9979,7 +10865,7 @@ async function apply2(installPlan) {
9979
10865
  if (raw === void 0) {
9980
10866
  throw new Error(`python installer: cannot read ${file} during apply`);
9981
10867
  }
9982
- const base = import_node_path47.default.basename(file);
10868
+ const base = import_node_path49.default.basename(file);
9983
10869
  if (base === "requirements.txt") {
9984
10870
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
9985
10871
  if (edits.length > 0) {
@@ -10004,7 +10890,7 @@ async function rollback2(installPlan, originals) {
10004
10890
  const restored = [];
10005
10891
  for (const [file, raw] of originals.entries()) {
10006
10892
  try {
10007
- await import_node_fs30.promises.writeFile(file, raw, "utf8");
10893
+ await import_node_fs32.promises.writeFile(file, raw, "utf8");
10008
10894
  restored.push(file);
10009
10895
  } catch {
10010
10896
  }
@@ -10018,8 +10904,8 @@ async function rollback2(installPlan, originals) {
10018
10904
  ...restored.map((f) => `restored: ${f}`),
10019
10905
  ""
10020
10906
  ];
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");
10907
+ const rollbackPath = import_node_path49.default.join(installPlan.serviceDir, "neat-rollback.patch");
10908
+ await import_node_fs32.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
10023
10909
  }
10024
10910
  var pythonInstaller = {
10025
10911
  name: "python",
@@ -10148,38 +11034,6 @@ var import_node_net = __toESM(require("net"), 1);
10148
11034
  var import_node_path50 = __toESM(require("path"), 1);
10149
11035
  var import_node_child_process3 = require("child_process");
10150
11036
  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
11037
  async function extractAndPersist(opts) {
10184
11038
  const services = await discoverServices(opts.scanPath);
10185
11039
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
@@ -10236,11 +11090,13 @@ async function applyInstallersOver(services, project, options = {}) {
10236
11090
  } else if (outcome.outcome === "already-instrumented") already++;
10237
11091
  else if (outcome.outcome === "lib-only") {
10238
11092
  libOnly++;
10239
- if (svc.pkg && looksLikeWebApp(svc.pkg)) {
11093
+ const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
11094
+ if (appDeps.length > 0) {
10240
11095
  const svcName = import_node_path50.default.basename(svc.dir);
11096
+ const list = appDeps.join(", ");
10241
11097
  console.warn(
10242
11098
  `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.
11099
+ ${svc.dir} depends on ${list} \u2014 a runnable app \u2014 but neat couldn't resolve an entry to instrument.
10244
11100
  Add a "start" script to package.json, or point neat at the entry file directly.`
10245
11101
  );
10246
11102
  }
@@ -10308,10 +11164,11 @@ async function applyInstallersOver(services, project, options = {}) {
10308
11164
  if (gaps.length > 0) {
10309
11165
  const svcName = import_node_path50.default.basename(svc.dir);
10310
11166
  const list = gaps.join(", ");
10311
- const verb = gaps.length === 1 ? "this library" : "these libraries";
11167
+ const subject = gaps.length === 1 ? "this library" : "these libraries";
11168
+ const aux = gaps.length === 1 ? "isn't" : "aren't";
10312
11169
  console.warn(
10313
11170
  `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.
11171
+ ${subject} ${aux} in the default instrumentation set, so they produce no OBSERVED edges.
10315
11172
  Run \`neat list-uninstrumented\` to review them, then \`neat extend\` to capture them.`
10316
11173
  );
10317
11174
  }
@@ -10405,29 +11262,22 @@ async function probeProjectHealth(restPort, name) {
10405
11262
  });
10406
11263
  });
10407
11264
  }
10408
- async function snapshotProjectStatus(restPort, body) {
11265
+ async function snapshotProjectStatus(restPort, project, body) {
10409
11266
  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
- );
11267
+ const mine = body.projects.filter((p) => p.name === project);
11268
+ if (mine.length > 0) {
11269
+ return mine.map((p) => ({ name: p.name, status: p.status ?? "active" }));
11270
+ }
11271
+ }
11272
+ return [{ name: project, status: await probeProjectHealth(restPort, project) }];
10423
11273
  }
10424
- async function waitForDaemonReady(restPort, timeoutMs) {
11274
+ async function waitForDaemonReady(restPort, project, timeoutMs) {
10425
11275
  const deadline = Date.now() + timeoutMs;
10426
11276
  let lastBootstrapping = [];
10427
11277
  while (Date.now() < deadline) {
10428
11278
  const body = await fetchDaemonHealth(restPort);
10429
11279
  if (body !== null) {
10430
- const projects2 = await snapshotProjectStatus(restPort, body);
11280
+ const projects2 = await snapshotProjectStatus(restPort, project, body);
10431
11281
  const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
10432
11282
  const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
10433
11283
  if (bootstrapping.length === 0) {
@@ -10446,7 +11296,7 @@ async function waitForDaemonReady(restPort, timeoutMs) {
10446
11296
  await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
10447
11297
  }
10448
11298
  const final = await fetchDaemonHealth(restPort);
10449
- const projects = final ? await snapshotProjectStatus(restPort, final) : [];
11299
+ const projects = final ? await snapshotProjectStatus(restPort, project, final) : [];
10450
11300
  return {
10451
11301
  ready: false,
10452
11302
  brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
@@ -10454,14 +11304,37 @@ async function waitForDaemonReady(restPort, timeoutMs) {
10454
11304
  };
10455
11305
  }
10456
11306
  var NEAT_PORTS = [8080, 4318, 6328];
10457
- async function isPortFree(port) {
11307
+ function probeBind(port, host) {
10458
11308
  return new Promise((resolve) => {
10459
11309
  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");
11310
+ server.once("error", (err) => {
11311
+ resolve(err.code === "EADDRINUSE" ? "in-use" : "unavailable");
11312
+ });
11313
+ server.once("listening", () => server.close(() => resolve("free")));
11314
+ server.listen(port, host);
10463
11315
  });
10464
11316
  }
11317
+ function crossFamilyHost(host) {
11318
+ switch (host) {
11319
+ case "127.0.0.1":
11320
+ case "localhost":
11321
+ return "::1";
11322
+ case "::1":
11323
+ return "127.0.0.1";
11324
+ case "0.0.0.0":
11325
+ return "::";
11326
+ case "::":
11327
+ return "0.0.0.0";
11328
+ default:
11329
+ return null;
11330
+ }
11331
+ }
11332
+ async function isPortFree(port, host = "127.0.0.1") {
11333
+ if (await probeBind(port, host) !== "free") return false;
11334
+ const sibling = crossFamilyHost(host);
11335
+ if (sibling && await probeBind(port, sibling) === "in-use") return false;
11336
+ return true;
11337
+ }
10465
11338
  function formatPortCollisionMessage(port) {
10466
11339
  return [
10467
11340
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -10471,13 +11344,13 @@ function formatPortCollisionMessage(port) {
10471
11344
  }
10472
11345
  var PORT_ALLOCATION_ATTEMPTS = 8;
10473
11346
  var PORT_STRIDE = 1;
10474
- async function tripleFree(ports) {
11347
+ async function tripleFree(ports, host = "127.0.0.1") {
10475
11348
  for (const p of [ports.rest, ports.otlp, ports.web]) {
10476
- if (!await isPortFree(p)) return false;
11349
+ if (!await isPortFree(p, host)) return false;
10477
11350
  }
10478
11351
  return true;
10479
11352
  }
10480
- async function allocatePorts() {
11353
+ async function allocatePorts(host = "127.0.0.1") {
10481
11354
  const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
10482
11355
  for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
10483
11356
  const candidate = {
@@ -10485,7 +11358,7 @@ async function allocatePorts() {
10485
11358
  otlp: baseOtlp + i * PORT_STRIDE,
10486
11359
  web: baseWeb + i * PORT_STRIDE
10487
11360
  };
10488
- if (await tripleFree(candidate)) return candidate;
11361
+ if (await tripleFree(candidate, host)) return candidate;
10489
11362
  }
10490
11363
  return null;
10491
11364
  }
@@ -10691,16 +11564,20 @@ async function runOrchestrator(opts) {
10691
11564
  }
10692
11565
  }
10693
11566
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
11567
+ const bindHost = resolveHost(
11568
+ {},
11569
+ typeof process.env.NEAT_AUTH_TOKEN === "string" && process.env.NEAT_AUTH_TOKEN.length > 0
11570
+ );
10694
11571
  const persistedPorts = await persistedPortsFor(opts.scanPath);
10695
11572
  let allocated = null;
10696
11573
  if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
10697
11574
  result.steps.daemon = "already-running";
10698
11575
  allocated = persistedPorts;
10699
11576
  } else {
10700
- if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
11577
+ if (persistedPorts && await isPortFree(persistedPorts.rest, bindHost) && await tripleFree(persistedPorts, bindHost)) {
10701
11578
  allocated = persistedPorts;
10702
11579
  } else {
10703
- allocated = await allocatePorts();
11580
+ allocated = await allocatePorts(bindHost);
10704
11581
  }
10705
11582
  if (!allocated) {
10706
11583
  for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
@@ -10729,7 +11606,7 @@ async function runOrchestrator(opts) {
10729
11606
  projectPath: opts.scanPath,
10730
11607
  ports: allocated
10731
11608
  });
10732
- const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
11609
+ const ready = await waitForDaemonReady(allocated.rest, currentProjectName, timeoutMs);
10733
11610
  result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10734
11611
  if (!ready.ready) {
10735
11612
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
@@ -10797,7 +11674,7 @@ var import_node_path51 = __toESM(require("path"), 1);
10797
11674
 
10798
11675
  // src/cli-client.ts
10799
11676
  init_cjs_shims();
10800
- var import_types27 = require("@neat.is/types");
11677
+ var import_types29 = require("@neat.is/types");
10801
11678
  var HttpError = class extends Error {
10802
11679
  constructor(status2, message, responseBody = "") {
10803
11680
  super(message);
@@ -10913,7 +11790,7 @@ async function runBlastRadius(client, input) {
10913
11790
  const result = await client.get(path53);
10914
11791
  if (result.totalAffected === 0) {
10915
11792
  return {
10916
- summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
11793
+ summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
10917
11794
  };
10918
11795
  }
10919
11796
  const sorted = [...result.affectedNodes].sort(
@@ -10926,7 +11803,7 @@ async function runBlastRadius(client, input) {
10926
11803
  );
10927
11804
  const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
10928
11805
  return {
10929
- summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
11806
+ summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
10930
11807
  block: blockLines.join("\n"),
10931
11808
  confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
10932
11809
  provenance: provenances.length ? provenances : void 0
@@ -10939,7 +11816,7 @@ async function runBlastRadius(client, input) {
10939
11816
  }
10940
11817
  }
10941
11818
  function formatBlastEntry(n) {
10942
- const tag = n.edgeProvenance === import_types27.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
11819
+ const tag = n.edgeProvenance === import_types29.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
10943
11820
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
10944
11821
  }
10945
11822
  async function runDependencies(client, input) {
@@ -10980,22 +11857,33 @@ async function runDependencies(client, input) {
10980
11857
  throw err;
10981
11858
  }
10982
11859
  }
11860
+ function observedDepLine(nodeId, e) {
11861
+ const via = e.source !== nodeId ? ` (via ${e.source})` : "";
11862
+ return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
11863
+ }
10983
11864
  async function runObservedDependencies(client, input) {
10984
11865
  try {
10985
- const edges = await client.get(
10986
- projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
11866
+ const result = await client.get(
11867
+ projectPath(
11868
+ input.project,
11869
+ `/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
11870
+ )
10987
11871
  );
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?" : "";
11872
+ if (result.dependencies.length === 0) {
11873
+ if (result.observed) {
11874
+ return {
11875
+ 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.`,
11876
+ provenance: import_types29.Provenance.OBSERVED
11877
+ };
11878
+ }
11879
+ const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
10992
11880
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
10993
11881
  }
10994
- const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
11882
+ const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
10995
11883
  return {
10996
- summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
11884
+ summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
10997
11885
  block: blockLines.join("\n"),
10998
- provenance: import_types27.Provenance.OBSERVED
11886
+ provenance: import_types29.Provenance.OBSERVED
10999
11887
  };
11000
11888
  } catch (err) {
11001
11889
  if (err instanceof HttpError && err.status === 404) {
@@ -11049,7 +11937,7 @@ async function runIncidents(client, input) {
11049
11937
  return {
11050
11938
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
11051
11939
  block: blockLines.join("\n"),
11052
- provenance: import_types27.Provenance.OBSERVED
11940
+ provenance: import_types29.Provenance.OBSERVED
11053
11941
  };
11054
11942
  } catch (err) {
11055
11943
  if (err instanceof HttpError && err.status === 404) {
@@ -11158,7 +12046,7 @@ async function runStaleEdges(client, input) {
11158
12046
  return {
11159
12047
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
11160
12048
  block: blockLines.join("\n"),
11161
- provenance: import_types27.Provenance.STALE
12049
+ provenance: import_types29.Provenance.STALE
11162
12050
  };
11163
12051
  }
11164
12052
  async function runPolicies(client, input) {
@@ -11475,7 +12363,7 @@ async function runSync(opts) {
11475
12363
  }
11476
12364
 
11477
12365
  // src/cli.ts
11478
- var import_types28 = require("@neat.is/types");
12366
+ var import_types30 = require("@neat.is/types");
11479
12367
  function isNpxInvocation() {
11480
12368
  if (process.env.npm_command === "exec") return true;
11481
12369
  const execpath = process.env.npm_execpath ?? "";
@@ -11540,7 +12428,7 @@ function usage() {
11540
12428
  console.log("query commands (mirror the MCP tools, ADR-050):");
11541
12429
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
11542
12430
  console.log(` example: ${neat} root-cause service:<name>`);
11543
- console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
12431
+ console.log(" blast-radius <node-id> BFS inbound \u2014 the dependents that break if this dies.");
11544
12432
  console.log(` example: ${neat} blast-radius database:<host>`);
11545
12433
  console.log(" dependencies <node-id> Transitive outbound dependencies.");
11546
12434
  console.log(" Flags: --depth N (default 3, max 10)");
@@ -12267,11 +13155,18 @@ Pass --project <name> to choose:
12267
13155
  ${names}`
12268
13156
  );
12269
13157
  }
12270
- function resolveDaemonUrl() {
12271
- return process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL ?? "http://localhost:8080";
13158
+ async function resolveDaemonUrl(project) {
13159
+ const explicit = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
13160
+ if (explicit) return explicit;
13161
+ if (project) {
13162
+ const daemon = await findDaemonByProject(project);
13163
+ if (daemon) return `http://localhost:${daemon.record.ports.rest}`;
13164
+ }
13165
+ return "http://localhost:8080";
12272
13166
  }
12273
13167
  async function runQueryVerb(cmd, parsed) {
12274
- const baseUrl = resolveDaemonUrl();
13168
+ const requestedProject = resolveProjectFlag(parsed);
13169
+ const baseUrl = await resolveDaemonUrl(requestedProject);
12275
13170
  const client = createHttpClient(baseUrl, resolveAuthToken());
12276
13171
  const positional = parsed.positional;
12277
13172
  let makeWork;
@@ -12389,10 +13284,10 @@ async function runQueryVerb(cmd, parsed) {
12389
13284
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
12390
13285
  const out = [];
12391
13286
  for (const p of parts) {
12392
- const r = import_types28.DivergenceTypeSchema.safeParse(p);
13287
+ const r = import_types30.DivergenceTypeSchema.safeParse(p);
12393
13288
  if (!r.success) {
12394
13289
  console.error(
12395
- `neat divergences: unknown --type "${p}". allowed: ${import_types28.DivergenceTypeSchema.options.join(", ")}`
13290
+ `neat divergences: unknown --type "${p}". allowed: ${import_types30.DivergenceTypeSchema.options.join(", ")}`
12396
13291
  );
12397
13292
  return 2;
12398
13293
  }
@@ -12427,7 +13322,7 @@ async function runQueryVerb(cmd, parsed) {
12427
13322
  const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
12428
13323
  console.error(`neat ${cmd}: ${detail.trim()}`);
12429
13324
  } else if (err instanceof TransportError) {
12430
- console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${resolveDaemonUrl()})`);
13325
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${baseUrl})`);
12431
13326
  } else {
12432
13327
  console.error(`neat ${cmd}: ${err.message}`);
12433
13328
  }
@@ -12452,6 +13347,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
12452
13347
  parseArgs,
12453
13348
  printBanner,
12454
13349
  readPackageVersion,
13350
+ resolveDaemonUrl,
12455
13351
  resolveProjectForVerb,
12456
13352
  runInit,
12457
13353
  runQueryVerb,