@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.
- package/dist/{chunk-75IBA4UL.js → chunk-4V23KYOP.js} +98 -2
- package/dist/chunk-4V23KYOP.js.map +1 -0
- package/dist/{chunk-CY67YKNO.js → chunk-IXIFJKMM.js} +16 -2
- package/dist/chunk-IXIFJKMM.js.map +1 -0
- package/dist/{chunk-EDHOCFOG.js → chunk-N6RPINEJ.js} +16 -5
- package/dist/chunk-N6RPINEJ.js.map +1 -0
- package/dist/{chunk-ZU2RQRCN.js → chunk-UPW4CMOH.js} +368 -270
- package/dist/chunk-UPW4CMOH.js.map +1 -0
- package/dist/cli.cjs +2009 -421
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +7 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +1607 -192
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +246 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +259 -29
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +14 -4
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-PM4SWPZE.js → otel-grpc-GGZHR7VM.js} +3 -3
- package/dist/server.cjs +387 -161
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +26 -7
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-75IBA4UL.js.map +0 -1
- package/dist/chunk-CY67YKNO.js.map +0 -1
- package/dist/chunk-EDHOCFOG.js.map +0 -1
- package/dist/chunk-ZU2RQRCN.js.map +0 -1
- /package/dist/{otel-grpc-PM4SWPZE.js.map → otel-grpc-GGZHR7VM.js.map} +0 -0
|
@@ -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, {
|
|
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
|
|
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
|
|
1121
|
-
if (graph.hasNode(
|
|
1122
|
-
|
|
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 (
|
|
1136
|
+
if (sameEnv) return;
|
|
1125
1137
|
const a = attrs;
|
|
1126
1138
|
if (a.type !== NodeType3.ServiceNode) return;
|
|
1127
|
-
|
|
1128
|
-
|
|
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 (
|
|
1132
|
-
|
|
1133
|
-
}
|
|
1147
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1148
|
+
else if (!anyMatch) anyMatch = id;
|
|
1134
1149
|
});
|
|
1135
|
-
return
|
|
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
|
|
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
|
|
1359
|
-
if (
|
|
1360
|
-
const parentId = ensureServiceNode(ctx.graph,
|
|
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
|
|
3766
|
-
var SCHEMA_VERSION =
|
|
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 =
|
|
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-
|
|
5053
|
+
//# sourceMappingURL=chunk-UPW4CMOH.js.map
|