@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/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  startConnectorPolling,
14
14
  validateConnectorEntry,
15
15
  writeDaemonRecord
16
- } from "./chunk-PWIT35Z4.js";
16
+ } from "./chunk-22X2YM5H.js";
17
17
  import {
18
18
  buildSearchIndex
19
19
  } from "./chunk-BIY46Q6U.js";
@@ -72,7 +72,7 @@ import {
72
72
  startPersistLoop,
73
73
  startStalenessLoop,
74
74
  upsertConnectorEntry
75
- } from "./chunk-L64CATKE.js";
75
+ } from "./chunk-UCDXHLCJ.js";
76
76
  import {
77
77
  startOtelGrpcReceiver
78
78
  } from "./chunk-7QXN726V.js";
package/dist/index.cjs CHANGED
@@ -2322,6 +2322,41 @@ async function writeExtractionErrors(errors, errorsPath) {
2322
2322
  const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2323
2323
  await import_node_fs4.promises.appendFile(errorsPath, lines, "utf8");
2324
2324
  }
2325
+ function extractionHealthPathFor(errorsPath) {
2326
+ const dir = import_node_path4.default.dirname(errorsPath);
2327
+ const suffix = import_node_path4.default.basename(errorsPath).replace(/^errors/, "").replace(/\.ndjson$/, "");
2328
+ return import_node_path4.default.join(dir, `extraction-health${suffix}.json`);
2329
+ }
2330
+ var HEALTH_FILE_SAMPLE = 100;
2331
+ async function writeExtractionHealth(errors, healthPath, now = (/* @__PURE__ */ new Date()).toISOString()) {
2332
+ const byProducer = {};
2333
+ for (const e of errors) byProducer[e.producer] = (byProducer[e.producer] ?? 0) + 1;
2334
+ const coverage = {
2335
+ skippedFiles: errors.length,
2336
+ byProducer,
2337
+ files: errors.slice(0, HEALTH_FILE_SAMPLE).map((e) => e.file),
2338
+ updatedAt: now
2339
+ };
2340
+ await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(healthPath), { recursive: true });
2341
+ await import_node_fs4.promises.writeFile(healthPath, JSON.stringify(coverage), "utf8");
2342
+ }
2343
+ async function readExtractionHealth(healthPath) {
2344
+ let raw;
2345
+ try {
2346
+ raw = await import_node_fs4.promises.readFile(healthPath, "utf8");
2347
+ } catch {
2348
+ return void 0;
2349
+ }
2350
+ try {
2351
+ const obj = JSON.parse(raw);
2352
+ if (typeof obj?.skippedFiles !== "number" || !obj.byProducer || !Array.isArray(obj.files)) {
2353
+ return void 0;
2354
+ }
2355
+ return obj;
2356
+ } catch {
2357
+ return void 0;
2358
+ }
2359
+ }
2325
2360
  var droppedSink = [];
2326
2361
  function noteExtractedDropped(edge) {
2327
2362
  droppedSink.push(edge);
@@ -3273,14 +3308,16 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
3273
3308
  return best ?? relPath;
3274
3309
  }
3275
3310
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3276
- const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
3277
- const fileNodeId = (0, import_types6.fileId)(serviceName, relPath);
3311
+ const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
3312
+ const canonicalService = svcAttrs && svcAttrs.type === import_types6.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
3313
+ const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
3314
+ const fileNodeId = (0, import_types6.fileId)(canonicalService, relPath);
3278
3315
  if (!graph.hasNode(fileNodeId)) {
3279
3316
  const language = languageForExt(relPath);
3280
3317
  const node = {
3281
3318
  id: fileNodeId,
3282
3319
  type: import_types6.NodeType.FileNode,
3283
- service: serviceName,
3320
+ service: canonicalService,
3284
3321
  path: relPath,
3285
3322
  ...language ? { language } : {},
3286
3323
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
@@ -8392,6 +8429,15 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
8392
8429
  );
8393
8430
  }
8394
8431
  }
8432
+ if (opts.errorsPath) {
8433
+ try {
8434
+ await writeExtractionHealth(errorEntries, extractionHealthPathFor(opts.errorsPath));
8435
+ } catch (err) {
8436
+ console.warn(
8437
+ `[neat] failed to write extraction health sidecar: ${err.message}`
8438
+ );
8439
+ }
8440
+ }
8395
8441
  const droppedEntries = drainDroppedExtracted();
8396
8442
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
8397
8443
  const rejectedPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.errorsPath), "rejected.ndjson");
@@ -9366,6 +9412,67 @@ function registryLockPath() {
9366
9412
  function daemonPidPath() {
9367
9413
  return import_node_path48.default.join(neatHome(), "neatd.pid");
9368
9414
  }
9415
+ function daemonsDir() {
9416
+ return import_node_path48.default.join(neatHome(), "daemons");
9417
+ }
9418
+ function isFiniteInt(v) {
9419
+ return typeof v === "number" && Number.isFinite(v);
9420
+ }
9421
+ function parseDaemonRecord(raw) {
9422
+ let obj;
9423
+ try {
9424
+ obj = JSON.parse(raw);
9425
+ } catch {
9426
+ return void 0;
9427
+ }
9428
+ if (typeof obj !== "object" || obj === null) return void 0;
9429
+ const r = obj;
9430
+ const ports = r.ports;
9431
+ 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)) {
9432
+ return void 0;
9433
+ }
9434
+ return {
9435
+ project: r.project,
9436
+ projectPath: r.projectPath,
9437
+ pid: r.pid,
9438
+ status: r.status,
9439
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
9440
+ startedAt: r.startedAt,
9441
+ neatVersion: r.neatVersion
9442
+ };
9443
+ }
9444
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
9445
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
9446
+ const dir = daemonsDir();
9447
+ let names;
9448
+ try {
9449
+ names = await import_node_fs28.promises.readdir(dir);
9450
+ } catch (err) {
9451
+ if (err.code === "ENOENT") return [];
9452
+ throw err;
9453
+ }
9454
+ const out = [];
9455
+ for (const name of names) {
9456
+ if (!name.endsWith(".json")) continue;
9457
+ const file = import_node_path48.default.join(dir, name);
9458
+ let raw;
9459
+ try {
9460
+ raw = await import_node_fs28.promises.readFile(file, "utf8");
9461
+ } catch {
9462
+ continue;
9463
+ }
9464
+ const record = parseDaemonRecord(raw);
9465
+ if (!record) continue;
9466
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
9467
+ out.push({ record, live, source: file });
9468
+ }
9469
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
9470
+ return out;
9471
+ }
9472
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
9473
+ const discovered = await discoverDaemons(probe);
9474
+ return discovered.find((d) => d.record.project === name);
9475
+ }
9369
9476
  function isPidAliveDefault(pid) {
9370
9477
  try {
9371
9478
  process.kill(pid, 0);
@@ -9933,7 +10040,11 @@ function resolveProject(registry, req, reply, bootstrap, singleProject) {
9933
10040
  void reply.code(503).send({ ready: false, project: name, status: "broken" });
9934
10041
  return null;
9935
10042
  }
9936
- void reply.code(404).send({ error: "project not found", project: name });
10043
+ void reply.code(404).send({
10044
+ error: "project not found",
10045
+ project: name,
10046
+ hint: "This daemon does not host that project. GET /projects lists what it serves (see hostedHere)."
10047
+ });
9937
10048
  return null;
9938
10049
  }
9939
10050
  return ctx;
@@ -9971,6 +10082,9 @@ function registerRoutes(scope, ctx) {
9971
10082
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
9972
10083
  if (!proj) return;
9973
10084
  const uptimeMs = Date.now() - startedAt;
10085
+ const coverage = await readExtractionHealth(
10086
+ extractionHealthPathFor(proj.paths.errorsPath)
10087
+ );
9974
10088
  return {
9975
10089
  ok: true,
9976
10090
  project: proj.name,
@@ -9981,7 +10095,8 @@ function registerRoutes(scope, ctx) {
9981
10095
  uptime: Math.floor(uptimeMs / 1e3),
9982
10096
  nodeCount: proj.graph.order,
9983
10097
  edgeCount: proj.graph.size,
9984
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
10098
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
10099
+ ...coverage ? { coverage } : {}
9985
10100
  };
9986
10101
  });
9987
10102
  }
@@ -10623,12 +10738,17 @@ async function buildApi(opts) {
10623
10738
  // The daemon is serving this project, so it's active unless its
10624
10739
  // bootstrap broke. ('bootstrapping' still reads as active — the project
10625
10740
  // is the one to land on; its graph route answers 503 until ready.)
10626
- status: phase === "broken" ? "broken" : "active"
10741
+ status: phase === "broken" ? "broken" : "active",
10742
+ hostedHere: true
10627
10743
  };
10628
10744
  return [entry];
10629
10745
  }
10630
10746
  try {
10631
- return await listProjects();
10747
+ const entries = await listProjects();
10748
+ return entries.map((entry) => ({
10749
+ ...entry,
10750
+ hostedHere: registry.has(entry.name)
10751
+ }));
10632
10752
  } catch (err) {
10633
10753
  return reply.code(500).send({
10634
10754
  error: "failed to read project registry",
@@ -10642,7 +10762,21 @@ async function buildApi(opts) {
10642
10762
  if (!entry) {
10643
10763
  return reply.code(404).send({ error: "project not found", project: req.params.project });
10644
10764
  }
10645
- return { project: entry };
10765
+ const hostedHere = registry.has(entry.name);
10766
+ if (!hostedHere) {
10767
+ const owner = await findDaemonByProject(entry.name).catch(() => void 0);
10768
+ if (owner) {
10769
+ return {
10770
+ project: { ...entry, hostedHere },
10771
+ servedBy: {
10772
+ path: owner.record.projectPath,
10773
+ restPort: owner.record.ports.rest,
10774
+ live: owner.live
10775
+ }
10776
+ };
10777
+ }
10778
+ }
10779
+ return { project: { ...entry, hostedHere } };
10646
10780
  } catch (err) {
10647
10781
  return reply.code(500).send({
10648
10782
  error: "failed to read project registry",