@neat.is/core 0.6.3 → 0.6.4

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/neatd.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  reconcileDaemonRecordSync,
4
4
  startDaemon
5
- } from "./chunk-PWIT35Z4.js";
5
+ } from "./chunk-22X2YM5H.js";
6
6
  import {
7
7
  listProjects,
8
8
  registryPath
9
- } from "./chunk-L64CATKE.js";
9
+ } from "./chunk-UCDXHLCJ.js";
10
10
  import {
11
11
  BindAuthorityError,
12
12
  __require
package/dist/server.cjs CHANGED
@@ -2038,6 +2038,41 @@ async function writeExtractionErrors(errors, errorsPath) {
2038
2038
  const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2039
2039
  await import_node_fs6.promises.appendFile(errorsPath, lines, "utf8");
2040
2040
  }
2041
+ function extractionHealthPathFor(errorsPath) {
2042
+ const dir = import_node_path6.default.dirname(errorsPath);
2043
+ const suffix = import_node_path6.default.basename(errorsPath).replace(/^errors/, "").replace(/\.ndjson$/, "");
2044
+ return import_node_path6.default.join(dir, `extraction-health${suffix}.json`);
2045
+ }
2046
+ var HEALTH_FILE_SAMPLE = 100;
2047
+ async function writeExtractionHealth(errors, healthPath, now = (/* @__PURE__ */ new Date()).toISOString()) {
2048
+ const byProducer = {};
2049
+ for (const e of errors) byProducer[e.producer] = (byProducer[e.producer] ?? 0) + 1;
2050
+ const coverage = {
2051
+ skippedFiles: errors.length,
2052
+ byProducer,
2053
+ files: errors.slice(0, HEALTH_FILE_SAMPLE).map((e) => e.file),
2054
+ updatedAt: now
2055
+ };
2056
+ await import_node_fs6.promises.mkdir(import_node_path6.default.dirname(healthPath), { recursive: true });
2057
+ await import_node_fs6.promises.writeFile(healthPath, JSON.stringify(coverage), "utf8");
2058
+ }
2059
+ async function readExtractionHealth(healthPath) {
2060
+ let raw;
2061
+ try {
2062
+ raw = await import_node_fs6.promises.readFile(healthPath, "utf8");
2063
+ } catch {
2064
+ return void 0;
2065
+ }
2066
+ try {
2067
+ const obj = JSON.parse(raw);
2068
+ if (typeof obj?.skippedFiles !== "number" || !obj.byProducer || !Array.isArray(obj.files)) {
2069
+ return void 0;
2070
+ }
2071
+ return obj;
2072
+ } catch {
2073
+ return void 0;
2074
+ }
2075
+ }
2041
2076
  var droppedSink = [];
2042
2077
  function noteExtractedDropped(edge) {
2043
2078
  droppedSink.push(edge);
@@ -2989,14 +3024,16 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
2989
3024
  return best ?? relPath;
2990
3025
  }
2991
3026
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
2992
- const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
2993
- const fileNodeId = (0, import_types5.fileId)(serviceName, relPath);
3027
+ const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
3028
+ const canonicalService = svcAttrs && svcAttrs.type === import_types5.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
3029
+ const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
3030
+ const fileNodeId = (0, import_types5.fileId)(canonicalService, relPath);
2994
3031
  if (!graph.hasNode(fileNodeId)) {
2995
3032
  const language = languageForExt(relPath);
2996
3033
  const node = {
2997
3034
  id: fileNodeId,
2998
3035
  type: import_types5.NodeType.FileNode,
2999
- service: serviceName,
3036
+ service: canonicalService,
3000
3037
  path: relPath,
3001
3038
  ...language ? { language } : {},
3002
3039
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
@@ -8914,6 +8951,15 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
8914
8951
  );
8915
8952
  }
8916
8953
  }
8954
+ if (opts.errorsPath) {
8955
+ try {
8956
+ await writeExtractionHealth(errorEntries, extractionHealthPathFor(opts.errorsPath));
8957
+ } catch (err) {
8958
+ console.warn(
8959
+ `[neat] failed to write extraction health sidecar: ${err.message}`
8960
+ );
8961
+ }
8962
+ }
8917
8963
  const droppedEntries = drainDroppedExtracted();
8918
8964
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
8919
8965
  const rejectedPath = import_node_path45.default.join(import_node_path45.default.dirname(opts.errorsPath), "rejected.ndjson");
@@ -9214,6 +9260,75 @@ function neatHome() {
9214
9260
  function registryPath() {
9215
9261
  return import_node_path48.default.join(neatHome(), "projects.json");
9216
9262
  }
9263
+ function daemonsDir() {
9264
+ return import_node_path48.default.join(neatHome(), "daemons");
9265
+ }
9266
+ function isFiniteInt(v) {
9267
+ return typeof v === "number" && Number.isFinite(v);
9268
+ }
9269
+ function parseDaemonRecord(raw) {
9270
+ let obj;
9271
+ try {
9272
+ obj = JSON.parse(raw);
9273
+ } catch {
9274
+ return void 0;
9275
+ }
9276
+ if (typeof obj !== "object" || obj === null) return void 0;
9277
+ const r = obj;
9278
+ const ports = r.ports;
9279
+ if (typeof r.project !== "string" || typeof r.projectPath !== "string" || !isFiniteInt(r.pid) || r.status !== "running" && r.status !== "stopped" || typeof r.startedAt !== "string" || typeof r.neatVersion !== "string" || !ports || !isFiniteInt(ports.rest) || !isFiniteInt(ports.otlp) || !isFiniteInt(ports.web)) {
9280
+ return void 0;
9281
+ }
9282
+ return {
9283
+ project: r.project,
9284
+ projectPath: r.projectPath,
9285
+ pid: r.pid,
9286
+ status: r.status,
9287
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
9288
+ startedAt: r.startedAt,
9289
+ neatVersion: r.neatVersion
9290
+ };
9291
+ }
9292
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
9293
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
9294
+ const dir = daemonsDir();
9295
+ let names;
9296
+ try {
9297
+ names = await import_node_fs28.promises.readdir(dir);
9298
+ } catch (err) {
9299
+ if (err.code === "ENOENT") return [];
9300
+ throw err;
9301
+ }
9302
+ const out = [];
9303
+ for (const name of names) {
9304
+ if (!name.endsWith(".json")) continue;
9305
+ const file = import_node_path48.default.join(dir, name);
9306
+ let raw;
9307
+ try {
9308
+ raw = await import_node_fs28.promises.readFile(file, "utf8");
9309
+ } catch {
9310
+ continue;
9311
+ }
9312
+ const record = parseDaemonRecord(raw);
9313
+ if (!record) continue;
9314
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
9315
+ out.push({ record, live, source: file });
9316
+ }
9317
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
9318
+ return out;
9319
+ }
9320
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
9321
+ const discovered = await discoverDaemons(probe);
9322
+ return discovered.find((d) => d.record.project === name);
9323
+ }
9324
+ function isPidAliveDefault(pid) {
9325
+ try {
9326
+ process.kill(pid, 0);
9327
+ return true;
9328
+ } catch (err) {
9329
+ return err.code === "EPERM";
9330
+ }
9331
+ }
9217
9332
  async function readRegistry() {
9218
9333
  const file = registryPath();
9219
9334
  let raw;
@@ -9491,7 +9606,11 @@ function resolveProject(registry, req, reply, bootstrap, singleProject) {
9491
9606
  void reply.code(503).send({ ready: false, project: name, status: "broken" });
9492
9607
  return null;
9493
9608
  }
9494
- void reply.code(404).send({ error: "project not found", project: name });
9609
+ void reply.code(404).send({
9610
+ error: "project not found",
9611
+ project: name,
9612
+ hint: "This daemon does not host that project. GET /projects lists what it serves (see hostedHere)."
9613
+ });
9495
9614
  return null;
9496
9615
  }
9497
9616
  return ctx;
@@ -9529,6 +9648,9 @@ function registerRoutes(scope, ctx) {
9529
9648
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
9530
9649
  if (!proj) return;
9531
9650
  const uptimeMs = Date.now() - startedAt;
9651
+ const coverage = await readExtractionHealth(
9652
+ extractionHealthPathFor(proj.paths.errorsPath)
9653
+ );
9532
9654
  return {
9533
9655
  ok: true,
9534
9656
  project: proj.name,
@@ -9539,7 +9661,8 @@ function registerRoutes(scope, ctx) {
9539
9661
  uptime: Math.floor(uptimeMs / 1e3),
9540
9662
  nodeCount: proj.graph.order,
9541
9663
  edgeCount: proj.graph.size,
9542
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
9664
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
9665
+ ...coverage ? { coverage } : {}
9543
9666
  };
9544
9667
  });
9545
9668
  }
@@ -10181,12 +10304,17 @@ async function buildApi(opts) {
10181
10304
  // The daemon is serving this project, so it's active unless its
10182
10305
  // bootstrap broke. ('bootstrapping' still reads as active — the project
10183
10306
  // is the one to land on; its graph route answers 503 until ready.)
10184
- status: phase === "broken" ? "broken" : "active"
10307
+ status: phase === "broken" ? "broken" : "active",
10308
+ hostedHere: true
10185
10309
  };
10186
10310
  return [entry];
10187
10311
  }
10188
10312
  try {
10189
- return await listProjects();
10313
+ const entries = await listProjects();
10314
+ return entries.map((entry) => ({
10315
+ ...entry,
10316
+ hostedHere: registry.has(entry.name)
10317
+ }));
10190
10318
  } catch (err) {
10191
10319
  return reply.code(500).send({
10192
10320
  error: "failed to read project registry",
@@ -10200,7 +10328,21 @@ async function buildApi(opts) {
10200
10328
  if (!entry) {
10201
10329
  return reply.code(404).send({ error: "project not found", project: req.params.project });
10202
10330
  }
10203
- return { project: entry };
10331
+ const hostedHere = registry.has(entry.name);
10332
+ if (!hostedHere) {
10333
+ const owner = await findDaemonByProject(entry.name).catch(() => void 0);
10334
+ if (owner) {
10335
+ return {
10336
+ project: { ...entry, hostedHere },
10337
+ servedBy: {
10338
+ path: owner.record.projectPath,
10339
+ restPort: owner.record.ports.rest,
10340
+ live: owner.live
10341
+ }
10342
+ };
10343
+ }
10344
+ }
10345
+ return { project: { ...entry, hostedHere } };
10204
10346
  } catch (err) {
10205
10347
  return reply.code(500).send({
10206
10348
  error: "failed to read project registry",