@neat.is/core 0.4.18 → 0.4.20-dev.20260617

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.
@@ -4980,7 +4980,9 @@ async function loadGraphFromDisk(graph, outPath) {
4980
4980
  graph.clear();
4981
4981
  graph.import(payload.graph);
4982
4982
  }
4983
- function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4983
+ function startPersistLoop(graph, outPath, opts = {}) {
4984
+ const intervalMs = opts.intervalMs ?? 6e4;
4985
+ const exitOnSignal = opts.exitOnSignal ?? true;
4984
4986
  let stopped = false;
4985
4987
  const tick = async () => {
4986
4988
  if (stopped) return;
@@ -4993,7 +4995,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4993
4995
  const interval = setInterval(() => {
4994
4996
  void tick();
4995
4997
  }, intervalMs);
4996
- const onSignal = (signal) => {
4998
+ const onSignal = exitOnSignal ? (signal) => {
4997
4999
  void (async () => {
4998
5000
  try {
4999
5001
  await saveGraphToDisk(graph, outPath);
@@ -5003,14 +5005,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5003
5005
  process.exit(0);
5004
5006
  }
5005
5007
  })();
5006
- };
5007
- process.on("SIGTERM", onSignal);
5008
- process.on("SIGINT", onSignal);
5008
+ } : null;
5009
+ if (onSignal) {
5010
+ process.on("SIGTERM", onSignal);
5011
+ process.on("SIGINT", onSignal);
5012
+ }
5009
5013
  return () => {
5010
5014
  stopped = true;
5011
5015
  clearInterval(interval);
5012
- process.off("SIGTERM", onSignal);
5013
- process.off("SIGINT", onSignal);
5016
+ if (onSignal) {
5017
+ process.off("SIGTERM", onSignal);
5018
+ process.off("SIGINT", onSignal);
5019
+ }
5014
5020
  };
5015
5021
  }
5016
5022
 
@@ -5926,12 +5932,16 @@ function serializeGraph(graph) {
5926
5932
  });
5927
5933
  return { nodes, edges };
5928
5934
  }
5929
- function projectFromReq(req) {
5935
+ function projectFromReq(req, singleProject) {
5930
5936
  const params = req.params;
5931
- return params.project ?? DEFAULT_PROJECT;
5937
+ const named = params.project;
5938
+ if (singleProject) {
5939
+ return named === void 0 || named === DEFAULT_PROJECT ? singleProject : named;
5940
+ }
5941
+ return named ?? DEFAULT_PROJECT;
5932
5942
  }
5933
- function resolveProject(registry, req, reply, bootstrap) {
5934
- const name = projectFromReq(req);
5943
+ function resolveProject(registry, req, reply, bootstrap, singleProject) {
5944
+ const name = projectFromReq(req, singleProject);
5935
5945
  const ctx = registry.get(name);
5936
5946
  if (!ctx) {
5937
5947
  const phase = bootstrap?.status(name);
@@ -5972,13 +5982,13 @@ function buildLegacyRegistry(opts) {
5972
5982
  function registerRoutes(scope, ctx) {
5973
5983
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
5974
5984
  scope.get("/events", (req, reply) => {
5975
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5985
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5976
5986
  if (!proj) return;
5977
5987
  handleSse(req, reply, { project: proj.name });
5978
5988
  });
5979
5989
  if (ctx.scope === "project") {
5980
5990
  scope.get("/health", async (req, reply) => {
5981
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5991
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
5982
5992
  if (!proj) return;
5983
5993
  const uptimeMs = Date.now() - startedAt;
5984
5994
  return {
@@ -5996,14 +6006,14 @@ function registerRoutes(scope, ctx) {
5996
6006
  });
5997
6007
  }
5998
6008
  scope.get("/graph", async (req, reply) => {
5999
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6009
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6000
6010
  if (!proj) return;
6001
6011
  return serializeGraph(proj.graph);
6002
6012
  });
6003
6013
  scope.get(
6004
6014
  "/graph/node/:id",
6005
6015
  async (req, reply) => {
6006
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6016
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6007
6017
  if (!proj) return;
6008
6018
  const { id } = req.params;
6009
6019
  if (!proj.graph.hasNode(id)) {
@@ -6015,7 +6025,7 @@ function registerRoutes(scope, ctx) {
6015
6025
  scope.get(
6016
6026
  "/graph/edges/:id",
6017
6027
  async (req, reply) => {
6018
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6028
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6019
6029
  if (!proj) return;
6020
6030
  const { id } = req.params;
6021
6031
  if (!proj.graph.hasNode(id)) {
@@ -6027,7 +6037,7 @@ function registerRoutes(scope, ctx) {
6027
6037
  }
6028
6038
  );
6029
6039
  scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
6030
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6040
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6031
6041
  if (!proj) return;
6032
6042
  const { nodeId } = req.params;
6033
6043
  if (!proj.graph.hasNode(nodeId)) {
@@ -6042,7 +6052,7 @@ function registerRoutes(scope, ctx) {
6042
6052
  return getTransitiveDependencies(proj.graph, nodeId, depth);
6043
6053
  });
6044
6054
  scope.get("/graph/divergences", async (req, reply) => {
6045
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6055
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6046
6056
  if (!proj) return;
6047
6057
  let typeFilter;
6048
6058
  if (req.query.type) {
@@ -6077,7 +6087,7 @@ function registerRoutes(scope, ctx) {
6077
6087
  });
6078
6088
  });
6079
6089
  scope.get("/incidents", async (req, reply) => {
6080
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6090
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6081
6091
  if (!proj) return;
6082
6092
  const epath = errorsPathFor(proj);
6083
6093
  if (!epath) return { count: 0, total: 0, events: [] };
@@ -6089,7 +6099,7 @@ function registerRoutes(scope, ctx) {
6089
6099
  return { count: sliced.length, total, events: sliced };
6090
6100
  });
6091
6101
  scope.get("/stale-events", async (req, reply) => {
6092
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6102
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6093
6103
  if (!proj) return;
6094
6104
  const spath = staleEventsPathFor(proj);
6095
6105
  if (!spath) return { count: 0, total: 0, events: [] };
@@ -6104,7 +6114,7 @@ function registerRoutes(scope, ctx) {
6104
6114
  scope.get(
6105
6115
  "/incidents/:nodeId",
6106
6116
  async (req, reply) => {
6107
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6117
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6108
6118
  if (!proj) return;
6109
6119
  const { nodeId } = req.params;
6110
6120
  if (!proj.graph.hasNode(nodeId)) {
@@ -6120,7 +6130,7 @@ function registerRoutes(scope, ctx) {
6120
6130
  }
6121
6131
  );
6122
6132
  scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
6123
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6133
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6124
6134
  if (!proj) return;
6125
6135
  const { nodeId } = req.params;
6126
6136
  if (!proj.graph.hasNode(nodeId)) {
@@ -6140,7 +6150,7 @@ function registerRoutes(scope, ctx) {
6140
6150
  return result;
6141
6151
  });
6142
6152
  scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
6143
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6153
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6144
6154
  if (!proj) return;
6145
6155
  const { nodeId } = req.params;
6146
6156
  if (!proj.graph.hasNode(nodeId)) {
@@ -6153,7 +6163,7 @@ function registerRoutes(scope, ctx) {
6153
6163
  return getBlastRadius(proj.graph, nodeId, depth);
6154
6164
  });
6155
6165
  scope.get("/search", async (req, reply) => {
6156
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6166
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6157
6167
  if (!proj) return;
6158
6168
  const raw = (req.query.q ?? "").trim();
6159
6169
  if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
@@ -6184,7 +6194,7 @@ function registerRoutes(scope, ctx) {
6184
6194
  scope.get(
6185
6195
  "/graph/diff",
6186
6196
  async (req, reply) => {
6187
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6197
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6188
6198
  if (!proj) return;
6189
6199
  const against = req.query.against;
6190
6200
  if (!against) {
@@ -6199,7 +6209,7 @@ function registerRoutes(scope, ctx) {
6199
6209
  }
6200
6210
  );
6201
6211
  scope.post("/snapshot", async (req, reply) => {
6202
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6212
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6203
6213
  if (!proj) return;
6204
6214
  const body = req.body;
6205
6215
  if (!body || typeof body !== "object" || !body.snapshot) {
@@ -6228,7 +6238,7 @@ function registerRoutes(scope, ctx) {
6228
6238
  }
6229
6239
  });
6230
6240
  scope.post("/graph/scan", async (req, reply) => {
6231
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6241
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6232
6242
  if (!proj) return;
6233
6243
  if (!proj.scanPath) {
6234
6244
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6244,7 +6254,7 @@ function registerRoutes(scope, ctx) {
6244
6254
  };
6245
6255
  });
6246
6256
  scope.get("/policies", async (req, reply) => {
6247
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6257
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6248
6258
  if (!proj) return;
6249
6259
  const policyPath = ctx.policyFilePathFor(proj);
6250
6260
  if (!policyPath) {
@@ -6261,7 +6271,7 @@ function registerRoutes(scope, ctx) {
6261
6271
  }
6262
6272
  });
6263
6273
  scope.get("/policies/violations", async (req, reply) => {
6264
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6274
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6265
6275
  if (!proj) return;
6266
6276
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6267
6277
  let violations = await log.readAll();
@@ -6281,7 +6291,7 @@ function registerRoutes(scope, ctx) {
6281
6291
  return { violations };
6282
6292
  });
6283
6293
  scope.post("/policies/check", async (req, reply) => {
6284
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6294
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6285
6295
  if (!proj) return;
6286
6296
  const parsed = PoliciesCheckBodySchema.safeParse(req.body ?? {});
6287
6297
  if (!parsed.success) {
@@ -6317,7 +6327,7 @@ function registerRoutes(scope, ctx) {
6317
6327
  };
6318
6328
  });
6319
6329
  scope.get("/extend/list-uninstrumented", async (req, reply) => {
6320
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6330
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6321
6331
  if (!proj) return;
6322
6332
  if (!proj.scanPath) {
6323
6333
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6330,7 +6340,7 @@ function registerRoutes(scope, ctx) {
6330
6340
  }
6331
6341
  });
6332
6342
  scope.get("/extend/lookup", async (req, reply) => {
6333
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6343
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6334
6344
  if (!proj) return;
6335
6345
  const { library, version } = req.query;
6336
6346
  if (!library) {
@@ -6343,7 +6353,7 @@ function registerRoutes(scope, ctx) {
6343
6353
  return result;
6344
6354
  });
6345
6355
  scope.get("/extend/describe", async (req, reply) => {
6346
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6356
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6347
6357
  if (!proj) return;
6348
6358
  if (!proj.scanPath) {
6349
6359
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6356,7 +6366,7 @@ function registerRoutes(scope, ctx) {
6356
6366
  }
6357
6367
  });
6358
6368
  scope.post("/extend/apply", async (req, reply) => {
6359
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6369
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6360
6370
  if (!proj) return;
6361
6371
  if (!proj.scanPath) {
6362
6372
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6376,7 +6386,7 @@ function registerRoutes(scope, ctx) {
6376
6386
  }
6377
6387
  });
6378
6388
  scope.post("/extend/dry-run", async (req, reply) => {
6379
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6389
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6380
6390
  if (!proj) return;
6381
6391
  if (!proj.scanPath) {
6382
6392
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6396,7 +6406,7 @@ function registerRoutes(scope, ctx) {
6396
6406
  }
6397
6407
  });
6398
6408
  scope.post("/extend/rollback", async (req, reply) => {
6399
- const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6409
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
6400
6410
  if (!proj) return;
6401
6411
  if (!proj.scanPath) {
6402
6412
  return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
@@ -6459,7 +6469,8 @@ async function buildApi(opts) {
6459
6469
  errorsPathFor,
6460
6470
  staleEventsPathFor,
6461
6471
  policyFilePathFor,
6462
- bootstrap: opts.bootstrap
6472
+ bootstrap: opts.bootstrap,
6473
+ singleProject: opts.singleProject?.name
6463
6474
  };
6464
6475
  app.get("/health", async () => {
6465
6476
  const uptimeMs = Date.now() - startedAt;
@@ -6482,10 +6493,29 @@ async function buildApi(opts) {
6482
6493
  return {
6483
6494
  ok: true,
6484
6495
  uptimeMs,
6496
+ // ADR-096 §7 — a per-project daemon carries its project at the top level
6497
+ // so a spawn can confirm a daemon found on a reused port is serving THIS
6498
+ // project before reusing it. The legacy multi-project daemon omits it and
6499
+ // is matched against the `projects` array instead.
6500
+ ...opts.singleProject ? { project: opts.singleProject.name } : {},
6485
6501
  projects
6486
6502
  };
6487
6503
  });
6488
6504
  app.get("/projects", async (_req, reply) => {
6505
+ if (opts.singleProject) {
6506
+ const phase = opts.bootstrap?.status(opts.singleProject.name);
6507
+ const entry = {
6508
+ name: opts.singleProject.name,
6509
+ path: opts.singleProject.path,
6510
+ registeredAt: new Date(startedAt).toISOString(),
6511
+ languages: [],
6512
+ // The daemon is serving this project, so it's active unless its
6513
+ // bootstrap broke. ('bootstrapping' still reads as active — the project
6514
+ // is the one to land on; its graph route answers 503 until ready.)
6515
+ status: phase === "broken" ? "broken" : "active"
6516
+ };
6517
+ return [entry];
6518
+ }
6489
6519
  try {
6490
6520
  return await listProjects();
6491
6521
  } catch (err) {
@@ -6587,4 +6617,4 @@ export {
6587
6617
  pruneRegistry,
6588
6618
  buildApi
6589
6619
  };
6590
- //# sourceMappingURL=chunk-T3X6XJPU.js.map
6620
+ //# sourceMappingURL=chunk-Q5EDVWKZ.js.map