@neat.is/core 0.3.7 → 0.4.0

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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ mountBearerAuth
3
+ } from "./chunk-4V23KYOP.js";
4
+
1
5
  // src/graph.ts
2
6
  import GraphDefault from "graphology";
3
7
  var MultiDirectedGraph = GraphDefault.MultiDirectedGraph;
@@ -1100,52 +1104,64 @@ function cacheSpanService(span, now) {
1100
1104
  if (!span.traceId || !span.spanId) return;
1101
1105
  const key = parentSpanKey(span.traceId, span.spanId);
1102
1106
  parentSpanCache.delete(key);
1103
- parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
1107
+ parentSpanCache.set(key, {
1108
+ service: span.service,
1109
+ env: span.env ?? "unknown",
1110
+ expiresAt: now + PARENT_SPAN_CACHE_TTL_MS
1111
+ });
1104
1112
  while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
1105
1113
  const oldest = parentSpanCache.keys().next().value;
1106
1114
  if (!oldest) break;
1107
1115
  parentSpanCache.delete(oldest);
1108
1116
  }
1109
1117
  }
1110
- function lookupParentSpanService(traceId, parentSpanId, now) {
1118
+ function lookupParentSpan(traceId, parentSpanId, now) {
1111
1119
  const entry = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
1112
1120
  if (!entry) return null;
1113
1121
  if (entry.expiresAt <= now) {
1114
1122
  parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
1115
1123
  return null;
1116
1124
  }
1117
- return entry.service;
1125
+ return { service: entry.service, env: entry.env };
1118
1126
  }
1119
- function resolveServiceId(graph, host) {
1120
- const direct = serviceId(host);
1121
- if (graph.hasNode(direct)) return direct;
1122
- let found = null;
1127
+ function resolveServiceId(graph, host, env) {
1128
+ const envTagged = serviceId(host, env);
1129
+ if (graph.hasNode(envTagged)) return envTagged;
1130
+ const envLess = serviceId(host);
1131
+ if (envLess !== envTagged && graph.hasNode(envLess)) return envLess;
1132
+ let sameEnv = null;
1133
+ let envLessMatch = null;
1134
+ let anyMatch = null;
1123
1135
  graph.forEachNode((id, attrs) => {
1124
- if (found) return;
1136
+ if (sameEnv) return;
1125
1137
  const a = attrs;
1126
1138
  if (a.type !== NodeType3.ServiceNode) return;
1127
- if (a.name === host) {
1128
- found = id;
1139
+ const matchesByName = a.name === host;
1140
+ const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
1141
+ if (!matchesByName && !matchesByAlias) return;
1142
+ const nodeEnv = a.env ?? "unknown";
1143
+ if (nodeEnv === env) {
1144
+ sameEnv = id;
1129
1145
  return;
1130
1146
  }
1131
- if (a.aliases && a.aliases.includes(host)) {
1132
- found = id;
1133
- }
1147
+ if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
1148
+ else if (!anyMatch) anyMatch = id;
1134
1149
  });
1135
- return found;
1150
+ return sameEnv ?? envLessMatch ?? anyMatch;
1136
1151
  }
1137
1152
  function frontierIdFor(host) {
1138
1153
  return frontierId(host);
1139
1154
  }
1140
- function ensureServiceNode(graph, serviceName) {
1141
- const id = serviceId(serviceName);
1155
+ function ensureServiceNode(graph, serviceName, env) {
1156
+ const id = serviceId(serviceName, env);
1142
1157
  if (graph.hasNode(id)) return id;
1143
1158
  const node = {
1144
1159
  id,
1145
1160
  type: NodeType3.ServiceNode,
1146
1161
  name: serviceName,
1147
1162
  language: "unknown",
1148
- discoveredVia: "otel"
1163
+ discoveredVia: "otel",
1164
+ ...env !== "unknown" ? { env } : {}
1149
1165
  };
1150
1166
  graph.addNode(id, node);
1151
1167
  return id;
@@ -1291,7 +1307,7 @@ function buildErrorEventForReceiver(span) {
1291
1307
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1292
1308
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1293
1309
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1294
- affectedNode: serviceId(span.service)
1310
+ affectedNode: serviceId(span.service, span.env)
1295
1311
  };
1296
1312
  }
1297
1313
  function makeErrorSpanWriter(errorsPath) {
@@ -1305,7 +1321,8 @@ function makeErrorSpanWriter(errorsPath) {
1305
1321
  async function handleSpan(ctx, span) {
1306
1322
  const ts = span.startTimeIso ?? nowIso(ctx);
1307
1323
  const nowMs = ctx.now ? ctx.now() : Date.now();
1308
- const sourceId = ensureServiceNode(ctx.graph, span.service);
1324
+ const env = span.env ?? "unknown";
1325
+ const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1309
1326
  const isError = span.statusCode === 2;
1310
1327
  cacheSpanService(span, nowMs);
1311
1328
  let affectedNode = sourceId;
@@ -1328,7 +1345,7 @@ async function handleSpan(ctx, span) {
1328
1345
  const host = pickAddress(span);
1329
1346
  let resolvedViaAddress = false;
1330
1347
  if (host && host !== span.service) {
1331
- const targetId = resolveServiceId(ctx.graph, host);
1348
+ const targetId = resolveServiceId(ctx.graph, host, env);
1332
1349
  if (targetId && targetId !== sourceId) {
1333
1350
  upsertObservedEdge(
1334
1351
  ctx.graph,
@@ -1355,9 +1372,9 @@ async function handleSpan(ctx, span) {
1355
1372
  }
1356
1373
  }
1357
1374
  if (!resolvedViaAddress && span.parentSpanId) {
1358
- const parentService = lookupParentSpanService(span.traceId, span.parentSpanId, nowMs);
1359
- if (parentService && parentService !== span.service) {
1360
- const parentId = ensureServiceNode(ctx.graph, parentService);
1375
+ const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
1376
+ if (parent && parent.service !== span.service) {
1377
+ const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1361
1378
  upsertObservedEdge(
1362
1379
  ctx.graph,
1363
1380
  EdgeType2.CALLS,
@@ -1553,6 +1570,28 @@ async function readErrorEvents(errorsPath) {
1553
1570
  throw err;
1554
1571
  }
1555
1572
  }
1573
+ function mergeSnapshot(graph, snapshot) {
1574
+ const exported = snapshot.graph;
1575
+ let nodesAdded = 0;
1576
+ let edgesAdded = 0;
1577
+ for (const node of exported.nodes ?? []) {
1578
+ if (graph.hasNode(node.key)) continue;
1579
+ if (!node.attributes) continue;
1580
+ graph.addNode(node.key, node.attributes);
1581
+ nodesAdded++;
1582
+ }
1583
+ for (const edge of exported.edges ?? []) {
1584
+ const attrs = edge.attributes;
1585
+ if (!attrs) continue;
1586
+ const id = edge.key ?? attrs.id;
1587
+ if (!id) continue;
1588
+ if (graph.hasEdge(id)) continue;
1589
+ if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
1590
+ graph.addEdgeWithKey(id, edge.source, edge.target, attrs);
1591
+ edgesAdded++;
1592
+ }
1593
+ return { nodesAdded, edgesAdded };
1594
+ }
1556
1595
 
1557
1596
  // src/extract/errors.ts
1558
1597
  import { promises as fs4 } from "fs";
@@ -1956,6 +1995,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1956
1995
  }
1957
1996
  return [...found];
1958
1997
  }
1998
+ function detectJsFramework(pkg) {
1999
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2000
+ if (deps["next"] !== void 0) return "next";
2001
+ if (deps["remix"] !== void 0) return "remix";
2002
+ for (const k of Object.keys(deps)) {
2003
+ if (k.startsWith("@remix-run/")) return "remix";
2004
+ }
2005
+ if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
2006
+ if (deps["nuxt"] !== void 0) return "nuxt";
2007
+ if (deps["astro"] !== void 0) return "astro";
2008
+ return void 0;
2009
+ }
1959
2010
  async function discoverNodeService(scanPath, dir) {
1960
2011
  const pkgPath = path8.join(dir, "package.json");
1961
2012
  if (!await exists(pkgPath)) return null;
@@ -1967,6 +2018,7 @@ async function discoverNodeService(scanPath, dir) {
1967
2018
  return null;
1968
2019
  }
1969
2020
  if (!pkg.name) return null;
2021
+ const framework = detectJsFramework(pkg);
1970
2022
  const node = {
1971
2023
  id: serviceId2(pkg.name),
1972
2024
  type: NodeType4.ServiceNode,
@@ -1975,7 +2027,8 @@ async function discoverNodeService(scanPath, dir) {
1975
2027
  version: pkg.version,
1976
2028
  dependencies: pkg.dependencies ?? {},
1977
2029
  repoPath: path8.relative(scanPath, dir),
1978
- ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2030
+ ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
2031
+ ...framework ? { framework } : {}
1979
2032
  };
1980
2033
  return { pkg, dir, node };
1981
2034
  }
@@ -3759,11 +3812,253 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3759
3812
  return result;
3760
3813
  }
3761
3814
 
3815
+ // src/divergences.ts
3816
+ import {
3817
+ DivergenceResultSchema,
3818
+ EdgeType as EdgeType9,
3819
+ NodeType as NodeType10,
3820
+ parseEdgeId,
3821
+ Provenance as Provenance9
3822
+ } from "@neat.is/types";
3823
+ function bucketKey(source, target, type) {
3824
+ return `${type}|${source}|${target}`;
3825
+ }
3826
+ function bucketEdges(graph) {
3827
+ const buckets = /* @__PURE__ */ new Map();
3828
+ graph.forEachEdge((id, attrs) => {
3829
+ const e = attrs;
3830
+ const parsed = parseEdgeId(id);
3831
+ const provenance = parsed?.provenance ?? e.provenance;
3832
+ const key = bucketKey(e.source, e.target, e.type);
3833
+ const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3834
+ switch (provenance) {
3835
+ case Provenance9.EXTRACTED:
3836
+ cur.extracted = e;
3837
+ break;
3838
+ case Provenance9.OBSERVED:
3839
+ cur.observed = e;
3840
+ break;
3841
+ case Provenance9.INFERRED:
3842
+ cur.inferred = e;
3843
+ break;
3844
+ default:
3845
+ if (e.provenance === Provenance9.STALE) cur.stale = e;
3846
+ }
3847
+ buckets.set(key, cur);
3848
+ });
3849
+ return buckets;
3850
+ }
3851
+ function nodeIsFrontier(graph, nodeId) {
3852
+ if (!graph.hasNode(nodeId)) return false;
3853
+ const attrs = graph.getNodeAttributes(nodeId);
3854
+ return attrs.type === NodeType10.FrontierNode;
3855
+ }
3856
+ function clampConfidence(n) {
3857
+ if (!Number.isFinite(n)) return 0;
3858
+ return Math.max(0, Math.min(1, n));
3859
+ }
3860
+ function reasonForMissingObserved(source, target, type) {
3861
+ return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
3862
+ }
3863
+ function reasonForMissingExtracted(source, target, type) {
3864
+ return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
3865
+ }
3866
+ var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
3867
+ 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.";
3868
+ var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
3869
+ function gradedConfidence(edge) {
3870
+ if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
3871
+ return clampConfidence(confidenceForEdge(edge));
3872
+ }
3873
+ function detectMissingDivergences(graph, bucket) {
3874
+ const out = [];
3875
+ if (bucket.extracted && !bucket.observed) {
3876
+ if (!nodeIsFrontier(graph, bucket.target)) {
3877
+ out.push({
3878
+ type: "missing-observed",
3879
+ source: bucket.source,
3880
+ target: bucket.target,
3881
+ edgeType: bucket.type,
3882
+ extracted: bucket.extracted,
3883
+ confidence: gradedConfidence(bucket.extracted),
3884
+ reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
3885
+ recommendation: RECOMMENDATION_MISSING_OBSERVED
3886
+ });
3887
+ }
3888
+ }
3889
+ if (bucket.observed && !bucket.extracted) {
3890
+ out.push({
3891
+ type: "missing-extracted",
3892
+ source: bucket.source,
3893
+ target: bucket.target,
3894
+ edgeType: bucket.type,
3895
+ observed: bucket.observed,
3896
+ confidence: gradedConfidence(bucket.observed),
3897
+ reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
3898
+ recommendation: RECOMMENDATION_MISSING_EXTRACTED
3899
+ });
3900
+ }
3901
+ return out;
3902
+ }
3903
+ function declaredHostFor(svc) {
3904
+ const raw = svc.dbConnectionTarget?.trim();
3905
+ if (!raw) return null;
3906
+ const colon = raw.lastIndexOf(":");
3907
+ if (colon === -1) return raw;
3908
+ const port = raw.slice(colon + 1);
3909
+ if (/^\d+$/.test(port)) return raw.slice(0, colon);
3910
+ return raw;
3911
+ }
3912
+ function hasExtractedConfiguredBy(graph, svcId) {
3913
+ for (const edgeId of graph.outboundEdges(svcId)) {
3914
+ const e = graph.getEdgeAttributes(edgeId);
3915
+ if (e.type === EdgeType9.CONFIGURED_BY && e.provenance === Provenance9.EXTRACTED) {
3916
+ return true;
3917
+ }
3918
+ }
3919
+ return false;
3920
+ }
3921
+ function detectHostMismatch(graph, svcId, svc) {
3922
+ const declaredHost = declaredHostFor(svc);
3923
+ if (!declaredHost) return [];
3924
+ if (!hasExtractedConfiguredBy(graph, svcId)) return [];
3925
+ const out = [];
3926
+ for (const edgeId of graph.outboundEdges(svcId)) {
3927
+ const edge = graph.getEdgeAttributes(edgeId);
3928
+ if (edge.type !== EdgeType9.CONNECTS_TO) continue;
3929
+ if (edge.provenance !== Provenance9.OBSERVED) continue;
3930
+ const target = graph.getNodeAttributes(edge.target);
3931
+ if (target.type !== NodeType10.DatabaseNode) continue;
3932
+ const observedHost = target.host?.trim();
3933
+ if (!observedHost) continue;
3934
+ if (observedHost === declaredHost) continue;
3935
+ out.push({
3936
+ type: "host-mismatch",
3937
+ source: svcId,
3938
+ target: edge.target,
3939
+ extractedHost: declaredHost,
3940
+ observedHost,
3941
+ confidence: clampConfidence(confidenceForEdge(edge)),
3942
+ reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
3943
+ recommendation: RECOMMENDATION_HOST_MISMATCH
3944
+ });
3945
+ }
3946
+ return out;
3947
+ }
3948
+ function detectCompatDivergences(graph, svcId, svc) {
3949
+ const out = [];
3950
+ const deps = svc.dependencies ?? {};
3951
+ for (const edgeId of graph.outboundEdges(svcId)) {
3952
+ const edge = graph.getEdgeAttributes(edgeId);
3953
+ if (edge.type !== EdgeType9.CONNECTS_TO) continue;
3954
+ if (edge.provenance !== Provenance9.OBSERVED) continue;
3955
+ const target = graph.getNodeAttributes(edge.target);
3956
+ if (target.type !== NodeType10.DatabaseNode) continue;
3957
+ for (const pair of compatPairs()) {
3958
+ if (pair.engine !== target.engine) continue;
3959
+ const declared = deps[pair.driver];
3960
+ if (!declared) continue;
3961
+ const result = checkCompatibility(
3962
+ pair.driver,
3963
+ declared,
3964
+ target.engine,
3965
+ target.engineVersion
3966
+ );
3967
+ if (!result.compatible && result.reason) {
3968
+ out.push({
3969
+ type: "version-mismatch",
3970
+ source: svcId,
3971
+ target: edge.target,
3972
+ extractedVersion: declared,
3973
+ observedVersion: target.engineVersion,
3974
+ compatibility: "incompatible",
3975
+ confidence: 1,
3976
+ reason: result.reason,
3977
+ recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
3978
+ });
3979
+ }
3980
+ }
3981
+ for (const rule of deprecatedApis()) {
3982
+ const declared = deps[rule.package];
3983
+ if (!declared) continue;
3984
+ const result = checkDeprecatedApi(rule, declared);
3985
+ if (!result.compatible && result.reason) {
3986
+ const ruleRef = {
3987
+ kind: rule.kind ?? "deprecated-api",
3988
+ reason: result.reason,
3989
+ package: rule.package
3990
+ };
3991
+ out.push({
3992
+ type: "compat-violation",
3993
+ source: svcId,
3994
+ target: edge.target,
3995
+ rule: ruleRef,
3996
+ observed: edge,
3997
+ confidence: 1,
3998
+ reason: result.reason,
3999
+ recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
4000
+ });
4001
+ }
4002
+ }
4003
+ }
4004
+ return out;
4005
+ }
4006
+ function involvesNode(d, nodeId) {
4007
+ return d.source === nodeId || d.target === nodeId;
4008
+ }
4009
+ function computeDivergences(graph, opts = {}) {
4010
+ const all = [];
4011
+ const buckets = bucketEdges(graph);
4012
+ for (const bucket of buckets.values()) {
4013
+ for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
4014
+ }
4015
+ graph.forEachNode((nodeId, attrs) => {
4016
+ const n = attrs;
4017
+ if (n.type !== NodeType10.ServiceNode) return;
4018
+ const svc = n;
4019
+ for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4020
+ for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
4021
+ });
4022
+ let filtered = all;
4023
+ if (opts.type) {
4024
+ const allowed = opts.type;
4025
+ filtered = filtered.filter((d) => allowed.has(d.type));
4026
+ }
4027
+ if (opts.minConfidence !== void 0) {
4028
+ const threshold = opts.minConfidence;
4029
+ filtered = filtered.filter((d) => d.confidence >= threshold);
4030
+ }
4031
+ if (opts.node) {
4032
+ const target = opts.node;
4033
+ filtered = filtered.filter((d) => involvesNode(d, target));
4034
+ }
4035
+ const TYPE_LEADERSHIP = {
4036
+ "missing-extracted": 0,
4037
+ "missing-observed": 1,
4038
+ "version-mismatch": 2,
4039
+ "host-mismatch": 3,
4040
+ "compat-violation": 4
4041
+ };
4042
+ filtered.sort((a, b) => {
4043
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
4044
+ const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
4045
+ if (lead !== 0) return lead;
4046
+ if (a.type !== b.type) return a.type.localeCompare(b.type);
4047
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
4048
+ return a.target.localeCompare(b.target);
4049
+ });
4050
+ return DivergenceResultSchema.parse({
4051
+ divergences: filtered,
4052
+ totalAffected: filtered.length,
4053
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
4054
+ });
4055
+ }
4056
+
3762
4057
  // src/persist.ts
3763
4058
  import { promises as fs17 } from "fs";
3764
4059
  import path31 from "path";
3765
- import { Provenance as Provenance9, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
3766
- var SCHEMA_VERSION = 3;
4060
+ import { Provenance as Provenance10, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4061
+ var SCHEMA_VERSION = 4;
3767
4062
  function migrateV1ToV2(payload) {
3768
4063
  const nodes = payload.graph.nodes;
3769
4064
  if (Array.isArray(nodes)) {
@@ -3775,13 +4070,16 @@ function migrateV1ToV2(payload) {
3775
4070
  }
3776
4071
  return { ...payload, schemaVersion: 2 };
3777
4072
  }
4073
+ function migrateV3ToV4(payload) {
4074
+ return { ...payload, schemaVersion: 4 };
4075
+ }
3778
4076
  function migrateV2ToV3(payload) {
3779
4077
  const edges = payload.graph.edges;
3780
4078
  if (Array.isArray(edges)) {
3781
4079
  for (const edge of edges) {
3782
4080
  const attrs = edge.attributes;
3783
4081
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
3784
- attrs.provenance = Provenance9.OBSERVED;
4082
+ attrs.provenance = Provenance10.OBSERVED;
3785
4083
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
3786
4084
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
3787
4085
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -3823,6 +4121,9 @@ async function loadGraphFromDisk(graph, outPath) {
3823
4121
  if (payload.schemaVersion === 2) {
3824
4122
  payload = migrateV2ToV3(payload);
3825
4123
  }
4124
+ if (payload.schemaVersion === 3) {
4125
+ payload = migrateV3ToV4(payload);
4126
+ }
3826
4127
  if (payload.schemaVersion !== SCHEMA_VERSION) {
3827
4128
  throw new Error(
3828
4129
  `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
@@ -4170,248 +4471,6 @@ import Fastify from "fastify";
4170
4471
  import cors from "@fastify/cors";
4171
4472
  import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
4172
4473
 
4173
- // src/divergences.ts
4174
- import {
4175
- DivergenceResultSchema,
4176
- EdgeType as EdgeType9,
4177
- NodeType as NodeType10,
4178
- parseEdgeId,
4179
- Provenance as Provenance10
4180
- } from "@neat.is/types";
4181
- function bucketKey(source, target, type) {
4182
- return `${type}|${source}|${target}`;
4183
- }
4184
- function bucketEdges(graph) {
4185
- const buckets = /* @__PURE__ */ new Map();
4186
- graph.forEachEdge((id, attrs) => {
4187
- const e = attrs;
4188
- const parsed = parseEdgeId(id);
4189
- const provenance = parsed?.provenance ?? e.provenance;
4190
- const key = bucketKey(e.source, e.target, e.type);
4191
- const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4192
- switch (provenance) {
4193
- case Provenance10.EXTRACTED:
4194
- cur.extracted = e;
4195
- break;
4196
- case Provenance10.OBSERVED:
4197
- cur.observed = e;
4198
- break;
4199
- case Provenance10.INFERRED:
4200
- cur.inferred = e;
4201
- break;
4202
- default:
4203
- if (e.provenance === Provenance10.STALE) cur.stale = e;
4204
- }
4205
- buckets.set(key, cur);
4206
- });
4207
- return buckets;
4208
- }
4209
- function nodeIsFrontier(graph, nodeId) {
4210
- if (!graph.hasNode(nodeId)) return false;
4211
- const attrs = graph.getNodeAttributes(nodeId);
4212
- return attrs.type === NodeType10.FrontierNode;
4213
- }
4214
- function clampConfidence(n) {
4215
- if (!Number.isFinite(n)) return 0;
4216
- return Math.max(0, Math.min(1, n));
4217
- }
4218
- function reasonForMissingObserved(source, target, type) {
4219
- return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
4220
- }
4221
- function reasonForMissingExtracted(source, target, type) {
4222
- return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
4223
- }
4224
- var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
4225
- 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.";
4226
- var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
4227
- function gradedConfidence(edge) {
4228
- if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
4229
- return clampConfidence(confidenceForEdge(edge));
4230
- }
4231
- function detectMissingDivergences(graph, bucket) {
4232
- const out = [];
4233
- if (bucket.extracted && !bucket.observed) {
4234
- if (!nodeIsFrontier(graph, bucket.target)) {
4235
- out.push({
4236
- type: "missing-observed",
4237
- source: bucket.source,
4238
- target: bucket.target,
4239
- edgeType: bucket.type,
4240
- extracted: bucket.extracted,
4241
- confidence: gradedConfidence(bucket.extracted),
4242
- reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
4243
- recommendation: RECOMMENDATION_MISSING_OBSERVED
4244
- });
4245
- }
4246
- }
4247
- if (bucket.observed && !bucket.extracted) {
4248
- out.push({
4249
- type: "missing-extracted",
4250
- source: bucket.source,
4251
- target: bucket.target,
4252
- edgeType: bucket.type,
4253
- observed: bucket.observed,
4254
- confidence: gradedConfidence(bucket.observed),
4255
- reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
4256
- recommendation: RECOMMENDATION_MISSING_EXTRACTED
4257
- });
4258
- }
4259
- return out;
4260
- }
4261
- function declaredHostFor(svc) {
4262
- const raw = svc.dbConnectionTarget?.trim();
4263
- if (!raw) return null;
4264
- const colon = raw.lastIndexOf(":");
4265
- if (colon === -1) return raw;
4266
- const port = raw.slice(colon + 1);
4267
- if (/^\d+$/.test(port)) return raw.slice(0, colon);
4268
- return raw;
4269
- }
4270
- function hasExtractedConfiguredBy(graph, svcId) {
4271
- for (const edgeId of graph.outboundEdges(svcId)) {
4272
- const e = graph.getEdgeAttributes(edgeId);
4273
- if (e.type === EdgeType9.CONFIGURED_BY && e.provenance === Provenance10.EXTRACTED) {
4274
- return true;
4275
- }
4276
- }
4277
- return false;
4278
- }
4279
- function detectHostMismatch(graph, svcId, svc) {
4280
- const declaredHost = declaredHostFor(svc);
4281
- if (!declaredHost) return [];
4282
- if (!hasExtractedConfiguredBy(graph, svcId)) return [];
4283
- const out = [];
4284
- for (const edgeId of graph.outboundEdges(svcId)) {
4285
- const edge = graph.getEdgeAttributes(edgeId);
4286
- if (edge.type !== EdgeType9.CONNECTS_TO) continue;
4287
- if (edge.provenance !== Provenance10.OBSERVED) continue;
4288
- const target = graph.getNodeAttributes(edge.target);
4289
- if (target.type !== NodeType10.DatabaseNode) continue;
4290
- const observedHost = target.host?.trim();
4291
- if (!observedHost) continue;
4292
- if (observedHost === declaredHost) continue;
4293
- out.push({
4294
- type: "host-mismatch",
4295
- source: svcId,
4296
- target: edge.target,
4297
- extractedHost: declaredHost,
4298
- observedHost,
4299
- confidence: clampConfidence(confidenceForEdge(edge)),
4300
- reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
4301
- recommendation: RECOMMENDATION_HOST_MISMATCH
4302
- });
4303
- }
4304
- return out;
4305
- }
4306
- function detectCompatDivergences(graph, svcId, svc) {
4307
- const out = [];
4308
- const deps = svc.dependencies ?? {};
4309
- for (const edgeId of graph.outboundEdges(svcId)) {
4310
- const edge = graph.getEdgeAttributes(edgeId);
4311
- if (edge.type !== EdgeType9.CONNECTS_TO) continue;
4312
- if (edge.provenance !== Provenance10.OBSERVED) continue;
4313
- const target = graph.getNodeAttributes(edge.target);
4314
- if (target.type !== NodeType10.DatabaseNode) continue;
4315
- for (const pair of compatPairs()) {
4316
- if (pair.engine !== target.engine) continue;
4317
- const declared = deps[pair.driver];
4318
- if (!declared) continue;
4319
- const result = checkCompatibility(
4320
- pair.driver,
4321
- declared,
4322
- target.engine,
4323
- target.engineVersion
4324
- );
4325
- if (!result.compatible && result.reason) {
4326
- out.push({
4327
- type: "version-mismatch",
4328
- source: svcId,
4329
- target: edge.target,
4330
- extractedVersion: declared,
4331
- observedVersion: target.engineVersion,
4332
- compatibility: "incompatible",
4333
- confidence: 1,
4334
- reason: result.reason,
4335
- recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
4336
- });
4337
- }
4338
- }
4339
- for (const rule of deprecatedApis()) {
4340
- const declared = deps[rule.package];
4341
- if (!declared) continue;
4342
- const result = checkDeprecatedApi(rule, declared);
4343
- if (!result.compatible && result.reason) {
4344
- const ruleRef = {
4345
- kind: rule.kind ?? "deprecated-api",
4346
- reason: result.reason,
4347
- package: rule.package
4348
- };
4349
- out.push({
4350
- type: "compat-violation",
4351
- source: svcId,
4352
- target: edge.target,
4353
- rule: ruleRef,
4354
- observed: edge,
4355
- confidence: 1,
4356
- reason: result.reason,
4357
- recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
4358
- });
4359
- }
4360
- }
4361
- }
4362
- return out;
4363
- }
4364
- function involvesNode(d, nodeId) {
4365
- return d.source === nodeId || d.target === nodeId;
4366
- }
4367
- function computeDivergences(graph, opts = {}) {
4368
- const all = [];
4369
- const buckets = bucketEdges(graph);
4370
- for (const bucket of buckets.values()) {
4371
- for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
4372
- }
4373
- graph.forEachNode((nodeId, attrs) => {
4374
- const n = attrs;
4375
- if (n.type !== NodeType10.ServiceNode) return;
4376
- const svc = n;
4377
- for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4378
- for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
4379
- });
4380
- let filtered = all;
4381
- if (opts.type) {
4382
- const allowed = opts.type;
4383
- filtered = filtered.filter((d) => allowed.has(d.type));
4384
- }
4385
- if (opts.minConfidence !== void 0) {
4386
- const threshold = opts.minConfidence;
4387
- filtered = filtered.filter((d) => d.confidence >= threshold);
4388
- }
4389
- if (opts.node) {
4390
- const target = opts.node;
4391
- filtered = filtered.filter((d) => involvesNode(d, target));
4392
- }
4393
- const TYPE_LEADERSHIP = {
4394
- "missing-extracted": 0,
4395
- "missing-observed": 1,
4396
- "version-mismatch": 2,
4397
- "host-mismatch": 3,
4398
- "compat-violation": 4
4399
- };
4400
- filtered.sort((a, b) => {
4401
- if (b.confidence !== a.confidence) return b.confidence - a.confidence;
4402
- const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
4403
- if (lead !== 0) return lead;
4404
- if (a.type !== b.type) return a.type.localeCompare(b.type);
4405
- if (a.source !== b.source) return a.source.localeCompare(b.source);
4406
- return a.target.localeCompare(b.target);
4407
- });
4408
- return DivergenceResultSchema.parse({
4409
- divergences: filtered,
4410
- totalAffected: filtered.length,
4411
- computedAt: (/* @__PURE__ */ new Date()).toISOString()
4412
- });
4413
- }
4414
-
4415
4474
  // src/streaming.ts
4416
4475
  var SSE_HEARTBEAT_MS = 3e4;
4417
4476
  var SSE_BACKPRESSURE_CAP = 1e3;
@@ -4739,6 +4798,35 @@ function registerRoutes(scope, ctx) {
4739
4798
  }
4740
4799
  }
4741
4800
  );
4801
+ scope.post("/snapshot", async (req, reply) => {
4802
+ const proj = resolveProject(registry, req, reply);
4803
+ if (!proj) return;
4804
+ const body = req.body;
4805
+ if (!body || typeof body !== "object" || !body.snapshot) {
4806
+ return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
4807
+ }
4808
+ const snap = body.snapshot;
4809
+ if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
4810
+ return reply.code(400).send({
4811
+ error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
4812
+ });
4813
+ }
4814
+ try {
4815
+ const result = mergeSnapshot(proj.graph, snap);
4816
+ return {
4817
+ project: proj.name,
4818
+ nodesAdded: result.nodesAdded,
4819
+ edgesAdded: result.edgesAdded,
4820
+ nodeCount: proj.graph.order,
4821
+ edgeCount: proj.graph.size
4822
+ };
4823
+ } catch (err) {
4824
+ return reply.code(400).send({
4825
+ error: "snapshot merge failed",
4826
+ details: err.message
4827
+ });
4828
+ }
4829
+ });
4742
4830
  scope.post("/graph/scan", async (req, reply) => {
4743
4831
  const proj = resolveProject(registry, req, reply);
4744
4832
  if (!proj) return;
@@ -4832,6 +4920,15 @@ function registerRoutes(scope, ctx) {
4832
4920
  async function buildApi(opts) {
4833
4921
  const app = Fastify({ logger: false });
4834
4922
  await app.register(cors, { origin: true });
4923
+ mountBearerAuth(app, {
4924
+ token: opts.authToken,
4925
+ trustProxy: opts.trustProxy,
4926
+ publicRead: opts.publicRead
4927
+ });
4928
+ app.get("/api/config", async () => ({
4929
+ publicRead: opts.publicRead === true,
4930
+ authProxy: opts.trustProxy === true
4931
+ }));
4835
4932
  const startedAt = opts.startedAt ?? Date.now();
4836
4933
  const registry = buildLegacyRegistry(opts);
4837
4934
  const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
@@ -4930,6 +5027,7 @@ export {
4930
5027
  addInfra,
4931
5028
  retireEdgesByFile,
4932
5029
  extractFromDirectory,
5030
+ computeDivergences,
4933
5031
  saveGraphToDisk,
4934
5032
  loadGraphFromDisk,
4935
5033
  startPersistLoop,
@@ -4952,4 +5050,4 @@ export {
4952
5050
  removeProject,
4953
5051
  buildApi
4954
5052
  };
4955
- //# sourceMappingURL=chunk-ZU2RQRCN.js.map
5053
+ //# sourceMappingURL=chunk-UPW4CMOH.js.map