@neat.is/core 0.2.7 → 0.2.9

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.
@@ -427,19 +427,19 @@ function confidenceFromMix(edges, now = Date.now()) {
427
427
  function longestIncomingWalk(graph, start, maxDepth) {
428
428
  let best = { path: [start], edges: [] };
429
429
  const visited = /* @__PURE__ */ new Set([start]);
430
- function step(node, path29, edges) {
431
- if (path29.length > best.path.length) {
432
- best = { path: [...path29], edges: [...edges] };
430
+ function step(node, path31, edges) {
431
+ if (path31.length > best.path.length) {
432
+ best = { path: [...path31], edges: [...edges] };
433
433
  }
434
- if (path29.length - 1 >= maxDepth) return;
434
+ if (path31.length - 1 >= maxDepth) return;
435
435
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
436
436
  for (const [srcId, edge] of incoming) {
437
437
  if (visited.has(srcId)) continue;
438
438
  visited.add(srcId);
439
- path29.push(srcId);
439
+ path31.push(srcId);
440
440
  edges.push(edge);
441
- step(srcId, path29, edges);
442
- path29.pop();
441
+ step(srcId, path31, edges);
442
+ path31.pop();
443
443
  edges.pop();
444
444
  visited.delete(srcId);
445
445
  }
@@ -634,6 +634,69 @@ import {
634
634
  PolicyFileSchema,
635
635
  Provenance as Provenance2
636
636
  } from "@neat.is/types";
637
+
638
+ // src/events.ts
639
+ import { EventEmitter } from "events";
640
+ var EVENT_BUS_CHANNEL = "event";
641
+ var NeatEventBus = class extends EventEmitter {
642
+ };
643
+ var eventBus = new NeatEventBus();
644
+ eventBus.setMaxListeners(0);
645
+ function emitNeatEvent(envelope) {
646
+ eventBus.emit(EVENT_BUS_CHANNEL, envelope);
647
+ }
648
+ function attachGraphToEventBus(graph, opts) {
649
+ const { project } = opts;
650
+ const onNodeAdded = (payload) => {
651
+ emitNeatEvent({
652
+ type: "node-added",
653
+ project,
654
+ payload: { node: payload.attributes }
655
+ });
656
+ };
657
+ const onNodeDropped = (payload) => {
658
+ emitNeatEvent({
659
+ type: "node-removed",
660
+ project,
661
+ payload: { id: payload.key }
662
+ });
663
+ };
664
+ const onEdgeAdded = (payload) => {
665
+ emitNeatEvent({
666
+ type: "edge-added",
667
+ project,
668
+ payload: { edge: payload.attributes }
669
+ });
670
+ };
671
+ const onEdgeDropped = (payload) => {
672
+ emitNeatEvent({
673
+ type: "edge-removed",
674
+ project,
675
+ payload: { id: payload.key }
676
+ });
677
+ };
678
+ const onNodeAttrsUpdated = (payload) => {
679
+ emitNeatEvent({
680
+ type: "node-updated",
681
+ project,
682
+ payload: { id: payload.key, changes: payload.attributes }
683
+ });
684
+ };
685
+ graph.on("nodeAdded", onNodeAdded);
686
+ graph.on("nodeDropped", onNodeDropped);
687
+ graph.on("edgeAdded", onEdgeAdded);
688
+ graph.on("edgeDropped", onEdgeDropped);
689
+ graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
690
+ return () => {
691
+ graph.off("nodeAdded", onNodeAdded);
692
+ graph.off("nodeDropped", onNodeDropped);
693
+ graph.off("edgeAdded", onEdgeAdded);
694
+ graph.off("edgeDropped", onEdgeDropped);
695
+ graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
696
+ };
697
+ }
698
+
699
+ // src/policy.ts
637
700
  var DEFAULT_ACTION_BY_SEVERITY = {
638
701
  info: "log",
639
702
  warning: "alert",
@@ -915,9 +978,11 @@ async function loadPolicyFile(policyPath) {
915
978
  }
916
979
  var PolicyViolationsLog = class {
917
980
  path;
981
+ project;
918
982
  seen = null;
919
- constructor(logPath) {
983
+ constructor(logPath, project = DEFAULT_PROJECT) {
920
984
  this.path = logPath;
985
+ this.project = project;
921
986
  }
922
987
  async append(v) {
923
988
  if (!this.seen) await this.hydrate();
@@ -925,6 +990,11 @@ var PolicyViolationsLog = class {
925
990
  this.seen.add(v.id);
926
991
  await fs2.mkdir(path2.dirname(this.path), { recursive: true });
927
992
  await fs2.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
993
+ emitNeatEvent({
994
+ type: "policy-violation",
995
+ project: this.project,
996
+ payload: { violation: v }
997
+ });
928
998
  return true;
929
999
  }
930
1000
  async readAll() {
@@ -1405,6 +1475,7 @@ async function markStaleEdges(graph, options = {}) {
1405
1475
  const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
1406
1476
  const now = options.now ?? Date.now();
1407
1477
  const events = [];
1478
+ const project = options.project ?? DEFAULT_PROJECT;
1408
1479
  graph.forEachEdge((id, attrs) => {
1409
1480
  const e = attrs;
1410
1481
  if (e.provenance !== Provenance3.OBSERVED) return;
@@ -1424,6 +1495,15 @@ async function markStaleEdges(graph, options = {}) {
1424
1495
  lastObserved: e.lastObserved,
1425
1496
  transitionedAt: new Date(now).toISOString()
1426
1497
  });
1498
+ emitNeatEvent({
1499
+ type: "stale-transition",
1500
+ project,
1501
+ payload: {
1502
+ edgeId: id,
1503
+ from: Provenance3.OBSERVED,
1504
+ to: Provenance3.STALE
1505
+ }
1506
+ });
1427
1507
  }
1428
1508
  });
1429
1509
  if (options.staleEventsPath && events.length > 0) {
@@ -1454,7 +1534,8 @@ function startStalenessLoop(graph, options = {}) {
1454
1534
  try {
1455
1535
  await markStaleEdges(graph, {
1456
1536
  thresholds: options.thresholds,
1457
- staleEventsPath: options.staleEventsPath
1537
+ staleEventsPath: options.staleEventsPath,
1538
+ project: options.project
1458
1539
  });
1459
1540
  if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
1460
1541
  } catch (err) {
@@ -1480,10 +1561,10 @@ async function readErrorEvents(errorsPath) {
1480
1561
  }
1481
1562
 
1482
1563
  // src/extract/services.ts
1483
- import { promises as fs6 } from "fs";
1484
- import path6 from "path";
1564
+ import { promises as fs7 } from "fs";
1565
+ import path7 from "path";
1485
1566
  import ignore from "ignore";
1486
- import { minimatch } from "minimatch";
1567
+ import { minimatch as minimatch2 } from "minimatch";
1487
1568
  import { NodeType as NodeType4, serviceId as serviceId2 } from "@neat.is/types";
1488
1569
 
1489
1570
  // src/extract/shared.ts
@@ -1594,6 +1675,72 @@ function pythonToPackage(service) {
1594
1675
  };
1595
1676
  }
1596
1677
 
1678
+ // src/extract/owners.ts
1679
+ import { promises as fs6 } from "fs";
1680
+ import path6 from "path";
1681
+ import { minimatch } from "minimatch";
1682
+ async function loadCodeowners(scanPath) {
1683
+ const candidates = [
1684
+ path6.join(scanPath, "CODEOWNERS"),
1685
+ path6.join(scanPath, ".github", "CODEOWNERS")
1686
+ ];
1687
+ for (const file of candidates) {
1688
+ if (await exists(file)) {
1689
+ const raw = await fs6.readFile(file, "utf8");
1690
+ return parseCodeowners(raw);
1691
+ }
1692
+ }
1693
+ return null;
1694
+ }
1695
+ function parseCodeowners(raw) {
1696
+ const rules = [];
1697
+ for (const line of raw.split("\n")) {
1698
+ const trimmed = line.trim();
1699
+ if (!trimmed || trimmed.startsWith("#")) continue;
1700
+ const match = /^(\S+)\s+(.+)$/.exec(trimmed);
1701
+ if (!match) continue;
1702
+ rules.push({ pattern: match[1], owners: match[2].trim() });
1703
+ }
1704
+ return { rules };
1705
+ }
1706
+ function matchOwner(file, repoPath) {
1707
+ const normalized = repoPath.split(path6.sep).join("/");
1708
+ for (const rule of file.rules) {
1709
+ if (matchesPattern(rule.pattern, normalized)) return rule.owners;
1710
+ }
1711
+ return null;
1712
+ }
1713
+ function matchesPattern(rawPattern, repoPath) {
1714
+ let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
1715
+ if (pattern === "*") return !repoPath.includes("/");
1716
+ if (pattern === "**" || pattern === "") return true;
1717
+ if (pattern.endsWith("/")) pattern = pattern + "**";
1718
+ if (minimatch(repoPath, pattern, { dot: true })) return true;
1719
+ if (!pattern.includes("*") && minimatch(repoPath, pattern + "/**", { dot: true })) return true;
1720
+ return false;
1721
+ }
1722
+ async function readPackageJsonAuthor(serviceDir) {
1723
+ const pkgPath = path6.join(serviceDir, "package.json");
1724
+ if (!await exists(pkgPath)) return null;
1725
+ try {
1726
+ const pkg = await readJson(pkgPath);
1727
+ if (!pkg.author) return null;
1728
+ if (typeof pkg.author === "string") return pkg.author;
1729
+ if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
1730
+ return null;
1731
+ } catch {
1732
+ return null;
1733
+ }
1734
+ }
1735
+ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
1736
+ if (codeowners && repoPath !== void 0) {
1737
+ const owner = matchOwner(codeowners, repoPath);
1738
+ if (owner) return owner;
1739
+ }
1740
+ const author = await readPackageJsonAuthor(serviceDir);
1741
+ return author ?? void 0;
1742
+ }
1743
+
1597
1744
  // src/extract/services.ts
1598
1745
  var DEFAULT_SCAN_DEPTH = 5;
1599
1746
  function parseScanDepth() {
@@ -1610,21 +1757,21 @@ function workspaceGlobs(pkg) {
1610
1757
  return null;
1611
1758
  }
1612
1759
  async function loadGitignore(scanPath) {
1613
- const gitignorePath = path6.join(scanPath, ".gitignore");
1760
+ const gitignorePath = path7.join(scanPath, ".gitignore");
1614
1761
  if (!await exists(gitignorePath)) return null;
1615
- const raw = await fs6.readFile(gitignorePath, "utf8");
1762
+ const raw = await fs7.readFile(gitignorePath, "utf8");
1616
1763
  return ignore().add(raw);
1617
1764
  }
1618
1765
  async function walkDirs(start, scanPath, options, visit) {
1619
1766
  async function recurse(current, depth) {
1620
1767
  if (depth > options.maxDepth) return;
1621
- const entries = await fs6.readdir(current, { withFileTypes: true }).catch(() => []);
1768
+ const entries = await fs7.readdir(current, { withFileTypes: true }).catch(() => []);
1622
1769
  for (const entry of entries) {
1623
1770
  if (!entry.isDirectory()) continue;
1624
1771
  if (IGNORED_DIRS.has(entry.name)) continue;
1625
- const child = path6.join(current, entry.name);
1772
+ const child = path7.join(current, entry.name);
1626
1773
  if (options.ig) {
1627
- const rel = path6.relative(scanPath, child).split(path6.sep).join("/");
1774
+ const rel = path7.relative(scanPath, child).split(path7.sep).join("/");
1628
1775
  if (rel && options.ig.ignores(rel + "/")) continue;
1629
1776
  }
1630
1777
  await visit(child);
@@ -1639,8 +1786,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1639
1786
  for (const raw of globs) {
1640
1787
  const pattern = raw.replace(/^\.\//, "");
1641
1788
  if (!pattern.includes("*")) {
1642
- const candidate = path6.join(scanPath, pattern);
1643
- if (await exists(path6.join(candidate, "package.json"))) found.add(candidate);
1789
+ const candidate = path7.join(scanPath, pattern);
1790
+ if (await exists(path7.join(candidate, "package.json"))) found.add(candidate);
1644
1791
  continue;
1645
1792
  }
1646
1793
  const segments = pattern.split("/");
@@ -1649,13 +1796,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1649
1796
  if (seg.includes("*")) break;
1650
1797
  staticSegments.push(seg);
1651
1798
  }
1652
- const start = path6.join(scanPath, ...staticSegments);
1799
+ const start = path7.join(scanPath, ...staticSegments);
1653
1800
  if (!await exists(start)) continue;
1654
1801
  const hasDoubleStar = pattern.includes("**");
1655
1802
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
1656
1803
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
1657
- const rel = path6.relative(scanPath, dir).split(path6.sep).join("/");
1658
- if (minimatch(rel, pattern) && await exists(path6.join(dir, "package.json"))) {
1804
+ const rel = path7.relative(scanPath, dir).split(path7.sep).join("/");
1805
+ if (minimatch2(rel, pattern) && await exists(path7.join(dir, "package.json"))) {
1659
1806
  found.add(dir);
1660
1807
  }
1661
1808
  });
@@ -1663,9 +1810,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1663
1810
  return [...found];
1664
1811
  }
1665
1812
  async function discoverNodeService(scanPath, dir) {
1666
- const pkgPath = path6.join(dir, "package.json");
1813
+ const pkgPath = path7.join(dir, "package.json");
1667
1814
  if (!await exists(pkgPath)) return null;
1668
- const pkg = await readJson(pkgPath);
1815
+ let pkg;
1816
+ try {
1817
+ pkg = await readJson(pkgPath);
1818
+ } catch (err) {
1819
+ console.warn(
1820
+ `[neat] services skipped ${path7.relative(scanPath, pkgPath)}: ${err.message}`
1821
+ );
1822
+ return null;
1823
+ }
1669
1824
  if (!pkg.name) return null;
1670
1825
  const node = {
1671
1826
  id: serviceId2(pkg.name),
@@ -1674,7 +1829,7 @@ async function discoverNodeService(scanPath, dir) {
1674
1829
  language: "javascript",
1675
1830
  version: pkg.version,
1676
1831
  dependencies: pkg.dependencies ?? {},
1677
- repoPath: path6.relative(scanPath, dir),
1832
+ repoPath: path7.relative(scanPath, dir),
1678
1833
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
1679
1834
  };
1680
1835
  return { pkg, dir, node };
@@ -1690,13 +1845,22 @@ async function discoverPyService(scanPath, dir) {
1690
1845
  language: "python",
1691
1846
  version: py.version,
1692
1847
  dependencies: py.dependencies,
1693
- repoPath: path6.relative(scanPath, dir)
1848
+ repoPath: path7.relative(scanPath, dir)
1694
1849
  };
1695
1850
  return { pkg, dir, node };
1696
1851
  }
1697
1852
  async function discoverServices(scanPath) {
1698
- const rootPkgPath = path6.join(scanPath, "package.json");
1699
- const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
1853
+ const rootPkgPath = path7.join(scanPath, "package.json");
1854
+ let rootPkg = null;
1855
+ if (await exists(rootPkgPath)) {
1856
+ try {
1857
+ rootPkg = await readJson(rootPkgPath);
1858
+ } catch (err) {
1859
+ console.warn(
1860
+ `[neat] services workspaces skipped ${path7.relative(scanPath, rootPkgPath)}: ${err.message}`
1861
+ );
1862
+ }
1863
+ }
1700
1864
  const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
1701
1865
  const candidateDirs = [];
1702
1866
  if (wsGlobs) {
@@ -1709,9 +1873,9 @@ async function discoverServices(scanPath) {
1709
1873
  scanPath,
1710
1874
  { maxDepth: parseScanDepth(), ig },
1711
1875
  async (dir) => {
1712
- if (await exists(path6.join(dir, "package.json"))) {
1876
+ if (await exists(path7.join(dir, "package.json"))) {
1713
1877
  candidateDirs.push(dir);
1714
- } else if (await exists(path6.join(dir, "pyproject.toml")) || await exists(path6.join(dir, "requirements.txt")) || await exists(path6.join(dir, "setup.py"))) {
1878
+ } else if (await exists(path7.join(dir, "pyproject.toml")) || await exists(path7.join(dir, "requirements.txt")) || await exists(path7.join(dir, "setup.py"))) {
1715
1879
  candidateDirs.push(dir);
1716
1880
  }
1717
1881
  }
@@ -1725,8 +1889,8 @@ async function discoverServices(scanPath) {
1725
1889
  if (!service) continue;
1726
1890
  const existingDir = seen.get(service.node.name);
1727
1891
  if (existingDir !== void 0) {
1728
- const a = path6.relative(scanPath, existingDir) || ".";
1729
- const b = path6.relative(scanPath, dir) || ".";
1892
+ const a = path7.relative(scanPath, existingDir) || ".";
1893
+ const b = path7.relative(scanPath, dir) || ".";
1730
1894
  console.warn(
1731
1895
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
1732
1896
  );
@@ -1735,6 +1899,11 @@ async function discoverServices(scanPath) {
1735
1899
  seen.set(service.node.name, dir);
1736
1900
  out.push(service);
1737
1901
  }
1902
+ const codeowners = await loadCodeowners(scanPath);
1903
+ for (const service of out) {
1904
+ const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
1905
+ if (owner !== void 0) service.node.owner = owner;
1906
+ }
1738
1907
  return out;
1739
1908
  }
1740
1909
  function addServiceNodes(graph, services) {
@@ -1757,8 +1926,8 @@ function addServiceNodes(graph, services) {
1757
1926
  }
1758
1927
 
1759
1928
  // src/extract/aliases.ts
1760
- import path7 from "path";
1761
- import { promises as fs7 } from "fs";
1929
+ import path8 from "path";
1930
+ import { promises as fs8 } from "fs";
1762
1931
  import { parseAllDocuments } from "yaml";
1763
1932
  import { NodeType as NodeType5 } from "@neat.is/types";
1764
1933
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -1785,21 +1954,29 @@ function indexServicesByName(services) {
1785
1954
  const map = /* @__PURE__ */ new Map();
1786
1955
  for (const s of services) {
1787
1956
  map.set(s.node.name, s.node.id);
1788
- map.set(path7.basename(s.dir), s.node.id);
1957
+ map.set(path8.basename(s.dir), s.node.id);
1789
1958
  }
1790
1959
  return map;
1791
1960
  }
1792
1961
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
1793
1962
  let composePath = null;
1794
1963
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
1795
- const abs = path7.join(scanPath, name);
1964
+ const abs = path8.join(scanPath, name);
1796
1965
  if (await exists(abs)) {
1797
1966
  composePath = abs;
1798
1967
  break;
1799
1968
  }
1800
1969
  }
1801
1970
  if (!composePath) return;
1802
- const compose = await readYaml(composePath);
1971
+ let compose;
1972
+ try {
1973
+ compose = await readYaml(composePath);
1974
+ } catch (err) {
1975
+ console.warn(
1976
+ `[neat] aliases compose skipped ${path8.relative(scanPath, composePath)}: ${err.message}`
1977
+ );
1978
+ return;
1979
+ }
1803
1980
  if (!compose?.services) return;
1804
1981
  for (const [composeName, svc] of Object.entries(compose.services)) {
1805
1982
  const serviceId3 = serviceIndex.get(composeName);
@@ -1838,9 +2015,17 @@ function parseDockerfileLabels(content) {
1838
2015
  }
1839
2016
  async function collectDockerfileAliases(graph, services) {
1840
2017
  for (const service of services) {
1841
- const dockerfilePath = path7.join(service.dir, "Dockerfile");
2018
+ const dockerfilePath = path8.join(service.dir, "Dockerfile");
1842
2019
  if (!await exists(dockerfilePath)) continue;
1843
- const content = await fs7.readFile(dockerfilePath, "utf8");
2020
+ let content;
2021
+ try {
2022
+ content = await fs8.readFile(dockerfilePath, "utf8");
2023
+ } catch (err) {
2024
+ console.warn(
2025
+ `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2026
+ );
2027
+ continue;
2028
+ }
1844
2029
  const aliases = parseDockerfileLabels(content);
1845
2030
  if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
1846
2031
  }
@@ -1848,13 +2033,13 @@ async function collectDockerfileAliases(graph, services) {
1848
2033
  async function walkYamlFiles(start, depth = 0, max = 5) {
1849
2034
  if (depth > max) return [];
1850
2035
  const out = [];
1851
- const entries = await fs7.readdir(start, { withFileTypes: true }).catch(() => []);
2036
+ const entries = await fs8.readdir(start, { withFileTypes: true }).catch(() => []);
1852
2037
  for (const entry of entries) {
1853
2038
  if (entry.isDirectory()) {
1854
2039
  if (IGNORED_DIRS.has(entry.name)) continue;
1855
- out.push(...await walkYamlFiles(path7.join(start, entry.name), depth + 1, max));
1856
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path7.extname(entry.name))) {
1857
- out.push(path7.join(start, entry.name));
2040
+ out.push(...await walkYamlFiles(path8.join(start, entry.name), depth + 1, max));
2041
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path8.extname(entry.name))) {
2042
+ out.push(path8.join(start, entry.name));
1858
2043
  }
1859
2044
  }
1860
2045
  return out;
@@ -1881,7 +2066,7 @@ function k8sServiceTarget(doc, byName) {
1881
2066
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
1882
2067
  const files = await walkYamlFiles(scanPath);
1883
2068
  for (const file of files) {
1884
- const content = await fs7.readFile(file, "utf8");
2069
+ const content = await fs8.readFile(file, "utf8");
1885
2070
  let docs;
1886
2071
  try {
1887
2072
  docs = parseAllDocuments(content).map((d) => d.toJSON());
@@ -1905,13 +2090,13 @@ async function addServiceAliases(graph, scanPath, services) {
1905
2090
  }
1906
2091
 
1907
2092
  // src/extract/databases/index.ts
1908
- import path15 from "path";
2093
+ import path16 from "path";
1909
2094
  import { EdgeType as EdgeType3, NodeType as NodeType6, Provenance as Provenance4, databaseId as databaseId2 } from "@neat.is/types";
1910
2095
 
1911
2096
  // src/extract/databases/db-config-yaml.ts
1912
- import path8 from "path";
2097
+ import path9 from "path";
1913
2098
  async function parse(serviceDir) {
1914
- const yamlPath = path8.join(serviceDir, "db-config.yaml");
2099
+ const yamlPath = path9.join(serviceDir, "db-config.yaml");
1915
2100
  if (!await exists(yamlPath)) return [];
1916
2101
  const raw = await readYaml(yamlPath);
1917
2102
  return [
@@ -1928,12 +2113,12 @@ async function parse(serviceDir) {
1928
2113
  var dbConfigYamlParser = { name: "db-config.yaml", parse };
1929
2114
 
1930
2115
  // src/extract/databases/dotenv.ts
1931
- import { promises as fs9 } from "fs";
1932
- import path10 from "path";
2116
+ import { promises as fs10 } from "fs";
2117
+ import path11 from "path";
1933
2118
 
1934
2119
  // src/extract/databases/shared.ts
1935
- import { promises as fs8 } from "fs";
1936
- import path9 from "path";
2120
+ import { promises as fs9 } from "fs";
2121
+ import path10 from "path";
1937
2122
  function schemeToEngine(scheme) {
1938
2123
  const s = scheme.toLowerCase().split("+")[0];
1939
2124
  switch (s) {
@@ -1972,14 +2157,14 @@ function parseConnectionString(url) {
1972
2157
  }
1973
2158
  async function readIfExists(filePath) {
1974
2159
  try {
1975
- return await fs8.readFile(filePath, "utf8");
2160
+ return await fs9.readFile(filePath, "utf8");
1976
2161
  } catch {
1977
2162
  return null;
1978
2163
  }
1979
2164
  }
1980
2165
  async function findFirst(serviceDir, candidates) {
1981
2166
  for (const rel of candidates) {
1982
- const abs = path9.join(serviceDir, rel);
2167
+ const abs = path10.join(serviceDir, rel);
1983
2168
  const content = await readIfExists(abs);
1984
2169
  if (content !== null) return abs;
1985
2170
  }
@@ -2030,15 +2215,15 @@ function parseDotenvLine(line) {
2030
2215
  return { key, value };
2031
2216
  }
2032
2217
  async function parse2(serviceDir) {
2033
- const entries = await fs9.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2218
+ const entries = await fs10.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2034
2219
  const configs = [];
2035
2220
  const seen = /* @__PURE__ */ new Set();
2036
2221
  for (const entry of entries) {
2037
2222
  if (!entry.isFile()) continue;
2038
2223
  const match = isConfigFile(entry.name);
2039
2224
  if (!match.match || match.fileType !== "env") continue;
2040
- const filePath = path10.join(serviceDir, entry.name);
2041
- const content = await fs9.readFile(filePath, "utf8");
2225
+ const filePath = path11.join(serviceDir, entry.name);
2226
+ const content = await fs10.readFile(filePath, "utf8");
2042
2227
  for (const line of content.split("\n")) {
2043
2228
  const parsed = parseDotenvLine(line);
2044
2229
  if (!parsed) continue;
@@ -2056,9 +2241,9 @@ async function parse2(serviceDir) {
2056
2241
  var dotenvParser = { name: ".env", parse: parse2 };
2057
2242
 
2058
2243
  // src/extract/databases/prisma.ts
2059
- import path11 from "path";
2244
+ import path12 from "path";
2060
2245
  async function parse3(serviceDir) {
2061
- const schemaPath = path11.join(serviceDir, "prisma", "schema.prisma");
2246
+ const schemaPath = path12.join(serviceDir, "prisma", "schema.prisma");
2062
2247
  const content = await readIfExists(schemaPath);
2063
2248
  if (!content) return [];
2064
2249
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2187,10 +2372,10 @@ async function parse5(serviceDir) {
2187
2372
  var knexParser = { name: "knex", parse: parse5 };
2188
2373
 
2189
2374
  // src/extract/databases/ormconfig.ts
2190
- import path12 from "path";
2375
+ import path13 from "path";
2191
2376
  async function parse6(serviceDir) {
2192
2377
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2193
- const abs = path12.join(serviceDir, candidate);
2378
+ const abs = path13.join(serviceDir, candidate);
2194
2379
  if (!await exists(abs)) continue;
2195
2380
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2196
2381
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2248,9 +2433,9 @@ async function parse7(serviceDir) {
2248
2433
  var typeormParser = { name: "typeorm", parse: parse7 };
2249
2434
 
2250
2435
  // src/extract/databases/sequelize.ts
2251
- import path13 from "path";
2436
+ import path14 from "path";
2252
2437
  async function parse8(serviceDir) {
2253
- const configPath = path13.join(serviceDir, "config", "config.json");
2438
+ const configPath = path14.join(serviceDir, "config", "config.json");
2254
2439
  if (!await exists(configPath)) return [];
2255
2440
  const raw = await readJson(configPath);
2256
2441
  const out = [];
@@ -2276,7 +2461,7 @@ async function parse8(serviceDir) {
2276
2461
  var sequelizeParser = { name: "sequelize", parse: parse8 };
2277
2462
 
2278
2463
  // src/extract/databases/docker-compose.ts
2279
- import path14 from "path";
2464
+ import path15 from "path";
2280
2465
  function portFromService(svc) {
2281
2466
  for (const raw of svc.ports ?? []) {
2282
2467
  const str = String(raw);
@@ -2303,7 +2488,7 @@ function databaseFromEnv(svc) {
2303
2488
  }
2304
2489
  async function parse9(serviceDir) {
2305
2490
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2306
- const abs = path14.join(serviceDir, name);
2491
+ const abs = path15.join(serviceDir, name);
2307
2492
  if (!await exists(abs)) continue;
2308
2493
  const raw = await readYaml(abs);
2309
2494
  if (!raw?.services) return [];
@@ -2483,7 +2668,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2483
2668
  provenance: Provenance4.EXTRACTED,
2484
2669
  ...config.sourceFile ? {
2485
2670
  evidence: {
2486
- file: path15.relative(scanPath, config.sourceFile).split(path15.sep).join("/")
2671
+ file: path16.relative(scanPath, config.sourceFile).split(path16.sep).join("/")
2487
2672
  }
2488
2673
  } : {}
2489
2674
  };
@@ -2510,15 +2695,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2510
2695
  }
2511
2696
 
2512
2697
  // src/extract/configs.ts
2513
- import { promises as fs10 } from "fs";
2514
- import path16 from "path";
2698
+ import { promises as fs11 } from "fs";
2699
+ import path17 from "path";
2515
2700
  import { EdgeType as EdgeType4, NodeType as NodeType7, Provenance as Provenance5, configId } from "@neat.is/types";
2516
2701
  async function walkConfigFiles(dir) {
2517
2702
  const out = [];
2518
2703
  async function walk(current) {
2519
- const entries = await fs10.readdir(current, { withFileTypes: true });
2704
+ const entries = await fs11.readdir(current, { withFileTypes: true });
2520
2705
  for (const entry of entries) {
2521
- const full = path16.join(current, entry.name);
2706
+ const full = path17.join(current, entry.name);
2522
2707
  if (entry.isDirectory()) {
2523
2708
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2524
2709
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -2535,13 +2720,13 @@ async function addConfigNodes(graph, services, scanPath) {
2535
2720
  for (const service of services) {
2536
2721
  const configFiles = await walkConfigFiles(service.dir);
2537
2722
  for (const file of configFiles) {
2538
- const relPath = path16.relative(scanPath, file);
2723
+ const relPath = path17.relative(scanPath, file);
2539
2724
  const node = {
2540
2725
  id: configId(relPath),
2541
2726
  type: NodeType7.ConfigNode,
2542
- name: path16.basename(file),
2727
+ name: path17.basename(file),
2543
2728
  path: relPath,
2544
- fileType: isConfigFile(path16.basename(file)).fileType
2729
+ fileType: isConfigFile(path17.basename(file)).fileType
2545
2730
  };
2546
2731
  if (!graph.hasNode(node.id)) {
2547
2732
  graph.addNode(node.id, node);
@@ -2553,7 +2738,7 @@ async function addConfigNodes(graph, services, scanPath) {
2553
2738
  target: node.id,
2554
2739
  type: EdgeType4.CONFIGURED_BY,
2555
2740
  provenance: Provenance5.EXTRACTED,
2556
- evidence: { file: relPath.split(path16.sep).join("/") }
2741
+ evidence: { file: relPath.split(path17.sep).join("/") }
2557
2742
  };
2558
2743
  if (!graph.hasEdge(edge.id)) {
2559
2744
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2568,24 +2753,24 @@ async function addConfigNodes(graph, services, scanPath) {
2568
2753
  import { EdgeType as EdgeType6, NodeType as NodeType8, Provenance as Provenance7 } from "@neat.is/types";
2569
2754
 
2570
2755
  // src/extract/calls/http.ts
2571
- import path18 from "path";
2756
+ import path19 from "path";
2572
2757
  import Parser from "tree-sitter";
2573
2758
  import JavaScript from "tree-sitter-javascript";
2574
2759
  import Python from "tree-sitter-python";
2575
2760
  import { EdgeType as EdgeType5, Provenance as Provenance6 } from "@neat.is/types";
2576
2761
 
2577
2762
  // src/extract/calls/shared.ts
2578
- import { promises as fs11 } from "fs";
2579
- import path17 from "path";
2763
+ import { promises as fs12 } from "fs";
2764
+ import path18 from "path";
2580
2765
  async function walkSourceFiles(dir) {
2581
2766
  const out = [];
2582
2767
  async function walk(current) {
2583
- const entries = await fs11.readdir(current, { withFileTypes: true }).catch(() => []);
2768
+ const entries = await fs12.readdir(current, { withFileTypes: true }).catch(() => []);
2584
2769
  for (const entry of entries) {
2585
- const full = path17.join(current, entry.name);
2770
+ const full = path18.join(current, entry.name);
2586
2771
  if (entry.isDirectory()) {
2587
2772
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2588
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path17.extname(entry.name))) {
2773
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path18.extname(entry.name))) {
2589
2774
  out.push(full);
2590
2775
  }
2591
2776
  }
@@ -2598,7 +2783,7 @@ async function loadSourceFiles(dir) {
2598
2783
  const out = [];
2599
2784
  for (const p of paths) {
2600
2785
  try {
2601
- const content = await fs11.readFile(p, "utf8");
2786
+ const content = await fs12.readFile(p, "utf8");
2602
2787
  out.push({ path: p, content });
2603
2788
  } catch {
2604
2789
  }
@@ -2654,9 +2839,9 @@ async function addHttpCallEdges(graph, services) {
2654
2839
  const knownHosts = /* @__PURE__ */ new Set();
2655
2840
  const hostToNodeId = /* @__PURE__ */ new Map();
2656
2841
  for (const service of services) {
2657
- knownHosts.add(path18.basename(service.dir));
2842
+ knownHosts.add(path19.basename(service.dir));
2658
2843
  knownHosts.add(service.pkg.name);
2659
- hostToNodeId.set(path18.basename(service.dir), service.node.id);
2844
+ hostToNodeId.set(path19.basename(service.dir), service.node.id);
2660
2845
  hostToNodeId.set(service.pkg.name, service.node.id);
2661
2846
  }
2662
2847
  let edgesAdded = 0;
@@ -2664,8 +2849,16 @@ async function addHttpCallEdges(graph, services) {
2664
2849
  const files = await loadSourceFiles(service.dir);
2665
2850
  const seenTargets = /* @__PURE__ */ new Map();
2666
2851
  for (const file of files) {
2667
- const parser = path18.extname(file.path) === ".py" ? pyParser : jsParser;
2668
- const targets = callsFromSource(file.content, parser, knownHosts);
2852
+ const parser = path19.extname(file.path) === ".py" ? pyParser : jsParser;
2853
+ let targets;
2854
+ try {
2855
+ targets = callsFromSource(file.content, parser, knownHosts);
2856
+ } catch (err) {
2857
+ console.warn(
2858
+ `[neat] http call extraction skipped ${file.path}: ${err.message}`
2859
+ );
2860
+ continue;
2861
+ }
2669
2862
  for (const t of targets) {
2670
2863
  const targetId = hostToNodeId.get(t);
2671
2864
  if (!targetId || targetId === service.node.id) continue;
@@ -2684,7 +2877,7 @@ async function addHttpCallEdges(graph, services) {
2684
2877
  type: EdgeType5.CALLS,
2685
2878
  provenance: Provenance6.EXTRACTED,
2686
2879
  evidence: {
2687
- file: path18.relative(service.dir, evidenceFile.file),
2880
+ file: path19.relative(service.dir, evidenceFile.file),
2688
2881
  line,
2689
2882
  snippet: snippet(fileContent, line)
2690
2883
  }
@@ -2699,7 +2892,7 @@ async function addHttpCallEdges(graph, services) {
2699
2892
  }
2700
2893
 
2701
2894
  // src/extract/calls/kafka.ts
2702
- import path19 from "path";
2895
+ import path20 from "path";
2703
2896
  import { infraId } from "@neat.is/types";
2704
2897
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
2705
2898
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -2726,7 +2919,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2726
2919
  kind: "kafka-topic",
2727
2920
  edgeType,
2728
2921
  evidence: {
2729
- file: path19.relative(serviceDir, file.path),
2922
+ file: path20.relative(serviceDir, file.path),
2730
2923
  line,
2731
2924
  snippet: snippet(file.content, line)
2732
2925
  }
@@ -2738,7 +2931,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2738
2931
  }
2739
2932
 
2740
2933
  // src/extract/calls/redis.ts
2741
- import path20 from "path";
2934
+ import path21 from "path";
2742
2935
  import { infraId as infraId2 } from "@neat.is/types";
2743
2936
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
2744
2937
  function redisEndpointsFromFile(file, serviceDir) {
@@ -2757,7 +2950,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2757
2950
  kind: "redis",
2758
2951
  edgeType: "CALLS",
2759
2952
  evidence: {
2760
- file: path20.relative(serviceDir, file.path),
2953
+ file: path21.relative(serviceDir, file.path),
2761
2954
  line,
2762
2955
  snippet: snippet(file.content, line)
2763
2956
  }
@@ -2767,7 +2960,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2767
2960
  }
2768
2961
 
2769
2962
  // src/extract/calls/aws.ts
2770
- import path21 from "path";
2963
+ import path22 from "path";
2771
2964
  import { infraId as infraId3 } from "@neat.is/types";
2772
2965
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
2773
2966
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -2797,7 +2990,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2797
2990
  kind,
2798
2991
  edgeType: "CALLS",
2799
2992
  evidence: {
2800
- file: path21.relative(serviceDir, file.path),
2993
+ file: path22.relative(serviceDir, file.path),
2801
2994
  line,
2802
2995
  snippet: snippet(file.content, line)
2803
2996
  }
@@ -2821,7 +3014,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2821
3014
  }
2822
3015
 
2823
3016
  // src/extract/calls/grpc.ts
2824
- import path22 from "path";
3017
+ import path23 from "path";
2825
3018
  import { infraId as infraId4 } from "@neat.is/types";
2826
3019
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
2827
3020
  function isLikelyAddress(value) {
@@ -2846,7 +3039,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
2846
3039
  kind: "grpc-service",
2847
3040
  edgeType: "CALLS",
2848
3041
  evidence: {
2849
- file: path22.relative(serviceDir, file.path),
3042
+ file: path23.relative(serviceDir, file.path),
2850
3043
  line,
2851
3044
  snippet: snippet(file.content, line)
2852
3045
  }
@@ -2922,7 +3115,7 @@ async function addCallEdges(graph, services) {
2922
3115
  }
2923
3116
 
2924
3117
  // src/extract/infra/docker-compose.ts
2925
- import path23 from "path";
3118
+ import path24 from "path";
2926
3119
  import { EdgeType as EdgeType7, Provenance as Provenance8 } from "@neat.is/types";
2927
3120
 
2928
3121
  // src/extract/infra/shared.ts
@@ -2959,7 +3152,7 @@ function dependsOnList(value) {
2959
3152
  }
2960
3153
  function serviceNameToServiceNode(name, services) {
2961
3154
  for (const s of services) {
2962
- if (s.node.name === name || path23.basename(s.dir) === name) return s.node.id;
3155
+ if (s.node.name === name || path24.basename(s.dir) === name) return s.node.id;
2963
3156
  }
2964
3157
  return null;
2965
3158
  }
@@ -2968,16 +3161,24 @@ async function addComposeInfra(graph, scanPath, services) {
2968
3161
  let edgesAdded = 0;
2969
3162
  let composePath = null;
2970
3163
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2971
- const abs = path23.join(scanPath, name);
3164
+ const abs = path24.join(scanPath, name);
2972
3165
  if (await exists(abs)) {
2973
3166
  composePath = abs;
2974
3167
  break;
2975
3168
  }
2976
3169
  }
2977
3170
  if (!composePath) return { nodesAdded, edgesAdded };
2978
- const compose = await readYaml(composePath);
3171
+ let compose;
3172
+ try {
3173
+ compose = await readYaml(composePath);
3174
+ } catch (err) {
3175
+ console.warn(
3176
+ `[neat] infra docker-compose skipped ${path24.relative(scanPath, composePath)}: ${err.message}`
3177
+ );
3178
+ return { nodesAdded, edgesAdded };
3179
+ }
2979
3180
  if (!compose?.services) return { nodesAdded, edgesAdded };
2980
- const evidenceFile = path23.relative(scanPath, composePath).split(path23.sep).join("/");
3181
+ const evidenceFile = path24.relative(scanPath, composePath).split(path24.sep).join("/");
2981
3182
  const composeNameToNodeId = /* @__PURE__ */ new Map();
2982
3183
  for (const [composeName, svc] of Object.entries(compose.services)) {
2983
3184
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3017,8 +3218,8 @@ async function addComposeInfra(graph, scanPath, services) {
3017
3218
  }
3018
3219
 
3019
3220
  // src/extract/infra/dockerfile.ts
3020
- import path24 from "path";
3021
- import { promises as fs12 } from "fs";
3221
+ import path25 from "path";
3222
+ import { promises as fs13 } from "fs";
3022
3223
  import { EdgeType as EdgeType8, Provenance as Provenance9 } from "@neat.is/types";
3023
3224
  function runtimeImage(content) {
3024
3225
  const lines = content.split("\n");
@@ -3038,9 +3239,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3038
3239
  let nodesAdded = 0;
3039
3240
  let edgesAdded = 0;
3040
3241
  for (const service of services) {
3041
- const dockerfilePath = path24.join(service.dir, "Dockerfile");
3242
+ const dockerfilePath = path25.join(service.dir, "Dockerfile");
3042
3243
  if (!await exists(dockerfilePath)) continue;
3043
- const content = await fs12.readFile(dockerfilePath, "utf8");
3244
+ let content;
3245
+ try {
3246
+ content = await fs13.readFile(dockerfilePath, "utf8");
3247
+ } catch (err) {
3248
+ console.warn(
3249
+ `[neat] infra dockerfile skipped ${path25.relative(scanPath, dockerfilePath)}: ${err.message}`
3250
+ );
3251
+ continue;
3252
+ }
3044
3253
  const image = runtimeImage(content);
3045
3254
  if (!image) continue;
3046
3255
  const node = makeInfraNode("container-image", image);
@@ -3057,7 +3266,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3057
3266
  type: EdgeType8.RUNS_ON,
3058
3267
  provenance: Provenance9.EXTRACTED,
3059
3268
  evidence: {
3060
- file: path24.relative(scanPath, dockerfilePath).split(path24.sep).join("/")
3269
+ file: path25.relative(scanPath, dockerfilePath).split(path25.sep).join("/")
3061
3270
  }
3062
3271
  };
3063
3272
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3068,19 +3277,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3068
3277
  }
3069
3278
 
3070
3279
  // src/extract/infra/terraform.ts
3071
- import { promises as fs13 } from "fs";
3072
- import path25 from "path";
3280
+ import { promises as fs14 } from "fs";
3281
+ import path26 from "path";
3073
3282
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3074
3283
  async function walkTfFiles(start, depth = 0, max = 5) {
3075
3284
  if (depth > max) return [];
3076
3285
  const out = [];
3077
- const entries = await fs13.readdir(start, { withFileTypes: true }).catch(() => []);
3286
+ const entries = await fs14.readdir(start, { withFileTypes: true }).catch(() => []);
3078
3287
  for (const entry of entries) {
3079
3288
  if (entry.isDirectory()) {
3080
3289
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3081
- out.push(...await walkTfFiles(path25.join(start, entry.name), depth + 1, max));
3290
+ out.push(...await walkTfFiles(path26.join(start, entry.name), depth + 1, max));
3082
3291
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3083
- out.push(path25.join(start, entry.name));
3292
+ out.push(path26.join(start, entry.name));
3084
3293
  }
3085
3294
  }
3086
3295
  return out;
@@ -3089,7 +3298,7 @@ async function addTerraformResources(graph, scanPath) {
3089
3298
  let nodesAdded = 0;
3090
3299
  const files = await walkTfFiles(scanPath);
3091
3300
  for (const file of files) {
3092
- const content = await fs13.readFile(file, "utf8");
3301
+ const content = await fs14.readFile(file, "utf8");
3093
3302
  RESOURCE_RE.lastIndex = 0;
3094
3303
  let m;
3095
3304
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3106,8 +3315,8 @@ async function addTerraformResources(graph, scanPath) {
3106
3315
  }
3107
3316
 
3108
3317
  // src/extract/infra/k8s.ts
3109
- import { promises as fs14 } from "fs";
3110
- import path26 from "path";
3318
+ import { promises as fs15 } from "fs";
3319
+ import path27 from "path";
3111
3320
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
3112
3321
  var K8S_KIND_TO_INFRA_KIND = {
3113
3322
  Service: "k8s-service",
@@ -3121,13 +3330,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3121
3330
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3122
3331
  if (depth > max) return [];
3123
3332
  const out = [];
3124
- const entries = await fs14.readdir(start, { withFileTypes: true }).catch(() => []);
3333
+ const entries = await fs15.readdir(start, { withFileTypes: true }).catch(() => []);
3125
3334
  for (const entry of entries) {
3126
3335
  if (entry.isDirectory()) {
3127
3336
  if (IGNORED_DIRS.has(entry.name)) continue;
3128
- out.push(...await walkYamlFiles2(path26.join(start, entry.name), depth + 1, max));
3129
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path26.extname(entry.name))) {
3130
- out.push(path26.join(start, entry.name));
3337
+ out.push(...await walkYamlFiles2(path27.join(start, entry.name), depth + 1, max));
3338
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path27.extname(entry.name))) {
3339
+ out.push(path27.join(start, entry.name));
3131
3340
  }
3132
3341
  }
3133
3342
  return out;
@@ -3136,7 +3345,7 @@ async function addK8sResources(graph, scanPath) {
3136
3345
  let nodesAdded = 0;
3137
3346
  const files = await walkYamlFiles2(scanPath);
3138
3347
  for (const file of files) {
3139
- const content = await fs14.readFile(file, "utf8");
3348
+ const content = await fs15.readFile(file, "utf8");
3140
3349
  let docs;
3141
3350
  try {
3142
3351
  docs = parseAllDocuments2(content).map((d) => d.toJSON());
@@ -3182,16 +3391,27 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3182
3391
  const phase5 = await addInfra(graph, scanPath, services);
3183
3392
  const frontiersPromoted = promoteFrontierNodes(graph);
3184
3393
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
3185
- return {
3394
+ const result = {
3186
3395
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3187
3396
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3188
3397
  frontiersPromoted
3189
3398
  };
3399
+ emitNeatEvent({
3400
+ type: "extraction-complete",
3401
+ project: opts.project ?? DEFAULT_PROJECT,
3402
+ payload: {
3403
+ project: opts.project ?? DEFAULT_PROJECT,
3404
+ fileCount: services.length,
3405
+ nodesAdded: result.nodesAdded,
3406
+ edgesAdded: result.edgesAdded
3407
+ }
3408
+ });
3409
+ return result;
3190
3410
  }
3191
3411
 
3192
3412
  // src/persist.ts
3193
- import { promises as fs15 } from "fs";
3194
- import path27 from "path";
3413
+ import { promises as fs16 } from "fs";
3414
+ import path28 from "path";
3195
3415
  var SCHEMA_VERSION = 2;
3196
3416
  function migrateV1ToV2(payload) {
3197
3417
  const nodes = payload.graph.nodes;
@@ -3205,7 +3425,7 @@ function migrateV1ToV2(payload) {
3205
3425
  return { ...payload, schemaVersion: 2 };
3206
3426
  }
3207
3427
  async function ensureDir(filePath) {
3208
- await fs15.mkdir(path27.dirname(filePath), { recursive: true });
3428
+ await fs16.mkdir(path28.dirname(filePath), { recursive: true });
3209
3429
  }
3210
3430
  async function saveGraphToDisk(graph, outPath) {
3211
3431
  await ensureDir(outPath);
@@ -3215,13 +3435,13 @@ async function saveGraphToDisk(graph, outPath) {
3215
3435
  graph: graph.export()
3216
3436
  };
3217
3437
  const tmp = `${outPath}.tmp`;
3218
- await fs15.writeFile(tmp, JSON.stringify(payload), "utf8");
3219
- await fs15.rename(tmp, outPath);
3438
+ await fs16.writeFile(tmp, JSON.stringify(payload), "utf8");
3439
+ await fs16.rename(tmp, outPath);
3220
3440
  }
3221
3441
  async function loadGraphFromDisk(graph, outPath) {
3222
3442
  let raw;
3223
3443
  try {
3224
- raw = await fs15.readFile(outPath, "utf8");
3444
+ raw = await fs16.readFile(outPath, "utf8");
3225
3445
  } catch (err) {
3226
3446
  if (err.code === "ENOENT") return;
3227
3447
  throw err;
@@ -3273,23 +3493,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3273
3493
  }
3274
3494
 
3275
3495
  // src/projects.ts
3276
- import path28 from "path";
3496
+ import path29 from "path";
3277
3497
  function pathsForProject(project, baseDir) {
3278
3498
  if (project === DEFAULT_PROJECT) {
3279
3499
  return {
3280
- snapshotPath: path28.join(baseDir, "graph.json"),
3281
- errorsPath: path28.join(baseDir, "errors.ndjson"),
3282
- staleEventsPath: path28.join(baseDir, "stale-events.ndjson"),
3283
- embeddingsCachePath: path28.join(baseDir, "embeddings.json"),
3284
- policyViolationsPath: path28.join(baseDir, "policy-violations.ndjson")
3500
+ snapshotPath: path29.join(baseDir, "graph.json"),
3501
+ errorsPath: path29.join(baseDir, "errors.ndjson"),
3502
+ staleEventsPath: path29.join(baseDir, "stale-events.ndjson"),
3503
+ embeddingsCachePath: path29.join(baseDir, "embeddings.json"),
3504
+ policyViolationsPath: path29.join(baseDir, "policy-violations.ndjson")
3285
3505
  };
3286
3506
  }
3287
3507
  return {
3288
- snapshotPath: path28.join(baseDir, `${project}.json`),
3289
- errorsPath: path28.join(baseDir, `errors.${project}.ndjson`),
3290
- staleEventsPath: path28.join(baseDir, `stale-events.${project}.ndjson`),
3291
- embeddingsCachePath: path28.join(baseDir, `embeddings.${project}.json`),
3292
- policyViolationsPath: path28.join(baseDir, `policy-violations.${project}.ndjson`)
3508
+ snapshotPath: path29.join(baseDir, `${project}.json`),
3509
+ errorsPath: path29.join(baseDir, `errors.${project}.ndjson`),
3510
+ staleEventsPath: path29.join(baseDir, `stale-events.${project}.ndjson`),
3511
+ embeddingsCachePath: path29.join(baseDir, `embeddings.${project}.json`),
3512
+ policyViolationsPath: path29.join(baseDir, `policy-violations.${project}.ndjson`)
3293
3513
  };
3294
3514
  }
3295
3515
  var Projects = class {
@@ -3327,6 +3547,175 @@ function parseExtraProjects(raw) {
3327
3547
  return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0 && p !== DEFAULT_PROJECT);
3328
3548
  }
3329
3549
 
3550
+ // src/registry.ts
3551
+ import { promises as fs17 } from "fs";
3552
+ import os2 from "os";
3553
+ import path30 from "path";
3554
+ import {
3555
+ RegistryFileSchema
3556
+ } from "@neat.is/types";
3557
+ var LOCK_TIMEOUT_MS = 5e3;
3558
+ var LOCK_RETRY_MS = 50;
3559
+ function neatHome() {
3560
+ const override = process.env.NEAT_HOME;
3561
+ if (override && override.length > 0) return path30.resolve(override);
3562
+ return path30.join(os2.homedir(), ".neat");
3563
+ }
3564
+ function registryPath() {
3565
+ return path30.join(neatHome(), "projects.json");
3566
+ }
3567
+ function registryLockPath() {
3568
+ return path30.join(neatHome(), "projects.json.lock");
3569
+ }
3570
+ async function normalizeProjectPath(input) {
3571
+ const resolved = path30.resolve(input);
3572
+ try {
3573
+ return await fs17.realpath(resolved);
3574
+ } catch {
3575
+ return resolved;
3576
+ }
3577
+ }
3578
+ async function writeAtomically(target, contents) {
3579
+ await fs17.mkdir(path30.dirname(target), { recursive: true });
3580
+ const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
3581
+ const fd = await fs17.open(tmp, "w");
3582
+ try {
3583
+ await fd.writeFile(contents, "utf8");
3584
+ await fd.sync();
3585
+ } finally {
3586
+ await fd.close();
3587
+ }
3588
+ await fs17.rename(tmp, target);
3589
+ }
3590
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
3591
+ const deadline = Date.now() + timeoutMs;
3592
+ await fs17.mkdir(path30.dirname(lockPath), { recursive: true });
3593
+ while (true) {
3594
+ try {
3595
+ const fd = await fs17.open(lockPath, "wx");
3596
+ await fd.close();
3597
+ return;
3598
+ } catch (err) {
3599
+ const code = err.code;
3600
+ if (code !== "EEXIST") throw err;
3601
+ if (Date.now() >= deadline) {
3602
+ throw new Error(
3603
+ `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
3604
+ );
3605
+ }
3606
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
3607
+ }
3608
+ }
3609
+ }
3610
+ async function releaseLock(lockPath) {
3611
+ await fs17.unlink(lockPath).catch(() => {
3612
+ });
3613
+ }
3614
+ async function withLock(fn) {
3615
+ const lock = registryLockPath();
3616
+ await acquireLock(lock);
3617
+ try {
3618
+ return await fn();
3619
+ } finally {
3620
+ await releaseLock(lock);
3621
+ }
3622
+ }
3623
+ async function readRegistry() {
3624
+ const file = registryPath();
3625
+ let raw;
3626
+ try {
3627
+ raw = await fs17.readFile(file, "utf8");
3628
+ } catch (err) {
3629
+ if (err.code === "ENOENT") {
3630
+ return { version: 1, projects: [] };
3631
+ }
3632
+ throw err;
3633
+ }
3634
+ const parsed = JSON.parse(raw);
3635
+ return RegistryFileSchema.parse(parsed);
3636
+ }
3637
+ async function writeRegistry(reg) {
3638
+ const validated = RegistryFileSchema.parse(reg);
3639
+ await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
3640
+ }
3641
+ var ProjectNameCollisionError = class extends Error {
3642
+ projectName;
3643
+ constructor(name) {
3644
+ super(`neat registry: a project named "${name}" is already registered`);
3645
+ this.name = "ProjectNameCollisionError";
3646
+ this.projectName = name;
3647
+ }
3648
+ };
3649
+ async function addProject(opts) {
3650
+ const resolvedPath = await normalizeProjectPath(opts.path);
3651
+ return withLock(async () => {
3652
+ const reg = await readRegistry();
3653
+ const byName = reg.projects.find((p) => p.name === opts.name);
3654
+ const byPath = reg.projects.find((p) => p.path === resolvedPath);
3655
+ if (byName && byName.path !== resolvedPath) {
3656
+ throw new ProjectNameCollisionError(opts.name);
3657
+ }
3658
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3659
+ if (byName && byName.path === resolvedPath) {
3660
+ byName.lastSeenAt = now;
3661
+ if (opts.languages) byName.languages = opts.languages;
3662
+ if (opts.status) byName.status = opts.status;
3663
+ await writeRegistry(reg);
3664
+ return byName;
3665
+ }
3666
+ if (byPath && byPath.name !== opts.name) {
3667
+ throw new ProjectNameCollisionError(byPath.name);
3668
+ }
3669
+ const entry = {
3670
+ name: opts.name,
3671
+ path: resolvedPath,
3672
+ registeredAt: now,
3673
+ languages: opts.languages ?? [],
3674
+ status: opts.status ?? "active"
3675
+ };
3676
+ reg.projects.push(entry);
3677
+ await writeRegistry(reg);
3678
+ return entry;
3679
+ });
3680
+ }
3681
+ async function getProject(name) {
3682
+ const reg = await readRegistry();
3683
+ return reg.projects.find((p) => p.name === name);
3684
+ }
3685
+ async function listProjects() {
3686
+ const reg = await readRegistry();
3687
+ return reg.projects;
3688
+ }
3689
+ async function setStatus(name, status) {
3690
+ return withLock(async () => {
3691
+ const reg = await readRegistry();
3692
+ const entry = reg.projects.find((p) => p.name === name);
3693
+ if (!entry) throw new Error(`neat registry: no project named "${name}"`);
3694
+ entry.status = status;
3695
+ await writeRegistry(reg);
3696
+ return entry;
3697
+ });
3698
+ }
3699
+ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
3700
+ await withLock(async () => {
3701
+ const reg = await readRegistry();
3702
+ const entry = reg.projects.find((p) => p.name === name);
3703
+ if (!entry) return;
3704
+ entry.lastSeenAt = at;
3705
+ await writeRegistry(reg);
3706
+ });
3707
+ }
3708
+ async function removeProject(name) {
3709
+ return withLock(async () => {
3710
+ const reg = await readRegistry();
3711
+ const idx = reg.projects.findIndex((p) => p.name === name);
3712
+ if (idx < 0) return void 0;
3713
+ const [removed] = reg.projects.splice(idx, 1);
3714
+ await writeRegistry(reg);
3715
+ return removed;
3716
+ });
3717
+ }
3718
+
3330
3719
  export {
3331
3720
  DEFAULT_PROJECT,
3332
3721
  getGraph,
@@ -3334,6 +3723,10 @@ export {
3334
3723
  checkCompatibility,
3335
3724
  ensureCompatLoaded,
3336
3725
  compatPairs,
3726
+ EVENT_BUS_CHANNEL,
3727
+ eventBus,
3728
+ emitNeatEvent,
3729
+ attachGraphToEventBus,
3337
3730
  confidenceForEdge,
3338
3731
  getRootCause,
3339
3732
  getBlastRadius,
@@ -3366,6 +3759,18 @@ export {
3366
3759
  startPersistLoop,
3367
3760
  pathsForProject,
3368
3761
  Projects,
3369
- parseExtraProjects
3762
+ parseExtraProjects,
3763
+ registryPath,
3764
+ registryLockPath,
3765
+ normalizeProjectPath,
3766
+ writeAtomically,
3767
+ readRegistry,
3768
+ ProjectNameCollisionError,
3769
+ addProject,
3770
+ getProject,
3771
+ listProjects,
3772
+ setStatus,
3773
+ touchLastSeen,
3774
+ removeProject
3370
3775
  };
3371
- //# sourceMappingURL=chunk-6SFEITLJ.js.map
3776
+ //# sourceMappingURL=chunk-IRPH6KL4.js.map