@neat.is/core 0.3.4 → 0.3.5

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
@@ -39,7 +39,7 @@ import {
39
39
  setStatus,
40
40
  startPersistLoop,
41
41
  startStalenessLoop
42
- } from "./chunk-33ZZ2ZID.js";
42
+ } from "./chunk-BUB3ASD5.js";
43
43
  import {
44
44
  startOtelGrpcReceiver
45
45
  } from "./chunk-G3PDTGOW.js";
package/dist/index.cjs CHANGED
@@ -794,11 +794,16 @@ init_cjs_shims();
794
794
  var import_types = require("@neat.is/types");
795
795
  var ROOT_CAUSE_MAX_DEPTH = 5;
796
796
  var BLAST_RADIUS_DEFAULT_DEPTH = 10;
797
+ function isFrontierNode(graph, nodeId) {
798
+ if (!graph.hasNode(nodeId)) return false;
799
+ const attrs = graph.getNodeAttributes(nodeId);
800
+ return attrs.type === import_types.NodeType.FrontierNode;
801
+ }
797
802
  function bestEdgeBySource(graph, edgeIds) {
798
803
  const best = /* @__PURE__ */ new Map();
799
804
  for (const id of edgeIds) {
800
805
  const e = graph.getEdgeAttributes(id);
801
- if (e.provenance === import_types.Provenance.FRONTIER) continue;
806
+ if (isFrontierNode(graph, e.source)) continue;
802
807
  const cur = best.get(e.source);
803
808
  if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
804
809
  best.set(e.source, e);
@@ -810,7 +815,7 @@ function bestEdgeByTarget(graph, edgeIds) {
810
815
  const best = /* @__PURE__ */ new Map();
811
816
  for (const id of edgeIds) {
812
817
  const e = graph.getEdgeAttributes(id);
813
- if (e.provenance === import_types.Provenance.FRONTIER) continue;
818
+ if (isFrontierNode(graph, e.target)) continue;
814
819
  const cur = best.get(e.target);
815
820
  if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
816
821
  best.set(e.target, e);
@@ -822,8 +827,7 @@ var PROVENANCE_CEILING = {
822
827
  OBSERVED: 1,
823
828
  INFERRED: 0.7,
824
829
  EXTRACTED: 0.5,
825
- STALE: 0.3,
826
- FRONTIER: 0.3
830
+ STALE: 0.3
827
831
  };
828
832
  function volumeWeight(spanCount) {
829
833
  if (!spanCount || spanCount <= 0) return 0.5;
@@ -1107,8 +1111,8 @@ var evaluateStructural = ({
1107
1111
  for (const edgeId of graph.outboundEdges(id)) {
1108
1112
  const e = graph.getEdgeAttributes(edgeId);
1109
1113
  if (e.type !== rule.edgeType) continue;
1110
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1111
1114
  const target = graph.getNodeAttributes(e.target);
1115
+ if (target.type === import_types2.NodeType.FrontierNode) continue;
1112
1116
  if (target.type === rule.toNodeType) {
1113
1117
  satisfied = true;
1114
1118
  break;
@@ -1227,8 +1231,8 @@ var evaluateCompatibility = ({
1227
1231
  for (const edgeId of graph.outboundEdges(svcId)) {
1228
1232
  const e = graph.getEdgeAttributes(edgeId);
1229
1233
  if (e.type !== import_types2.EdgeType.CONNECTS_TO) continue;
1230
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1231
1234
  const dbAttrs = graph.getNodeAttributes(e.target);
1235
+ if (dbAttrs.type === import_types2.NodeType.FrontierNode) continue;
1232
1236
  if (dbAttrs.type !== import_types2.NodeType.DatabaseNode) continue;
1233
1237
  const db = dbAttrs;
1234
1238
  for (const pair of compatPairs()) {
@@ -1545,31 +1549,6 @@ function ensureFrontierNode(graph, host, ts) {
1545
1549
  graph.addNode(id, node);
1546
1550
  return id;
1547
1551
  }
1548
- function upsertFrontierEdge(graph, type, source, target, ts) {
1549
- const id = (0, import_types3.frontierEdgeId)(source, target, type);
1550
- if (graph.hasEdge(id)) {
1551
- const existing = graph.getEdgeAttributes(id);
1552
- const updated = {
1553
- ...existing,
1554
- provenance: import_types3.Provenance.FRONTIER,
1555
- lastObserved: ts,
1556
- callCount: (existing.callCount ?? 0) + 1
1557
- };
1558
- graph.replaceEdgeAttributes(id, updated);
1559
- return;
1560
- }
1561
- const edge = {
1562
- id,
1563
- source,
1564
- target,
1565
- type,
1566
- provenance: import_types3.Provenance.FRONTIER,
1567
- confidence: 1,
1568
- lastObserved: ts,
1569
- callCount: 1
1570
- };
1571
- graph.addEdgeWithKey(id, source, target, edge);
1572
- }
1573
1552
  function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1574
1553
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1575
1554
  const id = makeObservedEdgeId(type, source, target);
@@ -1655,18 +1634,28 @@ async function appendErrorEvent(ctx, ev) {
1655
1634
  await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
1656
1635
  await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
1657
1636
  }
1637
+ function sanitizeAttributes(attrs) {
1638
+ const out = {};
1639
+ for (const [k, v] of Object.entries(attrs)) {
1640
+ if (typeof v === "bigint") out[k] = v.toString();
1641
+ else out[k] = v;
1642
+ }
1643
+ return out;
1644
+ }
1658
1645
  function buildErrorEventForReceiver(span) {
1659
1646
  if (span.statusCode !== 2) return null;
1660
1647
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
1648
+ const attrs = sanitizeAttributes(span.attributes);
1661
1649
  return {
1662
1650
  id: `${span.traceId}:${span.spanId}`,
1663
1651
  timestamp: ts,
1664
1652
  service: span.service,
1665
1653
  traceId: span.traceId,
1666
1654
  spanId: span.spanId,
1667
- errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1655
+ errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1668
1656
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1669
1657
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1658
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1670
1659
  affectedNode: (0, import_types3.serviceId)(span.service)
1671
1660
  };
1672
1661
  }
@@ -1717,11 +1706,16 @@ async function handleSpan(ctx, span) {
1717
1706
  affectedNode = targetId;
1718
1707
  resolvedViaAddress = true;
1719
1708
  } else if (!targetId) {
1720
- const frontierId2 = ensureFrontierNode(ctx.graph, host, ts);
1721
- if (ctx.graph.hasNode(sourceId)) {
1722
- upsertFrontierEdge(ctx.graph, import_types3.EdgeType.CALLS, sourceId, frontierId2, ts);
1723
- }
1724
- affectedNode = frontierId2;
1709
+ const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
1710
+ upsertObservedEdge(
1711
+ ctx.graph,
1712
+ import_types3.EdgeType.CALLS,
1713
+ sourceId,
1714
+ frontierNodeId,
1715
+ ts,
1716
+ isError
1717
+ );
1718
+ affectedNode = frontierNodeId;
1725
1719
  resolvedViaAddress = true;
1726
1720
  }
1727
1721
  }
@@ -1743,15 +1737,17 @@ async function handleSpan(ctx, span) {
1743
1737
  if (span.statusCode === 2) {
1744
1738
  stitchTrace(ctx.graph, sourceId, ts);
1745
1739
  if (ctx.writeErrorEventInline !== false) {
1740
+ const attrs = sanitizeAttributes(span.attributes);
1746
1741
  const ev = {
1747
1742
  id: `${span.traceId}:${span.spanId}`,
1748
1743
  timestamp: ts,
1749
1744
  service: span.service,
1750
1745
  traceId: span.traceId,
1751
1746
  spanId: span.spanId,
1752
- errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1747
+ errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1753
1748
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1754
1749
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1750
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1755
1751
  affectedNode
1756
1752
  };
1757
1753
  await appendErrorEvent(ctx, ev);
@@ -1807,8 +1803,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
1807
1803
  }
1808
1804
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1809
1805
  graph.dropEdge(oldEdgeId);
1810
- const promotedProvenance = edge.provenance === import_types3.Provenance.FRONTIER ? import_types3.Provenance.OBSERVED : edge.provenance;
1811
- const newId = promotedProvenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.EXTRACTED ? (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.frontierEdgeId)(newSource, newTarget, edge.type);
1806
+ const newId = edge.provenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : edge.provenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type);
1812
1807
  if (graph.hasEdge(newId)) {
1813
1808
  const existing = graph.getEdgeAttributes(newId);
1814
1809
  const merged = {
@@ -1823,8 +1818,7 @@ function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1823
1818
  ...edge,
1824
1819
  id: newId,
1825
1820
  source: newSource,
1826
- target: newTarget,
1827
- provenance: promotedProvenance
1821
+ target: newTarget
1828
1822
  };
1829
1823
  graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
1830
1824
  }
@@ -4121,7 +4115,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4121
4115
  init_cjs_shims();
4122
4116
  var import_node_fs18 = require("fs");
4123
4117
  var import_node_path31 = __toESM(require("path"), 1);
4124
- var SCHEMA_VERSION = 2;
4118
+ var import_types19 = require("@neat.is/types");
4119
+ var SCHEMA_VERSION = 3;
4125
4120
  function migrateV1ToV2(payload) {
4126
4121
  const nodes = payload.graph.nodes;
4127
4122
  if (Array.isArray(nodes)) {
@@ -4133,6 +4128,25 @@ function migrateV1ToV2(payload) {
4133
4128
  }
4134
4129
  return { ...payload, schemaVersion: 2 };
4135
4130
  }
4131
+ function migrateV2ToV3(payload) {
4132
+ const edges = payload.graph.edges;
4133
+ if (Array.isArray(edges)) {
4134
+ for (const edge of edges) {
4135
+ const attrs = edge.attributes;
4136
+ if (!attrs || attrs.provenance !== "FRONTIER") continue;
4137
+ attrs.provenance = import_types19.Provenance.OBSERVED;
4138
+ const type = typeof attrs.type === "string" ? attrs.type : void 0;
4139
+ const source = typeof attrs.source === "string" ? attrs.source : void 0;
4140
+ const target = typeof attrs.target === "string" ? attrs.target : void 0;
4141
+ if (type && source && target) {
4142
+ const newId = (0, import_types19.observedEdgeId)(source, target, type);
4143
+ attrs.id = newId;
4144
+ if (edge.key) edge.key = newId;
4145
+ }
4146
+ }
4147
+ }
4148
+ return { ...payload, schemaVersion: 3 };
4149
+ }
4136
4150
  async function ensureDir(filePath) {
4137
4151
  await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4138
4152
  }
@@ -4159,6 +4173,9 @@ async function loadGraphFromDisk(graph, outPath) {
4159
4173
  if (payload.schemaVersion === 1) {
4160
4174
  payload = migrateV1ToV2(payload);
4161
4175
  }
4176
+ if (payload.schemaVersion === 2) {
4177
+ payload = migrateV2ToV3(payload);
4178
+ }
4162
4179
  if (payload.schemaVersion !== SCHEMA_VERSION) {
4163
4180
  throw new Error(
4164
4181
  `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
@@ -4205,11 +4222,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
4205
4222
  init_cjs_shims();
4206
4223
  var import_fastify = __toESM(require("fastify"), 1);
4207
4224
  var import_cors = __toESM(require("@fastify/cors"), 1);
4208
- var import_types21 = require("@neat.is/types");
4225
+ var import_types22 = require("@neat.is/types");
4209
4226
 
4210
4227
  // src/divergences.ts
4211
4228
  init_cjs_shims();
4212
- var import_types19 = require("@neat.is/types");
4229
+ var import_types20 = require("@neat.is/types");
4213
4230
  function bucketKey(source, target, type) {
4214
4231
  return `${type}|${source}|${target}`;
4215
4232
  }
@@ -4217,25 +4234,22 @@ function bucketEdges(graph) {
4217
4234
  const buckets = /* @__PURE__ */ new Map();
4218
4235
  graph.forEachEdge((id, attrs) => {
4219
4236
  const e = attrs;
4220
- const parsed = (0, import_types19.parseEdgeId)(id);
4237
+ const parsed = (0, import_types20.parseEdgeId)(id);
4221
4238
  const provenance = parsed?.provenance ?? e.provenance;
4222
4239
  const key = bucketKey(e.source, e.target, e.type);
4223
4240
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4224
4241
  switch (provenance) {
4225
- case import_types19.Provenance.EXTRACTED:
4242
+ case import_types20.Provenance.EXTRACTED:
4226
4243
  cur.extracted = e;
4227
4244
  break;
4228
- case import_types19.Provenance.OBSERVED:
4245
+ case import_types20.Provenance.OBSERVED:
4229
4246
  cur.observed = e;
4230
4247
  break;
4231
- case import_types19.Provenance.INFERRED:
4248
+ case import_types20.Provenance.INFERRED:
4232
4249
  cur.inferred = e;
4233
4250
  break;
4234
- case import_types19.Provenance.FRONTIER:
4235
- cur.frontier = e;
4236
- break;
4237
4251
  default:
4238
- if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4252
+ if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4239
4253
  }
4240
4254
  buckets.set(key, cur);
4241
4255
  });
@@ -4244,7 +4258,7 @@ function bucketEdges(graph) {
4244
4258
  function nodeIsFrontier(graph, nodeId) {
4245
4259
  if (!graph.hasNode(nodeId)) return false;
4246
4260
  const attrs = graph.getNodeAttributes(nodeId);
4247
- return attrs.type === import_types19.NodeType.FrontierNode;
4261
+ return attrs.type === import_types20.NodeType.FrontierNode;
4248
4262
  }
4249
4263
  function clampConfidence(n) {
4250
4264
  if (!Number.isFinite(n)) return 0;
@@ -4305,7 +4319,7 @@ function declaredHostFor(svc) {
4305
4319
  function hasExtractedConfiguredBy(graph, svcId) {
4306
4320
  for (const edgeId of graph.outboundEdges(svcId)) {
4307
4321
  const e = graph.getEdgeAttributes(edgeId);
4308
- if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4322
+ if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4309
4323
  return true;
4310
4324
  }
4311
4325
  }
@@ -4318,10 +4332,10 @@ function detectHostMismatch(graph, svcId, svc) {
4318
4332
  const out = [];
4319
4333
  for (const edgeId of graph.outboundEdges(svcId)) {
4320
4334
  const edge = graph.getEdgeAttributes(edgeId);
4321
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4322
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4335
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4336
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4323
4337
  const target = graph.getNodeAttributes(edge.target);
4324
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4338
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4325
4339
  const observedHost = target.host?.trim();
4326
4340
  if (!observedHost) continue;
4327
4341
  if (observedHost === declaredHost) continue;
@@ -4343,10 +4357,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4343
4357
  const deps = svc.dependencies ?? {};
4344
4358
  for (const edgeId of graph.outboundEdges(svcId)) {
4345
4359
  const edge = graph.getEdgeAttributes(edgeId);
4346
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4347
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4360
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4361
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4348
4362
  const target = graph.getNodeAttributes(edge.target);
4349
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4363
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4350
4364
  for (const pair of compatPairs()) {
4351
4365
  if (pair.engine !== target.engine) continue;
4352
4366
  const declared = deps[pair.driver];
@@ -4407,7 +4421,7 @@ function computeDivergences(graph, opts = {}) {
4407
4421
  }
4408
4422
  graph.forEachNode((nodeId, attrs) => {
4409
4423
  const n = attrs;
4410
- if (n.type !== import_types19.NodeType.ServiceNode) return;
4424
+ if (n.type !== import_types20.NodeType.ServiceNode) return;
4411
4425
  const svc = n;
4412
4426
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4413
4427
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4440,7 +4454,7 @@ function computeDivergences(graph, opts = {}) {
4440
4454
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4441
4455
  return a.target.localeCompare(b.target);
4442
4456
  });
4443
- return import_types19.DivergenceResultSchema.parse({
4457
+ return import_types20.DivergenceResultSchema.parse({
4444
4458
  divergences: filtered,
4445
4459
  totalAffected: filtered.length,
4446
4460
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4581,7 +4595,7 @@ init_cjs_shims();
4581
4595
  var import_node_fs20 = require("fs");
4582
4596
  var import_node_os2 = __toESM(require("os"), 1);
4583
4597
  var import_node_path33 = __toESM(require("path"), 1);
4584
- var import_types20 = require("@neat.is/types");
4598
+ var import_types21 = require("@neat.is/types");
4585
4599
  var LOCK_TIMEOUT_MS = 5e3;
4586
4600
  var LOCK_RETRY_MS = 50;
4587
4601
  function neatHome() {
@@ -4660,10 +4674,10 @@ async function readRegistry() {
4660
4674
  throw err;
4661
4675
  }
4662
4676
  const parsed = JSON.parse(raw);
4663
- return import_types20.RegistryFileSchema.parse(parsed);
4677
+ return import_types21.RegistryFileSchema.parse(parsed);
4664
4678
  }
4665
4679
  async function writeRegistry(reg) {
4666
- const validated = import_types20.RegistryFileSchema.parse(reg);
4680
+ const validated = import_types21.RegistryFileSchema.parse(reg);
4667
4681
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4668
4682
  }
4669
4683
  var ProjectNameCollisionError = class extends Error {
@@ -4923,11 +4937,11 @@ function registerRoutes(scope, ctx) {
4923
4937
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4924
4938
  const parsed = [];
4925
4939
  for (const c of candidates) {
4926
- const r = import_types21.DivergenceTypeSchema.safeParse(c);
4940
+ const r = import_types22.DivergenceTypeSchema.safeParse(c);
4927
4941
  if (!r.success) {
4928
4942
  return reply.code(400).send({
4929
4943
  error: `unknown divergence type "${c}"`,
4930
- allowed: import_types21.DivergenceTypeSchema.options
4944
+ allowed: import_types22.DivergenceTypeSchema.options
4931
4945
  });
4932
4946
  }
4933
4947
  parsed.push(r.data);
@@ -5111,7 +5125,7 @@ function registerRoutes(scope, ctx) {
5111
5125
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5112
5126
  let violations = await log.readAll();
5113
5127
  if (req.query.severity) {
5114
- const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
5128
+ const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5115
5129
  if (!sev.success) {
5116
5130
  return reply.code(400).send({
5117
5131
  error: "invalid severity",
@@ -5128,7 +5142,7 @@ function registerRoutes(scope, ctx) {
5128
5142
  scope.post("/policies/check", async (req, reply) => {
5129
5143
  const proj = resolveProject(registry, req, reply);
5130
5144
  if (!proj) return;
5131
- const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5145
+ const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5132
5146
  if (!parsed.success) {
5133
5147
  return reply.code(400).send({
5134
5148
  error: "invalid /policies/check body",