@neat.is/core 0.6.3-dev.20260727 → 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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-PWIT35Z4.js";
4
+ } from "./chunk-22X2YM5H.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-L64CATKE.js";
40
+ } from "./chunk-UCDXHLCJ.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-7QXN726V.js";
package/dist/neatd.cjs CHANGED
@@ -2285,6 +2285,41 @@ async function writeExtractionErrors(errors, errorsPath) {
2285
2285
  const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2286
2286
  await import_node_fs4.promises.appendFile(errorsPath, lines, "utf8");
2287
2287
  }
2288
+ function extractionHealthPathFor(errorsPath) {
2289
+ const dir = import_node_path4.default.dirname(errorsPath);
2290
+ const suffix = import_node_path4.default.basename(errorsPath).replace(/^errors/, "").replace(/\.ndjson$/, "");
2291
+ return import_node_path4.default.join(dir, `extraction-health${suffix}.json`);
2292
+ }
2293
+ var HEALTH_FILE_SAMPLE = 100;
2294
+ async function writeExtractionHealth(errors, healthPath, now = (/* @__PURE__ */ new Date()).toISOString()) {
2295
+ const byProducer = {};
2296
+ for (const e of errors) byProducer[e.producer] = (byProducer[e.producer] ?? 0) + 1;
2297
+ const coverage = {
2298
+ skippedFiles: errors.length,
2299
+ byProducer,
2300
+ files: errors.slice(0, HEALTH_FILE_SAMPLE).map((e) => e.file),
2301
+ updatedAt: now
2302
+ };
2303
+ await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(healthPath), { recursive: true });
2304
+ await import_node_fs4.promises.writeFile(healthPath, JSON.stringify(coverage), "utf8");
2305
+ }
2306
+ async function readExtractionHealth(healthPath) {
2307
+ let raw;
2308
+ try {
2309
+ raw = await import_node_fs4.promises.readFile(healthPath, "utf8");
2310
+ } catch {
2311
+ return void 0;
2312
+ }
2313
+ try {
2314
+ const obj = JSON.parse(raw);
2315
+ if (typeof obj?.skippedFiles !== "number" || !obj.byProducer || !Array.isArray(obj.files)) {
2316
+ return void 0;
2317
+ }
2318
+ return obj;
2319
+ } catch {
2320
+ return void 0;
2321
+ }
2322
+ }
2288
2323
  var droppedSink = [];
2289
2324
  function noteExtractedDropped(edge) {
2290
2325
  droppedSink.push(edge);
@@ -3236,14 +3271,16 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
3236
3271
  return best ?? relPath;
3237
3272
  }
3238
3273
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3239
- const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
3240
- const fileNodeId = (0, import_types6.fileId)(serviceName, relPath);
3274
+ const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
3275
+ const canonicalService = svcAttrs && svcAttrs.type === import_types6.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
3276
+ const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
3277
+ const fileNodeId = (0, import_types6.fileId)(canonicalService, relPath);
3241
3278
  if (!graph.hasNode(fileNodeId)) {
3242
3279
  const language = languageForExt(relPath);
3243
3280
  const node = {
3244
3281
  id: fileNodeId,
3245
3282
  type: import_types6.NodeType.FileNode,
3246
- service: serviceName,
3283
+ service: canonicalService,
3247
3284
  path: relPath,
3248
3285
  ...language ? { language } : {},
3249
3286
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
@@ -8352,6 +8389,15 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
8352
8389
  );
8353
8390
  }
8354
8391
  }
8392
+ if (opts.errorsPath) {
8393
+ try {
8394
+ await writeExtractionHealth(errorEntries, extractionHealthPathFor(opts.errorsPath));
8395
+ } catch (err) {
8396
+ console.warn(
8397
+ `[neat] failed to write extraction health sidecar: ${err.message}`
8398
+ );
8399
+ }
8400
+ }
8355
8401
  const droppedEntries = drainDroppedExtracted();
8356
8402
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
8357
8403
  const rejectedPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.errorsPath), "rejected.ndjson");
@@ -9326,6 +9372,67 @@ function registryLockPath() {
9326
9372
  function daemonPidPath() {
9327
9373
  return import_node_path48.default.join(neatHome(), "neatd.pid");
9328
9374
  }
9375
+ function daemonsDir() {
9376
+ return import_node_path48.default.join(neatHome(), "daemons");
9377
+ }
9378
+ function isFiniteInt(v) {
9379
+ return typeof v === "number" && Number.isFinite(v);
9380
+ }
9381
+ function parseDaemonRecord(raw) {
9382
+ let obj;
9383
+ try {
9384
+ obj = JSON.parse(raw);
9385
+ } catch {
9386
+ return void 0;
9387
+ }
9388
+ if (typeof obj !== "object" || obj === null) return void 0;
9389
+ const r = obj;
9390
+ const ports = r.ports;
9391
+ 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)) {
9392
+ return void 0;
9393
+ }
9394
+ return {
9395
+ project: r.project,
9396
+ projectPath: r.projectPath,
9397
+ pid: r.pid,
9398
+ status: r.status,
9399
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
9400
+ startedAt: r.startedAt,
9401
+ neatVersion: r.neatVersion
9402
+ };
9403
+ }
9404
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
9405
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
9406
+ const dir = daemonsDir();
9407
+ let names;
9408
+ try {
9409
+ names = await import_node_fs28.promises.readdir(dir);
9410
+ } catch (err) {
9411
+ if (err.code === "ENOENT") return [];
9412
+ throw err;
9413
+ }
9414
+ const out = [];
9415
+ for (const name of names) {
9416
+ if (!name.endsWith(".json")) continue;
9417
+ const file = import_node_path48.default.join(dir, name);
9418
+ let raw;
9419
+ try {
9420
+ raw = await import_node_fs28.promises.readFile(file, "utf8");
9421
+ } catch {
9422
+ continue;
9423
+ }
9424
+ const record = parseDaemonRecord(raw);
9425
+ if (!record) continue;
9426
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
9427
+ out.push({ record, live, source: file });
9428
+ }
9429
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
9430
+ return out;
9431
+ }
9432
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
9433
+ const discovered = await discoverDaemons(probe);
9434
+ return discovered.find((d) => d.record.project === name);
9435
+ }
9329
9436
  function isPidAliveDefault(pid) {
9330
9437
  try {
9331
9438
  process.kill(pid, 0);
@@ -9835,7 +9942,11 @@ function resolveProject(registry, req2, reply, bootstrap, singleProject) {
9835
9942
  void reply.code(503).send({ ready: false, project: name, status: "broken" });
9836
9943
  return null;
9837
9944
  }
9838
- void reply.code(404).send({ error: "project not found", project: name });
9945
+ void reply.code(404).send({
9946
+ error: "project not found",
9947
+ project: name,
9948
+ hint: "This daemon does not host that project. GET /projects lists what it serves (see hostedHere)."
9949
+ });
9839
9950
  return null;
9840
9951
  }
9841
9952
  return ctx;
@@ -9873,6 +9984,9 @@ function registerRoutes(scope, ctx) {
9873
9984
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
9874
9985
  if (!proj) return;
9875
9986
  const uptimeMs = Date.now() - startedAt;
9987
+ const coverage = await readExtractionHealth(
9988
+ extractionHealthPathFor(proj.paths.errorsPath)
9989
+ );
9876
9990
  return {
9877
9991
  ok: true,
9878
9992
  project: proj.name,
@@ -9883,7 +9997,8 @@ function registerRoutes(scope, ctx) {
9883
9997
  uptime: Math.floor(uptimeMs / 1e3),
9884
9998
  nodeCount: proj.graph.order,
9885
9999
  edgeCount: proj.graph.size,
9886
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
10000
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
10001
+ ...coverage ? { coverage } : {}
9887
10002
  };
9888
10003
  });
9889
10004
  }
@@ -10525,12 +10640,17 @@ async function buildApi(opts) {
10525
10640
  // The daemon is serving this project, so it's active unless its
10526
10641
  // bootstrap broke. ('bootstrapping' still reads as active — the project
10527
10642
  // is the one to land on; its graph route answers 503 until ready.)
10528
- status: phase === "broken" ? "broken" : "active"
10643
+ status: phase === "broken" ? "broken" : "active",
10644
+ hostedHere: true
10529
10645
  };
10530
10646
  return [entry2];
10531
10647
  }
10532
10648
  try {
10533
- return await listProjects();
10649
+ const entries = await listProjects();
10650
+ return entries.map((entry2) => ({
10651
+ ...entry2,
10652
+ hostedHere: registry.has(entry2.name)
10653
+ }));
10534
10654
  } catch (err) {
10535
10655
  return reply.code(500).send({
10536
10656
  error: "failed to read project registry",
@@ -10544,7 +10664,21 @@ async function buildApi(opts) {
10544
10664
  if (!entry2) {
10545
10665
  return reply.code(404).send({ error: "project not found", project: req2.params.project });
10546
10666
  }
10547
- return { project: entry2 };
10667
+ const hostedHere = registry.has(entry2.name);
10668
+ if (!hostedHere) {
10669
+ const owner = await findDaemonByProject(entry2.name).catch(() => void 0);
10670
+ if (owner) {
10671
+ return {
10672
+ project: { ...entry2, hostedHere },
10673
+ servedBy: {
10674
+ path: owner.record.projectPath,
10675
+ restPort: owner.record.ports.rest,
10676
+ live: owner.live
10677
+ }
10678
+ };
10679
+ }
10680
+ }
10681
+ return { project: { ...entry2, hostedHere } };
10548
10682
  } catch (err) {
10549
10683
  return reply.code(500).send({
10550
10684
  error: "failed to read project registry",