@neat.is/core 0.4.11 → 0.4.13

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
@@ -56,9 +56,9 @@ function mountBearerAuth(app, opts) {
56
56
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
57
57
  const publicRead = opts.publicRead === true;
58
58
  app.addHook("preHandler", (req, reply, done) => {
59
- const path38 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
59
+ const path42 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
60
  for (const suffix of suffixes) {
61
- if (path38 === suffix || path38.endsWith(suffix)) {
61
+ if (path42 === suffix || path42.endsWith(suffix)) {
62
62
  done();
63
63
  return;
64
64
  }
@@ -184,8 +184,8 @@ function reshapeGrpcRequest(req) {
184
184
  };
185
185
  }
186
186
  function resolveProtoRoot() {
187
- const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
188
- return import_node_path34.default.resolve(here, "..", "proto");
187
+ const here = import_node_path38.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
188
+ return import_node_path38.default.resolve(here, "..", "proto");
189
189
  }
190
190
  function loadTraceService() {
191
191
  const protoRoot = resolveProtoRoot();
@@ -253,13 +253,13 @@ async function startOtelGrpcReceiver(opts) {
253
253
  })
254
254
  };
255
255
  }
256
- var import_node_url, import_node_path34, import_node_crypto2, grpc, protoLoader;
256
+ var import_node_url, import_node_path38, import_node_crypto2, grpc, protoLoader;
257
257
  var init_otel_grpc = __esm({
258
258
  "src/otel-grpc.ts"() {
259
259
  "use strict";
260
260
  init_cjs_shims();
261
261
  import_node_url = require("url");
262
- import_node_path34 = __toESM(require("path"), 1);
262
+ import_node_path38 = __toESM(require("path"), 1);
263
263
  import_node_crypto2 = require("crypto");
264
264
  grpc = __toESM(require("@grpc/grpc-js"), 1);
265
265
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -369,10 +369,10 @@ function parseOtlpRequest(body) {
369
369
  return out;
370
370
  }
371
371
  function loadProtoRoot() {
372
- const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
373
- const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
372
+ const here = import_node_path39.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
373
+ const protoRoot = import_node_path39.default.resolve(here, "..", "proto");
374
374
  const root = new import_protobufjs.default.Root();
375
- root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
375
+ root.resolvePath = (_origin, target) => import_node_path39.default.resolve(protoRoot, target);
376
376
  root.loadSync(
377
377
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
378
378
  { keepCase: true }
@@ -565,12 +565,12 @@ async function buildOtelReceiver(opts) {
565
565
  };
566
566
  return decorated;
567
567
  }
568
- var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
568
+ var import_node_path39, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
569
569
  var init_otel = __esm({
570
570
  "src/otel.ts"() {
571
571
  "use strict";
572
572
  init_cjs_shims();
573
- import_node_path35 = __toESM(require("path"), 1);
573
+ import_node_path39 = __toESM(require("path"), 1);
574
574
  import_node_url2 = require("url");
575
575
  import_fastify2 = __toESM(require("fastify"), 1);
576
576
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -586,7 +586,7 @@ var init_otel = __esm({
586
586
 
587
587
  // src/server.ts
588
588
  init_cjs_shims();
589
- var import_node_path37 = __toESM(require("path"), 1);
589
+ var import_node_path41 = __toESM(require("path"), 1);
590
590
 
591
591
  // src/graph.ts
592
592
  init_cjs_shims();
@@ -610,7 +610,322 @@ function getGraph(project = DEFAULT_PROJECT) {
610
610
  init_cjs_shims();
611
611
  var import_fastify = __toESM(require("fastify"), 1);
612
612
  var import_cors = __toESM(require("@fastify/cors"), 1);
613
- var import_types23 = require("@neat.is/types");
613
+ var import_types24 = require("@neat.is/types");
614
+
615
+ // src/extend/index.ts
616
+ init_cjs_shims();
617
+ var import_node_fs2 = require("fs");
618
+ var import_node_path2 = __toESM(require("path"), 1);
619
+ var import_node_os = __toESM(require("os"), 1);
620
+ var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
621
+
622
+ // src/installers/package-manager.ts
623
+ init_cjs_shims();
624
+ var import_node_fs = require("fs");
625
+ var import_node_path = __toESM(require("path"), 1);
626
+ var import_node_child_process = require("child_process");
627
+ var LOCKFILE_PRIORITY = [
628
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
629
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
630
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
631
+ {
632
+ lockfile: "package-lock.json",
633
+ pm: "npm",
634
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
635
+ }
636
+ ];
637
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
638
+ async function exists(p) {
639
+ try {
640
+ await import_node_fs.promises.access(p);
641
+ return true;
642
+ } catch {
643
+ return false;
644
+ }
645
+ }
646
+ async function detectPackageManager(serviceDir) {
647
+ let dir = import_node_path.default.resolve(serviceDir);
648
+ const stops = /* @__PURE__ */ new Set();
649
+ for (let i = 0; i < 64; i++) {
650
+ if (stops.has(dir)) break;
651
+ stops.add(dir);
652
+ for (const candidate of LOCKFILE_PRIORITY) {
653
+ const lockPath = import_node_path.default.join(dir, candidate.lockfile);
654
+ if (await exists(lockPath)) {
655
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
656
+ }
657
+ }
658
+ const parent = import_node_path.default.dirname(dir);
659
+ if (parent === dir) break;
660
+ dir = parent;
661
+ }
662
+ return { pm: "npm", cwd: import_node_path.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
663
+ }
664
+ async function runPackageManagerInstall(cmd) {
665
+ return new Promise((resolve) => {
666
+ const child = (0, import_node_child_process.spawn)(cmd.pm, cmd.args, {
667
+ cwd: cmd.cwd,
668
+ // Inherit PATH + HOME so the user's installed managers resolve.
669
+ env: process.env,
670
+ // `false` keeps the parent in control of cleanup if the orchestrator
671
+ // exits before install finishes. Cross-platform-safe.
672
+ shell: false,
673
+ stdio: ["ignore", "ignore", "pipe"]
674
+ });
675
+ let stderr = "";
676
+ child.stderr?.on("data", (chunk) => {
677
+ stderr += chunk.toString("utf8");
678
+ });
679
+ child.on("error", (err) => {
680
+ resolve({
681
+ pm: cmd.pm,
682
+ cwd: cmd.cwd,
683
+ args: cmd.args,
684
+ exitCode: 127,
685
+ stderr: stderr + `
686
+ ${err.message}`
687
+ });
688
+ });
689
+ child.on("close", (code) => {
690
+ resolve({
691
+ pm: cmd.pm,
692
+ cwd: cmd.cwd,
693
+ args: cmd.args,
694
+ exitCode: code ?? 1,
695
+ stderr: stderr.trim()
696
+ });
697
+ });
698
+ });
699
+ }
700
+
701
+ // src/extend/index.ts
702
+ async function fileExists(p) {
703
+ try {
704
+ await import_node_fs2.promises.access(p);
705
+ return true;
706
+ } catch {
707
+ return false;
708
+ }
709
+ }
710
+ async function readPackageJson(scanPath) {
711
+ const pkgPath = import_node_path2.default.join(scanPath, "package.json");
712
+ const raw = await import_node_fs2.promises.readFile(pkgPath, "utf8");
713
+ return JSON.parse(raw);
714
+ }
715
+ async function findHookFiles(scanPath) {
716
+ const entries = await import_node_fs2.promises.readdir(scanPath);
717
+ return entries.filter(
718
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
719
+ ).sort();
720
+ }
721
+ function extendLogPath() {
722
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path2.default.join(import_node_os.default.homedir(), ".neat", "extend-log.ndjson");
723
+ }
724
+ async function appendExtendLog(entry) {
725
+ const logPath = extendLogPath();
726
+ await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(logPath), { recursive: true });
727
+ await import_node_fs2.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
728
+ }
729
+ function splicedContent(fileContent, snippet2) {
730
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
731
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
732
+ __INSTRUMENTATION_BLOCK__`);
733
+ }
734
+ const lines = fileContent.split("\n");
735
+ let lastPushIdx = -1;
736
+ for (let i = 0; i < lines.length; i++) {
737
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
738
+ }
739
+ if (lastPushIdx >= 0) {
740
+ lines.splice(lastPushIdx + 1, 0, snippet2);
741
+ return lines.join("\n");
742
+ }
743
+ for (let i = 0; i < lines.length; i++) {
744
+ if (lines[i].includes("new NodeSDK(")) {
745
+ lines.splice(i, 0, snippet2);
746
+ return lines.join("\n");
747
+ }
748
+ }
749
+ return null;
750
+ }
751
+ async function listUninstrumented(ctx) {
752
+ const pkg = await readPackageJson(ctx.scanPath);
753
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
754
+ const results = [];
755
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
756
+ const entry = (0, import_instrumentation_registry.resolve)(library, installedVersion);
757
+ if (!entry) continue;
758
+ if (entry.coverage === "bundled" || entry.coverage === "http-only") continue;
759
+ results.push({
760
+ library,
761
+ coverage: entry.coverage,
762
+ installedVersion,
763
+ instrumentation_package: entry.instrumentation_package,
764
+ package_version: entry.package_version,
765
+ registration: entry.registration,
766
+ notes: entry.notes
767
+ });
768
+ }
769
+ return results;
770
+ }
771
+ function lookupInstrumentation(library, installedVersion) {
772
+ const entry = (0, import_instrumentation_registry.resolve)(library, installedVersion);
773
+ if (!entry) return null;
774
+ return {
775
+ library: entry.library,
776
+ coverage: entry.coverage,
777
+ instrumentation_package: entry.instrumentation_package,
778
+ package_version: entry.package_version,
779
+ registration: entry.registration,
780
+ notes: entry.notes
781
+ };
782
+ }
783
+ async function describeProjectInstrumentation(ctx) {
784
+ const hookFiles = await findHookFiles(ctx.scanPath);
785
+ const envNeat = await fileExists(import_node_path2.default.join(ctx.scanPath, ".env.neat"));
786
+ const registryInstrPackages = new Set(
787
+ (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
788
+ );
789
+ const pkg = await readPackageJson(ctx.scanPath);
790
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
791
+ const installedDeps = {};
792
+ for (const [key, version] of Object.entries(allDeps)) {
793
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
794
+ installedDeps[key] = version;
795
+ }
796
+ }
797
+ return { hookFiles, envNeat, installedDeps };
798
+ }
799
+ async function applyExtension(ctx, args, options) {
800
+ const hookFiles = await findHookFiles(ctx.scanPath);
801
+ if (hookFiles.length === 0) {
802
+ throw new Error(
803
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
804
+ );
805
+ }
806
+ for (const file of hookFiles) {
807
+ const content = await import_node_fs2.promises.readFile(import_node_path2.default.join(ctx.scanPath, file), "utf8");
808
+ if (content.includes(args.registration_snippet)) {
809
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
810
+ }
811
+ }
812
+ const primaryFile = hookFiles[0];
813
+ const primaryPath = import_node_path2.default.join(ctx.scanPath, primaryFile);
814
+ const filesTouched = [];
815
+ const depsAdded = [];
816
+ const pkgPath = import_node_path2.default.join(ctx.scanPath, "package.json");
817
+ const pkg = await readPackageJson(ctx.scanPath);
818
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
819
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
820
+ await import_node_fs2.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
821
+ filesTouched.push("package.json");
822
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
823
+ }
824
+ const hookContent = await import_node_fs2.promises.readFile(primaryPath, "utf8");
825
+ const patched = splicedContent(hookContent, args.registration_snippet);
826
+ if (!patched) {
827
+ throw new Error(
828
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
829
+ );
830
+ }
831
+ await import_node_fs2.promises.writeFile(primaryPath, patched, "utf8");
832
+ filesTouched.push(primaryFile);
833
+ const cmd = await detectPackageManager(ctx.scanPath);
834
+ const installer = options?.runInstall ?? runPackageManagerInstall;
835
+ const install = await installer(cmd);
836
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
837
+ await appendExtendLog({
838
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
839
+ project: ctx.project,
840
+ library: args.library,
841
+ instrumentation_package: args.instrumentation_package,
842
+ version: args.version,
843
+ registration_snippet: args.registration_snippet,
844
+ filesTouched,
845
+ depsAdded,
846
+ installOutput
847
+ });
848
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
849
+ }
850
+ async function dryRunExtension(ctx, args) {
851
+ const hookFiles = await findHookFiles(ctx.scanPath);
852
+ if (hookFiles.length === 0) {
853
+ return {
854
+ library: args.library,
855
+ filesTouched: [],
856
+ depsToAdd: [],
857
+ packageJsonPatch: {},
858
+ templatePatch: "No hook files found. Run 'neat init' first."
859
+ };
860
+ }
861
+ for (const file of hookFiles) {
862
+ const content = await import_node_fs2.promises.readFile(import_node_path2.default.join(ctx.scanPath, file), "utf8");
863
+ if (content.includes(args.registration_snippet)) {
864
+ return {
865
+ library: args.library,
866
+ filesTouched: [],
867
+ depsToAdd: [],
868
+ packageJsonPatch: {},
869
+ templatePatch: "Already applied \u2014 no changes would be made."
870
+ };
871
+ }
872
+ }
873
+ const primaryFile = hookFiles[0];
874
+ const filesTouched = [];
875
+ const depsToAdd = [];
876
+ let packageJsonPatch = {};
877
+ let templatePatch = "";
878
+ const pkg = await readPackageJson(ctx.scanPath);
879
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
880
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
881
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
882
+ filesTouched.push("package.json");
883
+ }
884
+ const hookContent = await import_node_fs2.promises.readFile(import_node_path2.default.join(ctx.scanPath, primaryFile), "utf8");
885
+ const patched = splicedContent(hookContent, args.registration_snippet);
886
+ if (patched) {
887
+ filesTouched.push(primaryFile);
888
+ templatePatch = `+ ${args.registration_snippet}`;
889
+ } else {
890
+ templatePatch = "Could not find insertion point in hook file.";
891
+ }
892
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
893
+ }
894
+ async function rollbackExtension(ctx, args) {
895
+ const logPath = extendLogPath();
896
+ if (!await fileExists(logPath)) {
897
+ return { undone: false, message: "no apply found for library" };
898
+ }
899
+ const raw = await import_node_fs2.promises.readFile(logPath, "utf8");
900
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
901
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
902
+ if (!match) {
903
+ return { undone: false, message: "no apply found for library" };
904
+ }
905
+ const pkgPath = import_node_path2.default.join(ctx.scanPath, "package.json");
906
+ if (await fileExists(pkgPath)) {
907
+ const pkg = await readPackageJson(ctx.scanPath);
908
+ if (pkg.dependencies?.[match.instrumentation_package]) {
909
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
910
+ pkg.dependencies = rest;
911
+ await import_node_fs2.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
912
+ }
913
+ }
914
+ const hookFiles = await findHookFiles(ctx.scanPath);
915
+ for (const file of hookFiles) {
916
+ const filePath = import_node_path2.default.join(ctx.scanPath, file);
917
+ const content = await import_node_fs2.promises.readFile(filePath, "utf8");
918
+ if (content.includes(match.registration_snippet)) {
919
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
920
+ await import_node_fs2.promises.writeFile(filePath, filtered, "utf8");
921
+ break;
922
+ }
923
+ }
924
+ return {
925
+ undone: true,
926
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
927
+ };
928
+ }
614
929
 
615
930
  // src/divergences.ts
616
931
  init_cjs_shims();
@@ -618,9 +933,9 @@ var import_types2 = require("@neat.is/types");
618
933
 
619
934
  // src/compat.ts
620
935
  init_cjs_shims();
621
- var import_node_fs = require("fs");
622
- var import_node_os = __toESM(require("os"), 1);
623
- var import_node_path = __toESM(require("path"), 1);
936
+ var import_node_fs3 = require("fs");
937
+ var import_node_os2 = __toESM(require("os"), 1);
938
+ var import_node_path3 = __toESM(require("path"), 1);
624
939
  var import_semver = __toESM(require("semver"), 1);
625
940
 
626
941
  // compat.json
@@ -749,8 +1064,8 @@ var compat_default = {
749
1064
  var bundledMatrix = compat_default;
750
1065
  var mergedMatrix = null;
751
1066
  var remoteLoadAttempted = false;
752
- var REMOTE_CACHE_DIR = import_node_path.default.join(import_node_os.default.homedir(), ".neat");
753
- var REMOTE_CACHE_PATH = import_node_path.default.join(REMOTE_CACHE_DIR, "compat-cache.json");
1067
+ var REMOTE_CACHE_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".neat");
1068
+ var REMOTE_CACHE_PATH = import_node_path3.default.join(REMOTE_CACHE_DIR, "compat-cache.json");
754
1069
  var REMOTE_TTL_MS = 24 * 60 * 60 * 1e3;
755
1070
  function engineMeetsThreshold(engineVersion, threshold) {
756
1071
  const e = parseInt(engineVersion, 10);
@@ -861,7 +1176,7 @@ function mergeMatrices(a, b) {
861
1176
  }
862
1177
  async function readRemoteCache(url) {
863
1178
  try {
864
- const raw = await import_node_fs.promises.readFile(REMOTE_CACHE_PATH, "utf8");
1179
+ const raw = await import_node_fs3.promises.readFile(REMOTE_CACHE_PATH, "utf8");
865
1180
  const parsed = JSON.parse(raw);
866
1181
  if (parsed.url !== url) return null;
867
1182
  const age = Date.now() - new Date(parsed.fetchedAt).getTime();
@@ -878,8 +1193,8 @@ async function writeRemoteCache(url, matrix) {
878
1193
  matrix
879
1194
  };
880
1195
  try {
881
- await import_node_fs.promises.mkdir(REMOTE_CACHE_DIR, { recursive: true });
882
- await import_node_fs.promises.writeFile(REMOTE_CACHE_PATH, JSON.stringify(file), "utf8");
1196
+ await import_node_fs3.promises.mkdir(REMOTE_CACHE_DIR, { recursive: true });
1197
+ await import_node_fs3.promises.writeFile(REMOTE_CACHE_PATH, JSON.stringify(file), "utf8");
883
1198
  } catch (err) {
884
1199
  console.warn(`[neat] failed to cache compat matrix: ${err.message}`);
885
1200
  }
@@ -1038,19 +1353,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1038
1353
  function longestIncomingWalk(graph, start, maxDepth) {
1039
1354
  let best = { path: [start], edges: [] };
1040
1355
  const visited = /* @__PURE__ */ new Set([start]);
1041
- function step(node, path38, edges) {
1042
- if (path38.length > best.path.length) {
1043
- best = { path: [...path38], edges: [...edges] };
1356
+ function step(node, path42, edges) {
1357
+ if (path42.length > best.path.length) {
1358
+ best = { path: [...path42], edges: [...edges] };
1044
1359
  }
1045
- if (path38.length - 1 >= maxDepth) return;
1360
+ if (path42.length - 1 >= maxDepth) return;
1046
1361
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1047
1362
  for (const [srcId, edge] of incoming) {
1048
1363
  if (visited.has(srcId)) continue;
1049
1364
  visited.add(srcId);
1050
- path38.push(srcId);
1365
+ path42.push(srcId);
1051
1366
  edges.push(edge);
1052
- step(srcId, path38, edges);
1053
- path38.pop();
1367
+ step(srcId, path42, edges);
1368
+ path42.pop();
1054
1369
  edges.pop();
1055
1370
  visited.delete(srcId);
1056
1371
  }
@@ -1476,8 +1791,8 @@ function computeDivergences(graph, opts = {}) {
1476
1791
 
1477
1792
  // src/policy.ts
1478
1793
  init_cjs_shims();
1479
- var import_node_fs2 = require("fs");
1480
- var import_node_path2 = __toESM(require("path"), 1);
1794
+ var import_node_fs4 = require("fs");
1795
+ var import_node_path4 = __toESM(require("path"), 1);
1481
1796
  var import_types3 = require("@neat.is/types");
1482
1797
 
1483
1798
  // src/events.ts
@@ -1763,7 +2078,7 @@ function evaluateAllPolicies(graph, policies, ctx) {
1763
2078
  async function loadPolicyFile(policyPath) {
1764
2079
  let raw;
1765
2080
  try {
1766
- raw = await import_node_fs2.promises.readFile(policyPath, "utf8");
2081
+ raw = await import_node_fs4.promises.readFile(policyPath, "utf8");
1767
2082
  } catch (err) {
1768
2083
  if (err.code === "ENOENT") return [];
1769
2084
  throw err;
@@ -1784,8 +2099,8 @@ var PolicyViolationsLog = class {
1784
2099
  if (!this.seen) await this.hydrate();
1785
2100
  if (this.seen.has(v.id)) return false;
1786
2101
  this.seen.add(v.id);
1787
- await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
1788
- await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
2102
+ await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(this.path), { recursive: true });
2103
+ await import_node_fs4.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
1789
2104
  emitNeatEvent({
1790
2105
  type: "policy-violation",
1791
2106
  project: this.project,
@@ -1795,7 +2110,7 @@ var PolicyViolationsLog = class {
1795
2110
  }
1796
2111
  async readAll() {
1797
2112
  try {
1798
- const raw = await import_node_fs2.promises.readFile(this.path, "utf8");
2113
+ const raw = await import_node_fs4.promises.readFile(this.path, "utf8");
1799
2114
  return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
1800
2115
  } catch (err) {
1801
2116
  if (err.code === "ENOENT") return [];
@@ -1817,8 +2132,8 @@ init_cjs_shims();
1817
2132
 
1818
2133
  // src/ingest.ts
1819
2134
  init_cjs_shims();
1820
- var import_node_fs3 = require("fs");
1821
- var import_node_path3 = __toESM(require("path"), 1);
2135
+ var import_node_fs5 = require("fs");
2136
+ var import_node_path5 = __toESM(require("path"), 1);
1822
2137
  var sourceMapJs = __toESM(require("source-map-js"), 1);
1823
2138
  var import_types4 = require("@neat.is/types");
1824
2139
  var HOUR_MS = 60 * 60 * 1e3;
@@ -1918,7 +2233,7 @@ function languageForExt(relPath) {
1918
2233
  function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
1919
2234
  let p = toPosix(filepath).replace(/^file:\/\//, "");
1920
2235
  if (scanPath && scanPath.length > 0) {
1921
- const absRoot = toPosix(import_node_path3.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
2236
+ const absRoot = toPosix(import_node_path5.default.resolve(scanPath, serviceNode?.repoPath ?? ""));
1922
2237
  const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
1923
2238
  if (p.startsWith(anchor)) return p.slice(anchor.length);
1924
2239
  }
@@ -1946,10 +2261,10 @@ function resolveDistToSrc(absFilepath, line) {
1946
2261
  entry = null;
1947
2262
  const mapPath = `${absFilepath}.map`;
1948
2263
  try {
1949
- if ((0, import_node_fs3.existsSync)(mapPath)) {
1950
- const raw = JSON.parse((0, import_node_fs3.readFileSync)(mapPath, "utf8"));
2264
+ if ((0, import_node_fs5.existsSync)(mapPath)) {
2265
+ const raw = JSON.parse((0, import_node_fs5.readFileSync)(mapPath, "utf8"));
1951
2266
  const consumer = new sourceMapJs.SourceMapConsumer(raw);
1952
- entry = { consumer, dir: import_node_path3.default.dirname(mapPath) };
2267
+ entry = { consumer, dir: import_node_path5.default.dirname(mapPath) };
1953
2268
  }
1954
2269
  } catch {
1955
2270
  entry = null;
@@ -1964,7 +2279,7 @@ function resolveDistToSrc(absFilepath, line) {
1964
2279
  });
1965
2280
  if (!pos || !pos.source) return null;
1966
2281
  const root = entry.consumer.sourceRoot ?? "";
1967
- const resolved = import_node_path3.default.resolve(entry.dir, root, pos.source);
2282
+ const resolved = import_node_path5.default.resolve(entry.dir, root, pos.source);
1968
2283
  return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
1969
2284
  } catch {
1970
2285
  return null;
@@ -2146,7 +2461,7 @@ function ensureFrontierNode(graph, host, ts) {
2146
2461
  graph.addNode(id, node);
2147
2462
  return id;
2148
2463
  }
2149
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
2464
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
2150
2465
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
2151
2466
  const id = makeObservedEdgeId(type, source, target);
2152
2467
  if (graph.hasEdge(id)) {
@@ -2183,7 +2498,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
2183
2498
  confidence: (0, import_types4.confidenceForObservedSignal)(signal),
2184
2499
  lastObserved: ts,
2185
2500
  callCount: 1,
2186
- signal
2501
+ signal,
2502
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
2503
+ // Only set when code.filepath was present on the span — never fabricated.
2504
+ ...evidence ? { evidence } : {}
2187
2505
  };
2188
2506
  graph.addEdgeWithKey(id, source, target, edge);
2189
2507
  return { edge, created: true };
@@ -2228,8 +2546,8 @@ function upsertInferredEdge(graph, type, source, target, ts) {
2228
2546
  graph.addEdgeWithKey(id, source, target, edge);
2229
2547
  }
2230
2548
  async function appendErrorEvent(ctx, ev) {
2231
- await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
2232
- await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
2549
+ await import_node_fs5.promises.mkdir(import_node_path5.default.dirname(ctx.errorsPath), { recursive: true });
2550
+ await import_node_fs5.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
2233
2551
  }
2234
2552
  function sanitizeAttributes(attrs) {
2235
2553
  const out = {};
@@ -2252,6 +2570,7 @@ async function handleSpan(ctx, span) {
2252
2570
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2253
2571
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
2254
2572
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
2573
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
2255
2574
  let affectedNode = sourceId;
2256
2575
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
2257
2576
  if (span.dbSystem) {
@@ -2265,7 +2584,8 @@ async function handleSpan(ctx, span) {
2265
2584
  observedSource(),
2266
2585
  targetId,
2267
2586
  ts,
2268
- isError
2587
+ isError,
2588
+ callSiteEvidence
2269
2589
  );
2270
2590
  if (result) affectedNode = targetId;
2271
2591
  }
@@ -2281,7 +2601,8 @@ async function handleSpan(ctx, span) {
2281
2601
  observedSource(),
2282
2602
  targetId,
2283
2603
  ts,
2284
- isError
2604
+ isError,
2605
+ callSiteEvidence
2285
2606
  );
2286
2607
  affectedNode = targetId;
2287
2608
  resolvedViaAddress = true;
@@ -2293,7 +2614,8 @@ async function handleSpan(ctx, span) {
2293
2614
  observedSource(),
2294
2615
  frontierNodeId,
2295
2616
  ts,
2296
- isError
2617
+ isError,
2618
+ callSiteEvidence
2297
2619
  );
2298
2620
  affectedNode = frontierNodeId;
2299
2621
  resolvedViaAddress = true;
@@ -2451,13 +2773,13 @@ async function markStaleEdges(graph, options = {}) {
2451
2773
  return { count: events.length, events };
2452
2774
  }
2453
2775
  async function appendStaleEvents(staleEventsPath, events) {
2454
- await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(staleEventsPath), { recursive: true });
2776
+ await import_node_fs5.promises.mkdir(import_node_path5.default.dirname(staleEventsPath), { recursive: true });
2455
2777
  const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
2456
- await import_node_fs3.promises.appendFile(staleEventsPath, lines, "utf8");
2778
+ await import_node_fs5.promises.appendFile(staleEventsPath, lines, "utf8");
2457
2779
  }
2458
2780
  async function readStaleEvents(staleEventsPath) {
2459
2781
  try {
2460
- const raw = await import_node_fs3.promises.readFile(staleEventsPath, "utf8");
2782
+ const raw = await import_node_fs5.promises.readFile(staleEventsPath, "utf8");
2461
2783
  return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2462
2784
  } catch (err) {
2463
2785
  if (err.code === "ENOENT") return [];
@@ -2491,7 +2813,7 @@ function startStalenessLoop(graph, options = {}) {
2491
2813
  }
2492
2814
  async function readErrorEvents(errorsPath) {
2493
2815
  try {
2494
- const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
2816
+ const raw = await import_node_fs5.promises.readFile(errorsPath, "utf8");
2495
2817
  return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2496
2818
  } catch (err) {
2497
2819
  if (err.code === "ENOENT") return [];
@@ -2523,16 +2845,16 @@ function mergeSnapshot(graph, snapshot) {
2523
2845
 
2524
2846
  // src/extract/services.ts
2525
2847
  init_cjs_shims();
2526
- var import_node_fs8 = require("fs");
2527
- var import_node_path8 = __toESM(require("path"), 1);
2848
+ var import_node_fs10 = require("fs");
2849
+ var import_node_path10 = __toESM(require("path"), 1);
2528
2850
  var import_ignore = __toESM(require("ignore"), 1);
2529
2851
  var import_minimatch2 = require("minimatch");
2530
2852
  var import_types6 = require("@neat.is/types");
2531
2853
 
2532
2854
  // src/extract/shared.ts
2533
2855
  init_cjs_shims();
2534
- var import_node_fs4 = require("fs");
2535
- var import_node_path4 = __toESM(require("path"), 1);
2856
+ var import_node_fs6 = require("fs");
2857
+ var import_node_path6 = __toESM(require("path"), 1);
2536
2858
  var import_yaml = require("yaml");
2537
2859
  var import_types5 = require("@neat.is/types");
2538
2860
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py"]);
@@ -2564,7 +2886,7 @@ async function isPythonVenvDir(dir) {
2564
2886
  const cached = PYVENV_MARKER_CACHE.get(dir);
2565
2887
  if (cached !== void 0) return cached;
2566
2888
  try {
2567
- const stat = await import_node_fs4.promises.stat(import_node_path4.default.join(dir, "pyvenv.cfg"));
2889
+ const stat = await import_node_fs6.promises.stat(import_node_path6.default.join(dir, "pyvenv.cfg"));
2568
2890
  const ok = stat.isFile();
2569
2891
  PYVENV_MARKER_CACHE.set(dir, ok);
2570
2892
  return ok;
@@ -2574,7 +2896,7 @@ async function isPythonVenvDir(dir) {
2574
2896
  }
2575
2897
  }
2576
2898
  function isConfigFile(name) {
2577
- const ext = import_node_path4.default.extname(name);
2899
+ const ext = import_node_path6.default.extname(name);
2578
2900
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2579
2901
  if (name === ".env" || name.startsWith(".env.")) {
2580
2902
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
@@ -2682,16 +3004,16 @@ function cleanVersion(raw) {
2682
3004
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
2683
3005
  }
2684
3006
  async function readJson(filePath) {
2685
- const raw = await import_node_fs4.promises.readFile(filePath, "utf8");
3007
+ const raw = await import_node_fs6.promises.readFile(filePath, "utf8");
2686
3008
  return JSON.parse(raw);
2687
3009
  }
2688
3010
  async function readYaml(filePath) {
2689
- const raw = await import_node_fs4.promises.readFile(filePath, "utf8");
3011
+ const raw = await import_node_fs6.promises.readFile(filePath, "utf8");
2690
3012
  return (0, import_yaml.parse)(raw);
2691
3013
  }
2692
- async function exists(p) {
3014
+ async function exists2(p) {
2693
3015
  try {
2694
- await import_node_fs4.promises.access(p);
3016
+ await import_node_fs6.promises.access(p);
2695
3017
  return true;
2696
3018
  } catch {
2697
3019
  return false;
@@ -2700,8 +3022,8 @@ async function exists(p) {
2700
3022
 
2701
3023
  // src/extract/python.ts
2702
3024
  init_cjs_shims();
2703
- var import_node_fs5 = require("fs");
2704
- var import_node_path5 = __toESM(require("path"), 1);
3025
+ var import_node_fs7 = require("fs");
3026
+ var import_node_path7 = __toESM(require("path"), 1);
2705
3027
  var import_smol_toml = require("smol-toml");
2706
3028
  var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
2707
3029
  function parseRequirementsTxt(content) {
@@ -2734,25 +3056,25 @@ function depsFromPyProject(pyproject) {
2734
3056
  return out;
2735
3057
  }
2736
3058
  async function discoverPythonService(serviceDir) {
2737
- const pyprojectPath = import_node_path5.default.join(serviceDir, "pyproject.toml");
2738
- const requirementsPath = import_node_path5.default.join(serviceDir, "requirements.txt");
2739
- const setupPath = import_node_path5.default.join(serviceDir, "setup.py");
2740
- const hasPyproject = await exists(pyprojectPath);
2741
- const hasRequirements = await exists(requirementsPath);
2742
- const hasSetup = await exists(setupPath);
3059
+ const pyprojectPath = import_node_path7.default.join(serviceDir, "pyproject.toml");
3060
+ const requirementsPath = import_node_path7.default.join(serviceDir, "requirements.txt");
3061
+ const setupPath = import_node_path7.default.join(serviceDir, "setup.py");
3062
+ const hasPyproject = await exists2(pyprojectPath);
3063
+ const hasRequirements = await exists2(requirementsPath);
3064
+ const hasSetup = await exists2(setupPath);
2743
3065
  if (!hasPyproject && !hasRequirements && !hasSetup) return null;
2744
- let name = import_node_path5.default.basename(serviceDir);
3066
+ let name = import_node_path7.default.basename(serviceDir);
2745
3067
  let version;
2746
3068
  const dependencies = {};
2747
3069
  if (hasPyproject) {
2748
- const raw = await import_node_fs5.promises.readFile(pyprojectPath, "utf8");
3070
+ const raw = await import_node_fs7.promises.readFile(pyprojectPath, "utf8");
2749
3071
  const pyproject = (0, import_smol_toml.parse)(raw);
2750
3072
  name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
2751
3073
  version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
2752
3074
  Object.assign(dependencies, depsFromPyProject(pyproject));
2753
3075
  }
2754
3076
  if (hasRequirements) {
2755
- const raw = await import_node_fs5.promises.readFile(requirementsPath, "utf8");
3077
+ const raw = await import_node_fs7.promises.readFile(requirementsPath, "utf8");
2756
3078
  Object.assign(dependencies, parseRequirementsTxt(raw));
2757
3079
  }
2758
3080
  return { name, version, dependencies };
@@ -2767,17 +3089,17 @@ function pythonToPackage(service) {
2767
3089
 
2768
3090
  // src/extract/owners.ts
2769
3091
  init_cjs_shims();
2770
- var import_node_fs6 = require("fs");
2771
- var import_node_path6 = __toESM(require("path"), 1);
3092
+ var import_node_fs8 = require("fs");
3093
+ var import_node_path8 = __toESM(require("path"), 1);
2772
3094
  var import_minimatch = require("minimatch");
2773
3095
  async function loadCodeowners(scanPath) {
2774
3096
  const candidates = [
2775
- import_node_path6.default.join(scanPath, "CODEOWNERS"),
2776
- import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
3097
+ import_node_path8.default.join(scanPath, "CODEOWNERS"),
3098
+ import_node_path8.default.join(scanPath, ".github", "CODEOWNERS")
2777
3099
  ];
2778
3100
  for (const file of candidates) {
2779
- if (await exists(file)) {
2780
- const raw = await import_node_fs6.promises.readFile(file, "utf8");
3101
+ if (await exists2(file)) {
3102
+ const raw = await import_node_fs8.promises.readFile(file, "utf8");
2781
3103
  return parseCodeowners(raw);
2782
3104
  }
2783
3105
  }
@@ -2795,7 +3117,7 @@ function parseCodeowners(raw) {
2795
3117
  return { rules };
2796
3118
  }
2797
3119
  function matchOwner(file, repoPath) {
2798
- const normalized = repoPath.split(import_node_path6.default.sep).join("/");
3120
+ const normalized = repoPath.split(import_node_path8.default.sep).join("/");
2799
3121
  for (const rule of file.rules) {
2800
3122
  if (matchesPattern(rule.pattern, normalized)) return rule.owners;
2801
3123
  }
@@ -2811,8 +3133,8 @@ function matchesPattern(rawPattern, repoPath) {
2811
3133
  return false;
2812
3134
  }
2813
3135
  async function readPackageJsonAuthor(serviceDir) {
2814
- const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
2815
- if (!await exists(pkgPath)) return null;
3136
+ const pkgPath = import_node_path8.default.join(serviceDir, "package.json");
3137
+ if (!await exists2(pkgPath)) return null;
2816
3138
  try {
2817
3139
  const pkg = await readJson(pkgPath);
2818
3140
  if (!pkg.author) return null;
@@ -2834,8 +3156,8 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2834
3156
 
2835
3157
  // src/extract/errors.ts
2836
3158
  init_cjs_shims();
2837
- var import_node_fs7 = require("fs");
2838
- var import_node_path7 = __toESM(require("path"), 1);
3159
+ var import_node_fs9 = require("fs");
3160
+ var import_node_path9 = __toESM(require("path"), 1);
2839
3161
  var sink = [];
2840
3162
  function recordExtractionError(producer, file, err) {
2841
3163
  const e = err instanceof Error ? err : new Error(String(err));
@@ -2854,9 +3176,9 @@ function drainExtractionErrors() {
2854
3176
  }
2855
3177
  async function writeExtractionErrors(errors, errorsPath) {
2856
3178
  if (errors.length === 0) return;
2857
- await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
3179
+ await import_node_fs9.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
2858
3180
  const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2859
- await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
3181
+ await import_node_fs9.promises.appendFile(errorsPath, lines, "utf8");
2860
3182
  }
2861
3183
  var droppedSink = [];
2862
3184
  function noteExtractedDropped(edge) {
@@ -2871,9 +3193,9 @@ function isRejectedLogEnabled() {
2871
3193
  }
2872
3194
  async function writeRejectedExtracted(drops, rejectedPath) {
2873
3195
  if (drops.length === 0) return;
2874
- await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
3196
+ await import_node_fs9.promises.mkdir(import_node_path9.default.dirname(rejectedPath), { recursive: true });
2875
3197
  const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
2876
- await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
3198
+ await import_node_fs9.promises.appendFile(rejectedPath, lines, "utf8");
2877
3199
  }
2878
3200
 
2879
3201
  // src/extract/services.ts
@@ -2892,21 +3214,21 @@ function workspaceGlobs(pkg) {
2892
3214
  return null;
2893
3215
  }
2894
3216
  async function loadGitignore(scanPath) {
2895
- const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
2896
- if (!await exists(gitignorePath)) return null;
2897
- const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
3217
+ const gitignorePath = import_node_path10.default.join(scanPath, ".gitignore");
3218
+ if (!await exists2(gitignorePath)) return null;
3219
+ const raw = await import_node_fs10.promises.readFile(gitignorePath, "utf8");
2898
3220
  return (0, import_ignore.default)().add(raw);
2899
3221
  }
2900
3222
  async function walkDirs(start, scanPath, options, visit) {
2901
3223
  async function recurse(current, depth) {
2902
3224
  if (depth > options.maxDepth) return;
2903
- const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3225
+ const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2904
3226
  for (const entry of entries) {
2905
3227
  if (!entry.isDirectory()) continue;
2906
3228
  if (IGNORED_DIRS.has(entry.name)) continue;
2907
- const child = import_node_path8.default.join(current, entry.name);
3229
+ const child = import_node_path10.default.join(current, entry.name);
2908
3230
  if (options.ig) {
2909
- const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
3231
+ const rel = import_node_path10.default.relative(scanPath, child).split(import_node_path10.default.sep).join("/");
2910
3232
  if (rel && options.ig.ignores(rel + "/")) continue;
2911
3233
  }
2912
3234
  if (await isPythonVenvDir(child)) continue;
@@ -2922,8 +3244,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2922
3244
  for (const raw of globs) {
2923
3245
  const pattern = raw.replace(/^\.\//, "");
2924
3246
  if (!pattern.includes("*")) {
2925
- const candidate = import_node_path8.default.join(scanPath, pattern);
2926
- if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
3247
+ const candidate = import_node_path10.default.join(scanPath, pattern);
3248
+ if (await exists2(import_node_path10.default.join(candidate, "package.json"))) found.add(candidate);
2927
3249
  continue;
2928
3250
  }
2929
3251
  const segments = pattern.split("/");
@@ -2932,13 +3254,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2932
3254
  if (seg.includes("*")) break;
2933
3255
  staticSegments.push(seg);
2934
3256
  }
2935
- const start = import_node_path8.default.join(scanPath, ...staticSegments);
2936
- if (!await exists(start)) continue;
3257
+ const start = import_node_path10.default.join(scanPath, ...staticSegments);
3258
+ if (!await exists2(start)) continue;
2937
3259
  const hasDoubleStar = pattern.includes("**");
2938
3260
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2939
3261
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2940
- const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2941
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
3262
+ const rel = import_node_path10.default.relative(scanPath, dir).split(import_node_path10.default.sep).join("/");
3263
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists2(import_node_path10.default.join(dir, "package.json"))) {
2942
3264
  found.add(dir);
2943
3265
  }
2944
3266
  });
@@ -2958,13 +3280,13 @@ function detectJsFramework(pkg) {
2958
3280
  return void 0;
2959
3281
  }
2960
3282
  async function discoverNodeService(scanPath, dir) {
2961
- const pkgPath = import_node_path8.default.join(dir, "package.json");
2962
- if (!await exists(pkgPath)) return null;
3283
+ const pkgPath = import_node_path10.default.join(dir, "package.json");
3284
+ if (!await exists2(pkgPath)) return null;
2963
3285
  let pkg;
2964
3286
  try {
2965
3287
  pkg = await readJson(pkgPath);
2966
3288
  } catch (err) {
2967
- recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
3289
+ recordExtractionError("services", import_node_path10.default.relative(scanPath, pkgPath), err);
2968
3290
  return null;
2969
3291
  }
2970
3292
  if (!pkg.name) return null;
@@ -2976,7 +3298,7 @@ async function discoverNodeService(scanPath, dir) {
2976
3298
  language: "javascript",
2977
3299
  version: pkg.version,
2978
3300
  dependencies: pkg.dependencies ?? {},
2979
- repoPath: import_node_path8.default.relative(scanPath, dir),
3301
+ repoPath: import_node_path10.default.relative(scanPath, dir),
2980
3302
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
2981
3303
  ...framework ? { framework } : {}
2982
3304
  };
@@ -2993,20 +3315,20 @@ async function discoverPyService(scanPath, dir) {
2993
3315
  language: "python",
2994
3316
  version: py.version,
2995
3317
  dependencies: py.dependencies,
2996
- repoPath: import_node_path8.default.relative(scanPath, dir)
3318
+ repoPath: import_node_path10.default.relative(scanPath, dir)
2997
3319
  };
2998
3320
  return { pkg, dir, node };
2999
3321
  }
3000
3322
  async function discoverServices(scanPath) {
3001
- const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
3323
+ const rootPkgPath = import_node_path10.default.join(scanPath, "package.json");
3002
3324
  let rootPkg = null;
3003
- if (await exists(rootPkgPath)) {
3325
+ if (await exists2(rootPkgPath)) {
3004
3326
  try {
3005
3327
  rootPkg = await readJson(rootPkgPath);
3006
3328
  } catch (err) {
3007
3329
  recordExtractionError(
3008
3330
  "services workspaces",
3009
- import_node_path8.default.relative(scanPath, rootPkgPath),
3331
+ import_node_path10.default.relative(scanPath, rootPkgPath),
3010
3332
  err
3011
3333
  );
3012
3334
  }
@@ -3023,9 +3345,9 @@ async function discoverServices(scanPath) {
3023
3345
  scanPath,
3024
3346
  { maxDepth: parseScanDepth(), ig },
3025
3347
  async (dir) => {
3026
- if (await exists(import_node_path8.default.join(dir, "package.json"))) {
3348
+ if (await exists2(import_node_path10.default.join(dir, "package.json"))) {
3027
3349
  candidateDirs.push(dir);
3028
- } else if (await exists(import_node_path8.default.join(dir, "pyproject.toml")) || await exists(import_node_path8.default.join(dir, "requirements.txt")) || await exists(import_node_path8.default.join(dir, "setup.py"))) {
3350
+ } else if (await exists2(import_node_path10.default.join(dir, "pyproject.toml")) || await exists2(import_node_path10.default.join(dir, "requirements.txt")) || await exists2(import_node_path10.default.join(dir, "setup.py"))) {
3029
3351
  candidateDirs.push(dir);
3030
3352
  }
3031
3353
  }
@@ -3039,8 +3361,8 @@ async function discoverServices(scanPath) {
3039
3361
  if (!service) continue;
3040
3362
  const existingDir = seen.get(service.node.name);
3041
3363
  if (existingDir !== void 0) {
3042
- const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
3043
- const b = import_node_path8.default.relative(scanPath, dir) || ".";
3364
+ const a = import_node_path10.default.relative(scanPath, existingDir) || ".";
3365
+ const b = import_node_path10.default.relative(scanPath, dir) || ".";
3044
3366
  console.warn(
3045
3367
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
3046
3368
  );
@@ -3077,8 +3399,8 @@ function addServiceNodes(graph, services) {
3077
3399
 
3078
3400
  // src/extract/aliases.ts
3079
3401
  init_cjs_shims();
3080
- var import_node_path9 = __toESM(require("path"), 1);
3081
- var import_node_fs9 = require("fs");
3402
+ var import_node_path11 = __toESM(require("path"), 1);
3403
+ var import_node_fs11 = require("fs");
3082
3404
  var import_yaml2 = require("yaml");
3083
3405
  var import_types7 = require("@neat.is/types");
3084
3406
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -3105,15 +3427,15 @@ function indexServicesByName(services) {
3105
3427
  const map = /* @__PURE__ */ new Map();
3106
3428
  for (const s of services) {
3107
3429
  map.set(s.node.name, s.node.id);
3108
- map.set(import_node_path9.default.basename(s.dir), s.node.id);
3430
+ map.set(import_node_path11.default.basename(s.dir), s.node.id);
3109
3431
  }
3110
3432
  return map;
3111
3433
  }
3112
3434
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
3113
3435
  let composePath = null;
3114
3436
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3115
- const abs = import_node_path9.default.join(scanPath, name);
3116
- if (await exists(abs)) {
3437
+ const abs = import_node_path11.default.join(scanPath, name);
3438
+ if (await exists2(abs)) {
3117
3439
  composePath = abs;
3118
3440
  break;
3119
3441
  }
@@ -3125,7 +3447,7 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
3125
3447
  } catch (err) {
3126
3448
  recordExtractionError(
3127
3449
  "aliases compose",
3128
- import_node_path9.default.relative(scanPath, composePath),
3450
+ import_node_path11.default.relative(scanPath, composePath),
3129
3451
  err
3130
3452
  );
3131
3453
  return;
@@ -3168,11 +3490,11 @@ function parseDockerfileLabels(content) {
3168
3490
  }
3169
3491
  async function collectDockerfileAliases(graph, services) {
3170
3492
  for (const service of services) {
3171
- const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
3172
- if (!await exists(dockerfilePath)) continue;
3493
+ const dockerfilePath = import_node_path11.default.join(service.dir, "Dockerfile");
3494
+ if (!await exists2(dockerfilePath)) continue;
3173
3495
  let content;
3174
3496
  try {
3175
- content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
3497
+ content = await import_node_fs11.promises.readFile(dockerfilePath, "utf8");
3176
3498
  } catch (err) {
3177
3499
  recordExtractionError("aliases dockerfile", dockerfilePath, err);
3178
3500
  continue;
@@ -3184,15 +3506,15 @@ async function collectDockerfileAliases(graph, services) {
3184
3506
  async function walkYamlFiles(start, depth = 0, max = 5) {
3185
3507
  if (depth > max) return [];
3186
3508
  const out = [];
3187
- const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3509
+ const entries = await import_node_fs11.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3188
3510
  for (const entry of entries) {
3189
3511
  if (entry.isDirectory()) {
3190
3512
  if (IGNORED_DIRS.has(entry.name)) continue;
3191
- const child = import_node_path9.default.join(start, entry.name);
3513
+ const child = import_node_path11.default.join(start, entry.name);
3192
3514
  if (await isPythonVenvDir(child)) continue;
3193
3515
  out.push(...await walkYamlFiles(child, depth + 1, max));
3194
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
3195
- out.push(import_node_path9.default.join(start, entry.name));
3516
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry.name))) {
3517
+ out.push(import_node_path11.default.join(start, entry.name));
3196
3518
  }
3197
3519
  }
3198
3520
  return out;
@@ -3219,7 +3541,7 @@ function k8sServiceTarget(doc, byName) {
3219
3541
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
3220
3542
  const files = await walkYamlFiles(scanPath);
3221
3543
  for (const file of files) {
3222
- const content = await import_node_fs9.promises.readFile(file, "utf8");
3544
+ const content = await import_node_fs11.promises.readFile(file, "utf8");
3223
3545
  let docs;
3224
3546
  try {
3225
3547
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3242,105 +3564,522 @@ async function addServiceAliases(graph, scanPath, services) {
3242
3564
  await collectK8sAliases(graph, scanPath, byName);
3243
3565
  }
3244
3566
 
3245
- // src/extract/databases/index.ts
3567
+ // src/extract/files.ts
3246
3568
  init_cjs_shims();
3247
- var import_node_path17 = __toESM(require("path"), 1);
3248
- var import_types8 = require("@neat.is/types");
3249
3569
 
3250
- // src/extract/databases/db-config-yaml.ts
3570
+ // src/extract/calls/shared.ts
3251
3571
  init_cjs_shims();
3252
- var import_node_path10 = __toESM(require("path"), 1);
3253
- async function parse(serviceDir) {
3254
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3255
- if (!await exists(yamlPath)) return [];
3256
- const raw = await readYaml(yamlPath);
3257
- return [
3258
- {
3259
- host: raw.host,
3260
- port: raw.port,
3261
- database: raw.database,
3262
- engine: raw.engine,
3263
- engineVersion: raw.engineVersion !== void 0 ? String(raw.engineVersion) : "unknown",
3264
- sourceFile: yamlPath
3572
+ var import_node_fs12 = require("fs");
3573
+ var import_node_path12 = __toESM(require("path"), 1);
3574
+ var import_types8 = require("@neat.is/types");
3575
+ async function walkSourceFiles(dir) {
3576
+ const out = [];
3577
+ async function walk(current) {
3578
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3579
+ for (const entry of entries) {
3580
+ const full = import_node_path12.default.join(current, entry.name);
3581
+ if (entry.isDirectory()) {
3582
+ if (IGNORED_DIRS.has(entry.name)) continue;
3583
+ if (await isPythonVenvDir(full)) continue;
3584
+ await walk(full);
3585
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name))) {
3586
+ out.push(full);
3587
+ }
3265
3588
  }
3266
- ];
3589
+ }
3590
+ await walk(dir);
3591
+ return out;
3267
3592
  }
3268
- var dbConfigYamlParser = { name: "db-config.yaml", parse };
3269
-
3270
- // src/extract/databases/dotenv.ts
3271
- init_cjs_shims();
3272
- var import_node_fs11 = require("fs");
3273
- var import_node_path12 = __toESM(require("path"), 1);
3274
-
3275
- // src/extract/databases/shared.ts
3276
- init_cjs_shims();
3277
- var import_node_fs10 = require("fs");
3278
- var import_node_path11 = __toESM(require("path"), 1);
3279
- function schemeToEngine(scheme) {
3280
- const s = scheme.toLowerCase().split("+")[0];
3281
- switch (s) {
3282
- case "postgres":
3283
- case "postgresql":
3284
- return "postgresql";
3285
- case "mysql":
3286
- case "mariadb":
3287
- return "mysql";
3288
- case "mongodb":
3289
- case "mongodb+srv":
3290
- return "mongodb";
3291
- case "redis":
3292
- case "rediss":
3293
- return "redis";
3294
- case "sqlite":
3295
- return "sqlite";
3296
- default:
3297
- return null;
3593
+ async function loadSourceFiles(dir) {
3594
+ const paths = await walkSourceFiles(dir);
3595
+ const out = [];
3596
+ for (const p of paths) {
3597
+ try {
3598
+ const content = await import_node_fs12.promises.readFile(p, "utf8");
3599
+ out.push({ path: p, content });
3600
+ } catch {
3601
+ }
3298
3602
  }
3603
+ return out;
3299
3604
  }
3300
- function parseConnectionString(url) {
3301
- const m = url.match(
3302
- /^(?<scheme>[a-z][a-z+]*):\/\/(?:[^@/]+(?::[^@]*)?@)?(?<host>[^:/?]+)(?::(?<port>\d+))?(?:\/(?<db>[^?#]*))?/i
3303
- );
3304
- if (!m || !m.groups) return null;
3305
- const engine = schemeToEngine(m.groups.scheme);
3306
- if (!engine) return null;
3307
- return {
3308
- host: m.groups.host,
3309
- port: m.groups.port ? Number(m.groups.port) : void 0,
3310
- database: m.groups.db ?? "",
3311
- engine,
3312
- engineVersion: "unknown"
3313
- };
3605
+ function lineOf(text, needle) {
3606
+ const idx = text.indexOf(needle);
3607
+ if (idx < 0) return 1;
3608
+ return text.slice(0, idx).split("\n").length;
3314
3609
  }
3315
- async function readIfExists(filePath) {
3316
- try {
3317
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3318
- } catch {
3319
- return null;
3320
- }
3610
+ function snippet(text, line) {
3611
+ const lines = text.split("\n");
3612
+ return (lines[line - 1] ?? "").trim();
3321
3613
  }
3322
- async function findFirst(serviceDir, candidates) {
3323
- for (const rel of candidates) {
3324
- const abs = import_node_path11.default.join(serviceDir, rel);
3325
- const content = await readIfExists(abs);
3326
- if (content !== null) return abs;
3614
+ function toPosix2(p) {
3615
+ return p.split("\\").join("/");
3616
+ }
3617
+ function languageForPath(relPath) {
3618
+ switch (import_node_path12.default.extname(relPath).toLowerCase()) {
3619
+ case ".py":
3620
+ return "python";
3621
+ case ".ts":
3622
+ case ".tsx":
3623
+ return "typescript";
3624
+ case ".js":
3625
+ case ".jsx":
3626
+ case ".mjs":
3627
+ case ".cjs":
3628
+ return "javascript";
3629
+ default:
3630
+ return void 0;
3327
3631
  }
3328
- return null;
3329
3632
  }
3330
- function engineFromImage(image) {
3331
- const lower = image.toLowerCase();
3332
- const colon = lower.lastIndexOf(":");
3333
- const repo = colon >= 0 ? lower.slice(0, colon) : lower;
3334
- const tag = colon >= 0 ? lower.slice(colon + 1) : "latest";
3335
- const last = repo.split("/").pop() ?? repo;
3336
- let engine = null;
3337
- if (last.startsWith("postgres")) engine = "postgresql";
3338
- else if (last.startsWith("mysql") || last.startsWith("mariadb")) engine = "mysql";
3339
- else if (last.startsWith("mongo")) engine = "mongodb";
3340
- else if (last.startsWith("redis")) engine = "redis";
3341
- else if (last.startsWith("sqlite")) engine = "sqlite";
3342
- if (!engine) return null;
3343
- const versionMatch = tag.match(/^(\d+(?:\.\d+){0,2})/);
3633
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3634
+ let nodesAdded = 0;
3635
+ let edgesAdded = 0;
3636
+ const fileNodeId = (0, import_types8.fileId)(serviceName, relPath);
3637
+ if (!graph.hasNode(fileNodeId)) {
3638
+ const language = languageForPath(relPath);
3639
+ const node = {
3640
+ id: fileNodeId,
3641
+ type: import_types8.NodeType.FileNode,
3642
+ service: serviceName,
3643
+ path: relPath,
3644
+ ...language ? { language } : {},
3645
+ discoveredVia: "static"
3646
+ };
3647
+ graph.addNode(fileNodeId, node);
3648
+ nodesAdded++;
3649
+ }
3650
+ const containsId = (0, import_types8.extractedEdgeId)(serviceNodeId, fileNodeId, import_types8.EdgeType.CONTAINS);
3651
+ if (!graph.hasEdge(containsId)) {
3652
+ const edge = {
3653
+ id: containsId,
3654
+ source: serviceNodeId,
3655
+ target: fileNodeId,
3656
+ type: import_types8.EdgeType.CONTAINS,
3657
+ provenance: import_types8.Provenance.EXTRACTED,
3658
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3659
+ evidence: { file: relPath }
3660
+ };
3661
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3662
+ edgesAdded++;
3663
+ }
3664
+ return { fileNodeId, nodesAdded, edgesAdded };
3665
+ }
3666
+
3667
+ // src/extract/files.ts
3668
+ var import_node_path13 = __toESM(require("path"), 1);
3669
+ async function addFiles(graph, services) {
3670
+ let nodesAdded = 0;
3671
+ let edgesAdded = 0;
3672
+ for (const service of services) {
3673
+ const filePaths = await walkSourceFiles(service.dir);
3674
+ for (const filePath of filePaths) {
3675
+ const relPath = toPosix2(import_node_path13.default.relative(service.dir, filePath));
3676
+ const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
3677
+ graph,
3678
+ service.pkg.name,
3679
+ service.node.id,
3680
+ relPath
3681
+ );
3682
+ nodesAdded += n;
3683
+ edgesAdded += e;
3684
+ }
3685
+ }
3686
+ return { nodesAdded, edgesAdded };
3687
+ }
3688
+
3689
+ // src/extract/imports.ts
3690
+ init_cjs_shims();
3691
+ var import_node_path14 = __toESM(require("path"), 1);
3692
+ var import_node_fs13 = require("fs");
3693
+ var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3694
+ var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3695
+ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3696
+ var import_types9 = require("@neat.is/types");
3697
+ var PARSE_CHUNK = 16384;
3698
+ function parseSource(parser, source) {
3699
+ return parser.parse(
3700
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
3701
+ );
3702
+ }
3703
+ function makeJsParser() {
3704
+ const p = new import_tree_sitter.default();
3705
+ p.setLanguage(import_tree_sitter_javascript.default);
3706
+ return p;
3707
+ }
3708
+ function makePyParser() {
3709
+ const p = new import_tree_sitter.default();
3710
+ p.setLanguage(import_tree_sitter_python.default);
3711
+ return p;
3712
+ }
3713
+ function stringLiteralText(node) {
3714
+ for (let i = 0; i < node.childCount; i++) {
3715
+ const child = node.child(i);
3716
+ if (child?.type === "string_fragment") return child.text;
3717
+ }
3718
+ const raw = node.text;
3719
+ if (raw.length >= 2) return raw.slice(1, -1);
3720
+ return raw.length === 0 ? null : "";
3721
+ }
3722
+ function clipSnippet(text) {
3723
+ const oneLine = text.split("\n")[0] ?? text;
3724
+ return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
3725
+ }
3726
+ function collectJsImports(node, out) {
3727
+ if (node.type === "import_statement") {
3728
+ const source = node.childForFieldName("source");
3729
+ if (source) {
3730
+ const specifier = stringLiteralText(source);
3731
+ if (specifier) {
3732
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3733
+ }
3734
+ }
3735
+ return;
3736
+ }
3737
+ if (node.type === "call_expression") {
3738
+ const fn = node.childForFieldName("function");
3739
+ if (fn?.type === "identifier" && fn.text === "require") {
3740
+ const args = node.childForFieldName("arguments");
3741
+ const firstArg = args?.namedChild(0);
3742
+ if (firstArg?.type === "string") {
3743
+ const specifier = stringLiteralText(firstArg);
3744
+ if (specifier) {
3745
+ out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3746
+ }
3747
+ }
3748
+ }
3749
+ }
3750
+ for (let i = 0; i < node.namedChildCount; i++) {
3751
+ const child = node.namedChild(i);
3752
+ if (child) collectJsImports(child, out);
3753
+ }
3754
+ }
3755
+ function collectPyImports(node, out) {
3756
+ if (node.type === "import_from_statement") {
3757
+ let level = 0;
3758
+ let modulePath = "";
3759
+ let pastFrom = false;
3760
+ for (let i = 0; i < node.childCount; i++) {
3761
+ const child = node.child(i);
3762
+ if (!child) continue;
3763
+ if (!pastFrom) {
3764
+ if (child.type === "from") pastFrom = true;
3765
+ continue;
3766
+ }
3767
+ if (child.type === "import") break;
3768
+ if (child.type === "relative_import") {
3769
+ for (let j = 0; j < child.childCount; j++) {
3770
+ const rc = child.child(j);
3771
+ if (!rc) continue;
3772
+ if (rc.type === "import_prefix") {
3773
+ for (let k = 0; k < rc.childCount; k++) {
3774
+ if (rc.child(k)?.type === ".") level++;
3775
+ }
3776
+ } else if (rc.type === "dotted_name") modulePath = rc.text;
3777
+ }
3778
+ break;
3779
+ }
3780
+ if (child.type === "dotted_name") {
3781
+ modulePath = child.text;
3782
+ break;
3783
+ }
3784
+ }
3785
+ if (level > 0 || modulePath) {
3786
+ out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
3787
+ }
3788
+ }
3789
+ for (let i = 0; i < node.namedChildCount; i++) {
3790
+ const child = node.namedChild(i);
3791
+ if (child) collectPyImports(child, out);
3792
+ }
3793
+ }
3794
+ async function fileExists2(p) {
3795
+ try {
3796
+ await import_node_fs13.promises.access(p);
3797
+ return true;
3798
+ } catch {
3799
+ return false;
3800
+ }
3801
+ }
3802
+ function isWithinServiceDir(candidate, serviceDir) {
3803
+ const rel = import_node_path14.default.relative(serviceDir, candidate);
3804
+ return rel !== "" && !rel.startsWith("..") && !import_node_path14.default.isAbsolute(rel);
3805
+ }
3806
+ var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
3807
+ var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
3808
+ async function firstExistingCandidate(base, serviceDir) {
3809
+ for (const ext of JS_EXTENSIONS) {
3810
+ const candidate = base + ext;
3811
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists2(candidate)) {
3812
+ return toPosix2(import_node_path14.default.relative(serviceDir, candidate));
3813
+ }
3814
+ }
3815
+ for (const indexFile of JS_INDEX_FILES) {
3816
+ const candidate = import_node_path14.default.join(base, indexFile);
3817
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists2(candidate)) {
3818
+ return toPosix2(import_node_path14.default.relative(serviceDir, candidate));
3819
+ }
3820
+ }
3821
+ return null;
3822
+ }
3823
+ async function loadTsPathConfig(serviceDir) {
3824
+ const tsconfigPath = import_node_path14.default.join(serviceDir, "tsconfig.json");
3825
+ let raw;
3826
+ try {
3827
+ raw = await import_node_fs13.promises.readFile(tsconfigPath, "utf8");
3828
+ } catch {
3829
+ return null;
3830
+ }
3831
+ try {
3832
+ const parsed = JSON.parse(raw);
3833
+ const paths = parsed.compilerOptions?.paths;
3834
+ if (!paths || Object.keys(paths).length === 0) return null;
3835
+ const baseUrl = parsed.compilerOptions?.baseUrl;
3836
+ return { paths, baseDir: baseUrl ? import_node_path14.default.resolve(serviceDir, baseUrl) : serviceDir };
3837
+ } catch (err) {
3838
+ recordExtractionError("import alias resolution", tsconfigPath, err);
3839
+ return null;
3840
+ }
3841
+ }
3842
+ async function resolveTsAlias(specifier, config, serviceDir) {
3843
+ for (const [pattern, targets] of Object.entries(config.paths)) {
3844
+ let suffix = null;
3845
+ if (pattern === specifier) {
3846
+ suffix = "";
3847
+ } else if (pattern.endsWith("/*")) {
3848
+ const prefix = pattern.slice(0, -1);
3849
+ if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
3850
+ }
3851
+ if (suffix === null) continue;
3852
+ for (const target of targets) {
3853
+ const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
3854
+ const resolvedBase = import_node_path14.default.resolve(config.baseDir, targetBase, suffix);
3855
+ const hit = await firstExistingCandidate(resolvedBase, serviceDir);
3856
+ if (hit) return hit;
3857
+ if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists2(resolvedBase)) {
3858
+ return toPosix2(import_node_path14.default.relative(serviceDir, resolvedBase));
3859
+ }
3860
+ }
3861
+ }
3862
+ return null;
3863
+ }
3864
+ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
3865
+ if (!specifier) return null;
3866
+ if (specifier.startsWith("./") || specifier.startsWith("../")) {
3867
+ const base = import_node_path14.default.resolve(importerDir, specifier);
3868
+ const ext = import_node_path14.default.extname(specifier);
3869
+ if (ext) {
3870
+ if (ext === ".js" || ext === ".jsx") {
3871
+ const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
3872
+ const tsSibling = base.slice(0, -ext.length) + tsExt;
3873
+ if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists2(tsSibling)) {
3874
+ return toPosix2(import_node_path14.default.relative(serviceDir, tsSibling));
3875
+ }
3876
+ }
3877
+ if (isWithinServiceDir(base, serviceDir) && await fileExists2(base)) {
3878
+ return toPosix2(import_node_path14.default.relative(serviceDir, base));
3879
+ }
3880
+ return null;
3881
+ }
3882
+ return firstExistingCandidate(base, serviceDir);
3883
+ }
3884
+ if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
3885
+ return null;
3886
+ }
3887
+ async function resolvePyImport(imp, importerPath, serviceDir) {
3888
+ if (!imp.modulePath) return null;
3889
+ const relPath = imp.modulePath.split(".").join("/");
3890
+ let baseDir;
3891
+ if (imp.level > 0) {
3892
+ baseDir = import_node_path14.default.dirname(importerPath);
3893
+ for (let i = 1; i < imp.level; i++) baseDir = import_node_path14.default.dirname(baseDir);
3894
+ } else {
3895
+ baseDir = serviceDir;
3896
+ }
3897
+ const candidates = [import_node_path14.default.join(baseDir, `${relPath}.py`), import_node_path14.default.join(baseDir, relPath, "__init__.py")];
3898
+ for (const candidate of candidates) {
3899
+ if (isWithinServiceDir(candidate, serviceDir) && await fileExists2(candidate)) {
3900
+ return toPosix2(import_node_path14.default.relative(serviceDir, candidate));
3901
+ }
3902
+ }
3903
+ return null;
3904
+ }
3905
+ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
3906
+ const importeeFileId = (0, import_types9.fileId)(serviceName, importeeRelPath);
3907
+ if (!graph.hasNode(importeeFileId)) return 0;
3908
+ const edgeId = (0, import_types9.extractedEdgeId)(importerFileId, importeeFileId, import_types9.EdgeType.IMPORTS);
3909
+ if (graph.hasEdge(edgeId)) return 0;
3910
+ const edge = {
3911
+ id: edgeId,
3912
+ source: importerFileId,
3913
+ target: importeeFileId,
3914
+ type: import_types9.EdgeType.IMPORTS,
3915
+ provenance: import_types9.Provenance.EXTRACTED,
3916
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3917
+ evidence: { file: importerRelPath, line, snippet: snippet2 }
3918
+ };
3919
+ graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
3920
+ return 1;
3921
+ }
3922
+ async function addImports(graph, services) {
3923
+ const jsParser = makeJsParser();
3924
+ const pyParser = makePyParser();
3925
+ let edgesAdded = 0;
3926
+ for (const service of services) {
3927
+ const tsPaths = await loadTsPathConfig(service.dir);
3928
+ const files = await loadSourceFiles(service.dir);
3929
+ for (const file of files) {
3930
+ if (isTestPath(file.path)) continue;
3931
+ const relFile = toPosix2(import_node_path14.default.relative(service.dir, file.path));
3932
+ const importerFileId = (0, import_types9.fileId)(service.pkg.name, relFile);
3933
+ const isPython = import_node_path14.default.extname(file.path) === ".py";
3934
+ if (isPython) {
3935
+ let pyImports = [];
3936
+ try {
3937
+ const tree = parseSource(pyParser, file.content);
3938
+ collectPyImports(tree.rootNode, pyImports);
3939
+ } catch (err) {
3940
+ recordExtractionError("import extraction", file.path, err);
3941
+ continue;
3942
+ }
3943
+ for (const imp of pyImports) {
3944
+ const resolved = await resolvePyImport(imp, file.path, service.dir);
3945
+ if (!resolved) continue;
3946
+ edgesAdded += emitImportEdge(
3947
+ graph,
3948
+ service.pkg.name,
3949
+ importerFileId,
3950
+ relFile,
3951
+ resolved,
3952
+ imp.line,
3953
+ imp.snippet
3954
+ );
3955
+ }
3956
+ continue;
3957
+ }
3958
+ let jsImports = [];
3959
+ try {
3960
+ const tree = parseSource(jsParser, file.content);
3961
+ collectJsImports(tree.rootNode, jsImports);
3962
+ } catch (err) {
3963
+ recordExtractionError("import extraction", file.path, err);
3964
+ continue;
3965
+ }
3966
+ for (const imp of jsImports) {
3967
+ const resolved = await resolveJsImport(imp.specifier, import_node_path14.default.dirname(file.path), service.dir, tsPaths);
3968
+ if (!resolved) continue;
3969
+ edgesAdded += emitImportEdge(
3970
+ graph,
3971
+ service.pkg.name,
3972
+ importerFileId,
3973
+ relFile,
3974
+ resolved,
3975
+ imp.line,
3976
+ imp.snippet
3977
+ );
3978
+ }
3979
+ }
3980
+ }
3981
+ return { nodesAdded: 0, edgesAdded };
3982
+ }
3983
+
3984
+ // src/extract/databases/index.ts
3985
+ init_cjs_shims();
3986
+ var import_node_path22 = __toESM(require("path"), 1);
3987
+ var import_types10 = require("@neat.is/types");
3988
+
3989
+ // src/extract/databases/db-config-yaml.ts
3990
+ init_cjs_shims();
3991
+ var import_node_path15 = __toESM(require("path"), 1);
3992
+ async function parse(serviceDir) {
3993
+ const yamlPath = import_node_path15.default.join(serviceDir, "db-config.yaml");
3994
+ if (!await exists2(yamlPath)) return [];
3995
+ const raw = await readYaml(yamlPath);
3996
+ return [
3997
+ {
3998
+ host: raw.host,
3999
+ port: raw.port,
4000
+ database: raw.database,
4001
+ engine: raw.engine,
4002
+ engineVersion: raw.engineVersion !== void 0 ? String(raw.engineVersion) : "unknown",
4003
+ sourceFile: yamlPath
4004
+ }
4005
+ ];
4006
+ }
4007
+ var dbConfigYamlParser = { name: "db-config.yaml", parse };
4008
+
4009
+ // src/extract/databases/dotenv.ts
4010
+ init_cjs_shims();
4011
+ var import_node_fs15 = require("fs");
4012
+ var import_node_path17 = __toESM(require("path"), 1);
4013
+
4014
+ // src/extract/databases/shared.ts
4015
+ init_cjs_shims();
4016
+ var import_node_fs14 = require("fs");
4017
+ var import_node_path16 = __toESM(require("path"), 1);
4018
+ function schemeToEngine(scheme) {
4019
+ const s = scheme.toLowerCase().split("+")[0];
4020
+ switch (s) {
4021
+ case "postgres":
4022
+ case "postgresql":
4023
+ return "postgresql";
4024
+ case "mysql":
4025
+ case "mariadb":
4026
+ return "mysql";
4027
+ case "mongodb":
4028
+ case "mongodb+srv":
4029
+ return "mongodb";
4030
+ case "redis":
4031
+ case "rediss":
4032
+ return "redis";
4033
+ case "sqlite":
4034
+ return "sqlite";
4035
+ default:
4036
+ return null;
4037
+ }
4038
+ }
4039
+ function parseConnectionString(url) {
4040
+ const m = url.match(
4041
+ /^(?<scheme>[a-z][a-z+]*):\/\/(?:[^@/]+(?::[^@]*)?@)?(?<host>[^:/?]+)(?::(?<port>\d+))?(?:\/(?<db>[^?#]*))?/i
4042
+ );
4043
+ if (!m || !m.groups) return null;
4044
+ const engine = schemeToEngine(m.groups.scheme);
4045
+ if (!engine) return null;
4046
+ return {
4047
+ host: m.groups.host,
4048
+ port: m.groups.port ? Number(m.groups.port) : void 0,
4049
+ database: m.groups.db ?? "",
4050
+ engine,
4051
+ engineVersion: "unknown"
4052
+ };
4053
+ }
4054
+ async function readIfExists(filePath) {
4055
+ try {
4056
+ return await import_node_fs14.promises.readFile(filePath, "utf8");
4057
+ } catch {
4058
+ return null;
4059
+ }
4060
+ }
4061
+ async function findFirst(serviceDir, candidates) {
4062
+ for (const rel of candidates) {
4063
+ const abs = import_node_path16.default.join(serviceDir, rel);
4064
+ const content = await readIfExists(abs);
4065
+ if (content !== null) return abs;
4066
+ }
4067
+ return null;
4068
+ }
4069
+ function engineFromImage(image) {
4070
+ const lower = image.toLowerCase();
4071
+ const colon = lower.lastIndexOf(":");
4072
+ const repo = colon >= 0 ? lower.slice(0, colon) : lower;
4073
+ const tag = colon >= 0 ? lower.slice(colon + 1) : "latest";
4074
+ const last = repo.split("/").pop() ?? repo;
4075
+ let engine = null;
4076
+ if (last.startsWith("postgres")) engine = "postgresql";
4077
+ else if (last.startsWith("mysql") || last.startsWith("mariadb")) engine = "mysql";
4078
+ else if (last.startsWith("mongo")) engine = "mongodb";
4079
+ else if (last.startsWith("redis")) engine = "redis";
4080
+ else if (last.startsWith("sqlite")) engine = "sqlite";
4081
+ if (!engine) return null;
4082
+ const versionMatch = tag.match(/^(\d+(?:\.\d+){0,2})/);
3344
4083
  return {
3345
4084
  engine,
3346
4085
  engineVersion: versionMatch ? versionMatch[1] : "unknown"
@@ -3372,15 +4111,15 @@ function parseDotenvLine(line) {
3372
4111
  return { key, value };
3373
4112
  }
3374
4113
  async function parse2(serviceDir) {
3375
- const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
4114
+ const entries = await import_node_fs15.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3376
4115
  const configs = [];
3377
4116
  const seen = /* @__PURE__ */ new Set();
3378
4117
  for (const entry of entries) {
3379
4118
  if (!entry.isFile()) continue;
3380
4119
  const match = isConfigFile(entry.name);
3381
4120
  if (!match.match || match.fileType !== "env") continue;
3382
- const filePath = import_node_path12.default.join(serviceDir, entry.name);
3383
- const content = await import_node_fs11.promises.readFile(filePath, "utf8");
4121
+ const filePath = import_node_path17.default.join(serviceDir, entry.name);
4122
+ const content = await import_node_fs15.promises.readFile(filePath, "utf8");
3384
4123
  for (const line of content.split("\n")) {
3385
4124
  const parsed = parseDotenvLine(line);
3386
4125
  if (!parsed) continue;
@@ -3399,9 +4138,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3399
4138
 
3400
4139
  // src/extract/databases/prisma.ts
3401
4140
  init_cjs_shims();
3402
- var import_node_path13 = __toESM(require("path"), 1);
4141
+ var import_node_path18 = __toESM(require("path"), 1);
3403
4142
  async function parse3(serviceDir) {
3404
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
4143
+ const schemaPath = import_node_path18.default.join(serviceDir, "prisma", "schema.prisma");
3405
4144
  const content = await readIfExists(schemaPath);
3406
4145
  if (!content) return [];
3407
4146
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3533,11 +4272,11 @@ var knexParser = { name: "knex", parse: parse5 };
3533
4272
 
3534
4273
  // src/extract/databases/ormconfig.ts
3535
4274
  init_cjs_shims();
3536
- var import_node_path14 = __toESM(require("path"), 1);
4275
+ var import_node_path19 = __toESM(require("path"), 1);
3537
4276
  async function parse6(serviceDir) {
3538
4277
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
3539
- const abs = import_node_path14.default.join(serviceDir, candidate);
3540
- if (!await exists(abs)) continue;
4278
+ const abs = import_node_path19.default.join(serviceDir, candidate);
4279
+ if (!await exists2(abs)) continue;
3541
4280
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3542
4281
  const entries = Array.isArray(raw) ? raw : [raw];
3543
4282
  const out = [];
@@ -3596,10 +4335,10 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3596
4335
 
3597
4336
  // src/extract/databases/sequelize.ts
3598
4337
  init_cjs_shims();
3599
- var import_node_path15 = __toESM(require("path"), 1);
4338
+ var import_node_path20 = __toESM(require("path"), 1);
3600
4339
  async function parse8(serviceDir) {
3601
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3602
- if (!await exists(configPath)) return [];
4340
+ const configPath = import_node_path20.default.join(serviceDir, "config", "config.json");
4341
+ if (!await exists2(configPath)) return [];
3603
4342
  const raw = await readJson(configPath);
3604
4343
  const out = [];
3605
4344
  const seen = /* @__PURE__ */ new Set();
@@ -3625,7 +4364,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3625
4364
 
3626
4365
  // src/extract/databases/docker-compose.ts
3627
4366
  init_cjs_shims();
3628
- var import_node_path16 = __toESM(require("path"), 1);
4367
+ var import_node_path21 = __toESM(require("path"), 1);
3629
4368
  function portFromService(svc) {
3630
4369
  for (const raw of svc.ports ?? []) {
3631
4370
  const str = String(raw);
@@ -3652,8 +4391,8 @@ function databaseFromEnv(svc) {
3652
4391
  }
3653
4392
  async function parse9(serviceDir) {
3654
4393
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3655
- const abs = import_node_path16.default.join(serviceDir, name);
3656
- if (!await exists(abs)) continue;
4394
+ const abs = import_node_path21.default.join(serviceDir, name);
4395
+ if (!await exists2(abs)) continue;
3657
4396
  const raw = await readYaml(abs);
3658
4397
  if (!raw?.services) return [];
3659
4398
  const out = [];
@@ -3693,8 +4432,8 @@ function compatibleDriversFor(engine) {
3693
4432
  }
3694
4433
  function toDatabaseNode(config) {
3695
4434
  return {
3696
- id: (0, import_types8.databaseId)(config.host),
3697
- type: import_types8.NodeType.DatabaseNode,
4435
+ id: (0, import_types10.databaseId)(config.host),
4436
+ type: import_types10.NodeType.DatabaseNode,
3698
4437
  name: config.database || config.host,
3699
4438
  engine: config.engine,
3700
4439
  engineVersion: config.engineVersion,
@@ -3824,19 +4563,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3824
4563
  discoveredVia: mergedDiscoveredVia
3825
4564
  });
3826
4565
  }
4566
+ const relConfigFile = toPosix2(import_node_path22.default.relative(service.dir, config.sourceFile));
4567
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4568
+ graph,
4569
+ service.pkg.name,
4570
+ service.node.id,
4571
+ relConfigFile
4572
+ );
4573
+ nodesAdded += fn;
4574
+ edgesAdded += fe;
4575
+ const evidenceFile = toPosix2(import_node_path22.default.relative(scanPath, config.sourceFile));
3827
4576
  const edge = {
3828
- id: (0, import_types5.extractedEdgeId)(service.node.id, dbNode.id, import_types8.EdgeType.CONNECTS_TO),
3829
- source: service.node.id,
4577
+ id: (0, import_types5.extractedEdgeId)(fileNodeId, dbNode.id, import_types10.EdgeType.CONNECTS_TO),
4578
+ source: fileNodeId,
3830
4579
  target: dbNode.id,
3831
- type: import_types8.EdgeType.CONNECTS_TO,
3832
- provenance: import_types8.Provenance.EXTRACTED,
3833
- confidence: (0, import_types8.confidenceForExtracted)("structural"),
3834
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3835
- // Ghost-edge cleanup keys retirement on this; the conditional
3836
- // sourceFile spread that used to live here was a v0.1.x leftover.
3837
- evidence: {
3838
- file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3839
- }
4580
+ type: import_types10.EdgeType.CONNECTS_TO,
4581
+ provenance: import_types10.Provenance.EXTRACTED,
4582
+ confidence: (0, import_types10.confidenceForExtracted)("structural"),
4583
+ evidence: { file: evidenceFile }
3840
4584
  };
3841
4585
  if (!graph.hasEdge(edge.id)) {
3842
4586
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3862,15 +4606,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3862
4606
 
3863
4607
  // src/extract/configs.ts
3864
4608
  init_cjs_shims();
3865
- var import_node_fs12 = require("fs");
3866
- var import_node_path18 = __toESM(require("path"), 1);
3867
- var import_types9 = require("@neat.is/types");
4609
+ var import_node_fs16 = require("fs");
4610
+ var import_node_path23 = __toESM(require("path"), 1);
4611
+ var import_types11 = require("@neat.is/types");
3868
4612
  async function walkConfigFiles(dir) {
3869
4613
  const out = [];
3870
4614
  async function walk(current) {
3871
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
4615
+ const entries = await import_node_fs16.promises.readdir(current, { withFileTypes: true });
3872
4616
  for (const entry of entries) {
3873
- const full = import_node_path18.default.join(current, entry.name);
4617
+ const full = import_node_path23.default.join(current, entry.name);
3874
4618
  if (entry.isDirectory()) {
3875
4619
  if (IGNORED_DIRS.has(entry.name)) continue;
3876
4620
  if (await isPythonVenvDir(full)) continue;
@@ -3889,26 +4633,35 @@ async function addConfigNodes(graph, services, scanPath) {
3889
4633
  for (const service of services) {
3890
4634
  const configFiles = await walkConfigFiles(service.dir);
3891
4635
  for (const file of configFiles) {
3892
- const relPath = import_node_path18.default.relative(scanPath, file);
4636
+ const relPath = import_node_path23.default.relative(scanPath, file);
3893
4637
  const node = {
3894
- id: (0, import_types9.configId)(relPath),
3895
- type: import_types9.NodeType.ConfigNode,
3896
- name: import_node_path18.default.basename(file),
4638
+ id: (0, import_types11.configId)(relPath),
4639
+ type: import_types11.NodeType.ConfigNode,
4640
+ name: import_node_path23.default.basename(file),
3897
4641
  path: relPath,
3898
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
4642
+ fileType: isConfigFile(import_node_path23.default.basename(file)).fileType
3899
4643
  };
3900
4644
  if (!graph.hasNode(node.id)) {
3901
4645
  graph.addNode(node.id, node);
3902
4646
  nodesAdded++;
3903
4647
  }
4648
+ const relToService = toPosix2(import_node_path23.default.relative(service.dir, file));
4649
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4650
+ graph,
4651
+ service.pkg.name,
4652
+ service.node.id,
4653
+ relToService
4654
+ );
4655
+ nodesAdded += fn;
4656
+ edgesAdded += fe;
3904
4657
  const edge = {
3905
- id: (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types9.EdgeType.CONFIGURED_BY),
3906
- source: service.node.id,
4658
+ id: (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types11.EdgeType.CONFIGURED_BY),
4659
+ source: fileNodeId,
3907
4660
  target: node.id,
3908
- type: import_types9.EdgeType.CONFIGURED_BY,
3909
- provenance: import_types9.Provenance.EXTRACTED,
3910
- confidence: (0, import_types9.confidenceForExtracted)("structural"),
3911
- evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
4661
+ type: import_types11.EdgeType.CONFIGURED_BY,
4662
+ provenance: import_types11.Provenance.EXTRACTED,
4663
+ confidence: (0, import_types11.confidenceForExtracted)("structural"),
4664
+ evidence: { file: relPath.split(import_node_path23.default.sep).join("/") }
3912
4665
  };
3913
4666
  if (!graph.hasEdge(edge.id)) {
3914
4667
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3921,114 +4674,15 @@ async function addConfigNodes(graph, services, scanPath) {
3921
4674
 
3922
4675
  // src/extract/calls/index.ts
3923
4676
  init_cjs_shims();
3924
- var import_types16 = require("@neat.is/types");
4677
+ var import_types17 = require("@neat.is/types");
3925
4678
 
3926
4679
  // src/extract/calls/http.ts
3927
4680
  init_cjs_shims();
3928
- var import_node_path20 = __toESM(require("path"), 1);
3929
- var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3930
- var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3931
- var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3932
- var import_types11 = require("@neat.is/types");
3933
-
3934
- // src/extract/calls/shared.ts
3935
- init_cjs_shims();
3936
- var import_node_fs13 = require("fs");
3937
- var import_node_path19 = __toESM(require("path"), 1);
3938
- var import_types10 = require("@neat.is/types");
3939
- async function walkSourceFiles(dir) {
3940
- const out = [];
3941
- async function walk(current) {
3942
- const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3943
- for (const entry of entries) {
3944
- const full = import_node_path19.default.join(current, entry.name);
3945
- if (entry.isDirectory()) {
3946
- if (IGNORED_DIRS.has(entry.name)) continue;
3947
- if (await isPythonVenvDir(full)) continue;
3948
- await walk(full);
3949
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3950
- out.push(full);
3951
- }
3952
- }
3953
- }
3954
- await walk(dir);
3955
- return out;
3956
- }
3957
- async function loadSourceFiles(dir) {
3958
- const paths = await walkSourceFiles(dir);
3959
- const out = [];
3960
- for (const p of paths) {
3961
- try {
3962
- const content = await import_node_fs13.promises.readFile(p, "utf8");
3963
- out.push({ path: p, content });
3964
- } catch {
3965
- }
3966
- }
3967
- return out;
3968
- }
3969
- function lineOf(text, needle) {
3970
- const idx = text.indexOf(needle);
3971
- if (idx < 0) return 1;
3972
- return text.slice(0, idx).split("\n").length;
3973
- }
3974
- function snippet(text, line) {
3975
- const lines = text.split("\n");
3976
- return (lines[line - 1] ?? "").trim();
3977
- }
3978
- function toPosix2(p) {
3979
- return p.split("\\").join("/");
3980
- }
3981
- function languageForPath(relPath) {
3982
- switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3983
- case ".py":
3984
- return "python";
3985
- case ".ts":
3986
- case ".tsx":
3987
- return "typescript";
3988
- case ".js":
3989
- case ".jsx":
3990
- case ".mjs":
3991
- case ".cjs":
3992
- return "javascript";
3993
- default:
3994
- return void 0;
3995
- }
3996
- }
3997
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3998
- let nodesAdded = 0;
3999
- let edgesAdded = 0;
4000
- const fileNodeId = (0, import_types10.fileId)(serviceName, relPath);
4001
- if (!graph.hasNode(fileNodeId)) {
4002
- const language = languageForPath(relPath);
4003
- const node = {
4004
- id: fileNodeId,
4005
- type: import_types10.NodeType.FileNode,
4006
- service: serviceName,
4007
- path: relPath,
4008
- ...language ? { language } : {},
4009
- discoveredVia: "static"
4010
- };
4011
- graph.addNode(fileNodeId, node);
4012
- nodesAdded++;
4013
- }
4014
- const containsId = (0, import_types10.extractedEdgeId)(serviceNodeId, fileNodeId, import_types10.EdgeType.CONTAINS);
4015
- if (!graph.hasEdge(containsId)) {
4016
- const edge = {
4017
- id: containsId,
4018
- source: serviceNodeId,
4019
- target: fileNodeId,
4020
- type: import_types10.EdgeType.CONTAINS,
4021
- provenance: import_types10.Provenance.EXTRACTED,
4022
- confidence: (0, import_types10.confidenceForExtracted)("structural"),
4023
- evidence: { file: relPath }
4024
- };
4025
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
4026
- edgesAdded++;
4027
- }
4028
- return { fileNodeId, nodesAdded, edgesAdded };
4029
- }
4030
-
4031
- // src/extract/calls/http.ts
4681
+ var import_node_path24 = __toESM(require("path"), 1);
4682
+ var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
4683
+ var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
4684
+ var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
4685
+ var import_types12 = require("@neat.is/types");
4032
4686
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
4033
4687
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
4034
4688
  function isInsideJsxExternalLink(node) {
@@ -4056,14 +4710,14 @@ function collectStringLiterals(node, out) {
4056
4710
  if (child) collectStringLiterals(child, out);
4057
4711
  }
4058
4712
  }
4059
- var PARSE_CHUNK = 16384;
4060
- function parseSource(parser, source) {
4713
+ var PARSE_CHUNK2 = 16384;
4714
+ function parseSource2(parser, source) {
4061
4715
  return parser.parse(
4062
- (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
4716
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
4063
4717
  );
4064
4718
  }
4065
4719
  function callsFromSource(source, parser, knownHosts) {
4066
- const tree = parseSource(parser, source);
4720
+ const tree = parseSource2(parser, source);
4067
4721
  const literals = [];
4068
4722
  collectStringLiterals(tree.rootNode, literals);
4069
4723
  const out = [];
@@ -4077,25 +4731,25 @@ function callsFromSource(source, parser, knownHosts) {
4077
4731
  }
4078
4732
  return out;
4079
4733
  }
4080
- function makeJsParser() {
4081
- const p = new import_tree_sitter.default();
4082
- p.setLanguage(import_tree_sitter_javascript.default);
4734
+ function makeJsParser2() {
4735
+ const p = new import_tree_sitter2.default();
4736
+ p.setLanguage(import_tree_sitter_javascript2.default);
4083
4737
  return p;
4084
4738
  }
4085
- function makePyParser() {
4086
- const p = new import_tree_sitter.default();
4087
- p.setLanguage(import_tree_sitter_python.default);
4739
+ function makePyParser2() {
4740
+ const p = new import_tree_sitter2.default();
4741
+ p.setLanguage(import_tree_sitter_python2.default);
4088
4742
  return p;
4089
4743
  }
4090
4744
  async function addHttpCallEdges(graph, services) {
4091
- const jsParser = makeJsParser();
4092
- const pyParser = makePyParser();
4745
+ const jsParser = makeJsParser2();
4746
+ const pyParser = makePyParser2();
4093
4747
  const knownHosts = /* @__PURE__ */ new Set();
4094
4748
  const hostToNodeId = /* @__PURE__ */ new Map();
4095
4749
  for (const service of services) {
4096
- knownHosts.add(import_node_path20.default.basename(service.dir));
4750
+ knownHosts.add(import_node_path24.default.basename(service.dir));
4097
4751
  knownHosts.add(service.pkg.name);
4098
- hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
4752
+ hostToNodeId.set(import_node_path24.default.basename(service.dir), service.node.id);
4099
4753
  hostToNodeId.set(service.pkg.name, service.node.id);
4100
4754
  }
4101
4755
  let nodesAdded = 0;
@@ -4105,7 +4759,7 @@ async function addHttpCallEdges(graph, services) {
4105
4759
  const seen = /* @__PURE__ */ new Set();
4106
4760
  for (const file of files) {
4107
4761
  if (isTestPath(file.path)) continue;
4108
- const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
4762
+ const parser = import_node_path24.default.extname(file.path) === ".py" ? pyParser : jsParser;
4109
4763
  let sites;
4110
4764
  try {
4111
4765
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -4114,14 +4768,14 @@ async function addHttpCallEdges(graph, services) {
4114
4768
  continue;
4115
4769
  }
4116
4770
  if (sites.length === 0) continue;
4117
- const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
4771
+ const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
4118
4772
  for (const site of sites) {
4119
4773
  const targetId = hostToNodeId.get(site.host);
4120
4774
  if (!targetId || targetId === service.node.id) continue;
4121
4775
  const dedupKey = `${relFile}|${targetId}`;
4122
4776
  if (seen.has(dedupKey)) continue;
4123
4777
  seen.add(dedupKey);
4124
- const confidence = (0, import_types11.confidenceForExtracted)("hostname-shape-match");
4778
+ const confidence = (0, import_types12.confidenceForExtracted)("hostname-shape-match");
4125
4779
  const ev = {
4126
4780
  file: relFile,
4127
4781
  line: site.line,
@@ -4135,25 +4789,25 @@ async function addHttpCallEdges(graph, services) {
4135
4789
  );
4136
4790
  nodesAdded += n;
4137
4791
  edgesAdded += e;
4138
- if (!(0, import_types11.passesExtractedFloor)(confidence)) {
4792
+ if (!(0, import_types12.passesExtractedFloor)(confidence)) {
4139
4793
  noteExtractedDropped({
4140
4794
  source: fileNodeId,
4141
4795
  target: targetId,
4142
- type: import_types11.EdgeType.CALLS,
4796
+ type: import_types12.EdgeType.CALLS,
4143
4797
  confidence,
4144
4798
  confidenceKind: "hostname-shape-match",
4145
4799
  evidence: ev
4146
4800
  });
4147
4801
  continue;
4148
4802
  }
4149
- const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, targetId, import_types11.EdgeType.CALLS);
4803
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, targetId, import_types12.EdgeType.CALLS);
4150
4804
  if (!graph.hasEdge(edgeId)) {
4151
4805
  const edge = {
4152
4806
  id: edgeId,
4153
4807
  source: fileNodeId,
4154
4808
  target: targetId,
4155
- type: import_types11.EdgeType.CALLS,
4156
- provenance: import_types11.Provenance.EXTRACTED,
4809
+ type: import_types12.EdgeType.CALLS,
4810
+ provenance: import_types12.Provenance.EXTRACTED,
4157
4811
  confidence,
4158
4812
  evidence: ev
4159
4813
  };
@@ -4168,8 +4822,8 @@ async function addHttpCallEdges(graph, services) {
4168
4822
 
4169
4823
  // src/extract/calls/kafka.ts
4170
4824
  init_cjs_shims();
4171
- var import_node_path21 = __toESM(require("path"), 1);
4172
- var import_types12 = require("@neat.is/types");
4825
+ var import_node_path25 = __toESM(require("path"), 1);
4826
+ var import_types13 = require("@neat.is/types");
4173
4827
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
4174
4828
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
4175
4829
  function findAll(re, text) {
@@ -4190,7 +4844,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4190
4844
  seen.add(key);
4191
4845
  const line = lineOf(file.content, topic);
4192
4846
  out.push({
4193
- infraId: (0, import_types12.infraId)("kafka-topic", topic),
4847
+ infraId: (0, import_types13.infraId)("kafka-topic", topic),
4194
4848
  name: topic,
4195
4849
  kind: "kafka-topic",
4196
4850
  edgeType,
@@ -4199,7 +4853,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4199
4853
  // tier (ADR-066).
4200
4854
  confidenceKind: "verified-call-site",
4201
4855
  evidence: {
4202
- file: import_node_path21.default.relative(serviceDir, file.path),
4856
+ file: import_node_path25.default.relative(serviceDir, file.path),
4203
4857
  line,
4204
4858
  snippet: snippet(file.content, line)
4205
4859
  }
@@ -4212,8 +4866,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4212
4866
 
4213
4867
  // src/extract/calls/redis.ts
4214
4868
  init_cjs_shims();
4215
- var import_node_path22 = __toESM(require("path"), 1);
4216
- var import_types13 = require("@neat.is/types");
4869
+ var import_node_path26 = __toESM(require("path"), 1);
4870
+ var import_types14 = require("@neat.is/types");
4217
4871
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
4218
4872
  function redisEndpointsFromFile(file, serviceDir) {
4219
4873
  const out = [];
@@ -4226,7 +4880,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4226
4880
  seen.add(host);
4227
4881
  const line = lineOf(file.content, host);
4228
4882
  out.push({
4229
- infraId: (0, import_types13.infraId)("redis", host),
4883
+ infraId: (0, import_types14.infraId)("redis", host),
4230
4884
  name: host,
4231
4885
  kind: "redis",
4232
4886
  edgeType: "CALLS",
@@ -4235,7 +4889,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4235
4889
  // support tier (ADR-066).
4236
4890
  confidenceKind: "url-with-structural-support",
4237
4891
  evidence: {
4238
- file: import_node_path22.default.relative(serviceDir, file.path),
4892
+ file: import_node_path26.default.relative(serviceDir, file.path),
4239
4893
  line,
4240
4894
  snippet: snippet(file.content, line)
4241
4895
  }
@@ -4246,8 +4900,8 @@ function redisEndpointsFromFile(file, serviceDir) {
4246
4900
 
4247
4901
  // src/extract/calls/aws.ts
4248
4902
  init_cjs_shims();
4249
- var import_node_path23 = __toESM(require("path"), 1);
4250
- var import_types14 = require("@neat.is/types");
4903
+ var import_node_path27 = __toESM(require("path"), 1);
4904
+ var import_types15 = require("@neat.is/types");
4251
4905
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
4252
4906
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
4253
4907
  function hasMarker(text, markers) {
@@ -4271,7 +4925,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4271
4925
  seen.add(key);
4272
4926
  const line = lineOf(file.content, name);
4273
4927
  out.push({
4274
- infraId: (0, import_types14.infraId)(kind, name),
4928
+ infraId: (0, import_types15.infraId)(kind, name),
4275
4929
  name,
4276
4930
  kind,
4277
4931
  edgeType: "CALLS",
@@ -4280,7 +4934,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4280
4934
  // (ADR-066).
4281
4935
  confidenceKind: "verified-call-site",
4282
4936
  evidence: {
4283
- file: import_node_path23.default.relative(serviceDir, file.path),
4937
+ file: import_node_path27.default.relative(serviceDir, file.path),
4284
4938
  line,
4285
4939
  snippet: snippet(file.content, line)
4286
4940
  }
@@ -4305,13 +4959,14 @@ function awsEndpointsFromFile(file, serviceDir) {
4305
4959
 
4306
4960
  // src/extract/calls/grpc.ts
4307
4961
  init_cjs_shims();
4308
- var import_node_path24 = __toESM(require("path"), 1);
4309
- var import_types15 = require("@neat.is/types");
4962
+ var import_node_path28 = __toESM(require("path"), 1);
4963
+ var import_types16 = require("@neat.is/types");
4310
4964
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
4311
4965
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
4312
4966
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
4313
4967
  function isLikelyAddress(value) {
4314
4968
  if (!value) return false;
4969
+ if (/\s/.test(value) || value.startsWith("{")) return false;
4315
4970
  return /:\d{2,5}$/.test(value) || value.includes(".");
4316
4971
  }
4317
4972
  function normaliseForMatch(s) {
@@ -4354,7 +5009,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4354
5009
  const { kind } = classified;
4355
5010
  const line = lineOf(file.content, m[0]);
4356
5011
  out.push({
4357
- infraId: (0, import_types15.infraId)(kind, name),
5012
+ infraId: (0, import_types16.infraId)(kind, name),
4358
5013
  name,
4359
5014
  kind,
4360
5015
  edgeType: "CALLS",
@@ -4363,7 +5018,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4363
5018
  // tier (ADR-066).
4364
5019
  confidenceKind: "verified-call-site",
4365
5020
  evidence: {
4366
- file: import_node_path24.default.relative(serviceDir, file.path),
5021
+ file: import_node_path28.default.relative(serviceDir, file.path),
4367
5022
  line,
4368
5023
  snippet: snippet(file.content, line)
4369
5024
  }
@@ -4376,11 +5031,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
4376
5031
  function edgeTypeFromEndpoint(ep) {
4377
5032
  switch (ep.edgeType) {
4378
5033
  case "PUBLISHES_TO":
4379
- return import_types16.EdgeType.PUBLISHES_TO;
5034
+ return import_types17.EdgeType.PUBLISHES_TO;
4380
5035
  case "CONSUMES_FROM":
4381
- return import_types16.EdgeType.CONSUMES_FROM;
5036
+ return import_types17.EdgeType.CONSUMES_FROM;
4382
5037
  default:
4383
- return import_types16.EdgeType.CALLS;
5038
+ return import_types17.EdgeType.CALLS;
4384
5039
  }
4385
5040
  }
4386
5041
  function isAwsKind(kind) {
@@ -4407,7 +5062,7 @@ async function addExternalEndpointEdges(graph, services) {
4407
5062
  if (!graph.hasNode(ep.infraId)) {
4408
5063
  const node = {
4409
5064
  id: ep.infraId,
4410
- type: import_types16.NodeType.InfraNode,
5065
+ type: import_types17.NodeType.InfraNode,
4411
5066
  name: ep.name,
4412
5067
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4413
5068
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4419,7 +5074,7 @@ async function addExternalEndpointEdges(graph, services) {
4419
5074
  nodesAdded++;
4420
5075
  }
4421
5076
  const edgeType = edgeTypeFromEndpoint(ep);
4422
- const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
5077
+ const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
4423
5078
  const relFile = toPosix2(ep.evidence.file);
4424
5079
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4425
5080
  graph,
@@ -4429,7 +5084,7 @@ async function addExternalEndpointEdges(graph, services) {
4429
5084
  );
4430
5085
  nodesAdded += n;
4431
5086
  edgesAdded += e;
4432
- if (!(0, import_types16.passesExtractedFloor)(confidence)) {
5087
+ if (!(0, import_types17.passesExtractedFloor)(confidence)) {
4433
5088
  noteExtractedDropped({
4434
5089
  source: fileNodeId,
4435
5090
  target: ep.infraId,
@@ -4449,7 +5104,7 @@ async function addExternalEndpointEdges(graph, services) {
4449
5104
  source: fileNodeId,
4450
5105
  target: ep.infraId,
4451
5106
  type: edgeType,
4452
- provenance: import_types16.Provenance.EXTRACTED,
5107
+ provenance: import_types17.Provenance.EXTRACTED,
4453
5108
  confidence,
4454
5109
  evidence: ep.evidence
4455
5110
  };
@@ -4474,16 +5129,16 @@ init_cjs_shims();
4474
5129
 
4475
5130
  // src/extract/infra/docker-compose.ts
4476
5131
  init_cjs_shims();
4477
- var import_node_path25 = __toESM(require("path"), 1);
4478
- var import_types18 = require("@neat.is/types");
5132
+ var import_node_path29 = __toESM(require("path"), 1);
5133
+ var import_types19 = require("@neat.is/types");
4479
5134
 
4480
5135
  // src/extract/infra/shared.ts
4481
5136
  init_cjs_shims();
4482
- var import_types17 = require("@neat.is/types");
5137
+ var import_types18 = require("@neat.is/types");
4483
5138
  function makeInfraNode(kind, name, provider = "self", extras) {
4484
5139
  return {
4485
- id: (0, import_types17.infraId)(kind, name),
4486
- type: import_types17.NodeType.InfraNode,
5140
+ id: (0, import_types18.infraId)(kind, name),
5141
+ type: import_types18.NodeType.InfraNode,
4487
5142
  name,
4488
5143
  provider,
4489
5144
  kind,
@@ -4512,7 +5167,7 @@ function dependsOnList(value) {
4512
5167
  }
4513
5168
  function serviceNameToServiceNode(name, services) {
4514
5169
  for (const s of services) {
4515
- if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
5170
+ if (s.node.name === name || import_node_path29.default.basename(s.dir) === name) return s.node.id;
4516
5171
  }
4517
5172
  return null;
4518
5173
  }
@@ -4521,8 +5176,8 @@ async function addComposeInfra(graph, scanPath, services) {
4521
5176
  let edgesAdded = 0;
4522
5177
  let composePath = null;
4523
5178
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4524
- const abs = import_node_path25.default.join(scanPath, name);
4525
- if (await exists(abs)) {
5179
+ const abs = import_node_path29.default.join(scanPath, name);
5180
+ if (await exists2(abs)) {
4526
5181
  composePath = abs;
4527
5182
  break;
4528
5183
  }
@@ -4534,13 +5189,13 @@ async function addComposeInfra(graph, scanPath, services) {
4534
5189
  } catch (err) {
4535
5190
  recordExtractionError(
4536
5191
  "infra docker-compose",
4537
- import_node_path25.default.relative(scanPath, composePath),
5192
+ import_node_path29.default.relative(scanPath, composePath),
4538
5193
  err
4539
5194
  );
4540
5195
  return { nodesAdded, edgesAdded };
4541
5196
  }
4542
5197
  if (!compose?.services) return { nodesAdded, edgesAdded };
4543
- const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
5198
+ const evidenceFile = import_node_path29.default.relative(scanPath, composePath).split(import_node_path29.default.sep).join("/");
4544
5199
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4545
5200
  for (const [composeName, svc] of Object.entries(compose.services)) {
4546
5201
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4562,15 +5217,15 @@ async function addComposeInfra(graph, scanPath, services) {
4562
5217
  for (const dep of dependsOnList(svc.depends_on)) {
4563
5218
  const targetId = composeNameToNodeId.get(dep);
4564
5219
  if (!targetId) continue;
4565
- const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
5220
+ const edgeId = (0, import_types5.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
4566
5221
  if (graph.hasEdge(edgeId)) continue;
4567
5222
  const edge = {
4568
5223
  id: edgeId,
4569
5224
  source: sourceId,
4570
5225
  target: targetId,
4571
- type: import_types18.EdgeType.DEPENDS_ON,
4572
- provenance: import_types18.Provenance.EXTRACTED,
4573
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
5226
+ type: import_types19.EdgeType.DEPENDS_ON,
5227
+ provenance: import_types19.Provenance.EXTRACTED,
5228
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4574
5229
  evidence: { file: evidenceFile }
4575
5230
  };
4576
5231
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4582,9 +5237,9 @@ async function addComposeInfra(graph, scanPath, services) {
4582
5237
 
4583
5238
  // src/extract/infra/dockerfile.ts
4584
5239
  init_cjs_shims();
4585
- var import_node_path26 = __toESM(require("path"), 1);
4586
- var import_node_fs14 = require("fs");
4587
- var import_types19 = require("@neat.is/types");
5240
+ var import_node_path30 = __toESM(require("path"), 1);
5241
+ var import_node_fs17 = require("fs");
5242
+ var import_types20 = require("@neat.is/types");
4588
5243
  function runtimeImage(content) {
4589
5244
  const lines = content.split("\n");
4590
5245
  let last = null;
@@ -4603,15 +5258,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4603
5258
  let nodesAdded = 0;
4604
5259
  let edgesAdded = 0;
4605
5260
  for (const service of services) {
4606
- const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
4607
- if (!await exists(dockerfilePath)) continue;
5261
+ const dockerfilePath = import_node_path30.default.join(service.dir, "Dockerfile");
5262
+ if (!await exists2(dockerfilePath)) continue;
4608
5263
  let content;
4609
5264
  try {
4610
- content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
5265
+ content = await import_node_fs17.promises.readFile(dockerfilePath, "utf8");
4611
5266
  } catch (err) {
4612
5267
  recordExtractionError(
4613
5268
  "infra dockerfile",
4614
- import_node_path26.default.relative(scanPath, dockerfilePath),
5269
+ import_node_path30.default.relative(scanPath, dockerfilePath),
4615
5270
  err
4616
5271
  );
4617
5272
  continue;
@@ -4623,17 +5278,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4623
5278
  graph.addNode(node.id, node);
4624
5279
  nodesAdded++;
4625
5280
  }
4626
- const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types19.EdgeType.RUNS_ON);
5281
+ const relDockerfile = toPosix2(import_node_path30.default.relative(service.dir, dockerfilePath));
5282
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
5283
+ graph,
5284
+ service.pkg.name,
5285
+ service.node.id,
5286
+ relDockerfile
5287
+ );
5288
+ nodesAdded += fn;
5289
+ edgesAdded += fe;
5290
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
4627
5291
  if (!graph.hasEdge(edgeId)) {
4628
5292
  const edge = {
4629
5293
  id: edgeId,
4630
- source: service.node.id,
5294
+ source: fileNodeId,
4631
5295
  target: node.id,
4632
- type: import_types19.EdgeType.RUNS_ON,
4633
- provenance: import_types19.Provenance.EXTRACTED,
4634
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
5296
+ type: import_types20.EdgeType.RUNS_ON,
5297
+ provenance: import_types20.Provenance.EXTRACTED,
5298
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
4635
5299
  evidence: {
4636
- file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
5300
+ file: toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath))
4637
5301
  }
4638
5302
  };
4639
5303
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4645,21 +5309,21 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4645
5309
 
4646
5310
  // src/extract/infra/terraform.ts
4647
5311
  init_cjs_shims();
4648
- var import_node_fs15 = require("fs");
4649
- var import_node_path27 = __toESM(require("path"), 1);
5312
+ var import_node_fs18 = require("fs");
5313
+ var import_node_path31 = __toESM(require("path"), 1);
4650
5314
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4651
5315
  async function walkTfFiles(start, depth = 0, max = 5) {
4652
5316
  if (depth > max) return [];
4653
5317
  const out = [];
4654
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5318
+ const entries = await import_node_fs18.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4655
5319
  for (const entry of entries) {
4656
5320
  if (entry.isDirectory()) {
4657
5321
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4658
- const child = import_node_path27.default.join(start, entry.name);
5322
+ const child = import_node_path31.default.join(start, entry.name);
4659
5323
  if (await isPythonVenvDir(child)) continue;
4660
5324
  out.push(...await walkTfFiles(child, depth + 1, max));
4661
5325
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4662
- out.push(import_node_path27.default.join(start, entry.name));
5326
+ out.push(import_node_path31.default.join(start, entry.name));
4663
5327
  }
4664
5328
  }
4665
5329
  return out;
@@ -4668,7 +5332,7 @@ async function addTerraformResources(graph, scanPath) {
4668
5332
  let nodesAdded = 0;
4669
5333
  const files = await walkTfFiles(scanPath);
4670
5334
  for (const file of files) {
4671
- const content = await import_node_fs15.promises.readFile(file, "utf8");
5335
+ const content = await import_node_fs18.promises.readFile(file, "utf8");
4672
5336
  RESOURCE_RE.lastIndex = 0;
4673
5337
  let m;
4674
5338
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -4686,8 +5350,8 @@ async function addTerraformResources(graph, scanPath) {
4686
5350
 
4687
5351
  // src/extract/infra/k8s.ts
4688
5352
  init_cjs_shims();
4689
- var import_node_fs16 = require("fs");
4690
- var import_node_path28 = __toESM(require("path"), 1);
5353
+ var import_node_fs19 = require("fs");
5354
+ var import_node_path32 = __toESM(require("path"), 1);
4691
5355
  var import_yaml3 = require("yaml");
4692
5356
  var K8S_KIND_TO_INFRA_KIND = {
4693
5357
  Service: "k8s-service",
@@ -4701,15 +5365,15 @@ var K8S_KIND_TO_INFRA_KIND = {
4701
5365
  async function walkYamlFiles2(start, depth = 0, max = 5) {
4702
5366
  if (depth > max) return [];
4703
5367
  const out = [];
4704
- const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5368
+ const entries = await import_node_fs19.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4705
5369
  for (const entry of entries) {
4706
5370
  if (entry.isDirectory()) {
4707
5371
  if (IGNORED_DIRS.has(entry.name)) continue;
4708
- const child = import_node_path28.default.join(start, entry.name);
5372
+ const child = import_node_path32.default.join(start, entry.name);
4709
5373
  if (await isPythonVenvDir(child)) continue;
4710
5374
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4711
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
4712
- out.push(import_node_path28.default.join(start, entry.name));
5375
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path32.default.extname(entry.name))) {
5376
+ out.push(import_node_path32.default.join(start, entry.name));
4713
5377
  }
4714
5378
  }
4715
5379
  return out;
@@ -4718,7 +5382,7 @@ async function addK8sResources(graph, scanPath) {
4718
5382
  let nodesAdded = 0;
4719
5383
  const files = await walkYamlFiles2(scanPath);
4720
5384
  for (const file of files) {
4721
- const content = await import_node_fs16.promises.readFile(file, "utf8");
5385
+ const content = await import_node_fs19.promises.readFile(file, "utf8");
4722
5386
  let docs;
4723
5387
  try {
4724
5388
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -4753,17 +5417,17 @@ async function addInfra(graph, scanPath, services) {
4753
5417
  }
4754
5418
 
4755
5419
  // src/extract/index.ts
4756
- var import_node_path30 = __toESM(require("path"), 1);
5420
+ var import_node_path34 = __toESM(require("path"), 1);
4757
5421
 
4758
5422
  // src/extract/retire.ts
4759
5423
  init_cjs_shims();
4760
- var import_node_fs17 = require("fs");
4761
- var import_node_path29 = __toESM(require("path"), 1);
4762
- var import_types20 = require("@neat.is/types");
5424
+ var import_node_fs20 = require("fs");
5425
+ var import_node_path33 = __toESM(require("path"), 1);
5426
+ var import_types21 = require("@neat.is/types");
4763
5427
  function dropOrphanedFileNodes(graph) {
4764
5428
  const orphans = [];
4765
5429
  graph.forEachNode((id, attrs) => {
4766
- if (attrs.type !== import_types20.NodeType.FileNode) return;
5430
+ if (attrs.type !== import_types21.NodeType.FileNode) return;
4767
5431
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4768
5432
  orphans.push(id);
4769
5433
  }
@@ -4776,14 +5440,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4776
5440
  const bases = [scanPath, ...serviceDirs];
4777
5441
  graph.forEachEdge((id, attrs) => {
4778
5442
  const edge = attrs;
4779
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
5443
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
4780
5444
  const evidenceFile = edge.evidence?.file;
4781
5445
  if (!evidenceFile) return;
4782
- if (import_node_path29.default.isAbsolute(evidenceFile)) {
4783
- if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
5446
+ if (import_node_path33.default.isAbsolute(evidenceFile)) {
5447
+ if (!(0, import_node_fs20.existsSync)(evidenceFile)) toDrop.push(id);
4784
5448
  return;
4785
5449
  }
4786
- const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
5450
+ const found = bases.some((base) => (0, import_node_fs20.existsSync)(import_node_path33.default.join(base, evidenceFile)));
4787
5451
  if (!found) toDrop.push(id);
4788
5452
  });
4789
5453
  for (const id of toDrop) graph.dropEdge(id);
@@ -4798,6 +5462,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4798
5462
  const services = await discoverServices(scanPath);
4799
5463
  const phase1Nodes = addServiceNodes(graph, services);
4800
5464
  await addServiceAliases(graph, scanPath, services);
5465
+ const fileEnum = await addFiles(graph, services);
5466
+ const importGraph = await addImports(graph, services);
4801
5467
  const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
4802
5468
  const phase3 = await addConfigNodes(graph, services, scanPath);
4803
5469
  const phase4 = await addCallEdges(graph, services);
@@ -4821,7 +5487,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4821
5487
  }
4822
5488
  const droppedEntries = drainDroppedExtracted();
4823
5489
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4824
- const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
5490
+ const rejectedPath = import_node_path34.default.join(import_node_path34.default.dirname(opts.errorsPath), "rejected.ndjson");
4825
5491
  try {
4826
5492
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4827
5493
  } catch (err) {
@@ -4831,8 +5497,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4831
5497
  }
4832
5498
  }
4833
5499
  const result = {
4834
- nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
4835
- edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
5500
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
5501
+ edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
4836
5502
  frontiersPromoted,
4837
5503
  extractionErrors: errorEntries.length,
4838
5504
  errorEntries,
@@ -4855,7 +5521,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4855
5521
 
4856
5522
  // src/diff.ts
4857
5523
  init_cjs_shims();
4858
- var import_node_fs18 = require("fs");
5524
+ var import_node_fs21 = require("fs");
4859
5525
  async function loadSnapshotForDiff(target) {
4860
5526
  if (/^https?:\/\//i.test(target)) {
4861
5527
  const res = await fetch(target);
@@ -4864,7 +5530,7 @@ async function loadSnapshotForDiff(target) {
4864
5530
  }
4865
5531
  return await res.json();
4866
5532
  }
4867
- const raw = await import_node_fs18.promises.readFile(target, "utf8");
5533
+ const raw = await import_node_fs21.promises.readFile(target, "utf8");
4868
5534
  return JSON.parse(raw);
4869
5535
  }
4870
5536
  function indexEntries(entries) {
@@ -4932,9 +5598,9 @@ function canonicalJson(value) {
4932
5598
 
4933
5599
  // src/persist.ts
4934
5600
  init_cjs_shims();
4935
- var import_node_fs19 = require("fs");
4936
- var import_node_path31 = __toESM(require("path"), 1);
4937
- var import_types21 = require("@neat.is/types");
5601
+ var import_node_fs22 = require("fs");
5602
+ var import_node_path35 = __toESM(require("path"), 1);
5603
+ var import_types22 = require("@neat.is/types");
4938
5604
  var SCHEMA_VERSION = 4;
4939
5605
  function migrateV1ToV2(payload) {
4940
5606
  const nodes = payload.graph.nodes;
@@ -4956,12 +5622,12 @@ function migrateV2ToV3(payload) {
4956
5622
  for (const edge of edges) {
4957
5623
  const attrs = edge.attributes;
4958
5624
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4959
- attrs.provenance = import_types21.Provenance.OBSERVED;
5625
+ attrs.provenance = import_types22.Provenance.OBSERVED;
4960
5626
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4961
5627
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4962
5628
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4963
5629
  if (type && source && target) {
4964
- const newId = (0, import_types21.observedEdgeId)(source, target, type);
5630
+ const newId = (0, import_types22.observedEdgeId)(source, target, type);
4965
5631
  attrs.id = newId;
4966
5632
  if (edge.key) edge.key = newId;
4967
5633
  }
@@ -4970,7 +5636,7 @@ function migrateV2ToV3(payload) {
4970
5636
  return { ...payload, schemaVersion: 3 };
4971
5637
  }
4972
5638
  async function ensureDir(filePath) {
4973
- await import_node_fs19.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
5639
+ await import_node_fs22.promises.mkdir(import_node_path35.default.dirname(filePath), { recursive: true });
4974
5640
  }
4975
5641
  async function saveGraphToDisk(graph, outPath) {
4976
5642
  await ensureDir(outPath);
@@ -4980,13 +5646,13 @@ async function saveGraphToDisk(graph, outPath) {
4980
5646
  graph: graph.export()
4981
5647
  };
4982
5648
  const tmp = `${outPath}.tmp`;
4983
- await import_node_fs19.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4984
- await import_node_fs19.promises.rename(tmp, outPath);
5649
+ await import_node_fs22.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
5650
+ await import_node_fs22.promises.rename(tmp, outPath);
4985
5651
  }
4986
5652
  async function loadGraphFromDisk(graph, outPath) {
4987
5653
  let raw;
4988
5654
  try {
4989
- raw = await import_node_fs19.promises.readFile(outPath, "utf8");
5655
+ raw = await import_node_fs22.promises.readFile(outPath, "utf8");
4990
5656
  } catch (err) {
4991
5657
  if (err.code === "ENOENT") return;
4992
5658
  throw err;
@@ -5045,23 +5711,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5045
5711
 
5046
5712
  // src/projects.ts
5047
5713
  init_cjs_shims();
5048
- var import_node_path32 = __toESM(require("path"), 1);
5714
+ var import_node_path36 = __toESM(require("path"), 1);
5049
5715
  function pathsForProject(project, baseDir) {
5050
5716
  if (project === DEFAULT_PROJECT) {
5051
5717
  return {
5052
- snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
5053
- errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
5054
- staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
5055
- embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
5056
- policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
5718
+ snapshotPath: import_node_path36.default.join(baseDir, "graph.json"),
5719
+ errorsPath: import_node_path36.default.join(baseDir, "errors.ndjson"),
5720
+ staleEventsPath: import_node_path36.default.join(baseDir, "stale-events.ndjson"),
5721
+ embeddingsCachePath: import_node_path36.default.join(baseDir, "embeddings.json"),
5722
+ policyViolationsPath: import_node_path36.default.join(baseDir, "policy-violations.ndjson")
5057
5723
  };
5058
5724
  }
5059
5725
  return {
5060
- snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
5061
- errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
5062
- staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
5063
- embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
5064
- policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
5726
+ snapshotPath: import_node_path36.default.join(baseDir, `${project}.json`),
5727
+ errorsPath: import_node_path36.default.join(baseDir, `errors.${project}.ndjson`),
5728
+ staleEventsPath: import_node_path36.default.join(baseDir, `stale-events.${project}.ndjson`),
5729
+ embeddingsCachePath: import_node_path36.default.join(baseDir, `embeddings.${project}.json`),
5730
+ policyViolationsPath: import_node_path36.default.join(baseDir, `policy-violations.${project}.ndjson`)
5065
5731
  };
5066
5732
  }
5067
5733
  var Projects = class {
@@ -5101,23 +5767,23 @@ function parseExtraProjects(raw) {
5101
5767
 
5102
5768
  // src/registry.ts
5103
5769
  init_cjs_shims();
5104
- var import_node_fs20 = require("fs");
5105
- var import_node_os2 = __toESM(require("os"), 1);
5106
- var import_node_path33 = __toESM(require("path"), 1);
5107
- var import_types22 = require("@neat.is/types");
5770
+ var import_node_fs23 = require("fs");
5771
+ var import_node_os3 = __toESM(require("os"), 1);
5772
+ var import_node_path37 = __toESM(require("path"), 1);
5773
+ var import_types23 = require("@neat.is/types");
5108
5774
  function neatHome() {
5109
5775
  const override = process.env.NEAT_HOME;
5110
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
5111
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
5776
+ if (override && override.length > 0) return import_node_path37.default.resolve(override);
5777
+ return import_node_path37.default.join(import_node_os3.default.homedir(), ".neat");
5112
5778
  }
5113
5779
  function registryPath() {
5114
- return import_node_path33.default.join(neatHome(), "projects.json");
5780
+ return import_node_path37.default.join(neatHome(), "projects.json");
5115
5781
  }
5116
5782
  async function readRegistry() {
5117
5783
  const file = registryPath();
5118
5784
  let raw;
5119
5785
  try {
5120
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5786
+ raw = await import_node_fs23.promises.readFile(file, "utf8");
5121
5787
  } catch (err) {
5122
5788
  if (err.code === "ENOENT") {
5123
5789
  return { version: 1, projects: [] };
@@ -5125,7 +5791,7 @@ async function readRegistry() {
5125
5791
  throw err;
5126
5792
  }
5127
5793
  const parsed = JSON.parse(raw);
5128
- return import_types22.RegistryFileSchema.parse(parsed);
5794
+ return import_types23.RegistryFileSchema.parse(parsed);
5129
5795
  }
5130
5796
  async function getProject(name) {
5131
5797
  const reg = await readRegistry();
@@ -5327,11 +5993,11 @@ function registerRoutes(scope, ctx) {
5327
5993
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5328
5994
  const parsed = [];
5329
5995
  for (const c of candidates) {
5330
- const r = import_types23.DivergenceTypeSchema.safeParse(c);
5996
+ const r = import_types24.DivergenceTypeSchema.safeParse(c);
5331
5997
  if (!r.success) {
5332
5998
  return reply.code(400).send({
5333
5999
  error: `unknown divergence type "${c}"`,
5334
- allowed: import_types23.DivergenceTypeSchema.options
6000
+ allowed: import_types24.DivergenceTypeSchema.options
5335
6001
  });
5336
6002
  }
5337
6003
  parsed.push(r.data);
@@ -5544,7 +6210,7 @@ function registerRoutes(scope, ctx) {
5544
6210
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5545
6211
  let violations = await log.readAll();
5546
6212
  if (req.query.severity) {
5547
- const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
6213
+ const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
5548
6214
  if (!sev.success) {
5549
6215
  return reply.code(400).send({
5550
6216
  error: "invalid severity",
@@ -5561,7 +6227,7 @@ function registerRoutes(scope, ctx) {
5561
6227
  scope.post("/policies/check", async (req, reply) => {
5562
6228
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5563
6229
  if (!proj) return;
5564
- const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6230
+ const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5565
6231
  if (!parsed.success) {
5566
6232
  return reply.code(400).send({
5567
6233
  error: "invalid /policies/check body",
@@ -5594,6 +6260,105 @@ function registerRoutes(scope, ctx) {
5594
6260
  violations
5595
6261
  };
5596
6262
  });
6263
+ scope.get("/extend/list-uninstrumented", async (req, reply) => {
6264
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6265
+ if (!proj) return;
6266
+ if (!proj.scanPath) {
6267
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6268
+ }
6269
+ try {
6270
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
6271
+ return { libraries: results };
6272
+ } catch (err) {
6273
+ return reply.code(500).send({ error: err.message });
6274
+ }
6275
+ });
6276
+ scope.get("/extend/lookup", async (req, reply) => {
6277
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6278
+ if (!proj) return;
6279
+ const { library, version } = req.query;
6280
+ if (!library) {
6281
+ return reply.code(400).send({ error: "query parameter `library` is required" });
6282
+ }
6283
+ const result = lookupInstrumentation(library, version);
6284
+ if (!result) {
6285
+ return reply.code(404).send({ error: "library not found in registry", library });
6286
+ }
6287
+ return result;
6288
+ });
6289
+ scope.get("/extend/describe", async (req, reply) => {
6290
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6291
+ if (!proj) return;
6292
+ if (!proj.scanPath) {
6293
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6294
+ }
6295
+ try {
6296
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
6297
+ return state;
6298
+ } catch (err) {
6299
+ return reply.code(500).send({ error: err.message });
6300
+ }
6301
+ });
6302
+ scope.post("/extend/apply", async (req, reply) => {
6303
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6304
+ if (!proj) return;
6305
+ if (!proj.scanPath) {
6306
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6307
+ }
6308
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6309
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6310
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6311
+ }
6312
+ try {
6313
+ const result = await applyExtension(
6314
+ { project: proj.name, scanPath: proj.scanPath },
6315
+ { library, instrumentation_package, version, registration_snippet }
6316
+ );
6317
+ return result;
6318
+ } catch (err) {
6319
+ return reply.code(500).send({ error: err.message });
6320
+ }
6321
+ });
6322
+ scope.post("/extend/dry-run", async (req, reply) => {
6323
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6324
+ if (!proj) return;
6325
+ if (!proj.scanPath) {
6326
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6327
+ }
6328
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6329
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6330
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6331
+ }
6332
+ try {
6333
+ const result = await dryRunExtension(
6334
+ { project: proj.name, scanPath: proj.scanPath },
6335
+ { library, instrumentation_package, version, registration_snippet }
6336
+ );
6337
+ return result;
6338
+ } catch (err) {
6339
+ return reply.code(500).send({ error: err.message });
6340
+ }
6341
+ });
6342
+ scope.post("/extend/rollback", async (req, reply) => {
6343
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6344
+ if (!proj) return;
6345
+ if (!proj.scanPath) {
6346
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6347
+ }
6348
+ const { library } = req.body ?? {};
6349
+ if (!library) {
6350
+ return reply.code(400).send({ error: "body must include library" });
6351
+ }
6352
+ try {
6353
+ const result = await rollbackExtension(
6354
+ { project: proj.name, scanPath: proj.scanPath },
6355
+ { library }
6356
+ );
6357
+ return result;
6358
+ } catch (err) {
6359
+ return reply.code(500).send({ error: err.message });
6360
+ }
6361
+ });
5597
6362
  }
5598
6363
  async function buildApi(opts) {
5599
6364
  const app = (0, import_fastify.default)({ logger: false });
@@ -5704,8 +6469,8 @@ init_otel_grpc();
5704
6469
 
5705
6470
  // src/search.ts
5706
6471
  init_cjs_shims();
5707
- var import_node_fs21 = require("fs");
5708
- var import_node_path36 = __toESM(require("path"), 1);
6472
+ var import_node_fs24 = require("fs");
6473
+ var import_node_path40 = __toESM(require("path"), 1);
5709
6474
  var import_node_crypto3 = require("crypto");
5710
6475
  var DEFAULT_LIMIT = 10;
5711
6476
  var NOMIC_DIM = 768;
@@ -5835,7 +6600,7 @@ async function pickEmbedder() {
5835
6600
  }
5836
6601
  async function readCache(cachePath) {
5837
6602
  try {
5838
- const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
6603
+ const raw = await import_node_fs24.promises.readFile(cachePath, "utf8");
5839
6604
  const parsed = JSON.parse(raw);
5840
6605
  if (parsed.version !== 1) return null;
5841
6606
  return parsed;
@@ -5844,8 +6609,8 @@ async function readCache(cachePath) {
5844
6609
  }
5845
6610
  }
5846
6611
  async function writeCache(cachePath, cache) {
5847
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(cachePath), { recursive: true });
5848
- await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
6612
+ await import_node_fs24.promises.mkdir(import_node_path40.default.dirname(cachePath), { recursive: true });
6613
+ await import_node_fs24.promises.writeFile(cachePath, JSON.stringify(cache));
5849
6614
  }
5850
6615
  var VectorIndex = class {
5851
6616
  constructor(embedder, cachePath) {
@@ -6025,14 +6790,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
6025
6790
  async function main() {
6026
6791
  const baseDirEnv = process.env.NEAT_OUT_DIR;
6027
6792
  const legacyOutPath = process.env.NEAT_OUT_PATH;
6028
- const baseDir = baseDirEnv ? import_node_path37.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path37.default.resolve(import_node_path37.default.dirname(legacyOutPath)) : import_node_path37.default.resolve("./neat-out");
6029
- const defaultScanPath = import_node_path37.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
6793
+ const baseDir = baseDirEnv ? import_node_path41.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path41.default.resolve(import_node_path41.default.dirname(legacyOutPath)) : import_node_path41.default.resolve("./neat-out");
6794
+ const defaultScanPath = import_node_path41.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
6030
6795
  const registry = new Projects();
6031
6796
  await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
6032
6797
  for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
6033
6798
  const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
6034
6799
  const projectScan = process.env[envKey];
6035
- await bootProject(registry, name, projectScan ? import_node_path37.default.resolve(projectScan) : void 0, baseDir);
6800
+ await bootProject(registry, name, projectScan ? import_node_path41.default.resolve(projectScan) : void 0, baseDir);
6036
6801
  }
6037
6802
  const host = process.env.HOST ?? "0.0.0.0";
6038
6803
  const port = Number(process.env.PORT ?? 8080);