@neat.is/core 0.4.6 → 0.4.8

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.
@@ -1,5 +1,6 @@
1
1
  import {
2
- mountBearerAuth
2
+ mountBearerAuth,
3
+ readAuthEnv
3
4
  } from "./chunk-HVF4S7J3.js";
4
5
 
5
6
  // src/graph.ts
@@ -341,6 +342,7 @@ function deprecatedApis() {
341
342
  // src/traverse.ts
342
343
  import {
343
344
  BlastRadiusResultSchema,
345
+ EdgeType,
344
346
  NodeType,
345
347
  PROV_RANK,
346
348
  RootCauseResultSchema,
@@ -353,6 +355,24 @@ function isFrontierNode(graph, nodeId) {
353
355
  const attrs = graph.getNodeAttributes(nodeId);
354
356
  return attrs.type === NodeType.FrontierNode;
355
357
  }
358
+ function resolveOwningService(graph, nodeId) {
359
+ if (!graph.hasNode(nodeId)) return null;
360
+ const attrs = graph.getNodeAttributes(nodeId);
361
+ if (attrs.type === NodeType.ServiceNode) {
362
+ return { id: nodeId, svc: attrs };
363
+ }
364
+ if (attrs.type === NodeType.FileNode) {
365
+ for (const edgeId of graph.inboundEdges(nodeId)) {
366
+ const e = graph.getEdgeAttributes(edgeId);
367
+ if (e.type !== EdgeType.CONTAINS) continue;
368
+ const owner = graph.getNodeAttributes(e.source);
369
+ if (owner.type === NodeType.ServiceNode) {
370
+ return { id: e.source, svc: owner };
371
+ }
372
+ }
373
+ }
374
+ return null;
375
+ }
356
376
  function bestEdgeBySource(graph, edgeIds) {
357
377
  const best = /* @__PURE__ */ new Map();
358
378
  for (const id of edgeIds) {
@@ -459,9 +479,9 @@ function databaseRootCauseShape(graph, origin, walk) {
459
479
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
460
480
  if (candidatePairs.length === 0) return null;
461
481
  for (const id of walk.path) {
462
- const attrs = graph.getNodeAttributes(id);
463
- if (attrs.type !== NodeType.ServiceNode) continue;
464
- const svc = attrs;
482
+ const owner = resolveOwningService(graph, id);
483
+ if (!owner) continue;
484
+ const { id: serviceId3, svc } = owner;
465
485
  const deps = svc.dependencies ?? {};
466
486
  for (const pair of candidatePairs) {
467
487
  const declared = deps[pair.driver];
@@ -474,7 +494,7 @@ function databaseRootCauseShape(graph, origin, walk) {
474
494
  );
475
495
  if (!result.compatible) {
476
496
  return {
477
- rootCauseNode: id,
497
+ rootCauseNode: serviceId3,
478
498
  rootCauseReason: result.reason ?? "incompatible driver",
479
499
  ...result.minDriverVersion ? {
480
500
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -487,9 +507,9 @@ function databaseRootCauseShape(graph, origin, walk) {
487
507
  }
488
508
  function serviceRootCauseShape(graph, _origin, walk) {
489
509
  for (const id of walk.path) {
490
- const attrs = graph.getNodeAttributes(id);
491
- if (attrs.type !== NodeType.ServiceNode) continue;
492
- const svc = attrs;
510
+ const owner = resolveOwningService(graph, id);
511
+ if (!owner) continue;
512
+ const { id: serviceId3, svc } = owner;
493
513
  const deps = svc.dependencies ?? {};
494
514
  const serviceNodeEngine = svc.nodeEngine;
495
515
  for (const constraint of nodeEngineConstraints()) {
@@ -498,7 +518,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
498
518
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
499
519
  if (!result.compatible && result.reason) {
500
520
  return {
501
- rootCauseNode: id,
521
+ rootCauseNode: serviceId3,
502
522
  rootCauseReason: result.reason,
503
523
  ...result.requiredNodeVersion ? {
504
524
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -513,7 +533,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
513
533
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
514
534
  if (!result.compatible && result.reason) {
515
535
  return {
516
- rootCauseNode: id,
536
+ rootCauseNode: serviceId3,
517
537
  rootCauseReason: result.reason,
518
538
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
519
539
  };
@@ -522,9 +542,15 @@ function serviceRootCauseShape(graph, _origin, walk) {
522
542
  }
523
543
  return null;
524
544
  }
545
+ function fileRootCauseShape(graph, origin, walk) {
546
+ const owner = resolveOwningService(graph, origin.id);
547
+ if (!owner) return null;
548
+ return serviceRootCauseShape(graph, owner.svc, walk);
549
+ }
525
550
  var rootCauseShapes = {
526
551
  [NodeType.DatabaseNode]: databaseRootCauseShape,
527
- [NodeType.ServiceNode]: serviceRootCauseShape
552
+ [NodeType.ServiceNode]: serviceRootCauseShape,
553
+ [NodeType.FileNode]: fileRootCauseShape
528
554
  };
529
555
  function getRootCause(graph, errorNodeId, errorEvent) {
530
556
  if (!graph.hasNode(errorNodeId)) return null;
@@ -636,7 +662,7 @@ import path3 from "path";
636
662
  import { promises as fs2 } from "fs";
637
663
  import path2 from "path";
638
664
  import {
639
- EdgeType,
665
+ EdgeType as EdgeType2,
640
666
  NodeType as NodeType2,
641
667
  PolicyFileSchema
642
668
  } from "@neat.is/types";
@@ -858,7 +884,7 @@ var evaluateCompatibility = ({
858
884
  if (wantsKind("driver-engine")) {
859
885
  for (const edgeId of graph.outboundEdges(svcId)) {
860
886
  const e = graph.getEdgeAttributes(edgeId);
861
- if (e.type !== EdgeType.CONNECTS_TO) continue;
887
+ if (e.type !== EdgeType2.CONNECTS_TO) continue;
862
888
  const dbAttrs = graph.getNodeAttributes(e.target);
863
889
  if (dbAttrs.type === NodeType2.FrontierNode) continue;
864
890
  if (dbAttrs.type !== NodeType2.DatabaseNode) continue;
@@ -1021,12 +1047,13 @@ var PolicyViolationsLog = class {
1021
1047
 
1022
1048
  // src/ingest.ts
1023
1049
  import {
1024
- EdgeType as EdgeType2,
1050
+ EdgeType as EdgeType3,
1025
1051
  NodeType as NodeType3,
1026
1052
  Provenance,
1027
1053
  confidenceForObservedSignal,
1028
1054
  databaseId,
1029
1055
  extractedEdgeId,
1056
+ fileId,
1030
1057
  frontierId,
1031
1058
  inferredEdgeId,
1032
1059
  observedEdgeId,
@@ -1094,6 +1121,86 @@ function hostFromUrl(u) {
1094
1121
  function pickAddress(span) {
1095
1122
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1096
1123
  }
1124
+ var CODE_FILEPATH_ATTR = "code.filepath";
1125
+ var CODE_LINENO_ATTR = "code.lineno";
1126
+ var CODE_FUNCTION_ATTR = "code.function";
1127
+ function toPosix(p) {
1128
+ return p.split("\\").join("/");
1129
+ }
1130
+ function languageForExt(relPath) {
1131
+ const dot = relPath.lastIndexOf(".");
1132
+ if (dot === -1) return void 0;
1133
+ switch (relPath.slice(dot).toLowerCase()) {
1134
+ case ".py":
1135
+ return "python";
1136
+ case ".ts":
1137
+ case ".tsx":
1138
+ return "typescript";
1139
+ case ".js":
1140
+ case ".jsx":
1141
+ case ".mjs":
1142
+ case ".cjs":
1143
+ return "javascript";
1144
+ default:
1145
+ return void 0;
1146
+ }
1147
+ }
1148
+ function relPathForRuntimeFile(filepath, serviceNode) {
1149
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1150
+ const root = serviceNode?.repoPath;
1151
+ if (root && root !== "." && root.length > 0) {
1152
+ const rootPosix = toPosix(root);
1153
+ const anchor = `/${rootPosix}/`;
1154
+ const idx = p.lastIndexOf(anchor);
1155
+ if (idx !== -1) return p.slice(idx + anchor.length);
1156
+ const base = rootPosix.split("/").filter(Boolean).pop();
1157
+ if (base) {
1158
+ const baseAnchor = `/${base}/`;
1159
+ const bidx = p.lastIndexOf(baseAnchor);
1160
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1161
+ }
1162
+ }
1163
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1164
+ return p.length > 0 ? p : null;
1165
+ }
1166
+ function callSiteFromSpan(span, serviceNode) {
1167
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1168
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1169
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1170
+ if (!relPath) return null;
1171
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1172
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1173
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1174
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1175
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1176
+ }
1177
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1178
+ const fileNodeId = fileId(serviceName, callSite.relPath);
1179
+ if (!graph.hasNode(fileNodeId)) {
1180
+ const language = languageForExt(callSite.relPath);
1181
+ const node = {
1182
+ id: fileNodeId,
1183
+ type: NodeType3.FileNode,
1184
+ service: serviceName,
1185
+ path: callSite.relPath,
1186
+ ...language ? { language } : {},
1187
+ discoveredVia: "otel"
1188
+ };
1189
+ graph.addNode(fileNodeId, node);
1190
+ }
1191
+ const containsId = makeObservedEdgeId(EdgeType3.CONTAINS, serviceNodeId, fileNodeId);
1192
+ if (!graph.hasEdge(containsId)) {
1193
+ const edge = {
1194
+ id: containsId,
1195
+ source: serviceNodeId,
1196
+ target: fileNodeId,
1197
+ type: EdgeType3.CONTAINS,
1198
+ provenance: Provenance.OBSERVED
1199
+ };
1200
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1201
+ }
1202
+ return fileNodeId;
1203
+ }
1097
1204
  function makeObservedEdgeId(type, source, target) {
1098
1205
  return observedEdgeId(source, target, type);
1099
1206
  }
@@ -1336,6 +1443,9 @@ async function handleSpan(ctx, span) {
1336
1443
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1337
1444
  const isError = span.statusCode === 2;
1338
1445
  cacheSpanService(span, nowMs);
1446
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1447
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
1448
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1339
1449
  let affectedNode = sourceId;
1340
1450
  if (span.dbSystem) {
1341
1451
  const host = pickAddress(span);
@@ -1344,8 +1454,8 @@ async function handleSpan(ctx, span) {
1344
1454
  const targetId = databaseId(host);
1345
1455
  const result = upsertObservedEdge(
1346
1456
  ctx.graph,
1347
- EdgeType2.CONNECTS_TO,
1348
- sourceId,
1457
+ EdgeType3.CONNECTS_TO,
1458
+ observedSource(),
1349
1459
  targetId,
1350
1460
  ts,
1351
1461
  isError
@@ -1360,8 +1470,8 @@ async function handleSpan(ctx, span) {
1360
1470
  if (targetId && targetId !== sourceId) {
1361
1471
  upsertObservedEdge(
1362
1472
  ctx.graph,
1363
- EdgeType2.CALLS,
1364
- sourceId,
1473
+ EdgeType3.CALLS,
1474
+ observedSource(),
1365
1475
  targetId,
1366
1476
  ts,
1367
1477
  isError
@@ -1372,8 +1482,8 @@ async function handleSpan(ctx, span) {
1372
1482
  const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
1373
1483
  upsertObservedEdge(
1374
1484
  ctx.graph,
1375
- EdgeType2.CALLS,
1376
- sourceId,
1485
+ EdgeType3.CALLS,
1486
+ observedSource(),
1377
1487
  frontierNodeId,
1378
1488
  ts,
1379
1489
  isError
@@ -1388,7 +1498,7 @@ async function handleSpan(ctx, span) {
1388
1498
  const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1389
1499
  upsertObservedEdge(
1390
1500
  ctx.graph,
1391
- EdgeType2.CALLS,
1501
+ EdgeType3.CALLS,
1392
1502
  parentId,
1393
1503
  sourceId,
1394
1504
  ts,
@@ -2334,7 +2444,7 @@ async function addServiceAliases(graph, scanPath, services) {
2334
2444
  // src/extract/databases/index.ts
2335
2445
  import path17 from "path";
2336
2446
  import {
2337
- EdgeType as EdgeType3,
2447
+ EdgeType as EdgeType4,
2338
2448
  NodeType as NodeType6,
2339
2449
  Provenance as Provenance2,
2340
2450
  databaseId as databaseId2,
@@ -2909,10 +3019,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2909
3019
  });
2910
3020
  }
2911
3021
  const edge = {
2912
- id: extractedEdgeId2(service.node.id, dbNode.id, EdgeType3.CONNECTS_TO),
3022
+ id: extractedEdgeId2(service.node.id, dbNode.id, EdgeType4.CONNECTS_TO),
2913
3023
  source: service.node.id,
2914
3024
  target: dbNode.id,
2915
- type: EdgeType3.CONNECTS_TO,
3025
+ type: EdgeType4.CONNECTS_TO,
2916
3026
  provenance: Provenance2.EXTRACTED,
2917
3027
  confidence: confidenceForExtracted("structural"),
2918
3028
  // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
@@ -2948,7 +3058,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2948
3058
  import { promises as fs12 } from "fs";
2949
3059
  import path18 from "path";
2950
3060
  import {
2951
- EdgeType as EdgeType4,
3061
+ EdgeType as EdgeType5,
2952
3062
  NodeType as NodeType7,
2953
3063
  Provenance as Provenance3,
2954
3064
  configId,
@@ -2991,10 +3101,10 @@ async function addConfigNodes(graph, services, scanPath) {
2991
3101
  nodesAdded++;
2992
3102
  }
2993
3103
  const edge = {
2994
- id: extractedEdgeId2(service.node.id, node.id, EdgeType4.CONFIGURED_BY),
3104
+ id: extractedEdgeId2(service.node.id, node.id, EdgeType5.CONFIGURED_BY),
2995
3105
  source: service.node.id,
2996
3106
  target: node.id,
2997
- type: EdgeType4.CONFIGURED_BY,
3107
+ type: EdgeType5.CONFIGURED_BY,
2998
3108
  provenance: Provenance3.EXTRACTED,
2999
3109
  confidence: confidenceForExtracted2("structural"),
3000
3110
  evidence: { file: relPath.split(path18.sep).join("/") }
@@ -3010,10 +3120,10 @@ async function addConfigNodes(graph, services, scanPath) {
3010
3120
 
3011
3121
  // src/extract/calls/index.ts
3012
3122
  import {
3013
- EdgeType as EdgeType6,
3014
- NodeType as NodeType8,
3015
- Provenance as Provenance5,
3016
- confidenceForExtracted as confidenceForExtracted4,
3123
+ EdgeType as EdgeType8,
3124
+ NodeType as NodeType9,
3125
+ Provenance as Provenance6,
3126
+ confidenceForExtracted as confidenceForExtracted5,
3017
3127
  passesExtractedFloor as passesExtractedFloor2
3018
3128
  } from "@neat.is/types";
3019
3129
 
@@ -3023,15 +3133,23 @@ import Parser from "tree-sitter";
3023
3133
  import JavaScript from "tree-sitter-javascript";
3024
3134
  import Python from "tree-sitter-python";
3025
3135
  import {
3026
- EdgeType as EdgeType5,
3027
- Provenance as Provenance4,
3028
- confidenceForExtracted as confidenceForExtracted3,
3136
+ EdgeType as EdgeType7,
3137
+ Provenance as Provenance5,
3138
+ confidenceForExtracted as confidenceForExtracted4,
3029
3139
  passesExtractedFloor
3030
3140
  } from "@neat.is/types";
3031
3141
 
3032
3142
  // src/extract/calls/shared.ts
3033
3143
  import { promises as fs13 } from "fs";
3034
3144
  import path19 from "path";
3145
+ import {
3146
+ EdgeType as EdgeType6,
3147
+ NodeType as NodeType8,
3148
+ Provenance as Provenance4,
3149
+ confidenceForExtracted as confidenceForExtracted3,
3150
+ extractedEdgeId as extractedEdgeId3,
3151
+ fileId as fileId2
3152
+ } from "@neat.is/types";
3035
3153
  async function walkSourceFiles(dir) {
3036
3154
  const out = [];
3037
3155
  async function walk(current) {
@@ -3071,6 +3189,58 @@ function snippet(text, line) {
3071
3189
  const lines = text.split("\n");
3072
3190
  return (lines[line - 1] ?? "").trim();
3073
3191
  }
3192
+ function toPosix2(p) {
3193
+ return p.split("\\").join("/");
3194
+ }
3195
+ function languageForPath(relPath) {
3196
+ switch (path19.extname(relPath).toLowerCase()) {
3197
+ case ".py":
3198
+ return "python";
3199
+ case ".ts":
3200
+ case ".tsx":
3201
+ return "typescript";
3202
+ case ".js":
3203
+ case ".jsx":
3204
+ case ".mjs":
3205
+ case ".cjs":
3206
+ return "javascript";
3207
+ default:
3208
+ return void 0;
3209
+ }
3210
+ }
3211
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3212
+ let nodesAdded = 0;
3213
+ let edgesAdded = 0;
3214
+ const fileNodeId = fileId2(serviceName, relPath);
3215
+ if (!graph.hasNode(fileNodeId)) {
3216
+ const language = languageForPath(relPath);
3217
+ const node = {
3218
+ id: fileNodeId,
3219
+ type: NodeType8.FileNode,
3220
+ service: serviceName,
3221
+ path: relPath,
3222
+ ...language ? { language } : {},
3223
+ discoveredVia: "static"
3224
+ };
3225
+ graph.addNode(fileNodeId, node);
3226
+ nodesAdded++;
3227
+ }
3228
+ const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType6.CONTAINS);
3229
+ if (!graph.hasEdge(containsId)) {
3230
+ const edge = {
3231
+ id: containsId,
3232
+ source: serviceNodeId,
3233
+ target: fileNodeId,
3234
+ type: EdgeType6.CONTAINS,
3235
+ provenance: Provenance4.EXTRACTED,
3236
+ confidence: confidenceForExtracted3("structural"),
3237
+ evidence: { file: relPath }
3238
+ };
3239
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3240
+ edgesAdded++;
3241
+ }
3242
+ return { fileNodeId, nodesAdded, edgesAdded };
3243
+ }
3074
3244
 
3075
3245
  // src/extract/calls/http.ts
3076
3246
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3104,16 +3274,16 @@ function callsFromSource(source, parser, knownHosts) {
3104
3274
  const tree = parser.parse(source);
3105
3275
  const literals = [];
3106
3276
  collectStringLiterals(tree.rootNode, literals);
3107
- const targets = /* @__PURE__ */ new Set();
3277
+ const out = [];
3108
3278
  for (const lit of literals) {
3109
3279
  if (isInsideJsxExternalLink(lit.node)) continue;
3110
3280
  for (const host of knownHosts) {
3111
3281
  if (urlMatchesHost(lit.text, host)) {
3112
- targets.add(host);
3282
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3113
3283
  }
3114
3284
  }
3115
3285
  }
3116
- return targets;
3286
+ return out;
3117
3287
  }
3118
3288
  function makeJsParser() {
3119
3289
  const p = new Parser();
@@ -3136,65 +3306,72 @@ async function addHttpCallEdges(graph, services) {
3136
3306
  hostToNodeId.set(path20.basename(service.dir), service.node.id);
3137
3307
  hostToNodeId.set(service.pkg.name, service.node.id);
3138
3308
  }
3309
+ let nodesAdded = 0;
3139
3310
  let edgesAdded = 0;
3140
3311
  for (const service of services) {
3141
3312
  const files = await loadSourceFiles(service.dir);
3142
- const seenTargets = /* @__PURE__ */ new Map();
3313
+ const seen = /* @__PURE__ */ new Set();
3143
3314
  for (const file of files) {
3144
3315
  if (isTestPath(file.path)) continue;
3145
3316
  const parser = path20.extname(file.path) === ".py" ? pyParser : jsParser;
3146
- let targets;
3317
+ let sites;
3147
3318
  try {
3148
- targets = callsFromSource(file.content, parser, knownHosts);
3319
+ sites = callsFromSource(file.content, parser, knownHosts);
3149
3320
  } catch (err) {
3150
3321
  recordExtractionError("http call extraction", file.path, err);
3151
3322
  continue;
3152
3323
  }
3153
- for (const t of targets) {
3154
- const targetId = hostToNodeId.get(t);
3324
+ if (sites.length === 0) continue;
3325
+ const relFile = toPosix2(path20.relative(service.dir, file.path));
3326
+ for (const site of sites) {
3327
+ const targetId = hostToNodeId.get(site.host);
3155
3328
  if (!targetId || targetId === service.node.id) continue;
3156
- if (!seenTargets.has(targetId)) {
3157
- seenTargets.set(targetId, { file: file.path, host: t });
3329
+ const dedupKey = `${relFile}|${targetId}`;
3330
+ if (seen.has(dedupKey)) continue;
3331
+ seen.add(dedupKey);
3332
+ const confidence = confidenceForExtracted4("hostname-shape-match");
3333
+ const ev = {
3334
+ file: relFile,
3335
+ line: site.line,
3336
+ snippet: snippet(file.content, site.line)
3337
+ };
3338
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3339
+ graph,
3340
+ service.pkg.name,
3341
+ service.node.id,
3342
+ relFile
3343
+ );
3344
+ nodesAdded += n;
3345
+ edgesAdded += e;
3346
+ if (!passesExtractedFloor(confidence)) {
3347
+ noteExtractedDropped({
3348
+ source: fileNodeId,
3349
+ target: targetId,
3350
+ type: EdgeType7.CALLS,
3351
+ confidence,
3352
+ confidenceKind: "hostname-shape-match",
3353
+ evidence: ev
3354
+ });
3355
+ continue;
3356
+ }
3357
+ const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType7.CALLS);
3358
+ if (!graph.hasEdge(edgeId)) {
3359
+ const edge = {
3360
+ id: edgeId,
3361
+ source: fileNodeId,
3362
+ target: targetId,
3363
+ type: EdgeType7.CALLS,
3364
+ provenance: Provenance5.EXTRACTED,
3365
+ confidence,
3366
+ evidence: ev
3367
+ };
3368
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3369
+ edgesAdded++;
3158
3370
  }
3159
- }
3160
- }
3161
- for (const [targetId, evidenceFile] of seenTargets) {
3162
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3163
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3164
- const confidence = confidenceForExtracted3("hostname-shape-match");
3165
- const ev = {
3166
- file: path20.relative(service.dir, evidenceFile.file),
3167
- line,
3168
- snippet: snippet(fileContent, line)
3169
- };
3170
- const edgeId = extractedEdgeId2(service.node.id, targetId, EdgeType5.CALLS);
3171
- if (!passesExtractedFloor(confidence)) {
3172
- noteExtractedDropped({
3173
- source: service.node.id,
3174
- target: targetId,
3175
- type: EdgeType5.CALLS,
3176
- confidence,
3177
- confidenceKind: "hostname-shape-match",
3178
- evidence: ev
3179
- });
3180
- continue;
3181
- }
3182
- const edge = {
3183
- id: edgeId,
3184
- source: service.node.id,
3185
- target: targetId,
3186
- type: EdgeType5.CALLS,
3187
- provenance: Provenance4.EXTRACTED,
3188
- confidence,
3189
- evidence: ev
3190
- };
3191
- if (!graph.hasEdge(edge.id)) {
3192
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3193
- edgesAdded++;
3194
3371
  }
3195
3372
  }
3196
3373
  }
3197
- return edgesAdded;
3374
+ return { nodesAdded, edgesAdded };
3198
3375
  }
3199
3376
 
3200
3377
  // src/extract/calls/kafka.ts
@@ -3403,11 +3580,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3403
3580
  function edgeTypeFromEndpoint(ep) {
3404
3581
  switch (ep.edgeType) {
3405
3582
  case "PUBLISHES_TO":
3406
- return EdgeType6.PUBLISHES_TO;
3583
+ return EdgeType8.PUBLISHES_TO;
3407
3584
  case "CONSUMES_FROM":
3408
- return EdgeType6.CONSUMES_FROM;
3585
+ return EdgeType8.CONSUMES_FROM;
3409
3586
  default:
3410
- return EdgeType6.CALLS;
3587
+ return EdgeType8.CALLS;
3411
3588
  }
3412
3589
  }
3413
3590
  function isAwsKind(kind) {
@@ -3434,7 +3611,7 @@ async function addExternalEndpointEdges(graph, services) {
3434
3611
  if (!graph.hasNode(ep.infraId)) {
3435
3612
  const node = {
3436
3613
  id: ep.infraId,
3437
- type: NodeType8.InfraNode,
3614
+ type: NodeType9.InfraNode,
3438
3615
  name: ep.name,
3439
3616
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3440
3617
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -3446,13 +3623,19 @@ async function addExternalEndpointEdges(graph, services) {
3446
3623
  nodesAdded++;
3447
3624
  }
3448
3625
  const edgeType = edgeTypeFromEndpoint(ep);
3449
- const edgeId = extractedEdgeId2(service.node.id, ep.infraId, edgeType);
3450
- if (seenEdges.has(edgeId)) continue;
3451
- seenEdges.add(edgeId);
3452
- const confidence = confidenceForExtracted4(ep.confidenceKind);
3626
+ const confidence = confidenceForExtracted5(ep.confidenceKind);
3627
+ const relFile = toPosix2(ep.evidence.file);
3628
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3629
+ graph,
3630
+ service.pkg.name,
3631
+ service.node.id,
3632
+ relFile
3633
+ );
3634
+ nodesAdded += n;
3635
+ edgesAdded += e;
3453
3636
  if (!passesExtractedFloor2(confidence)) {
3454
3637
  noteExtractedDropped({
3455
- source: service.node.id,
3638
+ source: fileNodeId,
3456
3639
  target: ep.infraId,
3457
3640
  type: edgeType,
3458
3641
  confidence,
@@ -3461,13 +3644,16 @@ async function addExternalEndpointEdges(graph, services) {
3461
3644
  });
3462
3645
  continue;
3463
3646
  }
3647
+ const edgeId = extractedEdgeId2(fileNodeId, ep.infraId, edgeType);
3648
+ if (seenEdges.has(edgeId)) continue;
3649
+ seenEdges.add(edgeId);
3464
3650
  if (!graph.hasEdge(edgeId)) {
3465
3651
  const edge = {
3466
3652
  id: edgeId,
3467
- source: service.node.id,
3653
+ source: fileNodeId,
3468
3654
  target: ep.infraId,
3469
3655
  type: edgeType,
3470
- provenance: Provenance5.EXTRACTED,
3656
+ provenance: Provenance6.EXTRACTED,
3471
3657
  confidence,
3472
3658
  evidence: ep.evidence
3473
3659
  };
@@ -3479,24 +3665,24 @@ async function addExternalEndpointEdges(graph, services) {
3479
3665
  return { nodesAdded, edgesAdded };
3480
3666
  }
3481
3667
  async function addCallEdges(graph, services) {
3482
- const httpEdges = await addHttpCallEdges(graph, services);
3668
+ const http = await addHttpCallEdges(graph, services);
3483
3669
  const ext = await addExternalEndpointEdges(graph, services);
3484
3670
  return {
3485
- nodesAdded: ext.nodesAdded,
3486
- edgesAdded: httpEdges + ext.edgesAdded
3671
+ nodesAdded: http.nodesAdded + ext.nodesAdded,
3672
+ edgesAdded: http.edgesAdded + ext.edgesAdded
3487
3673
  };
3488
3674
  }
3489
3675
 
3490
3676
  // src/extract/infra/docker-compose.ts
3491
3677
  import path25 from "path";
3492
- import { EdgeType as EdgeType7, Provenance as Provenance6, confidenceForExtracted as confidenceForExtracted5 } from "@neat.is/types";
3678
+ import { EdgeType as EdgeType9, Provenance as Provenance7, confidenceForExtracted as confidenceForExtracted6 } from "@neat.is/types";
3493
3679
 
3494
3680
  // src/extract/infra/shared.ts
3495
- import { NodeType as NodeType9, infraId as infraId5 } from "@neat.is/types";
3681
+ import { NodeType as NodeType10, infraId as infraId5 } from "@neat.is/types";
3496
3682
  function makeInfraNode(kind, name, provider = "self", extras) {
3497
3683
  return {
3498
3684
  id: infraId5(kind, name),
3499
- type: NodeType9.InfraNode,
3685
+ type: NodeType10.InfraNode,
3500
3686
  name,
3501
3687
  provider,
3502
3688
  kind,
@@ -3575,15 +3761,15 @@ async function addComposeInfra(graph, scanPath, services) {
3575
3761
  for (const dep of dependsOnList(svc.depends_on)) {
3576
3762
  const targetId = composeNameToNodeId.get(dep);
3577
3763
  if (!targetId) continue;
3578
- const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType7.DEPENDS_ON);
3764
+ const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType9.DEPENDS_ON);
3579
3765
  if (graph.hasEdge(edgeId)) continue;
3580
3766
  const edge = {
3581
3767
  id: edgeId,
3582
3768
  source: sourceId,
3583
3769
  target: targetId,
3584
- type: EdgeType7.DEPENDS_ON,
3585
- provenance: Provenance6.EXTRACTED,
3586
- confidence: confidenceForExtracted5("structural"),
3770
+ type: EdgeType9.DEPENDS_ON,
3771
+ provenance: Provenance7.EXTRACTED,
3772
+ confidence: confidenceForExtracted6("structural"),
3587
3773
  evidence: { file: evidenceFile }
3588
3774
  };
3589
3775
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3596,7 +3782,7 @@ async function addComposeInfra(graph, scanPath, services) {
3596
3782
  // src/extract/infra/dockerfile.ts
3597
3783
  import path26 from "path";
3598
3784
  import { promises as fs14 } from "fs";
3599
- import { EdgeType as EdgeType8, Provenance as Provenance7, confidenceForExtracted as confidenceForExtracted6 } from "@neat.is/types";
3785
+ import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
3600
3786
  function runtimeImage(content) {
3601
3787
  const lines = content.split("\n");
3602
3788
  let last = null;
@@ -3635,15 +3821,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3635
3821
  graph.addNode(node.id, node);
3636
3822
  nodesAdded++;
3637
3823
  }
3638
- const edgeId = extractedEdgeId2(service.node.id, node.id, EdgeType8.RUNS_ON);
3824
+ const edgeId = extractedEdgeId2(service.node.id, node.id, EdgeType10.RUNS_ON);
3639
3825
  if (!graph.hasEdge(edgeId)) {
3640
3826
  const edge = {
3641
3827
  id: edgeId,
3642
3828
  source: service.node.id,
3643
3829
  target: node.id,
3644
- type: EdgeType8.RUNS_ON,
3645
- provenance: Provenance7.EXTRACTED,
3646
- confidence: confidenceForExtracted6("structural"),
3830
+ type: EdgeType10.RUNS_ON,
3831
+ provenance: Provenance8.EXTRACTED,
3832
+ confidence: confidenceForExtracted7("structural"),
3647
3833
  evidence: {
3648
3834
  file: path26.relative(scanPath, dockerfilePath).split(path26.sep).join("/")
3649
3835
  }
@@ -3768,17 +3954,29 @@ import path30 from "path";
3768
3954
  // src/extract/retire.ts
3769
3955
  import { existsSync } from "fs";
3770
3956
  import path29 from "path";
3771
- import { Provenance as Provenance8 } from "@neat.is/types";
3957
+ import { NodeType as NodeType11, Provenance as Provenance9 } from "@neat.is/types";
3958
+ function dropOrphanedFileNodes(graph) {
3959
+ const orphans = [];
3960
+ graph.forEachNode((id, attrs) => {
3961
+ if (attrs.type !== NodeType11.FileNode) return;
3962
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
3963
+ orphans.push(id);
3964
+ }
3965
+ });
3966
+ for (const id of orphans) graph.dropNode(id);
3967
+ return orphans.length;
3968
+ }
3772
3969
  function retireEdgesByFile(graph, file) {
3773
3970
  const normalized = file.split("\\").join("/");
3774
3971
  const toDrop = [];
3775
3972
  graph.forEachEdge((id, attrs) => {
3776
3973
  const edge = attrs;
3777
- if (edge.provenance !== Provenance8.EXTRACTED) return;
3974
+ if (edge.provenance !== Provenance9.EXTRACTED) return;
3778
3975
  if (!edge.evidence?.file) return;
3779
3976
  if (edge.evidence.file === normalized) toDrop.push(id);
3780
3977
  });
3781
3978
  for (const id of toDrop) graph.dropEdge(id);
3979
+ dropOrphanedFileNodes(graph);
3782
3980
  return toDrop.length;
3783
3981
  }
3784
3982
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
@@ -3786,7 +3984,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
3786
3984
  const bases = [scanPath, ...serviceDirs];
3787
3985
  graph.forEachEdge((id, attrs) => {
3788
3986
  const edge = attrs;
3789
- if (edge.provenance !== Provenance8.EXTRACTED) return;
3987
+ if (edge.provenance !== Provenance9.EXTRACTED) return;
3790
3988
  const evidenceFile = edge.evidence?.file;
3791
3989
  if (!evidenceFile) return;
3792
3990
  if (path29.isAbsolute(evidenceFile)) {
@@ -3797,6 +3995,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
3797
3995
  if (!found) toDrop.push(id);
3798
3996
  });
3799
3997
  for (const id of toDrop) graph.dropEdge(id);
3998
+ dropOrphanedFileNodes(graph);
3800
3999
  return toDrop.length;
3801
4000
  }
3802
4001
 
@@ -3865,10 +4064,10 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3865
4064
  // src/divergences.ts
3866
4065
  import {
3867
4066
  DivergenceResultSchema,
3868
- EdgeType as EdgeType9,
3869
- NodeType as NodeType10,
4067
+ EdgeType as EdgeType11,
4068
+ NodeType as NodeType12,
3870
4069
  parseEdgeId,
3871
- Provenance as Provenance9
4070
+ Provenance as Provenance10
3872
4071
  } from "@neat.is/types";
3873
4072
  function bucketKey(source, target, type) {
3874
4073
  return `${type}|${source}|${target}`;
@@ -3882,17 +4081,17 @@ function bucketEdges(graph) {
3882
4081
  const key = bucketKey(e.source, e.target, e.type);
3883
4082
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3884
4083
  switch (provenance) {
3885
- case Provenance9.EXTRACTED:
4084
+ case Provenance10.EXTRACTED:
3886
4085
  cur.extracted = e;
3887
4086
  break;
3888
- case Provenance9.OBSERVED:
4087
+ case Provenance10.OBSERVED:
3889
4088
  cur.observed = e;
3890
4089
  break;
3891
- case Provenance9.INFERRED:
4090
+ case Provenance10.INFERRED:
3892
4091
  cur.inferred = e;
3893
4092
  break;
3894
4093
  default:
3895
- if (e.provenance === Provenance9.STALE) cur.stale = e;
4094
+ if (e.provenance === Provenance10.STALE) cur.stale = e;
3896
4095
  }
3897
4096
  buckets.set(key, cur);
3898
4097
  });
@@ -3901,7 +4100,7 @@ function bucketEdges(graph) {
3901
4100
  function nodeIsFrontier(graph, nodeId) {
3902
4101
  if (!graph.hasNode(nodeId)) return false;
3903
4102
  const attrs = graph.getNodeAttributes(nodeId);
3904
- return attrs.type === NodeType10.FrontierNode;
4103
+ return attrs.type === NodeType12.FrontierNode;
3905
4104
  }
3906
4105
  function clampConfidence(n) {
3907
4106
  if (!Number.isFinite(n)) return 0;
@@ -3922,6 +4121,7 @@ function gradedConfidence(edge) {
3922
4121
  }
3923
4122
  function detectMissingDivergences(graph, bucket) {
3924
4123
  const out = [];
4124
+ if (bucket.type === EdgeType11.CONTAINS) return out;
3925
4125
  if (bucket.extracted && !bucket.observed) {
3926
4126
  if (!nodeIsFrontier(graph, bucket.target)) {
3927
4127
  out.push({
@@ -3962,7 +4162,7 @@ function declaredHostFor(svc) {
3962
4162
  function hasExtractedConfiguredBy(graph, svcId) {
3963
4163
  for (const edgeId of graph.outboundEdges(svcId)) {
3964
4164
  const e = graph.getEdgeAttributes(edgeId);
3965
- if (e.type === EdgeType9.CONFIGURED_BY && e.provenance === Provenance9.EXTRACTED) {
4165
+ if (e.type === EdgeType11.CONFIGURED_BY && e.provenance === Provenance10.EXTRACTED) {
3966
4166
  return true;
3967
4167
  }
3968
4168
  }
@@ -3975,10 +4175,10 @@ function detectHostMismatch(graph, svcId, svc) {
3975
4175
  const out = [];
3976
4176
  for (const edgeId of graph.outboundEdges(svcId)) {
3977
4177
  const edge = graph.getEdgeAttributes(edgeId);
3978
- if (edge.type !== EdgeType9.CONNECTS_TO) continue;
3979
- if (edge.provenance !== Provenance9.OBSERVED) continue;
4178
+ if (edge.type !== EdgeType11.CONNECTS_TO) continue;
4179
+ if (edge.provenance !== Provenance10.OBSERVED) continue;
3980
4180
  const target = graph.getNodeAttributes(edge.target);
3981
- if (target.type !== NodeType10.DatabaseNode) continue;
4181
+ if (target.type !== NodeType12.DatabaseNode) continue;
3982
4182
  const observedHost = target.host?.trim();
3983
4183
  if (!observedHost) continue;
3984
4184
  if (observedHost === declaredHost) continue;
@@ -4000,10 +4200,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4000
4200
  const deps = svc.dependencies ?? {};
4001
4201
  for (const edgeId of graph.outboundEdges(svcId)) {
4002
4202
  const edge = graph.getEdgeAttributes(edgeId);
4003
- if (edge.type !== EdgeType9.CONNECTS_TO) continue;
4004
- if (edge.provenance !== Provenance9.OBSERVED) continue;
4203
+ if (edge.type !== EdgeType11.CONNECTS_TO) continue;
4204
+ if (edge.provenance !== Provenance10.OBSERVED) continue;
4005
4205
  const target = graph.getNodeAttributes(edge.target);
4006
- if (target.type !== NodeType10.DatabaseNode) continue;
4206
+ if (target.type !== NodeType12.DatabaseNode) continue;
4007
4207
  for (const pair of compatPairs()) {
4008
4208
  if (pair.engine !== target.engine) continue;
4009
4209
  const declared = deps[pair.driver];
@@ -4064,7 +4264,7 @@ function computeDivergences(graph, opts = {}) {
4064
4264
  }
4065
4265
  graph.forEachNode((nodeId, attrs) => {
4066
4266
  const n = attrs;
4067
- if (n.type !== NodeType10.ServiceNode) return;
4267
+ if (n.type !== NodeType12.ServiceNode) return;
4068
4268
  const svc = n;
4069
4269
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4070
4270
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4107,7 +4307,7 @@ function computeDivergences(graph, opts = {}) {
4107
4307
  // src/persist.ts
4108
4308
  import { promises as fs17 } from "fs";
4109
4309
  import path31 from "path";
4110
- import { Provenance as Provenance10, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4310
+ import { Provenance as Provenance11, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4111
4311
  var SCHEMA_VERSION = 4;
4112
4312
  function migrateV1ToV2(payload) {
4113
4313
  const nodes = payload.graph.nodes;
@@ -4129,7 +4329,7 @@ function migrateV2ToV3(payload) {
4129
4329
  for (const edge of edges) {
4130
4330
  const attrs = edge.attributes;
4131
4331
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4132
- attrs.provenance = Provenance10.OBSERVED;
4332
+ attrs.provenance = Provenance11.OBSERVED;
4133
4333
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4134
4334
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4135
4335
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -4981,14 +5181,18 @@ function registerRoutes(scope, ctx) {
4981
5181
  async function buildApi(opts) {
4982
5182
  const app = Fastify({ logger: false });
4983
5183
  await app.register(cors, { origin: true });
5184
+ const env = readAuthEnv();
5185
+ const authToken = opts.authToken ?? env.authToken;
5186
+ const trustProxy = opts.trustProxy ?? env.trustProxy;
5187
+ const publicRead = opts.publicRead ?? env.publicRead;
4984
5188
  mountBearerAuth(app, {
4985
- token: opts.authToken,
4986
- trustProxy: opts.trustProxy,
4987
- publicRead: opts.publicRead
5189
+ token: authToken,
5190
+ trustProxy,
5191
+ publicRead
4988
5192
  });
4989
5193
  app.get("/api/config", async () => ({
4990
- publicRead: opts.publicRead === true,
4991
- authProxy: opts.trustProxy === true
5194
+ publicRead: publicRead === true,
5195
+ authProxy: trustProxy === true
4992
5196
  }));
4993
5197
  const startedAt = opts.startedAt ?? Date.now();
4994
5198
  const registry = buildLegacyRegistry(opts);
@@ -5137,4 +5341,4 @@ export {
5137
5341
  removeProject,
5138
5342
  buildApi
5139
5343
  };
5140
- //# sourceMappingURL=chunk-RBWL4HRB.js.map
5344
+ //# sourceMappingURL=chunk-J5CEKCTR.js.map