@neat.is/core 0.3.2 → 0.3.3
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-66Z5IEFY.js → chunk-ICFH326H.js} +2 -2
- package/dist/{chunk-FIXKIYNF.js → chunk-VABXPLDT.js} +457 -206
- package/dist/chunk-VABXPLDT.js.map +1 -0
- package/dist/cli.cjs +555 -309
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +24 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +451 -221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +458 -228
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +420 -190
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-FIXKIYNF.js.map +0 -1
- /package/dist/{chunk-66Z5IEFY.js.map → chunk-ICFH326H.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_path33.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
105
|
+
return import_node_path33.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_path33, 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_path33 = __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_path34.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
264
|
+
const protoRoot = import_node_path34.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_path34.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_path34, 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_path34 = __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_path39 = __toESM(require("path"), 1);
|
|
384
|
+
var import_node_fs25 = 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, path40, edges) {
|
|
891
|
+
if (path40.length > best.path.length) {
|
|
892
|
+
best = { path: [...path40], edges: [...edges] };
|
|
893
893
|
}
|
|
894
|
-
if (
|
|
894
|
+
if (path40.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
|
+
path40.push(srcId);
|
|
900
900
|
edges.push(edge);
|
|
901
|
-
step(srcId,
|
|
902
|
-
|
|
901
|
+
step(srcId, path40, edges);
|
|
902
|
+
path40.pop();
|
|
903
903
|
edges.pop();
|
|
904
904
|
visited.delete(srcId);
|
|
905
905
|
}
|
|
@@ -1936,8 +1936,8 @@ 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_fs8 = require("fs");
|
|
1940
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1941
1941
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
1942
1942
|
var import_minimatch2 = require("minimatch");
|
|
1943
1943
|
var import_types5 = require("@neat.is/types");
|
|
@@ -1961,9 +1961,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1961
1961
|
function isConfigFile(name) {
|
|
1962
1962
|
const ext = import_node_path4.default.extname(name);
|
|
1963
1963
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
1964
|
-
if (name === ".env" || name.startsWith(".env."))
|
|
1964
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
1965
|
+
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
1966
|
+
return { match: true, fileType: "env" };
|
|
1967
|
+
}
|
|
1965
1968
|
return { match: false, fileType: "" };
|
|
1966
1969
|
}
|
|
1970
|
+
function isTestPath(filePath) {
|
|
1971
|
+
const normalised = filePath.replace(/\\/g, "/");
|
|
1972
|
+
const segments = normalised.split("/");
|
|
1973
|
+
for (const seg of segments) {
|
|
1974
|
+
if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const base = segments[segments.length - 1] ?? "";
|
|
1979
|
+
return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
|
|
1980
|
+
}
|
|
1981
|
+
function isEnvTemplateFile(name) {
|
|
1982
|
+
if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
|
|
1986
|
+
}
|
|
1987
|
+
function maskCommentsInSource(src) {
|
|
1988
|
+
const len = src.length;
|
|
1989
|
+
const out = new Array(len);
|
|
1990
|
+
let i = 0;
|
|
1991
|
+
let inString = 0;
|
|
1992
|
+
let escaped = false;
|
|
1993
|
+
while (i < len) {
|
|
1994
|
+
const c = src[i];
|
|
1995
|
+
if (inString !== 0) {
|
|
1996
|
+
out[i] = c;
|
|
1997
|
+
if (escaped) {
|
|
1998
|
+
escaped = false;
|
|
1999
|
+
} else if (c === "\\") {
|
|
2000
|
+
escaped = true;
|
|
2001
|
+
} else if (c === inString) {
|
|
2002
|
+
inString = 0;
|
|
2003
|
+
}
|
|
2004
|
+
i++;
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
if (c === "/" && i + 1 < len) {
|
|
2008
|
+
const next = src[i + 1];
|
|
2009
|
+
if (next === "/") {
|
|
2010
|
+
out[i] = " ";
|
|
2011
|
+
out[i + 1] = " ";
|
|
2012
|
+
let j = i + 2;
|
|
2013
|
+
while (j < len && src[j] !== "\n") {
|
|
2014
|
+
out[j] = " ";
|
|
2015
|
+
j++;
|
|
2016
|
+
}
|
|
2017
|
+
i = j;
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
if (next === "*") {
|
|
2021
|
+
out[i] = " ";
|
|
2022
|
+
out[i + 1] = " ";
|
|
2023
|
+
let j = i + 2;
|
|
2024
|
+
while (j < len) {
|
|
2025
|
+
if (src[j] === "\n") {
|
|
2026
|
+
out[j] = "\n";
|
|
2027
|
+
j++;
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
|
|
2031
|
+
out[j] = " ";
|
|
2032
|
+
out[j + 1] = " ";
|
|
2033
|
+
j += 2;
|
|
2034
|
+
break;
|
|
2035
|
+
}
|
|
2036
|
+
out[j] = " ";
|
|
2037
|
+
j++;
|
|
2038
|
+
}
|
|
2039
|
+
i = j;
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
out[i] = c;
|
|
2044
|
+
if (c === "'" || c === '"' || c === "`") inString = c;
|
|
2045
|
+
i++;
|
|
2046
|
+
}
|
|
2047
|
+
return out.join("");
|
|
2048
|
+
}
|
|
2049
|
+
var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
|
|
2050
|
+
function urlMatchesHost(urlString, host) {
|
|
2051
|
+
if (typeof urlString !== "string" || urlString.length === 0) return false;
|
|
2052
|
+
if (!URL_LIKE.test(urlString)) return false;
|
|
2053
|
+
const [wantedHost, wantedPort] = host.split(":");
|
|
2054
|
+
let parsed;
|
|
2055
|
+
try {
|
|
2056
|
+
const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
|
|
2057
|
+
parsed = new URL(candidate);
|
|
2058
|
+
} catch {
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
|
|
2062
|
+
if (wantedPort && parsed.port !== wantedPort) return false;
|
|
2063
|
+
return true;
|
|
2064
|
+
}
|
|
1967
2065
|
function cleanVersion(raw) {
|
|
1968
2066
|
if (!raw) return void 0;
|
|
1969
2067
|
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
@@ -2119,6 +2217,41 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
|
2119
2217
|
return author ?? void 0;
|
|
2120
2218
|
}
|
|
2121
2219
|
|
|
2220
|
+
// src/extract/errors.ts
|
|
2221
|
+
init_cjs_shims();
|
|
2222
|
+
var import_node_fs7 = require("fs");
|
|
2223
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2224
|
+
var sink = [];
|
|
2225
|
+
function recordExtractionError(producer, file, err) {
|
|
2226
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2227
|
+
sink.push({
|
|
2228
|
+
producer,
|
|
2229
|
+
file,
|
|
2230
|
+
error: e.message,
|
|
2231
|
+
stack: e.stack,
|
|
2232
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2233
|
+
source: "extract"
|
|
2234
|
+
});
|
|
2235
|
+
console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
|
|
2236
|
+
}
|
|
2237
|
+
function drainExtractionErrors() {
|
|
2238
|
+
return sink.splice(0, sink.length);
|
|
2239
|
+
}
|
|
2240
|
+
async function writeExtractionErrors(errors, errorsPath) {
|
|
2241
|
+
if (errors.length === 0) return;
|
|
2242
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
|
|
2243
|
+
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2244
|
+
await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
|
|
2245
|
+
}
|
|
2246
|
+
function isStrictExtractionEnabled() {
|
|
2247
|
+
const raw = process.env.NEAT_STRICT_EXTRACTION;
|
|
2248
|
+
return raw === "1" || raw === "true";
|
|
2249
|
+
}
|
|
2250
|
+
function formatExtractionBanner(count) {
|
|
2251
|
+
if (count === 1) return `[neat] 1 file skipped due to parse errors`;
|
|
2252
|
+
return `[neat] ${count} files skipped due to parse errors`;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2122
2255
|
// src/extract/services.ts
|
|
2123
2256
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
2124
2257
|
function parseScanDepth() {
|
|
@@ -2135,21 +2268,21 @@ function workspaceGlobs(pkg) {
|
|
|
2135
2268
|
return null;
|
|
2136
2269
|
}
|
|
2137
2270
|
async function loadGitignore(scanPath) {
|
|
2138
|
-
const gitignorePath =
|
|
2271
|
+
const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
|
|
2139
2272
|
if (!await exists(gitignorePath)) return null;
|
|
2140
|
-
const raw = await
|
|
2273
|
+
const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
|
|
2141
2274
|
return (0, import_ignore.default)().add(raw);
|
|
2142
2275
|
}
|
|
2143
2276
|
async function walkDirs(start, scanPath, options, visit) {
|
|
2144
2277
|
async function recurse(current, depth) {
|
|
2145
2278
|
if (depth > options.maxDepth) return;
|
|
2146
|
-
const entries = await
|
|
2279
|
+
const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2147
2280
|
for (const entry2 of entries) {
|
|
2148
2281
|
if (!entry2.isDirectory()) continue;
|
|
2149
2282
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2150
|
-
const child =
|
|
2283
|
+
const child = import_node_path8.default.join(current, entry2.name);
|
|
2151
2284
|
if (options.ig) {
|
|
2152
|
-
const rel =
|
|
2285
|
+
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2153
2286
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2154
2287
|
}
|
|
2155
2288
|
await visit(child);
|
|
@@ -2164,8 +2297,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2164
2297
|
for (const raw of globs) {
|
|
2165
2298
|
const pattern = raw.replace(/^\.\//, "");
|
|
2166
2299
|
if (!pattern.includes("*")) {
|
|
2167
|
-
const candidate =
|
|
2168
|
-
if (await exists(
|
|
2300
|
+
const candidate = import_node_path8.default.join(scanPath, pattern);
|
|
2301
|
+
if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2169
2302
|
continue;
|
|
2170
2303
|
}
|
|
2171
2304
|
const segments = pattern.split("/");
|
|
@@ -2174,13 +2307,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2174
2307
|
if (seg.includes("*")) break;
|
|
2175
2308
|
staticSegments.push(seg);
|
|
2176
2309
|
}
|
|
2177
|
-
const start =
|
|
2310
|
+
const start = import_node_path8.default.join(scanPath, ...staticSegments);
|
|
2178
2311
|
if (!await exists(start)) continue;
|
|
2179
2312
|
const hasDoubleStar = pattern.includes("**");
|
|
2180
2313
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2181
2314
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2182
|
-
const rel =
|
|
2183
|
-
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(
|
|
2315
|
+
const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
|
|
2316
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2184
2317
|
found.add(dir);
|
|
2185
2318
|
}
|
|
2186
2319
|
});
|
|
@@ -2188,15 +2321,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2188
2321
|
return [...found];
|
|
2189
2322
|
}
|
|
2190
2323
|
async function discoverNodeService(scanPath, dir) {
|
|
2191
|
-
const pkgPath =
|
|
2324
|
+
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2192
2325
|
if (!await exists(pkgPath)) return null;
|
|
2193
2326
|
let pkg;
|
|
2194
2327
|
try {
|
|
2195
2328
|
pkg = await readJson(pkgPath);
|
|
2196
2329
|
} catch (err) {
|
|
2197
|
-
|
|
2198
|
-
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2199
|
-
);
|
|
2330
|
+
recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
|
|
2200
2331
|
return null;
|
|
2201
2332
|
}
|
|
2202
2333
|
if (!pkg.name) return null;
|
|
@@ -2207,7 +2338,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2207
2338
|
language: "javascript",
|
|
2208
2339
|
version: pkg.version,
|
|
2209
2340
|
dependencies: pkg.dependencies ?? {},
|
|
2210
|
-
repoPath:
|
|
2341
|
+
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2211
2342
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2212
2343
|
};
|
|
2213
2344
|
return { pkg, dir, node };
|
|
@@ -2223,19 +2354,21 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2223
2354
|
language: "python",
|
|
2224
2355
|
version: py.version,
|
|
2225
2356
|
dependencies: py.dependencies,
|
|
2226
|
-
repoPath:
|
|
2357
|
+
repoPath: import_node_path8.default.relative(scanPath, dir)
|
|
2227
2358
|
};
|
|
2228
2359
|
return { pkg, dir, node };
|
|
2229
2360
|
}
|
|
2230
2361
|
async function discoverServices(scanPath) {
|
|
2231
|
-
const rootPkgPath =
|
|
2362
|
+
const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
|
|
2232
2363
|
let rootPkg = null;
|
|
2233
2364
|
if (await exists(rootPkgPath)) {
|
|
2234
2365
|
try {
|
|
2235
2366
|
rootPkg = await readJson(rootPkgPath);
|
|
2236
2367
|
} catch (err) {
|
|
2237
|
-
|
|
2238
|
-
|
|
2368
|
+
recordExtractionError(
|
|
2369
|
+
"services workspaces",
|
|
2370
|
+
import_node_path8.default.relative(scanPath, rootPkgPath),
|
|
2371
|
+
err
|
|
2239
2372
|
);
|
|
2240
2373
|
}
|
|
2241
2374
|
}
|
|
@@ -2251,9 +2384,9 @@ async function discoverServices(scanPath) {
|
|
|
2251
2384
|
scanPath,
|
|
2252
2385
|
{ maxDepth: parseScanDepth(), ig },
|
|
2253
2386
|
async (dir) => {
|
|
2254
|
-
if (await exists(
|
|
2387
|
+
if (await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2255
2388
|
candidateDirs.push(dir);
|
|
2256
|
-
} else if (await exists(
|
|
2389
|
+
} else if (await exists(import_node_path8.default.join(dir, "pyproject.toml")) || await exists(import_node_path8.default.join(dir, "requirements.txt")) || await exists(import_node_path8.default.join(dir, "setup.py"))) {
|
|
2257
2390
|
candidateDirs.push(dir);
|
|
2258
2391
|
}
|
|
2259
2392
|
}
|
|
@@ -2267,8 +2400,8 @@ async function discoverServices(scanPath) {
|
|
|
2267
2400
|
if (!service) continue;
|
|
2268
2401
|
const existingDir = seen.get(service.node.name);
|
|
2269
2402
|
if (existingDir !== void 0) {
|
|
2270
|
-
const a =
|
|
2271
|
-
const b =
|
|
2403
|
+
const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
|
|
2404
|
+
const b = import_node_path8.default.relative(scanPath, dir) || ".";
|
|
2272
2405
|
console.warn(
|
|
2273
2406
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2274
2407
|
);
|
|
@@ -2305,8 +2438,8 @@ function addServiceNodes(graph, services) {
|
|
|
2305
2438
|
|
|
2306
2439
|
// src/extract/aliases.ts
|
|
2307
2440
|
init_cjs_shims();
|
|
2308
|
-
var
|
|
2309
|
-
var
|
|
2441
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2442
|
+
var import_node_fs9 = require("fs");
|
|
2310
2443
|
var import_yaml2 = require("yaml");
|
|
2311
2444
|
var import_types6 = require("@neat.is/types");
|
|
2312
2445
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2333,14 +2466,14 @@ function indexServicesByName(services) {
|
|
|
2333
2466
|
const map = /* @__PURE__ */ new Map();
|
|
2334
2467
|
for (const s of services) {
|
|
2335
2468
|
map.set(s.node.name, s.node.id);
|
|
2336
|
-
map.set(
|
|
2469
|
+
map.set(import_node_path9.default.basename(s.dir), s.node.id);
|
|
2337
2470
|
}
|
|
2338
2471
|
return map;
|
|
2339
2472
|
}
|
|
2340
2473
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2341
2474
|
let composePath = null;
|
|
2342
2475
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2343
|
-
const abs =
|
|
2476
|
+
const abs = import_node_path9.default.join(scanPath, name);
|
|
2344
2477
|
if (await exists(abs)) {
|
|
2345
2478
|
composePath = abs;
|
|
2346
2479
|
break;
|
|
@@ -2351,8 +2484,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
2351
2484
|
try {
|
|
2352
2485
|
compose = await readYaml(composePath);
|
|
2353
2486
|
} catch (err) {
|
|
2354
|
-
|
|
2355
|
-
|
|
2487
|
+
recordExtractionError(
|
|
2488
|
+
"aliases compose",
|
|
2489
|
+
import_node_path9.default.relative(scanPath, composePath),
|
|
2490
|
+
err
|
|
2356
2491
|
);
|
|
2357
2492
|
return;
|
|
2358
2493
|
}
|
|
@@ -2394,15 +2529,13 @@ function parseDockerfileLabels(content) {
|
|
|
2394
2529
|
}
|
|
2395
2530
|
async function collectDockerfileAliases(graph, services) {
|
|
2396
2531
|
for (const service of services) {
|
|
2397
|
-
const dockerfilePath =
|
|
2532
|
+
const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
|
|
2398
2533
|
if (!await exists(dockerfilePath)) continue;
|
|
2399
2534
|
let content;
|
|
2400
2535
|
try {
|
|
2401
|
-
content = await
|
|
2536
|
+
content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
|
|
2402
2537
|
} catch (err) {
|
|
2403
|
-
|
|
2404
|
-
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2405
|
-
);
|
|
2538
|
+
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
2406
2539
|
continue;
|
|
2407
2540
|
}
|
|
2408
2541
|
const aliases = parseDockerfileLabels(content);
|
|
@@ -2412,13 +2545,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2412
2545
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2413
2546
|
if (depth > max) return [];
|
|
2414
2547
|
const out = [];
|
|
2415
|
-
const entries = await
|
|
2548
|
+
const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2416
2549
|
for (const entry2 of entries) {
|
|
2417
2550
|
if (entry2.isDirectory()) {
|
|
2418
2551
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2419
|
-
out.push(...await walkYamlFiles(
|
|
2420
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2421
|
-
out.push(
|
|
2552
|
+
out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
|
|
2553
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
|
|
2554
|
+
out.push(import_node_path9.default.join(start, entry2.name));
|
|
2422
2555
|
}
|
|
2423
2556
|
}
|
|
2424
2557
|
return out;
|
|
@@ -2445,7 +2578,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2445
2578
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2446
2579
|
const files = await walkYamlFiles(scanPath);
|
|
2447
2580
|
for (const file of files) {
|
|
2448
|
-
const content = await
|
|
2581
|
+
const content = await import_node_fs9.promises.readFile(file, "utf8");
|
|
2449
2582
|
let docs;
|
|
2450
2583
|
try {
|
|
2451
2584
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2470,14 +2603,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2470
2603
|
|
|
2471
2604
|
// src/extract/databases/index.ts
|
|
2472
2605
|
init_cjs_shims();
|
|
2473
|
-
var
|
|
2606
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2474
2607
|
var import_types7 = require("@neat.is/types");
|
|
2475
2608
|
|
|
2476
2609
|
// src/extract/databases/db-config-yaml.ts
|
|
2477
2610
|
init_cjs_shims();
|
|
2478
|
-
var
|
|
2611
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2479
2612
|
async function parse(serviceDir) {
|
|
2480
|
-
const yamlPath =
|
|
2613
|
+
const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
|
|
2481
2614
|
if (!await exists(yamlPath)) return [];
|
|
2482
2615
|
const raw = await readYaml(yamlPath);
|
|
2483
2616
|
return [
|
|
@@ -2495,13 +2628,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2495
2628
|
|
|
2496
2629
|
// src/extract/databases/dotenv.ts
|
|
2497
2630
|
init_cjs_shims();
|
|
2498
|
-
var
|
|
2499
|
-
var
|
|
2631
|
+
var import_node_fs11 = require("fs");
|
|
2632
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2500
2633
|
|
|
2501
2634
|
// src/extract/databases/shared.ts
|
|
2502
2635
|
init_cjs_shims();
|
|
2503
|
-
var
|
|
2504
|
-
var
|
|
2636
|
+
var import_node_fs10 = require("fs");
|
|
2637
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2505
2638
|
function schemeToEngine(scheme) {
|
|
2506
2639
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2507
2640
|
switch (s) {
|
|
@@ -2540,14 +2673,14 @@ function parseConnectionString(url) {
|
|
|
2540
2673
|
}
|
|
2541
2674
|
async function readIfExists(filePath) {
|
|
2542
2675
|
try {
|
|
2543
|
-
return await
|
|
2676
|
+
return await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2544
2677
|
} catch {
|
|
2545
2678
|
return null;
|
|
2546
2679
|
}
|
|
2547
2680
|
}
|
|
2548
2681
|
async function findFirst(serviceDir, candidates) {
|
|
2549
2682
|
for (const rel of candidates) {
|
|
2550
|
-
const abs =
|
|
2683
|
+
const abs = import_node_path11.default.join(serviceDir, rel);
|
|
2551
2684
|
const content = await readIfExists(abs);
|
|
2552
2685
|
if (content !== null) return abs;
|
|
2553
2686
|
}
|
|
@@ -2598,15 +2731,15 @@ function parseDotenvLine(line) {
|
|
|
2598
2731
|
return { key, value };
|
|
2599
2732
|
}
|
|
2600
2733
|
async function parse2(serviceDir) {
|
|
2601
|
-
const entries = await
|
|
2734
|
+
const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2602
2735
|
const configs = [];
|
|
2603
2736
|
const seen = /* @__PURE__ */ new Set();
|
|
2604
2737
|
for (const entry2 of entries) {
|
|
2605
2738
|
if (!entry2.isFile()) continue;
|
|
2606
2739
|
const match = isConfigFile(entry2.name);
|
|
2607
2740
|
if (!match.match || match.fileType !== "env") continue;
|
|
2608
|
-
const filePath =
|
|
2609
|
-
const content = await
|
|
2741
|
+
const filePath = import_node_path12.default.join(serviceDir, entry2.name);
|
|
2742
|
+
const content = await import_node_fs11.promises.readFile(filePath, "utf8");
|
|
2610
2743
|
for (const line of content.split("\n")) {
|
|
2611
2744
|
const parsed = parseDotenvLine(line);
|
|
2612
2745
|
if (!parsed) continue;
|
|
@@ -2625,9 +2758,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2625
2758
|
|
|
2626
2759
|
// src/extract/databases/prisma.ts
|
|
2627
2760
|
init_cjs_shims();
|
|
2628
|
-
var
|
|
2761
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2629
2762
|
async function parse3(serviceDir) {
|
|
2630
|
-
const schemaPath =
|
|
2763
|
+
const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2631
2764
|
const content = await readIfExists(schemaPath);
|
|
2632
2765
|
if (!content) return [];
|
|
2633
2766
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2759,10 +2892,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2759
2892
|
|
|
2760
2893
|
// src/extract/databases/ormconfig.ts
|
|
2761
2894
|
init_cjs_shims();
|
|
2762
|
-
var
|
|
2895
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2763
2896
|
async function parse6(serviceDir) {
|
|
2764
2897
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2765
|
-
const abs =
|
|
2898
|
+
const abs = import_node_path14.default.join(serviceDir, candidate);
|
|
2766
2899
|
if (!await exists(abs)) continue;
|
|
2767
2900
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2768
2901
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2822,9 +2955,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2822
2955
|
|
|
2823
2956
|
// src/extract/databases/sequelize.ts
|
|
2824
2957
|
init_cjs_shims();
|
|
2825
|
-
var
|
|
2958
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2826
2959
|
async function parse8(serviceDir) {
|
|
2827
|
-
const configPath =
|
|
2960
|
+
const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
|
|
2828
2961
|
if (!await exists(configPath)) return [];
|
|
2829
2962
|
const raw = await readJson(configPath);
|
|
2830
2963
|
const out = [];
|
|
@@ -2851,7 +2984,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
2851
2984
|
|
|
2852
2985
|
// src/extract/databases/docker-compose.ts
|
|
2853
2986
|
init_cjs_shims();
|
|
2854
|
-
var
|
|
2987
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2855
2988
|
function portFromService(svc) {
|
|
2856
2989
|
for (const raw of svc.ports ?? []) {
|
|
2857
2990
|
const str = String(raw);
|
|
@@ -2878,7 +3011,7 @@ function databaseFromEnv(svc) {
|
|
|
2878
3011
|
}
|
|
2879
3012
|
async function parse9(serviceDir) {
|
|
2880
3013
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2881
|
-
const abs =
|
|
3014
|
+
const abs = import_node_path16.default.join(serviceDir, name);
|
|
2882
3015
|
if (!await exists(abs)) continue;
|
|
2883
3016
|
const raw = await readYaml(abs);
|
|
2884
3017
|
if (!raw?.services) return [];
|
|
@@ -3056,11 +3189,12 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3056
3189
|
target: dbNode.id,
|
|
3057
3190
|
type: import_types7.EdgeType.CONNECTS_TO,
|
|
3058
3191
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3192
|
+
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
3193
|
+
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3194
|
+
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3195
|
+
evidence: {
|
|
3196
|
+
file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
|
|
3197
|
+
}
|
|
3064
3198
|
};
|
|
3065
3199
|
if (!graph.hasEdge(edge.id)) {
|
|
3066
3200
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3086,15 +3220,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3086
3220
|
|
|
3087
3221
|
// src/extract/configs.ts
|
|
3088
3222
|
init_cjs_shims();
|
|
3089
|
-
var
|
|
3090
|
-
var
|
|
3223
|
+
var import_node_fs12 = require("fs");
|
|
3224
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
3091
3225
|
var import_types8 = require("@neat.is/types");
|
|
3092
3226
|
async function walkConfigFiles(dir) {
|
|
3093
3227
|
const out = [];
|
|
3094
3228
|
async function walk(current) {
|
|
3095
|
-
const entries = await
|
|
3229
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
|
|
3096
3230
|
for (const entry2 of entries) {
|
|
3097
|
-
const full =
|
|
3231
|
+
const full = import_node_path18.default.join(current, entry2.name);
|
|
3098
3232
|
if (entry2.isDirectory()) {
|
|
3099
3233
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
3100
3234
|
} else if (entry2.isFile() && isConfigFile(entry2.name).match) {
|
|
@@ -3111,13 +3245,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3111
3245
|
for (const service of services) {
|
|
3112
3246
|
const configFiles = await walkConfigFiles(service.dir);
|
|
3113
3247
|
for (const file of configFiles) {
|
|
3114
|
-
const relPath =
|
|
3248
|
+
const relPath = import_node_path18.default.relative(scanPath, file);
|
|
3115
3249
|
const node = {
|
|
3116
3250
|
id: (0, import_types8.configId)(relPath),
|
|
3117
3251
|
type: import_types8.NodeType.ConfigNode,
|
|
3118
|
-
name:
|
|
3252
|
+
name: import_node_path18.default.basename(file),
|
|
3119
3253
|
path: relPath,
|
|
3120
|
-
fileType: isConfigFile(
|
|
3254
|
+
fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
|
|
3121
3255
|
};
|
|
3122
3256
|
if (!graph.hasNode(node.id)) {
|
|
3123
3257
|
graph.addNode(node.id, node);
|
|
@@ -3129,7 +3263,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3129
3263
|
target: node.id,
|
|
3130
3264
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
3131
3265
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
3132
|
-
evidence: { file: relPath.split(
|
|
3266
|
+
evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
|
|
3133
3267
|
};
|
|
3134
3268
|
if (!graph.hasEdge(edge.id)) {
|
|
3135
3269
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3146,7 +3280,7 @@ var import_types14 = require("@neat.is/types");
|
|
|
3146
3280
|
|
|
3147
3281
|
// src/extract/calls/http.ts
|
|
3148
3282
|
init_cjs_shims();
|
|
3149
|
-
var
|
|
3283
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3150
3284
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3151
3285
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3152
3286
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -3154,17 +3288,17 @@ var import_types9 = require("@neat.is/types");
|
|
|
3154
3288
|
|
|
3155
3289
|
// src/extract/calls/shared.ts
|
|
3156
3290
|
init_cjs_shims();
|
|
3157
|
-
var
|
|
3158
|
-
var
|
|
3291
|
+
var import_node_fs13 = require("fs");
|
|
3292
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3159
3293
|
async function walkSourceFiles(dir) {
|
|
3160
3294
|
const out = [];
|
|
3161
3295
|
async function walk(current) {
|
|
3162
|
-
const entries = await
|
|
3296
|
+
const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3163
3297
|
for (const entry2 of entries) {
|
|
3164
|
-
const full =
|
|
3298
|
+
const full = import_node_path19.default.join(current, entry2.name);
|
|
3165
3299
|
if (entry2.isDirectory()) {
|
|
3166
3300
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
3167
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3301
|
+
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
|
|
3168
3302
|
out.push(full);
|
|
3169
3303
|
}
|
|
3170
3304
|
}
|
|
@@ -3177,7 +3311,7 @@ async function loadSourceFiles(dir) {
|
|
|
3177
3311
|
const out = [];
|
|
3178
3312
|
for (const p of paths) {
|
|
3179
3313
|
try {
|
|
3180
|
-
const content = await
|
|
3314
|
+
const content = await import_node_fs13.promises.readFile(p, "utf8");
|
|
3181
3315
|
out.push({ path: p, content });
|
|
3182
3316
|
} catch {
|
|
3183
3317
|
}
|
|
@@ -3196,8 +3330,27 @@ function snippet(text, line) {
|
|
|
3196
3330
|
|
|
3197
3331
|
// src/extract/calls/http.ts
|
|
3198
3332
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3333
|
+
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3334
|
+
function isInsideJsxExternalLink(node) {
|
|
3335
|
+
let cursor = node.parent;
|
|
3336
|
+
while (cursor) {
|
|
3337
|
+
if (cursor.type === "jsx_attribute") {
|
|
3338
|
+
let owner = cursor.parent;
|
|
3339
|
+
while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
|
|
3340
|
+
owner = owner.parent;
|
|
3341
|
+
}
|
|
3342
|
+
if (!owner) return false;
|
|
3343
|
+
const tagNode = owner.namedChild(0);
|
|
3344
|
+
const tagName = tagNode?.text ?? "";
|
|
3345
|
+
const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
|
|
3346
|
+
return JSX_EXTERNAL_LINK_TAGS.has(right);
|
|
3347
|
+
}
|
|
3348
|
+
cursor = cursor.parent;
|
|
3349
|
+
}
|
|
3350
|
+
return false;
|
|
3351
|
+
}
|
|
3199
3352
|
function collectStringLiterals(node, out) {
|
|
3200
|
-
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
|
|
3353
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
|
|
3201
3354
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3202
3355
|
const child = node.namedChild(i);
|
|
3203
3356
|
if (child) collectStringLiterals(child, out);
|
|
@@ -3209,8 +3362,9 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3209
3362
|
collectStringLiterals(tree.rootNode, literals);
|
|
3210
3363
|
const targets = /* @__PURE__ */ new Set();
|
|
3211
3364
|
for (const lit of literals) {
|
|
3365
|
+
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
3212
3366
|
for (const host of knownHosts) {
|
|
3213
|
-
if (lit.
|
|
3367
|
+
if (urlMatchesHost(lit.text, host)) {
|
|
3214
3368
|
targets.add(host);
|
|
3215
3369
|
}
|
|
3216
3370
|
}
|
|
@@ -3233,9 +3387,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3233
3387
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3234
3388
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3235
3389
|
for (const service of services) {
|
|
3236
|
-
knownHosts.add(
|
|
3390
|
+
knownHosts.add(import_node_path20.default.basename(service.dir));
|
|
3237
3391
|
knownHosts.add(service.pkg.name);
|
|
3238
|
-
hostToNodeId.set(
|
|
3392
|
+
hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
|
|
3239
3393
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3240
3394
|
}
|
|
3241
3395
|
let edgesAdded = 0;
|
|
@@ -3243,14 +3397,13 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3243
3397
|
const files = await loadSourceFiles(service.dir);
|
|
3244
3398
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3245
3399
|
for (const file of files) {
|
|
3246
|
-
|
|
3400
|
+
if (isTestPath(file.path)) continue;
|
|
3401
|
+
const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3247
3402
|
let targets;
|
|
3248
3403
|
try {
|
|
3249
3404
|
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3250
3405
|
} catch (err) {
|
|
3251
|
-
|
|
3252
|
-
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3253
|
-
);
|
|
3406
|
+
recordExtractionError("http call extraction", file.path, err);
|
|
3254
3407
|
continue;
|
|
3255
3408
|
}
|
|
3256
3409
|
for (const t of targets) {
|
|
@@ -3271,7 +3424,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3271
3424
|
type: import_types9.EdgeType.CALLS,
|
|
3272
3425
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3273
3426
|
evidence: {
|
|
3274
|
-
file:
|
|
3427
|
+
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3275
3428
|
line,
|
|
3276
3429
|
snippet: snippet(fileContent, line)
|
|
3277
3430
|
}
|
|
@@ -3287,7 +3440,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3287
3440
|
|
|
3288
3441
|
// src/extract/calls/kafka.ts
|
|
3289
3442
|
init_cjs_shims();
|
|
3290
|
-
var
|
|
3443
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3291
3444
|
var import_types10 = require("@neat.is/types");
|
|
3292
3445
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3293
3446
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3314,7 +3467,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3314
3467
|
kind: "kafka-topic",
|
|
3315
3468
|
edgeType,
|
|
3316
3469
|
evidence: {
|
|
3317
|
-
file:
|
|
3470
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3318
3471
|
line,
|
|
3319
3472
|
snippet: snippet(file.content, line)
|
|
3320
3473
|
}
|
|
@@ -3327,7 +3480,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3327
3480
|
|
|
3328
3481
|
// src/extract/calls/redis.ts
|
|
3329
3482
|
init_cjs_shims();
|
|
3330
|
-
var
|
|
3483
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3331
3484
|
var import_types11 = require("@neat.is/types");
|
|
3332
3485
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3333
3486
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3346,7 +3499,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3346
3499
|
kind: "redis",
|
|
3347
3500
|
edgeType: "CALLS",
|
|
3348
3501
|
evidence: {
|
|
3349
|
-
file:
|
|
3502
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3350
3503
|
line,
|
|
3351
3504
|
snippet: snippet(file.content, line)
|
|
3352
3505
|
}
|
|
@@ -3357,7 +3510,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3357
3510
|
|
|
3358
3511
|
// src/extract/calls/aws.ts
|
|
3359
3512
|
init_cjs_shims();
|
|
3360
|
-
var
|
|
3513
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3361
3514
|
var import_types12 = require("@neat.is/types");
|
|
3362
3515
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3363
3516
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3387,7 +3540,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3387
3540
|
kind,
|
|
3388
3541
|
edgeType: "CALLS",
|
|
3389
3542
|
evidence: {
|
|
3390
|
-
file:
|
|
3543
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3391
3544
|
line,
|
|
3392
3545
|
snippet: snippet(file.content, line)
|
|
3393
3546
|
}
|
|
@@ -3412,16 +3565,42 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3412
3565
|
|
|
3413
3566
|
// src/extract/calls/grpc.ts
|
|
3414
3567
|
init_cjs_shims();
|
|
3415
|
-
var
|
|
3568
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3416
3569
|
var import_types13 = require("@neat.is/types");
|
|
3417
3570
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3571
|
+
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3572
|
+
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3418
3573
|
function isLikelyAddress(value) {
|
|
3419
3574
|
if (!value) return false;
|
|
3420
3575
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3421
3576
|
}
|
|
3577
|
+
function normaliseForMatch(s) {
|
|
3578
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3579
|
+
}
|
|
3580
|
+
function readImports(content) {
|
|
3581
|
+
const awsSdkSuffixes = /* @__PURE__ */ new Map();
|
|
3582
|
+
AWS_SDK_IMPORT_RE.lastIndex = 0;
|
|
3583
|
+
let m;
|
|
3584
|
+
while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
|
|
3585
|
+
const raw = m[1];
|
|
3586
|
+
awsSdkSuffixes.set(normaliseForMatch(raw), raw);
|
|
3587
|
+
}
|
|
3588
|
+
return {
|
|
3589
|
+
awsSdkSuffixes,
|
|
3590
|
+
hasGrpcImport: GRPC_IMPORT_RE.test(content)
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
function classifyClient(symbol, ctx) {
|
|
3594
|
+
const key = normaliseForMatch(symbol);
|
|
3595
|
+
const awsRaw = ctx.awsSdkSuffixes.get(key);
|
|
3596
|
+
if (awsRaw) return { kind: `aws-${awsRaw}` };
|
|
3597
|
+
if (ctx.hasGrpcImport) return { kind: "grpc-service" };
|
|
3598
|
+
return null;
|
|
3599
|
+
}
|
|
3422
3600
|
function grpcEndpointsFromFile(file, serviceDir) {
|
|
3423
3601
|
const out = [];
|
|
3424
3602
|
const seen = /* @__PURE__ */ new Set();
|
|
3603
|
+
const ctx = readImports(file.content);
|
|
3425
3604
|
GRPC_CLIENT_RE.lastIndex = 0;
|
|
3426
3605
|
let m;
|
|
3427
3606
|
while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
|
|
@@ -3429,15 +3608,18 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3429
3608
|
const addr = m[2]?.trim();
|
|
3430
3609
|
const name = isLikelyAddress(addr) ? addr : symbol;
|
|
3431
3610
|
if (seen.has(name)) continue;
|
|
3611
|
+
const classified = classifyClient(symbol, ctx);
|
|
3612
|
+
if (!classified) continue;
|
|
3432
3613
|
seen.add(name);
|
|
3614
|
+
const { kind } = classified;
|
|
3433
3615
|
const line = lineOf(file.content, m[0]);
|
|
3434
3616
|
out.push({
|
|
3435
|
-
infraId: (0, import_types13.infraId)(
|
|
3617
|
+
infraId: (0, import_types13.infraId)(kind, name),
|
|
3436
3618
|
name,
|
|
3437
|
-
kind
|
|
3619
|
+
kind,
|
|
3438
3620
|
edgeType: "CALLS",
|
|
3439
3621
|
evidence: {
|
|
3440
|
-
file:
|
|
3622
|
+
file: import_node_path24.default.relative(serviceDir, file.path),
|
|
3441
3623
|
line,
|
|
3442
3624
|
snippet: snippet(file.content, line)
|
|
3443
3625
|
}
|
|
@@ -3457,6 +3639,9 @@ function edgeTypeFromEndpoint(ep) {
|
|
|
3457
3639
|
return import_types14.EdgeType.CALLS;
|
|
3458
3640
|
}
|
|
3459
3641
|
}
|
|
3642
|
+
function isAwsKind(kind) {
|
|
3643
|
+
return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
|
|
3644
|
+
}
|
|
3460
3645
|
async function addExternalEndpointEdges(graph, services) {
|
|
3461
3646
|
let nodesAdded = 0;
|
|
3462
3647
|
let edgesAdded = 0;
|
|
@@ -3464,10 +3649,13 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3464
3649
|
const files = await loadSourceFiles(service.dir);
|
|
3465
3650
|
const endpoints = [];
|
|
3466
3651
|
for (const file of files) {
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
endpoints.push(...
|
|
3652
|
+
if (isTestPath(file.path)) continue;
|
|
3653
|
+
const masked = maskCommentsInSource(file.content);
|
|
3654
|
+
const maskedFile = { path: file.path, content: masked };
|
|
3655
|
+
endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
|
|
3656
|
+
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
3657
|
+
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
3658
|
+
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
3471
3659
|
}
|
|
3472
3660
|
if (endpoints.length === 0) continue;
|
|
3473
3661
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -3477,7 +3665,10 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3477
3665
|
id: ep.infraId,
|
|
3478
3666
|
type: import_types14.NodeType.InfraNode,
|
|
3479
3667
|
name: ep.name,
|
|
3480
|
-
|
|
3668
|
+
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3669
|
+
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
3670
|
+
// bucket / table kinds from aws.ts.
|
|
3671
|
+
provider: isAwsKind(ep.kind) ? "aws" : "self",
|
|
3481
3672
|
kind: ep.kind
|
|
3482
3673
|
};
|
|
3483
3674
|
graph.addNode(node.id, node);
|
|
@@ -3517,7 +3708,7 @@ init_cjs_shims();
|
|
|
3517
3708
|
|
|
3518
3709
|
// src/extract/infra/docker-compose.ts
|
|
3519
3710
|
init_cjs_shims();
|
|
3520
|
-
var
|
|
3711
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3521
3712
|
var import_types16 = require("@neat.is/types");
|
|
3522
3713
|
|
|
3523
3714
|
// src/extract/infra/shared.ts
|
|
@@ -3555,7 +3746,7 @@ function dependsOnList(value) {
|
|
|
3555
3746
|
}
|
|
3556
3747
|
function serviceNameToServiceNode(name, services) {
|
|
3557
3748
|
for (const s of services) {
|
|
3558
|
-
if (s.node.name === name ||
|
|
3749
|
+
if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
|
|
3559
3750
|
}
|
|
3560
3751
|
return null;
|
|
3561
3752
|
}
|
|
@@ -3564,7 +3755,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3564
3755
|
let edgesAdded = 0;
|
|
3565
3756
|
let composePath = null;
|
|
3566
3757
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3567
|
-
const abs =
|
|
3758
|
+
const abs = import_node_path25.default.join(scanPath, name);
|
|
3568
3759
|
if (await exists(abs)) {
|
|
3569
3760
|
composePath = abs;
|
|
3570
3761
|
break;
|
|
@@ -3575,13 +3766,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3575
3766
|
try {
|
|
3576
3767
|
compose = await readYaml(composePath);
|
|
3577
3768
|
} catch (err) {
|
|
3578
|
-
|
|
3579
|
-
|
|
3769
|
+
recordExtractionError(
|
|
3770
|
+
"infra docker-compose",
|
|
3771
|
+
import_node_path25.default.relative(scanPath, composePath),
|
|
3772
|
+
err
|
|
3580
3773
|
);
|
|
3581
3774
|
return { nodesAdded, edgesAdded };
|
|
3582
3775
|
}
|
|
3583
3776
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3584
|
-
const evidenceFile =
|
|
3777
|
+
const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
|
|
3585
3778
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3586
3779
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3587
3780
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3622,8 +3815,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3622
3815
|
|
|
3623
3816
|
// src/extract/infra/dockerfile.ts
|
|
3624
3817
|
init_cjs_shims();
|
|
3625
|
-
var
|
|
3626
|
-
var
|
|
3818
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3819
|
+
var import_node_fs14 = require("fs");
|
|
3627
3820
|
var import_types17 = require("@neat.is/types");
|
|
3628
3821
|
function runtimeImage(content) {
|
|
3629
3822
|
const lines = content.split("\n");
|
|
@@ -3643,14 +3836,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3643
3836
|
let nodesAdded = 0;
|
|
3644
3837
|
let edgesAdded = 0;
|
|
3645
3838
|
for (const service of services) {
|
|
3646
|
-
const dockerfilePath =
|
|
3839
|
+
const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
|
|
3647
3840
|
if (!await exists(dockerfilePath)) continue;
|
|
3648
3841
|
let content;
|
|
3649
3842
|
try {
|
|
3650
|
-
content = await
|
|
3843
|
+
content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
|
|
3651
3844
|
} catch (err) {
|
|
3652
|
-
|
|
3653
|
-
|
|
3845
|
+
recordExtractionError(
|
|
3846
|
+
"infra dockerfile",
|
|
3847
|
+
import_node_path26.default.relative(scanPath, dockerfilePath),
|
|
3848
|
+
err
|
|
3654
3849
|
);
|
|
3655
3850
|
continue;
|
|
3656
3851
|
}
|
|
@@ -3670,7 +3865,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3670
3865
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3671
3866
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3672
3867
|
evidence: {
|
|
3673
|
-
file:
|
|
3868
|
+
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
3674
3869
|
}
|
|
3675
3870
|
};
|
|
3676
3871
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3682,19 +3877,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3682
3877
|
|
|
3683
3878
|
// src/extract/infra/terraform.ts
|
|
3684
3879
|
init_cjs_shims();
|
|
3685
|
-
var
|
|
3686
|
-
var
|
|
3880
|
+
var import_node_fs15 = require("fs");
|
|
3881
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3687
3882
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3688
3883
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3689
3884
|
if (depth > max) return [];
|
|
3690
3885
|
const out = [];
|
|
3691
|
-
const entries = await
|
|
3886
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3692
3887
|
for (const entry2 of entries) {
|
|
3693
3888
|
if (entry2.isDirectory()) {
|
|
3694
3889
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
3695
|
-
out.push(...await walkTfFiles(
|
|
3890
|
+
out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
|
|
3696
3891
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
3697
|
-
out.push(
|
|
3892
|
+
out.push(import_node_path27.default.join(start, entry2.name));
|
|
3698
3893
|
}
|
|
3699
3894
|
}
|
|
3700
3895
|
return out;
|
|
@@ -3703,7 +3898,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3703
3898
|
let nodesAdded = 0;
|
|
3704
3899
|
const files = await walkTfFiles(scanPath);
|
|
3705
3900
|
for (const file of files) {
|
|
3706
|
-
const content = await
|
|
3901
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3707
3902
|
RESOURCE_RE.lastIndex = 0;
|
|
3708
3903
|
let m;
|
|
3709
3904
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3721,8 +3916,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3721
3916
|
|
|
3722
3917
|
// src/extract/infra/k8s.ts
|
|
3723
3918
|
init_cjs_shims();
|
|
3724
|
-
var
|
|
3725
|
-
var
|
|
3919
|
+
var import_node_fs16 = require("fs");
|
|
3920
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3726
3921
|
var import_yaml3 = require("yaml");
|
|
3727
3922
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3728
3923
|
Service: "k8s-service",
|
|
@@ -3736,13 +3931,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3736
3931
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3737
3932
|
if (depth > max) return [];
|
|
3738
3933
|
const out = [];
|
|
3739
|
-
const entries = await
|
|
3934
|
+
const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3740
3935
|
for (const entry2 of entries) {
|
|
3741
3936
|
if (entry2.isDirectory()) {
|
|
3742
3937
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3743
|
-
out.push(...await walkYamlFiles2(
|
|
3744
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3745
|
-
out.push(
|
|
3938
|
+
out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
|
|
3939
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
|
|
3940
|
+
out.push(import_node_path28.default.join(start, entry2.name));
|
|
3746
3941
|
}
|
|
3747
3942
|
}
|
|
3748
3943
|
return out;
|
|
@@ -3751,7 +3946,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3751
3946
|
let nodesAdded = 0;
|
|
3752
3947
|
const files = await walkYamlFiles2(scanPath);
|
|
3753
3948
|
for (const file of files) {
|
|
3754
|
-
const content = await
|
|
3949
|
+
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
3755
3950
|
let docs;
|
|
3756
3951
|
try {
|
|
3757
3952
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3785,9 +3980,46 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3785
3980
|
};
|
|
3786
3981
|
}
|
|
3787
3982
|
|
|
3983
|
+
// src/extract/retire.ts
|
|
3984
|
+
init_cjs_shims();
|
|
3985
|
+
var import_node_fs17 = require("fs");
|
|
3986
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
3987
|
+
var import_types18 = require("@neat.is/types");
|
|
3988
|
+
function retireEdgesByFile(graph, file) {
|
|
3989
|
+
const normalized = file.split("\\").join("/");
|
|
3990
|
+
const toDrop = [];
|
|
3991
|
+
graph.forEachEdge((id, attrs) => {
|
|
3992
|
+
const edge = attrs;
|
|
3993
|
+
if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
|
|
3994
|
+
if (!edge.evidence?.file) return;
|
|
3995
|
+
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
3996
|
+
});
|
|
3997
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
3998
|
+
return toDrop.length;
|
|
3999
|
+
}
|
|
4000
|
+
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
4001
|
+
const toDrop = [];
|
|
4002
|
+
const bases = [scanPath, ...serviceDirs];
|
|
4003
|
+
graph.forEachEdge((id, attrs) => {
|
|
4004
|
+
const edge = attrs;
|
|
4005
|
+
if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
|
|
4006
|
+
const evidenceFile = edge.evidence?.file;
|
|
4007
|
+
if (!evidenceFile) return;
|
|
4008
|
+
if (import_node_path29.default.isAbsolute(evidenceFile)) {
|
|
4009
|
+
if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
|
|
4013
|
+
if (!found) toDrop.push(id);
|
|
4014
|
+
});
|
|
4015
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
4016
|
+
return toDrop.length;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
3788
4019
|
// src/extract/index.ts
|
|
3789
4020
|
async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
3790
4021
|
await ensureCompatLoaded();
|
|
4022
|
+
drainExtractionErrors();
|
|
3791
4023
|
const services = await discoverServices(scanPath);
|
|
3792
4024
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
3793
4025
|
await addServiceAliases(graph, scanPath, services);
|
|
@@ -3795,12 +4027,30 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3795
4027
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
3796
4028
|
const phase4 = await addCallEdges(graph, services);
|
|
3797
4029
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
4030
|
+
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
4031
|
+
graph,
|
|
4032
|
+
scanPath,
|
|
4033
|
+
services.map((s) => s.dir)
|
|
4034
|
+
);
|
|
3798
4035
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3799
4036
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
4037
|
+
const errorEntries = drainExtractionErrors();
|
|
4038
|
+
if (opts.errorsPath && errorEntries.length > 0) {
|
|
4039
|
+
try {
|
|
4040
|
+
await writeExtractionErrors(errorEntries, opts.errorsPath);
|
|
4041
|
+
} catch (err) {
|
|
4042
|
+
console.warn(
|
|
4043
|
+
`[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
|
|
4044
|
+
);
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
3800
4047
|
const result = {
|
|
3801
4048
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3802
4049
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3803
|
-
frontiersPromoted
|
|
4050
|
+
frontiersPromoted,
|
|
4051
|
+
extractionErrors: errorEntries.length,
|
|
4052
|
+
errorEntries,
|
|
4053
|
+
ghostsRetired
|
|
3804
4054
|
};
|
|
3805
4055
|
emitNeatEvent({
|
|
3806
4056
|
type: "extraction-complete",
|
|
@@ -3817,8 +4067,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3817
4067
|
|
|
3818
4068
|
// src/persist.ts
|
|
3819
4069
|
init_cjs_shims();
|
|
3820
|
-
var
|
|
3821
|
-
var
|
|
4070
|
+
var import_node_fs18 = require("fs");
|
|
4071
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
3822
4072
|
var SCHEMA_VERSION = 2;
|
|
3823
4073
|
function migrateV1ToV2(payload) {
|
|
3824
4074
|
const nodes = payload.graph.nodes;
|
|
@@ -3832,7 +4082,7 @@ function migrateV1ToV2(payload) {
|
|
|
3832
4082
|
return { ...payload, schemaVersion: 2 };
|
|
3833
4083
|
}
|
|
3834
4084
|
async function ensureDir(filePath) {
|
|
3835
|
-
await
|
|
4085
|
+
await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(filePath), { recursive: true });
|
|
3836
4086
|
}
|
|
3837
4087
|
async function saveGraphToDisk(graph, outPath) {
|
|
3838
4088
|
await ensureDir(outPath);
|
|
@@ -3842,13 +4092,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3842
4092
|
graph: graph.export()
|
|
3843
4093
|
};
|
|
3844
4094
|
const tmp = `${outPath}.tmp`;
|
|
3845
|
-
await
|
|
3846
|
-
await
|
|
4095
|
+
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4096
|
+
await import_node_fs18.promises.rename(tmp, outPath);
|
|
3847
4097
|
}
|
|
3848
4098
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3849
4099
|
let raw;
|
|
3850
4100
|
try {
|
|
3851
|
-
raw = await
|
|
4101
|
+
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
3852
4102
|
} catch (err) {
|
|
3853
4103
|
if (err.code === "ENOENT") return;
|
|
3854
4104
|
throw err;
|
|
@@ -3901,19 +4151,19 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3901
4151
|
|
|
3902
4152
|
// src/watch.ts
|
|
3903
4153
|
init_cjs_shims();
|
|
3904
|
-
var
|
|
3905
|
-
var
|
|
4154
|
+
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
4155
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
3906
4156
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3907
4157
|
|
|
3908
4158
|
// src/api.ts
|
|
3909
4159
|
init_cjs_shims();
|
|
3910
4160
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3911
4161
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3912
|
-
var
|
|
4162
|
+
var import_types21 = require("@neat.is/types");
|
|
3913
4163
|
|
|
3914
4164
|
// src/divergences.ts
|
|
3915
4165
|
init_cjs_shims();
|
|
3916
|
-
var
|
|
4166
|
+
var import_types19 = require("@neat.is/types");
|
|
3917
4167
|
function bucketKey(source, target, type) {
|
|
3918
4168
|
return `${type}|${source}|${target}`;
|
|
3919
4169
|
}
|
|
@@ -3921,25 +4171,25 @@ function bucketEdges(graph) {
|
|
|
3921
4171
|
const buckets = /* @__PURE__ */ new Map();
|
|
3922
4172
|
graph.forEachEdge((id, attrs) => {
|
|
3923
4173
|
const e = attrs;
|
|
3924
|
-
const parsed = (0,
|
|
4174
|
+
const parsed = (0, import_types19.parseEdgeId)(id);
|
|
3925
4175
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
3926
4176
|
const key = bucketKey(e.source, e.target, e.type);
|
|
3927
4177
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3928
4178
|
switch (provenance) {
|
|
3929
|
-
case
|
|
4179
|
+
case import_types19.Provenance.EXTRACTED:
|
|
3930
4180
|
cur.extracted = e;
|
|
3931
4181
|
break;
|
|
3932
|
-
case
|
|
4182
|
+
case import_types19.Provenance.OBSERVED:
|
|
3933
4183
|
cur.observed = e;
|
|
3934
4184
|
break;
|
|
3935
|
-
case
|
|
4185
|
+
case import_types19.Provenance.INFERRED:
|
|
3936
4186
|
cur.inferred = e;
|
|
3937
4187
|
break;
|
|
3938
|
-
case
|
|
4188
|
+
case import_types19.Provenance.FRONTIER:
|
|
3939
4189
|
cur.frontier = e;
|
|
3940
4190
|
break;
|
|
3941
4191
|
default:
|
|
3942
|
-
if (e.provenance ===
|
|
4192
|
+
if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
|
|
3943
4193
|
}
|
|
3944
4194
|
buckets.set(key, cur);
|
|
3945
4195
|
});
|
|
@@ -3948,13 +4198,13 @@ function bucketEdges(graph) {
|
|
|
3948
4198
|
function nodeIsFrontier(graph, nodeId) {
|
|
3949
4199
|
if (!graph.hasNode(nodeId)) return false;
|
|
3950
4200
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
3951
|
-
return attrs.type ===
|
|
4201
|
+
return attrs.type === import_types19.NodeType.FrontierNode;
|
|
3952
4202
|
}
|
|
3953
4203
|
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3954
4204
|
if (!graph.hasNode(sourceId)) return false;
|
|
3955
4205
|
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3956
4206
|
const e = graph.getEdgeAttributes(edgeId);
|
|
3957
|
-
if (e.provenance ===
|
|
4207
|
+
if (e.provenance === import_types19.Provenance.OBSERVED) return true;
|
|
3958
4208
|
}
|
|
3959
4209
|
return false;
|
|
3960
4210
|
}
|
|
@@ -4015,7 +4265,7 @@ function declaredHostFor(svc) {
|
|
|
4015
4265
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4016
4266
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4017
4267
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4018
|
-
if (e.type ===
|
|
4268
|
+
if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
|
|
4019
4269
|
return true;
|
|
4020
4270
|
}
|
|
4021
4271
|
}
|
|
@@ -4028,10 +4278,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4028
4278
|
const out = [];
|
|
4029
4279
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4030
4280
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4031
|
-
if (edge.type !==
|
|
4032
|
-
if (edge.provenance !==
|
|
4281
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4282
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4033
4283
|
const target = graph.getNodeAttributes(edge.target);
|
|
4034
|
-
if (target.type !==
|
|
4284
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4035
4285
|
const observedHost = target.host?.trim();
|
|
4036
4286
|
if (!observedHost) continue;
|
|
4037
4287
|
if (observedHost === declaredHost) continue;
|
|
@@ -4053,10 +4303,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4053
4303
|
const deps = svc.dependencies ?? {};
|
|
4054
4304
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4055
4305
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4056
|
-
if (edge.type !==
|
|
4057
|
-
if (edge.provenance !==
|
|
4306
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4307
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4058
4308
|
const target = graph.getNodeAttributes(edge.target);
|
|
4059
|
-
if (target.type !==
|
|
4309
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4060
4310
|
for (const pair of compatPairs()) {
|
|
4061
4311
|
if (pair.engine !== target.engine) continue;
|
|
4062
4312
|
const declared = deps[pair.driver];
|
|
@@ -4117,7 +4367,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4117
4367
|
}
|
|
4118
4368
|
graph.forEachNode((nodeId, attrs) => {
|
|
4119
4369
|
const n = attrs;
|
|
4120
|
-
if (n.type !==
|
|
4370
|
+
if (n.type !== import_types19.NodeType.ServiceNode) return;
|
|
4121
4371
|
const svc = n;
|
|
4122
4372
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4123
4373
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4141,7 +4391,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4141
4391
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4142
4392
|
return a.target.localeCompare(b.target);
|
|
4143
4393
|
});
|
|
4144
|
-
return
|
|
4394
|
+
return import_types19.DivergenceResultSchema.parse({
|
|
4145
4395
|
divergences: filtered,
|
|
4146
4396
|
totalAffected: filtered.length,
|
|
4147
4397
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4150,7 +4400,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4150
4400
|
|
|
4151
4401
|
// src/diff.ts
|
|
4152
4402
|
init_cjs_shims();
|
|
4153
|
-
var
|
|
4403
|
+
var import_node_fs19 = require("fs");
|
|
4154
4404
|
async function loadSnapshotForDiff(target) {
|
|
4155
4405
|
if (/^https?:\/\//i.test(target)) {
|
|
4156
4406
|
const res = await fetch(target);
|
|
@@ -4159,7 +4409,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
4159
4409
|
}
|
|
4160
4410
|
return await res.json();
|
|
4161
4411
|
}
|
|
4162
|
-
const raw = await
|
|
4412
|
+
const raw = await import_node_fs19.promises.readFile(target, "utf8");
|
|
4163
4413
|
return JSON.parse(raw);
|
|
4164
4414
|
}
|
|
4165
4415
|
function indexEntries(entries) {
|
|
@@ -4227,23 +4477,23 @@ function canonicalJson(value) {
|
|
|
4227
4477
|
|
|
4228
4478
|
// src/projects.ts
|
|
4229
4479
|
init_cjs_shims();
|
|
4230
|
-
var
|
|
4480
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4231
4481
|
function pathsForProject(project, baseDir) {
|
|
4232
4482
|
if (project === DEFAULT_PROJECT) {
|
|
4233
4483
|
return {
|
|
4234
|
-
snapshotPath:
|
|
4235
|
-
errorsPath:
|
|
4236
|
-
staleEventsPath:
|
|
4237
|
-
embeddingsCachePath:
|
|
4238
|
-
policyViolationsPath:
|
|
4484
|
+
snapshotPath: import_node_path31.default.join(baseDir, "graph.json"),
|
|
4485
|
+
errorsPath: import_node_path31.default.join(baseDir, "errors.ndjson"),
|
|
4486
|
+
staleEventsPath: import_node_path31.default.join(baseDir, "stale-events.ndjson"),
|
|
4487
|
+
embeddingsCachePath: import_node_path31.default.join(baseDir, "embeddings.json"),
|
|
4488
|
+
policyViolationsPath: import_node_path31.default.join(baseDir, "policy-violations.ndjson")
|
|
4239
4489
|
};
|
|
4240
4490
|
}
|
|
4241
4491
|
return {
|
|
4242
|
-
snapshotPath:
|
|
4243
|
-
errorsPath:
|
|
4244
|
-
staleEventsPath:
|
|
4245
|
-
embeddingsCachePath:
|
|
4246
|
-
policyViolationsPath:
|
|
4492
|
+
snapshotPath: import_node_path31.default.join(baseDir, `${project}.json`),
|
|
4493
|
+
errorsPath: import_node_path31.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4494
|
+
staleEventsPath: import_node_path31.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4495
|
+
embeddingsCachePath: import_node_path31.default.join(baseDir, `embeddings.${project}.json`),
|
|
4496
|
+
policyViolationsPath: import_node_path31.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4247
4497
|
};
|
|
4248
4498
|
}
|
|
4249
4499
|
var Projects = class {
|
|
@@ -4279,49 +4529,49 @@ var Projects = class {
|
|
|
4279
4529
|
|
|
4280
4530
|
// src/registry.ts
|
|
4281
4531
|
init_cjs_shims();
|
|
4282
|
-
var
|
|
4532
|
+
var import_node_fs20 = require("fs");
|
|
4283
4533
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4284
|
-
var
|
|
4285
|
-
var
|
|
4534
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4535
|
+
var import_types20 = require("@neat.is/types");
|
|
4286
4536
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4287
4537
|
var LOCK_RETRY_MS = 50;
|
|
4288
4538
|
function neatHome() {
|
|
4289
4539
|
const override = process.env.NEAT_HOME;
|
|
4290
|
-
if (override && override.length > 0) return
|
|
4291
|
-
return
|
|
4540
|
+
if (override && override.length > 0) return import_node_path32.default.resolve(override);
|
|
4541
|
+
return import_node_path32.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4292
4542
|
}
|
|
4293
4543
|
function registryPath() {
|
|
4294
|
-
return
|
|
4544
|
+
return import_node_path32.default.join(neatHome(), "projects.json");
|
|
4295
4545
|
}
|
|
4296
4546
|
function registryLockPath() {
|
|
4297
|
-
return
|
|
4547
|
+
return import_node_path32.default.join(neatHome(), "projects.json.lock");
|
|
4298
4548
|
}
|
|
4299
4549
|
async function normalizeProjectPath(input) {
|
|
4300
|
-
const resolved =
|
|
4550
|
+
const resolved = import_node_path32.default.resolve(input);
|
|
4301
4551
|
try {
|
|
4302
|
-
return await
|
|
4552
|
+
return await import_node_fs20.promises.realpath(resolved);
|
|
4303
4553
|
} catch {
|
|
4304
4554
|
return resolved;
|
|
4305
4555
|
}
|
|
4306
4556
|
}
|
|
4307
4557
|
async function writeAtomically(target, contents) {
|
|
4308
|
-
await
|
|
4558
|
+
await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(target), { recursive: true });
|
|
4309
4559
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4310
|
-
const fd = await
|
|
4560
|
+
const fd = await import_node_fs20.promises.open(tmp, "w");
|
|
4311
4561
|
try {
|
|
4312
4562
|
await fd.writeFile(contents, "utf8");
|
|
4313
4563
|
await fd.sync();
|
|
4314
4564
|
} finally {
|
|
4315
4565
|
await fd.close();
|
|
4316
4566
|
}
|
|
4317
|
-
await
|
|
4567
|
+
await import_node_fs20.promises.rename(tmp, target);
|
|
4318
4568
|
}
|
|
4319
4569
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4320
4570
|
const deadline = Date.now() + timeoutMs;
|
|
4321
|
-
await
|
|
4571
|
+
await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(lockPath), { recursive: true });
|
|
4322
4572
|
while (true) {
|
|
4323
4573
|
try {
|
|
4324
|
-
const fd = await
|
|
4574
|
+
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
4325
4575
|
await fd.close();
|
|
4326
4576
|
return;
|
|
4327
4577
|
} catch (err) {
|
|
@@ -4337,7 +4587,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
4337
4587
|
}
|
|
4338
4588
|
}
|
|
4339
4589
|
async function releaseLock(lockPath) {
|
|
4340
|
-
await
|
|
4590
|
+
await import_node_fs20.promises.unlink(lockPath).catch(() => {
|
|
4341
4591
|
});
|
|
4342
4592
|
}
|
|
4343
4593
|
async function withLock(fn) {
|
|
@@ -4353,7 +4603,7 @@ async function readRegistry() {
|
|
|
4353
4603
|
const file = registryPath();
|
|
4354
4604
|
let raw;
|
|
4355
4605
|
try {
|
|
4356
|
-
raw = await
|
|
4606
|
+
raw = await import_node_fs20.promises.readFile(file, "utf8");
|
|
4357
4607
|
} catch (err) {
|
|
4358
4608
|
if (err.code === "ENOENT") {
|
|
4359
4609
|
return { version: 1, projects: [] };
|
|
@@ -4361,10 +4611,10 @@ async function readRegistry() {
|
|
|
4361
4611
|
throw err;
|
|
4362
4612
|
}
|
|
4363
4613
|
const parsed = JSON.parse(raw);
|
|
4364
|
-
return
|
|
4614
|
+
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4365
4615
|
}
|
|
4366
4616
|
async function writeRegistry(reg) {
|
|
4367
|
-
const validated =
|
|
4617
|
+
const validated = import_types20.RegistryFileSchema.parse(reg);
|
|
4368
4618
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4369
4619
|
}
|
|
4370
4620
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4615,11 +4865,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4615
4865
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4616
4866
|
const parsed = [];
|
|
4617
4867
|
for (const c of candidates) {
|
|
4618
|
-
const r =
|
|
4868
|
+
const r = import_types21.DivergenceTypeSchema.safeParse(c);
|
|
4619
4869
|
if (!r.success) {
|
|
4620
4870
|
return reply.code(400).send({
|
|
4621
4871
|
error: `unknown divergence type "${c}"`,
|
|
4622
|
-
allowed:
|
|
4872
|
+
allowed: import_types21.DivergenceTypeSchema.options
|
|
4623
4873
|
});
|
|
4624
4874
|
}
|
|
4625
4875
|
parsed.push(r.data);
|
|
@@ -4803,7 +5053,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4803
5053
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4804
5054
|
let violations = await log.readAll();
|
|
4805
5055
|
if (req.query.severity) {
|
|
4806
|
-
const sev =
|
|
5056
|
+
const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4807
5057
|
if (!sev.success) {
|
|
4808
5058
|
return reply.code(400).send({
|
|
4809
5059
|
error: "invalid severity",
|
|
@@ -4820,7 +5070,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4820
5070
|
scope.post("/policies/check", async (req, reply) => {
|
|
4821
5071
|
const proj = resolveProject(registry, req, reply);
|
|
4822
5072
|
if (!proj) return;
|
|
4823
|
-
const parsed =
|
|
5073
|
+
const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4824
5074
|
if (!parsed.success) {
|
|
4825
5075
|
return reply.code(400).send({
|
|
4826
5076
|
error: "invalid /policies/check body",
|
|
@@ -4918,30 +5168,14 @@ async function buildApi(opts) {
|
|
|
4918
5168
|
return app;
|
|
4919
5169
|
}
|
|
4920
5170
|
|
|
4921
|
-
// src/extract/retire.ts
|
|
4922
|
-
init_cjs_shims();
|
|
4923
|
-
var import_types21 = require("@neat.is/types");
|
|
4924
|
-
function retireEdgesByFile(graph, file) {
|
|
4925
|
-
const normalized = file.split("\\").join("/");
|
|
4926
|
-
const toDrop = [];
|
|
4927
|
-
graph.forEachEdge((id, attrs) => {
|
|
4928
|
-
const edge = attrs;
|
|
4929
|
-
if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
|
|
4930
|
-
if (!edge.evidence?.file) return;
|
|
4931
|
-
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4932
|
-
});
|
|
4933
|
-
for (const id of toDrop) graph.dropEdge(id);
|
|
4934
|
-
return toDrop.length;
|
|
4935
|
-
}
|
|
4936
|
-
|
|
4937
5171
|
// src/watch.ts
|
|
4938
5172
|
init_otel();
|
|
4939
5173
|
init_otel_grpc();
|
|
4940
5174
|
|
|
4941
5175
|
// src/search.ts
|
|
4942
5176
|
init_cjs_shims();
|
|
4943
|
-
var
|
|
4944
|
-
var
|
|
5177
|
+
var import_node_fs21 = require("fs");
|
|
5178
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
4945
5179
|
var import_node_crypto = require("crypto");
|
|
4946
5180
|
var DEFAULT_LIMIT = 10;
|
|
4947
5181
|
var NOMIC_DIM = 768;
|
|
@@ -5071,7 +5305,7 @@ async function pickEmbedder() {
|
|
|
5071
5305
|
}
|
|
5072
5306
|
async function readCache(cachePath) {
|
|
5073
5307
|
try {
|
|
5074
|
-
const raw = await
|
|
5308
|
+
const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
|
|
5075
5309
|
const parsed = JSON.parse(raw);
|
|
5076
5310
|
if (parsed.version !== 1) return null;
|
|
5077
5311
|
return parsed;
|
|
@@ -5080,8 +5314,8 @@ async function readCache(cachePath) {
|
|
|
5080
5314
|
}
|
|
5081
5315
|
}
|
|
5082
5316
|
async function writeCache(cachePath, cache) {
|
|
5083
|
-
await
|
|
5084
|
-
await
|
|
5317
|
+
await import_node_fs21.promises.mkdir(import_node_path35.default.dirname(cachePath), { recursive: true });
|
|
5318
|
+
await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
5085
5319
|
}
|
|
5086
5320
|
var VectorIndex = class {
|
|
5087
5321
|
constructor(embedder, cachePath) {
|
|
@@ -5236,8 +5470,8 @@ var ALL_PHASES = [
|
|
|
5236
5470
|
];
|
|
5237
5471
|
function classifyChange(relPath) {
|
|
5238
5472
|
const phases = /* @__PURE__ */ new Set();
|
|
5239
|
-
const base =
|
|
5240
|
-
const segments = relPath.split(
|
|
5473
|
+
const base = import_node_path36.default.basename(relPath).toLowerCase();
|
|
5474
|
+
const segments = relPath.split(import_node_path36.default.sep).map((s) => s.toLowerCase());
|
|
5241
5475
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
5242
5476
|
phases.add("services");
|
|
5243
5477
|
phases.add("aliases");
|
|
@@ -5332,16 +5566,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
5332
5566
|
if (count >= limit) return;
|
|
5333
5567
|
let entries;
|
|
5334
5568
|
try {
|
|
5335
|
-
entries =
|
|
5569
|
+
entries = import_node_fs22.default.readdirSync(dir, { withFileTypes: true });
|
|
5336
5570
|
} catch {
|
|
5337
5571
|
return;
|
|
5338
5572
|
}
|
|
5339
5573
|
for (const e of entries) {
|
|
5340
5574
|
if (count >= limit) return;
|
|
5341
5575
|
if (!e.isDirectory()) continue;
|
|
5342
|
-
if (IGNORED_WATCH_PATHS.some((re) => re.test(
|
|
5576
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path36.default.join(dir, e.name) + import_node_path36.default.sep))) continue;
|
|
5343
5577
|
count++;
|
|
5344
|
-
if (depth < 2) visit(
|
|
5578
|
+
if (depth < 2) visit(import_node_path36.default.join(dir, e.name), depth + 1);
|
|
5345
5579
|
}
|
|
5346
5580
|
};
|
|
5347
5581
|
visit(scanPath, 0);
|
|
@@ -5359,8 +5593,8 @@ async function startWatch(graph, opts) {
|
|
|
5359
5593
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
5360
5594
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
5361
5595
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
5362
|
-
const policyFilePath =
|
|
5363
|
-
const policyViolationsPath =
|
|
5596
|
+
const policyFilePath = import_node_path36.default.join(opts.scanPath, "policy.json");
|
|
5597
|
+
const policyViolationsPath = import_node_path36.default.join(import_node_path36.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
5364
5598
|
let policies = [];
|
|
5365
5599
|
try {
|
|
5366
5600
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -5409,7 +5643,7 @@ async function startWatch(graph, opts) {
|
|
|
5409
5643
|
const host = opts.host ?? "0.0.0.0";
|
|
5410
5644
|
const port = opts.port ?? 8080;
|
|
5411
5645
|
const otelPort = opts.otelPort ?? 4318;
|
|
5412
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
5646
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path36.default.join(import_node_path36.default.dirname(opts.outPath), "embeddings.json");
|
|
5413
5647
|
let searchIndex;
|
|
5414
5648
|
try {
|
|
5415
5649
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -5427,7 +5661,7 @@ async function startWatch(graph, opts) {
|
|
|
5427
5661
|
// Paths are derived from the explicit options the watch caller passes
|
|
5428
5662
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
5429
5663
|
// fields so the registry shape is complete.
|
|
5430
|
-
...pathsForProject(projectName,
|
|
5664
|
+
...pathsForProject(projectName, import_node_path36.default.dirname(opts.outPath)),
|
|
5431
5665
|
snapshotPath: opts.outPath,
|
|
5432
5666
|
errorsPath: opts.errorsPath,
|
|
5433
5667
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -5512,9 +5746,9 @@ async function startWatch(graph, opts) {
|
|
|
5512
5746
|
};
|
|
5513
5747
|
const onPath = (absPath) => {
|
|
5514
5748
|
if (shouldIgnore(absPath)) return;
|
|
5515
|
-
const rel =
|
|
5749
|
+
const rel = import_node_path36.default.relative(opts.scanPath, absPath);
|
|
5516
5750
|
if (!rel || rel.startsWith("..")) return;
|
|
5517
|
-
pendingPaths.add(rel.split(
|
|
5751
|
+
pendingPaths.add(rel.split(import_node_path36.default.sep).join("/"));
|
|
5518
5752
|
const phases = classifyChange(rel);
|
|
5519
5753
|
if (phases.size === 0) {
|
|
5520
5754
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -5571,8 +5805,8 @@ init_cjs_shims();
|
|
|
5571
5805
|
|
|
5572
5806
|
// src/installers/javascript.ts
|
|
5573
5807
|
init_cjs_shims();
|
|
5574
|
-
var
|
|
5575
|
-
var
|
|
5808
|
+
var import_node_fs23 = require("fs");
|
|
5809
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5576
5810
|
var SDK_PACKAGES = [
|
|
5577
5811
|
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
5578
5812
|
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
@@ -5588,7 +5822,7 @@ var OTEL_ENV = {
|
|
|
5588
5822
|
};
|
|
5589
5823
|
async function readPackageJson(serviceDir) {
|
|
5590
5824
|
try {
|
|
5591
|
-
const raw = await
|
|
5825
|
+
const raw = await import_node_fs23.promises.readFile(import_node_path37.default.join(serviceDir, "package.json"), "utf8");
|
|
5592
5826
|
return JSON.parse(raw);
|
|
5593
5827
|
} catch {
|
|
5594
5828
|
return null;
|
|
@@ -5607,7 +5841,7 @@ function rewriteStartScript(start) {
|
|
|
5607
5841
|
}
|
|
5608
5842
|
async function plan(serviceDir) {
|
|
5609
5843
|
const pkg = await readPackageJson(serviceDir);
|
|
5610
|
-
const manifestPath =
|
|
5844
|
+
const manifestPath = import_node_path37.default.join(serviceDir, "package.json");
|
|
5611
5845
|
const empty = {
|
|
5612
5846
|
language: "javascript",
|
|
5613
5847
|
serviceDir,
|
|
@@ -5654,7 +5888,7 @@ async function apply(installPlan) {
|
|
|
5654
5888
|
const originals = /* @__PURE__ */ new Map();
|
|
5655
5889
|
for (const file of touched) {
|
|
5656
5890
|
try {
|
|
5657
|
-
originals.set(file, await
|
|
5891
|
+
originals.set(file, await import_node_fs23.promises.readFile(file, "utf8"));
|
|
5658
5892
|
} catch {
|
|
5659
5893
|
}
|
|
5660
5894
|
}
|
|
@@ -5680,8 +5914,8 @@ async function apply(installPlan) {
|
|
|
5680
5914
|
}
|
|
5681
5915
|
const newRaw = JSON.stringify(pkg, null, 2) + "\n";
|
|
5682
5916
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
5683
|
-
await
|
|
5684
|
-
await
|
|
5917
|
+
await import_node_fs23.promises.writeFile(tmp, newRaw, "utf8");
|
|
5918
|
+
await import_node_fs23.promises.rename(tmp, file);
|
|
5685
5919
|
}
|
|
5686
5920
|
} catch (err) {
|
|
5687
5921
|
await rollback(installPlan, originals);
|
|
@@ -5692,7 +5926,7 @@ async function rollback(installPlan, originals) {
|
|
|
5692
5926
|
const restored = [];
|
|
5693
5927
|
for (const [file, raw] of originals.entries()) {
|
|
5694
5928
|
try {
|
|
5695
|
-
await
|
|
5929
|
+
await import_node_fs23.promises.writeFile(file, raw, "utf8");
|
|
5696
5930
|
restored.push(file);
|
|
5697
5931
|
} catch {
|
|
5698
5932
|
}
|
|
@@ -5706,8 +5940,8 @@ async function rollback(installPlan, originals) {
|
|
|
5706
5940
|
...restored.map((f) => `restored: ${f}`),
|
|
5707
5941
|
""
|
|
5708
5942
|
];
|
|
5709
|
-
const rollbackPath =
|
|
5710
|
-
await
|
|
5943
|
+
const rollbackPath = import_node_path37.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
5944
|
+
await import_node_fs23.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
5711
5945
|
}
|
|
5712
5946
|
var javascriptInstaller = {
|
|
5713
5947
|
name: "javascript",
|
|
@@ -5718,8 +5952,8 @@ var javascriptInstaller = {
|
|
|
5718
5952
|
|
|
5719
5953
|
// src/installers/python.ts
|
|
5720
5954
|
init_cjs_shims();
|
|
5721
|
-
var
|
|
5722
|
-
var
|
|
5955
|
+
var import_node_fs24 = require("fs");
|
|
5956
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
5723
5957
|
var SDK_PACKAGES2 = [
|
|
5724
5958
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
5725
5959
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -5731,7 +5965,7 @@ var OTEL_ENV2 = {
|
|
|
5731
5965
|
};
|
|
5732
5966
|
async function exists2(p) {
|
|
5733
5967
|
try {
|
|
5734
|
-
await
|
|
5968
|
+
await import_node_fs24.promises.stat(p);
|
|
5735
5969
|
return true;
|
|
5736
5970
|
} catch {
|
|
5737
5971
|
return false;
|
|
@@ -5740,7 +5974,7 @@ async function exists2(p) {
|
|
|
5740
5974
|
async function detect2(serviceDir) {
|
|
5741
5975
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
5742
5976
|
for (const m of markers) {
|
|
5743
|
-
if (await exists2(
|
|
5977
|
+
if (await exists2(import_node_path38.default.join(serviceDir, m))) return true;
|
|
5744
5978
|
}
|
|
5745
5979
|
return false;
|
|
5746
5980
|
}
|
|
@@ -5750,9 +5984,9 @@ function reqPackageName(line) {
|
|
|
5750
5984
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
5751
5985
|
}
|
|
5752
5986
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
5753
|
-
const file =
|
|
5987
|
+
const file = import_node_path38.default.join(serviceDir, "requirements.txt");
|
|
5754
5988
|
if (!await exists2(file)) return null;
|
|
5755
|
-
const raw = await
|
|
5989
|
+
const raw = await import_node_fs24.promises.readFile(file, "utf8");
|
|
5756
5990
|
const presentNames = new Set(
|
|
5757
5991
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
5758
5992
|
);
|
|
@@ -5760,9 +5994,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
5760
5994
|
return { manifest: file, missing: [...missing] };
|
|
5761
5995
|
}
|
|
5762
5996
|
async function planProcfileEdits(serviceDir) {
|
|
5763
|
-
const procfile =
|
|
5997
|
+
const procfile = import_node_path38.default.join(serviceDir, "Procfile");
|
|
5764
5998
|
if (!await exists2(procfile)) return [];
|
|
5765
|
-
const raw = await
|
|
5999
|
+
const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
|
|
5766
6000
|
const edits = [];
|
|
5767
6001
|
for (const line of raw.split(/\r?\n/)) {
|
|
5768
6002
|
if (line.length === 0) continue;
|
|
@@ -5814,8 +6048,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
5814
6048
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
5815
6049
|
`;
|
|
5816
6050
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
5817
|
-
await
|
|
5818
|
-
await
|
|
6051
|
+
await import_node_fs24.promises.writeFile(tmp, next, "utf8");
|
|
6052
|
+
await import_node_fs24.promises.rename(tmp, manifest);
|
|
5819
6053
|
}
|
|
5820
6054
|
async function applyProcfile(procfile, edits, original) {
|
|
5821
6055
|
let next = original;
|
|
@@ -5824,8 +6058,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
5824
6058
|
next = next.replace(e.before, e.after);
|
|
5825
6059
|
}
|
|
5826
6060
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
5827
|
-
await
|
|
5828
|
-
await
|
|
6061
|
+
await import_node_fs24.promises.writeFile(tmp, next, "utf8");
|
|
6062
|
+
await import_node_fs24.promises.rename(tmp, procfile);
|
|
5829
6063
|
}
|
|
5830
6064
|
async function apply2(installPlan) {
|
|
5831
6065
|
const touched = /* @__PURE__ */ new Set();
|
|
@@ -5835,7 +6069,7 @@ async function apply2(installPlan) {
|
|
|
5835
6069
|
const originals = /* @__PURE__ */ new Map();
|
|
5836
6070
|
for (const file of touched) {
|
|
5837
6071
|
try {
|
|
5838
|
-
originals.set(file, await
|
|
6072
|
+
originals.set(file, await import_node_fs24.promises.readFile(file, "utf8"));
|
|
5839
6073
|
} catch {
|
|
5840
6074
|
}
|
|
5841
6075
|
}
|
|
@@ -5845,7 +6079,7 @@ async function apply2(installPlan) {
|
|
|
5845
6079
|
if (raw === void 0) {
|
|
5846
6080
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
5847
6081
|
}
|
|
5848
|
-
const base =
|
|
6082
|
+
const base = import_node_path38.default.basename(file);
|
|
5849
6083
|
if (base === "requirements.txt") {
|
|
5850
6084
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
5851
6085
|
if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
|
|
@@ -5863,7 +6097,7 @@ async function rollback2(installPlan, originals) {
|
|
|
5863
6097
|
const restored = [];
|
|
5864
6098
|
for (const [file, raw] of originals.entries()) {
|
|
5865
6099
|
try {
|
|
5866
|
-
await
|
|
6100
|
+
await import_node_fs24.promises.writeFile(file, raw, "utf8");
|
|
5867
6101
|
restored.push(file);
|
|
5868
6102
|
} catch {
|
|
5869
6103
|
}
|
|
@@ -5877,8 +6111,8 @@ async function rollback2(installPlan, originals) {
|
|
|
5877
6111
|
...restored.map((f) => `restored: ${f}`),
|
|
5878
6112
|
""
|
|
5879
6113
|
];
|
|
5880
|
-
const rollbackPath =
|
|
5881
|
-
await
|
|
6114
|
+
const rollbackPath = import_node_path38.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
6115
|
+
await import_node_fs24.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
5882
6116
|
}
|
|
5883
6117
|
var pythonInstaller = {
|
|
5884
6118
|
name: "python",
|
|
@@ -5991,10 +6225,10 @@ var TransportError = class extends Error {
|
|
|
5991
6225
|
function createHttpClient(baseUrl) {
|
|
5992
6226
|
const root = baseUrl.replace(/\/$/, "");
|
|
5993
6227
|
return {
|
|
5994
|
-
async get(
|
|
6228
|
+
async get(path40) {
|
|
5995
6229
|
let res;
|
|
5996
6230
|
try {
|
|
5997
|
-
res = await fetch(`${root}${
|
|
6231
|
+
res = await fetch(`${root}${path40}`);
|
|
5998
6232
|
} catch (err) {
|
|
5999
6233
|
throw new TransportError(
|
|
6000
6234
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -6004,16 +6238,16 @@ function createHttpClient(baseUrl) {
|
|
|
6004
6238
|
const body = await res.text().catch(() => "");
|
|
6005
6239
|
throw new HttpError(
|
|
6006
6240
|
res.status,
|
|
6007
|
-
`${res.status} ${res.statusText} on GET ${
|
|
6241
|
+
`${res.status} ${res.statusText} on GET ${path40}: ${body}`,
|
|
6008
6242
|
body
|
|
6009
6243
|
);
|
|
6010
6244
|
}
|
|
6011
6245
|
return await res.json();
|
|
6012
6246
|
},
|
|
6013
|
-
async post(
|
|
6247
|
+
async post(path40, body) {
|
|
6014
6248
|
let res;
|
|
6015
6249
|
try {
|
|
6016
|
-
res = await fetch(`${root}${
|
|
6250
|
+
res = await fetch(`${root}${path40}`, {
|
|
6017
6251
|
method: "POST",
|
|
6018
6252
|
headers: { "content-type": "application/json" },
|
|
6019
6253
|
body: JSON.stringify(body)
|
|
@@ -6027,7 +6261,7 @@ function createHttpClient(baseUrl) {
|
|
|
6027
6261
|
const text = await res.text().catch(() => "");
|
|
6028
6262
|
throw new HttpError(
|
|
6029
6263
|
res.status,
|
|
6030
|
-
`${res.status} ${res.statusText} on POST ${
|
|
6264
|
+
`${res.status} ${res.statusText} on POST ${path40}: ${text}`,
|
|
6031
6265
|
text
|
|
6032
6266
|
);
|
|
6033
6267
|
}
|
|
@@ -6041,12 +6275,12 @@ function projectPath(project, suffix) {
|
|
|
6041
6275
|
}
|
|
6042
6276
|
async function runRootCause(client, input) {
|
|
6043
6277
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
6044
|
-
const
|
|
6278
|
+
const path40 = projectPath(
|
|
6045
6279
|
input.project,
|
|
6046
6280
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
6047
6281
|
);
|
|
6048
6282
|
try {
|
|
6049
|
-
const result = await client.get(
|
|
6283
|
+
const result = await client.get(path40);
|
|
6050
6284
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
6051
6285
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
6052
6286
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -6072,12 +6306,12 @@ async function runRootCause(client, input) {
|
|
|
6072
6306
|
}
|
|
6073
6307
|
async function runBlastRadius(client, input) {
|
|
6074
6308
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
6075
|
-
const
|
|
6309
|
+
const path40 = projectPath(
|
|
6076
6310
|
input.project,
|
|
6077
6311
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
6078
6312
|
);
|
|
6079
6313
|
try {
|
|
6080
|
-
const result = await client.get(
|
|
6314
|
+
const result = await client.get(path40);
|
|
6081
6315
|
if (result.totalAffected === 0) {
|
|
6082
6316
|
return {
|
|
6083
6317
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -6111,12 +6345,12 @@ function formatBlastEntry(n) {
|
|
|
6111
6345
|
}
|
|
6112
6346
|
async function runDependencies(client, input) {
|
|
6113
6347
|
const depth = input.depth ?? 3;
|
|
6114
|
-
const
|
|
6348
|
+
const path40 = projectPath(
|
|
6115
6349
|
input.project,
|
|
6116
6350
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
6117
6351
|
);
|
|
6118
6352
|
try {
|
|
6119
|
-
const result = await client.get(
|
|
6353
|
+
const result = await client.get(path40);
|
|
6120
6354
|
if (result.total === 0) {
|
|
6121
6355
|
return {
|
|
6122
6356
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -6197,9 +6431,9 @@ function formatDuration(ms) {
|
|
|
6197
6431
|
return `${Math.round(h / 24)}d`;
|
|
6198
6432
|
}
|
|
6199
6433
|
async function runIncidents(client, input) {
|
|
6200
|
-
const
|
|
6434
|
+
const path40 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
6201
6435
|
try {
|
|
6202
|
-
const body = await client.get(
|
|
6436
|
+
const body = await client.get(path40);
|
|
6203
6437
|
const events = body.events;
|
|
6204
6438
|
if (events.length === 0) {
|
|
6205
6439
|
return {
|
|
@@ -6714,7 +6948,7 @@ async function buildPatchSections(services) {
|
|
|
6714
6948
|
}
|
|
6715
6949
|
async function runInit(opts) {
|
|
6716
6950
|
const written = [];
|
|
6717
|
-
const stat = await
|
|
6951
|
+
const stat = await import_node_fs25.promises.stat(opts.scanPath).catch(() => null);
|
|
6718
6952
|
if (!stat || !stat.isDirectory()) {
|
|
6719
6953
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
6720
6954
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -6723,9 +6957,9 @@ async function runInit(opts) {
|
|
|
6723
6957
|
printDiscoveryReport(opts, services);
|
|
6724
6958
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
6725
6959
|
const patch = renderPatch(sections);
|
|
6726
|
-
const patchPath =
|
|
6960
|
+
const patchPath = import_node_path39.default.join(opts.scanPath, "neat.patch");
|
|
6727
6961
|
if (opts.dryRun) {
|
|
6728
|
-
await
|
|
6962
|
+
await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
|
|
6729
6963
|
written.push(patchPath);
|
|
6730
6964
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
6731
6965
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -6734,7 +6968,12 @@ async function runInit(opts) {
|
|
|
6734
6968
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
6735
6969
|
resetGraph(graphKey);
|
|
6736
6970
|
const graph = getGraph(graphKey);
|
|
6737
|
-
const
|
|
6971
|
+
const projectPaths = pathsForProject(
|
|
6972
|
+
graphKey,
|
|
6973
|
+
import_node_path39.default.join(opts.scanPath, "neat-out")
|
|
6974
|
+
);
|
|
6975
|
+
const errorsPath = import_node_path39.default.join(import_node_path39.default.dirname(opts.outPath), import_node_path39.default.basename(projectPaths.errorsPath));
|
|
6976
|
+
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
6738
6977
|
await saveGraphToDisk(graph, opts.outPath);
|
|
6739
6978
|
written.push(opts.outPath);
|
|
6740
6979
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
@@ -6765,7 +7004,7 @@ async function runInit(opts) {
|
|
|
6765
7004
|
console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
6766
7005
|
}
|
|
6767
7006
|
} else {
|
|
6768
|
-
await
|
|
7007
|
+
await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
|
|
6769
7008
|
written.push(patchPath);
|
|
6770
7009
|
}
|
|
6771
7010
|
}
|
|
@@ -6779,6 +7018,10 @@ async function runInit(opts) {
|
|
|
6779
7018
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
6780
7019
|
console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
|
|
6781
7020
|
console.log(summarise(nodes, edges));
|
|
7021
|
+
console.log(formatExtractionBanner(result.extractionErrors));
|
|
7022
|
+
if (result.extractionErrors > 0) {
|
|
7023
|
+
console.log(`errors: ${errorsPath}`);
|
|
7024
|
+
}
|
|
6782
7025
|
const incompatibilities = findIncompatibilities(nodes);
|
|
6783
7026
|
if (incompatibilities.length > 0) {
|
|
6784
7027
|
console.log("");
|
|
@@ -6789,6 +7032,9 @@ async function runInit(opts) {
|
|
|
6789
7032
|
}
|
|
6790
7033
|
}
|
|
6791
7034
|
}
|
|
7035
|
+
if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
|
|
7036
|
+
return { exitCode: 4, writtenFiles: written };
|
|
7037
|
+
}
|
|
6792
7038
|
return { exitCode: 0, writtenFiles: written };
|
|
6793
7039
|
}
|
|
6794
7040
|
var CLAUDE_SKILL_CONFIG = {
|
|
@@ -6805,9 +7051,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
6805
7051
|
};
|
|
6806
7052
|
function claudeConfigPath() {
|
|
6807
7053
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
6808
|
-
if (override && override.length > 0) return
|
|
7054
|
+
if (override && override.length > 0) return import_node_path39.default.resolve(override);
|
|
6809
7055
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6810
|
-
return
|
|
7056
|
+
return import_node_path39.default.join(home, ".claude.json");
|
|
6811
7057
|
}
|
|
6812
7058
|
async function runSkill(opts) {
|
|
6813
7059
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -6819,7 +7065,7 @@ async function runSkill(opts) {
|
|
|
6819
7065
|
const target = claudeConfigPath();
|
|
6820
7066
|
let existing = {};
|
|
6821
7067
|
try {
|
|
6822
|
-
existing = JSON.parse(await
|
|
7068
|
+
existing = JSON.parse(await import_node_fs25.promises.readFile(target, "utf8"));
|
|
6823
7069
|
} catch (err) {
|
|
6824
7070
|
if (err.code !== "ENOENT") {
|
|
6825
7071
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -6831,8 +7077,8 @@ async function runSkill(opts) {
|
|
|
6831
7077
|
...existing,
|
|
6832
7078
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
6833
7079
|
};
|
|
6834
|
-
await
|
|
6835
|
-
await
|
|
7080
|
+
await import_node_fs25.promises.mkdir(import_node_path39.default.dirname(target), { recursive: true });
|
|
7081
|
+
await import_node_fs25.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
6836
7082
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
6837
7083
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
6838
7084
|
return { exitCode: 0 };
|
|
@@ -6866,12 +7112,12 @@ async function main() {
|
|
|
6866
7112
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
6867
7113
|
process.exit(2);
|
|
6868
7114
|
}
|
|
6869
|
-
const scanPath =
|
|
7115
|
+
const scanPath = import_node_path39.default.resolve(target);
|
|
6870
7116
|
const projectExplicit = parsed.project !== null;
|
|
6871
|
-
const projectName = projectExplicit ? project :
|
|
7117
|
+
const projectName = projectExplicit ? project : import_node_path39.default.basename(scanPath);
|
|
6872
7118
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
6873
|
-
const fallback = pathsForProject(projectKey,
|
|
6874
|
-
const outPath =
|
|
7119
|
+
const fallback = pathsForProject(projectKey, import_node_path39.default.join(scanPath, "neat-out")).snapshotPath;
|
|
7120
|
+
const outPath = import_node_path39.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
6875
7121
|
const result = await runInit({
|
|
6876
7122
|
scanPath,
|
|
6877
7123
|
outPath,
|
|
@@ -6891,21 +7137,21 @@ async function main() {
|
|
|
6891
7137
|
usage();
|
|
6892
7138
|
process.exit(2);
|
|
6893
7139
|
}
|
|
6894
|
-
const scanPath =
|
|
6895
|
-
const stat = await
|
|
7140
|
+
const scanPath = import_node_path39.default.resolve(target);
|
|
7141
|
+
const stat = await import_node_fs25.promises.stat(scanPath).catch(() => null);
|
|
6896
7142
|
if (!stat || !stat.isDirectory()) {
|
|
6897
7143
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
6898
7144
|
process.exit(2);
|
|
6899
7145
|
}
|
|
6900
|
-
const projectPaths = pathsForProject(project,
|
|
6901
|
-
const outPath =
|
|
6902
|
-
const errorsPath =
|
|
6903
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
7146
|
+
const projectPaths = pathsForProject(project, import_node_path39.default.join(scanPath, "neat-out"));
|
|
7147
|
+
const outPath = import_node_path39.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
7148
|
+
const errorsPath = import_node_path39.default.resolve(
|
|
7149
|
+
process.env.NEAT_ERRORS_PATH ?? import_node_path39.default.join(import_node_path39.default.dirname(outPath), import_node_path39.default.basename(projectPaths.errorsPath))
|
|
6904
7150
|
);
|
|
6905
|
-
const staleEventsPath =
|
|
6906
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
7151
|
+
const staleEventsPath = import_node_path39.default.resolve(
|
|
7152
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path39.default.join(import_node_path39.default.dirname(outPath), import_node_path39.default.basename(projectPaths.staleEventsPath))
|
|
6907
7153
|
);
|
|
6908
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
7154
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path39.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
6909
7155
|
const handle = await startWatch(getGraph(project), {
|
|
6910
7156
|
scanPath,
|
|
6911
7157
|
outPath,
|