@neat.is/core 0.2.8 → 0.2.10

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
@@ -99,8 +99,8 @@ function reshapeGrpcRequest(req) {
99
99
  };
100
100
  }
101
101
  function resolveProtoRoot() {
102
- const here = import_node_path30.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
103
- return import_node_path30.default.resolve(here, "..", "proto");
102
+ const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
103
+ return import_node_path31.default.resolve(here, "..", "proto");
104
104
  }
105
105
  function loadTraceService() {
106
106
  const protoRoot = resolveProtoRoot();
@@ -155,13 +155,13 @@ async function startOtelGrpcReceiver(opts) {
155
155
  })
156
156
  };
157
157
  }
158
- var import_node_url, import_node_path30, grpc, protoLoader;
158
+ var import_node_url, import_node_path31, grpc, protoLoader;
159
159
  var init_otel_grpc = __esm({
160
160
  "src/otel-grpc.ts"() {
161
161
  "use strict";
162
162
  init_cjs_shims();
163
163
  import_node_url = require("url");
164
- import_node_path30 = __toESM(require("path"), 1);
164
+ import_node_path31 = __toESM(require("path"), 1);
165
165
  grpc = __toESM(require("@grpc/grpc-js"), 1);
166
166
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
167
167
  init_otel();
@@ -258,10 +258,10 @@ function parseOtlpRequest(body) {
258
258
  }
259
259
  function loadProtobufDecoder() {
260
260
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
261
- const here = import_node_path31.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
262
- const protoRoot = import_node_path31.default.resolve(here, "..", "proto");
261
+ const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
262
+ const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
263
263
  const root = new import_protobufjs.default.Root();
264
- root.resolvePath = (_origin, target) => import_node_path31.default.resolve(protoRoot, target);
264
+ root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
265
265
  root.loadSync(
266
266
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
267
267
  { keepCase: true }
@@ -353,12 +353,12 @@ async function buildOtelReceiver(opts) {
353
353
  };
354
354
  return decorated;
355
355
  }
356
- var import_node_path31, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
356
+ var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
357
357
  var init_otel = __esm({
358
358
  "src/otel.ts"() {
359
359
  "use strict";
360
360
  init_cjs_shims();
361
- import_node_path31 = __toESM(require("path"), 1);
361
+ import_node_path32 = __toESM(require("path"), 1);
362
362
  import_node_url2 = require("url");
363
363
  import_fastify2 = __toESM(require("fastify"), 1);
364
364
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -368,7 +368,7 @@ var init_otel = __esm({
368
368
 
369
369
  // src/server.ts
370
370
  init_cjs_shims();
371
- var import_node_path33 = __toESM(require("path"), 1);
371
+ var import_node_path34 = __toESM(require("path"), 1);
372
372
 
373
373
  // src/graph.ts
374
374
  init_cjs_shims();
@@ -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");
@@ -812,19 +798,19 @@ function confidenceFromMix(edges, now = Date.now()) {
812
798
  function longestIncomingWalk(graph, start, maxDepth) {
813
799
  let best = { path: [start], edges: [] };
814
800
  const visited = /* @__PURE__ */ new Set([start]);
815
- function step(node, path34, edges) {
816
- if (path34.length > best.path.length) {
817
- best = { path: [...path34], edges: [...edges] };
801
+ function step(node, path35, edges) {
802
+ if (path35.length > best.path.length) {
803
+ best = { path: [...path35], edges: [...edges] };
818
804
  }
819
- if (path34.length - 1 >= maxDepth) return;
805
+ if (path35.length - 1 >= maxDepth) return;
820
806
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
821
807
  for (const [srcId, edge] of incoming) {
822
808
  if (visited.has(srcId)) continue;
823
809
  visited.add(srcId);
824
- path34.push(srcId);
810
+ path35.push(srcId);
825
811
  edges.push(edge);
826
- step(srcId, path34, edges);
827
- path34.pop();
812
+ step(srcId, path35, edges);
813
+ path35.pop();
828
814
  edges.pop();
829
815
  visited.delete(srcId);
830
816
  }
@@ -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
  }
@@ -1847,18 +2086,18 @@ async function readErrorEvents(errorsPath) {
1847
2086
 
1848
2087
  // src/extract/services.ts
1849
2088
  init_cjs_shims();
1850
- var import_node_fs6 = require("fs");
1851
- var import_node_path6 = __toESM(require("path"), 1);
2089
+ var import_node_fs7 = require("fs");
2090
+ var import_node_path7 = __toESM(require("path"), 1);
1852
2091
  var import_ignore = __toESM(require("ignore"), 1);
1853
- var import_minimatch = require("minimatch");
1854
- var import_types5 = require("@neat.is/types");
2092
+ var import_minimatch2 = require("minimatch");
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([
@@ -1963,6 +2202,73 @@ function pythonToPackage(service) {
1963
2202
  };
1964
2203
  }
1965
2204
 
2205
+ // src/extract/owners.ts
2206
+ init_cjs_shims();
2207
+ var import_node_fs6 = require("fs");
2208
+ var import_node_path6 = __toESM(require("path"), 1);
2209
+ var import_minimatch = require("minimatch");
2210
+ async function loadCodeowners(scanPath) {
2211
+ const candidates = [
2212
+ import_node_path6.default.join(scanPath, "CODEOWNERS"),
2213
+ import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
2214
+ ];
2215
+ for (const file of candidates) {
2216
+ if (await exists(file)) {
2217
+ const raw = await import_node_fs6.promises.readFile(file, "utf8");
2218
+ return parseCodeowners(raw);
2219
+ }
2220
+ }
2221
+ return null;
2222
+ }
2223
+ function parseCodeowners(raw) {
2224
+ const rules = [];
2225
+ for (const line of raw.split("\n")) {
2226
+ const trimmed = line.trim();
2227
+ if (!trimmed || trimmed.startsWith("#")) continue;
2228
+ const match = /^(\S+)\s+(.+)$/.exec(trimmed);
2229
+ if (!match) continue;
2230
+ rules.push({ pattern: match[1], owners: match[2].trim() });
2231
+ }
2232
+ return { rules };
2233
+ }
2234
+ function matchOwner(file, repoPath) {
2235
+ const normalized = repoPath.split(import_node_path6.default.sep).join("/");
2236
+ for (const rule of file.rules) {
2237
+ if (matchesPattern(rule.pattern, normalized)) return rule.owners;
2238
+ }
2239
+ return null;
2240
+ }
2241
+ function matchesPattern(rawPattern, repoPath) {
2242
+ let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
2243
+ if (pattern === "*") return !repoPath.includes("/");
2244
+ if (pattern === "**" || pattern === "") return true;
2245
+ if (pattern.endsWith("/")) pattern = pattern + "**";
2246
+ if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
2247
+ if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
2248
+ return false;
2249
+ }
2250
+ async function readPackageJsonAuthor(serviceDir) {
2251
+ const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
2252
+ if (!await exists(pkgPath)) return null;
2253
+ try {
2254
+ const pkg = await readJson(pkgPath);
2255
+ if (!pkg.author) return null;
2256
+ if (typeof pkg.author === "string") return pkg.author;
2257
+ if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
2258
+ return null;
2259
+ } catch {
2260
+ return null;
2261
+ }
2262
+ }
2263
+ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2264
+ if (codeowners && repoPath !== void 0) {
2265
+ const owner = matchOwner(codeowners, repoPath);
2266
+ if (owner) return owner;
2267
+ }
2268
+ const author = await readPackageJsonAuthor(serviceDir);
2269
+ return author ?? void 0;
2270
+ }
2271
+
1966
2272
  // src/extract/services.ts
1967
2273
  var DEFAULT_SCAN_DEPTH = 5;
1968
2274
  function parseScanDepth() {
@@ -1979,21 +2285,21 @@ function workspaceGlobs(pkg) {
1979
2285
  return null;
1980
2286
  }
1981
2287
  async function loadGitignore(scanPath) {
1982
- const gitignorePath = import_node_path6.default.join(scanPath, ".gitignore");
2288
+ const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
1983
2289
  if (!await exists(gitignorePath)) return null;
1984
- const raw = await import_node_fs6.promises.readFile(gitignorePath, "utf8");
2290
+ const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
1985
2291
  return (0, import_ignore.default)().add(raw);
1986
2292
  }
1987
2293
  async function walkDirs(start, scanPath, options, visit) {
1988
2294
  async function recurse(current, depth) {
1989
2295
  if (depth > options.maxDepth) return;
1990
- const entries = await import_node_fs6.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2296
+ const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1991
2297
  for (const entry of entries) {
1992
2298
  if (!entry.isDirectory()) continue;
1993
2299
  if (IGNORED_DIRS.has(entry.name)) continue;
1994
- const child = import_node_path6.default.join(current, entry.name);
2300
+ const child = import_node_path7.default.join(current, entry.name);
1995
2301
  if (options.ig) {
1996
- const rel = import_node_path6.default.relative(scanPath, child).split(import_node_path6.default.sep).join("/");
2302
+ const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
1997
2303
  if (rel && options.ig.ignores(rel + "/")) continue;
1998
2304
  }
1999
2305
  await visit(child);
@@ -2008,8 +2314,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2008
2314
  for (const raw of globs) {
2009
2315
  const pattern = raw.replace(/^\.\//, "");
2010
2316
  if (!pattern.includes("*")) {
2011
- const candidate = import_node_path6.default.join(scanPath, pattern);
2012
- if (await exists(import_node_path6.default.join(candidate, "package.json"))) found.add(candidate);
2317
+ const candidate = import_node_path7.default.join(scanPath, pattern);
2318
+ if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2013
2319
  continue;
2014
2320
  }
2015
2321
  const segments = pattern.split("/");
@@ -2018,13 +2324,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2018
2324
  if (seg.includes("*")) break;
2019
2325
  staticSegments.push(seg);
2020
2326
  }
2021
- const start = import_node_path6.default.join(scanPath, ...staticSegments);
2327
+ const start = import_node_path7.default.join(scanPath, ...staticSegments);
2022
2328
  if (!await exists(start)) continue;
2023
2329
  const hasDoubleStar = pattern.includes("**");
2024
2330
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2025
2331
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2026
- const rel = import_node_path6.default.relative(scanPath, dir).split(import_node_path6.default.sep).join("/");
2027
- if ((0, import_minimatch.minimatch)(rel, pattern) && await exists(import_node_path6.default.join(dir, "package.json"))) {
2332
+ const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2333
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2028
2334
  found.add(dir);
2029
2335
  }
2030
2336
  });
@@ -2032,18 +2338,26 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2032
2338
  return [...found];
2033
2339
  }
2034
2340
  async function discoverNodeService(scanPath, dir) {
2035
- const pkgPath = import_node_path6.default.join(dir, "package.json");
2341
+ const pkgPath = import_node_path7.default.join(dir, "package.json");
2036
2342
  if (!await exists(pkgPath)) return null;
2037
- const pkg = await readJson(pkgPath);
2343
+ let pkg;
2344
+ try {
2345
+ pkg = await readJson(pkgPath);
2346
+ } catch (err) {
2347
+ console.warn(
2348
+ `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2349
+ );
2350
+ return null;
2351
+ }
2038
2352
  if (!pkg.name) return null;
2039
2353
  const node = {
2040
- id: (0, import_types5.serviceId)(pkg.name),
2041
- type: import_types5.NodeType.ServiceNode,
2354
+ id: (0, import_types6.serviceId)(pkg.name),
2355
+ type: import_types6.NodeType.ServiceNode,
2042
2356
  name: pkg.name,
2043
2357
  language: "javascript",
2044
2358
  version: pkg.version,
2045
2359
  dependencies: pkg.dependencies ?? {},
2046
- repoPath: import_node_path6.default.relative(scanPath, dir),
2360
+ repoPath: import_node_path7.default.relative(scanPath, dir),
2047
2361
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2048
2362
  };
2049
2363
  return { pkg, dir, node };
@@ -2053,19 +2367,28 @@ async function discoverPyService(scanPath, dir) {
2053
2367
  if (!py) return null;
2054
2368
  const pkg = pythonToPackage(py);
2055
2369
  const node = {
2056
- id: (0, import_types5.serviceId)(py.name),
2057
- type: import_types5.NodeType.ServiceNode,
2370
+ id: (0, import_types6.serviceId)(py.name),
2371
+ type: import_types6.NodeType.ServiceNode,
2058
2372
  name: py.name,
2059
2373
  language: "python",
2060
2374
  version: py.version,
2061
2375
  dependencies: py.dependencies,
2062
- repoPath: import_node_path6.default.relative(scanPath, dir)
2376
+ repoPath: import_node_path7.default.relative(scanPath, dir)
2063
2377
  };
2064
2378
  return { pkg, dir, node };
2065
2379
  }
2066
2380
  async function discoverServices(scanPath) {
2067
- const rootPkgPath = import_node_path6.default.join(scanPath, "package.json");
2068
- const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
2381
+ const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2382
+ let rootPkg = null;
2383
+ if (await exists(rootPkgPath)) {
2384
+ try {
2385
+ rootPkg = await readJson(rootPkgPath);
2386
+ } catch (err) {
2387
+ console.warn(
2388
+ `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2389
+ );
2390
+ }
2391
+ }
2069
2392
  const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
2070
2393
  const candidateDirs = [];
2071
2394
  if (wsGlobs) {
@@ -2078,9 +2401,9 @@ async function discoverServices(scanPath) {
2078
2401
  scanPath,
2079
2402
  { maxDepth: parseScanDepth(), ig },
2080
2403
  async (dir) => {
2081
- if (await exists(import_node_path6.default.join(dir, "package.json"))) {
2404
+ if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2082
2405
  candidateDirs.push(dir);
2083
- } else if (await exists(import_node_path6.default.join(dir, "pyproject.toml")) || await exists(import_node_path6.default.join(dir, "requirements.txt")) || await exists(import_node_path6.default.join(dir, "setup.py"))) {
2406
+ } else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
2084
2407
  candidateDirs.push(dir);
2085
2408
  }
2086
2409
  }
@@ -2094,8 +2417,8 @@ async function discoverServices(scanPath) {
2094
2417
  if (!service) continue;
2095
2418
  const existingDir = seen.get(service.node.name);
2096
2419
  if (existingDir !== void 0) {
2097
- const a = import_node_path6.default.relative(scanPath, existingDir) || ".";
2098
- const b = import_node_path6.default.relative(scanPath, dir) || ".";
2420
+ const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2421
+ const b = import_node_path7.default.relative(scanPath, dir) || ".";
2099
2422
  console.warn(
2100
2423
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2101
2424
  );
@@ -2104,6 +2427,11 @@ async function discoverServices(scanPath) {
2104
2427
  seen.set(service.node.name, dir);
2105
2428
  out.push(service);
2106
2429
  }
2430
+ const codeowners = await loadCodeowners(scanPath);
2431
+ for (const service of out) {
2432
+ const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
2433
+ if (owner !== void 0) service.node.owner = owner;
2434
+ }
2107
2435
  return out;
2108
2436
  }
2109
2437
  function addServiceNodes(graph, services) {
@@ -2127,10 +2455,10 @@ function addServiceNodes(graph, services) {
2127
2455
 
2128
2456
  // src/extract/aliases.ts
2129
2457
  init_cjs_shims();
2130
- var import_node_path7 = __toESM(require("path"), 1);
2131
- var import_node_fs7 = require("fs");
2458
+ var import_node_path8 = __toESM(require("path"), 1);
2459
+ var import_node_fs8 = require("fs");
2132
2460
  var import_yaml2 = require("yaml");
2133
- var import_types6 = require("@neat.is/types");
2461
+ var import_types7 = require("@neat.is/types");
2134
2462
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
2135
2463
  "Service",
2136
2464
  "Deployment",
@@ -2140,7 +2468,7 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
2140
2468
  function addAliases(graph, serviceId3, candidates) {
2141
2469
  if (!graph.hasNode(serviceId3)) return;
2142
2470
  const node = graph.getNodeAttributes(serviceId3);
2143
- if (node.type !== import_types6.NodeType.ServiceNode) return;
2471
+ if (node.type !== import_types7.NodeType.ServiceNode) return;
2144
2472
  const set = new Set(node.aliases ?? []);
2145
2473
  for (const c of candidates) {
2146
2474
  if (!c) continue;
@@ -2155,21 +2483,29 @@ function indexServicesByName(services) {
2155
2483
  const map = /* @__PURE__ */ new Map();
2156
2484
  for (const s of services) {
2157
2485
  map.set(s.node.name, s.node.id);
2158
- map.set(import_node_path7.default.basename(s.dir), s.node.id);
2486
+ map.set(import_node_path8.default.basename(s.dir), s.node.id);
2159
2487
  }
2160
2488
  return map;
2161
2489
  }
2162
2490
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2163
2491
  let composePath = null;
2164
2492
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2165
- const abs = import_node_path7.default.join(scanPath, name);
2493
+ const abs = import_node_path8.default.join(scanPath, name);
2166
2494
  if (await exists(abs)) {
2167
2495
  composePath = abs;
2168
2496
  break;
2169
2497
  }
2170
2498
  }
2171
2499
  if (!composePath) return;
2172
- const compose = await readYaml(composePath);
2500
+ let compose;
2501
+ try {
2502
+ compose = await readYaml(composePath);
2503
+ } catch (err) {
2504
+ console.warn(
2505
+ `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2506
+ );
2507
+ return;
2508
+ }
2173
2509
  if (!compose?.services) return;
2174
2510
  for (const [composeName, svc] of Object.entries(compose.services)) {
2175
2511
  const serviceId3 = serviceIndex.get(composeName);
@@ -2208,9 +2544,17 @@ function parseDockerfileLabels(content) {
2208
2544
  }
2209
2545
  async function collectDockerfileAliases(graph, services) {
2210
2546
  for (const service of services) {
2211
- const dockerfilePath = import_node_path7.default.join(service.dir, "Dockerfile");
2547
+ const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2212
2548
  if (!await exists(dockerfilePath)) continue;
2213
- const content = await import_node_fs7.promises.readFile(dockerfilePath, "utf8");
2549
+ let content;
2550
+ try {
2551
+ content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2552
+ } catch (err) {
2553
+ console.warn(
2554
+ `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2555
+ );
2556
+ continue;
2557
+ }
2214
2558
  const aliases = parseDockerfileLabels(content);
2215
2559
  if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
2216
2560
  }
@@ -2218,13 +2562,13 @@ async function collectDockerfileAliases(graph, services) {
2218
2562
  async function walkYamlFiles(start, depth = 0, max = 5) {
2219
2563
  if (depth > max) return [];
2220
2564
  const out = [];
2221
- const entries = await import_node_fs7.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2565
+ const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2222
2566
  for (const entry of entries) {
2223
2567
  if (entry.isDirectory()) {
2224
2568
  if (IGNORED_DIRS.has(entry.name)) continue;
2225
- out.push(...await walkYamlFiles(import_node_path7.default.join(start, entry.name), depth + 1, max));
2226
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path7.default.extname(entry.name))) {
2227
- out.push(import_node_path7.default.join(start, entry.name));
2569
+ out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry.name), depth + 1, max));
2570
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry.name))) {
2571
+ out.push(import_node_path8.default.join(start, entry.name));
2228
2572
  }
2229
2573
  }
2230
2574
  return out;
@@ -2251,7 +2595,7 @@ function k8sServiceTarget(doc, byName) {
2251
2595
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2252
2596
  const files = await walkYamlFiles(scanPath);
2253
2597
  for (const file of files) {
2254
- const content = await import_node_fs7.promises.readFile(file, "utf8");
2598
+ const content = await import_node_fs8.promises.readFile(file, "utf8");
2255
2599
  let docs;
2256
2600
  try {
2257
2601
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2276,14 +2620,14 @@ async function addServiceAliases(graph, scanPath, services) {
2276
2620
 
2277
2621
  // src/extract/databases/index.ts
2278
2622
  init_cjs_shims();
2279
- var import_node_path15 = __toESM(require("path"), 1);
2280
- var import_types7 = require("@neat.is/types");
2623
+ var import_node_path16 = __toESM(require("path"), 1);
2624
+ var import_types8 = require("@neat.is/types");
2281
2625
 
2282
2626
  // src/extract/databases/db-config-yaml.ts
2283
2627
  init_cjs_shims();
2284
- var import_node_path8 = __toESM(require("path"), 1);
2628
+ var import_node_path9 = __toESM(require("path"), 1);
2285
2629
  async function parse(serviceDir) {
2286
- const yamlPath = import_node_path8.default.join(serviceDir, "db-config.yaml");
2630
+ const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2287
2631
  if (!await exists(yamlPath)) return [];
2288
2632
  const raw = await readYaml(yamlPath);
2289
2633
  return [
@@ -2301,13 +2645,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2301
2645
 
2302
2646
  // src/extract/databases/dotenv.ts
2303
2647
  init_cjs_shims();
2304
- var import_node_fs9 = require("fs");
2305
- var import_node_path10 = __toESM(require("path"), 1);
2648
+ var import_node_fs10 = require("fs");
2649
+ var import_node_path11 = __toESM(require("path"), 1);
2306
2650
 
2307
2651
  // src/extract/databases/shared.ts
2308
2652
  init_cjs_shims();
2309
- var import_node_fs8 = require("fs");
2310
- var import_node_path9 = __toESM(require("path"), 1);
2653
+ var import_node_fs9 = require("fs");
2654
+ var import_node_path10 = __toESM(require("path"), 1);
2311
2655
  function schemeToEngine(scheme) {
2312
2656
  const s = scheme.toLowerCase().split("+")[0];
2313
2657
  switch (s) {
@@ -2346,14 +2690,14 @@ function parseConnectionString(url) {
2346
2690
  }
2347
2691
  async function readIfExists(filePath) {
2348
2692
  try {
2349
- return await import_node_fs8.promises.readFile(filePath, "utf8");
2693
+ return await import_node_fs9.promises.readFile(filePath, "utf8");
2350
2694
  } catch {
2351
2695
  return null;
2352
2696
  }
2353
2697
  }
2354
2698
  async function findFirst(serviceDir, candidates) {
2355
2699
  for (const rel of candidates) {
2356
- const abs = import_node_path9.default.join(serviceDir, rel);
2700
+ const abs = import_node_path10.default.join(serviceDir, rel);
2357
2701
  const content = await readIfExists(abs);
2358
2702
  if (content !== null) return abs;
2359
2703
  }
@@ -2404,15 +2748,15 @@ function parseDotenvLine(line) {
2404
2748
  return { key, value };
2405
2749
  }
2406
2750
  async function parse2(serviceDir) {
2407
- const entries = await import_node_fs9.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2751
+ const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2408
2752
  const configs = [];
2409
2753
  const seen = /* @__PURE__ */ new Set();
2410
2754
  for (const entry of entries) {
2411
2755
  if (!entry.isFile()) continue;
2412
2756
  const match = isConfigFile(entry.name);
2413
2757
  if (!match.match || match.fileType !== "env") continue;
2414
- const filePath = import_node_path10.default.join(serviceDir, entry.name);
2415
- const content = await import_node_fs9.promises.readFile(filePath, "utf8");
2758
+ const filePath = import_node_path11.default.join(serviceDir, entry.name);
2759
+ const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2416
2760
  for (const line of content.split("\n")) {
2417
2761
  const parsed = parseDotenvLine(line);
2418
2762
  if (!parsed) continue;
@@ -2431,9 +2775,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2431
2775
 
2432
2776
  // src/extract/databases/prisma.ts
2433
2777
  init_cjs_shims();
2434
- var import_node_path11 = __toESM(require("path"), 1);
2778
+ var import_node_path12 = __toESM(require("path"), 1);
2435
2779
  async function parse3(serviceDir) {
2436
- const schemaPath = import_node_path11.default.join(serviceDir, "prisma", "schema.prisma");
2780
+ const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2437
2781
  const content = await readIfExists(schemaPath);
2438
2782
  if (!content) return [];
2439
2783
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2565,10 +2909,10 @@ var knexParser = { name: "knex", parse: parse5 };
2565
2909
 
2566
2910
  // src/extract/databases/ormconfig.ts
2567
2911
  init_cjs_shims();
2568
- var import_node_path12 = __toESM(require("path"), 1);
2912
+ var import_node_path13 = __toESM(require("path"), 1);
2569
2913
  async function parse6(serviceDir) {
2570
2914
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2571
- const abs = import_node_path12.default.join(serviceDir, candidate);
2915
+ const abs = import_node_path13.default.join(serviceDir, candidate);
2572
2916
  if (!await exists(abs)) continue;
2573
2917
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2574
2918
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2628,9 +2972,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2628
2972
 
2629
2973
  // src/extract/databases/sequelize.ts
2630
2974
  init_cjs_shims();
2631
- var import_node_path13 = __toESM(require("path"), 1);
2975
+ var import_node_path14 = __toESM(require("path"), 1);
2632
2976
  async function parse8(serviceDir) {
2633
- const configPath = import_node_path13.default.join(serviceDir, "config", "config.json");
2977
+ const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2634
2978
  if (!await exists(configPath)) return [];
2635
2979
  const raw = await readJson(configPath);
2636
2980
  const out = [];
@@ -2657,7 +3001,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2657
3001
 
2658
3002
  // src/extract/databases/docker-compose.ts
2659
3003
  init_cjs_shims();
2660
- var import_node_path14 = __toESM(require("path"), 1);
3004
+ var import_node_path15 = __toESM(require("path"), 1);
2661
3005
  function portFromService(svc) {
2662
3006
  for (const raw of svc.ports ?? []) {
2663
3007
  const str = String(raw);
@@ -2684,7 +3028,7 @@ function databaseFromEnv(svc) {
2684
3028
  }
2685
3029
  async function parse9(serviceDir) {
2686
3030
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2687
- const abs = import_node_path14.default.join(serviceDir, name);
3031
+ const abs = import_node_path15.default.join(serviceDir, name);
2688
3032
  if (!await exists(abs)) continue;
2689
3033
  const raw = await readYaml(abs);
2690
3034
  if (!raw?.services) return [];
@@ -2725,8 +3069,8 @@ function compatibleDriversFor(engine) {
2725
3069
  }
2726
3070
  function toDatabaseNode(config) {
2727
3071
  return {
2728
- id: (0, import_types7.databaseId)(config.host),
2729
- type: import_types7.NodeType.DatabaseNode,
3072
+ id: (0, import_types8.databaseId)(config.host),
3073
+ type: import_types8.NodeType.DatabaseNode,
2730
3074
  name: config.database || config.host,
2731
3075
  engine: config.engine,
2732
3076
  engineVersion: config.engineVersion,
@@ -2857,14 +3201,14 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2857
3201
  });
2858
3202
  }
2859
3203
  const edge = {
2860
- 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),
2861
3205
  source: service.node.id,
2862
3206
  target: dbNode.id,
2863
- type: import_types7.EdgeType.CONNECTS_TO,
2864
- provenance: import_types7.Provenance.EXTRACTED,
3207
+ type: import_types8.EdgeType.CONNECTS_TO,
3208
+ provenance: import_types8.Provenance.EXTRACTED,
2865
3209
  ...config.sourceFile ? {
2866
3210
  evidence: {
2867
- file: import_node_path15.default.relative(scanPath, config.sourceFile).split(import_node_path15.default.sep).join("/")
3211
+ file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
2868
3212
  }
2869
3213
  } : {}
2870
3214
  };
@@ -2892,15 +3236,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2892
3236
 
2893
3237
  // src/extract/configs.ts
2894
3238
  init_cjs_shims();
2895
- var import_node_fs10 = require("fs");
2896
- var import_node_path16 = __toESM(require("path"), 1);
2897
- var import_types8 = require("@neat.is/types");
3239
+ var import_node_fs11 = require("fs");
3240
+ var import_node_path17 = __toESM(require("path"), 1);
3241
+ var import_types9 = require("@neat.is/types");
2898
3242
  async function walkConfigFiles(dir) {
2899
3243
  const out = [];
2900
3244
  async function walk(current) {
2901
- const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true });
3245
+ const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
2902
3246
  for (const entry of entries) {
2903
- const full = import_node_path16.default.join(current, entry.name);
3247
+ const full = import_node_path17.default.join(current, entry.name);
2904
3248
  if (entry.isDirectory()) {
2905
3249
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2906
3250
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -2917,25 +3261,25 @@ async function addConfigNodes(graph, services, scanPath) {
2917
3261
  for (const service of services) {
2918
3262
  const configFiles = await walkConfigFiles(service.dir);
2919
3263
  for (const file of configFiles) {
2920
- const relPath = import_node_path16.default.relative(scanPath, file);
3264
+ const relPath = import_node_path17.default.relative(scanPath, file);
2921
3265
  const node = {
2922
- id: (0, import_types8.configId)(relPath),
2923
- type: import_types8.NodeType.ConfigNode,
2924
- name: import_node_path16.default.basename(file),
3266
+ id: (0, import_types9.configId)(relPath),
3267
+ type: import_types9.NodeType.ConfigNode,
3268
+ name: import_node_path17.default.basename(file),
2925
3269
  path: relPath,
2926
- fileType: isConfigFile(import_node_path16.default.basename(file)).fileType
3270
+ fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
2927
3271
  };
2928
3272
  if (!graph.hasNode(node.id)) {
2929
3273
  graph.addNode(node.id, node);
2930
3274
  nodesAdded++;
2931
3275
  }
2932
3276
  const edge = {
2933
- 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),
2934
3278
  source: service.node.id,
2935
3279
  target: node.id,
2936
- type: import_types8.EdgeType.CONFIGURED_BY,
2937
- provenance: import_types8.Provenance.EXTRACTED,
2938
- evidence: { file: relPath.split(import_node_path16.default.sep).join("/") }
3280
+ type: import_types9.EdgeType.CONFIGURED_BY,
3281
+ provenance: import_types9.Provenance.EXTRACTED,
3282
+ evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
2939
3283
  };
2940
3284
  if (!graph.hasEdge(edge.id)) {
2941
3285
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2948,29 +3292,29 @@ async function addConfigNodes(graph, services, scanPath) {
2948
3292
 
2949
3293
  // src/extract/calls/index.ts
2950
3294
  init_cjs_shims();
2951
- var import_types14 = require("@neat.is/types");
3295
+ var import_types15 = require("@neat.is/types");
2952
3296
 
2953
3297
  // src/extract/calls/http.ts
2954
3298
  init_cjs_shims();
2955
- var import_node_path18 = __toESM(require("path"), 1);
3299
+ var import_node_path19 = __toESM(require("path"), 1);
2956
3300
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2957
3301
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2958
3302
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2959
- var import_types9 = require("@neat.is/types");
3303
+ var import_types10 = require("@neat.is/types");
2960
3304
 
2961
3305
  // src/extract/calls/shared.ts
2962
3306
  init_cjs_shims();
2963
- var import_node_fs11 = require("fs");
2964
- var import_node_path17 = __toESM(require("path"), 1);
3307
+ var import_node_fs12 = require("fs");
3308
+ var import_node_path18 = __toESM(require("path"), 1);
2965
3309
  async function walkSourceFiles(dir) {
2966
3310
  const out = [];
2967
3311
  async function walk(current) {
2968
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3312
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2969
3313
  for (const entry of entries) {
2970
- const full = import_node_path17.default.join(current, entry.name);
3314
+ const full = import_node_path18.default.join(current, entry.name);
2971
3315
  if (entry.isDirectory()) {
2972
3316
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2973
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path17.default.extname(entry.name))) {
3317
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry.name))) {
2974
3318
  out.push(full);
2975
3319
  }
2976
3320
  }
@@ -2983,7 +3327,7 @@ async function loadSourceFiles(dir) {
2983
3327
  const out = [];
2984
3328
  for (const p of paths) {
2985
3329
  try {
2986
- const content = await import_node_fs11.promises.readFile(p, "utf8");
3330
+ const content = await import_node_fs12.promises.readFile(p, "utf8");
2987
3331
  out.push({ path: p, content });
2988
3332
  } catch {
2989
3333
  }
@@ -3039,9 +3383,9 @@ async function addHttpCallEdges(graph, services) {
3039
3383
  const knownHosts = /* @__PURE__ */ new Set();
3040
3384
  const hostToNodeId = /* @__PURE__ */ new Map();
3041
3385
  for (const service of services) {
3042
- knownHosts.add(import_node_path18.default.basename(service.dir));
3386
+ knownHosts.add(import_node_path19.default.basename(service.dir));
3043
3387
  knownHosts.add(service.pkg.name);
3044
- hostToNodeId.set(import_node_path18.default.basename(service.dir), service.node.id);
3388
+ hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3045
3389
  hostToNodeId.set(service.pkg.name, service.node.id);
3046
3390
  }
3047
3391
  let edgesAdded = 0;
@@ -3049,7 +3393,7 @@ async function addHttpCallEdges(graph, services) {
3049
3393
  const files = await loadSourceFiles(service.dir);
3050
3394
  const seenTargets = /* @__PURE__ */ new Map();
3051
3395
  for (const file of files) {
3052
- const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
3396
+ const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3053
3397
  let targets;
3054
3398
  try {
3055
3399
  targets = callsFromSource(file.content, parser, knownHosts);
@@ -3071,13 +3415,13 @@ async function addHttpCallEdges(graph, services) {
3071
3415
  const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3072
3416
  const line = lineOf(fileContent, `//${evidenceFile.host}`);
3073
3417
  const edge = {
3074
- 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),
3075
3419
  source: service.node.id,
3076
3420
  target: targetId,
3077
- type: import_types9.EdgeType.CALLS,
3078
- provenance: import_types9.Provenance.EXTRACTED,
3421
+ type: import_types10.EdgeType.CALLS,
3422
+ provenance: import_types10.Provenance.EXTRACTED,
3079
3423
  evidence: {
3080
- file: import_node_path18.default.relative(service.dir, evidenceFile.file),
3424
+ file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3081
3425
  line,
3082
3426
  snippet: snippet(fileContent, line)
3083
3427
  }
@@ -3093,8 +3437,8 @@ async function addHttpCallEdges(graph, services) {
3093
3437
 
3094
3438
  // src/extract/calls/kafka.ts
3095
3439
  init_cjs_shims();
3096
- var import_node_path19 = __toESM(require("path"), 1);
3097
- var import_types10 = require("@neat.is/types");
3440
+ var import_node_path20 = __toESM(require("path"), 1);
3441
+ var import_types11 = require("@neat.is/types");
3098
3442
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3099
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;
3100
3444
  function findAll(re, text) {
@@ -3115,12 +3459,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3115
3459
  seen.add(key);
3116
3460
  const line = lineOf(file.content, topic);
3117
3461
  out.push({
3118
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3462
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3119
3463
  name: topic,
3120
3464
  kind: "kafka-topic",
3121
3465
  edgeType,
3122
3466
  evidence: {
3123
- file: import_node_path19.default.relative(serviceDir, file.path),
3467
+ file: import_node_path20.default.relative(serviceDir, file.path),
3124
3468
  line,
3125
3469
  snippet: snippet(file.content, line)
3126
3470
  }
@@ -3133,8 +3477,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3133
3477
 
3134
3478
  // src/extract/calls/redis.ts
3135
3479
  init_cjs_shims();
3136
- var import_node_path20 = __toESM(require("path"), 1);
3137
- var import_types11 = require("@neat.is/types");
3480
+ var import_node_path21 = __toESM(require("path"), 1);
3481
+ var import_types12 = require("@neat.is/types");
3138
3482
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3139
3483
  function redisEndpointsFromFile(file, serviceDir) {
3140
3484
  const out = [];
@@ -3147,12 +3491,12 @@ function redisEndpointsFromFile(file, serviceDir) {
3147
3491
  seen.add(host);
3148
3492
  const line = lineOf(file.content, host);
3149
3493
  out.push({
3150
- infraId: (0, import_types11.infraId)("redis", host),
3494
+ infraId: (0, import_types12.infraId)("redis", host),
3151
3495
  name: host,
3152
3496
  kind: "redis",
3153
3497
  edgeType: "CALLS",
3154
3498
  evidence: {
3155
- file: import_node_path20.default.relative(serviceDir, file.path),
3499
+ file: import_node_path21.default.relative(serviceDir, file.path),
3156
3500
  line,
3157
3501
  snippet: snippet(file.content, line)
3158
3502
  }
@@ -3163,8 +3507,8 @@ function redisEndpointsFromFile(file, serviceDir) {
3163
3507
 
3164
3508
  // src/extract/calls/aws.ts
3165
3509
  init_cjs_shims();
3166
- var import_node_path21 = __toESM(require("path"), 1);
3167
- var import_types12 = require("@neat.is/types");
3510
+ var import_node_path22 = __toESM(require("path"), 1);
3511
+ var import_types13 = require("@neat.is/types");
3168
3512
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3169
3513
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3170
3514
  function hasMarker(text, markers) {
@@ -3188,12 +3532,12 @@ function awsEndpointsFromFile(file, serviceDir) {
3188
3532
  seen.add(key);
3189
3533
  const line = lineOf(file.content, name);
3190
3534
  out.push({
3191
- infraId: (0, import_types12.infraId)(kind, name),
3535
+ infraId: (0, import_types13.infraId)(kind, name),
3192
3536
  name,
3193
3537
  kind,
3194
3538
  edgeType: "CALLS",
3195
3539
  evidence: {
3196
- file: import_node_path21.default.relative(serviceDir, file.path),
3540
+ file: import_node_path22.default.relative(serviceDir, file.path),
3197
3541
  line,
3198
3542
  snippet: snippet(file.content, line)
3199
3543
  }
@@ -3218,8 +3562,8 @@ function awsEndpointsFromFile(file, serviceDir) {
3218
3562
 
3219
3563
  // src/extract/calls/grpc.ts
3220
3564
  init_cjs_shims();
3221
- var import_node_path22 = __toESM(require("path"), 1);
3222
- var import_types13 = require("@neat.is/types");
3565
+ var import_node_path23 = __toESM(require("path"), 1);
3566
+ var import_types14 = require("@neat.is/types");
3223
3567
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3224
3568
  function isLikelyAddress(value) {
3225
3569
  if (!value) return false;
@@ -3238,12 +3582,12 @@ function grpcEndpointsFromFile(file, serviceDir) {
3238
3582
  seen.add(name);
3239
3583
  const line = lineOf(file.content, m[0]);
3240
3584
  out.push({
3241
- infraId: (0, import_types13.infraId)("grpc-service", name),
3585
+ infraId: (0, import_types14.infraId)("grpc-service", name),
3242
3586
  name,
3243
3587
  kind: "grpc-service",
3244
3588
  edgeType: "CALLS",
3245
3589
  evidence: {
3246
- file: import_node_path22.default.relative(serviceDir, file.path),
3590
+ file: import_node_path23.default.relative(serviceDir, file.path),
3247
3591
  line,
3248
3592
  snippet: snippet(file.content, line)
3249
3593
  }
@@ -3256,11 +3600,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3256
3600
  function edgeTypeFromEndpoint(ep) {
3257
3601
  switch (ep.edgeType) {
3258
3602
  case "PUBLISHES_TO":
3259
- return import_types14.EdgeType.PUBLISHES_TO;
3603
+ return import_types15.EdgeType.PUBLISHES_TO;
3260
3604
  case "CONSUMES_FROM":
3261
- return import_types14.EdgeType.CONSUMES_FROM;
3605
+ return import_types15.EdgeType.CONSUMES_FROM;
3262
3606
  default:
3263
- return import_types14.EdgeType.CALLS;
3607
+ return import_types15.EdgeType.CALLS;
3264
3608
  }
3265
3609
  }
3266
3610
  async function addExternalEndpointEdges(graph, services) {
@@ -3281,7 +3625,7 @@ async function addExternalEndpointEdges(graph, services) {
3281
3625
  if (!graph.hasNode(ep.infraId)) {
3282
3626
  const node = {
3283
3627
  id: ep.infraId,
3284
- type: import_types14.NodeType.InfraNode,
3628
+ type: import_types15.NodeType.InfraNode,
3285
3629
  name: ep.name,
3286
3630
  provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3287
3631
  kind: ep.kind
@@ -3290,7 +3634,7 @@ async function addExternalEndpointEdges(graph, services) {
3290
3634
  nodesAdded++;
3291
3635
  }
3292
3636
  const edgeType = edgeTypeFromEndpoint(ep);
3293
- 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);
3294
3638
  if (seenEdges.has(edgeId)) continue;
3295
3639
  seenEdges.add(edgeId);
3296
3640
  if (!graph.hasEdge(edgeId)) {
@@ -3299,7 +3643,7 @@ async function addExternalEndpointEdges(graph, services) {
3299
3643
  source: service.node.id,
3300
3644
  target: ep.infraId,
3301
3645
  type: edgeType,
3302
- provenance: import_types14.Provenance.EXTRACTED,
3646
+ provenance: import_types15.Provenance.EXTRACTED,
3303
3647
  evidence: ep.evidence
3304
3648
  };
3305
3649
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3323,16 +3667,16 @@ init_cjs_shims();
3323
3667
 
3324
3668
  // src/extract/infra/docker-compose.ts
3325
3669
  init_cjs_shims();
3326
- var import_node_path23 = __toESM(require("path"), 1);
3327
- var import_types16 = require("@neat.is/types");
3670
+ var import_node_path24 = __toESM(require("path"), 1);
3671
+ var import_types17 = require("@neat.is/types");
3328
3672
 
3329
3673
  // src/extract/infra/shared.ts
3330
3674
  init_cjs_shims();
3331
- var import_types15 = require("@neat.is/types");
3675
+ var import_types16 = require("@neat.is/types");
3332
3676
  function makeInfraNode(kind, name, provider = "self", extras) {
3333
3677
  return {
3334
- id: (0, import_types15.infraId)(kind, name),
3335
- type: import_types15.NodeType.InfraNode,
3678
+ id: (0, import_types16.infraId)(kind, name),
3679
+ type: import_types16.NodeType.InfraNode,
3336
3680
  name,
3337
3681
  provider,
3338
3682
  kind,
@@ -3361,7 +3705,7 @@ function dependsOnList(value) {
3361
3705
  }
3362
3706
  function serviceNameToServiceNode(name, services) {
3363
3707
  for (const s of services) {
3364
- if (s.node.name === name || import_node_path23.default.basename(s.dir) === name) return s.node.id;
3708
+ if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3365
3709
  }
3366
3710
  return null;
3367
3711
  }
@@ -3370,16 +3714,24 @@ async function addComposeInfra(graph, scanPath, services) {
3370
3714
  let edgesAdded = 0;
3371
3715
  let composePath = null;
3372
3716
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3373
- const abs = import_node_path23.default.join(scanPath, name);
3717
+ const abs = import_node_path24.default.join(scanPath, name);
3374
3718
  if (await exists(abs)) {
3375
3719
  composePath = abs;
3376
3720
  break;
3377
3721
  }
3378
3722
  }
3379
3723
  if (!composePath) return { nodesAdded, edgesAdded };
3380
- const compose = await readYaml(composePath);
3724
+ let compose;
3725
+ try {
3726
+ compose = await readYaml(composePath);
3727
+ } catch (err) {
3728
+ console.warn(
3729
+ `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3730
+ );
3731
+ return { nodesAdded, edgesAdded };
3732
+ }
3381
3733
  if (!compose?.services) return { nodesAdded, edgesAdded };
3382
- const evidenceFile = import_node_path23.default.relative(scanPath, composePath).split(import_node_path23.default.sep).join("/");
3734
+ const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3383
3735
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3384
3736
  for (const [composeName, svc] of Object.entries(compose.services)) {
3385
3737
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3401,14 +3753,14 @@ async function addComposeInfra(graph, scanPath, services) {
3401
3753
  for (const dep of dependsOnList(svc.depends_on)) {
3402
3754
  const targetId = composeNameToNodeId.get(dep);
3403
3755
  if (!targetId) continue;
3404
- 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);
3405
3757
  if (graph.hasEdge(edgeId)) continue;
3406
3758
  const edge = {
3407
3759
  id: edgeId,
3408
3760
  source: sourceId,
3409
3761
  target: targetId,
3410
- type: import_types16.EdgeType.DEPENDS_ON,
3411
- provenance: import_types16.Provenance.EXTRACTED,
3762
+ type: import_types17.EdgeType.DEPENDS_ON,
3763
+ provenance: import_types17.Provenance.EXTRACTED,
3412
3764
  evidence: { file: evidenceFile }
3413
3765
  };
3414
3766
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3420,9 +3772,9 @@ async function addComposeInfra(graph, scanPath, services) {
3420
3772
 
3421
3773
  // src/extract/infra/dockerfile.ts
3422
3774
  init_cjs_shims();
3423
- var import_node_path24 = __toESM(require("path"), 1);
3424
- var import_node_fs12 = require("fs");
3425
- var import_types17 = require("@neat.is/types");
3775
+ var import_node_path25 = __toESM(require("path"), 1);
3776
+ var import_node_fs13 = require("fs");
3777
+ var import_types18 = require("@neat.is/types");
3426
3778
  function runtimeImage(content) {
3427
3779
  const lines = content.split("\n");
3428
3780
  let last = null;
@@ -3441,9 +3793,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3441
3793
  let nodesAdded = 0;
3442
3794
  let edgesAdded = 0;
3443
3795
  for (const service of services) {
3444
- const dockerfilePath = import_node_path24.default.join(service.dir, "Dockerfile");
3796
+ const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3445
3797
  if (!await exists(dockerfilePath)) continue;
3446
- const content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
3798
+ let content;
3799
+ try {
3800
+ content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3801
+ } catch (err) {
3802
+ console.warn(
3803
+ `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3804
+ );
3805
+ continue;
3806
+ }
3447
3807
  const image = runtimeImage(content);
3448
3808
  if (!image) continue;
3449
3809
  const node = makeInfraNode("container-image", image);
@@ -3451,16 +3811,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3451
3811
  graph.addNode(node.id, node);
3452
3812
  nodesAdded++;
3453
3813
  }
3454
- 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);
3455
3815
  if (!graph.hasEdge(edgeId)) {
3456
3816
  const edge = {
3457
3817
  id: edgeId,
3458
3818
  source: service.node.id,
3459
3819
  target: node.id,
3460
- type: import_types17.EdgeType.RUNS_ON,
3461
- provenance: import_types17.Provenance.EXTRACTED,
3820
+ type: import_types18.EdgeType.RUNS_ON,
3821
+ provenance: import_types18.Provenance.EXTRACTED,
3462
3822
  evidence: {
3463
- file: import_node_path24.default.relative(scanPath, dockerfilePath).split(import_node_path24.default.sep).join("/")
3823
+ file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3464
3824
  }
3465
3825
  };
3466
3826
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3472,19 +3832,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3472
3832
 
3473
3833
  // src/extract/infra/terraform.ts
3474
3834
  init_cjs_shims();
3475
- var import_node_fs13 = require("fs");
3476
- var import_node_path25 = __toESM(require("path"), 1);
3835
+ var import_node_fs14 = require("fs");
3836
+ var import_node_path26 = __toESM(require("path"), 1);
3477
3837
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3478
3838
  async function walkTfFiles(start, depth = 0, max = 5) {
3479
3839
  if (depth > max) return [];
3480
3840
  const out = [];
3481
- const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3841
+ const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3482
3842
  for (const entry of entries) {
3483
3843
  if (entry.isDirectory()) {
3484
3844
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3485
- out.push(...await walkTfFiles(import_node_path25.default.join(start, entry.name), depth + 1, max));
3845
+ out.push(...await walkTfFiles(import_node_path26.default.join(start, entry.name), depth + 1, max));
3486
3846
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3487
- out.push(import_node_path25.default.join(start, entry.name));
3847
+ out.push(import_node_path26.default.join(start, entry.name));
3488
3848
  }
3489
3849
  }
3490
3850
  return out;
@@ -3493,7 +3853,7 @@ async function addTerraformResources(graph, scanPath) {
3493
3853
  let nodesAdded = 0;
3494
3854
  const files = await walkTfFiles(scanPath);
3495
3855
  for (const file of files) {
3496
- const content = await import_node_fs13.promises.readFile(file, "utf8");
3856
+ const content = await import_node_fs14.promises.readFile(file, "utf8");
3497
3857
  RESOURCE_RE.lastIndex = 0;
3498
3858
  let m;
3499
3859
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3511,8 +3871,8 @@ async function addTerraformResources(graph, scanPath) {
3511
3871
 
3512
3872
  // src/extract/infra/k8s.ts
3513
3873
  init_cjs_shims();
3514
- var import_node_fs14 = require("fs");
3515
- var import_node_path26 = __toESM(require("path"), 1);
3874
+ var import_node_fs15 = require("fs");
3875
+ var import_node_path27 = __toESM(require("path"), 1);
3516
3876
  var import_yaml3 = require("yaml");
3517
3877
  var K8S_KIND_TO_INFRA_KIND = {
3518
3878
  Service: "k8s-service",
@@ -3526,13 +3886,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3526
3886
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3527
3887
  if (depth > max) return [];
3528
3888
  const out = [];
3529
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3889
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3530
3890
  for (const entry of entries) {
3531
3891
  if (entry.isDirectory()) {
3532
3892
  if (IGNORED_DIRS.has(entry.name)) continue;
3533
- out.push(...await walkYamlFiles2(import_node_path26.default.join(start, entry.name), depth + 1, max));
3534
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path26.default.extname(entry.name))) {
3535
- out.push(import_node_path26.default.join(start, entry.name));
3893
+ out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry.name), depth + 1, max));
3894
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry.name))) {
3895
+ out.push(import_node_path27.default.join(start, entry.name));
3536
3896
  }
3537
3897
  }
3538
3898
  return out;
@@ -3541,7 +3901,7 @@ async function addK8sResources(graph, scanPath) {
3541
3901
  let nodesAdded = 0;
3542
3902
  const files = await walkYamlFiles2(scanPath);
3543
3903
  for (const file of files) {
3544
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3904
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3545
3905
  let docs;
3546
3906
  try {
3547
3907
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3607,7 +3967,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3607
3967
 
3608
3968
  // src/diff.ts
3609
3969
  init_cjs_shims();
3610
- var import_node_fs15 = require("fs");
3970
+ var import_node_fs16 = require("fs");
3611
3971
  async function loadSnapshotForDiff(target) {
3612
3972
  if (/^https?:\/\//i.test(target)) {
3613
3973
  const res = await fetch(target);
@@ -3616,7 +3976,7 @@ async function loadSnapshotForDiff(target) {
3616
3976
  }
3617
3977
  return await res.json();
3618
3978
  }
3619
- const raw = await import_node_fs15.promises.readFile(target, "utf8");
3979
+ const raw = await import_node_fs16.promises.readFile(target, "utf8");
3620
3980
  return JSON.parse(raw);
3621
3981
  }
3622
3982
  function indexEntries(entries) {
@@ -3684,23 +4044,23 @@ function canonicalJson(value) {
3684
4044
 
3685
4045
  // src/projects.ts
3686
4046
  init_cjs_shims();
3687
- var import_node_path27 = __toESM(require("path"), 1);
4047
+ var import_node_path28 = __toESM(require("path"), 1);
3688
4048
  function pathsForProject(project, baseDir) {
3689
4049
  if (project === DEFAULT_PROJECT) {
3690
4050
  return {
3691
- snapshotPath: import_node_path27.default.join(baseDir, "graph.json"),
3692
- errorsPath: import_node_path27.default.join(baseDir, "errors.ndjson"),
3693
- staleEventsPath: import_node_path27.default.join(baseDir, "stale-events.ndjson"),
3694
- embeddingsCachePath: import_node_path27.default.join(baseDir, "embeddings.json"),
3695
- policyViolationsPath: import_node_path27.default.join(baseDir, "policy-violations.ndjson")
4051
+ snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
4052
+ errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
4053
+ staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
4054
+ embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
4055
+ policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
3696
4056
  };
3697
4057
  }
3698
4058
  return {
3699
- snapshotPath: import_node_path27.default.join(baseDir, `${project}.json`),
3700
- errorsPath: import_node_path27.default.join(baseDir, `errors.${project}.ndjson`),
3701
- staleEventsPath: import_node_path27.default.join(baseDir, `stale-events.${project}.ndjson`),
3702
- embeddingsCachePath: import_node_path27.default.join(baseDir, `embeddings.${project}.json`),
3703
- policyViolationsPath: import_node_path27.default.join(baseDir, `policy-violations.${project}.ndjson`)
4059
+ snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
4060
+ errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
4061
+ staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
4062
+ embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
4063
+ policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
3704
4064
  };
3705
4065
  }
3706
4066
  var Projects = class {
@@ -3740,23 +4100,23 @@ function parseExtraProjects(raw) {
3740
4100
 
3741
4101
  // src/registry.ts
3742
4102
  init_cjs_shims();
3743
- var import_node_fs16 = require("fs");
4103
+ var import_node_fs17 = require("fs");
3744
4104
  var import_node_os2 = __toESM(require("os"), 1);
3745
- var import_node_path28 = __toESM(require("path"), 1);
3746
- var import_types18 = require("@neat.is/types");
4105
+ var import_node_path29 = __toESM(require("path"), 1);
4106
+ var import_types19 = require("@neat.is/types");
3747
4107
  function neatHome() {
3748
4108
  const override = process.env.NEAT_HOME;
3749
- if (override && override.length > 0) return import_node_path28.default.resolve(override);
3750
- return import_node_path28.default.join(import_node_os2.default.homedir(), ".neat");
4109
+ if (override && override.length > 0) return import_node_path29.default.resolve(override);
4110
+ return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
3751
4111
  }
3752
4112
  function registryPath() {
3753
- return import_node_path28.default.join(neatHome(), "projects.json");
4113
+ return import_node_path29.default.join(neatHome(), "projects.json");
3754
4114
  }
3755
4115
  async function readRegistry() {
3756
4116
  const file = registryPath();
3757
4117
  let raw;
3758
4118
  try {
3759
- raw = await import_node_fs16.promises.readFile(file, "utf8");
4119
+ raw = await import_node_fs17.promises.readFile(file, "utf8");
3760
4120
  } catch (err) {
3761
4121
  if (err.code === "ENOENT") {
3762
4122
  return { version: 1, projects: [] };
@@ -3764,7 +4124,7 @@ async function readRegistry() {
3764
4124
  throw err;
3765
4125
  }
3766
4126
  const parsed = JSON.parse(raw);
3767
- return import_types18.RegistryFileSchema.parse(parsed);
4127
+ return import_types19.RegistryFileSchema.parse(parsed);
3768
4128
  }
3769
4129
  async function listProjects() {
3770
4130
  const reg = await readRegistry();
@@ -3936,6 +4296,41 @@ function registerRoutes(scope, ctx) {
3936
4296
  }
3937
4297
  return getTransitiveDependencies(proj.graph, id, depth);
3938
4298
  });
4299
+ scope.get("/graph/divergences", async (req, reply) => {
4300
+ const proj = resolveProject(registry, req, reply);
4301
+ if (!proj) return;
4302
+ let typeFilter;
4303
+ if (req.query.type) {
4304
+ const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4305
+ const parsed = [];
4306
+ for (const c of candidates) {
4307
+ const r = import_types20.DivergenceTypeSchema.safeParse(c);
4308
+ if (!r.success) {
4309
+ return reply.code(400).send({
4310
+ error: `unknown divergence type "${c}"`,
4311
+ allowed: import_types20.DivergenceTypeSchema.options
4312
+ });
4313
+ }
4314
+ parsed.push(r.data);
4315
+ }
4316
+ typeFilter = new Set(parsed);
4317
+ }
4318
+ let minConfidence;
4319
+ if (req.query.minConfidence !== void 0) {
4320
+ const n = Number(req.query.minConfidence);
4321
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
4322
+ return reply.code(400).send({
4323
+ error: "minConfidence must be a number in [0, 1]"
4324
+ });
4325
+ }
4326
+ minConfidence = n;
4327
+ }
4328
+ return computeDivergences(proj.graph, {
4329
+ ...typeFilter ? { type: typeFilter } : {},
4330
+ ...minConfidence !== void 0 ? { minConfidence } : {},
4331
+ ...req.query.node ? { node: req.query.node } : {}
4332
+ });
4333
+ });
3939
4334
  scope.get("/incidents", async (req, reply) => {
3940
4335
  const proj = resolveProject(registry, req, reply);
3941
4336
  if (!proj) return;
@@ -4089,7 +4484,7 @@ function registerRoutes(scope, ctx) {
4089
4484
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4090
4485
  let violations = await log.readAll();
4091
4486
  if (req.query.severity) {
4092
- const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4487
+ const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
4093
4488
  if (!sev.success) {
4094
4489
  return reply.code(400).send({
4095
4490
  error: "invalid severity",
@@ -4106,7 +4501,7 @@ function registerRoutes(scope, ctx) {
4106
4501
  scope.post("/policies/check", async (req, reply) => {
4107
4502
  const proj = resolveProject(registry, req, reply);
4108
4503
  if (!proj) return;
4109
- const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4504
+ const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4110
4505
  if (!parsed.success) {
4111
4506
  return reply.code(400).send({
4112
4507
  error: "invalid /policies/check body",
@@ -4192,8 +4587,8 @@ async function buildApi(opts) {
4192
4587
 
4193
4588
  // src/persist.ts
4194
4589
  init_cjs_shims();
4195
- var import_node_fs17 = require("fs");
4196
- var import_node_path29 = __toESM(require("path"), 1);
4590
+ var import_node_fs18 = require("fs");
4591
+ var import_node_path30 = __toESM(require("path"), 1);
4197
4592
  var SCHEMA_VERSION = 2;
4198
4593
  function migrateV1ToV2(payload) {
4199
4594
  const nodes = payload.graph.nodes;
@@ -4207,7 +4602,7 @@ function migrateV1ToV2(payload) {
4207
4602
  return { ...payload, schemaVersion: 2 };
4208
4603
  }
4209
4604
  async function ensureDir(filePath) {
4210
- await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(filePath), { recursive: true });
4605
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(filePath), { recursive: true });
4211
4606
  }
4212
4607
  async function saveGraphToDisk(graph, outPath) {
4213
4608
  await ensureDir(outPath);
@@ -4217,13 +4612,13 @@ async function saveGraphToDisk(graph, outPath) {
4217
4612
  graph: graph.export()
4218
4613
  };
4219
4614
  const tmp = `${outPath}.tmp`;
4220
- await import_node_fs17.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4221
- await import_node_fs17.promises.rename(tmp, outPath);
4615
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4616
+ await import_node_fs18.promises.rename(tmp, outPath);
4222
4617
  }
4223
4618
  async function loadGraphFromDisk(graph, outPath) {
4224
4619
  let raw;
4225
4620
  try {
4226
- raw = await import_node_fs17.promises.readFile(outPath, "utf8");
4621
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4227
4622
  } catch (err) {
4228
4623
  if (err.code === "ENOENT") return;
4229
4624
  throw err;
@@ -4280,8 +4675,8 @@ init_otel_grpc();
4280
4675
 
4281
4676
  // src/search.ts
4282
4677
  init_cjs_shims();
4283
- var import_node_fs18 = require("fs");
4284
- var import_node_path32 = __toESM(require("path"), 1);
4678
+ var import_node_fs19 = require("fs");
4679
+ var import_node_path33 = __toESM(require("path"), 1);
4285
4680
  var import_node_crypto = require("crypto");
4286
4681
  var DEFAULT_LIMIT = 10;
4287
4682
  var NOMIC_DIM = 768;
@@ -4411,7 +4806,7 @@ async function pickEmbedder() {
4411
4806
  }
4412
4807
  async function readCache(cachePath) {
4413
4808
  try {
4414
- const raw = await import_node_fs18.promises.readFile(cachePath, "utf8");
4809
+ const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
4415
4810
  const parsed = JSON.parse(raw);
4416
4811
  if (parsed.version !== 1) return null;
4417
4812
  return parsed;
@@ -4420,8 +4815,8 @@ async function readCache(cachePath) {
4420
4815
  }
4421
4816
  }
4422
4817
  async function writeCache(cachePath, cache) {
4423
- await import_node_fs18.promises.mkdir(import_node_path32.default.dirname(cachePath), { recursive: true });
4424
- await import_node_fs18.promises.writeFile(cachePath, JSON.stringify(cache));
4818
+ await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
4819
+ await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
4425
4820
  }
4426
4821
  var VectorIndex = class {
4427
4822
  constructor(embedder, cachePath) {
@@ -4600,14 +4995,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
4600
4995
  async function main() {
4601
4996
  const baseDirEnv = process.env.NEAT_OUT_DIR;
4602
4997
  const legacyOutPath = process.env.NEAT_OUT_PATH;
4603
- const baseDir = baseDirEnv ? import_node_path33.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path33.default.resolve(import_node_path33.default.dirname(legacyOutPath)) : import_node_path33.default.resolve("./neat-out");
4604
- const defaultScanPath = import_node_path33.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
4998
+ const baseDir = baseDirEnv ? import_node_path34.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path34.default.resolve(import_node_path34.default.dirname(legacyOutPath)) : import_node_path34.default.resolve("./neat-out");
4999
+ const defaultScanPath = import_node_path34.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
4605
5000
  const registry = new Projects();
4606
5001
  await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
4607
5002
  for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
4608
5003
  const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
4609
5004
  const projectScan = process.env[envKey];
4610
- await bootProject(registry, name, projectScan ? import_node_path33.default.resolve(projectScan) : void 0, baseDir);
5005
+ await bootProject(registry, name, projectScan ? import_node_path34.default.resolve(projectScan) : void 0, baseDir);
4611
5006
  }
4612
5007
  const host = process.env.HOST ?? "0.0.0.0";
4613
5008
  const port = Number(process.env.PORT ?? 8080);