@neat.is/core 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-HWO746IM.js → chunk-5BQWBQJR.js} +2 -2
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-GAYTAGEH.js → chunk-JMLZOKCS.js} +268 -146
- package/dist/chunk-JMLZOKCS.js.map +1 -0
- package/dist/{chunk-I5IMCXRO.js → chunk-QYUB3FQL.js} +2 -2
- package/dist/{chunk-FTKDVKBC.js → chunk-W7ZYJZC7.js} +285 -3
- package/dist/chunk-W7ZYJZC7.js.map +1 -0
- package/dist/cli.cjs +739 -252
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +100 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +565 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -4
- package/dist/neatd.cjs +373 -152
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +112 -6
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-B4XBSI4W.js → otel-grpc-USFL3OI3.js} +3 -2
- package/dist/otel-grpc-USFL3OI3.js.map +1 -0
- package/dist/server.cjs +659 -264
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -3
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-FTKDVKBC.js.map +0 -1
- package/dist/chunk-GAYTAGEH.js.map +0 -1
- /package/dist/{chunk-HWO746IM.js.map → chunk-5BQWBQJR.js.map} +0 -0
- /package/dist/{otel-grpc-B4XBSI4W.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-I5IMCXRO.js.map → chunk-QYUB3FQL.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -101,8 +101,8 @@ function reshapeGrpcRequest(req) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
function resolveProtoRoot() {
|
|
104
|
-
const here =
|
|
105
|
-
return
|
|
104
|
+
const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
105
|
+
return import_node_path31.default.resolve(here, "..", "proto");
|
|
106
106
|
}
|
|
107
107
|
function loadTraceService() {
|
|
108
108
|
const protoRoot = resolveProtoRoot();
|
|
@@ -157,13 +157,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
157
157
|
})
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
-
var import_node_url,
|
|
160
|
+
var import_node_url, import_node_path31, grpc, protoLoader;
|
|
161
161
|
var init_otel_grpc = __esm({
|
|
162
162
|
"src/otel-grpc.ts"() {
|
|
163
163
|
"use strict";
|
|
164
164
|
init_cjs_shims();
|
|
165
165
|
import_node_url = require("url");
|
|
166
|
-
|
|
166
|
+
import_node_path31 = __toESM(require("path"), 1);
|
|
167
167
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
168
168
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
169
169
|
init_otel();
|
|
@@ -260,10 +260,10 @@ function parseOtlpRequest(body) {
|
|
|
260
260
|
}
|
|
261
261
|
function loadProtobufDecoder() {
|
|
262
262
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
263
|
-
const here =
|
|
264
|
-
const protoRoot =
|
|
263
|
+
const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
264
|
+
const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
|
|
265
265
|
const root = new import_protobufjs.default.Root();
|
|
266
|
-
root.resolvePath = (_origin, target) =>
|
|
266
|
+
root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
|
|
267
267
|
root.loadSync(
|
|
268
268
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
269
269
|
{ keepCase: true }
|
|
@@ -355,12 +355,12 @@ async function buildOtelReceiver(opts) {
|
|
|
355
355
|
};
|
|
356
356
|
return decorated;
|
|
357
357
|
}
|
|
358
|
-
var
|
|
358
|
+
var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
359
359
|
var init_otel = __esm({
|
|
360
360
|
"src/otel.ts"() {
|
|
361
361
|
"use strict";
|
|
362
362
|
init_cjs_shims();
|
|
363
|
-
|
|
363
|
+
import_node_path32 = __toESM(require("path"), 1);
|
|
364
364
|
import_node_url2 = require("url");
|
|
365
365
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
366
366
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -380,8 +380,8 @@ __export(cli_exports, {
|
|
|
380
380
|
});
|
|
381
381
|
module.exports = __toCommonJS(cli_exports);
|
|
382
382
|
init_cjs_shims();
|
|
383
|
-
var
|
|
384
|
-
var
|
|
383
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
384
|
+
var import_node_fs22 = require("fs");
|
|
385
385
|
|
|
386
386
|
// src/graph.ts
|
|
387
387
|
init_cjs_shims();
|
|
@@ -887,19 +887,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
887
887
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
888
888
|
let best = { path: [start], edges: [] };
|
|
889
889
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
890
|
-
function step(node,
|
|
891
|
-
if (
|
|
892
|
-
best = { path: [...
|
|
890
|
+
function step(node, path38, edges) {
|
|
891
|
+
if (path38.length > best.path.length) {
|
|
892
|
+
best = { path: [...path38], edges: [...edges] };
|
|
893
893
|
}
|
|
894
|
-
if (
|
|
894
|
+
if (path38.length - 1 >= maxDepth) return;
|
|
895
895
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
896
896
|
for (const [srcId, edge] of incoming) {
|
|
897
897
|
if (visited.has(srcId)) continue;
|
|
898
898
|
visited.add(srcId);
|
|
899
|
-
|
|
899
|
+
path38.push(srcId);
|
|
900
900
|
edges.push(edge);
|
|
901
|
-
step(srcId,
|
|
902
|
-
|
|
901
|
+
step(srcId, path38, edges);
|
|
902
|
+
path38.pop();
|
|
903
903
|
edges.pop();
|
|
904
904
|
visited.delete(srcId);
|
|
905
905
|
}
|
|
@@ -1936,10 +1936,10 @@ async function readErrorEvents(errorsPath) {
|
|
|
1936
1936
|
|
|
1937
1937
|
// src/extract/services.ts
|
|
1938
1938
|
init_cjs_shims();
|
|
1939
|
-
var
|
|
1940
|
-
var
|
|
1939
|
+
var import_node_fs7 = require("fs");
|
|
1940
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1941
1941
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
1942
|
-
var
|
|
1942
|
+
var import_minimatch2 = require("minimatch");
|
|
1943
1943
|
var import_types5 = require("@neat.is/types");
|
|
1944
1944
|
|
|
1945
1945
|
// src/extract/shared.ts
|
|
@@ -2052,6 +2052,73 @@ function pythonToPackage(service) {
|
|
|
2052
2052
|
};
|
|
2053
2053
|
}
|
|
2054
2054
|
|
|
2055
|
+
// src/extract/owners.ts
|
|
2056
|
+
init_cjs_shims();
|
|
2057
|
+
var import_node_fs6 = require("fs");
|
|
2058
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
2059
|
+
var import_minimatch = require("minimatch");
|
|
2060
|
+
async function loadCodeowners(scanPath) {
|
|
2061
|
+
const candidates = [
|
|
2062
|
+
import_node_path6.default.join(scanPath, "CODEOWNERS"),
|
|
2063
|
+
import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
|
|
2064
|
+
];
|
|
2065
|
+
for (const file of candidates) {
|
|
2066
|
+
if (await exists(file)) {
|
|
2067
|
+
const raw = await import_node_fs6.promises.readFile(file, "utf8");
|
|
2068
|
+
return parseCodeowners(raw);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
function parseCodeowners(raw) {
|
|
2074
|
+
const rules = [];
|
|
2075
|
+
for (const line of raw.split("\n")) {
|
|
2076
|
+
const trimmed = line.trim();
|
|
2077
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2078
|
+
const match = /^(\S+)\s+(.+)$/.exec(trimmed);
|
|
2079
|
+
if (!match) continue;
|
|
2080
|
+
rules.push({ pattern: match[1], owners: match[2].trim() });
|
|
2081
|
+
}
|
|
2082
|
+
return { rules };
|
|
2083
|
+
}
|
|
2084
|
+
function matchOwner(file, repoPath) {
|
|
2085
|
+
const normalized = repoPath.split(import_node_path6.default.sep).join("/");
|
|
2086
|
+
for (const rule of file.rules) {
|
|
2087
|
+
if (matchesPattern(rule.pattern, normalized)) return rule.owners;
|
|
2088
|
+
}
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
2091
|
+
function matchesPattern(rawPattern, repoPath) {
|
|
2092
|
+
let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
|
|
2093
|
+
if (pattern === "*") return !repoPath.includes("/");
|
|
2094
|
+
if (pattern === "**" || pattern === "") return true;
|
|
2095
|
+
if (pattern.endsWith("/")) pattern = pattern + "**";
|
|
2096
|
+
if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
|
|
2097
|
+
if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
async function readPackageJsonAuthor(serviceDir) {
|
|
2101
|
+
const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
|
|
2102
|
+
if (!await exists(pkgPath)) return null;
|
|
2103
|
+
try {
|
|
2104
|
+
const pkg = await readJson(pkgPath);
|
|
2105
|
+
if (!pkg.author) return null;
|
|
2106
|
+
if (typeof pkg.author === "string") return pkg.author;
|
|
2107
|
+
if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
|
|
2108
|
+
return null;
|
|
2109
|
+
} catch {
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
2114
|
+
if (codeowners && repoPath !== void 0) {
|
|
2115
|
+
const owner = matchOwner(codeowners, repoPath);
|
|
2116
|
+
if (owner) return owner;
|
|
2117
|
+
}
|
|
2118
|
+
const author = await readPackageJsonAuthor(serviceDir);
|
|
2119
|
+
return author ?? void 0;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2055
2122
|
// src/extract/services.ts
|
|
2056
2123
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
2057
2124
|
function parseScanDepth() {
|
|
@@ -2068,21 +2135,21 @@ function workspaceGlobs(pkg) {
|
|
|
2068
2135
|
return null;
|
|
2069
2136
|
}
|
|
2070
2137
|
async function loadGitignore(scanPath) {
|
|
2071
|
-
const gitignorePath =
|
|
2138
|
+
const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
|
|
2072
2139
|
if (!await exists(gitignorePath)) return null;
|
|
2073
|
-
const raw = await
|
|
2140
|
+
const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
|
|
2074
2141
|
return (0, import_ignore.default)().add(raw);
|
|
2075
2142
|
}
|
|
2076
2143
|
async function walkDirs(start, scanPath, options, visit) {
|
|
2077
2144
|
async function recurse(current, depth) {
|
|
2078
2145
|
if (depth > options.maxDepth) return;
|
|
2079
|
-
const entries = await
|
|
2146
|
+
const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2080
2147
|
for (const entry2 of entries) {
|
|
2081
2148
|
if (!entry2.isDirectory()) continue;
|
|
2082
2149
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2083
|
-
const child =
|
|
2150
|
+
const child = import_node_path7.default.join(current, entry2.name);
|
|
2084
2151
|
if (options.ig) {
|
|
2085
|
-
const rel =
|
|
2152
|
+
const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
|
|
2086
2153
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2087
2154
|
}
|
|
2088
2155
|
await visit(child);
|
|
@@ -2097,8 +2164,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2097
2164
|
for (const raw of globs) {
|
|
2098
2165
|
const pattern = raw.replace(/^\.\//, "");
|
|
2099
2166
|
if (!pattern.includes("*")) {
|
|
2100
|
-
const candidate =
|
|
2101
|
-
if (await exists(
|
|
2167
|
+
const candidate = import_node_path7.default.join(scanPath, pattern);
|
|
2168
|
+
if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2102
2169
|
continue;
|
|
2103
2170
|
}
|
|
2104
2171
|
const segments = pattern.split("/");
|
|
@@ -2107,13 +2174,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2107
2174
|
if (seg.includes("*")) break;
|
|
2108
2175
|
staticSegments.push(seg);
|
|
2109
2176
|
}
|
|
2110
|
-
const start =
|
|
2177
|
+
const start = import_node_path7.default.join(scanPath, ...staticSegments);
|
|
2111
2178
|
if (!await exists(start)) continue;
|
|
2112
2179
|
const hasDoubleStar = pattern.includes("**");
|
|
2113
2180
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2114
2181
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2115
|
-
const rel =
|
|
2116
|
-
if ((0,
|
|
2182
|
+
const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
|
|
2183
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
|
|
2117
2184
|
found.add(dir);
|
|
2118
2185
|
}
|
|
2119
2186
|
});
|
|
@@ -2121,9 +2188,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2121
2188
|
return [...found];
|
|
2122
2189
|
}
|
|
2123
2190
|
async function discoverNodeService(scanPath, dir) {
|
|
2124
|
-
const pkgPath =
|
|
2191
|
+
const pkgPath = import_node_path7.default.join(dir, "package.json");
|
|
2125
2192
|
if (!await exists(pkgPath)) return null;
|
|
2126
|
-
|
|
2193
|
+
let pkg;
|
|
2194
|
+
try {
|
|
2195
|
+
pkg = await readJson(pkgPath);
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
console.warn(
|
|
2198
|
+
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2199
|
+
);
|
|
2200
|
+
return null;
|
|
2201
|
+
}
|
|
2127
2202
|
if (!pkg.name) return null;
|
|
2128
2203
|
const node = {
|
|
2129
2204
|
id: (0, import_types5.serviceId)(pkg.name),
|
|
@@ -2132,7 +2207,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2132
2207
|
language: "javascript",
|
|
2133
2208
|
version: pkg.version,
|
|
2134
2209
|
dependencies: pkg.dependencies ?? {},
|
|
2135
|
-
repoPath:
|
|
2210
|
+
repoPath: import_node_path7.default.relative(scanPath, dir),
|
|
2136
2211
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2137
2212
|
};
|
|
2138
2213
|
return { pkg, dir, node };
|
|
@@ -2148,13 +2223,22 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2148
2223
|
language: "python",
|
|
2149
2224
|
version: py.version,
|
|
2150
2225
|
dependencies: py.dependencies,
|
|
2151
|
-
repoPath:
|
|
2226
|
+
repoPath: import_node_path7.default.relative(scanPath, dir)
|
|
2152
2227
|
};
|
|
2153
2228
|
return { pkg, dir, node };
|
|
2154
2229
|
}
|
|
2155
2230
|
async function discoverServices(scanPath) {
|
|
2156
|
-
const rootPkgPath =
|
|
2157
|
-
|
|
2231
|
+
const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
|
|
2232
|
+
let rootPkg = null;
|
|
2233
|
+
if (await exists(rootPkgPath)) {
|
|
2234
|
+
try {
|
|
2235
|
+
rootPkg = await readJson(rootPkgPath);
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
console.warn(
|
|
2238
|
+
`[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2158
2242
|
const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
|
|
2159
2243
|
const candidateDirs = [];
|
|
2160
2244
|
if (wsGlobs) {
|
|
@@ -2167,9 +2251,9 @@ async function discoverServices(scanPath) {
|
|
|
2167
2251
|
scanPath,
|
|
2168
2252
|
{ maxDepth: parseScanDepth(), ig },
|
|
2169
2253
|
async (dir) => {
|
|
2170
|
-
if (await exists(
|
|
2254
|
+
if (await exists(import_node_path7.default.join(dir, "package.json"))) {
|
|
2171
2255
|
candidateDirs.push(dir);
|
|
2172
|
-
} else if (await exists(
|
|
2256
|
+
} else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
|
|
2173
2257
|
candidateDirs.push(dir);
|
|
2174
2258
|
}
|
|
2175
2259
|
}
|
|
@@ -2183,8 +2267,8 @@ async function discoverServices(scanPath) {
|
|
|
2183
2267
|
if (!service) continue;
|
|
2184
2268
|
const existingDir = seen.get(service.node.name);
|
|
2185
2269
|
if (existingDir !== void 0) {
|
|
2186
|
-
const a =
|
|
2187
|
-
const b =
|
|
2270
|
+
const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
|
|
2271
|
+
const b = import_node_path7.default.relative(scanPath, dir) || ".";
|
|
2188
2272
|
console.warn(
|
|
2189
2273
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2190
2274
|
);
|
|
@@ -2193,6 +2277,11 @@ async function discoverServices(scanPath) {
|
|
|
2193
2277
|
seen.set(service.node.name, dir);
|
|
2194
2278
|
out.push(service);
|
|
2195
2279
|
}
|
|
2280
|
+
const codeowners = await loadCodeowners(scanPath);
|
|
2281
|
+
for (const service of out) {
|
|
2282
|
+
const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
|
|
2283
|
+
if (owner !== void 0) service.node.owner = owner;
|
|
2284
|
+
}
|
|
2196
2285
|
return out;
|
|
2197
2286
|
}
|
|
2198
2287
|
function addServiceNodes(graph, services) {
|
|
@@ -2216,8 +2305,8 @@ function addServiceNodes(graph, services) {
|
|
|
2216
2305
|
|
|
2217
2306
|
// src/extract/aliases.ts
|
|
2218
2307
|
init_cjs_shims();
|
|
2219
|
-
var
|
|
2220
|
-
var
|
|
2308
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2309
|
+
var import_node_fs8 = require("fs");
|
|
2221
2310
|
var import_yaml2 = require("yaml");
|
|
2222
2311
|
var import_types6 = require("@neat.is/types");
|
|
2223
2312
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2244,21 +2333,29 @@ function indexServicesByName(services) {
|
|
|
2244
2333
|
const map = /* @__PURE__ */ new Map();
|
|
2245
2334
|
for (const s of services) {
|
|
2246
2335
|
map.set(s.node.name, s.node.id);
|
|
2247
|
-
map.set(
|
|
2336
|
+
map.set(import_node_path8.default.basename(s.dir), s.node.id);
|
|
2248
2337
|
}
|
|
2249
2338
|
return map;
|
|
2250
2339
|
}
|
|
2251
2340
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2252
2341
|
let composePath = null;
|
|
2253
2342
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2254
|
-
const abs =
|
|
2343
|
+
const abs = import_node_path8.default.join(scanPath, name);
|
|
2255
2344
|
if (await exists(abs)) {
|
|
2256
2345
|
composePath = abs;
|
|
2257
2346
|
break;
|
|
2258
2347
|
}
|
|
2259
2348
|
}
|
|
2260
2349
|
if (!composePath) return;
|
|
2261
|
-
|
|
2350
|
+
let compose;
|
|
2351
|
+
try {
|
|
2352
|
+
compose = await readYaml(composePath);
|
|
2353
|
+
} catch (err) {
|
|
2354
|
+
console.warn(
|
|
2355
|
+
`[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
|
|
2356
|
+
);
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2262
2359
|
if (!compose?.services) return;
|
|
2263
2360
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
2264
2361
|
const serviceId3 = serviceIndex.get(composeName);
|
|
@@ -2297,9 +2394,17 @@ function parseDockerfileLabels(content) {
|
|
|
2297
2394
|
}
|
|
2298
2395
|
async function collectDockerfileAliases(graph, services) {
|
|
2299
2396
|
for (const service of services) {
|
|
2300
|
-
const dockerfilePath =
|
|
2397
|
+
const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
|
|
2301
2398
|
if (!await exists(dockerfilePath)) continue;
|
|
2302
|
-
|
|
2399
|
+
let content;
|
|
2400
|
+
try {
|
|
2401
|
+
content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
|
|
2402
|
+
} catch (err) {
|
|
2403
|
+
console.warn(
|
|
2404
|
+
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2405
|
+
);
|
|
2406
|
+
continue;
|
|
2407
|
+
}
|
|
2303
2408
|
const aliases = parseDockerfileLabels(content);
|
|
2304
2409
|
if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
|
|
2305
2410
|
}
|
|
@@ -2307,13 +2412,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2307
2412
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2308
2413
|
if (depth > max) return [];
|
|
2309
2414
|
const out = [];
|
|
2310
|
-
const entries = await
|
|
2415
|
+
const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2311
2416
|
for (const entry2 of entries) {
|
|
2312
2417
|
if (entry2.isDirectory()) {
|
|
2313
2418
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2314
|
-
out.push(...await walkYamlFiles(
|
|
2315
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2316
|
-
out.push(
|
|
2419
|
+
out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
|
|
2420
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
|
|
2421
|
+
out.push(import_node_path8.default.join(start, entry2.name));
|
|
2317
2422
|
}
|
|
2318
2423
|
}
|
|
2319
2424
|
return out;
|
|
@@ -2340,7 +2445,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2340
2445
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2341
2446
|
const files = await walkYamlFiles(scanPath);
|
|
2342
2447
|
for (const file of files) {
|
|
2343
|
-
const content = await
|
|
2448
|
+
const content = await import_node_fs8.promises.readFile(file, "utf8");
|
|
2344
2449
|
let docs;
|
|
2345
2450
|
try {
|
|
2346
2451
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2365,14 +2470,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2365
2470
|
|
|
2366
2471
|
// src/extract/databases/index.ts
|
|
2367
2472
|
init_cjs_shims();
|
|
2368
|
-
var
|
|
2473
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2369
2474
|
var import_types7 = require("@neat.is/types");
|
|
2370
2475
|
|
|
2371
2476
|
// src/extract/databases/db-config-yaml.ts
|
|
2372
2477
|
init_cjs_shims();
|
|
2373
|
-
var
|
|
2478
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2374
2479
|
async function parse(serviceDir) {
|
|
2375
|
-
const yamlPath =
|
|
2480
|
+
const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
|
|
2376
2481
|
if (!await exists(yamlPath)) return [];
|
|
2377
2482
|
const raw = await readYaml(yamlPath);
|
|
2378
2483
|
return [
|
|
@@ -2390,13 +2495,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2390
2495
|
|
|
2391
2496
|
// src/extract/databases/dotenv.ts
|
|
2392
2497
|
init_cjs_shims();
|
|
2393
|
-
var
|
|
2394
|
-
var
|
|
2498
|
+
var import_node_fs10 = require("fs");
|
|
2499
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2395
2500
|
|
|
2396
2501
|
// src/extract/databases/shared.ts
|
|
2397
2502
|
init_cjs_shims();
|
|
2398
|
-
var
|
|
2399
|
-
var
|
|
2503
|
+
var import_node_fs9 = require("fs");
|
|
2504
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2400
2505
|
function schemeToEngine(scheme) {
|
|
2401
2506
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2402
2507
|
switch (s) {
|
|
@@ -2435,14 +2540,14 @@ function parseConnectionString(url) {
|
|
|
2435
2540
|
}
|
|
2436
2541
|
async function readIfExists(filePath) {
|
|
2437
2542
|
try {
|
|
2438
|
-
return await
|
|
2543
|
+
return await import_node_fs9.promises.readFile(filePath, "utf8");
|
|
2439
2544
|
} catch {
|
|
2440
2545
|
return null;
|
|
2441
2546
|
}
|
|
2442
2547
|
}
|
|
2443
2548
|
async function findFirst(serviceDir, candidates) {
|
|
2444
2549
|
for (const rel of candidates) {
|
|
2445
|
-
const abs =
|
|
2550
|
+
const abs = import_node_path10.default.join(serviceDir, rel);
|
|
2446
2551
|
const content = await readIfExists(abs);
|
|
2447
2552
|
if (content !== null) return abs;
|
|
2448
2553
|
}
|
|
@@ -2493,15 +2598,15 @@ function parseDotenvLine(line) {
|
|
|
2493
2598
|
return { key, value };
|
|
2494
2599
|
}
|
|
2495
2600
|
async function parse2(serviceDir) {
|
|
2496
|
-
const entries = await
|
|
2601
|
+
const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2497
2602
|
const configs = [];
|
|
2498
2603
|
const seen = /* @__PURE__ */ new Set();
|
|
2499
2604
|
for (const entry2 of entries) {
|
|
2500
2605
|
if (!entry2.isFile()) continue;
|
|
2501
2606
|
const match = isConfigFile(entry2.name);
|
|
2502
2607
|
if (!match.match || match.fileType !== "env") continue;
|
|
2503
|
-
const filePath =
|
|
2504
|
-
const content = await
|
|
2608
|
+
const filePath = import_node_path11.default.join(serviceDir, entry2.name);
|
|
2609
|
+
const content = await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2505
2610
|
for (const line of content.split("\n")) {
|
|
2506
2611
|
const parsed = parseDotenvLine(line);
|
|
2507
2612
|
if (!parsed) continue;
|
|
@@ -2520,9 +2625,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2520
2625
|
|
|
2521
2626
|
// src/extract/databases/prisma.ts
|
|
2522
2627
|
init_cjs_shims();
|
|
2523
|
-
var
|
|
2628
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2524
2629
|
async function parse3(serviceDir) {
|
|
2525
|
-
const schemaPath =
|
|
2630
|
+
const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2526
2631
|
const content = await readIfExists(schemaPath);
|
|
2527
2632
|
if (!content) return [];
|
|
2528
2633
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2654,10 +2759,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2654
2759
|
|
|
2655
2760
|
// src/extract/databases/ormconfig.ts
|
|
2656
2761
|
init_cjs_shims();
|
|
2657
|
-
var
|
|
2762
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2658
2763
|
async function parse6(serviceDir) {
|
|
2659
2764
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2660
|
-
const abs =
|
|
2765
|
+
const abs = import_node_path13.default.join(serviceDir, candidate);
|
|
2661
2766
|
if (!await exists(abs)) continue;
|
|
2662
2767
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2663
2768
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2717,9 +2822,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2717
2822
|
|
|
2718
2823
|
// src/extract/databases/sequelize.ts
|
|
2719
2824
|
init_cjs_shims();
|
|
2720
|
-
var
|
|
2825
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2721
2826
|
async function parse8(serviceDir) {
|
|
2722
|
-
const configPath =
|
|
2827
|
+
const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
|
|
2723
2828
|
if (!await exists(configPath)) return [];
|
|
2724
2829
|
const raw = await readJson(configPath);
|
|
2725
2830
|
const out = [];
|
|
@@ -2746,7 +2851,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
2746
2851
|
|
|
2747
2852
|
// src/extract/databases/docker-compose.ts
|
|
2748
2853
|
init_cjs_shims();
|
|
2749
|
-
var
|
|
2854
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2750
2855
|
function portFromService(svc) {
|
|
2751
2856
|
for (const raw of svc.ports ?? []) {
|
|
2752
2857
|
const str = String(raw);
|
|
@@ -2773,7 +2878,7 @@ function databaseFromEnv(svc) {
|
|
|
2773
2878
|
}
|
|
2774
2879
|
async function parse9(serviceDir) {
|
|
2775
2880
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2776
|
-
const abs =
|
|
2881
|
+
const abs = import_node_path15.default.join(serviceDir, name);
|
|
2777
2882
|
if (!await exists(abs)) continue;
|
|
2778
2883
|
const raw = await readYaml(abs);
|
|
2779
2884
|
if (!raw?.services) return [];
|
|
@@ -2953,7 +3058,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2953
3058
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
2954
3059
|
...config.sourceFile ? {
|
|
2955
3060
|
evidence: {
|
|
2956
|
-
file:
|
|
3061
|
+
file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
|
|
2957
3062
|
}
|
|
2958
3063
|
} : {}
|
|
2959
3064
|
};
|
|
@@ -2981,15 +3086,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2981
3086
|
|
|
2982
3087
|
// src/extract/configs.ts
|
|
2983
3088
|
init_cjs_shims();
|
|
2984
|
-
var
|
|
2985
|
-
var
|
|
3089
|
+
var import_node_fs11 = require("fs");
|
|
3090
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2986
3091
|
var import_types8 = require("@neat.is/types");
|
|
2987
3092
|
async function walkConfigFiles(dir) {
|
|
2988
3093
|
const out = [];
|
|
2989
3094
|
async function walk(current) {
|
|
2990
|
-
const entries = await
|
|
3095
|
+
const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
|
|
2991
3096
|
for (const entry2 of entries) {
|
|
2992
|
-
const full =
|
|
3097
|
+
const full = import_node_path17.default.join(current, entry2.name);
|
|
2993
3098
|
if (entry2.isDirectory()) {
|
|
2994
3099
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
2995
3100
|
} else if (entry2.isFile() && isConfigFile(entry2.name).match) {
|
|
@@ -3006,13 +3111,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3006
3111
|
for (const service of services) {
|
|
3007
3112
|
const configFiles = await walkConfigFiles(service.dir);
|
|
3008
3113
|
for (const file of configFiles) {
|
|
3009
|
-
const relPath =
|
|
3114
|
+
const relPath = import_node_path17.default.relative(scanPath, file);
|
|
3010
3115
|
const node = {
|
|
3011
3116
|
id: (0, import_types8.configId)(relPath),
|
|
3012
3117
|
type: import_types8.NodeType.ConfigNode,
|
|
3013
|
-
name:
|
|
3118
|
+
name: import_node_path17.default.basename(file),
|
|
3014
3119
|
path: relPath,
|
|
3015
|
-
fileType: isConfigFile(
|
|
3120
|
+
fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
|
|
3016
3121
|
};
|
|
3017
3122
|
if (!graph.hasNode(node.id)) {
|
|
3018
3123
|
graph.addNode(node.id, node);
|
|
@@ -3024,7 +3129,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3024
3129
|
target: node.id,
|
|
3025
3130
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
3026
3131
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
3027
|
-
evidence: { file: relPath.split(
|
|
3132
|
+
evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
|
|
3028
3133
|
};
|
|
3029
3134
|
if (!graph.hasEdge(edge.id)) {
|
|
3030
3135
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3041,7 +3146,7 @@ var import_types14 = require("@neat.is/types");
|
|
|
3041
3146
|
|
|
3042
3147
|
// src/extract/calls/http.ts
|
|
3043
3148
|
init_cjs_shims();
|
|
3044
|
-
var
|
|
3149
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3045
3150
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3046
3151
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3047
3152
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -3049,17 +3154,17 @@ var import_types9 = require("@neat.is/types");
|
|
|
3049
3154
|
|
|
3050
3155
|
// src/extract/calls/shared.ts
|
|
3051
3156
|
init_cjs_shims();
|
|
3052
|
-
var
|
|
3053
|
-
var
|
|
3157
|
+
var import_node_fs12 = require("fs");
|
|
3158
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
3054
3159
|
async function walkSourceFiles(dir) {
|
|
3055
3160
|
const out = [];
|
|
3056
3161
|
async function walk(current) {
|
|
3057
|
-
const entries = await
|
|
3162
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3058
3163
|
for (const entry2 of entries) {
|
|
3059
|
-
const full =
|
|
3164
|
+
const full = import_node_path18.default.join(current, entry2.name);
|
|
3060
3165
|
if (entry2.isDirectory()) {
|
|
3061
3166
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
3062
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3167
|
+
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
|
|
3063
3168
|
out.push(full);
|
|
3064
3169
|
}
|
|
3065
3170
|
}
|
|
@@ -3072,7 +3177,7 @@ async function loadSourceFiles(dir) {
|
|
|
3072
3177
|
const out = [];
|
|
3073
3178
|
for (const p of paths) {
|
|
3074
3179
|
try {
|
|
3075
|
-
const content = await
|
|
3180
|
+
const content = await import_node_fs12.promises.readFile(p, "utf8");
|
|
3076
3181
|
out.push({ path: p, content });
|
|
3077
3182
|
} catch {
|
|
3078
3183
|
}
|
|
@@ -3128,9 +3233,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3128
3233
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3129
3234
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3130
3235
|
for (const service of services) {
|
|
3131
|
-
knownHosts.add(
|
|
3236
|
+
knownHosts.add(import_node_path19.default.basename(service.dir));
|
|
3132
3237
|
knownHosts.add(service.pkg.name);
|
|
3133
|
-
hostToNodeId.set(
|
|
3238
|
+
hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
|
|
3134
3239
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3135
3240
|
}
|
|
3136
3241
|
let edgesAdded = 0;
|
|
@@ -3138,7 +3243,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3138
3243
|
const files = await loadSourceFiles(service.dir);
|
|
3139
3244
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3140
3245
|
for (const file of files) {
|
|
3141
|
-
const parser =
|
|
3246
|
+
const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3142
3247
|
let targets;
|
|
3143
3248
|
try {
|
|
3144
3249
|
targets = callsFromSource(file.content, parser, knownHosts);
|
|
@@ -3166,7 +3271,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3166
3271
|
type: import_types9.EdgeType.CALLS,
|
|
3167
3272
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3168
3273
|
evidence: {
|
|
3169
|
-
file:
|
|
3274
|
+
file: import_node_path19.default.relative(service.dir, evidenceFile.file),
|
|
3170
3275
|
line,
|
|
3171
3276
|
snippet: snippet(fileContent, line)
|
|
3172
3277
|
}
|
|
@@ -3182,7 +3287,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3182
3287
|
|
|
3183
3288
|
// src/extract/calls/kafka.ts
|
|
3184
3289
|
init_cjs_shims();
|
|
3185
|
-
var
|
|
3290
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3186
3291
|
var import_types10 = require("@neat.is/types");
|
|
3187
3292
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3188
3293
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3209,7 +3314,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3209
3314
|
kind: "kafka-topic",
|
|
3210
3315
|
edgeType,
|
|
3211
3316
|
evidence: {
|
|
3212
|
-
file:
|
|
3317
|
+
file: import_node_path20.default.relative(serviceDir, file.path),
|
|
3213
3318
|
line,
|
|
3214
3319
|
snippet: snippet(file.content, line)
|
|
3215
3320
|
}
|
|
@@ -3222,7 +3327,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3222
3327
|
|
|
3223
3328
|
// src/extract/calls/redis.ts
|
|
3224
3329
|
init_cjs_shims();
|
|
3225
|
-
var
|
|
3330
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3226
3331
|
var import_types11 = require("@neat.is/types");
|
|
3227
3332
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3228
3333
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3241,7 +3346,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3241
3346
|
kind: "redis",
|
|
3242
3347
|
edgeType: "CALLS",
|
|
3243
3348
|
evidence: {
|
|
3244
|
-
file:
|
|
3349
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3245
3350
|
line,
|
|
3246
3351
|
snippet: snippet(file.content, line)
|
|
3247
3352
|
}
|
|
@@ -3252,7 +3357,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3252
3357
|
|
|
3253
3358
|
// src/extract/calls/aws.ts
|
|
3254
3359
|
init_cjs_shims();
|
|
3255
|
-
var
|
|
3360
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3256
3361
|
var import_types12 = require("@neat.is/types");
|
|
3257
3362
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3258
3363
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3282,7 +3387,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3282
3387
|
kind,
|
|
3283
3388
|
edgeType: "CALLS",
|
|
3284
3389
|
evidence: {
|
|
3285
|
-
file:
|
|
3390
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3286
3391
|
line,
|
|
3287
3392
|
snippet: snippet(file.content, line)
|
|
3288
3393
|
}
|
|
@@ -3307,7 +3412,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3307
3412
|
|
|
3308
3413
|
// src/extract/calls/grpc.ts
|
|
3309
3414
|
init_cjs_shims();
|
|
3310
|
-
var
|
|
3415
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3311
3416
|
var import_types13 = require("@neat.is/types");
|
|
3312
3417
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3313
3418
|
function isLikelyAddress(value) {
|
|
@@ -3332,7 +3437,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3332
3437
|
kind: "grpc-service",
|
|
3333
3438
|
edgeType: "CALLS",
|
|
3334
3439
|
evidence: {
|
|
3335
|
-
file:
|
|
3440
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3336
3441
|
line,
|
|
3337
3442
|
snippet: snippet(file.content, line)
|
|
3338
3443
|
}
|
|
@@ -3412,7 +3517,7 @@ init_cjs_shims();
|
|
|
3412
3517
|
|
|
3413
3518
|
// src/extract/infra/docker-compose.ts
|
|
3414
3519
|
init_cjs_shims();
|
|
3415
|
-
var
|
|
3520
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3416
3521
|
var import_types16 = require("@neat.is/types");
|
|
3417
3522
|
|
|
3418
3523
|
// src/extract/infra/shared.ts
|
|
@@ -3450,7 +3555,7 @@ function dependsOnList(value) {
|
|
|
3450
3555
|
}
|
|
3451
3556
|
function serviceNameToServiceNode(name, services) {
|
|
3452
3557
|
for (const s of services) {
|
|
3453
|
-
if (s.node.name === name ||
|
|
3558
|
+
if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
|
|
3454
3559
|
}
|
|
3455
3560
|
return null;
|
|
3456
3561
|
}
|
|
@@ -3459,16 +3564,24 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3459
3564
|
let edgesAdded = 0;
|
|
3460
3565
|
let composePath = null;
|
|
3461
3566
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3462
|
-
const abs =
|
|
3567
|
+
const abs = import_node_path24.default.join(scanPath, name);
|
|
3463
3568
|
if (await exists(abs)) {
|
|
3464
3569
|
composePath = abs;
|
|
3465
3570
|
break;
|
|
3466
3571
|
}
|
|
3467
3572
|
}
|
|
3468
3573
|
if (!composePath) return { nodesAdded, edgesAdded };
|
|
3469
|
-
|
|
3574
|
+
let compose;
|
|
3575
|
+
try {
|
|
3576
|
+
compose = await readYaml(composePath);
|
|
3577
|
+
} catch (err) {
|
|
3578
|
+
console.warn(
|
|
3579
|
+
`[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
|
|
3580
|
+
);
|
|
3581
|
+
return { nodesAdded, edgesAdded };
|
|
3582
|
+
}
|
|
3470
3583
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3471
|
-
const evidenceFile =
|
|
3584
|
+
const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
|
|
3472
3585
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3473
3586
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3474
3587
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3509,8 +3622,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3509
3622
|
|
|
3510
3623
|
// src/extract/infra/dockerfile.ts
|
|
3511
3624
|
init_cjs_shims();
|
|
3512
|
-
var
|
|
3513
|
-
var
|
|
3625
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3626
|
+
var import_node_fs13 = require("fs");
|
|
3514
3627
|
var import_types17 = require("@neat.is/types");
|
|
3515
3628
|
function runtimeImage(content) {
|
|
3516
3629
|
const lines = content.split("\n");
|
|
@@ -3530,9 +3643,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3530
3643
|
let nodesAdded = 0;
|
|
3531
3644
|
let edgesAdded = 0;
|
|
3532
3645
|
for (const service of services) {
|
|
3533
|
-
const dockerfilePath =
|
|
3646
|
+
const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
|
|
3534
3647
|
if (!await exists(dockerfilePath)) continue;
|
|
3535
|
-
|
|
3648
|
+
let content;
|
|
3649
|
+
try {
|
|
3650
|
+
content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
|
|
3651
|
+
} catch (err) {
|
|
3652
|
+
console.warn(
|
|
3653
|
+
`[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
|
|
3654
|
+
);
|
|
3655
|
+
continue;
|
|
3656
|
+
}
|
|
3536
3657
|
const image = runtimeImage(content);
|
|
3537
3658
|
if (!image) continue;
|
|
3538
3659
|
const node = makeInfraNode("container-image", image);
|
|
@@ -3549,7 +3670,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3549
3670
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3550
3671
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3551
3672
|
evidence: {
|
|
3552
|
-
file:
|
|
3673
|
+
file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
|
|
3553
3674
|
}
|
|
3554
3675
|
};
|
|
3555
3676
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3561,19 +3682,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3561
3682
|
|
|
3562
3683
|
// src/extract/infra/terraform.ts
|
|
3563
3684
|
init_cjs_shims();
|
|
3564
|
-
var
|
|
3565
|
-
var
|
|
3685
|
+
var import_node_fs14 = require("fs");
|
|
3686
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3566
3687
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3567
3688
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3568
3689
|
if (depth > max) return [];
|
|
3569
3690
|
const out = [];
|
|
3570
|
-
const entries = await
|
|
3691
|
+
const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3571
3692
|
for (const entry2 of entries) {
|
|
3572
3693
|
if (entry2.isDirectory()) {
|
|
3573
3694
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
3574
|
-
out.push(...await walkTfFiles(
|
|
3695
|
+
out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
|
|
3575
3696
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
3576
|
-
out.push(
|
|
3697
|
+
out.push(import_node_path26.default.join(start, entry2.name));
|
|
3577
3698
|
}
|
|
3578
3699
|
}
|
|
3579
3700
|
return out;
|
|
@@ -3582,7 +3703,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3582
3703
|
let nodesAdded = 0;
|
|
3583
3704
|
const files = await walkTfFiles(scanPath);
|
|
3584
3705
|
for (const file of files) {
|
|
3585
|
-
const content = await
|
|
3706
|
+
const content = await import_node_fs14.promises.readFile(file, "utf8");
|
|
3586
3707
|
RESOURCE_RE.lastIndex = 0;
|
|
3587
3708
|
let m;
|
|
3588
3709
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3600,8 +3721,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3600
3721
|
|
|
3601
3722
|
// src/extract/infra/k8s.ts
|
|
3602
3723
|
init_cjs_shims();
|
|
3603
|
-
var
|
|
3604
|
-
var
|
|
3724
|
+
var import_node_fs15 = require("fs");
|
|
3725
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3605
3726
|
var import_yaml3 = require("yaml");
|
|
3606
3727
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3607
3728
|
Service: "k8s-service",
|
|
@@ -3615,13 +3736,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3615
3736
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3616
3737
|
if (depth > max) return [];
|
|
3617
3738
|
const out = [];
|
|
3618
|
-
const entries = await
|
|
3739
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3619
3740
|
for (const entry2 of entries) {
|
|
3620
3741
|
if (entry2.isDirectory()) {
|
|
3621
3742
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3622
|
-
out.push(...await walkYamlFiles2(
|
|
3623
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3624
|
-
out.push(
|
|
3743
|
+
out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
|
|
3744
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
|
|
3745
|
+
out.push(import_node_path27.default.join(start, entry2.name));
|
|
3625
3746
|
}
|
|
3626
3747
|
}
|
|
3627
3748
|
return out;
|
|
@@ -3630,7 +3751,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3630
3751
|
let nodesAdded = 0;
|
|
3631
3752
|
const files = await walkYamlFiles2(scanPath);
|
|
3632
3753
|
for (const file of files) {
|
|
3633
|
-
const content = await
|
|
3754
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3634
3755
|
let docs;
|
|
3635
3756
|
try {
|
|
3636
3757
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3696,8 +3817,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3696
3817
|
|
|
3697
3818
|
// src/persist.ts
|
|
3698
3819
|
init_cjs_shims();
|
|
3699
|
-
var
|
|
3700
|
-
var
|
|
3820
|
+
var import_node_fs16 = require("fs");
|
|
3821
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3701
3822
|
var SCHEMA_VERSION = 2;
|
|
3702
3823
|
function migrateV1ToV2(payload) {
|
|
3703
3824
|
const nodes = payload.graph.nodes;
|
|
@@ -3711,7 +3832,7 @@ function migrateV1ToV2(payload) {
|
|
|
3711
3832
|
return { ...payload, schemaVersion: 2 };
|
|
3712
3833
|
}
|
|
3713
3834
|
async function ensureDir(filePath) {
|
|
3714
|
-
await
|
|
3835
|
+
await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
|
|
3715
3836
|
}
|
|
3716
3837
|
async function saveGraphToDisk(graph, outPath) {
|
|
3717
3838
|
await ensureDir(outPath);
|
|
@@ -3721,13 +3842,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3721
3842
|
graph: graph.export()
|
|
3722
3843
|
};
|
|
3723
3844
|
const tmp = `${outPath}.tmp`;
|
|
3724
|
-
await
|
|
3725
|
-
await
|
|
3845
|
+
await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
3846
|
+
await import_node_fs16.promises.rename(tmp, outPath);
|
|
3726
3847
|
}
|
|
3727
3848
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3728
3849
|
let raw;
|
|
3729
3850
|
try {
|
|
3730
|
-
raw = await
|
|
3851
|
+
raw = await import_node_fs16.promises.readFile(outPath, "utf8");
|
|
3731
3852
|
} catch (err) {
|
|
3732
3853
|
if (err.code === "ENOENT") return;
|
|
3733
3854
|
throw err;
|
|
@@ -3780,18 +3901,255 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3780
3901
|
|
|
3781
3902
|
// src/watch.ts
|
|
3782
3903
|
init_cjs_shims();
|
|
3783
|
-
var
|
|
3904
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
3784
3905
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3785
3906
|
|
|
3786
3907
|
// src/api.ts
|
|
3787
3908
|
init_cjs_shims();
|
|
3788
3909
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3789
3910
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3790
|
-
var
|
|
3911
|
+
var import_types20 = require("@neat.is/types");
|
|
3912
|
+
|
|
3913
|
+
// src/divergences.ts
|
|
3914
|
+
init_cjs_shims();
|
|
3915
|
+
var import_types18 = require("@neat.is/types");
|
|
3916
|
+
function bucketKey(source, target, type) {
|
|
3917
|
+
return `${type}|${source}|${target}`;
|
|
3918
|
+
}
|
|
3919
|
+
function bucketEdges(graph) {
|
|
3920
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3921
|
+
graph.forEachEdge((id, attrs) => {
|
|
3922
|
+
const e = attrs;
|
|
3923
|
+
const parsed = (0, import_types18.parseEdgeId)(id);
|
|
3924
|
+
const provenance = parsed?.provenance ?? e.provenance;
|
|
3925
|
+
const key = bucketKey(e.source, e.target, e.type);
|
|
3926
|
+
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3927
|
+
switch (provenance) {
|
|
3928
|
+
case import_types18.Provenance.EXTRACTED:
|
|
3929
|
+
cur.extracted = e;
|
|
3930
|
+
break;
|
|
3931
|
+
case import_types18.Provenance.OBSERVED:
|
|
3932
|
+
cur.observed = e;
|
|
3933
|
+
break;
|
|
3934
|
+
case import_types18.Provenance.INFERRED:
|
|
3935
|
+
cur.inferred = e;
|
|
3936
|
+
break;
|
|
3937
|
+
case import_types18.Provenance.FRONTIER:
|
|
3938
|
+
cur.frontier = e;
|
|
3939
|
+
break;
|
|
3940
|
+
default:
|
|
3941
|
+
if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
|
|
3942
|
+
}
|
|
3943
|
+
buckets.set(key, cur);
|
|
3944
|
+
});
|
|
3945
|
+
return buckets;
|
|
3946
|
+
}
|
|
3947
|
+
function nodeIsFrontier(graph, nodeId) {
|
|
3948
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
3949
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
3950
|
+
return attrs.type === import_types18.NodeType.FrontierNode;
|
|
3951
|
+
}
|
|
3952
|
+
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3953
|
+
if (!graph.hasNode(sourceId)) return false;
|
|
3954
|
+
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3955
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
3956
|
+
if (e.provenance === import_types18.Provenance.OBSERVED) return true;
|
|
3957
|
+
}
|
|
3958
|
+
return false;
|
|
3959
|
+
}
|
|
3960
|
+
function clampConfidence(n) {
|
|
3961
|
+
if (!Number.isFinite(n)) return 0;
|
|
3962
|
+
return Math.max(0, Math.min(1, n));
|
|
3963
|
+
}
|
|
3964
|
+
function reasonForMissingObserved(source, target, type) {
|
|
3965
|
+
return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
|
|
3966
|
+
}
|
|
3967
|
+
function reasonForMissingExtracted(source, target, type) {
|
|
3968
|
+
return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
|
|
3969
|
+
}
|
|
3970
|
+
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
3971
|
+
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.";
|
|
3972
|
+
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
3973
|
+
function detectMissingDivergences(graph, bucket) {
|
|
3974
|
+
const out = [];
|
|
3975
|
+
if (bucket.extracted && !bucket.observed) {
|
|
3976
|
+
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
3977
|
+
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
3978
|
+
out.push({
|
|
3979
|
+
type: "missing-observed",
|
|
3980
|
+
source: bucket.source,
|
|
3981
|
+
target: bucket.target,
|
|
3982
|
+
edgeType: bucket.type,
|
|
3983
|
+
extracted: bucket.extracted,
|
|
3984
|
+
confidence: sourceHasTraffic ? 1 : 0.5,
|
|
3985
|
+
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
3986
|
+
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
if (bucket.observed && !bucket.extracted) {
|
|
3991
|
+
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
3992
|
+
out.push({
|
|
3993
|
+
type: "missing-extracted",
|
|
3994
|
+
source: bucket.source,
|
|
3995
|
+
target: bucket.target,
|
|
3996
|
+
edgeType: bucket.type,
|
|
3997
|
+
observed: bucket.observed,
|
|
3998
|
+
confidence: cascaded,
|
|
3999
|
+
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
4000
|
+
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
return out;
|
|
4004
|
+
}
|
|
4005
|
+
function declaredHostFor(svc) {
|
|
4006
|
+
const raw = svc.dbConnectionTarget?.trim();
|
|
4007
|
+
if (!raw) return null;
|
|
4008
|
+
const colon = raw.lastIndexOf(":");
|
|
4009
|
+
if (colon === -1) return raw;
|
|
4010
|
+
const port = raw.slice(colon + 1);
|
|
4011
|
+
if (/^\d+$/.test(port)) return raw.slice(0, colon);
|
|
4012
|
+
return raw;
|
|
4013
|
+
}
|
|
4014
|
+
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4015
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4016
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
4017
|
+
if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
|
|
4018
|
+
return true;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
return false;
|
|
4022
|
+
}
|
|
4023
|
+
function detectHostMismatch(graph, svcId, svc) {
|
|
4024
|
+
const declaredHost = declaredHostFor(svc);
|
|
4025
|
+
if (!declaredHost) return [];
|
|
4026
|
+
if (!hasExtractedConfiguredBy(graph, svcId)) return [];
|
|
4027
|
+
const out = [];
|
|
4028
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4029
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
4030
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
4031
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
4032
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
4033
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
4034
|
+
const observedHost = target.host?.trim();
|
|
4035
|
+
if (!observedHost) continue;
|
|
4036
|
+
if (observedHost === declaredHost) continue;
|
|
4037
|
+
out.push({
|
|
4038
|
+
type: "host-mismatch",
|
|
4039
|
+
source: svcId,
|
|
4040
|
+
target: edge.target,
|
|
4041
|
+
extractedHost: declaredHost,
|
|
4042
|
+
observedHost,
|
|
4043
|
+
confidence: clampConfidence(confidenceForEdge(edge)),
|
|
4044
|
+
reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
|
|
4045
|
+
recommendation: RECOMMENDATION_HOST_MISMATCH
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
return out;
|
|
4049
|
+
}
|
|
4050
|
+
function detectCompatDivergences(graph, svcId, svc) {
|
|
4051
|
+
const out = [];
|
|
4052
|
+
const deps = svc.dependencies ?? {};
|
|
4053
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4054
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
4055
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
4056
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
4057
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
4058
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
4059
|
+
for (const pair of compatPairs()) {
|
|
4060
|
+
if (pair.engine !== target.engine) continue;
|
|
4061
|
+
const declared = deps[pair.driver];
|
|
4062
|
+
if (!declared) continue;
|
|
4063
|
+
const result = checkCompatibility(
|
|
4064
|
+
pair.driver,
|
|
4065
|
+
declared,
|
|
4066
|
+
target.engine,
|
|
4067
|
+
target.engineVersion
|
|
4068
|
+
);
|
|
4069
|
+
if (!result.compatible && result.reason) {
|
|
4070
|
+
out.push({
|
|
4071
|
+
type: "version-mismatch",
|
|
4072
|
+
source: svcId,
|
|
4073
|
+
target: edge.target,
|
|
4074
|
+
extractedVersion: declared,
|
|
4075
|
+
observedVersion: target.engineVersion,
|
|
4076
|
+
compatibility: "incompatible",
|
|
4077
|
+
confidence: 1,
|
|
4078
|
+
reason: result.reason,
|
|
4079
|
+
recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
for (const rule of deprecatedApis()) {
|
|
4084
|
+
const declared = deps[rule.package];
|
|
4085
|
+
if (!declared) continue;
|
|
4086
|
+
const result = checkDeprecatedApi(rule, declared);
|
|
4087
|
+
if (!result.compatible && result.reason) {
|
|
4088
|
+
const ruleRef = {
|
|
4089
|
+
kind: rule.kind ?? "deprecated-api",
|
|
4090
|
+
reason: result.reason,
|
|
4091
|
+
package: rule.package
|
|
4092
|
+
};
|
|
4093
|
+
out.push({
|
|
4094
|
+
type: "compat-violation",
|
|
4095
|
+
source: svcId,
|
|
4096
|
+
target: edge.target,
|
|
4097
|
+
rule: ruleRef,
|
|
4098
|
+
observed: edge,
|
|
4099
|
+
confidence: 1,
|
|
4100
|
+
reason: result.reason,
|
|
4101
|
+
recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
return out;
|
|
4107
|
+
}
|
|
4108
|
+
function involvesNode(d, nodeId) {
|
|
4109
|
+
return d.source === nodeId || d.target === nodeId;
|
|
4110
|
+
}
|
|
4111
|
+
function computeDivergences(graph, opts = {}) {
|
|
4112
|
+
const all = [];
|
|
4113
|
+
const buckets = bucketEdges(graph);
|
|
4114
|
+
for (const bucket of buckets.values()) {
|
|
4115
|
+
for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
|
|
4116
|
+
}
|
|
4117
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
4118
|
+
const n = attrs;
|
|
4119
|
+
if (n.type !== import_types18.NodeType.ServiceNode) return;
|
|
4120
|
+
const svc = n;
|
|
4121
|
+
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4122
|
+
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
4123
|
+
});
|
|
4124
|
+
let filtered = all;
|
|
4125
|
+
if (opts.type) {
|
|
4126
|
+
const allowed = opts.type;
|
|
4127
|
+
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
4128
|
+
}
|
|
4129
|
+
if (opts.minConfidence !== void 0) {
|
|
4130
|
+
const threshold = opts.minConfidence;
|
|
4131
|
+
filtered = filtered.filter((d) => d.confidence >= threshold);
|
|
4132
|
+
}
|
|
4133
|
+
if (opts.node) {
|
|
4134
|
+
const target = opts.node;
|
|
4135
|
+
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4136
|
+
}
|
|
4137
|
+
filtered.sort((a, b) => {
|
|
4138
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4139
|
+
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4140
|
+
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4141
|
+
return a.target.localeCompare(b.target);
|
|
4142
|
+
});
|
|
4143
|
+
return import_types18.DivergenceResultSchema.parse({
|
|
4144
|
+
divergences: filtered,
|
|
4145
|
+
totalAffected: filtered.length,
|
|
4146
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
3791
4149
|
|
|
3792
4150
|
// src/diff.ts
|
|
3793
4151
|
init_cjs_shims();
|
|
3794
|
-
var
|
|
4152
|
+
var import_node_fs17 = require("fs");
|
|
3795
4153
|
async function loadSnapshotForDiff(target) {
|
|
3796
4154
|
if (/^https?:\/\//i.test(target)) {
|
|
3797
4155
|
const res = await fetch(target);
|
|
@@ -3800,7 +4158,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
3800
4158
|
}
|
|
3801
4159
|
return await res.json();
|
|
3802
4160
|
}
|
|
3803
|
-
const raw = await
|
|
4161
|
+
const raw = await import_node_fs17.promises.readFile(target, "utf8");
|
|
3804
4162
|
return JSON.parse(raw);
|
|
3805
4163
|
}
|
|
3806
4164
|
function indexEntries(entries) {
|
|
@@ -3868,23 +4226,23 @@ function canonicalJson(value) {
|
|
|
3868
4226
|
|
|
3869
4227
|
// src/projects.ts
|
|
3870
4228
|
init_cjs_shims();
|
|
3871
|
-
var
|
|
4229
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
3872
4230
|
function pathsForProject(project, baseDir) {
|
|
3873
4231
|
if (project === DEFAULT_PROJECT) {
|
|
3874
4232
|
return {
|
|
3875
|
-
snapshotPath:
|
|
3876
|
-
errorsPath:
|
|
3877
|
-
staleEventsPath:
|
|
3878
|
-
embeddingsCachePath:
|
|
3879
|
-
policyViolationsPath:
|
|
4233
|
+
snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
|
|
4234
|
+
errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
|
|
4235
|
+
staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
|
|
4236
|
+
embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
|
|
4237
|
+
policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
|
|
3880
4238
|
};
|
|
3881
4239
|
}
|
|
3882
4240
|
return {
|
|
3883
|
-
snapshotPath:
|
|
3884
|
-
errorsPath:
|
|
3885
|
-
staleEventsPath:
|
|
3886
|
-
embeddingsCachePath:
|
|
3887
|
-
policyViolationsPath:
|
|
4241
|
+
snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
|
|
4242
|
+
errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4243
|
+
staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4244
|
+
embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
|
|
4245
|
+
policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
3888
4246
|
};
|
|
3889
4247
|
}
|
|
3890
4248
|
var Projects = class {
|
|
@@ -3920,49 +4278,49 @@ var Projects = class {
|
|
|
3920
4278
|
|
|
3921
4279
|
// src/registry.ts
|
|
3922
4280
|
init_cjs_shims();
|
|
3923
|
-
var
|
|
4281
|
+
var import_node_fs18 = require("fs");
|
|
3924
4282
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
3925
|
-
var
|
|
3926
|
-
var
|
|
4283
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4284
|
+
var import_types19 = require("@neat.is/types");
|
|
3927
4285
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
3928
4286
|
var LOCK_RETRY_MS = 50;
|
|
3929
4287
|
function neatHome() {
|
|
3930
4288
|
const override = process.env.NEAT_HOME;
|
|
3931
|
-
if (override && override.length > 0) return
|
|
3932
|
-
return
|
|
4289
|
+
if (override && override.length > 0) return import_node_path30.default.resolve(override);
|
|
4290
|
+
return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
|
|
3933
4291
|
}
|
|
3934
4292
|
function registryPath() {
|
|
3935
|
-
return
|
|
4293
|
+
return import_node_path30.default.join(neatHome(), "projects.json");
|
|
3936
4294
|
}
|
|
3937
4295
|
function registryLockPath() {
|
|
3938
|
-
return
|
|
4296
|
+
return import_node_path30.default.join(neatHome(), "projects.json.lock");
|
|
3939
4297
|
}
|
|
3940
4298
|
async function normalizeProjectPath(input) {
|
|
3941
|
-
const resolved =
|
|
4299
|
+
const resolved = import_node_path30.default.resolve(input);
|
|
3942
4300
|
try {
|
|
3943
|
-
return await
|
|
4301
|
+
return await import_node_fs18.promises.realpath(resolved);
|
|
3944
4302
|
} catch {
|
|
3945
4303
|
return resolved;
|
|
3946
4304
|
}
|
|
3947
4305
|
}
|
|
3948
4306
|
async function writeAtomically(target, contents) {
|
|
3949
|
-
await
|
|
4307
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
|
|
3950
4308
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
3951
|
-
const fd = await
|
|
4309
|
+
const fd = await import_node_fs18.promises.open(tmp, "w");
|
|
3952
4310
|
try {
|
|
3953
4311
|
await fd.writeFile(contents, "utf8");
|
|
3954
4312
|
await fd.sync();
|
|
3955
4313
|
} finally {
|
|
3956
4314
|
await fd.close();
|
|
3957
4315
|
}
|
|
3958
|
-
await
|
|
4316
|
+
await import_node_fs18.promises.rename(tmp, target);
|
|
3959
4317
|
}
|
|
3960
4318
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
3961
4319
|
const deadline = Date.now() + timeoutMs;
|
|
3962
|
-
await
|
|
4320
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
|
|
3963
4321
|
while (true) {
|
|
3964
4322
|
try {
|
|
3965
|
-
const fd = await
|
|
4323
|
+
const fd = await import_node_fs18.promises.open(lockPath, "wx");
|
|
3966
4324
|
await fd.close();
|
|
3967
4325
|
return;
|
|
3968
4326
|
} catch (err) {
|
|
@@ -3978,7 +4336,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
3978
4336
|
}
|
|
3979
4337
|
}
|
|
3980
4338
|
async function releaseLock(lockPath) {
|
|
3981
|
-
await
|
|
4339
|
+
await import_node_fs18.promises.unlink(lockPath).catch(() => {
|
|
3982
4340
|
});
|
|
3983
4341
|
}
|
|
3984
4342
|
async function withLock(fn) {
|
|
@@ -3994,7 +4352,7 @@ async function readRegistry() {
|
|
|
3994
4352
|
const file = registryPath();
|
|
3995
4353
|
let raw;
|
|
3996
4354
|
try {
|
|
3997
|
-
raw = await
|
|
4355
|
+
raw = await import_node_fs18.promises.readFile(file, "utf8");
|
|
3998
4356
|
} catch (err) {
|
|
3999
4357
|
if (err.code === "ENOENT") {
|
|
4000
4358
|
return { version: 1, projects: [] };
|
|
@@ -4002,10 +4360,10 @@ async function readRegistry() {
|
|
|
4002
4360
|
throw err;
|
|
4003
4361
|
}
|
|
4004
4362
|
const parsed = JSON.parse(raw);
|
|
4005
|
-
return
|
|
4363
|
+
return import_types19.RegistryFileSchema.parse(parsed);
|
|
4006
4364
|
}
|
|
4007
4365
|
async function writeRegistry(reg) {
|
|
4008
|
-
const validated =
|
|
4366
|
+
const validated = import_types19.RegistryFileSchema.parse(reg);
|
|
4009
4367
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4010
4368
|
}
|
|
4011
4369
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4238,6 +4596,41 @@ function registerRoutes(scope, ctx) {
|
|
|
4238
4596
|
}
|
|
4239
4597
|
return getTransitiveDependencies(proj.graph, id, depth);
|
|
4240
4598
|
});
|
|
4599
|
+
scope.get("/graph/divergences", async (req, reply) => {
|
|
4600
|
+
const proj = resolveProject(registry, req, reply);
|
|
4601
|
+
if (!proj) return;
|
|
4602
|
+
let typeFilter;
|
|
4603
|
+
if (req.query.type) {
|
|
4604
|
+
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4605
|
+
const parsed = [];
|
|
4606
|
+
for (const c of candidates) {
|
|
4607
|
+
const r = import_types20.DivergenceTypeSchema.safeParse(c);
|
|
4608
|
+
if (!r.success) {
|
|
4609
|
+
return reply.code(400).send({
|
|
4610
|
+
error: `unknown divergence type "${c}"`,
|
|
4611
|
+
allowed: import_types20.DivergenceTypeSchema.options
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
parsed.push(r.data);
|
|
4615
|
+
}
|
|
4616
|
+
typeFilter = new Set(parsed);
|
|
4617
|
+
}
|
|
4618
|
+
let minConfidence;
|
|
4619
|
+
if (req.query.minConfidence !== void 0) {
|
|
4620
|
+
const n = Number(req.query.minConfidence);
|
|
4621
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
4622
|
+
return reply.code(400).send({
|
|
4623
|
+
error: "minConfidence must be a number in [0, 1]"
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
minConfidence = n;
|
|
4627
|
+
}
|
|
4628
|
+
return computeDivergences(proj.graph, {
|
|
4629
|
+
...typeFilter ? { type: typeFilter } : {},
|
|
4630
|
+
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
4631
|
+
...req.query.node ? { node: req.query.node } : {}
|
|
4632
|
+
});
|
|
4633
|
+
});
|
|
4241
4634
|
scope.get("/incidents", async (req, reply) => {
|
|
4242
4635
|
const proj = resolveProject(registry, req, reply);
|
|
4243
4636
|
if (!proj) return;
|
|
@@ -4391,7 +4784,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4391
4784
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4392
4785
|
let violations = await log.readAll();
|
|
4393
4786
|
if (req.query.severity) {
|
|
4394
|
-
const sev =
|
|
4787
|
+
const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4395
4788
|
if (!sev.success) {
|
|
4396
4789
|
return reply.code(400).send({
|
|
4397
4790
|
error: "invalid severity",
|
|
@@ -4408,7 +4801,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4408
4801
|
scope.post("/policies/check", async (req, reply) => {
|
|
4409
4802
|
const proj = resolveProject(registry, req, reply);
|
|
4410
4803
|
if (!proj) return;
|
|
4411
|
-
const parsed =
|
|
4804
|
+
const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4412
4805
|
if (!parsed.success) {
|
|
4413
4806
|
return reply.code(400).send({
|
|
4414
4807
|
error: "invalid /policies/check body",
|
|
@@ -4494,13 +4887,13 @@ async function buildApi(opts) {
|
|
|
4494
4887
|
|
|
4495
4888
|
// src/extract/retire.ts
|
|
4496
4889
|
init_cjs_shims();
|
|
4497
|
-
var
|
|
4890
|
+
var import_types21 = require("@neat.is/types");
|
|
4498
4891
|
function retireEdgesByFile(graph, file) {
|
|
4499
4892
|
const normalized = file.split("\\").join("/");
|
|
4500
4893
|
const toDrop = [];
|
|
4501
4894
|
graph.forEachEdge((id, attrs) => {
|
|
4502
4895
|
const edge = attrs;
|
|
4503
|
-
if (edge.provenance !==
|
|
4896
|
+
if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
|
|
4504
4897
|
if (!edge.evidence?.file) return;
|
|
4505
4898
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4506
4899
|
});
|
|
@@ -4514,8 +4907,8 @@ init_otel_grpc();
|
|
|
4514
4907
|
|
|
4515
4908
|
// src/search.ts
|
|
4516
4909
|
init_cjs_shims();
|
|
4517
|
-
var
|
|
4518
|
-
var
|
|
4910
|
+
var import_node_fs19 = require("fs");
|
|
4911
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4519
4912
|
var import_node_crypto = require("crypto");
|
|
4520
4913
|
var DEFAULT_LIMIT = 10;
|
|
4521
4914
|
var NOMIC_DIM = 768;
|
|
@@ -4645,7 +5038,7 @@ async function pickEmbedder() {
|
|
|
4645
5038
|
}
|
|
4646
5039
|
async function readCache(cachePath) {
|
|
4647
5040
|
try {
|
|
4648
|
-
const raw = await
|
|
5041
|
+
const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
|
|
4649
5042
|
const parsed = JSON.parse(raw);
|
|
4650
5043
|
if (parsed.version !== 1) return null;
|
|
4651
5044
|
return parsed;
|
|
@@ -4654,8 +5047,8 @@ async function readCache(cachePath) {
|
|
|
4654
5047
|
}
|
|
4655
5048
|
}
|
|
4656
5049
|
async function writeCache(cachePath, cache) {
|
|
4657
|
-
await
|
|
4658
|
-
await
|
|
5050
|
+
await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
|
|
5051
|
+
await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
4659
5052
|
}
|
|
4660
5053
|
var VectorIndex = class {
|
|
4661
5054
|
constructor(embedder, cachePath) {
|
|
@@ -4810,8 +5203,8 @@ var ALL_PHASES = [
|
|
|
4810
5203
|
];
|
|
4811
5204
|
function classifyChange(relPath) {
|
|
4812
5205
|
const phases = /* @__PURE__ */ new Set();
|
|
4813
|
-
const base =
|
|
4814
|
-
const segments = relPath.split(
|
|
5206
|
+
const base = import_node_path34.default.basename(relPath).toLowerCase();
|
|
5207
|
+
const segments = relPath.split(import_node_path34.default.sep).map((s) => s.toLowerCase());
|
|
4815
5208
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
4816
5209
|
phases.add("services");
|
|
4817
5210
|
phases.add("aliases");
|
|
@@ -4894,8 +5287,8 @@ async function startWatch(graph, opts) {
|
|
|
4894
5287
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
4895
5288
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
4896
5289
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
4897
|
-
const policyFilePath =
|
|
4898
|
-
const policyViolationsPath =
|
|
5290
|
+
const policyFilePath = import_node_path34.default.join(opts.scanPath, "policy.json");
|
|
5291
|
+
const policyViolationsPath = import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
4899
5292
|
let policies = [];
|
|
4900
5293
|
try {
|
|
4901
5294
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -4944,7 +5337,7 @@ async function startWatch(graph, opts) {
|
|
|
4944
5337
|
const host = opts.host ?? "0.0.0.0";
|
|
4945
5338
|
const port = opts.port ?? 8080;
|
|
4946
5339
|
const otelPort = opts.otelPort ?? 4318;
|
|
4947
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
5340
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "embeddings.json");
|
|
4948
5341
|
let searchIndex;
|
|
4949
5342
|
try {
|
|
4950
5343
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -4962,7 +5355,7 @@ async function startWatch(graph, opts) {
|
|
|
4962
5355
|
// Paths are derived from the explicit options the watch caller passes
|
|
4963
5356
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
4964
5357
|
// fields so the registry shape is complete.
|
|
4965
|
-
...pathsForProject(projectName,
|
|
5358
|
+
...pathsForProject(projectName, import_node_path34.default.dirname(opts.outPath)),
|
|
4966
5359
|
snapshotPath: opts.outPath,
|
|
4967
5360
|
errorsPath: opts.errorsPath,
|
|
4968
5361
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -5047,9 +5440,9 @@ async function startWatch(graph, opts) {
|
|
|
5047
5440
|
};
|
|
5048
5441
|
const onPath = (absPath) => {
|
|
5049
5442
|
if (shouldIgnore(absPath)) return;
|
|
5050
|
-
const rel =
|
|
5443
|
+
const rel = import_node_path34.default.relative(opts.scanPath, absPath);
|
|
5051
5444
|
if (!rel || rel.startsWith("..")) return;
|
|
5052
|
-
pendingPaths.add(rel.split(
|
|
5445
|
+
pendingPaths.add(rel.split(import_node_path34.default.sep).join("/"));
|
|
5053
5446
|
const phases = classifyChange(rel);
|
|
5054
5447
|
if (phases.size === 0) {
|
|
5055
5448
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -5097,8 +5490,8 @@ init_cjs_shims();
|
|
|
5097
5490
|
|
|
5098
5491
|
// src/installers/javascript.ts
|
|
5099
5492
|
init_cjs_shims();
|
|
5100
|
-
var
|
|
5101
|
-
var
|
|
5493
|
+
var import_node_fs20 = require("fs");
|
|
5494
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
5102
5495
|
var SDK_PACKAGES = [
|
|
5103
5496
|
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
5104
5497
|
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
@@ -5114,7 +5507,7 @@ var OTEL_ENV = {
|
|
|
5114
5507
|
};
|
|
5115
5508
|
async function readPackageJson(serviceDir) {
|
|
5116
5509
|
try {
|
|
5117
|
-
const raw = await
|
|
5510
|
+
const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(serviceDir, "package.json"), "utf8");
|
|
5118
5511
|
return JSON.parse(raw);
|
|
5119
5512
|
} catch {
|
|
5120
5513
|
return null;
|
|
@@ -5133,7 +5526,7 @@ function rewriteStartScript(start) {
|
|
|
5133
5526
|
}
|
|
5134
5527
|
async function plan(serviceDir) {
|
|
5135
5528
|
const pkg = await readPackageJson(serviceDir);
|
|
5136
|
-
const manifestPath =
|
|
5529
|
+
const manifestPath = import_node_path35.default.join(serviceDir, "package.json");
|
|
5137
5530
|
const empty = {
|
|
5138
5531
|
language: "javascript",
|
|
5139
5532
|
serviceDir,
|
|
@@ -5180,7 +5573,7 @@ async function apply(installPlan) {
|
|
|
5180
5573
|
const originals = /* @__PURE__ */ new Map();
|
|
5181
5574
|
for (const file of touched) {
|
|
5182
5575
|
try {
|
|
5183
|
-
originals.set(file, await
|
|
5576
|
+
originals.set(file, await import_node_fs20.promises.readFile(file, "utf8"));
|
|
5184
5577
|
} catch {
|
|
5185
5578
|
}
|
|
5186
5579
|
}
|
|
@@ -5206,8 +5599,8 @@ async function apply(installPlan) {
|
|
|
5206
5599
|
}
|
|
5207
5600
|
const newRaw = JSON.stringify(pkg, null, 2) + "\n";
|
|
5208
5601
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
5209
|
-
await
|
|
5210
|
-
await
|
|
5602
|
+
await import_node_fs20.promises.writeFile(tmp, newRaw, "utf8");
|
|
5603
|
+
await import_node_fs20.promises.rename(tmp, file);
|
|
5211
5604
|
}
|
|
5212
5605
|
} catch (err) {
|
|
5213
5606
|
await rollback(installPlan, originals);
|
|
@@ -5218,7 +5611,7 @@ async function rollback(installPlan, originals) {
|
|
|
5218
5611
|
const restored = [];
|
|
5219
5612
|
for (const [file, raw] of originals.entries()) {
|
|
5220
5613
|
try {
|
|
5221
|
-
await
|
|
5614
|
+
await import_node_fs20.promises.writeFile(file, raw, "utf8");
|
|
5222
5615
|
restored.push(file);
|
|
5223
5616
|
} catch {
|
|
5224
5617
|
}
|
|
@@ -5232,8 +5625,8 @@ async function rollback(installPlan, originals) {
|
|
|
5232
5625
|
...restored.map((f) => `restored: ${f}`),
|
|
5233
5626
|
""
|
|
5234
5627
|
];
|
|
5235
|
-
const rollbackPath =
|
|
5236
|
-
await
|
|
5628
|
+
const rollbackPath = import_node_path35.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
5629
|
+
await import_node_fs20.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
5237
5630
|
}
|
|
5238
5631
|
var javascriptInstaller = {
|
|
5239
5632
|
name: "javascript",
|
|
@@ -5244,8 +5637,8 @@ var javascriptInstaller = {
|
|
|
5244
5637
|
|
|
5245
5638
|
// src/installers/python.ts
|
|
5246
5639
|
init_cjs_shims();
|
|
5247
|
-
var
|
|
5248
|
-
var
|
|
5640
|
+
var import_node_fs21 = require("fs");
|
|
5641
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5249
5642
|
var SDK_PACKAGES2 = [
|
|
5250
5643
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
5251
5644
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -5257,7 +5650,7 @@ var OTEL_ENV2 = {
|
|
|
5257
5650
|
};
|
|
5258
5651
|
async function exists2(p) {
|
|
5259
5652
|
try {
|
|
5260
|
-
await
|
|
5653
|
+
await import_node_fs21.promises.stat(p);
|
|
5261
5654
|
return true;
|
|
5262
5655
|
} catch {
|
|
5263
5656
|
return false;
|
|
@@ -5266,7 +5659,7 @@ async function exists2(p) {
|
|
|
5266
5659
|
async function detect2(serviceDir) {
|
|
5267
5660
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
5268
5661
|
for (const m of markers) {
|
|
5269
|
-
if (await exists2(
|
|
5662
|
+
if (await exists2(import_node_path36.default.join(serviceDir, m))) return true;
|
|
5270
5663
|
}
|
|
5271
5664
|
return false;
|
|
5272
5665
|
}
|
|
@@ -5276,9 +5669,9 @@ function reqPackageName(line) {
|
|
|
5276
5669
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
5277
5670
|
}
|
|
5278
5671
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
5279
|
-
const file =
|
|
5672
|
+
const file = import_node_path36.default.join(serviceDir, "requirements.txt");
|
|
5280
5673
|
if (!await exists2(file)) return null;
|
|
5281
|
-
const raw = await
|
|
5674
|
+
const raw = await import_node_fs21.promises.readFile(file, "utf8");
|
|
5282
5675
|
const presentNames = new Set(
|
|
5283
5676
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
5284
5677
|
);
|
|
@@ -5286,9 +5679,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
5286
5679
|
return { manifest: file, missing: [...missing] };
|
|
5287
5680
|
}
|
|
5288
5681
|
async function planProcfileEdits(serviceDir) {
|
|
5289
|
-
const procfile =
|
|
5682
|
+
const procfile = import_node_path36.default.join(serviceDir, "Procfile");
|
|
5290
5683
|
if (!await exists2(procfile)) return [];
|
|
5291
|
-
const raw = await
|
|
5684
|
+
const raw = await import_node_fs21.promises.readFile(procfile, "utf8");
|
|
5292
5685
|
const edits = [];
|
|
5293
5686
|
for (const line of raw.split(/\r?\n/)) {
|
|
5294
5687
|
if (line.length === 0) continue;
|
|
@@ -5340,8 +5733,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
5340
5733
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
5341
5734
|
`;
|
|
5342
5735
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
5343
|
-
await
|
|
5344
|
-
await
|
|
5736
|
+
await import_node_fs21.promises.writeFile(tmp, next, "utf8");
|
|
5737
|
+
await import_node_fs21.promises.rename(tmp, manifest);
|
|
5345
5738
|
}
|
|
5346
5739
|
async function applyProcfile(procfile, edits, original) {
|
|
5347
5740
|
let next = original;
|
|
@@ -5350,8 +5743,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
5350
5743
|
next = next.replace(e.before, e.after);
|
|
5351
5744
|
}
|
|
5352
5745
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
5353
|
-
await
|
|
5354
|
-
await
|
|
5746
|
+
await import_node_fs21.promises.writeFile(tmp, next, "utf8");
|
|
5747
|
+
await import_node_fs21.promises.rename(tmp, procfile);
|
|
5355
5748
|
}
|
|
5356
5749
|
async function apply2(installPlan) {
|
|
5357
5750
|
const touched = /* @__PURE__ */ new Set();
|
|
@@ -5361,7 +5754,7 @@ async function apply2(installPlan) {
|
|
|
5361
5754
|
const originals = /* @__PURE__ */ new Map();
|
|
5362
5755
|
for (const file of touched) {
|
|
5363
5756
|
try {
|
|
5364
|
-
originals.set(file, await
|
|
5757
|
+
originals.set(file, await import_node_fs21.promises.readFile(file, "utf8"));
|
|
5365
5758
|
} catch {
|
|
5366
5759
|
}
|
|
5367
5760
|
}
|
|
@@ -5371,7 +5764,7 @@ async function apply2(installPlan) {
|
|
|
5371
5764
|
if (raw === void 0) {
|
|
5372
5765
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
5373
5766
|
}
|
|
5374
|
-
const base =
|
|
5767
|
+
const base = import_node_path36.default.basename(file);
|
|
5375
5768
|
if (base === "requirements.txt") {
|
|
5376
5769
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
5377
5770
|
if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
|
|
@@ -5389,7 +5782,7 @@ async function rollback2(installPlan, originals) {
|
|
|
5389
5782
|
const restored = [];
|
|
5390
5783
|
for (const [file, raw] of originals.entries()) {
|
|
5391
5784
|
try {
|
|
5392
|
-
await
|
|
5785
|
+
await import_node_fs21.promises.writeFile(file, raw, "utf8");
|
|
5393
5786
|
restored.push(file);
|
|
5394
5787
|
} catch {
|
|
5395
5788
|
}
|
|
@@ -5403,8 +5796,8 @@ async function rollback2(installPlan, originals) {
|
|
|
5403
5796
|
...restored.map((f) => `restored: ${f}`),
|
|
5404
5797
|
""
|
|
5405
5798
|
];
|
|
5406
|
-
const rollbackPath =
|
|
5407
|
-
await
|
|
5799
|
+
const rollbackPath = import_node_path36.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
5800
|
+
await import_node_fs21.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
5408
5801
|
}
|
|
5409
5802
|
var pythonInstaller = {
|
|
5410
5803
|
name: "python",
|
|
@@ -5492,9 +5885,12 @@ function renderPatch(sections) {
|
|
|
5492
5885
|
return lines.join("\n");
|
|
5493
5886
|
}
|
|
5494
5887
|
|
|
5888
|
+
// src/cli.ts
|
|
5889
|
+
var import_types23 = require("@neat.is/types");
|
|
5890
|
+
|
|
5495
5891
|
// src/cli-client.ts
|
|
5496
5892
|
init_cjs_shims();
|
|
5497
|
-
var
|
|
5893
|
+
var import_types22 = require("@neat.is/types");
|
|
5498
5894
|
var HttpError = class extends Error {
|
|
5499
5895
|
constructor(status2, message, responseBody = "") {
|
|
5500
5896
|
super(message);
|
|
@@ -5514,10 +5910,10 @@ var TransportError = class extends Error {
|
|
|
5514
5910
|
function createHttpClient(baseUrl) {
|
|
5515
5911
|
const root = baseUrl.replace(/\/$/, "");
|
|
5516
5912
|
return {
|
|
5517
|
-
async get(
|
|
5913
|
+
async get(path38) {
|
|
5518
5914
|
let res;
|
|
5519
5915
|
try {
|
|
5520
|
-
res = await fetch(`${root}${
|
|
5916
|
+
res = await fetch(`${root}${path38}`);
|
|
5521
5917
|
} catch (err) {
|
|
5522
5918
|
throw new TransportError(
|
|
5523
5919
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -5527,16 +5923,16 @@ function createHttpClient(baseUrl) {
|
|
|
5527
5923
|
const body = await res.text().catch(() => "");
|
|
5528
5924
|
throw new HttpError(
|
|
5529
5925
|
res.status,
|
|
5530
|
-
`${res.status} ${res.statusText} on GET ${
|
|
5926
|
+
`${res.status} ${res.statusText} on GET ${path38}: ${body}`,
|
|
5531
5927
|
body
|
|
5532
5928
|
);
|
|
5533
5929
|
}
|
|
5534
5930
|
return await res.json();
|
|
5535
5931
|
},
|
|
5536
|
-
async post(
|
|
5932
|
+
async post(path38, body) {
|
|
5537
5933
|
let res;
|
|
5538
5934
|
try {
|
|
5539
|
-
res = await fetch(`${root}${
|
|
5935
|
+
res = await fetch(`${root}${path38}`, {
|
|
5540
5936
|
method: "POST",
|
|
5541
5937
|
headers: { "content-type": "application/json" },
|
|
5542
5938
|
body: JSON.stringify(body)
|
|
@@ -5550,7 +5946,7 @@ function createHttpClient(baseUrl) {
|
|
|
5550
5946
|
const text = await res.text().catch(() => "");
|
|
5551
5947
|
throw new HttpError(
|
|
5552
5948
|
res.status,
|
|
5553
|
-
`${res.status} ${res.statusText} on POST ${
|
|
5949
|
+
`${res.status} ${res.statusText} on POST ${path38}: ${text}`,
|
|
5554
5950
|
text
|
|
5555
5951
|
);
|
|
5556
5952
|
}
|
|
@@ -5564,12 +5960,12 @@ function projectPath(project, suffix) {
|
|
|
5564
5960
|
}
|
|
5565
5961
|
async function runRootCause(client, input) {
|
|
5566
5962
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5567
|
-
const
|
|
5963
|
+
const path38 = projectPath(
|
|
5568
5964
|
input.project,
|
|
5569
5965
|
`/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5570
5966
|
);
|
|
5571
5967
|
try {
|
|
5572
|
-
const result = await client.get(
|
|
5968
|
+
const result = await client.get(path38);
|
|
5573
5969
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5574
5970
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5575
5971
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -5595,12 +5991,12 @@ async function runRootCause(client, input) {
|
|
|
5595
5991
|
}
|
|
5596
5992
|
async function runBlastRadius(client, input) {
|
|
5597
5993
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
5598
|
-
const
|
|
5994
|
+
const path38 = projectPath(
|
|
5599
5995
|
input.project,
|
|
5600
5996
|
`/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
5601
5997
|
);
|
|
5602
5998
|
try {
|
|
5603
|
-
const result = await client.get(
|
|
5999
|
+
const result = await client.get(path38);
|
|
5604
6000
|
if (result.totalAffected === 0) {
|
|
5605
6001
|
return {
|
|
5606
6002
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -5629,17 +6025,17 @@ async function runBlastRadius(client, input) {
|
|
|
5629
6025
|
}
|
|
5630
6026
|
}
|
|
5631
6027
|
function formatBlastEntry(n) {
|
|
5632
|
-
const tag = n.edgeProvenance ===
|
|
6028
|
+
const tag = n.edgeProvenance === import_types22.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
5633
6029
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
5634
6030
|
}
|
|
5635
6031
|
async function runDependencies(client, input) {
|
|
5636
6032
|
const depth = input.depth ?? 3;
|
|
5637
|
-
const
|
|
6033
|
+
const path38 = projectPath(
|
|
5638
6034
|
input.project,
|
|
5639
6035
|
`/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
|
|
5640
6036
|
);
|
|
5641
6037
|
try {
|
|
5642
|
-
const result = await client.get(
|
|
6038
|
+
const result = await client.get(path38);
|
|
5643
6039
|
if (result.total === 0) {
|
|
5644
6040
|
return {
|
|
5645
6041
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -5675,9 +6071,9 @@ async function runObservedDependencies(client, input) {
|
|
|
5675
6071
|
const edges = await client.get(
|
|
5676
6072
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
5677
6073
|
);
|
|
5678
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
6074
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types22.Provenance.OBSERVED);
|
|
5679
6075
|
if (observed.length === 0) {
|
|
5680
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
6076
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types22.Provenance.EXTRACTED);
|
|
5681
6077
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
5682
6078
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
5683
6079
|
}
|
|
@@ -5685,7 +6081,7 @@ async function runObservedDependencies(client, input) {
|
|
|
5685
6081
|
return {
|
|
5686
6082
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5687
6083
|
block: blockLines.join("\n"),
|
|
5688
|
-
provenance:
|
|
6084
|
+
provenance: import_types22.Provenance.OBSERVED
|
|
5689
6085
|
};
|
|
5690
6086
|
} catch (err) {
|
|
5691
6087
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -5720,9 +6116,9 @@ function formatDuration(ms) {
|
|
|
5720
6116
|
return `${Math.round(h / 24)}d`;
|
|
5721
6117
|
}
|
|
5722
6118
|
async function runIncidents(client, input) {
|
|
5723
|
-
const
|
|
6119
|
+
const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5724
6120
|
try {
|
|
5725
|
-
const events = await client.get(
|
|
6121
|
+
const events = await client.get(path38);
|
|
5726
6122
|
if (events.length === 0) {
|
|
5727
6123
|
return {
|
|
5728
6124
|
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
@@ -5738,7 +6134,7 @@ async function runIncidents(client, input) {
|
|
|
5738
6134
|
return {
|
|
5739
6135
|
summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
5740
6136
|
block: blockLines.join("\n"),
|
|
5741
|
-
provenance:
|
|
6137
|
+
provenance: import_types22.Provenance.OBSERVED
|
|
5742
6138
|
};
|
|
5743
6139
|
} catch (err) {
|
|
5744
6140
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -5846,7 +6242,7 @@ async function runStaleEdges(client, input) {
|
|
|
5846
6242
|
return {
|
|
5847
6243
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
5848
6244
|
block: blockLines.join("\n"),
|
|
5849
|
-
provenance:
|
|
6245
|
+
provenance: import_types22.Provenance.STALE
|
|
5850
6246
|
};
|
|
5851
6247
|
}
|
|
5852
6248
|
async function runPolicies(client, input) {
|
|
@@ -5909,6 +6305,54 @@ async function runPolicies(client, input) {
|
|
|
5909
6305
|
provenance: severities.join(" ")
|
|
5910
6306
|
};
|
|
5911
6307
|
}
|
|
6308
|
+
function formatDivergenceLine(d) {
|
|
6309
|
+
switch (d.type) {
|
|
6310
|
+
case "missing-observed":
|
|
6311
|
+
case "missing-extracted":
|
|
6312
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
6313
|
+
case "version-mismatch":
|
|
6314
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
|
|
6315
|
+
case "host-mismatch":
|
|
6316
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
6317
|
+
case "compat-violation":
|
|
6318
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
async function runDivergences(client, input) {
|
|
6322
|
+
const params = new URLSearchParams();
|
|
6323
|
+
if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
|
|
6324
|
+
if (input.minConfidence !== void 0) {
|
|
6325
|
+
params.set("minConfidence", String(input.minConfidence));
|
|
6326
|
+
}
|
|
6327
|
+
if (input.node) params.set("node", input.node);
|
|
6328
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
6329
|
+
const result = await client.get(
|
|
6330
|
+
projectPath(input.project, `/graph/divergences${qs}`)
|
|
6331
|
+
);
|
|
6332
|
+
if (result.totalAffected === 0) {
|
|
6333
|
+
return {
|
|
6334
|
+
summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
|
|
6335
|
+
};
|
|
6336
|
+
}
|
|
6337
|
+
const headline = result.divergences[0];
|
|
6338
|
+
const summary = `Found ${result.totalAffected} divergence${result.totalAffected === 1 ? "" : "s"} between code and production. Highest-confidence: ${headline.type} on ${headline.source} \u2192 ${headline.target}. ${headline.reason}`;
|
|
6339
|
+
const blockLines = [];
|
|
6340
|
+
for (const d of result.divergences) {
|
|
6341
|
+
blockLines.push(formatDivergenceLine(d));
|
|
6342
|
+
blockLines.push(` reason: ${d.reason}`);
|
|
6343
|
+
blockLines.push(` recommendation: ${d.recommendation}`);
|
|
6344
|
+
}
|
|
6345
|
+
const maxConfidence = result.divergences.reduce(
|
|
6346
|
+
(m, d) => Math.max(m, d.confidence),
|
|
6347
|
+
0
|
|
6348
|
+
);
|
|
6349
|
+
return {
|
|
6350
|
+
summary,
|
|
6351
|
+
block: blockLines.join("\n"),
|
|
6352
|
+
confidence: maxConfidence,
|
|
6353
|
+
provenance: "composite (EXTRACTED + OBSERVED)"
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
5912
6356
|
function formatFooter(confidence, provenance) {
|
|
5913
6357
|
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
5914
6358
|
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
@@ -5987,6 +6431,9 @@ function usage() {
|
|
|
5987
6431
|
console.log(" policies Current policy violations.");
|
|
5988
6432
|
console.log(" Flags: --node <id>, --hypothetical-action <json>");
|
|
5989
6433
|
console.log(" example: neat policies --node service:<name> --json");
|
|
6434
|
+
console.log(" divergences Where code (EXTRACTED) and production (OBSERVED) disagree.");
|
|
6435
|
+
console.log(" Flags: --type <list>, --min-confidence <0..1>, --node <id>");
|
|
6436
|
+
console.log(" example: neat divergences --min-confidence 0.7");
|
|
5990
6437
|
console.log("");
|
|
5991
6438
|
console.log("flags:");
|
|
5992
6439
|
console.log(' --project <name> Name the project this command targets. Default: "default".');
|
|
@@ -6011,7 +6458,9 @@ var STRING_FLAGS = [
|
|
|
6011
6458
|
["--since", "since"],
|
|
6012
6459
|
["--against", "against"],
|
|
6013
6460
|
["--error-id", "errorId"],
|
|
6014
|
-
["--hypothetical-action", "hypotheticalAction"]
|
|
6461
|
+
["--hypothetical-action", "hypotheticalAction"],
|
|
6462
|
+
["--type", "type"],
|
|
6463
|
+
["--min-confidence", "minConfidence"]
|
|
6015
6464
|
];
|
|
6016
6465
|
function parseArgs(rest) {
|
|
6017
6466
|
const positional = [];
|
|
@@ -6030,6 +6479,8 @@ function parseArgs(rest) {
|
|
|
6030
6479
|
against: null,
|
|
6031
6480
|
errorId: null,
|
|
6032
6481
|
hypotheticalAction: null,
|
|
6482
|
+
type: null,
|
|
6483
|
+
minConfidence: null,
|
|
6033
6484
|
positional: []
|
|
6034
6485
|
};
|
|
6035
6486
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -6089,6 +6540,15 @@ function assignFlag(out, field, value) {
|
|
|
6089
6540
|
out[field] = n;
|
|
6090
6541
|
return;
|
|
6091
6542
|
}
|
|
6543
|
+
if (field === "minConfidence") {
|
|
6544
|
+
const n = Number(value);
|
|
6545
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
6546
|
+
console.error("neat: --min-confidence must be a number in [0, 1]");
|
|
6547
|
+
process.exit(2);
|
|
6548
|
+
}
|
|
6549
|
+
out.minConfidence = n;
|
|
6550
|
+
return;
|
|
6551
|
+
}
|
|
6092
6552
|
;
|
|
6093
6553
|
out[field] = value;
|
|
6094
6554
|
}
|
|
@@ -6170,7 +6630,7 @@ async function buildPatchSections(services) {
|
|
|
6170
6630
|
}
|
|
6171
6631
|
async function runInit(opts) {
|
|
6172
6632
|
const written = [];
|
|
6173
|
-
const stat = await
|
|
6633
|
+
const stat = await import_node_fs22.promises.stat(opts.scanPath).catch(() => null);
|
|
6174
6634
|
if (!stat || !stat.isDirectory()) {
|
|
6175
6635
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
6176
6636
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -6179,9 +6639,9 @@ async function runInit(opts) {
|
|
|
6179
6639
|
printDiscoveryReport(opts, services);
|
|
6180
6640
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
6181
6641
|
const patch = renderPatch(sections);
|
|
6182
|
-
const patchPath =
|
|
6642
|
+
const patchPath = import_node_path37.default.join(opts.scanPath, "neat.patch");
|
|
6183
6643
|
if (opts.dryRun) {
|
|
6184
|
-
await
|
|
6644
|
+
await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
|
|
6185
6645
|
written.push(patchPath);
|
|
6186
6646
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
6187
6647
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -6221,7 +6681,7 @@ async function runInit(opts) {
|
|
|
6221
6681
|
console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
6222
6682
|
}
|
|
6223
6683
|
} else {
|
|
6224
|
-
await
|
|
6684
|
+
await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
|
|
6225
6685
|
written.push(patchPath);
|
|
6226
6686
|
}
|
|
6227
6687
|
}
|
|
@@ -6261,9 +6721,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
6261
6721
|
};
|
|
6262
6722
|
function claudeConfigPath() {
|
|
6263
6723
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
6264
|
-
if (override && override.length > 0) return
|
|
6724
|
+
if (override && override.length > 0) return import_node_path37.default.resolve(override);
|
|
6265
6725
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6266
|
-
return
|
|
6726
|
+
return import_node_path37.default.join(home, ".claude.json");
|
|
6267
6727
|
}
|
|
6268
6728
|
async function runSkill(opts) {
|
|
6269
6729
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -6275,7 +6735,7 @@ async function runSkill(opts) {
|
|
|
6275
6735
|
const target = claudeConfigPath();
|
|
6276
6736
|
let existing = {};
|
|
6277
6737
|
try {
|
|
6278
|
-
existing = JSON.parse(await
|
|
6738
|
+
existing = JSON.parse(await import_node_fs22.promises.readFile(target, "utf8"));
|
|
6279
6739
|
} catch (err) {
|
|
6280
6740
|
if (err.code !== "ENOENT") {
|
|
6281
6741
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -6287,8 +6747,8 @@ async function runSkill(opts) {
|
|
|
6287
6747
|
...existing,
|
|
6288
6748
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
6289
6749
|
};
|
|
6290
|
-
await
|
|
6291
|
-
await
|
|
6750
|
+
await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
|
|
6751
|
+
await import_node_fs22.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
6292
6752
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
6293
6753
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
6294
6754
|
return { exitCode: 0 };
|
|
@@ -6322,12 +6782,12 @@ async function main() {
|
|
|
6322
6782
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
6323
6783
|
process.exit(2);
|
|
6324
6784
|
}
|
|
6325
|
-
const scanPath =
|
|
6785
|
+
const scanPath = import_node_path37.default.resolve(target);
|
|
6326
6786
|
const projectExplicit = parsed.project !== null;
|
|
6327
|
-
const projectName = projectExplicit ? project :
|
|
6787
|
+
const projectName = projectExplicit ? project : import_node_path37.default.basename(scanPath);
|
|
6328
6788
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
6329
|
-
const fallback = pathsForProject(projectKey,
|
|
6330
|
-
const outPath =
|
|
6789
|
+
const fallback = pathsForProject(projectKey, import_node_path37.default.join(scanPath, "neat-out")).snapshotPath;
|
|
6790
|
+
const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
6331
6791
|
const result = await runInit({
|
|
6332
6792
|
scanPath,
|
|
6333
6793
|
outPath,
|
|
@@ -6347,21 +6807,21 @@ async function main() {
|
|
|
6347
6807
|
usage();
|
|
6348
6808
|
process.exit(2);
|
|
6349
6809
|
}
|
|
6350
|
-
const scanPath =
|
|
6351
|
-
const stat = await
|
|
6810
|
+
const scanPath = import_node_path37.default.resolve(target);
|
|
6811
|
+
const stat = await import_node_fs22.promises.stat(scanPath).catch(() => null);
|
|
6352
6812
|
if (!stat || !stat.isDirectory()) {
|
|
6353
6813
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
6354
6814
|
process.exit(2);
|
|
6355
6815
|
}
|
|
6356
|
-
const projectPaths = pathsForProject(project,
|
|
6357
|
-
const outPath =
|
|
6358
|
-
const errorsPath =
|
|
6359
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
6816
|
+
const projectPaths = pathsForProject(project, import_node_path37.default.join(scanPath, "neat-out"));
|
|
6817
|
+
const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
6818
|
+
const errorsPath = import_node_path37.default.resolve(
|
|
6819
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.errorsPath))
|
|
6360
6820
|
);
|
|
6361
|
-
const staleEventsPath =
|
|
6362
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
6821
|
+
const staleEventsPath = import_node_path37.default.resolve(
|
|
6822
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.staleEventsPath))
|
|
6363
6823
|
);
|
|
6364
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
6824
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path37.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
6365
6825
|
const handle = await startWatch(getGraph(project), {
|
|
6366
6826
|
scanPath,
|
|
6367
6827
|
outPath,
|
|
@@ -6472,7 +6932,9 @@ var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
|
6472
6932
|
"search",
|
|
6473
6933
|
"diff",
|
|
6474
6934
|
"stale-edges",
|
|
6475
|
-
"policies"
|
|
6935
|
+
"policies",
|
|
6936
|
+
// Tenth verb (ADR-060) — amends ADR-050's locked allowlist of nine.
|
|
6937
|
+
"divergences"
|
|
6476
6938
|
]);
|
|
6477
6939
|
function resolveProjectFlag(parsed) {
|
|
6478
6940
|
if (parsed.project) return parsed.project;
|
|
@@ -6594,6 +7056,31 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
6594
7056
|
});
|
|
6595
7057
|
break;
|
|
6596
7058
|
}
|
|
7059
|
+
case "divergences": {
|
|
7060
|
+
let typeFilter;
|
|
7061
|
+
if (parsed.type) {
|
|
7062
|
+
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
7063
|
+
const out = [];
|
|
7064
|
+
for (const p of parts) {
|
|
7065
|
+
const r = import_types23.DivergenceTypeSchema.safeParse(p);
|
|
7066
|
+
if (!r.success) {
|
|
7067
|
+
console.error(
|
|
7068
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types23.DivergenceTypeSchema.options.join(", ")}`
|
|
7069
|
+
);
|
|
7070
|
+
return 2;
|
|
7071
|
+
}
|
|
7072
|
+
out.push(r.data);
|
|
7073
|
+
}
|
|
7074
|
+
typeFilter = out;
|
|
7075
|
+
}
|
|
7076
|
+
work = runDivergences(client, {
|
|
7077
|
+
...typeFilter ? { type: typeFilter } : {},
|
|
7078
|
+
...parsed.minConfidence !== null ? { minConfidence: parsed.minConfidence } : {},
|
|
7079
|
+
...parsed.node ? { node: parsed.node } : {},
|
|
7080
|
+
...project ? { project } : {}
|
|
7081
|
+
});
|
|
7082
|
+
break;
|
|
7083
|
+
}
|
|
6597
7084
|
default:
|
|
6598
7085
|
console.error(`neat: unknown query verb "${cmd}"`);
|
|
6599
7086
|
return 2;
|