@neat.is/core 0.3.1 → 0.3.3

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
@@ -21,7 +21,9 @@ import {
21
21
  ensureCompatLoaded,
22
22
  evaluateAllPolicies,
23
23
  extractFromDirectory,
24
+ formatExtractionBanner,
24
25
  getGraph,
26
+ isStrictExtractionEnabled,
25
27
  listProjects,
26
28
  loadGraphFromDisk,
27
29
  loadPolicyFile,
@@ -31,11 +33,12 @@ import {
31
33
  promoteFrontierNodes,
32
34
  removeProject,
33
35
  resetGraph,
36
+ retireEdgesByFile,
34
37
  saveGraphToDisk,
35
38
  setStatus,
36
39
  startPersistLoop,
37
40
  startStalenessLoop
38
- } from "./chunk-FIXKIYNF.js";
41
+ } from "./chunk-VABXPLDT.js";
39
42
  import {
40
43
  startOtelGrpcReceiver
41
44
  } from "./chunk-G3PDTGOW.js";
@@ -45,28 +48,12 @@ import {
45
48
 
46
49
  // src/cli.ts
47
50
  import path4 from "path";
48
- import { promises as fs3 } from "fs";
51
+ import { promises as fs4 } from "fs";
49
52
 
50
53
  // src/watch.ts
54
+ import fs from "fs";
51
55
  import path from "path";
52
56
  import chokidar from "chokidar";
53
-
54
- // src/extract/retire.ts
55
- import { Provenance } from "@neat.is/types";
56
- function retireEdgesByFile(graph, file) {
57
- const normalized = file.split("\\").join("/");
58
- const toDrop = [];
59
- graph.forEachEdge((id, attrs) => {
60
- const edge = attrs;
61
- if (edge.provenance !== Provenance.EXTRACTED) return;
62
- if (!edge.evidence?.file) return;
63
- if (edge.evidence.file === normalized) toDrop.push(id);
64
- });
65
- for (const id of toDrop) graph.dropEdge(id);
66
- return toDrop.length;
67
- }
68
-
69
- // src/watch.ts
70
57
  var ALL_PHASES = [
71
58
  "services",
72
59
  "aliases",
@@ -143,6 +130,16 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
143
130
  durationMs: Date.now() - started
144
131
  };
145
132
  }
133
+ var IGNORED_WATCH_GLOBS = [
134
+ "**/node_modules/**",
135
+ "**/.git/**",
136
+ "**/dist/**",
137
+ "**/build/**",
138
+ "**/.turbo/**",
139
+ "**/.next/**",
140
+ "**/neat-out/**",
141
+ "**/.DS_Store"
142
+ ];
146
143
  var IGNORED_WATCH_PATHS = [
147
144
  /(?:^|[\\/])node_modules[\\/]/,
148
145
  /(?:^|[\\/])\.git[\\/]/,
@@ -156,6 +153,35 @@ var IGNORED_WATCH_PATHS = [
156
153
  function shouldIgnore(absPath) {
157
154
  return IGNORED_WATCH_PATHS.some((re) => re.test(absPath));
158
155
  }
156
+ var DARWIN_POLLING_DIR_THRESHOLD = 400;
157
+ function countWatchableDirs(scanPath, limit) {
158
+ let count = 0;
159
+ const visit = (dir, depth) => {
160
+ if (count >= limit) return;
161
+ let entries;
162
+ try {
163
+ entries = fs.readdirSync(dir, { withFileTypes: true });
164
+ } catch {
165
+ return;
166
+ }
167
+ for (const e of entries) {
168
+ if (count >= limit) return;
169
+ if (!e.isDirectory()) continue;
170
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(path.join(dir, e.name) + path.sep))) continue;
171
+ count++;
172
+ if (depth < 2) visit(path.join(dir, e.name), depth + 1);
173
+ }
174
+ };
175
+ visit(scanPath, 0);
176
+ return count;
177
+ }
178
+ function shouldUsePolling(scanPath) {
179
+ const env = process.env.NEAT_WATCH_POLLING;
180
+ if (env === "1" || env === "true") return true;
181
+ if (env === "0" || env === "false") return false;
182
+ if (process.platform !== "darwin") return false;
183
+ return countWatchableDirs(scanPath, DARWIN_POLLING_DIR_THRESHOLD) >= DARWIN_POLLING_DIR_THRESHOLD;
184
+ }
159
185
  async function startWatch(graph, opts) {
160
186
  const debounceMs = opts.debounceMs ?? 1e3;
161
187
  const projectName = opts.project ?? DEFAULT_PROJECT;
@@ -325,10 +351,19 @@ async function startWatch(graph, opts) {
325
351
  }
326
352
  schedule();
327
353
  };
354
+ const usePolling = shouldUsePolling(opts.scanPath);
355
+ if (usePolling) {
356
+ const reason = process.env.NEAT_WATCH_POLLING === "1" || process.env.NEAT_WATCH_POLLING === "true" ? "NEAT_WATCH_POLLING env override" : "darwin heuristic \u2014 large scan root, kqueue cap risk";
357
+ console.log(`[${projectName}] watch: usePolling=true (${reason})`);
358
+ }
328
359
  const watcher = chokidar.watch(opts.scanPath, {
329
360
  ignoreInitial: true,
330
- ignored: (p) => shouldIgnore(p),
361
+ // Glob array prunes at descent time (#233) so chokidar never opens a
362
+ // kqueue handle for `node_modules` and friends. The function backstop
363
+ // catches any path that slipped through and matches the regex set.
364
+ ignored: [...IGNORED_WATCH_GLOBS, (p) => shouldIgnore(p)],
331
365
  persistent: true,
366
+ usePolling,
332
367
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
333
368
  });
334
369
  watcher.on("add", onPath);
@@ -360,7 +395,7 @@ async function startWatch(graph, opts) {
360
395
  }
361
396
 
362
397
  // src/installers/javascript.ts
363
- import { promises as fs } from "fs";
398
+ import { promises as fs2 } from "fs";
364
399
  import path2 from "path";
365
400
  var SDK_PACKAGES = [
366
401
  { name: "@opentelemetry/api", version: "^1.9.0" },
@@ -377,7 +412,7 @@ var OTEL_ENV = {
377
412
  };
378
413
  async function readPackageJson(serviceDir) {
379
414
  try {
380
- const raw = await fs.readFile(path2.join(serviceDir, "package.json"), "utf8");
415
+ const raw = await fs2.readFile(path2.join(serviceDir, "package.json"), "utf8");
381
416
  return JSON.parse(raw);
382
417
  } catch {
383
418
  return null;
@@ -443,7 +478,7 @@ async function apply(installPlan) {
443
478
  const originals = /* @__PURE__ */ new Map();
444
479
  for (const file of touched) {
445
480
  try {
446
- originals.set(file, await fs.readFile(file, "utf8"));
481
+ originals.set(file, await fs2.readFile(file, "utf8"));
447
482
  } catch {
448
483
  }
449
484
  }
@@ -469,8 +504,8 @@ async function apply(installPlan) {
469
504
  }
470
505
  const newRaw = JSON.stringify(pkg, null, 2) + "\n";
471
506
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
472
- await fs.writeFile(tmp, newRaw, "utf8");
473
- await fs.rename(tmp, file);
507
+ await fs2.writeFile(tmp, newRaw, "utf8");
508
+ await fs2.rename(tmp, file);
474
509
  }
475
510
  } catch (err) {
476
511
  await rollback(installPlan, originals);
@@ -481,7 +516,7 @@ async function rollback(installPlan, originals) {
481
516
  const restored = [];
482
517
  for (const [file, raw] of originals.entries()) {
483
518
  try {
484
- await fs.writeFile(file, raw, "utf8");
519
+ await fs2.writeFile(file, raw, "utf8");
485
520
  restored.push(file);
486
521
  } catch {
487
522
  }
@@ -496,7 +531,7 @@ async function rollback(installPlan, originals) {
496
531
  ""
497
532
  ];
498
533
  const rollbackPath = path2.join(installPlan.serviceDir, "neat-rollback.patch");
499
- await fs.writeFile(rollbackPath, lines.join("\n"), "utf8");
534
+ await fs2.writeFile(rollbackPath, lines.join("\n"), "utf8");
500
535
  }
501
536
  var javascriptInstaller = {
502
537
  name: "javascript",
@@ -506,7 +541,7 @@ var javascriptInstaller = {
506
541
  };
507
542
 
508
543
  // src/installers/python.ts
509
- import { promises as fs2 } from "fs";
544
+ import { promises as fs3 } from "fs";
510
545
  import path3 from "path";
511
546
  var SDK_PACKAGES2 = [
512
547
  { name: "opentelemetry-distro", version: ">=0.49b0" },
@@ -519,7 +554,7 @@ var OTEL_ENV2 = {
519
554
  };
520
555
  async function exists(p) {
521
556
  try {
522
- await fs2.stat(p);
557
+ await fs3.stat(p);
523
558
  return true;
524
559
  } catch {
525
560
  return false;
@@ -540,7 +575,7 @@ function reqPackageName(line) {
540
575
  async function planRequirementsTxtEdits(serviceDir) {
541
576
  const file = path3.join(serviceDir, "requirements.txt");
542
577
  if (!await exists(file)) return null;
543
- const raw = await fs2.readFile(file, "utf8");
578
+ const raw = await fs3.readFile(file, "utf8");
544
579
  const presentNames = new Set(
545
580
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
546
581
  );
@@ -550,7 +585,7 @@ async function planRequirementsTxtEdits(serviceDir) {
550
585
  async function planProcfileEdits(serviceDir) {
551
586
  const procfile = path3.join(serviceDir, "Procfile");
552
587
  if (!await exists(procfile)) return [];
553
- const raw = await fs2.readFile(procfile, "utf8");
588
+ const raw = await fs3.readFile(procfile, "utf8");
554
589
  const edits = [];
555
590
  for (const line of raw.split(/\r?\n/)) {
556
591
  if (line.length === 0) continue;
@@ -602,8 +637,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
602
637
  const next = `${original}${trailing}${newlines.join("\n")}
603
638
  `;
604
639
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
605
- await fs2.writeFile(tmp, next, "utf8");
606
- await fs2.rename(tmp, manifest);
640
+ await fs3.writeFile(tmp, next, "utf8");
641
+ await fs3.rename(tmp, manifest);
607
642
  }
608
643
  async function applyProcfile(procfile, edits, original) {
609
644
  let next = original;
@@ -612,8 +647,8 @@ async function applyProcfile(procfile, edits, original) {
612
647
  next = next.replace(e.before, e.after);
613
648
  }
614
649
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
615
- await fs2.writeFile(tmp, next, "utf8");
616
- await fs2.rename(tmp, procfile);
650
+ await fs3.writeFile(tmp, next, "utf8");
651
+ await fs3.rename(tmp, procfile);
617
652
  }
618
653
  async function apply2(installPlan) {
619
654
  const touched = /* @__PURE__ */ new Set();
@@ -623,7 +658,7 @@ async function apply2(installPlan) {
623
658
  const originals = /* @__PURE__ */ new Map();
624
659
  for (const file of touched) {
625
660
  try {
626
- originals.set(file, await fs2.readFile(file, "utf8"));
661
+ originals.set(file, await fs3.readFile(file, "utf8"));
627
662
  } catch {
628
663
  }
629
664
  }
@@ -651,7 +686,7 @@ async function rollback2(installPlan, originals) {
651
686
  const restored = [];
652
687
  for (const [file, raw] of originals.entries()) {
653
688
  try {
654
- await fs2.writeFile(file, raw, "utf8");
689
+ await fs3.writeFile(file, raw, "utf8");
655
690
  restored.push(file);
656
691
  } catch {
657
692
  }
@@ -666,7 +701,7 @@ async function rollback2(installPlan, originals) {
666
701
  ""
667
702
  ];
668
703
  const rollbackPath = path3.join(installPlan.serviceDir, "neat-rollback.patch");
669
- await fs2.writeFile(rollbackPath, lines.join("\n"), "utf8");
704
+ await fs3.writeFile(rollbackPath, lines.join("\n"), "utf8");
670
705
  }
671
706
  var pythonInstaller = {
672
707
  name: "python",
@@ -757,7 +792,7 @@ function renderPatch(sections) {
757
792
  import { DivergenceTypeSchema } from "@neat.is/types";
758
793
 
759
794
  // src/cli-client.ts
760
- import { Provenance as Provenance2 } from "@neat.is/types";
795
+ import { Provenance } from "@neat.is/types";
761
796
  var HttpError = class extends Error {
762
797
  constructor(status, message, responseBody = "") {
763
798
  super(message);
@@ -892,7 +927,7 @@ async function runBlastRadius(client, input) {
892
927
  }
893
928
  }
894
929
  function formatBlastEntry(n) {
895
- const tag = n.edgeProvenance === Provenance2.STALE ? " [STALE \u2014 last seen too long ago]" : "";
930
+ const tag = n.edgeProvenance === Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
896
931
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
897
932
  }
898
933
  async function runDependencies(client, input) {
@@ -938,9 +973,9 @@ async function runObservedDependencies(client, input) {
938
973
  const edges = await client.get(
939
974
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
940
975
  );
941
- const observed = edges.outbound.filter((e) => e.provenance === Provenance2.OBSERVED);
976
+ const observed = edges.outbound.filter((e) => e.provenance === Provenance.OBSERVED);
942
977
  if (observed.length === 0) {
943
- const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance2.EXTRACTED);
978
+ const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance.EXTRACTED);
944
979
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
945
980
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
946
981
  }
@@ -948,7 +983,7 @@ async function runObservedDependencies(client, input) {
948
983
  return {
949
984
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
950
985
  block: blockLines.join("\n"),
951
- provenance: Provenance2.OBSERVED
986
+ provenance: Provenance.OBSERVED
952
987
  };
953
988
  } catch (err) {
954
989
  if (err instanceof HttpError && err.status === 404) {
@@ -1002,7 +1037,7 @@ async function runIncidents(client, input) {
1002
1037
  return {
1003
1038
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
1004
1039
  block: blockLines.join("\n"),
1005
- provenance: Provenance2.OBSERVED
1040
+ provenance: Provenance.OBSERVED
1006
1041
  };
1007
1042
  } catch (err) {
1008
1043
  if (err instanceof HttpError && err.status === 404) {
@@ -1111,7 +1146,7 @@ async function runStaleEdges(client, input) {
1111
1146
  return {
1112
1147
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
1113
1148
  block: blockLines.join("\n"),
1114
- provenance: Provenance2.STALE
1149
+ provenance: Provenance.STALE
1115
1150
  };
1116
1151
  }
1117
1152
  async function runPolicies(client, input) {
@@ -1500,7 +1535,7 @@ async function buildPatchSections(services) {
1500
1535
  }
1501
1536
  async function runInit(opts) {
1502
1537
  const written = [];
1503
- const stat = await fs3.stat(opts.scanPath).catch(() => null);
1538
+ const stat = await fs4.stat(opts.scanPath).catch(() => null);
1504
1539
  if (!stat || !stat.isDirectory()) {
1505
1540
  console.error(`neat init: ${opts.scanPath} is not a directory`);
1506
1541
  return { exitCode: 2, writtenFiles: written };
@@ -1511,7 +1546,7 @@ async function runInit(opts) {
1511
1546
  const patch = renderPatch(sections);
1512
1547
  const patchPath = path4.join(opts.scanPath, "neat.patch");
1513
1548
  if (opts.dryRun) {
1514
- await fs3.writeFile(patchPath, patch, "utf8");
1549
+ await fs4.writeFile(patchPath, patch, "utf8");
1515
1550
  written.push(patchPath);
1516
1551
  console.log(`dry-run: patch written to ${patchPath}`);
1517
1552
  console.log("rerun without --dry-run to register and snapshot.");
@@ -1520,7 +1555,12 @@ async function runInit(opts) {
1520
1555
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
1521
1556
  resetGraph(graphKey);
1522
1557
  const graph = getGraph(graphKey);
1523
- const result = await extractFromDirectory(graph, opts.scanPath);
1558
+ const projectPaths = pathsForProject(
1559
+ graphKey,
1560
+ path4.join(opts.scanPath, "neat-out")
1561
+ );
1562
+ const errorsPath = path4.join(path4.dirname(opts.outPath), path4.basename(projectPaths.errorsPath));
1563
+ const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
1524
1564
  await saveGraphToDisk(graph, opts.outPath);
1525
1565
  written.push(opts.outPath);
1526
1566
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
@@ -1551,7 +1591,7 @@ async function runInit(opts) {
1551
1591
  console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
1552
1592
  }
1553
1593
  } else {
1554
- await fs3.writeFile(patchPath, patch, "utf8");
1594
+ await fs4.writeFile(patchPath, patch, "utf8");
1555
1595
  written.push(patchPath);
1556
1596
  }
1557
1597
  }
@@ -1565,6 +1605,10 @@ async function runInit(opts) {
1565
1605
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
1566
1606
  console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
1567
1607
  console.log(summarise(nodes, edges));
1608
+ console.log(formatExtractionBanner(result.extractionErrors));
1609
+ if (result.extractionErrors > 0) {
1610
+ console.log(`errors: ${errorsPath}`);
1611
+ }
1568
1612
  const incompatibilities = findIncompatibilities(nodes);
1569
1613
  if (incompatibilities.length > 0) {
1570
1614
  console.log("");
@@ -1575,6 +1619,9 @@ async function runInit(opts) {
1575
1619
  }
1576
1620
  }
1577
1621
  }
1622
+ if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
1623
+ return { exitCode: 4, writtenFiles: written };
1624
+ }
1578
1625
  return { exitCode: 0, writtenFiles: written };
1579
1626
  }
1580
1627
  var CLAUDE_SKILL_CONFIG = {
@@ -1605,7 +1652,7 @@ async function runSkill(opts) {
1605
1652
  const target = claudeConfigPath();
1606
1653
  let existing = {};
1607
1654
  try {
1608
- existing = JSON.parse(await fs3.readFile(target, "utf8"));
1655
+ existing = JSON.parse(await fs4.readFile(target, "utf8"));
1609
1656
  } catch (err) {
1610
1657
  if (err.code !== "ENOENT") {
1611
1658
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -1617,8 +1664,8 @@ async function runSkill(opts) {
1617
1664
  ...existing,
1618
1665
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
1619
1666
  };
1620
- await fs3.mkdir(path4.dirname(target), { recursive: true });
1621
- await fs3.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
1667
+ await fs4.mkdir(path4.dirname(target), { recursive: true });
1668
+ await fs4.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
1622
1669
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
1623
1670
  console.log("restart Claude Code to pick up the new MCP server.");
1624
1671
  return { exitCode: 0 };
@@ -1678,7 +1725,7 @@ async function main() {
1678
1725
  process.exit(2);
1679
1726
  }
1680
1727
  const scanPath = path4.resolve(target);
1681
- const stat = await fs3.stat(scanPath).catch(() => null);
1728
+ const stat = await fs4.stat(scanPath).catch(() => null);
1682
1729
  if (!stat || !stat.isDirectory()) {
1683
1730
  console.error(`neat watch: ${scanPath} is not a directory`);
1684
1731
  process.exit(2);