@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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-T3A2GK3X.js";
4
+ } from "./chunk-KCEZSFU2.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-33ZZ2ZID.js";
40
+ } from "./chunk-BUB3ASD5.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-G3PDTGOW.js";
package/dist/neatd.cjs CHANGED
@@ -749,11 +749,16 @@ init_cjs_shims();
749
749
  var import_types = require("@neat.is/types");
750
750
  var ROOT_CAUSE_MAX_DEPTH = 5;
751
751
  var BLAST_RADIUS_DEFAULT_DEPTH = 10;
752
+ function isFrontierNode(graph, nodeId) {
753
+ if (!graph.hasNode(nodeId)) return false;
754
+ const attrs = graph.getNodeAttributes(nodeId);
755
+ return attrs.type === import_types.NodeType.FrontierNode;
756
+ }
752
757
  function bestEdgeBySource(graph, edgeIds) {
753
758
  const best = /* @__PURE__ */ new Map();
754
759
  for (const id of edgeIds) {
755
760
  const e = graph.getEdgeAttributes(id);
756
- if (e.provenance === import_types.Provenance.FRONTIER) continue;
761
+ if (isFrontierNode(graph, e.source)) continue;
757
762
  const cur = best.get(e.source);
758
763
  if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
759
764
  best.set(e.source, e);
@@ -765,7 +770,7 @@ function bestEdgeByTarget(graph, edgeIds) {
765
770
  const best = /* @__PURE__ */ new Map();
766
771
  for (const id of edgeIds) {
767
772
  const e = graph.getEdgeAttributes(id);
768
- if (e.provenance === import_types.Provenance.FRONTIER) continue;
773
+ if (isFrontierNode(graph, e.target)) continue;
769
774
  const cur = best.get(e.target);
770
775
  if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
771
776
  best.set(e.target, e);
@@ -777,8 +782,7 @@ var PROVENANCE_CEILING = {
777
782
  OBSERVED: 1,
778
783
  INFERRED: 0.7,
779
784
  EXTRACTED: 0.5,
780
- STALE: 0.3,
781
- FRONTIER: 0.3
785
+ STALE: 0.3
782
786
  };
783
787
  function volumeWeight(spanCount) {
784
788
  if (!spanCount || spanCount <= 0) return 0.5;
@@ -1062,8 +1066,8 @@ var evaluateStructural = ({
1062
1066
  for (const edgeId of graph.outboundEdges(id)) {
1063
1067
  const e = graph.getEdgeAttributes(edgeId);
1064
1068
  if (e.type !== rule.edgeType) continue;
1065
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1066
1069
  const target = graph.getNodeAttributes(e.target);
1070
+ if (target.type === import_types2.NodeType.FrontierNode) continue;
1067
1071
  if (target.type === rule.toNodeType) {
1068
1072
  satisfied = true;
1069
1073
  break;
@@ -1182,8 +1186,8 @@ var evaluateCompatibility = ({
1182
1186
  for (const edgeId of graph.outboundEdges(svcId)) {
1183
1187
  const e = graph.getEdgeAttributes(edgeId);
1184
1188
  if (e.type !== import_types2.EdgeType.CONNECTS_TO) continue;
1185
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1186
1189
  const dbAttrs = graph.getNodeAttributes(e.target);
1190
+ if (dbAttrs.type === import_types2.NodeType.FrontierNode) continue;
1187
1191
  if (dbAttrs.type !== import_types2.NodeType.DatabaseNode) continue;
1188
1192
  const db = dbAttrs;
1189
1193
  for (const pair of compatPairs()) {
@@ -1478,31 +1482,6 @@ function ensureFrontierNode(graph, host, ts) {
1478
1482
  graph.addNode(id, node);
1479
1483
  return id;
1480
1484
  }
1481
- function upsertFrontierEdge(graph, type, source, target, ts) {
1482
- const id = (0, import_types3.frontierEdgeId)(source, target, type);
1483
- if (graph.hasEdge(id)) {
1484
- const existing = graph.getEdgeAttributes(id);
1485
- const updated = {
1486
- ...existing,
1487
- provenance: import_types3.Provenance.FRONTIER,
1488
- lastObserved: ts,
1489
- callCount: (existing.callCount ?? 0) + 1
1490
- };
1491
- graph.replaceEdgeAttributes(id, updated);
1492
- return;
1493
- }
1494
- const edge = {
1495
- id,
1496
- source,
1497
- target,
1498
- type,
1499
- provenance: import_types3.Provenance.FRONTIER,
1500
- confidence: 1,
1501
- lastObserved: ts,
1502
- callCount: 1
1503
- };
1504
- graph.addEdgeWithKey(id, source, target, edge);
1505
- }
1506
1485
  function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1507
1486
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1508
1487
  const id = makeObservedEdgeId(type, source, target);
@@ -1588,18 +1567,28 @@ async function appendErrorEvent(ctx, ev) {
1588
1567
  await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
1589
1568
  await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
1590
1569
  }
1570
+ function sanitizeAttributes(attrs) {
1571
+ const out = {};
1572
+ for (const [k, v] of Object.entries(attrs)) {
1573
+ if (typeof v === "bigint") out[k] = v.toString();
1574
+ else out[k] = v;
1575
+ }
1576
+ return out;
1577
+ }
1591
1578
  function buildErrorEventForReceiver(span) {
1592
1579
  if (span.statusCode !== 2) return null;
1593
1580
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
1581
+ const attrs = sanitizeAttributes(span.attributes);
1594
1582
  return {
1595
1583
  id: `${span.traceId}:${span.spanId}`,
1596
1584
  timestamp: ts,
1597
1585
  service: span.service,
1598
1586
  traceId: span.traceId,
1599
1587
  spanId: span.spanId,
1600
- errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1588
+ errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1601
1589
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1602
1590
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1591
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1603
1592
  affectedNode: (0, import_types3.serviceId)(span.service)
1604
1593
  };
1605
1594
  }
@@ -1650,11 +1639,16 @@ async function handleSpan(ctx, span) {
1650
1639
  affectedNode = targetId;
1651
1640
  resolvedViaAddress = true;
1652
1641
  } else if (!targetId) {
1653
- const frontierId2 = ensureFrontierNode(ctx.graph, host, ts);
1654
- if (ctx.graph.hasNode(sourceId)) {
1655
- upsertFrontierEdge(ctx.graph, import_types3.EdgeType.CALLS, sourceId, frontierId2, ts);
1656
- }
1657
- affectedNode = frontierId2;
1642
+ const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
1643
+ upsertObservedEdge(
1644
+ ctx.graph,
1645
+ import_types3.EdgeType.CALLS,
1646
+ sourceId,
1647
+ frontierNodeId,
1648
+ ts,
1649
+ isError
1650
+ );
1651
+ affectedNode = frontierNodeId;
1658
1652
  resolvedViaAddress = true;
1659
1653
  }
1660
1654
  }
@@ -1676,15 +1670,17 @@ async function handleSpan(ctx, span) {
1676
1670
  if (span.statusCode === 2) {
1677
1671
  stitchTrace(ctx.graph, sourceId, ts);
1678
1672
  if (ctx.writeErrorEventInline !== false) {
1673
+ const attrs = sanitizeAttributes(span.attributes);
1679
1674
  const ev = {
1680
1675
  id: `${span.traceId}:${span.spanId}`,
1681
1676
  timestamp: ts,
1682
1677
  service: span.service,
1683
1678
  traceId: span.traceId,
1684
1679
  spanId: span.spanId,
1685
- errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1680
+ errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1686
1681
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1687
1682
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1683
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1688
1684
  affectedNode
1689
1685
  };
1690
1686
  await appendErrorEvent(ctx, ev);
@@ -1740,8 +1736,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
1740
1736
  }
1741
1737
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1742
1738
  graph.dropEdge(oldEdgeId);
1743
- const promotedProvenance = edge.provenance === import_types3.Provenance.FRONTIER ? import_types3.Provenance.OBSERVED : edge.provenance;
1744
- 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);
1739
+ 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);
1745
1740
  if (graph.hasEdge(newId)) {
1746
1741
  const existing = graph.getEdgeAttributes(newId);
1747
1742
  const merged = {
@@ -1756,8 +1751,7 @@ function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1756
1751
  ...edge,
1757
1752
  id: newId,
1758
1753
  source: newSource,
1759
- target: newTarget,
1760
- provenance: promotedProvenance
1754
+ target: newTarget
1761
1755
  };
1762
1756
  graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
1763
1757
  }
@@ -3981,7 +3975,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3981
3975
  init_cjs_shims();
3982
3976
  var import_node_fs18 = require("fs");
3983
3977
  var import_node_path31 = __toESM(require("path"), 1);
3984
- var SCHEMA_VERSION = 2;
3978
+ var import_types19 = require("@neat.is/types");
3979
+ var SCHEMA_VERSION = 3;
3985
3980
  function migrateV1ToV2(payload) {
3986
3981
  const nodes = payload.graph.nodes;
3987
3982
  if (Array.isArray(nodes)) {
@@ -3993,6 +3988,25 @@ function migrateV1ToV2(payload) {
3993
3988
  }
3994
3989
  return { ...payload, schemaVersion: 2 };
3995
3990
  }
3991
+ function migrateV2ToV3(payload) {
3992
+ const edges = payload.graph.edges;
3993
+ if (Array.isArray(edges)) {
3994
+ for (const edge of edges) {
3995
+ const attrs = edge.attributes;
3996
+ if (!attrs || attrs.provenance !== "FRONTIER") continue;
3997
+ attrs.provenance = import_types19.Provenance.OBSERVED;
3998
+ const type = typeof attrs.type === "string" ? attrs.type : void 0;
3999
+ const source = typeof attrs.source === "string" ? attrs.source : void 0;
4000
+ const target = typeof attrs.target === "string" ? attrs.target : void 0;
4001
+ if (type && source && target) {
4002
+ const newId = (0, import_types19.observedEdgeId)(source, target, type);
4003
+ attrs.id = newId;
4004
+ if (edge.key) edge.key = newId;
4005
+ }
4006
+ }
4007
+ }
4008
+ return { ...payload, schemaVersion: 3 };
4009
+ }
3996
4010
  async function ensureDir(filePath) {
3997
4011
  await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
3998
4012
  }
@@ -4019,6 +4033,9 @@ async function loadGraphFromDisk(graph, outPath) {
4019
4033
  if (payload.schemaVersion === 1) {
4020
4034
  payload = migrateV1ToV2(payload);
4021
4035
  }
4036
+ if (payload.schemaVersion === 2) {
4037
+ payload = migrateV2ToV3(payload);
4038
+ }
4022
4039
  if (payload.schemaVersion !== SCHEMA_VERSION) {
4023
4040
  throw new Error(
4024
4041
  `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
@@ -4117,11 +4134,11 @@ var Projects = class {
4117
4134
  init_cjs_shims();
4118
4135
  var import_fastify = __toESM(require("fastify"), 1);
4119
4136
  var import_cors = __toESM(require("@fastify/cors"), 1);
4120
- var import_types21 = require("@neat.is/types");
4137
+ var import_types22 = require("@neat.is/types");
4121
4138
 
4122
4139
  // src/divergences.ts
4123
4140
  init_cjs_shims();
4124
- var import_types19 = require("@neat.is/types");
4141
+ var import_types20 = require("@neat.is/types");
4125
4142
  function bucketKey(source, target, type) {
4126
4143
  return `${type}|${source}|${target}`;
4127
4144
  }
@@ -4129,25 +4146,22 @@ function bucketEdges(graph) {
4129
4146
  const buckets = /* @__PURE__ */ new Map();
4130
4147
  graph.forEachEdge((id, attrs) => {
4131
4148
  const e = attrs;
4132
- const parsed = (0, import_types19.parseEdgeId)(id);
4149
+ const parsed = (0, import_types20.parseEdgeId)(id);
4133
4150
  const provenance = parsed?.provenance ?? e.provenance;
4134
4151
  const key = bucketKey(e.source, e.target, e.type);
4135
4152
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4136
4153
  switch (provenance) {
4137
- case import_types19.Provenance.EXTRACTED:
4154
+ case import_types20.Provenance.EXTRACTED:
4138
4155
  cur.extracted = e;
4139
4156
  break;
4140
- case import_types19.Provenance.OBSERVED:
4157
+ case import_types20.Provenance.OBSERVED:
4141
4158
  cur.observed = e;
4142
4159
  break;
4143
- case import_types19.Provenance.INFERRED:
4160
+ case import_types20.Provenance.INFERRED:
4144
4161
  cur.inferred = e;
4145
4162
  break;
4146
- case import_types19.Provenance.FRONTIER:
4147
- cur.frontier = e;
4148
- break;
4149
4163
  default:
4150
- if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4164
+ if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4151
4165
  }
4152
4166
  buckets.set(key, cur);
4153
4167
  });
@@ -4156,7 +4170,7 @@ function bucketEdges(graph) {
4156
4170
  function nodeIsFrontier(graph, nodeId) {
4157
4171
  if (!graph.hasNode(nodeId)) return false;
4158
4172
  const attrs = graph.getNodeAttributes(nodeId);
4159
- return attrs.type === import_types19.NodeType.FrontierNode;
4173
+ return attrs.type === import_types20.NodeType.FrontierNode;
4160
4174
  }
4161
4175
  function clampConfidence(n) {
4162
4176
  if (!Number.isFinite(n)) return 0;
@@ -4217,7 +4231,7 @@ function declaredHostFor(svc) {
4217
4231
  function hasExtractedConfiguredBy(graph, svcId) {
4218
4232
  for (const edgeId of graph.outboundEdges(svcId)) {
4219
4233
  const e = graph.getEdgeAttributes(edgeId);
4220
- if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4234
+ if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4221
4235
  return true;
4222
4236
  }
4223
4237
  }
@@ -4230,10 +4244,10 @@ function detectHostMismatch(graph, svcId, svc) {
4230
4244
  const out = [];
4231
4245
  for (const edgeId of graph.outboundEdges(svcId)) {
4232
4246
  const edge = graph.getEdgeAttributes(edgeId);
4233
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4234
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4247
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4248
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4235
4249
  const target = graph.getNodeAttributes(edge.target);
4236
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4250
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4237
4251
  const observedHost = target.host?.trim();
4238
4252
  if (!observedHost) continue;
4239
4253
  if (observedHost === declaredHost) continue;
@@ -4255,10 +4269,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4255
4269
  const deps = svc.dependencies ?? {};
4256
4270
  for (const edgeId of graph.outboundEdges(svcId)) {
4257
4271
  const edge = graph.getEdgeAttributes(edgeId);
4258
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4259
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4272
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4273
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4260
4274
  const target = graph.getNodeAttributes(edge.target);
4261
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4275
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4262
4276
  for (const pair of compatPairs()) {
4263
4277
  if (pair.engine !== target.engine) continue;
4264
4278
  const declared = deps[pair.driver];
@@ -4319,7 +4333,7 @@ function computeDivergences(graph, opts = {}) {
4319
4333
  }
4320
4334
  graph.forEachNode((nodeId, attrs) => {
4321
4335
  const n = attrs;
4322
- if (n.type !== import_types19.NodeType.ServiceNode) return;
4336
+ if (n.type !== import_types20.NodeType.ServiceNode) return;
4323
4337
  const svc = n;
4324
4338
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4325
4339
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4352,7 +4366,7 @@ function computeDivergences(graph, opts = {}) {
4352
4366
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4353
4367
  return a.target.localeCompare(b.target);
4354
4368
  });
4355
- return import_types19.DivergenceResultSchema.parse({
4369
+ return import_types20.DivergenceResultSchema.parse({
4356
4370
  divergences: filtered,
4357
4371
  totalAffected: filtered.length,
4358
4372
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4441,7 +4455,7 @@ init_cjs_shims();
4441
4455
  var import_node_fs20 = require("fs");
4442
4456
  var import_node_os2 = __toESM(require("os"), 1);
4443
4457
  var import_node_path33 = __toESM(require("path"), 1);
4444
- var import_types20 = require("@neat.is/types");
4458
+ var import_types21 = require("@neat.is/types");
4445
4459
  var LOCK_TIMEOUT_MS = 5e3;
4446
4460
  var LOCK_RETRY_MS = 50;
4447
4461
  function neatHome() {
@@ -4512,10 +4526,10 @@ async function readRegistry() {
4512
4526
  throw err;
4513
4527
  }
4514
4528
  const parsed = JSON.parse(raw);
4515
- return import_types20.RegistryFileSchema.parse(parsed);
4529
+ return import_types21.RegistryFileSchema.parse(parsed);
4516
4530
  }
4517
4531
  async function writeRegistry(reg) {
4518
- const validated = import_types20.RegistryFileSchema.parse(reg);
4532
+ const validated = import_types21.RegistryFileSchema.parse(reg);
4519
4533
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4520
4534
  }
4521
4535
  async function getProject(name) {
@@ -4725,11 +4739,11 @@ function registerRoutes(scope, ctx) {
4725
4739
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4726
4740
  const parsed = [];
4727
4741
  for (const c of candidates) {
4728
- const r = import_types21.DivergenceTypeSchema.safeParse(c);
4742
+ const r = import_types22.DivergenceTypeSchema.safeParse(c);
4729
4743
  if (!r.success) {
4730
4744
  return reply.code(400).send({
4731
4745
  error: `unknown divergence type "${c}"`,
4732
- allowed: import_types21.DivergenceTypeSchema.options
4746
+ allowed: import_types22.DivergenceTypeSchema.options
4733
4747
  });
4734
4748
  }
4735
4749
  parsed.push(r.data);
@@ -4913,7 +4927,7 @@ function registerRoutes(scope, ctx) {
4913
4927
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4914
4928
  let violations = await log.readAll();
4915
4929
  if (req2.query.severity) {
4916
- const sev = import_types21.PolicySeveritySchema.safeParse(req2.query.severity);
4930
+ const sev = import_types22.PolicySeveritySchema.safeParse(req2.query.severity);
4917
4931
  if (!sev.success) {
4918
4932
  return reply.code(400).send({
4919
4933
  error: "invalid severity",
@@ -4930,7 +4944,7 @@ function registerRoutes(scope, ctx) {
4930
4944
  scope.post("/policies/check", async (req2, reply) => {
4931
4945
  const proj = resolveProject(registry, req2, reply);
4932
4946
  if (!proj) return;
4933
- const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
4947
+ const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
4934
4948
  if (!parsed.success) {
4935
4949
  return reply.code(400).send({
4936
4950
  error: "invalid /policies/check body",