@neat.is/core 0.3.8 → 0.4.2
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-LQ3JFBTX.js → chunk-CB2UK4EH.js} +139 -41
- package/dist/chunk-CB2UK4EH.js.map +1 -0
- package/dist/{chunk-V4TU7OKZ.js → chunk-D5PIJFBE.js} +2 -2
- package/dist/{chunk-7TYESDAI.js → chunk-KYRIQIPG.js} +48 -11
- package/dist/chunk-KYRIQIPG.js.map +1 -0
- package/dist/{chunk-CZ3T6TE2.js → chunk-NTQHMXWE.js} +239 -71
- package/dist/chunk-NTQHMXWE.js.map +1 -0
- package/dist/cli.cjs +1209 -176
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +4 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +940 -99
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +418 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +427 -131
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-S3AENOZ6.js → otel-grpc-QTX2YQJZ.js} +3 -3
- package/dist/server.cjs +401 -203
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +5 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-7TYESDAI.js.map +0 -1
- package/dist/chunk-CZ3T6TE2.js.map +0 -1
- package/dist/chunk-LQ3JFBTX.js.map +0 -1
- /package/dist/{chunk-V4TU7OKZ.js.map → chunk-D5PIJFBE.js.map} +0 -0
- /package/dist/{otel-grpc-S3AENOZ6.js.map → otel-grpc-QTX2YQJZ.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mountBearerAuth
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KYRIQIPG.js";
|
|
4
4
|
|
|
5
5
|
// src/graph.ts
|
|
6
6
|
import GraphDefault from "graphology";
|
|
@@ -1104,52 +1104,64 @@ function cacheSpanService(span, now) {
|
|
|
1104
1104
|
if (!span.traceId || !span.spanId) return;
|
|
1105
1105
|
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1106
1106
|
parentSpanCache.delete(key);
|
|
1107
|
-
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
|
+
});
|
|
1108
1112
|
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1109
1113
|
const oldest = parentSpanCache.keys().next().value;
|
|
1110
1114
|
if (!oldest) break;
|
|
1111
1115
|
parentSpanCache.delete(oldest);
|
|
1112
1116
|
}
|
|
1113
1117
|
}
|
|
1114
|
-
function
|
|
1118
|
+
function lookupParentSpan(traceId, parentSpanId, now) {
|
|
1115
1119
|
const entry = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1116
1120
|
if (!entry) return null;
|
|
1117
1121
|
if (entry.expiresAt <= now) {
|
|
1118
1122
|
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1119
1123
|
return null;
|
|
1120
1124
|
}
|
|
1121
|
-
return entry.service;
|
|
1125
|
+
return { service: entry.service, env: entry.env };
|
|
1122
1126
|
}
|
|
1123
|
-
function resolveServiceId(graph, host) {
|
|
1124
|
-
const
|
|
1125
|
-
if (graph.hasNode(
|
|
1126
|
-
|
|
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;
|
|
1127
1135
|
graph.forEachNode((id, attrs) => {
|
|
1128
|
-
if (
|
|
1136
|
+
if (sameEnv) return;
|
|
1129
1137
|
const a = attrs;
|
|
1130
1138
|
if (a.type !== NodeType3.ServiceNode) return;
|
|
1131
|
-
|
|
1132
|
-
|
|
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;
|
|
1133
1145
|
return;
|
|
1134
1146
|
}
|
|
1135
|
-
if (
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1147
|
+
if (nodeEnv === "unknown" && !envLessMatch) envLessMatch = id;
|
|
1148
|
+
else if (!anyMatch) anyMatch = id;
|
|
1138
1149
|
});
|
|
1139
|
-
return
|
|
1150
|
+
return sameEnv ?? envLessMatch ?? anyMatch;
|
|
1140
1151
|
}
|
|
1141
1152
|
function frontierIdFor(host) {
|
|
1142
1153
|
return frontierId(host);
|
|
1143
1154
|
}
|
|
1144
|
-
function ensureServiceNode(graph, serviceName) {
|
|
1145
|
-
const id = serviceId(serviceName);
|
|
1155
|
+
function ensureServiceNode(graph, serviceName, env) {
|
|
1156
|
+
const id = serviceId(serviceName, env);
|
|
1146
1157
|
if (graph.hasNode(id)) return id;
|
|
1147
1158
|
const node = {
|
|
1148
1159
|
id,
|
|
1149
1160
|
type: NodeType3.ServiceNode,
|
|
1150
1161
|
name: serviceName,
|
|
1151
1162
|
language: "unknown",
|
|
1152
|
-
discoveredVia: "otel"
|
|
1163
|
+
discoveredVia: "otel",
|
|
1164
|
+
...env !== "unknown" ? { env } : {}
|
|
1153
1165
|
};
|
|
1154
1166
|
graph.addNode(id, node);
|
|
1155
1167
|
return id;
|
|
@@ -1295,7 +1307,7 @@ function buildErrorEventForReceiver(span) {
|
|
|
1295
1307
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1296
1308
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1297
1309
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1298
|
-
affectedNode: serviceId(span.service)
|
|
1310
|
+
affectedNode: serviceId(span.service, span.env)
|
|
1299
1311
|
};
|
|
1300
1312
|
}
|
|
1301
1313
|
function makeErrorSpanWriter(errorsPath) {
|
|
@@ -1309,7 +1321,8 @@ function makeErrorSpanWriter(errorsPath) {
|
|
|
1309
1321
|
async function handleSpan(ctx, span) {
|
|
1310
1322
|
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1311
1323
|
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1312
|
-
const
|
|
1324
|
+
const env = span.env ?? "unknown";
|
|
1325
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1313
1326
|
const isError = span.statusCode === 2;
|
|
1314
1327
|
cacheSpanService(span, nowMs);
|
|
1315
1328
|
let affectedNode = sourceId;
|
|
@@ -1332,7 +1345,7 @@ async function handleSpan(ctx, span) {
|
|
|
1332
1345
|
const host = pickAddress(span);
|
|
1333
1346
|
let resolvedViaAddress = false;
|
|
1334
1347
|
if (host && host !== span.service) {
|
|
1335
|
-
const targetId = resolveServiceId(ctx.graph, host);
|
|
1348
|
+
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1336
1349
|
if (targetId && targetId !== sourceId) {
|
|
1337
1350
|
upsertObservedEdge(
|
|
1338
1351
|
ctx.graph,
|
|
@@ -1359,9 +1372,9 @@ async function handleSpan(ctx, span) {
|
|
|
1359
1372
|
}
|
|
1360
1373
|
}
|
|
1361
1374
|
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1362
|
-
const
|
|
1363
|
-
if (
|
|
1364
|
-
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);
|
|
1365
1378
|
upsertObservedEdge(
|
|
1366
1379
|
ctx.graph,
|
|
1367
1380
|
EdgeType2.CALLS,
|
|
@@ -1557,6 +1570,28 @@ async function readErrorEvents(errorsPath) {
|
|
|
1557
1570
|
throw err;
|
|
1558
1571
|
}
|
|
1559
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
|
+
}
|
|
1560
1595
|
|
|
1561
1596
|
// src/extract/errors.ts
|
|
1562
1597
|
import { promises as fs4 } from "fs";
|
|
@@ -1633,8 +1668,36 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1633
1668
|
".turbo",
|
|
1634
1669
|
"dist",
|
|
1635
1670
|
"build",
|
|
1636
|
-
".next"
|
|
1671
|
+
".next",
|
|
1672
|
+
// Python virtualenv shapes (issue #344). Walking into a venv pulls in the
|
|
1673
|
+
// entire CPython stdlib + every installed package as if it were first-party
|
|
1674
|
+
// service code — 20k+ files, none of which the user wrote. The shape names
|
|
1675
|
+
// cover the three common venv tools (`venv` / `python -m venv`,
|
|
1676
|
+
// `virtualenv`) and the tox + PEP 582 conventions.
|
|
1677
|
+
".venv",
|
|
1678
|
+
"venv",
|
|
1679
|
+
"__pypackages__",
|
|
1680
|
+
".tox",
|
|
1681
|
+
// `site-packages` shows up nested inside venvs that don't carry one of the
|
|
1682
|
+
// outer names (system Python on macOS, in-place pyenv layouts). Listing it
|
|
1683
|
+
// here means we stop the walk at the boundary even when the wrapper dir
|
|
1684
|
+
// wasn't recognisable.
|
|
1685
|
+
"site-packages"
|
|
1637
1686
|
]);
|
|
1687
|
+
var PYVENV_MARKER_CACHE = /* @__PURE__ */ new Map();
|
|
1688
|
+
async function isPythonVenvDir(dir) {
|
|
1689
|
+
const cached = PYVENV_MARKER_CACHE.get(dir);
|
|
1690
|
+
if (cached !== void 0) return cached;
|
|
1691
|
+
try {
|
|
1692
|
+
const stat = await fs5.stat(path5.join(dir, "pyvenv.cfg"));
|
|
1693
|
+
const ok = stat.isFile();
|
|
1694
|
+
PYVENV_MARKER_CACHE.set(dir, ok);
|
|
1695
|
+
return ok;
|
|
1696
|
+
} catch {
|
|
1697
|
+
PYVENV_MARKER_CACHE.set(dir, false);
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1638
1701
|
function isConfigFile(name) {
|
|
1639
1702
|
const ext = path5.extname(name);
|
|
1640
1703
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
@@ -1925,6 +1988,7 @@ async function walkDirs(start, scanPath, options, visit) {
|
|
|
1925
1988
|
const rel = path8.relative(scanPath, child).split(path8.sep).join("/");
|
|
1926
1989
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
1927
1990
|
}
|
|
1991
|
+
if (await isPythonVenvDir(child)) continue;
|
|
1928
1992
|
await visit(child);
|
|
1929
1993
|
await recurse(child, depth + 1);
|
|
1930
1994
|
}
|
|
@@ -1960,6 +2024,18 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
1960
2024
|
}
|
|
1961
2025
|
return [...found];
|
|
1962
2026
|
}
|
|
2027
|
+
function detectJsFramework(pkg) {
|
|
2028
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
2029
|
+
if (deps["next"] !== void 0) return "next";
|
|
2030
|
+
if (deps["remix"] !== void 0) return "remix";
|
|
2031
|
+
for (const k of Object.keys(deps)) {
|
|
2032
|
+
if (k.startsWith("@remix-run/")) return "remix";
|
|
2033
|
+
}
|
|
2034
|
+
if (deps["@sveltejs/kit"] !== void 0) return "sveltekit";
|
|
2035
|
+
if (deps["nuxt"] !== void 0) return "nuxt";
|
|
2036
|
+
if (deps["astro"] !== void 0) return "astro";
|
|
2037
|
+
return void 0;
|
|
2038
|
+
}
|
|
1963
2039
|
async function discoverNodeService(scanPath, dir) {
|
|
1964
2040
|
const pkgPath = path8.join(dir, "package.json");
|
|
1965
2041
|
if (!await exists(pkgPath)) return null;
|
|
@@ -1971,6 +2047,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
1971
2047
|
return null;
|
|
1972
2048
|
}
|
|
1973
2049
|
if (!pkg.name) return null;
|
|
2050
|
+
const framework = detectJsFramework(pkg);
|
|
1974
2051
|
const node = {
|
|
1975
2052
|
id: serviceId2(pkg.name),
|
|
1976
2053
|
type: NodeType4.ServiceNode,
|
|
@@ -1979,7 +2056,8 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
1979
2056
|
version: pkg.version,
|
|
1980
2057
|
dependencies: pkg.dependencies ?? {},
|
|
1981
2058
|
repoPath: path8.relative(scanPath, dir),
|
|
1982
|
-
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2059
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
2060
|
+
...framework ? { framework } : {}
|
|
1983
2061
|
};
|
|
1984
2062
|
return { pkg, dir, node };
|
|
1985
2063
|
}
|
|
@@ -2188,7 +2266,9 @@ async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
|
2188
2266
|
for (const entry of entries) {
|
|
2189
2267
|
if (entry.isDirectory()) {
|
|
2190
2268
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2191
|
-
|
|
2269
|
+
const child = path9.join(start, entry.name);
|
|
2270
|
+
if (await isPythonVenvDir(child)) continue;
|
|
2271
|
+
out.push(...await walkYamlFiles(child, depth + 1, max));
|
|
2192
2272
|
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path9.extname(entry.name))) {
|
|
2193
2273
|
out.push(path9.join(start, entry.name));
|
|
2194
2274
|
}
|
|
@@ -2870,7 +2950,9 @@ async function walkConfigFiles(dir) {
|
|
|
2870
2950
|
for (const entry of entries) {
|
|
2871
2951
|
const full = path18.join(current, entry.name);
|
|
2872
2952
|
if (entry.isDirectory()) {
|
|
2873
|
-
if (
|
|
2953
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2954
|
+
if (await isPythonVenvDir(full)) continue;
|
|
2955
|
+
await walk(full);
|
|
2874
2956
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
2875
2957
|
out.push(full);
|
|
2876
2958
|
}
|
|
@@ -2946,7 +3028,9 @@ async function walkSourceFiles(dir) {
|
|
|
2946
3028
|
for (const entry of entries) {
|
|
2947
3029
|
const full = path19.join(current, entry.name);
|
|
2948
3030
|
if (entry.isDirectory()) {
|
|
2949
|
-
if (
|
|
3031
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3032
|
+
if (await isPythonVenvDir(full)) continue;
|
|
3033
|
+
await walk(full);
|
|
2950
3034
|
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
|
|
2951
3035
|
out.push(full);
|
|
2952
3036
|
}
|
|
@@ -3571,7 +3655,9 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
3571
3655
|
for (const entry of entries) {
|
|
3572
3656
|
if (entry.isDirectory()) {
|
|
3573
3657
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3574
|
-
|
|
3658
|
+
const child = path27.join(start, entry.name);
|
|
3659
|
+
if (await isPythonVenvDir(child)) continue;
|
|
3660
|
+
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
3575
3661
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3576
3662
|
out.push(path27.join(start, entry.name));
|
|
3577
3663
|
}
|
|
@@ -3618,7 +3704,9 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
3618
3704
|
for (const entry of entries) {
|
|
3619
3705
|
if (entry.isDirectory()) {
|
|
3620
3706
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3621
|
-
|
|
3707
|
+
const child = path28.join(start, entry.name);
|
|
3708
|
+
if (await isPythonVenvDir(child)) continue;
|
|
3709
|
+
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
3622
3710
|
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path28.extname(entry.name))) {
|
|
3623
3711
|
out.push(path28.join(start, entry.name));
|
|
3624
3712
|
}
|
|
@@ -4009,7 +4097,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4009
4097
|
import { promises as fs17 } from "fs";
|
|
4010
4098
|
import path31 from "path";
|
|
4011
4099
|
import { Provenance as Provenance10, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
4012
|
-
var SCHEMA_VERSION =
|
|
4100
|
+
var SCHEMA_VERSION = 4;
|
|
4013
4101
|
function migrateV1ToV2(payload) {
|
|
4014
4102
|
const nodes = payload.graph.nodes;
|
|
4015
4103
|
if (Array.isArray(nodes)) {
|
|
@@ -4021,6 +4109,9 @@ function migrateV1ToV2(payload) {
|
|
|
4021
4109
|
}
|
|
4022
4110
|
return { ...payload, schemaVersion: 2 };
|
|
4023
4111
|
}
|
|
4112
|
+
function migrateV3ToV4(payload) {
|
|
4113
|
+
return { ...payload, schemaVersion: 4 };
|
|
4114
|
+
}
|
|
4024
4115
|
function migrateV2ToV3(payload) {
|
|
4025
4116
|
const edges = payload.graph.edges;
|
|
4026
4117
|
if (Array.isArray(edges)) {
|
|
@@ -4069,6 +4160,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4069
4160
|
if (payload.schemaVersion === 2) {
|
|
4070
4161
|
payload = migrateV2ToV3(payload);
|
|
4071
4162
|
}
|
|
4163
|
+
if (payload.schemaVersion === 3) {
|
|
4164
|
+
payload = migrateV3ToV4(payload);
|
|
4165
|
+
}
|
|
4072
4166
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4073
4167
|
throw new Error(
|
|
4074
4168
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -4486,10 +4580,19 @@ function projectFromReq(req) {
|
|
|
4486
4580
|
const params = req.params;
|
|
4487
4581
|
return params.project ?? DEFAULT_PROJECT;
|
|
4488
4582
|
}
|
|
4489
|
-
function resolveProject(registry, req, reply) {
|
|
4583
|
+
function resolveProject(registry, req, reply, bootstrap) {
|
|
4490
4584
|
const name = projectFromReq(req);
|
|
4491
4585
|
const ctx = registry.get(name);
|
|
4492
4586
|
if (!ctx) {
|
|
4587
|
+
const phase = bootstrap?.status(name);
|
|
4588
|
+
if (phase === "bootstrapping") {
|
|
4589
|
+
void reply.code(503).send({ ready: false, project: name, status: "bootstrapping" });
|
|
4590
|
+
return null;
|
|
4591
|
+
}
|
|
4592
|
+
if (phase === "broken") {
|
|
4593
|
+
void reply.code(503).send({ ready: false, project: name, status: "broken" });
|
|
4594
|
+
return null;
|
|
4595
|
+
}
|
|
4493
4596
|
void reply.code(404).send({ error: "project not found", project: name });
|
|
4494
4597
|
return null;
|
|
4495
4598
|
}
|
|
@@ -4519,36 +4622,38 @@ function buildLegacyRegistry(opts) {
|
|
|
4519
4622
|
function registerRoutes(scope, ctx) {
|
|
4520
4623
|
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
4521
4624
|
scope.get("/events", (req, reply) => {
|
|
4522
|
-
const proj = resolveProject(registry, req, reply);
|
|
4625
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4523
4626
|
if (!proj) return;
|
|
4524
4627
|
handleSse(req, reply, { project: proj.name });
|
|
4525
4628
|
});
|
|
4526
|
-
scope
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4629
|
+
if (ctx.scope === "project") {
|
|
4630
|
+
scope.get("/health", async (req, reply) => {
|
|
4631
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4632
|
+
if (!proj) return;
|
|
4633
|
+
const uptimeMs = Date.now() - startedAt;
|
|
4634
|
+
return {
|
|
4635
|
+
ok: true,
|
|
4636
|
+
project: proj.name,
|
|
4637
|
+
uptimeMs,
|
|
4638
|
+
// Legacy fields kept additively. The web shell's StatusBar reads
|
|
4639
|
+
// nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
|
|
4640
|
+
// the canonical triple and lets the extras pass through.
|
|
4641
|
+
uptime: Math.floor(uptimeMs / 1e3),
|
|
4642
|
+
nodeCount: proj.graph.order,
|
|
4643
|
+
edgeCount: proj.graph.size,
|
|
4644
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
4645
|
+
};
|
|
4646
|
+
});
|
|
4647
|
+
}
|
|
4543
4648
|
scope.get("/graph", async (req, reply) => {
|
|
4544
|
-
const proj = resolveProject(registry, req, reply);
|
|
4649
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4545
4650
|
if (!proj) return;
|
|
4546
4651
|
return serializeGraph(proj.graph);
|
|
4547
4652
|
});
|
|
4548
4653
|
scope.get(
|
|
4549
4654
|
"/graph/node/:id",
|
|
4550
4655
|
async (req, reply) => {
|
|
4551
|
-
const proj = resolveProject(registry, req, reply);
|
|
4656
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4552
4657
|
if (!proj) return;
|
|
4553
4658
|
const { id } = req.params;
|
|
4554
4659
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -4560,7 +4665,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4560
4665
|
scope.get(
|
|
4561
4666
|
"/graph/edges/:id",
|
|
4562
4667
|
async (req, reply) => {
|
|
4563
|
-
const proj = resolveProject(registry, req, reply);
|
|
4668
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4564
4669
|
if (!proj) return;
|
|
4565
4670
|
const { id } = req.params;
|
|
4566
4671
|
if (!proj.graph.hasNode(id)) {
|
|
@@ -4572,7 +4677,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4572
4677
|
}
|
|
4573
4678
|
);
|
|
4574
4679
|
scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
|
|
4575
|
-
const proj = resolveProject(registry, req, reply);
|
|
4680
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4576
4681
|
if (!proj) return;
|
|
4577
4682
|
const { nodeId } = req.params;
|
|
4578
4683
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4587,7 +4692,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4587
4692
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
4588
4693
|
});
|
|
4589
4694
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
4590
|
-
const proj = resolveProject(registry, req, reply);
|
|
4695
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4591
4696
|
if (!proj) return;
|
|
4592
4697
|
let typeFilter;
|
|
4593
4698
|
if (req.query.type) {
|
|
@@ -4622,7 +4727,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4622
4727
|
});
|
|
4623
4728
|
});
|
|
4624
4729
|
scope.get("/incidents", async (req, reply) => {
|
|
4625
|
-
const proj = resolveProject(registry, req, reply);
|
|
4730
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4626
4731
|
if (!proj) return;
|
|
4627
4732
|
const epath = errorsPathFor(proj);
|
|
4628
4733
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
@@ -4634,7 +4739,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4634
4739
|
return { count: sliced.length, total, events: sliced };
|
|
4635
4740
|
});
|
|
4636
4741
|
scope.get("/stale-events", async (req, reply) => {
|
|
4637
|
-
const proj = resolveProject(registry, req, reply);
|
|
4742
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4638
4743
|
if (!proj) return;
|
|
4639
4744
|
const spath = staleEventsPathFor(proj);
|
|
4640
4745
|
if (!spath) return { count: 0, total: 0, events: [] };
|
|
@@ -4649,7 +4754,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4649
4754
|
scope.get(
|
|
4650
4755
|
"/incidents/:nodeId",
|
|
4651
4756
|
async (req, reply) => {
|
|
4652
|
-
const proj = resolveProject(registry, req, reply);
|
|
4757
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4653
4758
|
if (!proj) return;
|
|
4654
4759
|
const { nodeId } = req.params;
|
|
4655
4760
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4665,7 +4770,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4665
4770
|
}
|
|
4666
4771
|
);
|
|
4667
4772
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
4668
|
-
const proj = resolveProject(registry, req, reply);
|
|
4773
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4669
4774
|
if (!proj) return;
|
|
4670
4775
|
const { nodeId } = req.params;
|
|
4671
4776
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4685,7 +4790,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4685
4790
|
return result;
|
|
4686
4791
|
});
|
|
4687
4792
|
scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
|
|
4688
|
-
const proj = resolveProject(registry, req, reply);
|
|
4793
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4689
4794
|
if (!proj) return;
|
|
4690
4795
|
const { nodeId } = req.params;
|
|
4691
4796
|
if (!proj.graph.hasNode(nodeId)) {
|
|
@@ -4698,7 +4803,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4698
4803
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
4699
4804
|
});
|
|
4700
4805
|
scope.get("/search", async (req, reply) => {
|
|
4701
|
-
const proj = resolveProject(registry, req, reply);
|
|
4806
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4702
4807
|
if (!proj) return;
|
|
4703
4808
|
const raw = (req.query.q ?? "").trim();
|
|
4704
4809
|
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
@@ -4729,7 +4834,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4729
4834
|
scope.get(
|
|
4730
4835
|
"/graph/diff",
|
|
4731
4836
|
async (req, reply) => {
|
|
4732
|
-
const proj = resolveProject(registry, req, reply);
|
|
4837
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4733
4838
|
if (!proj) return;
|
|
4734
4839
|
const against = req.query.against;
|
|
4735
4840
|
if (!against) {
|
|
@@ -4743,8 +4848,37 @@ function registerRoutes(scope, ctx) {
|
|
|
4743
4848
|
}
|
|
4744
4849
|
}
|
|
4745
4850
|
);
|
|
4851
|
+
scope.post("/snapshot", async (req, reply) => {
|
|
4852
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4853
|
+
if (!proj) return;
|
|
4854
|
+
const body = req.body;
|
|
4855
|
+
if (!body || typeof body !== "object" || !body.snapshot) {
|
|
4856
|
+
return reply.code(400).send({ error: "request body must be { snapshot: <persisted-graph> }" });
|
|
4857
|
+
}
|
|
4858
|
+
const snap = body.snapshot;
|
|
4859
|
+
if (typeof snap.schemaVersion !== "number" || snap.schemaVersion !== SCHEMA_VERSION) {
|
|
4860
|
+
return reply.code(400).send({
|
|
4861
|
+
error: `unsupported snapshot schemaVersion ${snap.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
4862
|
+
});
|
|
4863
|
+
}
|
|
4864
|
+
try {
|
|
4865
|
+
const result = mergeSnapshot(proj.graph, snap);
|
|
4866
|
+
return {
|
|
4867
|
+
project: proj.name,
|
|
4868
|
+
nodesAdded: result.nodesAdded,
|
|
4869
|
+
edgesAdded: result.edgesAdded,
|
|
4870
|
+
nodeCount: proj.graph.order,
|
|
4871
|
+
edgeCount: proj.graph.size
|
|
4872
|
+
};
|
|
4873
|
+
} catch (err) {
|
|
4874
|
+
return reply.code(400).send({
|
|
4875
|
+
error: "snapshot merge failed",
|
|
4876
|
+
details: err.message
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4746
4880
|
scope.post("/graph/scan", async (req, reply) => {
|
|
4747
|
-
const proj = resolveProject(registry, req, reply);
|
|
4881
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4748
4882
|
if (!proj) return;
|
|
4749
4883
|
if (!proj.scanPath) {
|
|
4750
4884
|
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
@@ -4760,7 +4894,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4760
4894
|
};
|
|
4761
4895
|
});
|
|
4762
4896
|
scope.get("/policies", async (req, reply) => {
|
|
4763
|
-
const proj = resolveProject(registry, req, reply);
|
|
4897
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4764
4898
|
if (!proj) return;
|
|
4765
4899
|
const policyPath = ctx.policyFilePathFor(proj);
|
|
4766
4900
|
if (!policyPath) {
|
|
@@ -4777,7 +4911,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4777
4911
|
}
|
|
4778
4912
|
});
|
|
4779
4913
|
scope.get("/policies/violations", async (req, reply) => {
|
|
4780
|
-
const proj = resolveProject(registry, req, reply);
|
|
4914
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4781
4915
|
if (!proj) return;
|
|
4782
4916
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4783
4917
|
let violations = await log.readAll();
|
|
@@ -4797,7 +4931,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4797
4931
|
return { violations };
|
|
4798
4932
|
});
|
|
4799
4933
|
scope.post("/policies/check", async (req, reply) => {
|
|
4800
|
-
const proj = resolveProject(registry, req, reply);
|
|
4934
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
4801
4935
|
if (!proj) return;
|
|
4802
4936
|
const parsed = PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4803
4937
|
if (!parsed.success) {
|
|
@@ -4836,7 +4970,15 @@ function registerRoutes(scope, ctx) {
|
|
|
4836
4970
|
async function buildApi(opts) {
|
|
4837
4971
|
const app = Fastify({ logger: false });
|
|
4838
4972
|
await app.register(cors, { origin: true });
|
|
4839
|
-
mountBearerAuth(app, {
|
|
4973
|
+
mountBearerAuth(app, {
|
|
4974
|
+
token: opts.authToken,
|
|
4975
|
+
trustProxy: opts.trustProxy,
|
|
4976
|
+
publicRead: opts.publicRead
|
|
4977
|
+
});
|
|
4978
|
+
app.get("/api/config", async () => ({
|
|
4979
|
+
publicRead: opts.publicRead === true,
|
|
4980
|
+
authProxy: opts.trustProxy === true
|
|
4981
|
+
}));
|
|
4840
4982
|
const startedAt = opts.startedAt ?? Date.now();
|
|
4841
4983
|
const registry = buildLegacyRegistry(opts);
|
|
4842
4984
|
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
@@ -4860,10 +5002,36 @@ async function buildApi(opts) {
|
|
|
4860
5002
|
const routeCtx = {
|
|
4861
5003
|
registry,
|
|
4862
5004
|
startedAt,
|
|
5005
|
+
scope: "root",
|
|
4863
5006
|
errorsPathFor,
|
|
4864
5007
|
staleEventsPathFor,
|
|
4865
|
-
policyFilePathFor
|
|
5008
|
+
policyFilePathFor,
|
|
5009
|
+
bootstrap: opts.bootstrap
|
|
4866
5010
|
};
|
|
5011
|
+
app.get("/health", async () => {
|
|
5012
|
+
const uptimeMs = Date.now() - startedAt;
|
|
5013
|
+
const bootstrapList = opts.bootstrap?.list() ?? [];
|
|
5014
|
+
const byName = new Map(bootstrapList.map((p) => [p.name, p]));
|
|
5015
|
+
const names = /* @__PURE__ */ new Set([
|
|
5016
|
+
...registry.list(),
|
|
5017
|
+
...bootstrapList.map((p) => p.name)
|
|
5018
|
+
]);
|
|
5019
|
+
const projects = [...names].sort().map((name) => {
|
|
5020
|
+
const proj = registry.get(name);
|
|
5021
|
+
const tracked = byName.get(name);
|
|
5022
|
+
return {
|
|
5023
|
+
name,
|
|
5024
|
+
nodeCount: proj?.graph.order ?? 0,
|
|
5025
|
+
edgeCount: proj?.graph.size ?? 0,
|
|
5026
|
+
...tracked ? { status: tracked.status, elapsedMs: tracked.elapsedMs } : {}
|
|
5027
|
+
};
|
|
5028
|
+
});
|
|
5029
|
+
return {
|
|
5030
|
+
ok: true,
|
|
5031
|
+
uptimeMs,
|
|
5032
|
+
projects
|
|
5033
|
+
};
|
|
5034
|
+
});
|
|
4867
5035
|
app.get("/projects", async (_req, reply) => {
|
|
4868
5036
|
try {
|
|
4869
5037
|
return await listProjects();
|
|
@@ -4891,7 +5059,7 @@ async function buildApi(opts) {
|
|
|
4891
5059
|
registerRoutes(app, routeCtx);
|
|
4892
5060
|
await app.register(
|
|
4893
5061
|
async (scope) => {
|
|
4894
|
-
registerRoutes(scope, routeCtx);
|
|
5062
|
+
registerRoutes(scope, { ...routeCtx, scope: "project" });
|
|
4895
5063
|
},
|
|
4896
5064
|
{ prefix: "/projects/:project" }
|
|
4897
5065
|
);
|
|
@@ -4958,4 +5126,4 @@ export {
|
|
|
4958
5126
|
removeProject,
|
|
4959
5127
|
buildApi
|
|
4960
5128
|
};
|
|
4961
|
-
//# sourceMappingURL=chunk-
|
|
5129
|
+
//# sourceMappingURL=chunk-NTQHMXWE.js.map
|