@neat.is/core 0.2.9 → 0.2.10

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.cjs CHANGED
@@ -3869,7 +3869,244 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3869
3869
  init_cjs_shims();
3870
3870
  var import_fastify = __toESM(require("fastify"), 1);
3871
3871
  var import_cors = __toESM(require("@fastify/cors"), 1);
3872
- var import_types19 = require("@neat.is/types");
3872
+ var import_types20 = require("@neat.is/types");
3873
+
3874
+ // src/divergences.ts
3875
+ init_cjs_shims();
3876
+ var import_types18 = require("@neat.is/types");
3877
+ function bucketKey(source, target, type) {
3878
+ return `${type}|${source}|${target}`;
3879
+ }
3880
+ function bucketEdges(graph) {
3881
+ const buckets = /* @__PURE__ */ new Map();
3882
+ graph.forEachEdge((id, attrs) => {
3883
+ const e = attrs;
3884
+ const parsed = (0, import_types18.parseEdgeId)(id);
3885
+ const provenance = parsed?.provenance ?? e.provenance;
3886
+ const key = bucketKey(e.source, e.target, e.type);
3887
+ const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3888
+ switch (provenance) {
3889
+ case import_types18.Provenance.EXTRACTED:
3890
+ cur.extracted = e;
3891
+ break;
3892
+ case import_types18.Provenance.OBSERVED:
3893
+ cur.observed = e;
3894
+ break;
3895
+ case import_types18.Provenance.INFERRED:
3896
+ cur.inferred = e;
3897
+ break;
3898
+ case import_types18.Provenance.FRONTIER:
3899
+ cur.frontier = e;
3900
+ break;
3901
+ default:
3902
+ if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
3903
+ }
3904
+ buckets.set(key, cur);
3905
+ });
3906
+ return buckets;
3907
+ }
3908
+ function nodeIsFrontier(graph, nodeId) {
3909
+ if (!graph.hasNode(nodeId)) return false;
3910
+ const attrs = graph.getNodeAttributes(nodeId);
3911
+ return attrs.type === import_types18.NodeType.FrontierNode;
3912
+ }
3913
+ function hasAnyObservedFromSource(graph, sourceId) {
3914
+ if (!graph.hasNode(sourceId)) return false;
3915
+ for (const edgeId of graph.outboundEdges(sourceId)) {
3916
+ const e = graph.getEdgeAttributes(edgeId);
3917
+ if (e.provenance === import_types18.Provenance.OBSERVED) return true;
3918
+ }
3919
+ return false;
3920
+ }
3921
+ function clampConfidence(n) {
3922
+ if (!Number.isFinite(n)) return 0;
3923
+ return Math.max(0, Math.min(1, n));
3924
+ }
3925
+ function reasonForMissingObserved(source, target, type) {
3926
+ return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
3927
+ }
3928
+ function reasonForMissingExtracted(source, target, type) {
3929
+ return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
3930
+ }
3931
+ var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
3932
+ var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
3933
+ var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
3934
+ function detectMissingDivergences(graph, bucket) {
3935
+ const out = [];
3936
+ if (bucket.extracted && !bucket.observed) {
3937
+ if (!nodeIsFrontier(graph, bucket.target)) {
3938
+ const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
3939
+ out.push({
3940
+ type: "missing-observed",
3941
+ source: bucket.source,
3942
+ target: bucket.target,
3943
+ edgeType: bucket.type,
3944
+ extracted: bucket.extracted,
3945
+ confidence: sourceHasTraffic ? 1 : 0.5,
3946
+ reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
3947
+ recommendation: RECOMMENDATION_MISSING_OBSERVED
3948
+ });
3949
+ }
3950
+ }
3951
+ if (bucket.observed && !bucket.extracted) {
3952
+ const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
3953
+ out.push({
3954
+ type: "missing-extracted",
3955
+ source: bucket.source,
3956
+ target: bucket.target,
3957
+ edgeType: bucket.type,
3958
+ observed: bucket.observed,
3959
+ confidence: cascaded,
3960
+ reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
3961
+ recommendation: RECOMMENDATION_MISSING_EXTRACTED
3962
+ });
3963
+ }
3964
+ return out;
3965
+ }
3966
+ function declaredHostFor(svc) {
3967
+ const raw = svc.dbConnectionTarget?.trim();
3968
+ if (!raw) return null;
3969
+ const colon = raw.lastIndexOf(":");
3970
+ if (colon === -1) return raw;
3971
+ const port = raw.slice(colon + 1);
3972
+ if (/^\d+$/.test(port)) return raw.slice(0, colon);
3973
+ return raw;
3974
+ }
3975
+ function hasExtractedConfiguredBy(graph, svcId) {
3976
+ for (const edgeId of graph.outboundEdges(svcId)) {
3977
+ const e = graph.getEdgeAttributes(edgeId);
3978
+ if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
3979
+ return true;
3980
+ }
3981
+ }
3982
+ return false;
3983
+ }
3984
+ function detectHostMismatch(graph, svcId, svc) {
3985
+ const declaredHost = declaredHostFor(svc);
3986
+ if (!declaredHost) return [];
3987
+ if (!hasExtractedConfiguredBy(graph, svcId)) return [];
3988
+ const out = [];
3989
+ for (const edgeId of graph.outboundEdges(svcId)) {
3990
+ const edge = graph.getEdgeAttributes(edgeId);
3991
+ if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
3992
+ if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
3993
+ const target = graph.getNodeAttributes(edge.target);
3994
+ if (target.type !== import_types18.NodeType.DatabaseNode) continue;
3995
+ const observedHost = target.host?.trim();
3996
+ if (!observedHost) continue;
3997
+ if (observedHost === declaredHost) continue;
3998
+ out.push({
3999
+ type: "host-mismatch",
4000
+ source: svcId,
4001
+ target: edge.target,
4002
+ extractedHost: declaredHost,
4003
+ observedHost,
4004
+ confidence: clampConfidence(confidenceForEdge(edge)),
4005
+ reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
4006
+ recommendation: RECOMMENDATION_HOST_MISMATCH
4007
+ });
4008
+ }
4009
+ return out;
4010
+ }
4011
+ function detectCompatDivergences(graph, svcId, svc) {
4012
+ const out = [];
4013
+ const deps = svc.dependencies ?? {};
4014
+ for (const edgeId of graph.outboundEdges(svcId)) {
4015
+ const edge = graph.getEdgeAttributes(edgeId);
4016
+ if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4017
+ if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4018
+ const target = graph.getNodeAttributes(edge.target);
4019
+ if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4020
+ for (const pair of compatPairs()) {
4021
+ if (pair.engine !== target.engine) continue;
4022
+ const declared = deps[pair.driver];
4023
+ if (!declared) continue;
4024
+ const result = checkCompatibility(
4025
+ pair.driver,
4026
+ declared,
4027
+ target.engine,
4028
+ target.engineVersion
4029
+ );
4030
+ if (!result.compatible && result.reason) {
4031
+ out.push({
4032
+ type: "version-mismatch",
4033
+ source: svcId,
4034
+ target: edge.target,
4035
+ extractedVersion: declared,
4036
+ observedVersion: target.engineVersion,
4037
+ compatibility: "incompatible",
4038
+ confidence: 1,
4039
+ reason: result.reason,
4040
+ recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
4041
+ });
4042
+ }
4043
+ }
4044
+ for (const rule of deprecatedApis()) {
4045
+ const declared = deps[rule.package];
4046
+ if (!declared) continue;
4047
+ const result = checkDeprecatedApi(rule, declared);
4048
+ if (!result.compatible && result.reason) {
4049
+ const ruleRef = {
4050
+ kind: rule.kind ?? "deprecated-api",
4051
+ reason: result.reason,
4052
+ package: rule.package
4053
+ };
4054
+ out.push({
4055
+ type: "compat-violation",
4056
+ source: svcId,
4057
+ target: edge.target,
4058
+ rule: ruleRef,
4059
+ observed: edge,
4060
+ confidence: 1,
4061
+ reason: result.reason,
4062
+ recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
4063
+ });
4064
+ }
4065
+ }
4066
+ }
4067
+ return out;
4068
+ }
4069
+ function involvesNode(d, nodeId) {
4070
+ return d.source === nodeId || d.target === nodeId;
4071
+ }
4072
+ function computeDivergences(graph, opts = {}) {
4073
+ const all = [];
4074
+ const buckets = bucketEdges(graph);
4075
+ for (const bucket of buckets.values()) {
4076
+ for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
4077
+ }
4078
+ graph.forEachNode((nodeId, attrs) => {
4079
+ const n = attrs;
4080
+ if (n.type !== import_types18.NodeType.ServiceNode) return;
4081
+ const svc = n;
4082
+ for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4083
+ for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
4084
+ });
4085
+ let filtered = all;
4086
+ if (opts.type) {
4087
+ const allowed = opts.type;
4088
+ filtered = filtered.filter((d) => allowed.has(d.type));
4089
+ }
4090
+ if (opts.minConfidence !== void 0) {
4091
+ const threshold = opts.minConfidence;
4092
+ filtered = filtered.filter((d) => d.confidence >= threshold);
4093
+ }
4094
+ if (opts.node) {
4095
+ const target = opts.node;
4096
+ filtered = filtered.filter((d) => involvesNode(d, target));
4097
+ }
4098
+ filtered.sort((a, b) => {
4099
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
4100
+ if (a.type !== b.type) return a.type.localeCompare(b.type);
4101
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
4102
+ return a.target.localeCompare(b.target);
4103
+ });
4104
+ return import_types18.DivergenceResultSchema.parse({
4105
+ divergences: filtered,
4106
+ totalAffected: filtered.length,
4107
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
4108
+ });
4109
+ }
3873
4110
 
3874
4111
  // src/diff.ts
3875
4112
  init_cjs_shims();
@@ -4005,7 +4242,7 @@ init_cjs_shims();
4005
4242
  var import_node_fs18 = require("fs");
4006
4243
  var import_node_os2 = __toESM(require("os"), 1);
4007
4244
  var import_node_path30 = __toESM(require("path"), 1);
4008
- var import_types18 = require("@neat.is/types");
4245
+ var import_types19 = require("@neat.is/types");
4009
4246
  var LOCK_TIMEOUT_MS = 5e3;
4010
4247
  var LOCK_RETRY_MS = 50;
4011
4248
  function neatHome() {
@@ -4084,10 +4321,10 @@ async function readRegistry() {
4084
4321
  throw err;
4085
4322
  }
4086
4323
  const parsed = JSON.parse(raw);
4087
- return import_types18.RegistryFileSchema.parse(parsed);
4324
+ return import_types19.RegistryFileSchema.parse(parsed);
4088
4325
  }
4089
4326
  async function writeRegistry(reg) {
4090
- const validated = import_types18.RegistryFileSchema.parse(reg);
4327
+ const validated = import_types19.RegistryFileSchema.parse(reg);
4091
4328
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4092
4329
  }
4093
4330
  var ProjectNameCollisionError = class extends Error {
@@ -4333,6 +4570,41 @@ function registerRoutes(scope, ctx) {
4333
4570
  }
4334
4571
  return getTransitiveDependencies(proj.graph, id, depth);
4335
4572
  });
4573
+ scope.get("/graph/divergences", async (req, reply) => {
4574
+ const proj = resolveProject(registry, req, reply);
4575
+ if (!proj) return;
4576
+ let typeFilter;
4577
+ if (req.query.type) {
4578
+ const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4579
+ const parsed = [];
4580
+ for (const c of candidates) {
4581
+ const r = import_types20.DivergenceTypeSchema.safeParse(c);
4582
+ if (!r.success) {
4583
+ return reply.code(400).send({
4584
+ error: `unknown divergence type "${c}"`,
4585
+ allowed: import_types20.DivergenceTypeSchema.options
4586
+ });
4587
+ }
4588
+ parsed.push(r.data);
4589
+ }
4590
+ typeFilter = new Set(parsed);
4591
+ }
4592
+ let minConfidence;
4593
+ if (req.query.minConfidence !== void 0) {
4594
+ const n = Number(req.query.minConfidence);
4595
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
4596
+ return reply.code(400).send({
4597
+ error: "minConfidence must be a number in [0, 1]"
4598
+ });
4599
+ }
4600
+ minConfidence = n;
4601
+ }
4602
+ return computeDivergences(proj.graph, {
4603
+ ...typeFilter ? { type: typeFilter } : {},
4604
+ ...minConfidence !== void 0 ? { minConfidence } : {},
4605
+ ...req.query.node ? { node: req.query.node } : {}
4606
+ });
4607
+ });
4336
4608
  scope.get("/incidents", async (req, reply) => {
4337
4609
  const proj = resolveProject(registry, req, reply);
4338
4610
  if (!proj) return;
@@ -4486,7 +4758,7 @@ function registerRoutes(scope, ctx) {
4486
4758
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4487
4759
  let violations = await log.readAll();
4488
4760
  if (req.query.severity) {
4489
- const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4761
+ const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
4490
4762
  if (!sev.success) {
4491
4763
  return reply.code(400).send({
4492
4764
  error: "invalid severity",
@@ -4503,7 +4775,7 @@ function registerRoutes(scope, ctx) {
4503
4775
  scope.post("/policies/check", async (req, reply) => {
4504
4776
  const proj = resolveProject(registry, req, reply);
4505
4777
  if (!proj) return;
4506
- const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4778
+ const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4507
4779
  if (!parsed.success) {
4508
4780
  return reply.code(400).send({
4509
4781
  error: "invalid /policies/check body",