@neat.is/core 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  mountBearerAuth
3
- } from "./chunk-4V23KYOP.js";
3
+ } from "./chunk-KYRIQIPG.js";
4
4
 
5
5
  // src/graph.ts
6
6
  import GraphDefault from "graphology";
@@ -1668,8 +1668,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1668
1668
  ".turbo",
1669
1669
  "dist",
1670
1670
  "build",
1671
- ".next"
1671
+ ".next",
1672
+ // Python virtualenv shapes (issue #344). Walking into a venv pulls in the
1673
+ // entire CPython stdlib + every installed package as if it were first-party
1674
+ // service code — 20k+ files, none of which the user wrote. The shape names
1675
+ // cover the three common venv tools (`venv` / `python -m venv`,
1676
+ // `virtualenv`) and the tox + PEP 582 conventions.
1677
+ ".venv",
1678
+ "venv",
1679
+ "__pypackages__",
1680
+ ".tox",
1681
+ // `site-packages` shows up nested inside venvs that don't carry one of the
1682
+ // outer names (system Python on macOS, in-place pyenv layouts). Listing it
1683
+ // here means we stop the walk at the boundary even when the wrapper dir
1684
+ // wasn't recognisable.
1685
+ "site-packages"
1672
1686
  ]);
1687
+ var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
1688
+ async function isPythonVenvDir(dir) {
1689
+ const cached = PYVENV_MARKER_CACHE.get(dir);
1690
+ if (cached !== void 0) return cached;
1691
+ try {
1692
+ const stat = await fs5.stat(path5.join(dir, "pyvenv.cfg"));
1693
+ const ok = stat.isFile();
1694
+ PYVENV_MARKER_CACHE.set(dir, ok);
1695
+ return ok;
1696
+ } catch {
1697
+ PYVENV_MARKER_CACHE.set(dir, false);
1698
+ return false;
1699
+ }
1700
+ }
1673
1701
  function isConfigFile(name) {
1674
1702
  const ext = path5.extname(name);
1675
1703
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
@@ -1960,6 +1988,7 @@ async function walkDirs(start, scanPath, options, visit) {
1960
1988
  const rel = path8.relative(scanPath, child).split(path8.sep).join("/");
1961
1989
  if (rel && options.ig.ignores(rel + "/")) continue;
1962
1990
  }
1991
+ if (await isPythonVenvDir(child)) continue;
1963
1992
  await visit(child);
1964
1993
  await recurse(child, depth + 1);
1965
1994
  }
@@ -2237,7 +2266,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
2237
2266
  for (const entry of entries) {
2238
2267
  if (entry.isDirectory()) {
2239
2268
  if (IGNORED_DIRS.has(entry.name)) continue;
2240
- out.push(...await walkYamlFiles(path9.join(start, entry.name), depth + 1, max));
2269
+ const child = path9.join(start, entry.name);
2270
+ if (await isPythonVenvDir(child)) continue;
2271
+ out.push(...await walkYamlFiles(child, depth + 1, max));
2241
2272
  } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path9.extname(entry.name))) {
2242
2273
  out.push(path9.join(start, entry.name));
2243
2274
  }
@@ -2919,7 +2950,9 @@ async function walkConfigFiles(dir) {
2919
2950
  for (const entry of entries) {
2920
2951
  const full = path18.join(current, entry.name);
2921
2952
  if (entry.isDirectory()) {
2922
- if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2953
+ if (IGNORED_DIRS.has(entry.name)) continue;
2954
+ if (await isPythonVenvDir(full)) continue;
2955
+ await walk(full);
2923
2956
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
2924
2957
  out.push(full);
2925
2958
  }
@@ -2995,7 +3028,9 @@ async function walkSourceFiles(dir) {
2995
3028
  for (const entry of entries) {
2996
3029
  const full = path19.join(current, entry.name);
2997
3030
  if (entry.isDirectory()) {
2998
- if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3031
+ if (IGNORED_DIRS.has(entry.name)) continue;
3032
+ if (await isPythonVenvDir(full)) continue;
3033
+ await walk(full);
2999
3034
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
3000
3035
  out.push(full);
3001
3036
  }
@@ -3620,7 +3655,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
3620
3655
  for (const entry of entries) {
3621
3656
  if (entry.isDirectory()) {
3622
3657
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3623
- out.push(...await walkTfFiles(path27.join(start, entry.name), depth + 1, max));
3658
+ const child = path27.join(start, entry.name);
3659
+ if (await isPythonVenvDir(child)) continue;
3660
+ out.push(...await walkTfFiles(child, depth + 1, max));
3624
3661
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3625
3662
  out.push(path27.join(start, entry.name));
3626
3663
  }
@@ -3667,7 +3704,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
3667
3704
  for (const entry of entries) {
3668
3705
  if (entry.isDirectory()) {
3669
3706
  if (IGNORED_DIRS.has(entry.name)) continue;
3670
- out.push(...await walkYamlFiles2(path28.join(start, entry.name), depth + 1, max));
3707
+ const child = path28.join(start, entry.name);
3708
+ if (await isPythonVenvDir(child)) continue;
3709
+ out.push(...await walkYamlFiles2(child, depth + 1, max));
3671
3710
  } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path28.extname(entry.name))) {
3672
3711
  out.push(path28.join(start, entry.name));
3673
3712
  }
@@ -4541,10 +4580,19 @@ function projectFromReq(req) {
4541
4580
  const params = req.params;
4542
4581
  return params.project ?? DEFAULT_PROJECT;
4543
4582
  }
4544
- function resolveProject(registry, req, reply) {
4583
+ function resolveProject(registry, req, reply, bootstrap) {
4545
4584
  const name = projectFromReq(req);
4546
4585
  const ctx = registry.get(name);
4547
4586
  if (!ctx) {
4587
+ const phase = bootstrap?.status(name);
4588
+ if (phase === "bootstrapping") {
4589
+ void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
4590
+ return null;
4591
+ }
4592
+ if (phase === "broken") {
4593
+ void reply.code(503).send({ ready: false, project: name, status: "broken" });
4594
+ return null;
4595
+ }
4548
4596
  void reply.code(404).send({ error: "project not found", project: name });
4549
4597
  return null;
4550
4598
  }
@@ -4574,36 +4622,38 @@ function buildLegacyRegistry(opts) {
4574
4622
  function registerRoutes(scope, ctx) {
4575
4623
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
4576
4624
  scope.get("/events", (req, reply) => {
4577
- const proj = resolveProject(registry, req, reply);
4625
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4578
4626
  if (!proj) return;
4579
4627
  handleSse(req, reply, { project: proj.name });
4580
4628
  });
4581
- scope.get("/health", async (req, reply) => {
4582
- const proj = resolveProject(registry, req, reply);
4583
- if (!proj) return;
4584
- const uptimeMs = Date.now() - startedAt;
4585
- return {
4586
- ok: true,
4587
- project: proj.name,
4588
- uptimeMs,
4589
- // Legacy fields kept additively. The web shell's StatusBar reads
4590
- // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4591
- // the canonical triple and lets the extras pass through.
4592
- uptime: Math.floor(uptimeMs / 1e3),
4593
- nodeCount: proj.graph.order,
4594
- edgeCount: proj.graph.size,
4595
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4596
- };
4597
- });
4629
+ if (ctx.scope === "project") {
4630
+ scope.get("/health", async (req, reply) => {
4631
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4632
+ if (!proj) return;
4633
+ const uptimeMs = Date.now() - startedAt;
4634
+ return {
4635
+ ok: true,
4636
+ project: proj.name,
4637
+ uptimeMs,
4638
+ // Legacy fields kept additively. The web shell's StatusBar reads
4639
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4640
+ // the canonical triple and lets the extras pass through.
4641
+ uptime: Math.floor(uptimeMs / 1e3),
4642
+ nodeCount: proj.graph.order,
4643
+ edgeCount: proj.graph.size,
4644
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4645
+ };
4646
+ });
4647
+ }
4598
4648
  scope.get("/graph", async (req, reply) => {
4599
- const proj = resolveProject(registry, req, reply);
4649
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4600
4650
  if (!proj) return;
4601
4651
  return serializeGraph(proj.graph);
4602
4652
  });
4603
4653
  scope.get(
4604
4654
  "/graph/node/:id",
4605
4655
  async (req, reply) => {
4606
- const proj = resolveProject(registry, req, reply);
4656
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4607
4657
  if (!proj) return;
4608
4658
  const { id } = req.params;
4609
4659
  if (!proj.graph.hasNode(id)) {
@@ -4615,7 +4665,7 @@ function registerRoutes(scope, ctx) {
4615
4665
  scope.get(
4616
4666
  "/graph/edges/:id",
4617
4667
  async (req, reply) => {
4618
- const proj = resolveProject(registry, req, reply);
4668
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4619
4669
  if (!proj) return;
4620
4670
  const { id } = req.params;
4621
4671
  if (!proj.graph.hasNode(id)) {
@@ -4627,7 +4677,7 @@ function registerRoutes(scope, ctx) {
4627
4677
  }
4628
4678
  );
4629
4679
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
4630
- const proj = resolveProject(registry, req, reply);
4680
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4631
4681
  if (!proj) return;
4632
4682
  const { nodeId } = req.params;
4633
4683
  if (!proj.graph.hasNode(nodeId)) {
@@ -4642,7 +4692,7 @@ function registerRoutes(scope, ctx) {
4642
4692
  return getTransitiveDependencies(proj.graph, nodeId, depth);
4643
4693
  });
4644
4694
  scope.get("/graph/divergences", async (req, reply) => {
4645
- const proj = resolveProject(registry, req, reply);
4695
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4646
4696
  if (!proj) return;
4647
4697
  let typeFilter;
4648
4698
  if (req.query.type) {
@@ -4677,7 +4727,7 @@ function registerRoutes(scope, ctx) {
4677
4727
  });
4678
4728
  });
4679
4729
  scope.get("/incidents", async (req, reply) => {
4680
- const proj = resolveProject(registry, req, reply);
4730
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4681
4731
  if (!proj) return;
4682
4732
  const epath = errorsPathFor(proj);
4683
4733
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -4689,7 +4739,7 @@ function registerRoutes(scope, ctx) {
4689
4739
  return { count: sliced.length, total, events: sliced };
4690
4740
  });
4691
4741
  scope.get("/stale-events", async (req, reply) => {
4692
- const proj = resolveProject(registry, req, reply);
4742
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4693
4743
  if (!proj) return;
4694
4744
  const spath = staleEventsPathFor(proj);
4695
4745
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -4704,7 +4754,7 @@ function registerRoutes(scope, ctx) {
4704
4754
  scope.get(
4705
4755
  "/incidents/:nodeId",
4706
4756
  async (req, reply) => {
4707
- const proj = resolveProject(registry, req, reply);
4757
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4708
4758
  if (!proj) return;
4709
4759
  const { nodeId } = req.params;
4710
4760
  if (!proj.graph.hasNode(nodeId)) {
@@ -4720,7 +4770,7 @@ function registerRoutes(scope, ctx) {
4720
4770
  }
4721
4771
  );
4722
4772
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
4723
- const proj = resolveProject(registry, req, reply);
4773
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4724
4774
  if (!proj) return;
4725
4775
  const { nodeId } = req.params;
4726
4776
  if (!proj.graph.hasNode(nodeId)) {
@@ -4740,7 +4790,7 @@ function registerRoutes(scope, ctx) {
4740
4790
  return result;
4741
4791
  });
4742
4792
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
4743
- const proj = resolveProject(registry, req, reply);
4793
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4744
4794
  if (!proj) return;
4745
4795
  const { nodeId } = req.params;
4746
4796
  if (!proj.graph.hasNode(nodeId)) {
@@ -4753,7 +4803,7 @@ function registerRoutes(scope, ctx) {
4753
4803
  return getBlastRadius(proj.graph, nodeId, depth);
4754
4804
  });
4755
4805
  scope.get("/search", async (req, reply) => {
4756
- const proj = resolveProject(registry, req, reply);
4806
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4757
4807
  if (!proj) return;
4758
4808
  const raw = (req.query.q ?? "").trim();
4759
4809
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -4784,7 +4834,7 @@ function registerRoutes(scope, ctx) {
4784
4834
  scope.get(
4785
4835
  "/graph/diff",
4786
4836
  async (req, reply) => {
4787
- const proj = resolveProject(registry, req, reply);
4837
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4788
4838
  if (!proj) return;
4789
4839
  const against = req.query.against;
4790
4840
  if (!against) {
@@ -4799,7 +4849,7 @@ function registerRoutes(scope, ctx) {
4799
4849
  }
4800
4850
  );
4801
4851
  scope.post("/snapshot", async (req, reply) => {
4802
- const proj = resolveProject(registry, req, reply);
4852
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4803
4853
  if (!proj) return;
4804
4854
  const body = req.body;
4805
4855
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -4828,7 +4878,7 @@ function registerRoutes(scope, ctx) {
4828
4878
  }
4829
4879
  });
4830
4880
  scope.post("/graph/scan", async (req, reply) => {
4831
- const proj = resolveProject(registry, req, reply);
4881
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4832
4882
  if (!proj) return;
4833
4883
  if (!proj.scanPath) {
4834
4884
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -4844,7 +4894,7 @@ function registerRoutes(scope, ctx) {
4844
4894
  };
4845
4895
  });
4846
4896
  scope.get("/policies", async (req, reply) => {
4847
- const proj = resolveProject(registry, req, reply);
4897
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4848
4898
  if (!proj) return;
4849
4899
  const policyPath = ctx.policyFilePathFor(proj);
4850
4900
  if (!policyPath) {
@@ -4861,7 +4911,7 @@ function registerRoutes(scope, ctx) {
4861
4911
  }
4862
4912
  });
4863
4913
  scope.get("/policies/violations", async (req, reply) => {
4864
- const proj = resolveProject(registry, req, reply);
4914
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4865
4915
  if (!proj) return;
4866
4916
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4867
4917
  let violations = await log.readAll();
@@ -4881,7 +4931,7 @@ function registerRoutes(scope, ctx) {
4881
4931
  return { violations };
4882
4932
  });
4883
4933
  scope.post("/policies/check", async (req, reply) => {
4884
- const proj = resolveProject(registry, req, reply);
4934
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
4885
4935
  if (!proj) return;
4886
4936
  const parsed = PoliciesCheckBodySchema.safeParse(req.body ?? {});
4887
4937
  if (!parsed.success) {
@@ -4952,10 +5002,36 @@ async function buildApi(opts) {
4952
5002
  const routeCtx = {
4953
5003
  registry,
4954
5004
  startedAt,
5005
+ scope: "root",
4955
5006
  errorsPathFor,
4956
5007
  staleEventsPathFor,
4957
- policyFilePathFor
5008
+ policyFilePathFor,
5009
+ bootstrap: opts.bootstrap
4958
5010
  };
5011
+ app.get("/health", async () => {
5012
+ const uptimeMs = Date.now() - startedAt;
5013
+ const bootstrapList = opts.bootstrap?.list() ?? [];
5014
+ const byName = new Map(bootstrapList.map((p) => [p.name, p]));
5015
+ const names = /* @__PURE__ */ new Set([
5016
+ ...registry.list(),
5017
+ ...bootstrapList.map((p) => p.name)
5018
+ ]);
5019
+ const projects = [...names].sort().map((name) => {
5020
+ const proj = registry.get(name);
5021
+ const tracked = byName.get(name);
5022
+ return {
5023
+ name,
5024
+ nodeCount: proj?.graph.order ?? 0,
5025
+ edgeCount: proj?.graph.size ?? 0,
5026
+ ...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
5027
+ };
5028
+ });
5029
+ return {
5030
+ ok: true,
5031
+ uptimeMs,
5032
+ projects
5033
+ };
5034
+ });
4959
5035
  app.get("/projects", async (_req, reply) => {
4960
5036
  try {
4961
5037
  return await listProjects();
@@ -4983,7 +5059,7 @@ async function buildApi(opts) {
4983
5059
  registerRoutes(app, routeCtx);
4984
5060
  await app.register(
4985
5061
  async (scope) => {
4986
- registerRoutes(scope, routeCtx);
5062
+ registerRoutes(scope, { ...routeCtx, scope: "project" });
4987
5063
  },
4988
5064
  { prefix: "/projects/:project" }
4989
5065
  );
@@ -5050,4 +5126,4 @@ export {
5050
5126
  removeProject,
5051
5127
  buildApi
5052
5128
  };
5053
- //# sourceMappingURL=chunk-UPW4CMOH.js.map
5129
+ //# sourceMappingURL=chunk-NTQHMXWE.js.map