@neat.is/core 0.4.11 → 0.4.12

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 path40 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
60
  for (const suffix of suffixes) {
61
- if (path38 === suffix || path38.endsWith(suffix)) {
61
+ if (path40 === suffix || path40.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_path36.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
188
+ return import_node_path36.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_path36, 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_path36 = __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_path37.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
373
+ const protoRoot = import_node_path37.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_path37.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_path37, 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_path37 = __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_path39 = __toESM(require("path"), 1);
590
590
 
591
591
  // src/graph.ts
592
592
  init_cjs_shims();
@@ -612,15 +612,330 @@ var import_fastify = __toESM(require("fastify"), 1);
612
612
  var import_cors = __toESM(require("@fastify/cors"), 1);
613
613
  var import_types23 = require("@neat.is/types");
614
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
+ }
929
+
615
930
  // src/divergences.ts
616
931
  init_cjs_shims();
617
932
  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, path40, edges) {
1357
+ if (path40.length > best.path.length) {
1358
+ best = { path: [...path40], edges: [...edges] };
1044
1359
  }
1045
- if (path38.length - 1 >= maxDepth) return;
1360
+ if (path40.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
+ path40.push(srcId);
1051
1366
  edges.push(edge);
1052
- step(srcId, path38, edges);
1053
- path38.pop();
1367
+ step(srcId, path40, edges);
1368
+ path40.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());
@@ -3244,15 +3566,112 @@ async function addServiceAliases(graph, scanPath, services) {
3244
3566
 
3245
3567
  // src/extract/databases/index.ts
3246
3568
  init_cjs_shims();
3247
- var import_node_path17 = __toESM(require("path"), 1);
3569
+ var import_node_path20 = __toESM(require("path"), 1);
3570
+ var import_types9 = require("@neat.is/types");
3571
+
3572
+ // src/extract/calls/shared.ts
3573
+ init_cjs_shims();
3574
+ var import_node_fs12 = require("fs");
3575
+ var import_node_path12 = __toESM(require("path"), 1);
3248
3576
  var import_types8 = require("@neat.is/types");
3577
+ async function walkSourceFiles(dir) {
3578
+ const out = [];
3579
+ async function walk(current) {
3580
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3581
+ for (const entry of entries) {
3582
+ const full = import_node_path12.default.join(current, entry.name);
3583
+ if (entry.isDirectory()) {
3584
+ if (IGNORED_DIRS.has(entry.name)) continue;
3585
+ if (await isPythonVenvDir(full)) continue;
3586
+ await walk(full);
3587
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name))) {
3588
+ out.push(full);
3589
+ }
3590
+ }
3591
+ }
3592
+ await walk(dir);
3593
+ return out;
3594
+ }
3595
+ async function loadSourceFiles(dir) {
3596
+ const paths = await walkSourceFiles(dir);
3597
+ const out = [];
3598
+ for (const p of paths) {
3599
+ try {
3600
+ const content = await import_node_fs12.promises.readFile(p, "utf8");
3601
+ out.push({ path: p, content });
3602
+ } catch {
3603
+ }
3604
+ }
3605
+ return out;
3606
+ }
3607
+ function lineOf(text, needle) {
3608
+ const idx = text.indexOf(needle);
3609
+ if (idx < 0) return 1;
3610
+ return text.slice(0, idx).split("\n").length;
3611
+ }
3612
+ function snippet(text, line) {
3613
+ const lines = text.split("\n");
3614
+ return (lines[line - 1] ?? "").trim();
3615
+ }
3616
+ function toPosix2(p) {
3617
+ return p.split("\\").join("/");
3618
+ }
3619
+ function languageForPath(relPath) {
3620
+ switch (import_node_path12.default.extname(relPath).toLowerCase()) {
3621
+ case ".py":
3622
+ return "python";
3623
+ case ".ts":
3624
+ case ".tsx":
3625
+ return "typescript";
3626
+ case ".js":
3627
+ case ".jsx":
3628
+ case ".mjs":
3629
+ case ".cjs":
3630
+ return "javascript";
3631
+ default:
3632
+ return void 0;
3633
+ }
3634
+ }
3635
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3636
+ let nodesAdded = 0;
3637
+ let edgesAdded = 0;
3638
+ const fileNodeId = (0, import_types8.fileId)(serviceName, relPath);
3639
+ if (!graph.hasNode(fileNodeId)) {
3640
+ const language = languageForPath(relPath);
3641
+ const node = {
3642
+ id: fileNodeId,
3643
+ type: import_types8.NodeType.FileNode,
3644
+ service: serviceName,
3645
+ path: relPath,
3646
+ ...language ? { language } : {},
3647
+ discoveredVia: "static"
3648
+ };
3649
+ graph.addNode(fileNodeId, node);
3650
+ nodesAdded++;
3651
+ }
3652
+ const containsId = (0, import_types8.extractedEdgeId)(serviceNodeId, fileNodeId, import_types8.EdgeType.CONTAINS);
3653
+ if (!graph.hasEdge(containsId)) {
3654
+ const edge = {
3655
+ id: containsId,
3656
+ source: serviceNodeId,
3657
+ target: fileNodeId,
3658
+ type: import_types8.EdgeType.CONTAINS,
3659
+ provenance: import_types8.Provenance.EXTRACTED,
3660
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3661
+ evidence: { file: relPath }
3662
+ };
3663
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3664
+ edgesAdded++;
3665
+ }
3666
+ return { fileNodeId, nodesAdded, edgesAdded };
3667
+ }
3249
3668
 
3250
3669
  // src/extract/databases/db-config-yaml.ts
3251
3670
  init_cjs_shims();
3252
- var import_node_path10 = __toESM(require("path"), 1);
3671
+ var import_node_path13 = __toESM(require("path"), 1);
3253
3672
  async function parse(serviceDir) {
3254
- const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
3255
- if (!await exists(yamlPath)) return [];
3673
+ const yamlPath = import_node_path13.default.join(serviceDir, "db-config.yaml");
3674
+ if (!await exists2(yamlPath)) return [];
3256
3675
  const raw = await readYaml(yamlPath);
3257
3676
  return [
3258
3677
  {
@@ -3269,13 +3688,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
3269
3688
 
3270
3689
  // src/extract/databases/dotenv.ts
3271
3690
  init_cjs_shims();
3272
- var import_node_fs11 = require("fs");
3273
- var import_node_path12 = __toESM(require("path"), 1);
3691
+ var import_node_fs14 = require("fs");
3692
+ var import_node_path15 = __toESM(require("path"), 1);
3274
3693
 
3275
3694
  // src/extract/databases/shared.ts
3276
3695
  init_cjs_shims();
3277
- var import_node_fs10 = require("fs");
3278
- var import_node_path11 = __toESM(require("path"), 1);
3696
+ var import_node_fs13 = require("fs");
3697
+ var import_node_path14 = __toESM(require("path"), 1);
3279
3698
  function schemeToEngine(scheme) {
3280
3699
  const s = scheme.toLowerCase().split("+")[0];
3281
3700
  switch (s) {
@@ -3314,14 +3733,14 @@ function parseConnectionString(url) {
3314
3733
  }
3315
3734
  async function readIfExists(filePath) {
3316
3735
  try {
3317
- return await import_node_fs10.promises.readFile(filePath, "utf8");
3736
+ return await import_node_fs13.promises.readFile(filePath, "utf8");
3318
3737
  } catch {
3319
3738
  return null;
3320
3739
  }
3321
3740
  }
3322
3741
  async function findFirst(serviceDir, candidates) {
3323
3742
  for (const rel of candidates) {
3324
- const abs = import_node_path11.default.join(serviceDir, rel);
3743
+ const abs = import_node_path14.default.join(serviceDir, rel);
3325
3744
  const content = await readIfExists(abs);
3326
3745
  if (content !== null) return abs;
3327
3746
  }
@@ -3372,15 +3791,15 @@ function parseDotenvLine(line) {
3372
3791
  return { key, value };
3373
3792
  }
3374
3793
  async function parse2(serviceDir) {
3375
- const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3794
+ const entries = await import_node_fs14.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
3376
3795
  const configs = [];
3377
3796
  const seen = /* @__PURE__ */ new Set();
3378
3797
  for (const entry of entries) {
3379
3798
  if (!entry.isFile()) continue;
3380
3799
  const match = isConfigFile(entry.name);
3381
3800
  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");
3801
+ const filePath = import_node_path15.default.join(serviceDir, entry.name);
3802
+ const content = await import_node_fs14.promises.readFile(filePath, "utf8");
3384
3803
  for (const line of content.split("\n")) {
3385
3804
  const parsed = parseDotenvLine(line);
3386
3805
  if (!parsed) continue;
@@ -3399,9 +3818,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
3399
3818
 
3400
3819
  // src/extract/databases/prisma.ts
3401
3820
  init_cjs_shims();
3402
- var import_node_path13 = __toESM(require("path"), 1);
3821
+ var import_node_path16 = __toESM(require("path"), 1);
3403
3822
  async function parse3(serviceDir) {
3404
- const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
3823
+ const schemaPath = import_node_path16.default.join(serviceDir, "prisma", "schema.prisma");
3405
3824
  const content = await readIfExists(schemaPath);
3406
3825
  if (!content) return [];
3407
3826
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -3533,11 +3952,11 @@ var knexParser = { name: "knex", parse: parse5 };
3533
3952
 
3534
3953
  // src/extract/databases/ormconfig.ts
3535
3954
  init_cjs_shims();
3536
- var import_node_path14 = __toESM(require("path"), 1);
3955
+ var import_node_path17 = __toESM(require("path"), 1);
3537
3956
  async function parse6(serviceDir) {
3538
3957
  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;
3958
+ const abs = import_node_path17.default.join(serviceDir, candidate);
3959
+ if (!await exists2(abs)) continue;
3541
3960
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
3542
3961
  const entries = Array.isArray(raw) ? raw : [raw];
3543
3962
  const out = [];
@@ -3596,10 +4015,10 @@ var typeormParser = { name: "typeorm", parse: parse7 };
3596
4015
 
3597
4016
  // src/extract/databases/sequelize.ts
3598
4017
  init_cjs_shims();
3599
- var import_node_path15 = __toESM(require("path"), 1);
4018
+ var import_node_path18 = __toESM(require("path"), 1);
3600
4019
  async function parse8(serviceDir) {
3601
- const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
3602
- if (!await exists(configPath)) return [];
4020
+ const configPath = import_node_path18.default.join(serviceDir, "config", "config.json");
4021
+ if (!await exists2(configPath)) return [];
3603
4022
  const raw = await readJson(configPath);
3604
4023
  const out = [];
3605
4024
  const seen = /* @__PURE__ */ new Set();
@@ -3625,7 +4044,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3625
4044
 
3626
4045
  // src/extract/databases/docker-compose.ts
3627
4046
  init_cjs_shims();
3628
- var import_node_path16 = __toESM(require("path"), 1);
4047
+ var import_node_path19 = __toESM(require("path"), 1);
3629
4048
  function portFromService(svc) {
3630
4049
  for (const raw of svc.ports ?? []) {
3631
4050
  const str = String(raw);
@@ -3652,8 +4071,8 @@ function databaseFromEnv(svc) {
3652
4071
  }
3653
4072
  async function parse9(serviceDir) {
3654
4073
  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;
4074
+ const abs = import_node_path19.default.join(serviceDir, name);
4075
+ if (!await exists2(abs)) continue;
3657
4076
  const raw = await readYaml(abs);
3658
4077
  if (!raw?.services) return [];
3659
4078
  const out = [];
@@ -3693,8 +4112,8 @@ function compatibleDriversFor(engine) {
3693
4112
  }
3694
4113
  function toDatabaseNode(config) {
3695
4114
  return {
3696
- id: (0, import_types8.databaseId)(config.host),
3697
- type: import_types8.NodeType.DatabaseNode,
4115
+ id: (0, import_types9.databaseId)(config.host),
4116
+ type: import_types9.NodeType.DatabaseNode,
3698
4117
  name: config.database || config.host,
3699
4118
  engine: config.engine,
3700
4119
  engineVersion: config.engineVersion,
@@ -3824,19 +4243,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3824
4243
  discoveredVia: mergedDiscoveredVia
3825
4244
  });
3826
4245
  }
4246
+ const relConfigFile = toPosix2(import_node_path20.default.relative(service.dir, config.sourceFile));
4247
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4248
+ graph,
4249
+ service.pkg.name,
4250
+ service.node.id,
4251
+ relConfigFile
4252
+ );
4253
+ nodesAdded += fn;
4254
+ edgesAdded += fe;
4255
+ const evidenceFile = toPosix2(import_node_path20.default.relative(scanPath, config.sourceFile));
3827
4256
  const edge = {
3828
- id: (0, import_types5.extractedEdgeId)(service.node.id, dbNode.id, import_types8.EdgeType.CONNECTS_TO),
3829
- source: service.node.id,
4257
+ id: (0, import_types5.extractedEdgeId)(fileNodeId, dbNode.id, import_types9.EdgeType.CONNECTS_TO),
4258
+ source: fileNodeId,
3830
4259
  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
- }
4260
+ type: import_types9.EdgeType.CONNECTS_TO,
4261
+ provenance: import_types9.Provenance.EXTRACTED,
4262
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
4263
+ evidence: { file: evidenceFile }
3840
4264
  };
3841
4265
  if (!graph.hasEdge(edge.id)) {
3842
4266
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3862,15 +4286,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3862
4286
 
3863
4287
  // src/extract/configs.ts
3864
4288
  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");
4289
+ var import_node_fs15 = require("fs");
4290
+ var import_node_path21 = __toESM(require("path"), 1);
4291
+ var import_types10 = require("@neat.is/types");
3868
4292
  async function walkConfigFiles(dir) {
3869
4293
  const out = [];
3870
4294
  async function walk(current) {
3871
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
4295
+ const entries = await import_node_fs15.promises.readdir(current, { withFileTypes: true });
3872
4296
  for (const entry of entries) {
3873
- const full = import_node_path18.default.join(current, entry.name);
4297
+ const full = import_node_path21.default.join(current, entry.name);
3874
4298
  if (entry.isDirectory()) {
3875
4299
  if (IGNORED_DIRS.has(entry.name)) continue;
3876
4300
  if (await isPythonVenvDir(full)) continue;
@@ -3889,26 +4313,35 @@ async function addConfigNodes(graph, services, scanPath) {
3889
4313
  for (const service of services) {
3890
4314
  const configFiles = await walkConfigFiles(service.dir);
3891
4315
  for (const file of configFiles) {
3892
- const relPath = import_node_path18.default.relative(scanPath, file);
4316
+ const relPath = import_node_path21.default.relative(scanPath, file);
3893
4317
  const node = {
3894
- id: (0, import_types9.configId)(relPath),
3895
- type: import_types9.NodeType.ConfigNode,
3896
- name: import_node_path18.default.basename(file),
4318
+ id: (0, import_types10.configId)(relPath),
4319
+ type: import_types10.NodeType.ConfigNode,
4320
+ name: import_node_path21.default.basename(file),
3897
4321
  path: relPath,
3898
- fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
4322
+ fileType: isConfigFile(import_node_path21.default.basename(file)).fileType
3899
4323
  };
3900
4324
  if (!graph.hasNode(node.id)) {
3901
4325
  graph.addNode(node.id, node);
3902
4326
  nodesAdded++;
3903
4327
  }
4328
+ const relToService = toPosix2(import_node_path21.default.relative(service.dir, file));
4329
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4330
+ graph,
4331
+ service.pkg.name,
4332
+ service.node.id,
4333
+ relToService
4334
+ );
4335
+ nodesAdded += fn;
4336
+ edgesAdded += fe;
3904
4337
  const edge = {
3905
- id: (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types9.EdgeType.CONFIGURED_BY),
3906
- source: service.node.id,
4338
+ id: (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types10.EdgeType.CONFIGURED_BY),
4339
+ source: fileNodeId,
3907
4340
  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("/") }
4341
+ type: import_types10.EdgeType.CONFIGURED_BY,
4342
+ provenance: import_types10.Provenance.EXTRACTED,
4343
+ confidence: (0, import_types10.confidenceForExtracted)("structural"),
4344
+ evidence: { file: relPath.split(import_node_path21.default.sep).join("/") }
3912
4345
  };
3913
4346
  if (!graph.hasEdge(edge.id)) {
3914
4347
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3925,110 +4358,11 @@ var import_types16 = require("@neat.is/types");
3925
4358
 
3926
4359
  // src/extract/calls/http.ts
3927
4360
  init_cjs_shims();
3928
- var import_node_path20 = __toESM(require("path"), 1);
4361
+ var import_node_path22 = __toESM(require("path"), 1);
3929
4362
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3930
4363
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3931
4364
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3932
4365
  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
4032
4366
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
4033
4367
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
4034
4368
  function isInsideJsxExternalLink(node) {
@@ -4093,9 +4427,9 @@ async function addHttpCallEdges(graph, services) {
4093
4427
  const knownHosts = /* @__PURE__ */ new Set();
4094
4428
  const hostToNodeId = /* @__PURE__ */ new Map();
4095
4429
  for (const service of services) {
4096
- knownHosts.add(import_node_path20.default.basename(service.dir));
4430
+ knownHosts.add(import_node_path22.default.basename(service.dir));
4097
4431
  knownHosts.add(service.pkg.name);
4098
- hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
4432
+ hostToNodeId.set(import_node_path22.default.basename(service.dir), service.node.id);
4099
4433
  hostToNodeId.set(service.pkg.name, service.node.id);
4100
4434
  }
4101
4435
  let nodesAdded = 0;
@@ -4105,7 +4439,7 @@ async function addHttpCallEdges(graph, services) {
4105
4439
  const seen = /* @__PURE__ */ new Set();
4106
4440
  for (const file of files) {
4107
4441
  if (isTestPath(file.path)) continue;
4108
- const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
4442
+ const parser = import_node_path22.default.extname(file.path) === ".py" ? pyParser : jsParser;
4109
4443
  let sites;
4110
4444
  try {
4111
4445
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -4114,7 +4448,7 @@ async function addHttpCallEdges(graph, services) {
4114
4448
  continue;
4115
4449
  }
4116
4450
  if (sites.length === 0) continue;
4117
- const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
4451
+ const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
4118
4452
  for (const site of sites) {
4119
4453
  const targetId = hostToNodeId.get(site.host);
4120
4454
  if (!targetId || targetId === service.node.id) continue;
@@ -4168,7 +4502,7 @@ async function addHttpCallEdges(graph, services) {
4168
4502
 
4169
4503
  // src/extract/calls/kafka.ts
4170
4504
  init_cjs_shims();
4171
- var import_node_path21 = __toESM(require("path"), 1);
4505
+ var import_node_path23 = __toESM(require("path"), 1);
4172
4506
  var import_types12 = require("@neat.is/types");
4173
4507
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
4174
4508
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -4199,7 +4533,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4199
4533
  // tier (ADR-066).
4200
4534
  confidenceKind: "verified-call-site",
4201
4535
  evidence: {
4202
- file: import_node_path21.default.relative(serviceDir, file.path),
4536
+ file: import_node_path23.default.relative(serviceDir, file.path),
4203
4537
  line,
4204
4538
  snippet: snippet(file.content, line)
4205
4539
  }
@@ -4212,7 +4546,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
4212
4546
 
4213
4547
  // src/extract/calls/redis.ts
4214
4548
  init_cjs_shims();
4215
- var import_node_path22 = __toESM(require("path"), 1);
4549
+ var import_node_path24 = __toESM(require("path"), 1);
4216
4550
  var import_types13 = require("@neat.is/types");
4217
4551
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
4218
4552
  function redisEndpointsFromFile(file, serviceDir) {
@@ -4235,7 +4569,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4235
4569
  // support tier (ADR-066).
4236
4570
  confidenceKind: "url-with-structural-support",
4237
4571
  evidence: {
4238
- file: import_node_path22.default.relative(serviceDir, file.path),
4572
+ file: import_node_path24.default.relative(serviceDir, file.path),
4239
4573
  line,
4240
4574
  snippet: snippet(file.content, line)
4241
4575
  }
@@ -4246,7 +4580,7 @@ function redisEndpointsFromFile(file, serviceDir) {
4246
4580
 
4247
4581
  // src/extract/calls/aws.ts
4248
4582
  init_cjs_shims();
4249
- var import_node_path23 = __toESM(require("path"), 1);
4583
+ var import_node_path25 = __toESM(require("path"), 1);
4250
4584
  var import_types14 = require("@neat.is/types");
4251
4585
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
4252
4586
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -4280,7 +4614,7 @@ function awsEndpointsFromFile(file, serviceDir) {
4280
4614
  // (ADR-066).
4281
4615
  confidenceKind: "verified-call-site",
4282
4616
  evidence: {
4283
- file: import_node_path23.default.relative(serviceDir, file.path),
4617
+ file: import_node_path25.default.relative(serviceDir, file.path),
4284
4618
  line,
4285
4619
  snippet: snippet(file.content, line)
4286
4620
  }
@@ -4305,13 +4639,14 @@ function awsEndpointsFromFile(file, serviceDir) {
4305
4639
 
4306
4640
  // src/extract/calls/grpc.ts
4307
4641
  init_cjs_shims();
4308
- var import_node_path24 = __toESM(require("path"), 1);
4642
+ var import_node_path26 = __toESM(require("path"), 1);
4309
4643
  var import_types15 = require("@neat.is/types");
4310
4644
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
4311
4645
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
4312
4646
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
4313
4647
  function isLikelyAddress(value) {
4314
4648
  if (!value) return false;
4649
+ if (/\s/.test(value) || value.startsWith("{")) return false;
4315
4650
  return /:\d{2,5}$/.test(value) || value.includes(".");
4316
4651
  }
4317
4652
  function normaliseForMatch(s) {
@@ -4363,7 +4698,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
4363
4698
  // tier (ADR-066).
4364
4699
  confidenceKind: "verified-call-site",
4365
4700
  evidence: {
4366
- file: import_node_path24.default.relative(serviceDir, file.path),
4701
+ file: import_node_path26.default.relative(serviceDir, file.path),
4367
4702
  line,
4368
4703
  snippet: snippet(file.content, line)
4369
4704
  }
@@ -4474,7 +4809,7 @@ init_cjs_shims();
4474
4809
 
4475
4810
  // src/extract/infra/docker-compose.ts
4476
4811
  init_cjs_shims();
4477
- var import_node_path25 = __toESM(require("path"), 1);
4812
+ var import_node_path27 = __toESM(require("path"), 1);
4478
4813
  var import_types18 = require("@neat.is/types");
4479
4814
 
4480
4815
  // src/extract/infra/shared.ts
@@ -4512,7 +4847,7 @@ function dependsOnList(value) {
4512
4847
  }
4513
4848
  function serviceNameToServiceNode(name, services) {
4514
4849
  for (const s of services) {
4515
- if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
4850
+ if (s.node.name === name || import_node_path27.default.basename(s.dir) === name) return s.node.id;
4516
4851
  }
4517
4852
  return null;
4518
4853
  }
@@ -4521,8 +4856,8 @@ async function addComposeInfra(graph, scanPath, services) {
4521
4856
  let edgesAdded = 0;
4522
4857
  let composePath = null;
4523
4858
  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)) {
4859
+ const abs = import_node_path27.default.join(scanPath, name);
4860
+ if (await exists2(abs)) {
4526
4861
  composePath = abs;
4527
4862
  break;
4528
4863
  }
@@ -4534,13 +4869,13 @@ async function addComposeInfra(graph, scanPath, services) {
4534
4869
  } catch (err) {
4535
4870
  recordExtractionError(
4536
4871
  "infra docker-compose",
4537
- import_node_path25.default.relative(scanPath, composePath),
4872
+ import_node_path27.default.relative(scanPath, composePath),
4538
4873
  err
4539
4874
  );
4540
4875
  return { nodesAdded, edgesAdded };
4541
4876
  }
4542
4877
  if (!compose?.services) return { nodesAdded, edgesAdded };
4543
- const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
4878
+ const evidenceFile = import_node_path27.default.relative(scanPath, composePath).split(import_node_path27.default.sep).join("/");
4544
4879
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4545
4880
  for (const [composeName, svc] of Object.entries(compose.services)) {
4546
4881
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4582,8 +4917,8 @@ async function addComposeInfra(graph, scanPath, services) {
4582
4917
 
4583
4918
  // src/extract/infra/dockerfile.ts
4584
4919
  init_cjs_shims();
4585
- var import_node_path26 = __toESM(require("path"), 1);
4586
- var import_node_fs14 = require("fs");
4920
+ var import_node_path28 = __toESM(require("path"), 1);
4921
+ var import_node_fs16 = require("fs");
4587
4922
  var import_types19 = require("@neat.is/types");
4588
4923
  function runtimeImage(content) {
4589
4924
  const lines = content.split("\n");
@@ -4603,15 +4938,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4603
4938
  let nodesAdded = 0;
4604
4939
  let edgesAdded = 0;
4605
4940
  for (const service of services) {
4606
- const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
4607
- if (!await exists(dockerfilePath)) continue;
4941
+ const dockerfilePath = import_node_path28.default.join(service.dir, "Dockerfile");
4942
+ if (!await exists2(dockerfilePath)) continue;
4608
4943
  let content;
4609
4944
  try {
4610
- content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
4945
+ content = await import_node_fs16.promises.readFile(dockerfilePath, "utf8");
4611
4946
  } catch (err) {
4612
4947
  recordExtractionError(
4613
4948
  "infra dockerfile",
4614
- import_node_path26.default.relative(scanPath, dockerfilePath),
4949
+ import_node_path28.default.relative(scanPath, dockerfilePath),
4615
4950
  err
4616
4951
  );
4617
4952
  continue;
@@ -4623,17 +4958,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4623
4958
  graph.addNode(node.id, node);
4624
4959
  nodesAdded++;
4625
4960
  }
4626
- const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, node.id, import_types19.EdgeType.RUNS_ON);
4961
+ const relDockerfile = toPosix2(import_node_path28.default.relative(service.dir, dockerfilePath));
4962
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4963
+ graph,
4964
+ service.pkg.name,
4965
+ service.node.id,
4966
+ relDockerfile
4967
+ );
4968
+ nodesAdded += fn;
4969
+ edgesAdded += fe;
4970
+ const edgeId = (0, import_types5.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
4627
4971
  if (!graph.hasEdge(edgeId)) {
4628
4972
  const edge = {
4629
4973
  id: edgeId,
4630
- source: service.node.id,
4974
+ source: fileNodeId,
4631
4975
  target: node.id,
4632
4976
  type: import_types19.EdgeType.RUNS_ON,
4633
4977
  provenance: import_types19.Provenance.EXTRACTED,
4634
4978
  confidence: (0, import_types19.confidenceForExtracted)("structural"),
4635
4979
  evidence: {
4636
- file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4980
+ file: toPosix2(import_node_path28.default.relative(scanPath, dockerfilePath))
4637
4981
  }
4638
4982
  };
4639
4983
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4645,21 +4989,21 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4645
4989
 
4646
4990
  // src/extract/infra/terraform.ts
4647
4991
  init_cjs_shims();
4648
- var import_node_fs15 = require("fs");
4649
- var import_node_path27 = __toESM(require("path"), 1);
4992
+ var import_node_fs17 = require("fs");
4993
+ var import_node_path29 = __toESM(require("path"), 1);
4650
4994
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4651
4995
  async function walkTfFiles(start, depth = 0, max = 5) {
4652
4996
  if (depth > max) return [];
4653
4997
  const out = [];
4654
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4998
+ const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4655
4999
  for (const entry of entries) {
4656
5000
  if (entry.isDirectory()) {
4657
5001
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4658
- const child = import_node_path27.default.join(start, entry.name);
5002
+ const child = import_node_path29.default.join(start, entry.name);
4659
5003
  if (await isPythonVenvDir(child)) continue;
4660
5004
  out.push(...await walkTfFiles(child, depth + 1, max));
4661
5005
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4662
- out.push(import_node_path27.default.join(start, entry.name));
5006
+ out.push(import_node_path29.default.join(start, entry.name));
4663
5007
  }
4664
5008
  }
4665
5009
  return out;
@@ -4668,7 +5012,7 @@ async function addTerraformResources(graph, scanPath) {
4668
5012
  let nodesAdded = 0;
4669
5013
  const files = await walkTfFiles(scanPath);
4670
5014
  for (const file of files) {
4671
- const content = await import_node_fs15.promises.readFile(file, "utf8");
5015
+ const content = await import_node_fs17.promises.readFile(file, "utf8");
4672
5016
  RESOURCE_RE.lastIndex = 0;
4673
5017
  let m;
4674
5018
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -4686,8 +5030,8 @@ async function addTerraformResources(graph, scanPath) {
4686
5030
 
4687
5031
  // src/extract/infra/k8s.ts
4688
5032
  init_cjs_shims();
4689
- var import_node_fs16 = require("fs");
4690
- var import_node_path28 = __toESM(require("path"), 1);
5033
+ var import_node_fs18 = require("fs");
5034
+ var import_node_path30 = __toESM(require("path"), 1);
4691
5035
  var import_yaml3 = require("yaml");
4692
5036
  var K8S_KIND_TO_INFRA_KIND = {
4693
5037
  Service: "k8s-service",
@@ -4701,15 +5045,15 @@ var K8S_KIND_TO_INFRA_KIND = {
4701
5045
  async function walkYamlFiles2(start, depth = 0, max = 5) {
4702
5046
  if (depth > max) return [];
4703
5047
  const out = [];
4704
- const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
5048
+ const entries = await import_node_fs18.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4705
5049
  for (const entry of entries) {
4706
5050
  if (entry.isDirectory()) {
4707
5051
  if (IGNORED_DIRS.has(entry.name)) continue;
4708
- const child = import_node_path28.default.join(start, entry.name);
5052
+ const child = import_node_path30.default.join(start, entry.name);
4709
5053
  if (await isPythonVenvDir(child)) continue;
4710
5054
  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));
5055
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path30.default.extname(entry.name))) {
5056
+ out.push(import_node_path30.default.join(start, entry.name));
4713
5057
  }
4714
5058
  }
4715
5059
  return out;
@@ -4718,7 +5062,7 @@ async function addK8sResources(graph, scanPath) {
4718
5062
  let nodesAdded = 0;
4719
5063
  const files = await walkYamlFiles2(scanPath);
4720
5064
  for (const file of files) {
4721
- const content = await import_node_fs16.promises.readFile(file, "utf8");
5065
+ const content = await import_node_fs18.promises.readFile(file, "utf8");
4722
5066
  let docs;
4723
5067
  try {
4724
5068
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -4753,12 +5097,12 @@ async function addInfra(graph, scanPath, services) {
4753
5097
  }
4754
5098
 
4755
5099
  // src/extract/index.ts
4756
- var import_node_path30 = __toESM(require("path"), 1);
5100
+ var import_node_path32 = __toESM(require("path"), 1);
4757
5101
 
4758
5102
  // src/extract/retire.ts
4759
5103
  init_cjs_shims();
4760
- var import_node_fs17 = require("fs");
4761
- var import_node_path29 = __toESM(require("path"), 1);
5104
+ var import_node_fs19 = require("fs");
5105
+ var import_node_path31 = __toESM(require("path"), 1);
4762
5106
  var import_types20 = require("@neat.is/types");
4763
5107
  function dropOrphanedFileNodes(graph) {
4764
5108
  const orphans = [];
@@ -4779,11 +5123,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4779
5123
  if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
4780
5124
  const evidenceFile = edge.evidence?.file;
4781
5125
  if (!evidenceFile) return;
4782
- if (import_node_path29.default.isAbsolute(evidenceFile)) {
4783
- if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
5126
+ if (import_node_path31.default.isAbsolute(evidenceFile)) {
5127
+ if (!(0, import_node_fs19.existsSync)(evidenceFile)) toDrop.push(id);
4784
5128
  return;
4785
5129
  }
4786
- const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
5130
+ const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path31.default.join(base, evidenceFile)));
4787
5131
  if (!found) toDrop.push(id);
4788
5132
  });
4789
5133
  for (const id of toDrop) graph.dropEdge(id);
@@ -4821,7 +5165,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4821
5165
  }
4822
5166
  const droppedEntries = drainDroppedExtracted();
4823
5167
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4824
- const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
5168
+ const rejectedPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.errorsPath), "rejected.ndjson");
4825
5169
  try {
4826
5170
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4827
5171
  } catch (err) {
@@ -4855,7 +5199,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4855
5199
 
4856
5200
  // src/diff.ts
4857
5201
  init_cjs_shims();
4858
- var import_node_fs18 = require("fs");
5202
+ var import_node_fs20 = require("fs");
4859
5203
  async function loadSnapshotForDiff(target) {
4860
5204
  if (/^https?:\/\//i.test(target)) {
4861
5205
  const res = await fetch(target);
@@ -4864,7 +5208,7 @@ async function loadSnapshotForDiff(target) {
4864
5208
  }
4865
5209
  return await res.json();
4866
5210
  }
4867
- const raw = await import_node_fs18.promises.readFile(target, "utf8");
5211
+ const raw = await import_node_fs20.promises.readFile(target, "utf8");
4868
5212
  return JSON.parse(raw);
4869
5213
  }
4870
5214
  function indexEntries(entries) {
@@ -4932,8 +5276,8 @@ function canonicalJson(value) {
4932
5276
 
4933
5277
  // src/persist.ts
4934
5278
  init_cjs_shims();
4935
- var import_node_fs19 = require("fs");
4936
- var import_node_path31 = __toESM(require("path"), 1);
5279
+ var import_node_fs21 = require("fs");
5280
+ var import_node_path33 = __toESM(require("path"), 1);
4937
5281
  var import_types21 = require("@neat.is/types");
4938
5282
  var SCHEMA_VERSION = 4;
4939
5283
  function migrateV1ToV2(payload) {
@@ -4970,7 +5314,7 @@ function migrateV2ToV3(payload) {
4970
5314
  return { ...payload, schemaVersion: 3 };
4971
5315
  }
4972
5316
  async function ensureDir(filePath) {
4973
- await import_node_fs19.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
5317
+ await import_node_fs21.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
4974
5318
  }
4975
5319
  async function saveGraphToDisk(graph, outPath) {
4976
5320
  await ensureDir(outPath);
@@ -4980,13 +5324,13 @@ async function saveGraphToDisk(graph, outPath) {
4980
5324
  graph: graph.export()
4981
5325
  };
4982
5326
  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);
5327
+ await import_node_fs21.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
5328
+ await import_node_fs21.promises.rename(tmp, outPath);
4985
5329
  }
4986
5330
  async function loadGraphFromDisk(graph, outPath) {
4987
5331
  let raw;
4988
5332
  try {
4989
- raw = await import_node_fs19.promises.readFile(outPath, "utf8");
5333
+ raw = await import_node_fs21.promises.readFile(outPath, "utf8");
4990
5334
  } catch (err) {
4991
5335
  if (err.code === "ENOENT") return;
4992
5336
  throw err;
@@ -5045,23 +5389,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5045
5389
 
5046
5390
  // src/projects.ts
5047
5391
  init_cjs_shims();
5048
- var import_node_path32 = __toESM(require("path"), 1);
5392
+ var import_node_path34 = __toESM(require("path"), 1);
5049
5393
  function pathsForProject(project, baseDir) {
5050
5394
  if (project === DEFAULT_PROJECT) {
5051
5395
  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")
5396
+ snapshotPath: import_node_path34.default.join(baseDir, "graph.json"),
5397
+ errorsPath: import_node_path34.default.join(baseDir, "errors.ndjson"),
5398
+ staleEventsPath: import_node_path34.default.join(baseDir, "stale-events.ndjson"),
5399
+ embeddingsCachePath: import_node_path34.default.join(baseDir, "embeddings.json"),
5400
+ policyViolationsPath: import_node_path34.default.join(baseDir, "policy-violations.ndjson")
5057
5401
  };
5058
5402
  }
5059
5403
  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`)
5404
+ snapshotPath: import_node_path34.default.join(baseDir, `${project}.json`),
5405
+ errorsPath: import_node_path34.default.join(baseDir, `errors.${project}.ndjson`),
5406
+ staleEventsPath: import_node_path34.default.join(baseDir, `stale-events.${project}.ndjson`),
5407
+ embeddingsCachePath: import_node_path34.default.join(baseDir, `embeddings.${project}.json`),
5408
+ policyViolationsPath: import_node_path34.default.join(baseDir, `policy-violations.${project}.ndjson`)
5065
5409
  };
5066
5410
  }
5067
5411
  var Projects = class {
@@ -5101,23 +5445,23 @@ function parseExtraProjects(raw) {
5101
5445
 
5102
5446
  // src/registry.ts
5103
5447
  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);
5448
+ var import_node_fs22 = require("fs");
5449
+ var import_node_os3 = __toESM(require("os"), 1);
5450
+ var import_node_path35 = __toESM(require("path"), 1);
5107
5451
  var import_types22 = require("@neat.is/types");
5108
5452
  function neatHome() {
5109
5453
  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");
5454
+ if (override && override.length > 0) return import_node_path35.default.resolve(override);
5455
+ return import_node_path35.default.join(import_node_os3.default.homedir(), ".neat");
5112
5456
  }
5113
5457
  function registryPath() {
5114
- return import_node_path33.default.join(neatHome(), "projects.json");
5458
+ return import_node_path35.default.join(neatHome(), "projects.json");
5115
5459
  }
5116
5460
  async function readRegistry() {
5117
5461
  const file = registryPath();
5118
5462
  let raw;
5119
5463
  try {
5120
- raw = await import_node_fs20.promises.readFile(file, "utf8");
5464
+ raw = await import_node_fs22.promises.readFile(file, "utf8");
5121
5465
  } catch (err) {
5122
5466
  if (err.code === "ENOENT") {
5123
5467
  return { version: 1, projects: [] };
@@ -5594,6 +5938,105 @@ function registerRoutes(scope, ctx) {
5594
5938
  violations
5595
5939
  };
5596
5940
  });
5941
+ scope.get("/extend/list-uninstrumented", async (req, reply) => {
5942
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5943
+ if (!proj) return;
5944
+ if (!proj.scanPath) {
5945
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5946
+ }
5947
+ try {
5948
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
5949
+ return { libraries: results };
5950
+ } catch (err) {
5951
+ return reply.code(500).send({ error: err.message });
5952
+ }
5953
+ });
5954
+ scope.get("/extend/lookup", async (req, reply) => {
5955
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5956
+ if (!proj) return;
5957
+ const { library, version } = req.query;
5958
+ if (!library) {
5959
+ return reply.code(400).send({ error: "query parameter `library` is required" });
5960
+ }
5961
+ const result = lookupInstrumentation(library, version);
5962
+ if (!result) {
5963
+ return reply.code(404).send({ error: "library not found in registry", library });
5964
+ }
5965
+ return result;
5966
+ });
5967
+ scope.get("/extend/describe", async (req, reply) => {
5968
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5969
+ if (!proj) return;
5970
+ if (!proj.scanPath) {
5971
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5972
+ }
5973
+ try {
5974
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
5975
+ return state;
5976
+ } catch (err) {
5977
+ return reply.code(500).send({ error: err.message });
5978
+ }
5979
+ });
5980
+ scope.post("/extend/apply", async (req, reply) => {
5981
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5982
+ if (!proj) return;
5983
+ if (!proj.scanPath) {
5984
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5985
+ }
5986
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
5987
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
5988
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
5989
+ }
5990
+ try {
5991
+ const result = await applyExtension(
5992
+ { project: proj.name, scanPath: proj.scanPath },
5993
+ { library, instrumentation_package, version, registration_snippet }
5994
+ );
5995
+ return result;
5996
+ } catch (err) {
5997
+ return reply.code(500).send({ error: err.message });
5998
+ }
5999
+ });
6000
+ scope.post("/extend/dry-run", async (req, reply) => {
6001
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6002
+ if (!proj) return;
6003
+ if (!proj.scanPath) {
6004
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6005
+ }
6006
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
6007
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
6008
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
6009
+ }
6010
+ try {
6011
+ const result = await dryRunExtension(
6012
+ { project: proj.name, scanPath: proj.scanPath },
6013
+ { library, instrumentation_package, version, registration_snippet }
6014
+ );
6015
+ return result;
6016
+ } catch (err) {
6017
+ return reply.code(500).send({ error: err.message });
6018
+ }
6019
+ });
6020
+ scope.post("/extend/rollback", async (req, reply) => {
6021
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6022
+ if (!proj) return;
6023
+ if (!proj.scanPath) {
6024
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
6025
+ }
6026
+ const { library } = req.body ?? {};
6027
+ if (!library) {
6028
+ return reply.code(400).send({ error: "body must include library" });
6029
+ }
6030
+ try {
6031
+ const result = await rollbackExtension(
6032
+ { project: proj.name, scanPath: proj.scanPath },
6033
+ { library }
6034
+ );
6035
+ return result;
6036
+ } catch (err) {
6037
+ return reply.code(500).send({ error: err.message });
6038
+ }
6039
+ });
5597
6040
  }
5598
6041
  async function buildApi(opts) {
5599
6042
  const app = (0, import_fastify.default)({ logger: false });
@@ -5704,8 +6147,8 @@ init_otel_grpc();
5704
6147
 
5705
6148
  // src/search.ts
5706
6149
  init_cjs_shims();
5707
- var import_node_fs21 = require("fs");
5708
- var import_node_path36 = __toESM(require("path"), 1);
6150
+ var import_node_fs23 = require("fs");
6151
+ var import_node_path38 = __toESM(require("path"), 1);
5709
6152
  var import_node_crypto3 = require("crypto");
5710
6153
  var DEFAULT_LIMIT = 10;
5711
6154
  var NOMIC_DIM = 768;
@@ -5835,7 +6278,7 @@ async function pickEmbedder() {
5835
6278
  }
5836
6279
  async function readCache(cachePath) {
5837
6280
  try {
5838
- const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
6281
+ const raw = await import_node_fs23.promises.readFile(cachePath, "utf8");
5839
6282
  const parsed = JSON.parse(raw);
5840
6283
  if (parsed.version !== 1) return null;
5841
6284
  return parsed;
@@ -5844,8 +6287,8 @@ async function readCache(cachePath) {
5844
6287
  }
5845
6288
  }
5846
6289
  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));
6290
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(cachePath), { recursive: true });
6291
+ await import_node_fs23.promises.writeFile(cachePath, JSON.stringify(cache));
5849
6292
  }
5850
6293
  var VectorIndex = class {
5851
6294
  constructor(embedder, cachePath) {
@@ -6025,14 +6468,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
6025
6468
  async function main() {
6026
6469
  const baseDirEnv = process.env.NEAT_OUT_DIR;
6027
6470
  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");
6471
+ const baseDir = baseDirEnv ? import_node_path39.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path39.default.resolve(import_node_path39.default.dirname(legacyOutPath)) : import_node_path39.default.resolve("./neat-out");
6472
+ const defaultScanPath = import_node_path39.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
6030
6473
  const registry = new Projects();
6031
6474
  await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
6032
6475
  for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
6033
6476
  const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
6034
6477
  const projectScan = process.env[envKey];
6035
- await bootProject(registry, name, projectScan ? import_node_path37.default.resolve(projectScan) : void 0, baseDir);
6478
+ await bootProject(registry, name, projectScan ? import_node_path39.default.resolve(projectScan) : void 0, baseDir);
6036
6479
  }
6037
6480
  const host = process.env.HOST ?? "0.0.0.0";
6038
6481
  const port = Number(process.env.PORT ?? 8080);