@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/cli.cjs CHANGED
@@ -2329,6 +2329,41 @@ async function writeExtractionErrors(errors, errorsPath) {
2329
2329
  const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2330
2330
  await import_node_fs5.promises.appendFile(errorsPath, lines, "utf8");
2331
2331
  }
2332
+ function extractionHealthPathFor(errorsPath) {
2333
+ const dir = import_node_path5.default.dirname(errorsPath);
2334
+ const suffix = import_node_path5.default.basename(errorsPath).replace(/^errors/, "").replace(/\.ndjson$/, "");
2335
+ return import_node_path5.default.join(dir, `extraction-health${suffix}.json`);
2336
+ }
2337
+ var HEALTH_FILE_SAMPLE = 100;
2338
+ async function writeExtractionHealth(errors, healthPath, now = (/* @__PURE__ */ new Date()).toISOString()) {
2339
+ const byProducer = {};
2340
+ for (const e of errors) byProducer[e.producer] = (byProducer[e.producer] ?? 0) + 1;
2341
+ const coverage = {
2342
+ skippedFiles: errors.length,
2343
+ byProducer,
2344
+ files: errors.slice(0, HEALTH_FILE_SAMPLE).map((e) => e.file),
2345
+ updatedAt: now
2346
+ };
2347
+ await import_node_fs5.promises.mkdir(import_node_path5.default.dirname(healthPath), { recursive: true });
2348
+ await import_node_fs5.promises.writeFile(healthPath, JSON.stringify(coverage), "utf8");
2349
+ }
2350
+ async function readExtractionHealth(healthPath) {
2351
+ let raw;
2352
+ try {
2353
+ raw = await import_node_fs5.promises.readFile(healthPath, "utf8");
2354
+ } catch {
2355
+ return void 0;
2356
+ }
2357
+ try {
2358
+ const obj = JSON.parse(raw);
2359
+ if (typeof obj?.skippedFiles !== "number" || !obj.byProducer || !Array.isArray(obj.files)) {
2360
+ return void 0;
2361
+ }
2362
+ return obj;
2363
+ } catch {
2364
+ return void 0;
2365
+ }
2366
+ }
2332
2367
  function isStrictExtractionEnabled() {
2333
2368
  const raw = process.env.NEAT_STRICT_EXTRACTION;
2334
2369
  return raw === "1" || raw === "true";
@@ -3292,14 +3327,16 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
3292
3327
  return best ?? relPath;
3293
3328
  }
3294
3329
  function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
3295
- const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
3296
- const fileNodeId = (0, import_types6.fileId)(serviceName, relPath);
3330
+ const svcAttrs = graph.hasNode(serviceNodeId) ? graph.getNodeAttributes(serviceNodeId) : void 0;
3331
+ const canonicalService = svcAttrs && svcAttrs.type === import_types6.NodeType.ServiceNode && typeof svcAttrs.name === "string" ? svcAttrs.name : serviceName;
3332
+ const relPath = reconcileObservedRelPath(graph, canonicalService, callSite.relPath);
3333
+ const fileNodeId = (0, import_types6.fileId)(canonicalService, relPath);
3297
3334
  if (!graph.hasNode(fileNodeId)) {
3298
3335
  const language = languageForExt(relPath);
3299
3336
  const node = {
3300
3337
  id: fileNodeId,
3301
3338
  type: import_types6.NodeType.FileNode,
3302
- service: serviceName,
3339
+ service: canonicalService,
3303
3340
  path: relPath,
3304
3341
  ...language ? { language } : {},
3305
3342
  ...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
@@ -8424,6 +8461,15 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
8424
8461
  );
8425
8462
  }
8426
8463
  }
8464
+ if (opts.errorsPath) {
8465
+ try {
8466
+ await writeExtractionHealth(errorEntries, extractionHealthPathFor(opts.errorsPath));
8467
+ } catch (err) {
8468
+ console.warn(
8469
+ `[neat] failed to write extraction health sidecar: ${err.message}`
8470
+ );
8471
+ }
8472
+ }
8427
8473
  const droppedEntries = drainDroppedExtracted();
8428
8474
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
8429
8475
  const rejectedPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.errorsPath), "rejected.ndjson");
@@ -10320,7 +10366,11 @@ function resolveProject(registry, req, reply, bootstrap, singleProject) {
10320
10366
  void reply.code(503).send({ ready: false, project: name, status: "broken" });
10321
10367
  return null;
10322
10368
  }
10323
- void reply.code(404).send({ error: "project not found", project: name });
10369
+ void reply.code(404).send({
10370
+ error: "project not found",
10371
+ project: name,
10372
+ hint: "This daemon does not host that project. GET /projects lists what it serves (see hostedHere)."
10373
+ });
10324
10374
  return null;
10325
10375
  }
10326
10376
  return ctx;
@@ -10358,6 +10408,9 @@ function registerRoutes(scope, ctx) {
10358
10408
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
10359
10409
  if (!proj) return;
10360
10410
  const uptimeMs = Date.now() - startedAt;
10411
+ const coverage = await readExtractionHealth(
10412
+ extractionHealthPathFor(proj.paths.errorsPath)
10413
+ );
10361
10414
  return {
10362
10415
  ok: true,
10363
10416
  project: proj.name,
@@ -10368,7 +10421,8 @@ function registerRoutes(scope, ctx) {
10368
10421
  uptime: Math.floor(uptimeMs / 1e3),
10369
10422
  nodeCount: proj.graph.order,
10370
10423
  edgeCount: proj.graph.size,
10371
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
10424
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
10425
+ ...coverage ? { coverage } : {}
10372
10426
  };
10373
10427
  });
10374
10428
  }
@@ -11010,12 +11064,17 @@ async function buildApi(opts) {
11010
11064
  // The daemon is serving this project, so it's active unless its
11011
11065
  // bootstrap broke. ('bootstrapping' still reads as active — the project
11012
11066
  // is the one to land on; its graph route answers 503 until ready.)
11013
- status: phase === "broken" ? "broken" : "active"
11067
+ status: phase === "broken" ? "broken" : "active",
11068
+ hostedHere: true
11014
11069
  };
11015
11070
  return [entry2];
11016
11071
  }
11017
11072
  try {
11018
- return await listProjects();
11073
+ const entries = await listProjects();
11074
+ return entries.map((entry2) => ({
11075
+ ...entry2,
11076
+ hostedHere: registry.has(entry2.name)
11077
+ }));
11019
11078
  } catch (err) {
11020
11079
  return reply.code(500).send({
11021
11080
  error: "failed to read project registry",
@@ -11029,7 +11088,21 @@ async function buildApi(opts) {
11029
11088
  if (!entry2) {
11030
11089
  return reply.code(404).send({ error: "project not found", project: req.params.project });
11031
11090
  }
11032
- return { project: entry2 };
11091
+ const hostedHere = registry.has(entry2.name);
11092
+ if (!hostedHere) {
11093
+ const owner = await findDaemonByProject(entry2.name).catch(() => void 0);
11094
+ if (owner) {
11095
+ return {
11096
+ project: { ...entry2, hostedHere },
11097
+ servedBy: {
11098
+ path: owner.record.projectPath,
11099
+ restPort: owner.record.ports.rest,
11100
+ live: owner.live
11101
+ }
11102
+ };
11103
+ }
11104
+ }
11105
+ return { project: { ...entry2, hostedHere } };
11033
11106
  } catch (err) {
11034
11107
  return reply.code(500).send({
11035
11108
  error: "failed to read project registry",