@neat.is/core 0.2.9 → 0.3.0

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/server.cjs CHANGED
@@ -392,12 +392,10 @@ function getGraph(project = DEFAULT_PROJECT) {
392
392
  init_cjs_shims();
393
393
  var import_fastify = __toESM(require("fastify"), 1);
394
394
  var import_cors = __toESM(require("@fastify/cors"), 1);
395
- var import_types19 = require("@neat.is/types");
395
+ var import_types20 = require("@neat.is/types");
396
396
 
397
- // src/policy.ts
397
+ // src/divergences.ts
398
398
  init_cjs_shims();
399
- var import_node_fs2 = require("fs");
400
- var import_node_path2 = __toESM(require("path"), 1);
401
399
  var import_types2 = require("@neat.is/types");
402
400
 
403
401
  // src/compat.ts
@@ -713,18 +711,6 @@ function deprecatedApis() {
713
711
  return currentMatrix().deprecatedApis ?? [];
714
712
  }
715
713
 
716
- // src/events.ts
717
- init_cjs_shims();
718
- var import_node_events = require("events");
719
- var EVENT_BUS_CHANNEL = "event";
720
- var NeatEventBus = class extends import_node_events.EventEmitter {
721
- };
722
- var eventBus = new NeatEventBus();
723
- eventBus.setMaxListeners(0);
724
- function emitNeatEvent(envelope) {
725
- eventBus.emit(EVENT_BUS_CHANNEL, envelope);
726
- }
727
-
728
714
  // src/traverse.ts
729
715
  init_cjs_shims();
730
716
  var import_types = require("@neat.is/types");
@@ -1006,6 +992,259 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
1006
992
  });
1007
993
  }
1008
994
 
995
+ // src/divergences.ts
996
+ function bucketKey(source, target, type) {
997
+ return `${type}|${source}|${target}`;
998
+ }
999
+ function bucketEdges(graph) {
1000
+ const buckets = /* @__PURE__ */ new Map();
1001
+ graph.forEachEdge((id, attrs) => {
1002
+ const e = attrs;
1003
+ const parsed = (0, import_types2.parseEdgeId)(id);
1004
+ const provenance = parsed?.provenance ?? e.provenance;
1005
+ const key = bucketKey(e.source, e.target, e.type);
1006
+ const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
1007
+ switch (provenance) {
1008
+ case import_types2.Provenance.EXTRACTED:
1009
+ cur.extracted = e;
1010
+ break;
1011
+ case import_types2.Provenance.OBSERVED:
1012
+ cur.observed = e;
1013
+ break;
1014
+ case import_types2.Provenance.INFERRED:
1015
+ cur.inferred = e;
1016
+ break;
1017
+ case import_types2.Provenance.FRONTIER:
1018
+ cur.frontier = e;
1019
+ break;
1020
+ default:
1021
+ if (e.provenance === import_types2.Provenance.STALE) cur.stale = e;
1022
+ }
1023
+ buckets.set(key, cur);
1024
+ });
1025
+ return buckets;
1026
+ }
1027
+ function nodeIsFrontier(graph, nodeId) {
1028
+ if (!graph.hasNode(nodeId)) return false;
1029
+ const attrs = graph.getNodeAttributes(nodeId);
1030
+ return attrs.type === import_types2.NodeType.FrontierNode;
1031
+ }
1032
+ function hasAnyObservedFromSource(graph, sourceId) {
1033
+ if (!graph.hasNode(sourceId)) return false;
1034
+ for (const edgeId of graph.outboundEdges(sourceId)) {
1035
+ const e = graph.getEdgeAttributes(edgeId);
1036
+ if (e.provenance === import_types2.Provenance.OBSERVED) return true;
1037
+ }
1038
+ return false;
1039
+ }
1040
+ function clampConfidence(n) {
1041
+ if (!Number.isFinite(n)) return 0;
1042
+ return Math.max(0, Math.min(1, n));
1043
+ }
1044
+ function reasonForMissingObserved(source, target, type) {
1045
+ return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
1046
+ }
1047
+ function reasonForMissingExtracted(source, target, type) {
1048
+ return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
1049
+ }
1050
+ var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
1051
+ var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
1052
+ var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
1053
+ function detectMissingDivergences(graph, bucket) {
1054
+ const out = [];
1055
+ if (bucket.extracted && !bucket.observed) {
1056
+ if (!nodeIsFrontier(graph, bucket.target)) {
1057
+ const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
1058
+ out.push({
1059
+ type: "missing-observed",
1060
+ source: bucket.source,
1061
+ target: bucket.target,
1062
+ edgeType: bucket.type,
1063
+ extracted: bucket.extracted,
1064
+ confidence: sourceHasTraffic ? 1 : 0.5,
1065
+ reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
1066
+ recommendation: RECOMMENDATION_MISSING_OBSERVED
1067
+ });
1068
+ }
1069
+ }
1070
+ if (bucket.observed && !bucket.extracted) {
1071
+ const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
1072
+ out.push({
1073
+ type: "missing-extracted",
1074
+ source: bucket.source,
1075
+ target: bucket.target,
1076
+ edgeType: bucket.type,
1077
+ observed: bucket.observed,
1078
+ confidence: cascaded,
1079
+ reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
1080
+ recommendation: RECOMMENDATION_MISSING_EXTRACTED
1081
+ });
1082
+ }
1083
+ return out;
1084
+ }
1085
+ function declaredHostFor(svc) {
1086
+ const raw = svc.dbConnectionTarget?.trim();
1087
+ if (!raw) return null;
1088
+ const colon = raw.lastIndexOf(":");
1089
+ if (colon === -1) return raw;
1090
+ const port = raw.slice(colon + 1);
1091
+ if (/^\d+$/.test(port)) return raw.slice(0, colon);
1092
+ return raw;
1093
+ }
1094
+ function hasExtractedConfiguredBy(graph, svcId) {
1095
+ for (const edgeId of graph.outboundEdges(svcId)) {
1096
+ const e = graph.getEdgeAttributes(edgeId);
1097
+ if (e.type === import_types2.EdgeType.CONFIGURED_BY && e.provenance === import_types2.Provenance.EXTRACTED) {
1098
+ return true;
1099
+ }
1100
+ }
1101
+ return false;
1102
+ }
1103
+ function detectHostMismatch(graph, svcId, svc) {
1104
+ const declaredHost = declaredHostFor(svc);
1105
+ if (!declaredHost) return [];
1106
+ if (!hasExtractedConfiguredBy(graph, svcId)) return [];
1107
+ const out = [];
1108
+ for (const edgeId of graph.outboundEdges(svcId)) {
1109
+ const edge = graph.getEdgeAttributes(edgeId);
1110
+ if (edge.type !== import_types2.EdgeType.CONNECTS_TO) continue;
1111
+ if (edge.provenance !== import_types2.Provenance.OBSERVED) continue;
1112
+ const target = graph.getNodeAttributes(edge.target);
1113
+ if (target.type !== import_types2.NodeType.DatabaseNode) continue;
1114
+ const observedHost = target.host?.trim();
1115
+ if (!observedHost) continue;
1116
+ if (observedHost === declaredHost) continue;
1117
+ out.push({
1118
+ type: "host-mismatch",
1119
+ source: svcId,
1120
+ target: edge.target,
1121
+ extractedHost: declaredHost,
1122
+ observedHost,
1123
+ confidence: clampConfidence(confidenceForEdge(edge)),
1124
+ reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
1125
+ recommendation: RECOMMENDATION_HOST_MISMATCH
1126
+ });
1127
+ }
1128
+ return out;
1129
+ }
1130
+ function detectCompatDivergences(graph, svcId, svc) {
1131
+ const out = [];
1132
+ const deps = svc.dependencies ?? {};
1133
+ for (const edgeId of graph.outboundEdges(svcId)) {
1134
+ const edge = graph.getEdgeAttributes(edgeId);
1135
+ if (edge.type !== import_types2.EdgeType.CONNECTS_TO) continue;
1136
+ if (edge.provenance !== import_types2.Provenance.OBSERVED) continue;
1137
+ const target = graph.getNodeAttributes(edge.target);
1138
+ if (target.type !== import_types2.NodeType.DatabaseNode) continue;
1139
+ for (const pair of compatPairs()) {
1140
+ if (pair.engine !== target.engine) continue;
1141
+ const declared = deps[pair.driver];
1142
+ if (!declared) continue;
1143
+ const result = checkCompatibility(
1144
+ pair.driver,
1145
+ declared,
1146
+ target.engine,
1147
+ target.engineVersion
1148
+ );
1149
+ if (!result.compatible && result.reason) {
1150
+ out.push({
1151
+ type: "version-mismatch",
1152
+ source: svcId,
1153
+ target: edge.target,
1154
+ extractedVersion: declared,
1155
+ observedVersion: target.engineVersion,
1156
+ compatibility: "incompatible",
1157
+ confidence: 1,
1158
+ reason: result.reason,
1159
+ recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
1160
+ });
1161
+ }
1162
+ }
1163
+ for (const rule of deprecatedApis()) {
1164
+ const declared = deps[rule.package];
1165
+ if (!declared) continue;
1166
+ const result = checkDeprecatedApi(rule, declared);
1167
+ if (!result.compatible && result.reason) {
1168
+ const ruleRef = {
1169
+ kind: rule.kind ?? "deprecated-api",
1170
+ reason: result.reason,
1171
+ package: rule.package
1172
+ };
1173
+ out.push({
1174
+ type: "compat-violation",
1175
+ source: svcId,
1176
+ target: edge.target,
1177
+ rule: ruleRef,
1178
+ observed: edge,
1179
+ confidence: 1,
1180
+ reason: result.reason,
1181
+ recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
1182
+ });
1183
+ }
1184
+ }
1185
+ }
1186
+ return out;
1187
+ }
1188
+ function involvesNode(d, nodeId) {
1189
+ return d.source === nodeId || d.target === nodeId;
1190
+ }
1191
+ function computeDivergences(graph, opts = {}) {
1192
+ const all = [];
1193
+ const buckets = bucketEdges(graph);
1194
+ for (const bucket of buckets.values()) {
1195
+ for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
1196
+ }
1197
+ graph.forEachNode((nodeId, attrs) => {
1198
+ const n = attrs;
1199
+ if (n.type !== import_types2.NodeType.ServiceNode) return;
1200
+ const svc = n;
1201
+ for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
1202
+ for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
1203
+ });
1204
+ let filtered = all;
1205
+ if (opts.type) {
1206
+ const allowed = opts.type;
1207
+ filtered = filtered.filter((d) => allowed.has(d.type));
1208
+ }
1209
+ if (opts.minConfidence !== void 0) {
1210
+ const threshold = opts.minConfidence;
1211
+ filtered = filtered.filter((d) => d.confidence >= threshold);
1212
+ }
1213
+ if (opts.node) {
1214
+ const target = opts.node;
1215
+ filtered = filtered.filter((d) => involvesNode(d, target));
1216
+ }
1217
+ filtered.sort((a, b) => {
1218
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
1219
+ if (a.type !== b.type) return a.type.localeCompare(b.type);
1220
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
1221
+ return a.target.localeCompare(b.target);
1222
+ });
1223
+ return import_types2.DivergenceResultSchema.parse({
1224
+ divergences: filtered,
1225
+ totalAffected: filtered.length,
1226
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
1227
+ });
1228
+ }
1229
+
1230
+ // src/policy.ts
1231
+ init_cjs_shims();
1232
+ var import_node_fs2 = require("fs");
1233
+ var import_node_path2 = __toESM(require("path"), 1);
1234
+ var import_types3 = require("@neat.is/types");
1235
+
1236
+ // src/events.ts
1237
+ init_cjs_shims();
1238
+ var import_node_events = require("events");
1239
+ var EVENT_BUS_CHANNEL = "event";
1240
+ var NeatEventBus = class extends import_node_events.EventEmitter {
1241
+ };
1242
+ var eventBus = new NeatEventBus();
1243
+ eventBus.setMaxListeners(0);
1244
+ function emitNeatEvent(envelope) {
1245
+ eventBus.emit(EVENT_BUS_CHANNEL, envelope);
1246
+ }
1247
+
1009
1248
  // src/policy.ts
1010
1249
  var DEFAULT_ACTION_BY_SEVERITY = {
1011
1250
  info: "log",
@@ -1043,7 +1282,7 @@ var evaluateStructural = ({
1043
1282
  for (const edgeId of graph.outboundEdges(id)) {
1044
1283
  const e = graph.getEdgeAttributes(edgeId);
1045
1284
  if (e.type !== rule.edgeType) continue;
1046
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1285
+ if (e.provenance === import_types3.Provenance.FRONTIER) continue;
1047
1286
  const target = graph.getNodeAttributes(e.target);
1048
1287
  if (target.type === rule.toNodeType) {
1049
1288
  satisfied = true;
@@ -1156,16 +1395,16 @@ var evaluateCompatibility = ({
1156
1395
  const wantsKind = (kind) => rule.kind === void 0 || rule.kind === kind;
1157
1396
  graph.forEachNode((svcId, attrs) => {
1158
1397
  const a = attrs;
1159
- if (a.type !== import_types2.NodeType.ServiceNode) return;
1398
+ if (a.type !== import_types3.NodeType.ServiceNode) return;
1160
1399
  const svc = a;
1161
1400
  const deps = svc.dependencies ?? {};
1162
1401
  if (wantsKind("driver-engine")) {
1163
1402
  for (const edgeId of graph.outboundEdges(svcId)) {
1164
1403
  const e = graph.getEdgeAttributes(edgeId);
1165
- if (e.type !== import_types2.EdgeType.CONNECTS_TO) continue;
1166
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1404
+ if (e.type !== import_types3.EdgeType.CONNECTS_TO) continue;
1405
+ if (e.provenance === import_types3.Provenance.FRONTIER) continue;
1167
1406
  const dbAttrs = graph.getNodeAttributes(e.target);
1168
- if (dbAttrs.type !== import_types2.NodeType.DatabaseNode) continue;
1407
+ if (dbAttrs.type !== import_types3.NodeType.DatabaseNode) continue;
1169
1408
  const db = dbAttrs;
1170
1409
  for (const pair of compatPairs()) {
1171
1410
  if (pair.engine !== db.engine) continue;
@@ -1283,7 +1522,7 @@ async function loadPolicyFile(policyPath) {
1283
1522
  throw err;
1284
1523
  }
1285
1524
  const json = JSON.parse(raw);
1286
- const file = import_types2.PolicyFileSchema.parse(json);
1525
+ const file = import_types3.PolicyFileSchema.parse(json);
1287
1526
  return file.policies;
1288
1527
  }
1289
1528
  var PolicyViolationsLog = class {
@@ -1333,7 +1572,7 @@ init_cjs_shims();
1333
1572
  init_cjs_shims();
1334
1573
  var import_node_fs3 = require("fs");
1335
1574
  var import_node_path3 = __toESM(require("path"), 1);
1336
- var import_types3 = require("@neat.is/types");
1575
+ var import_types4 = require("@neat.is/types");
1337
1576
  var HOUR_MS = 60 * 60 * 1e3;
1338
1577
  var DAY_MS = 24 * HOUR_MS;
1339
1578
  var DEFAULT_STALE_THRESHOLDS = {
@@ -1389,10 +1628,10 @@ function pickAddress(span) {
1389
1628
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1390
1629
  }
1391
1630
  function makeObservedEdgeId(type, source, target) {
1392
- return (0, import_types3.observedEdgeId)(source, target, type);
1631
+ return (0, import_types4.observedEdgeId)(source, target, type);
1393
1632
  }
1394
1633
  function makeInferredEdgeId(type, source, target) {
1395
- return (0, import_types3.inferredEdgeId)(source, target, type);
1634
+ return (0, import_types4.inferredEdgeId)(source, target, type);
1396
1635
  }
1397
1636
  var INFERRED_CONFIDENCE = 0.6;
1398
1637
  var STITCH_MAX_DEPTH = 2;
@@ -1423,13 +1662,13 @@ function lookupParentSpanService(traceId, parentSpanId, now) {
1423
1662
  return entry.service;
1424
1663
  }
1425
1664
  function resolveServiceId(graph, host) {
1426
- const direct = (0, import_types3.serviceId)(host);
1665
+ const direct = (0, import_types4.serviceId)(host);
1427
1666
  if (graph.hasNode(direct)) return direct;
1428
1667
  let found = null;
1429
1668
  graph.forEachNode((id, attrs) => {
1430
1669
  if (found) return;
1431
1670
  const a = attrs;
1432
- if (a.type !== import_types3.NodeType.ServiceNode) return;
1671
+ if (a.type !== import_types4.NodeType.ServiceNode) return;
1433
1672
  if (a.name === host) {
1434
1673
  found = id;
1435
1674
  return;
@@ -1441,14 +1680,14 @@ function resolveServiceId(graph, host) {
1441
1680
  return found;
1442
1681
  }
1443
1682
  function frontierIdFor(host) {
1444
- return (0, import_types3.frontierId)(host);
1683
+ return (0, import_types4.frontierId)(host);
1445
1684
  }
1446
1685
  function ensureServiceNode(graph, serviceName) {
1447
- const id = (0, import_types3.serviceId)(serviceName);
1686
+ const id = (0, import_types4.serviceId)(serviceName);
1448
1687
  if (graph.hasNode(id)) return id;
1449
1688
  const node = {
1450
1689
  id,
1451
- type: import_types3.NodeType.ServiceNode,
1690
+ type: import_types4.NodeType.ServiceNode,
1452
1691
  name: serviceName,
1453
1692
  language: "unknown",
1454
1693
  discoveredVia: "otel"
@@ -1457,11 +1696,11 @@ function ensureServiceNode(graph, serviceName) {
1457
1696
  return id;
1458
1697
  }
1459
1698
  function ensureDatabaseNode(graph, host, engine) {
1460
- const id = (0, import_types3.databaseId)(host);
1699
+ const id = (0, import_types4.databaseId)(host);
1461
1700
  if (graph.hasNode(id)) return id;
1462
1701
  const node = {
1463
1702
  id,
1464
- type: import_types3.NodeType.DatabaseNode,
1703
+ type: import_types4.NodeType.DatabaseNode,
1465
1704
  name: host,
1466
1705
  engine,
1467
1706
  engineVersion: "unknown",
@@ -1481,7 +1720,7 @@ function ensureFrontierNode(graph, host, ts) {
1481
1720
  }
1482
1721
  const node = {
1483
1722
  id,
1484
- type: import_types3.NodeType.FrontierNode,
1723
+ type: import_types4.NodeType.FrontierNode,
1485
1724
  name: host,
1486
1725
  host,
1487
1726
  firstObserved: ts,
@@ -1491,12 +1730,12 @@ function ensureFrontierNode(graph, host, ts) {
1491
1730
  return id;
1492
1731
  }
1493
1732
  function upsertFrontierEdge(graph, type, source, target, ts) {
1494
- const id = (0, import_types3.frontierEdgeId)(source, target, type);
1733
+ const id = (0, import_types4.frontierEdgeId)(source, target, type);
1495
1734
  if (graph.hasEdge(id)) {
1496
1735
  const existing = graph.getEdgeAttributes(id);
1497
1736
  const updated = {
1498
1737
  ...existing,
1499
- provenance: import_types3.Provenance.FRONTIER,
1738
+ provenance: import_types4.Provenance.FRONTIER,
1500
1739
  lastObserved: ts,
1501
1740
  callCount: (existing.callCount ?? 0) + 1
1502
1741
  };
@@ -1508,7 +1747,7 @@ function upsertFrontierEdge(graph, type, source, target, ts) {
1508
1747
  source,
1509
1748
  target,
1510
1749
  type,
1511
- provenance: import_types3.Provenance.FRONTIER,
1750
+ provenance: import_types4.Provenance.FRONTIER,
1512
1751
  confidence: 1,
1513
1752
  lastObserved: ts,
1514
1753
  callCount: 1
@@ -1524,7 +1763,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1524
1763
  const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
1525
1764
  const updated = {
1526
1765
  ...existing,
1527
- provenance: import_types3.Provenance.OBSERVED,
1766
+ provenance: import_types4.Provenance.OBSERVED,
1528
1767
  lastObserved: ts,
1529
1768
  callCount: newSpanCount,
1530
1769
  signal: {
@@ -1542,7 +1781,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1542
1781
  source,
1543
1782
  target,
1544
1783
  type,
1545
- provenance: import_types3.Provenance.OBSERVED,
1784
+ provenance: import_types4.Provenance.OBSERVED,
1546
1785
  confidence: 1,
1547
1786
  lastObserved: ts,
1548
1787
  callCount: 1,
@@ -1565,8 +1804,8 @@ function stitchTrace(graph, sourceServiceId, ts) {
1565
1804
  const outbound = graph.outboundEdges(nodeId);
1566
1805
  for (const edgeId of outbound) {
1567
1806
  const edge = graph.getEdgeAttributes(edgeId);
1568
- if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
1569
- if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
1807
+ if (edge.provenance !== import_types4.Provenance.EXTRACTED) continue;
1808
+ if (graph.hasEdge((0, import_types4.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
1570
1809
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
1571
1810
  if (!visited.has(edge.target)) {
1572
1811
  visited.add(edge.target);
@@ -1588,7 +1827,7 @@ function upsertInferredEdge(graph, type, source, target, ts) {
1588
1827
  source,
1589
1828
  target,
1590
1829
  type,
1591
- provenance: import_types3.Provenance.INFERRED,
1830
+ provenance: import_types4.Provenance.INFERRED,
1592
1831
  confidence: INFERRED_CONFIDENCE,
1593
1832
  lastObserved: ts
1594
1833
  };
@@ -1609,10 +1848,10 @@ async function handleSpan(ctx, span) {
1609
1848
  const host = pickAddress(span);
1610
1849
  if (host) {
1611
1850
  ensureDatabaseNode(ctx.graph, host, span.dbSystem);
1612
- const targetId = (0, import_types3.databaseId)(host);
1851
+ const targetId = (0, import_types4.databaseId)(host);
1613
1852
  const result = upsertObservedEdge(
1614
1853
  ctx.graph,
1615
- import_types3.EdgeType.CONNECTS_TO,
1854
+ import_types4.EdgeType.CONNECTS_TO,
1616
1855
  sourceId,
1617
1856
  targetId,
1618
1857
  ts,
@@ -1628,7 +1867,7 @@ async function handleSpan(ctx, span) {
1628
1867
  if (targetId && targetId !== sourceId) {
1629
1868
  upsertObservedEdge(
1630
1869
  ctx.graph,
1631
- import_types3.EdgeType.CALLS,
1870
+ import_types4.EdgeType.CALLS,
1632
1871
  sourceId,
1633
1872
  targetId,
1634
1873
  ts,
@@ -1639,7 +1878,7 @@ async function handleSpan(ctx, span) {
1639
1878
  } else if (!targetId) {
1640
1879
  const frontierId2 = ensureFrontierNode(ctx.graph, host, ts);
1641
1880
  if (ctx.graph.hasNode(sourceId)) {
1642
- upsertFrontierEdge(ctx.graph, import_types3.EdgeType.CALLS, sourceId, frontierId2, ts);
1881
+ upsertFrontierEdge(ctx.graph, import_types4.EdgeType.CALLS, sourceId, frontierId2, ts);
1643
1882
  }
1644
1883
  affectedNode = frontierId2;
1645
1884
  resolvedViaAddress = true;
@@ -1651,7 +1890,7 @@ async function handleSpan(ctx, span) {
1651
1890
  const parentId = ensureServiceNode(ctx.graph, parentService);
1652
1891
  upsertObservedEdge(
1653
1892
  ctx.graph,
1654
- import_types3.EdgeType.CALLS,
1893
+ import_types4.EdgeType.CALLS,
1655
1894
  parentId,
1656
1895
  sourceId,
1657
1896
  ts,
@@ -1684,7 +1923,7 @@ function promoteFrontierNodes(graph, opts = {}) {
1684
1923
  const aliasIndex = /* @__PURE__ */ new Map();
1685
1924
  graph.forEachNode((id, attrs) => {
1686
1925
  const a = attrs;
1687
- if (a.type !== import_types3.NodeType.ServiceNode) return;
1926
+ if (a.type !== import_types4.NodeType.ServiceNode) return;
1688
1927
  aliasIndex.set(a.name, id);
1689
1928
  if (a.aliases) {
1690
1929
  for (const alias of a.aliases) aliasIndex.set(alias, id);
@@ -1693,7 +1932,7 @@ function promoteFrontierNodes(graph, opts = {}) {
1693
1932
  const toPromote = [];
1694
1933
  graph.forEachNode((id, attrs) => {
1695
1934
  const a = attrs;
1696
- if (a.type !== import_types3.NodeType.FrontierNode) return;
1935
+ if (a.type !== import_types4.NodeType.FrontierNode) return;
1697
1936
  const target = aliasIndex.get(a.host);
1698
1937
  if (!target) return;
1699
1938
  if (target === id) return;
@@ -1727,8 +1966,8 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
1727
1966
  }
1728
1967
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1729
1968
  graph.dropEdge(oldEdgeId);
1730
- const promotedProvenance = edge.provenance === import_types3.Provenance.FRONTIER ? import_types3.Provenance.OBSERVED : edge.provenance;
1731
- const newId = promotedProvenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.EXTRACTED ? (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.frontierEdgeId)(newSource, newTarget, edge.type);
1969
+ const promotedProvenance = edge.provenance === import_types4.Provenance.FRONTIER ? import_types4.Provenance.OBSERVED : edge.provenance;
1970
+ const newId = promotedProvenance === import_types4.Provenance.OBSERVED ? (0, import_types4.observedEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types4.Provenance.INFERRED ? (0, import_types4.inferredEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types4.Provenance.EXTRACTED ? (0, import_types4.extractedEdgeId)(newSource, newTarget, edge.type) : (0, import_types4.frontierEdgeId)(newSource, newTarget, edge.type);
1732
1971
  if (graph.hasEdge(newId)) {
1733
1972
  const existing = graph.getEdgeAttributes(newId);
1734
1973
  const merged = {
@@ -1763,12 +2002,12 @@ async function markStaleEdges(graph, options = {}) {
1763
2002
  const project = options.project ?? DEFAULT_PROJECT;
1764
2003
  graph.forEachEdge((id, attrs) => {
1765
2004
  const e = attrs;
1766
- if (e.provenance !== import_types3.Provenance.OBSERVED) return;
2005
+ if (e.provenance !== import_types4.Provenance.OBSERVED) return;
1767
2006
  if (!e.lastObserved) return;
1768
2007
  const threshold = thresholdForEdgeType(e.type, thresholds);
1769
2008
  const age = now - new Date(e.lastObserved).getTime();
1770
2009
  if (age > threshold) {
1771
- const updated = { ...e, provenance: import_types3.Provenance.STALE, confidence: 0.3 };
2010
+ const updated = { ...e, provenance: import_types4.Provenance.STALE, confidence: 0.3 };
1772
2011
  graph.replaceEdgeAttributes(id, updated);
1773
2012
  events.push({
1774
2013
  edgeId: id,
@@ -1785,8 +2024,8 @@ async function markStaleEdges(graph, options = {}) {
1785
2024
  project,
1786
2025
  payload: {
1787
2026
  edgeId: id,
1788
- from: import_types3.Provenance.OBSERVED,
1789
- to: import_types3.Provenance.STALE
2027
+ from: import_types4.Provenance.OBSERVED,
2028
+ to: import_types4.Provenance.STALE
1790
2029
  }
1791
2030
  });
1792
2031
  }
@@ -1851,14 +2090,14 @@ var import_node_fs7 = require("fs");
1851
2090
  var import_node_path7 = __toESM(require("path"), 1);
1852
2091
  var import_ignore = __toESM(require("ignore"), 1);
1853
2092
  var import_minimatch2 = require("minimatch");
1854
- var import_types5 = require("@neat.is/types");
2093
+ var import_types6 = require("@neat.is/types");
1855
2094
 
1856
2095
  // src/extract/shared.ts
1857
2096
  init_cjs_shims();
1858
2097
  var import_node_fs4 = require("fs");
1859
2098
  var import_node_path4 = __toESM(require("path"), 1);
1860
2099
  var import_yaml = require("yaml");
1861
- var import_types4 = require("@neat.is/types");
2100
+ var import_types5 = require("@neat.is/types");
1862
2101
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py"]);
1863
2102
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
1864
2103
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
@@ -2112,8 +2351,8 @@ async function discoverNodeService(scanPath, dir) {
2112
2351
  }
2113
2352
  if (!pkg.name) return null;
2114
2353
  const node = {
2115
- id: (0, import_types5.serviceId)(pkg.name),
2116
- type: import_types5.NodeType.ServiceNode,
2354
+ id: (0, import_types6.serviceId)(pkg.name),
2355
+ type: import_types6.NodeType.ServiceNode,
2117
2356
  name: pkg.name,
2118
2357
  language: "javascript",
2119
2358
  version: pkg.version,
@@ -2128,8 +2367,8 @@ async function discoverPyService(scanPath, dir) {
2128
2367
  if (!py) return null;
2129
2368
  const pkg = pythonToPackage(py);
2130
2369
  const node = {
2131
- id: (0, import_types5.serviceId)(py.name),
2132
- type: import_types5.NodeType.ServiceNode,
2370
+ id: (0, import_types6.serviceId)(py.name),
2371
+ type: import_types6.NodeType.ServiceNode,
2133
2372
  name: py.name,
2134
2373
  language: "python",
2135
2374
  version: py.version,
@@ -2219,7 +2458,7 @@ init_cjs_shims();
2219
2458
  var import_node_path8 = __toESM(require("path"), 1);
2220
2459
  var import_node_fs8 = require("fs");
2221
2460
  var import_yaml2 = require("yaml");
2222
- var import_types6 = require("@neat.is/types");
2461
+ var import_types7 = require("@neat.is/types");
2223
2462
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
2224
2463
  "Service",
2225
2464
  "Deployment",
@@ -2229,7 +2468,7 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
2229
2468
  function addAliases(graph, serviceId3, candidates) {
2230
2469
  if (!graph.hasNode(serviceId3)) return;
2231
2470
  const node = graph.getNodeAttributes(serviceId3);
2232
- if (node.type !== import_types6.NodeType.ServiceNode) return;
2471
+ if (node.type !== import_types7.NodeType.ServiceNode) return;
2233
2472
  const set = new Set(node.aliases ?? []);
2234
2473
  for (const c of candidates) {
2235
2474
  if (!c) continue;
@@ -2382,7 +2621,7 @@ async function addServiceAliases(graph, scanPath, services) {
2382
2621
  // src/extract/databases/index.ts
2383
2622
  init_cjs_shims();
2384
2623
  var import_node_path16 = __toESM(require("path"), 1);
2385
- var import_types7 = require("@neat.is/types");
2624
+ var import_types8 = require("@neat.is/types");
2386
2625
 
2387
2626
  // src/extract/databases/db-config-yaml.ts
2388
2627
  init_cjs_shims();
@@ -2830,8 +3069,8 @@ function compatibleDriversFor(engine) {
2830
3069
  }
2831
3070
  function toDatabaseNode(config) {
2832
3071
  return {
2833
- id: (0, import_types7.databaseId)(config.host),
2834
- type: import_types7.NodeType.DatabaseNode,
3072
+ id: (0, import_types8.databaseId)(config.host),
3073
+ type: import_types8.NodeType.DatabaseNode,
2835
3074
  name: config.database || config.host,
2836
3075
  engine: config.engine,
2837
3076
  engineVersion: config.engineVersion,
@@ -2962,11 +3201,11 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2962
3201
  });
2963
3202
  }
2964
3203
  const edge = {
2965
- id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
3204
+ id: (0, import_types5.extractedEdgeId)(service.node.id, dbNode.id, import_types8.EdgeType.CONNECTS_TO),
2966
3205
  source: service.node.id,
2967
3206
  target: dbNode.id,
2968
- type: import_types7.EdgeType.CONNECTS_TO,
2969
- provenance: import_types7.Provenance.EXTRACTED,
3207
+ type: import_types8.EdgeType.CONNECTS_TO,
3208
+ provenance: import_types8.Provenance.EXTRACTED,
2970
3209
  ...config.sourceFile ? {
2971
3210
  evidence: {
2972
3211
  file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
@@ -2999,7 +3238,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2999
3238
  init_cjs_shims();
3000
3239
  var import_node_fs11 = require("fs");
3001
3240
  var import_node_path17 = __toESM(require("path"), 1);
3002
- var import_types8 = require("@neat.is/types");
3241
+ var import_types9 = require("@neat.is/types");
3003
3242
  async function walkConfigFiles(dir) {
3004
3243
  const out = [];
3005
3244
  async function walk(current) {
@@ -3024,8 +3263,8 @@ async function addConfigNodes(graph, services, scanPath) {
3024
3263
  for (const file of configFiles) {
3025
3264
  const relPath = import_node_path17.default.relative(scanPath, file);
3026
3265
  const node = {
3027
- id: (0, import_types8.configId)(relPath),
3028
- type: import_types8.NodeType.ConfigNode,
3266
+ id: (0, import_types9.configId)(relPath),
3267
+ type: import_types9.NodeType.ConfigNode,
3029
3268
  name: import_node_path17.default.basename(file),
3030
3269
  path: relPath,
3031
3270
  fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
@@ -3035,11 +3274,11 @@ async function addConfigNodes(graph, services, scanPath) {
3035
3274
  nodesAdded++;
3036
3275
  }
3037
3276
  const edge = {
3038
- id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
3277
+ id: (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types9.EdgeType.CONFIGURED_BY),
3039
3278
  source: service.node.id,
3040
3279
  target: node.id,
3041
- type: import_types8.EdgeType.CONFIGURED_BY,
3042
- provenance: import_types8.Provenance.EXTRACTED,
3280
+ type: import_types9.EdgeType.CONFIGURED_BY,
3281
+ provenance: import_types9.Provenance.EXTRACTED,
3043
3282
  evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3044
3283
  };
3045
3284
  if (!graph.hasEdge(edge.id)) {
@@ -3053,7 +3292,7 @@ async function addConfigNodes(graph, services, scanPath) {
3053
3292
 
3054
3293
  // src/extract/calls/index.ts
3055
3294
  init_cjs_shims();
3056
- var import_types14 = require("@neat.is/types");
3295
+ var import_types15 = require("@neat.is/types");
3057
3296
 
3058
3297
  // src/extract/calls/http.ts
3059
3298
  init_cjs_shims();
@@ -3061,7 +3300,7 @@ var import_node_path19 = __toESM(require("path"), 1);
3061
3300
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3062
3301
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3063
3302
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3064
- var import_types9 = require("@neat.is/types");
3303
+ var import_types10 = require("@neat.is/types");
3065
3304
 
3066
3305
  // src/extract/calls/shared.ts
3067
3306
  init_cjs_shims();
@@ -3176,11 +3415,11 @@ async function addHttpCallEdges(graph, services) {
3176
3415
  const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3177
3416
  const line = lineOf(fileContent, `//${evidenceFile.host}`);
3178
3417
  const edge = {
3179
- id: (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS),
3418
+ id: (0, import_types5.extractedEdgeId)(service.node.id, targetId, import_types10.EdgeType.CALLS),
3180
3419
  source: service.node.id,
3181
3420
  target: targetId,
3182
- type: import_types9.EdgeType.CALLS,
3183
- provenance: import_types9.Provenance.EXTRACTED,
3421
+ type: import_types10.EdgeType.CALLS,
3422
+ provenance: import_types10.Provenance.EXTRACTED,
3184
3423
  evidence: {
3185
3424
  file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3186
3425
  line,
@@ -3199,7 +3438,7 @@ async function addHttpCallEdges(graph, services) {
3199
3438
  // src/extract/calls/kafka.ts
3200
3439
  init_cjs_shims();
3201
3440
  var import_node_path20 = __toESM(require("path"), 1);
3202
- var import_types10 = require("@neat.is/types");
3441
+ var import_types11 = require("@neat.is/types");
3203
3442
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3204
3443
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3205
3444
  function findAll(re, text) {
@@ -3220,7 +3459,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3220
3459
  seen.add(key);
3221
3460
  const line = lineOf(file.content, topic);
3222
3461
  out.push({
3223
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3462
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3224
3463
  name: topic,
3225
3464
  kind: "kafka-topic",
3226
3465
  edgeType,
@@ -3239,7 +3478,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3239
3478
  // src/extract/calls/redis.ts
3240
3479
  init_cjs_shims();
3241
3480
  var import_node_path21 = __toESM(require("path"), 1);
3242
- var import_types11 = require("@neat.is/types");
3481
+ var import_types12 = require("@neat.is/types");
3243
3482
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3244
3483
  function redisEndpointsFromFile(file, serviceDir) {
3245
3484
  const out = [];
@@ -3252,7 +3491,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3252
3491
  seen.add(host);
3253
3492
  const line = lineOf(file.content, host);
3254
3493
  out.push({
3255
- infraId: (0, import_types11.infraId)("redis", host),
3494
+ infraId: (0, import_types12.infraId)("redis", host),
3256
3495
  name: host,
3257
3496
  kind: "redis",
3258
3497
  edgeType: "CALLS",
@@ -3269,7 +3508,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3269
3508
  // src/extract/calls/aws.ts
3270
3509
  init_cjs_shims();
3271
3510
  var import_node_path22 = __toESM(require("path"), 1);
3272
- var import_types12 = require("@neat.is/types");
3511
+ var import_types13 = require("@neat.is/types");
3273
3512
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3274
3513
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3275
3514
  function hasMarker(text, markers) {
@@ -3293,7 +3532,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3293
3532
  seen.add(key);
3294
3533
  const line = lineOf(file.content, name);
3295
3534
  out.push({
3296
- infraId: (0, import_types12.infraId)(kind, name),
3535
+ infraId: (0, import_types13.infraId)(kind, name),
3297
3536
  name,
3298
3537
  kind,
3299
3538
  edgeType: "CALLS",
@@ -3324,7 +3563,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3324
3563
  // src/extract/calls/grpc.ts
3325
3564
  init_cjs_shims();
3326
3565
  var import_node_path23 = __toESM(require("path"), 1);
3327
- var import_types13 = require("@neat.is/types");
3566
+ var import_types14 = require("@neat.is/types");
3328
3567
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3329
3568
  function isLikelyAddress(value) {
3330
3569
  if (!value) return false;
@@ -3343,7 +3582,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3343
3582
  seen.add(name);
3344
3583
  const line = lineOf(file.content, m[0]);
3345
3584
  out.push({
3346
- infraId: (0, import_types13.infraId)("grpc-service", name),
3585
+ infraId: (0, import_types14.infraId)("grpc-service", name),
3347
3586
  name,
3348
3587
  kind: "grpc-service",
3349
3588
  edgeType: "CALLS",
@@ -3361,11 +3600,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3361
3600
  function edgeTypeFromEndpoint(ep) {
3362
3601
  switch (ep.edgeType) {
3363
3602
  case "PUBLISHES_TO":
3364
- return import_types14.EdgeType.PUBLISHES_TO;
3603
+ return import_types15.EdgeType.PUBLISHES_TO;
3365
3604
  case "CONSUMES_FROM":
3366
- return import_types14.EdgeType.CONSUMES_FROM;
3605
+ return import_types15.EdgeType.CONSUMES_FROM;
3367
3606
  default:
3368
- return import_types14.EdgeType.CALLS;
3607
+ return import_types15.EdgeType.CALLS;
3369
3608
  }
3370
3609
  }
3371
3610
  async function addExternalEndpointEdges(graph, services) {
@@ -3386,7 +3625,7 @@ async function addExternalEndpointEdges(graph, services) {
3386
3625
  if (!graph.hasNode(ep.infraId)) {
3387
3626
  const node = {
3388
3627
  id: ep.infraId,
3389
- type: import_types14.NodeType.InfraNode,
3628
+ type: import_types15.NodeType.InfraNode,
3390
3629
  name: ep.name,
3391
3630
  provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3392
3631
  kind: ep.kind
@@ -3395,7 +3634,7 @@ async function addExternalEndpointEdges(graph, services) {
3395
3634
  nodesAdded++;
3396
3635
  }
3397
3636
  const edgeType = edgeTypeFromEndpoint(ep);
3398
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3637
+ const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3399
3638
  if (seenEdges.has(edgeId)) continue;
3400
3639
  seenEdges.add(edgeId);
3401
3640
  if (!graph.hasEdge(edgeId)) {
@@ -3404,7 +3643,7 @@ async function addExternalEndpointEdges(graph, services) {
3404
3643
  source: service.node.id,
3405
3644
  target: ep.infraId,
3406
3645
  type: edgeType,
3407
- provenance: import_types14.Provenance.EXTRACTED,
3646
+ provenance: import_types15.Provenance.EXTRACTED,
3408
3647
  evidence: ep.evidence
3409
3648
  };
3410
3649
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3429,15 +3668,15 @@ init_cjs_shims();
3429
3668
  // src/extract/infra/docker-compose.ts
3430
3669
  init_cjs_shims();
3431
3670
  var import_node_path24 = __toESM(require("path"), 1);
3432
- var import_types16 = require("@neat.is/types");
3671
+ var import_types17 = require("@neat.is/types");
3433
3672
 
3434
3673
  // src/extract/infra/shared.ts
3435
3674
  init_cjs_shims();
3436
- var import_types15 = require("@neat.is/types");
3675
+ var import_types16 = require("@neat.is/types");
3437
3676
  function makeInfraNode(kind, name, provider = "self", extras) {
3438
3677
  return {
3439
- id: (0, import_types15.infraId)(kind, name),
3440
- type: import_types15.NodeType.InfraNode,
3678
+ id: (0, import_types16.infraId)(kind, name),
3679
+ type: import_types16.NodeType.InfraNode,
3441
3680
  name,
3442
3681
  provider,
3443
3682
  kind,
@@ -3514,14 +3753,14 @@ async function addComposeInfra(graph, scanPath, services) {
3514
3753
  for (const dep of dependsOnList(svc.depends_on)) {
3515
3754
  const targetId = composeNameToNodeId.get(dep);
3516
3755
  if (!targetId) continue;
3517
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
3756
+ const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
3518
3757
  if (graph.hasEdge(edgeId)) continue;
3519
3758
  const edge = {
3520
3759
  id: edgeId,
3521
3760
  source: sourceId,
3522
3761
  target: targetId,
3523
- type: import_types16.EdgeType.DEPENDS_ON,
3524
- provenance: import_types16.Provenance.EXTRACTED,
3762
+ type: import_types17.EdgeType.DEPENDS_ON,
3763
+ provenance: import_types17.Provenance.EXTRACTED,
3525
3764
  evidence: { file: evidenceFile }
3526
3765
  };
3527
3766
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3535,7 +3774,7 @@ async function addComposeInfra(graph, scanPath, services) {
3535
3774
  init_cjs_shims();
3536
3775
  var import_node_path25 = __toESM(require("path"), 1);
3537
3776
  var import_node_fs13 = require("fs");
3538
- var import_types17 = require("@neat.is/types");
3777
+ var import_types18 = require("@neat.is/types");
3539
3778
  function runtimeImage(content) {
3540
3779
  const lines = content.split("\n");
3541
3780
  let last = null;
@@ -3572,14 +3811,14 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3572
3811
  graph.addNode(node.id, node);
3573
3812
  nodesAdded++;
3574
3813
  }
3575
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
3814
+ const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
3576
3815
  if (!graph.hasEdge(edgeId)) {
3577
3816
  const edge = {
3578
3817
  id: edgeId,
3579
3818
  source: service.node.id,
3580
3819
  target: node.id,
3581
- type: import_types17.EdgeType.RUNS_ON,
3582
- provenance: import_types17.Provenance.EXTRACTED,
3820
+ type: import_types18.EdgeType.RUNS_ON,
3821
+ provenance: import_types18.Provenance.EXTRACTED,
3583
3822
  evidence: {
3584
3823
  file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3585
3824
  }
@@ -3864,7 +4103,7 @@ init_cjs_shims();
3864
4103
  var import_node_fs17 = require("fs");
3865
4104
  var import_node_os2 = __toESM(require("os"), 1);
3866
4105
  var import_node_path29 = __toESM(require("path"), 1);
3867
- var import_types18 = require("@neat.is/types");
4106
+ var import_types19 = require("@neat.is/types");
3868
4107
  function neatHome() {
3869
4108
  const override = process.env.NEAT_HOME;
3870
4109
  if (override && override.length > 0) return import_node_path29.default.resolve(override);
@@ -3885,7 +4124,11 @@ async function readRegistry() {
3885
4124
  throw err;
3886
4125
  }
3887
4126
  const parsed = JSON.parse(raw);
3888
- return import_types18.RegistryFileSchema.parse(parsed);
4127
+ return import_types19.RegistryFileSchema.parse(parsed);
4128
+ }
4129
+ async function getProject(name) {
4130
+ const reg = await readRegistry();
4131
+ return reg.projects.find((p) => p.name === name);
3889
4132
  }
3890
4133
  async function listProjects() {
3891
4134
  const reg = await readRegistry();
@@ -4003,9 +4246,15 @@ function registerRoutes(scope, ctx) {
4003
4246
  scope.get("/health", async (req, reply) => {
4004
4247
  const proj = resolveProject(registry, req, reply);
4005
4248
  if (!proj) return;
4249
+ const uptimeMs = Date.now() - startedAt;
4006
4250
  return {
4007
- uptime: Math.floor((Date.now() - startedAt) / 1e3),
4251
+ ok: true,
4008
4252
  project: proj.name,
4253
+ uptimeMs,
4254
+ // Legacy fields kept additively. The web shell's StatusBar reads
4255
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4256
+ // the canonical triple and lets the extras pass through.
4257
+ uptime: Math.floor(uptimeMs / 1e3),
4009
4258
  nodeCount: proj.graph.order,
4010
4259
  edgeCount: proj.graph.size,
4011
4260
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
@@ -4025,7 +4274,7 @@ function registerRoutes(scope, ctx) {
4025
4274
  if (!proj.graph.hasNode(id)) {
4026
4275
  return reply.code(404).send({ error: "node not found", id });
4027
4276
  }
4028
- return proj.graph.getNodeAttributes(id);
4277
+ return { node: proj.graph.getNodeAttributes(id) };
4029
4278
  }
4030
4279
  );
4031
4280
  scope.get(
@@ -4042,12 +4291,12 @@ function registerRoutes(scope, ctx) {
4042
4291
  return { inbound, outbound };
4043
4292
  }
4044
4293
  );
4045
- scope.get("/graph/node/:id/dependencies", async (req, reply) => {
4294
+ scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
4046
4295
  const proj = resolveProject(registry, req, reply);
4047
4296
  if (!proj) return;
4048
- const { id } = req.params;
4049
- if (!proj.graph.hasNode(id)) {
4050
- return reply.code(404).send({ error: "node not found", id });
4297
+ const { nodeId } = req.params;
4298
+ if (!proj.graph.hasNode(nodeId)) {
4299
+ return reply.code(404).send({ error: "node not found", id: nodeId });
4051
4300
  }
4052
4301
  const depth = req.query.depth ? Number(req.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
4053
4302
  if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
@@ -4055,25 +4304,67 @@ function registerRoutes(scope, ctx) {
4055
4304
  error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
4056
4305
  });
4057
4306
  }
4058
- return getTransitiveDependencies(proj.graph, id, depth);
4307
+ return getTransitiveDependencies(proj.graph, nodeId, depth);
4308
+ });
4309
+ scope.get("/graph/divergences", async (req, reply) => {
4310
+ const proj = resolveProject(registry, req, reply);
4311
+ if (!proj) return;
4312
+ let typeFilter;
4313
+ if (req.query.type) {
4314
+ const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4315
+ const parsed = [];
4316
+ for (const c of candidates) {
4317
+ const r = import_types20.DivergenceTypeSchema.safeParse(c);
4318
+ if (!r.success) {
4319
+ return reply.code(400).send({
4320
+ error: `unknown divergence type "${c}"`,
4321
+ allowed: import_types20.DivergenceTypeSchema.options
4322
+ });
4323
+ }
4324
+ parsed.push(r.data);
4325
+ }
4326
+ typeFilter = new Set(parsed);
4327
+ }
4328
+ let minConfidence;
4329
+ if (req.query.minConfidence !== void 0) {
4330
+ const n = Number(req.query.minConfidence);
4331
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
4332
+ return reply.code(400).send({
4333
+ error: "minConfidence must be a number in [0, 1]"
4334
+ });
4335
+ }
4336
+ minConfidence = n;
4337
+ }
4338
+ return computeDivergences(proj.graph, {
4339
+ ...typeFilter ? { type: typeFilter } : {},
4340
+ ...minConfidence !== void 0 ? { minConfidence } : {},
4341
+ ...req.query.node ? { node: req.query.node } : {}
4342
+ });
4059
4343
  });
4060
4344
  scope.get("/incidents", async (req, reply) => {
4061
4345
  const proj = resolveProject(registry, req, reply);
4062
4346
  if (!proj) return;
4063
4347
  const epath = errorsPathFor(proj);
4064
- if (!epath) return [];
4065
- return readErrorEvents(epath);
4348
+ if (!epath) return { count: 0, total: 0, events: [] };
4349
+ const events = await readErrorEvents(epath);
4350
+ const total = events.length;
4351
+ const limit = req.query.limit ? Number(req.query.limit) : 50;
4352
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
4353
+ const sliced = events.slice(0, safeLimit);
4354
+ return { count: sliced.length, total, events: sliced };
4066
4355
  });
4067
- scope.get("/incidents/stale", async (req, reply) => {
4356
+ scope.get("/stale-events", async (req, reply) => {
4068
4357
  const proj = resolveProject(registry, req, reply);
4069
4358
  if (!proj) return;
4070
4359
  const spath = staleEventsPathFor(proj);
4071
- if (!spath) return [];
4360
+ if (!spath) return { count: 0, total: 0, events: [] };
4072
4361
  const events = await readStaleEvents(spath);
4073
4362
  const filtered = req.query.edgeType ? events.filter((e) => e.edgeType === req.query.edgeType) : events;
4074
4363
  const ordered = [...filtered].reverse();
4364
+ const total = ordered.length;
4075
4365
  const limit = req.query.limit ? Number(req.query.limit) : 50;
4076
- return ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
4366
+ const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
4367
+ return { count: sliced.length, total, events: sliced };
4077
4368
  });
4078
4369
  scope.get(
4079
4370
  "/incidents/:nodeId",
@@ -4085,14 +4376,15 @@ function registerRoutes(scope, ctx) {
4085
4376
  return reply.code(404).send({ error: "node not found", id: nodeId });
4086
4377
  }
4087
4378
  const epath = errorsPathFor(proj);
4088
- if (!epath) return [];
4379
+ if (!epath) return { count: 0, total: 0, events: [] };
4089
4380
  const events = await readErrorEvents(epath);
4090
- return events.filter(
4381
+ const filtered = events.filter(
4091
4382
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
4092
4383
  );
4384
+ return { count: filtered.length, total: filtered.length, events: filtered };
4093
4385
  }
4094
4386
  );
4095
- scope.get("/traverse/root-cause/:nodeId", async (req, reply) => {
4387
+ scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
4096
4388
  const proj = resolveProject(registry, req, reply);
4097
4389
  if (!proj) return;
4098
4390
  const { nodeId } = req.params;
@@ -4112,7 +4404,7 @@ function registerRoutes(scope, ctx) {
4112
4404
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
4113
4405
  return result;
4114
4406
  });
4115
- scope.get("/traverse/blast-radius/:nodeId", async (req, reply) => {
4407
+ scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
4116
4408
  const proj = resolveProject(registry, req, reply);
4117
4409
  if (!proj) return;
4118
4410
  const { nodeId } = req.params;
@@ -4210,7 +4502,7 @@ function registerRoutes(scope, ctx) {
4210
4502
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4211
4503
  let violations = await log.readAll();
4212
4504
  if (req.query.severity) {
4213
- const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4505
+ const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
4214
4506
  if (!sev.success) {
4215
4507
  return reply.code(400).send({
4216
4508
  error: "invalid severity",
@@ -4222,12 +4514,12 @@ function registerRoutes(scope, ctx) {
4222
4514
  if (req.query.policyId) {
4223
4515
  violations = violations.filter((v) => v.policyId === req.query.policyId);
4224
4516
  }
4225
- return violations;
4517
+ return { violations };
4226
4518
  });
4227
4519
  scope.post("/policies/check", async (req, reply) => {
4228
4520
  const proj = resolveProject(registry, req, reply);
4229
4521
  if (!proj) return;
4230
- const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4522
+ const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4231
4523
  if (!parsed.success) {
4232
4524
  return reply.code(400).send({
4233
4525
  error: "invalid /policies/check body",
@@ -4301,6 +4593,20 @@ async function buildApi(opts) {
4301
4593
  });
4302
4594
  }
4303
4595
  });
4596
+ app.get("/projects/:project", async (req, reply) => {
4597
+ try {
4598
+ const entry = await getProject(req.params.project);
4599
+ if (!entry) {
4600
+ return reply.code(404).send({ error: "project not found", project: req.params.project });
4601
+ }
4602
+ return { project: entry };
4603
+ } catch (err) {
4604
+ return reply.code(500).send({
4605
+ error: "failed to read project registry",
4606
+ details: err.message
4607
+ });
4608
+ }
4609
+ });
4304
4610
  registerRoutes(app, routeCtx);
4305
4611
  await app.register(
4306
4612
  async (scope) => {