@neat.is/core 0.3.1 → 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 +601 -306
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +100 -53
- 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 +470 -228
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +16 -4
- package/dist/neatd.js.map +1 -1
- 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,18 +4151,19 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3901
4151
|
|
|
3902
4152
|
// src/watch.ts
|
|
3903
4153
|
init_cjs_shims();
|
|
3904
|
-
var
|
|
4154
|
+
var import_node_fs22 = __toESM(require("fs"), 1);
|
|
4155
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
3905
4156
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
3906
4157
|
|
|
3907
4158
|
// src/api.ts
|
|
3908
4159
|
init_cjs_shims();
|
|
3909
4160
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3910
4161
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3911
|
-
var
|
|
4162
|
+
var import_types21 = require("@neat.is/types");
|
|
3912
4163
|
|
|
3913
4164
|
// src/divergences.ts
|
|
3914
4165
|
init_cjs_shims();
|
|
3915
|
-
var
|
|
4166
|
+
var import_types19 = require("@neat.is/types");
|
|
3916
4167
|
function bucketKey(source, target, type) {
|
|
3917
4168
|
return `${type}|${source}|${target}`;
|
|
3918
4169
|
}
|
|
@@ -3920,25 +4171,25 @@ function bucketEdges(graph) {
|
|
|
3920
4171
|
const buckets = /* @__PURE__ */ new Map();
|
|
3921
4172
|
graph.forEachEdge((id, attrs) => {
|
|
3922
4173
|
const e = attrs;
|
|
3923
|
-
const parsed = (0,
|
|
4174
|
+
const parsed = (0, import_types19.parseEdgeId)(id);
|
|
3924
4175
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
3925
4176
|
const key = bucketKey(e.source, e.target, e.type);
|
|
3926
4177
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3927
4178
|
switch (provenance) {
|
|
3928
|
-
case
|
|
4179
|
+
case import_types19.Provenance.EXTRACTED:
|
|
3929
4180
|
cur.extracted = e;
|
|
3930
4181
|
break;
|
|
3931
|
-
case
|
|
4182
|
+
case import_types19.Provenance.OBSERVED:
|
|
3932
4183
|
cur.observed = e;
|
|
3933
4184
|
break;
|
|
3934
|
-
case
|
|
4185
|
+
case import_types19.Provenance.INFERRED:
|
|
3935
4186
|
cur.inferred = e;
|
|
3936
4187
|
break;
|
|
3937
|
-
case
|
|
4188
|
+
case import_types19.Provenance.FRONTIER:
|
|
3938
4189
|
cur.frontier = e;
|
|
3939
4190
|
break;
|
|
3940
4191
|
default:
|
|
3941
|
-
if (e.provenance ===
|
|
4192
|
+
if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
|
|
3942
4193
|
}
|
|
3943
4194
|
buckets.set(key, cur);
|
|
3944
4195
|
});
|
|
@@ -3947,13 +4198,13 @@ function bucketEdges(graph) {
|
|
|
3947
4198
|
function nodeIsFrontier(graph, nodeId) {
|
|
3948
4199
|
if (!graph.hasNode(nodeId)) return false;
|
|
3949
4200
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
3950
|
-
return attrs.type ===
|
|
4201
|
+
return attrs.type === import_types19.NodeType.FrontierNode;
|
|
3951
4202
|
}
|
|
3952
4203
|
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3953
4204
|
if (!graph.hasNode(sourceId)) return false;
|
|
3954
4205
|
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3955
4206
|
const e = graph.getEdgeAttributes(edgeId);
|
|
3956
|
-
if (e.provenance ===
|
|
4207
|
+
if (e.provenance === import_types19.Provenance.OBSERVED) return true;
|
|
3957
4208
|
}
|
|
3958
4209
|
return false;
|
|
3959
4210
|
}
|
|
@@ -4014,7 +4265,7 @@ function declaredHostFor(svc) {
|
|
|
4014
4265
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4015
4266
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4016
4267
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4017
|
-
if (e.type ===
|
|
4268
|
+
if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
|
|
4018
4269
|
return true;
|
|
4019
4270
|
}
|
|
4020
4271
|
}
|
|
@@ -4027,10 +4278,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4027
4278
|
const out = [];
|
|
4028
4279
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4029
4280
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4030
|
-
if (edge.type !==
|
|
4031
|
-
if (edge.provenance !==
|
|
4281
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4282
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4032
4283
|
const target = graph.getNodeAttributes(edge.target);
|
|
4033
|
-
if (target.type !==
|
|
4284
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4034
4285
|
const observedHost = target.host?.trim();
|
|
4035
4286
|
if (!observedHost) continue;
|
|
4036
4287
|
if (observedHost === declaredHost) continue;
|
|
@@ -4052,10 +4303,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4052
4303
|
const deps = svc.dependencies ?? {};
|
|
4053
4304
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4054
4305
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4055
|
-
if (edge.type !==
|
|
4056
|
-
if (edge.provenance !==
|
|
4306
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4307
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4057
4308
|
const target = graph.getNodeAttributes(edge.target);
|
|
4058
|
-
if (target.type !==
|
|
4309
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4059
4310
|
for (const pair of compatPairs()) {
|
|
4060
4311
|
if (pair.engine !== target.engine) continue;
|
|
4061
4312
|
const declared = deps[pair.driver];
|
|
@@ -4116,7 +4367,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4116
4367
|
}
|
|
4117
4368
|
graph.forEachNode((nodeId, attrs) => {
|
|
4118
4369
|
const n = attrs;
|
|
4119
|
-
if (n.type !==
|
|
4370
|
+
if (n.type !== import_types19.NodeType.ServiceNode) return;
|
|
4120
4371
|
const svc = n;
|
|
4121
4372
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4122
4373
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4140,7 +4391,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4140
4391
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4141
4392
|
return a.target.localeCompare(b.target);
|
|
4142
4393
|
});
|
|
4143
|
-
return
|
|
4394
|
+
return import_types19.DivergenceResultSchema.parse({
|
|
4144
4395
|
divergences: filtered,
|
|
4145
4396
|
totalAffected: filtered.length,
|
|
4146
4397
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4149,7 +4400,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4149
4400
|
|
|
4150
4401
|
// src/diff.ts
|
|
4151
4402
|
init_cjs_shims();
|
|
4152
|
-
var
|
|
4403
|
+
var import_node_fs19 = require("fs");
|
|
4153
4404
|
async function loadSnapshotForDiff(target) {
|
|
4154
4405
|
if (/^https?:\/\//i.test(target)) {
|
|
4155
4406
|
const res = await fetch(target);
|
|
@@ -4158,7 +4409,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
4158
4409
|
}
|
|
4159
4410
|
return await res.json();
|
|
4160
4411
|
}
|
|
4161
|
-
const raw = await
|
|
4412
|
+
const raw = await import_node_fs19.promises.readFile(target, "utf8");
|
|
4162
4413
|
return JSON.parse(raw);
|
|
4163
4414
|
}
|
|
4164
4415
|
function indexEntries(entries) {
|
|
@@ -4226,23 +4477,23 @@ function canonicalJson(value) {
|
|
|
4226
4477
|
|
|
4227
4478
|
// src/projects.ts
|
|
4228
4479
|
init_cjs_shims();
|
|
4229
|
-
var
|
|
4480
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4230
4481
|
function pathsForProject(project, baseDir) {
|
|
4231
4482
|
if (project === DEFAULT_PROJECT) {
|
|
4232
4483
|
return {
|
|
4233
|
-
snapshotPath:
|
|
4234
|
-
errorsPath:
|
|
4235
|
-
staleEventsPath:
|
|
4236
|
-
embeddingsCachePath:
|
|
4237
|
-
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")
|
|
4238
4489
|
};
|
|
4239
4490
|
}
|
|
4240
4491
|
return {
|
|
4241
|
-
snapshotPath:
|
|
4242
|
-
errorsPath:
|
|
4243
|
-
staleEventsPath:
|
|
4244
|
-
embeddingsCachePath:
|
|
4245
|
-
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`)
|
|
4246
4497
|
};
|
|
4247
4498
|
}
|
|
4248
4499
|
var Projects = class {
|
|
@@ -4278,49 +4529,49 @@ var Projects = class {
|
|
|
4278
4529
|
|
|
4279
4530
|
// src/registry.ts
|
|
4280
4531
|
init_cjs_shims();
|
|
4281
|
-
var
|
|
4532
|
+
var import_node_fs20 = require("fs");
|
|
4282
4533
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4283
|
-
var
|
|
4284
|
-
var
|
|
4534
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4535
|
+
var import_types20 = require("@neat.is/types");
|
|
4285
4536
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4286
4537
|
var LOCK_RETRY_MS = 50;
|
|
4287
4538
|
function neatHome() {
|
|
4288
4539
|
const override = process.env.NEAT_HOME;
|
|
4289
|
-
if (override && override.length > 0) return
|
|
4290
|
-
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");
|
|
4291
4542
|
}
|
|
4292
4543
|
function registryPath() {
|
|
4293
|
-
return
|
|
4544
|
+
return import_node_path32.default.join(neatHome(), "projects.json");
|
|
4294
4545
|
}
|
|
4295
4546
|
function registryLockPath() {
|
|
4296
|
-
return
|
|
4547
|
+
return import_node_path32.default.join(neatHome(), "projects.json.lock");
|
|
4297
4548
|
}
|
|
4298
4549
|
async function normalizeProjectPath(input) {
|
|
4299
|
-
const resolved =
|
|
4550
|
+
const resolved = import_node_path32.default.resolve(input);
|
|
4300
4551
|
try {
|
|
4301
|
-
return await
|
|
4552
|
+
return await import_node_fs20.promises.realpath(resolved);
|
|
4302
4553
|
} catch {
|
|
4303
4554
|
return resolved;
|
|
4304
4555
|
}
|
|
4305
4556
|
}
|
|
4306
4557
|
async function writeAtomically(target, contents) {
|
|
4307
|
-
await
|
|
4558
|
+
await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(target), { recursive: true });
|
|
4308
4559
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4309
|
-
const fd = await
|
|
4560
|
+
const fd = await import_node_fs20.promises.open(tmp, "w");
|
|
4310
4561
|
try {
|
|
4311
4562
|
await fd.writeFile(contents, "utf8");
|
|
4312
4563
|
await fd.sync();
|
|
4313
4564
|
} finally {
|
|
4314
4565
|
await fd.close();
|
|
4315
4566
|
}
|
|
4316
|
-
await
|
|
4567
|
+
await import_node_fs20.promises.rename(tmp, target);
|
|
4317
4568
|
}
|
|
4318
4569
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4319
4570
|
const deadline = Date.now() + timeoutMs;
|
|
4320
|
-
await
|
|
4571
|
+
await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(lockPath), { recursive: true });
|
|
4321
4572
|
while (true) {
|
|
4322
4573
|
try {
|
|
4323
|
-
const fd = await
|
|
4574
|
+
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
4324
4575
|
await fd.close();
|
|
4325
4576
|
return;
|
|
4326
4577
|
} catch (err) {
|
|
@@ -4336,7 +4587,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
4336
4587
|
}
|
|
4337
4588
|
}
|
|
4338
4589
|
async function releaseLock(lockPath) {
|
|
4339
|
-
await
|
|
4590
|
+
await import_node_fs20.promises.unlink(lockPath).catch(() => {
|
|
4340
4591
|
});
|
|
4341
4592
|
}
|
|
4342
4593
|
async function withLock(fn) {
|
|
@@ -4352,7 +4603,7 @@ async function readRegistry() {
|
|
|
4352
4603
|
const file = registryPath();
|
|
4353
4604
|
let raw;
|
|
4354
4605
|
try {
|
|
4355
|
-
raw = await
|
|
4606
|
+
raw = await import_node_fs20.promises.readFile(file, "utf8");
|
|
4356
4607
|
} catch (err) {
|
|
4357
4608
|
if (err.code === "ENOENT") {
|
|
4358
4609
|
return { version: 1, projects: [] };
|
|
@@ -4360,10 +4611,10 @@ async function readRegistry() {
|
|
|
4360
4611
|
throw err;
|
|
4361
4612
|
}
|
|
4362
4613
|
const parsed = JSON.parse(raw);
|
|
4363
|
-
return
|
|
4614
|
+
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4364
4615
|
}
|
|
4365
4616
|
async function writeRegistry(reg) {
|
|
4366
|
-
const validated =
|
|
4617
|
+
const validated = import_types20.RegistryFileSchema.parse(reg);
|
|
4367
4618
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4368
4619
|
}
|
|
4369
4620
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4614,11 +4865,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4614
4865
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4615
4866
|
const parsed = [];
|
|
4616
4867
|
for (const c of candidates) {
|
|
4617
|
-
const r =
|
|
4868
|
+
const r = import_types21.DivergenceTypeSchema.safeParse(c);
|
|
4618
4869
|
if (!r.success) {
|
|
4619
4870
|
return reply.code(400).send({
|
|
4620
4871
|
error: `unknown divergence type "${c}"`,
|
|
4621
|
-
allowed:
|
|
4872
|
+
allowed: import_types21.DivergenceTypeSchema.options
|
|
4622
4873
|
});
|
|
4623
4874
|
}
|
|
4624
4875
|
parsed.push(r.data);
|
|
@@ -4802,7 +5053,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4802
5053
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4803
5054
|
let violations = await log.readAll();
|
|
4804
5055
|
if (req.query.severity) {
|
|
4805
|
-
const sev =
|
|
5056
|
+
const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4806
5057
|
if (!sev.success) {
|
|
4807
5058
|
return reply.code(400).send({
|
|
4808
5059
|
error: "invalid severity",
|
|
@@ -4819,7 +5070,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4819
5070
|
scope.post("/policies/check", async (req, reply) => {
|
|
4820
5071
|
const proj = resolveProject(registry, req, reply);
|
|
4821
5072
|
if (!proj) return;
|
|
4822
|
-
const parsed =
|
|
5073
|
+
const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4823
5074
|
if (!parsed.success) {
|
|
4824
5075
|
return reply.code(400).send({
|
|
4825
5076
|
error: "invalid /policies/check body",
|
|
@@ -4917,30 +5168,14 @@ async function buildApi(opts) {
|
|
|
4917
5168
|
return app;
|
|
4918
5169
|
}
|
|
4919
5170
|
|
|
4920
|
-
// src/extract/retire.ts
|
|
4921
|
-
init_cjs_shims();
|
|
4922
|
-
var import_types21 = require("@neat.is/types");
|
|
4923
|
-
function retireEdgesByFile(graph, file) {
|
|
4924
|
-
const normalized = file.split("\\").join("/");
|
|
4925
|
-
const toDrop = [];
|
|
4926
|
-
graph.forEachEdge((id, attrs) => {
|
|
4927
|
-
const edge = attrs;
|
|
4928
|
-
if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
|
|
4929
|
-
if (!edge.evidence?.file) return;
|
|
4930
|
-
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4931
|
-
});
|
|
4932
|
-
for (const id of toDrop) graph.dropEdge(id);
|
|
4933
|
-
return toDrop.length;
|
|
4934
|
-
}
|
|
4935
|
-
|
|
4936
5171
|
// src/watch.ts
|
|
4937
5172
|
init_otel();
|
|
4938
5173
|
init_otel_grpc();
|
|
4939
5174
|
|
|
4940
5175
|
// src/search.ts
|
|
4941
5176
|
init_cjs_shims();
|
|
4942
|
-
var
|
|
4943
|
-
var
|
|
5177
|
+
var import_node_fs21 = require("fs");
|
|
5178
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
4944
5179
|
var import_node_crypto = require("crypto");
|
|
4945
5180
|
var DEFAULT_LIMIT = 10;
|
|
4946
5181
|
var NOMIC_DIM = 768;
|
|
@@ -5070,7 +5305,7 @@ async function pickEmbedder() {
|
|
|
5070
5305
|
}
|
|
5071
5306
|
async function readCache(cachePath) {
|
|
5072
5307
|
try {
|
|
5073
|
-
const raw = await
|
|
5308
|
+
const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
|
|
5074
5309
|
const parsed = JSON.parse(raw);
|
|
5075
5310
|
if (parsed.version !== 1) return null;
|
|
5076
5311
|
return parsed;
|
|
@@ -5079,8 +5314,8 @@ async function readCache(cachePath) {
|
|
|
5079
5314
|
}
|
|
5080
5315
|
}
|
|
5081
5316
|
async function writeCache(cachePath, cache) {
|
|
5082
|
-
await
|
|
5083
|
-
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));
|
|
5084
5319
|
}
|
|
5085
5320
|
var VectorIndex = class {
|
|
5086
5321
|
constructor(embedder, cachePath) {
|
|
@@ -5235,8 +5470,8 @@ var ALL_PHASES = [
|
|
|
5235
5470
|
];
|
|
5236
5471
|
function classifyChange(relPath) {
|
|
5237
5472
|
const phases = /* @__PURE__ */ new Set();
|
|
5238
|
-
const base =
|
|
5239
|
-
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());
|
|
5240
5475
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
5241
5476
|
phases.add("services");
|
|
5242
5477
|
phases.add("aliases");
|
|
@@ -5301,6 +5536,16 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
5301
5536
|
durationMs: Date.now() - started
|
|
5302
5537
|
};
|
|
5303
5538
|
}
|
|
5539
|
+
var IGNORED_WATCH_GLOBS = [
|
|
5540
|
+
"**/node_modules/**",
|
|
5541
|
+
"**/.git/**",
|
|
5542
|
+
"**/dist/**",
|
|
5543
|
+
"**/build/**",
|
|
5544
|
+
"**/.turbo/**",
|
|
5545
|
+
"**/.next/**",
|
|
5546
|
+
"**/neat-out/**",
|
|
5547
|
+
"**/.DS_Store"
|
|
5548
|
+
];
|
|
5304
5549
|
var IGNORED_WATCH_PATHS = [
|
|
5305
5550
|
/(?:^|[\\/])node_modules[\\/]/,
|
|
5306
5551
|
/(?:^|[\\/])\.git[\\/]/,
|
|
@@ -5314,13 +5559,42 @@ var IGNORED_WATCH_PATHS = [
|
|
|
5314
5559
|
function shouldIgnore(absPath) {
|
|
5315
5560
|
return IGNORED_WATCH_PATHS.some((re) => re.test(absPath));
|
|
5316
5561
|
}
|
|
5562
|
+
var DARWIN_POLLING_DIR_THRESHOLD = 400;
|
|
5563
|
+
function countWatchableDirs(scanPath, limit) {
|
|
5564
|
+
let count = 0;
|
|
5565
|
+
const visit = (dir, depth) => {
|
|
5566
|
+
if (count >= limit) return;
|
|
5567
|
+
let entries;
|
|
5568
|
+
try {
|
|
5569
|
+
entries = import_node_fs22.default.readdirSync(dir, { withFileTypes: true });
|
|
5570
|
+
} catch {
|
|
5571
|
+
return;
|
|
5572
|
+
}
|
|
5573
|
+
for (const e of entries) {
|
|
5574
|
+
if (count >= limit) return;
|
|
5575
|
+
if (!e.isDirectory()) continue;
|
|
5576
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path36.default.join(dir, e.name) + import_node_path36.default.sep))) continue;
|
|
5577
|
+
count++;
|
|
5578
|
+
if (depth < 2) visit(import_node_path36.default.join(dir, e.name), depth + 1);
|
|
5579
|
+
}
|
|
5580
|
+
};
|
|
5581
|
+
visit(scanPath, 0);
|
|
5582
|
+
return count;
|
|
5583
|
+
}
|
|
5584
|
+
function shouldUsePolling(scanPath) {
|
|
5585
|
+
const env = process.env.NEAT_WATCH_POLLING;
|
|
5586
|
+
if (env === "1" || env === "true") return true;
|
|
5587
|
+
if (env === "0" || env === "false") return false;
|
|
5588
|
+
if (process.platform !== "darwin") return false;
|
|
5589
|
+
return countWatchableDirs(scanPath, DARWIN_POLLING_DIR_THRESHOLD) >= DARWIN_POLLING_DIR_THRESHOLD;
|
|
5590
|
+
}
|
|
5317
5591
|
async function startWatch(graph, opts) {
|
|
5318
5592
|
const debounceMs = opts.debounceMs ?? 1e3;
|
|
5319
5593
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
5320
5594
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
5321
5595
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
5322
|
-
const policyFilePath =
|
|
5323
|
-
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");
|
|
5324
5598
|
let policies = [];
|
|
5325
5599
|
try {
|
|
5326
5600
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -5369,7 +5643,7 @@ async function startWatch(graph, opts) {
|
|
|
5369
5643
|
const host = opts.host ?? "0.0.0.0";
|
|
5370
5644
|
const port = opts.port ?? 8080;
|
|
5371
5645
|
const otelPort = opts.otelPort ?? 4318;
|
|
5372
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
5646
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path36.default.join(import_node_path36.default.dirname(opts.outPath), "embeddings.json");
|
|
5373
5647
|
let searchIndex;
|
|
5374
5648
|
try {
|
|
5375
5649
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -5387,7 +5661,7 @@ async function startWatch(graph, opts) {
|
|
|
5387
5661
|
// Paths are derived from the explicit options the watch caller passes
|
|
5388
5662
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
5389
5663
|
// fields so the registry shape is complete.
|
|
5390
|
-
...pathsForProject(projectName,
|
|
5664
|
+
...pathsForProject(projectName, import_node_path36.default.dirname(opts.outPath)),
|
|
5391
5665
|
snapshotPath: opts.outPath,
|
|
5392
5666
|
errorsPath: opts.errorsPath,
|
|
5393
5667
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -5472,9 +5746,9 @@ async function startWatch(graph, opts) {
|
|
|
5472
5746
|
};
|
|
5473
5747
|
const onPath = (absPath) => {
|
|
5474
5748
|
if (shouldIgnore(absPath)) return;
|
|
5475
|
-
const rel =
|
|
5749
|
+
const rel = import_node_path36.default.relative(opts.scanPath, absPath);
|
|
5476
5750
|
if (!rel || rel.startsWith("..")) return;
|
|
5477
|
-
pendingPaths.add(rel.split(
|
|
5751
|
+
pendingPaths.add(rel.split(import_node_path36.default.sep).join("/"));
|
|
5478
5752
|
const phases = classifyChange(rel);
|
|
5479
5753
|
if (phases.size === 0) {
|
|
5480
5754
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -5483,10 +5757,19 @@ async function startWatch(graph, opts) {
|
|
|
5483
5757
|
}
|
|
5484
5758
|
schedule();
|
|
5485
5759
|
};
|
|
5760
|
+
const usePolling = shouldUsePolling(opts.scanPath);
|
|
5761
|
+
if (usePolling) {
|
|
5762
|
+
const reason = process.env.NEAT_WATCH_POLLING === "1" || process.env.NEAT_WATCH_POLLING === "true" ? "NEAT_WATCH_POLLING env override" : "darwin heuristic \u2014 large scan root, kqueue cap risk";
|
|
5763
|
+
console.log(`[${projectName}] watch: usePolling=true (${reason})`);
|
|
5764
|
+
}
|
|
5486
5765
|
const watcher = import_chokidar.default.watch(opts.scanPath, {
|
|
5487
5766
|
ignoreInitial: true,
|
|
5488
|
-
|
|
5767
|
+
// Glob array prunes at descent time (#233) so chokidar never opens a
|
|
5768
|
+
// kqueue handle for `node_modules` and friends. The function backstop
|
|
5769
|
+
// catches any path that slipped through and matches the regex set.
|
|
5770
|
+
ignored: [...IGNORED_WATCH_GLOBS, (p) => shouldIgnore(p)],
|
|
5489
5771
|
persistent: true,
|
|
5772
|
+
usePolling,
|
|
5490
5773
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
5491
5774
|
});
|
|
5492
5775
|
watcher.on("add", onPath);
|
|
@@ -5522,8 +5805,8 @@ init_cjs_shims();
|
|
|
5522
5805
|
|
|
5523
5806
|
// src/installers/javascript.ts
|
|
5524
5807
|
init_cjs_shims();
|
|
5525
|
-
var
|
|
5526
|
-
var
|
|
5808
|
+
var import_node_fs23 = require("fs");
|
|
5809
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5527
5810
|
var SDK_PACKAGES = [
|
|
5528
5811
|
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
5529
5812
|
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
@@ -5539,7 +5822,7 @@ var OTEL_ENV = {
|
|
|
5539
5822
|
};
|
|
5540
5823
|
async function readPackageJson(serviceDir) {
|
|
5541
5824
|
try {
|
|
5542
|
-
const raw = await
|
|
5825
|
+
const raw = await import_node_fs23.promises.readFile(import_node_path37.default.join(serviceDir, "package.json"), "utf8");
|
|
5543
5826
|
return JSON.parse(raw);
|
|
5544
5827
|
} catch {
|
|
5545
5828
|
return null;
|
|
@@ -5558,7 +5841,7 @@ function rewriteStartScript(start) {
|
|
|
5558
5841
|
}
|
|
5559
5842
|
async function plan(serviceDir) {
|
|
5560
5843
|
const pkg = await readPackageJson(serviceDir);
|
|
5561
|
-
const manifestPath =
|
|
5844
|
+
const manifestPath = import_node_path37.default.join(serviceDir, "package.json");
|
|
5562
5845
|
const empty = {
|
|
5563
5846
|
language: "javascript",
|
|
5564
5847
|
serviceDir,
|
|
@@ -5605,7 +5888,7 @@ async function apply(installPlan) {
|
|
|
5605
5888
|
const originals = /* @__PURE__ */ new Map();
|
|
5606
5889
|
for (const file of touched) {
|
|
5607
5890
|
try {
|
|
5608
|
-
originals.set(file, await
|
|
5891
|
+
originals.set(file, await import_node_fs23.promises.readFile(file, "utf8"));
|
|
5609
5892
|
} catch {
|
|
5610
5893
|
}
|
|
5611
5894
|
}
|
|
@@ -5631,8 +5914,8 @@ async function apply(installPlan) {
|
|
|
5631
5914
|
}
|
|
5632
5915
|
const newRaw = JSON.stringify(pkg, null, 2) + "\n";
|
|
5633
5916
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
5634
|
-
await
|
|
5635
|
-
await
|
|
5917
|
+
await import_node_fs23.promises.writeFile(tmp, newRaw, "utf8");
|
|
5918
|
+
await import_node_fs23.promises.rename(tmp, file);
|
|
5636
5919
|
}
|
|
5637
5920
|
} catch (err) {
|
|
5638
5921
|
await rollback(installPlan, originals);
|
|
@@ -5643,7 +5926,7 @@ async function rollback(installPlan, originals) {
|
|
|
5643
5926
|
const restored = [];
|
|
5644
5927
|
for (const [file, raw] of originals.entries()) {
|
|
5645
5928
|
try {
|
|
5646
|
-
await
|
|
5929
|
+
await import_node_fs23.promises.writeFile(file, raw, "utf8");
|
|
5647
5930
|
restored.push(file);
|
|
5648
5931
|
} catch {
|
|
5649
5932
|
}
|
|
@@ -5657,8 +5940,8 @@ async function rollback(installPlan, originals) {
|
|
|
5657
5940
|
...restored.map((f) => `restored: ${f}`),
|
|
5658
5941
|
""
|
|
5659
5942
|
];
|
|
5660
|
-
const rollbackPath =
|
|
5661
|
-
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");
|
|
5662
5945
|
}
|
|
5663
5946
|
var javascriptInstaller = {
|
|
5664
5947
|
name: "javascript",
|
|
@@ -5669,8 +5952,8 @@ var javascriptInstaller = {
|
|
|
5669
5952
|
|
|
5670
5953
|
// src/installers/python.ts
|
|
5671
5954
|
init_cjs_shims();
|
|
5672
|
-
var
|
|
5673
|
-
var
|
|
5955
|
+
var import_node_fs24 = require("fs");
|
|
5956
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
5674
5957
|
var SDK_PACKAGES2 = [
|
|
5675
5958
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
5676
5959
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -5682,7 +5965,7 @@ var OTEL_ENV2 = {
|
|
|
5682
5965
|
};
|
|
5683
5966
|
async function exists2(p) {
|
|
5684
5967
|
try {
|
|
5685
|
-
await
|
|
5968
|
+
await import_node_fs24.promises.stat(p);
|
|
5686
5969
|
return true;
|
|
5687
5970
|
} catch {
|
|
5688
5971
|
return false;
|
|
@@ -5691,7 +5974,7 @@ async function exists2(p) {
|
|
|
5691
5974
|
async function detect2(serviceDir) {
|
|
5692
5975
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
5693
5976
|
for (const m of markers) {
|
|
5694
|
-
if (await exists2(
|
|
5977
|
+
if (await exists2(import_node_path38.default.join(serviceDir, m))) return true;
|
|
5695
5978
|
}
|
|
5696
5979
|
return false;
|
|
5697
5980
|
}
|
|
@@ -5701,9 +5984,9 @@ function reqPackageName(line) {
|
|
|
5701
5984
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
5702
5985
|
}
|
|
5703
5986
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
5704
|
-
const file =
|
|
5987
|
+
const file = import_node_path38.default.join(serviceDir, "requirements.txt");
|
|
5705
5988
|
if (!await exists2(file)) return null;
|
|
5706
|
-
const raw = await
|
|
5989
|
+
const raw = await import_node_fs24.promises.readFile(file, "utf8");
|
|
5707
5990
|
const presentNames = new Set(
|
|
5708
5991
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
5709
5992
|
);
|
|
@@ -5711,9 +5994,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
5711
5994
|
return { manifest: file, missing: [...missing] };
|
|
5712
5995
|
}
|
|
5713
5996
|
async function planProcfileEdits(serviceDir) {
|
|
5714
|
-
const procfile =
|
|
5997
|
+
const procfile = import_node_path38.default.join(serviceDir, "Procfile");
|
|
5715
5998
|
if (!await exists2(procfile)) return [];
|
|
5716
|
-
const raw = await
|
|
5999
|
+
const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
|
|
5717
6000
|
const edits = [];
|
|
5718
6001
|
for (const line of raw.split(/\r?\n/)) {
|
|
5719
6002
|
if (line.length === 0) continue;
|
|
@@ -5765,8 +6048,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
5765
6048
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
5766
6049
|
`;
|
|
5767
6050
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
5768
|
-
await
|
|
5769
|
-
await
|
|
6051
|
+
await import_node_fs24.promises.writeFile(tmp, next, "utf8");
|
|
6052
|
+
await import_node_fs24.promises.rename(tmp, manifest);
|
|
5770
6053
|
}
|
|
5771
6054
|
async function applyProcfile(procfile, edits, original) {
|
|
5772
6055
|
let next = original;
|
|
@@ -5775,8 +6058,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
5775
6058
|
next = next.replace(e.before, e.after);
|
|
5776
6059
|
}
|
|
5777
6060
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
5778
|
-
await
|
|
5779
|
-
await
|
|
6061
|
+
await import_node_fs24.promises.writeFile(tmp, next, "utf8");
|
|
6062
|
+
await import_node_fs24.promises.rename(tmp, procfile);
|
|
5780
6063
|
}
|
|
5781
6064
|
async function apply2(installPlan) {
|
|
5782
6065
|
const touched = /* @__PURE__ */ new Set();
|
|
@@ -5786,7 +6069,7 @@ async function apply2(installPlan) {
|
|
|
5786
6069
|
const originals = /* @__PURE__ */ new Map();
|
|
5787
6070
|
for (const file of touched) {
|
|
5788
6071
|
try {
|
|
5789
|
-
originals.set(file, await
|
|
6072
|
+
originals.set(file, await import_node_fs24.promises.readFile(file, "utf8"));
|
|
5790
6073
|
} catch {
|
|
5791
6074
|
}
|
|
5792
6075
|
}
|
|
@@ -5796,7 +6079,7 @@ async function apply2(installPlan) {
|
|
|
5796
6079
|
if (raw === void 0) {
|
|
5797
6080
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
5798
6081
|
}
|
|
5799
|
-
const base =
|
|
6082
|
+
const base = import_node_path38.default.basename(file);
|
|
5800
6083
|
if (base === "requirements.txt") {
|
|
5801
6084
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
5802
6085
|
if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
|
|
@@ -5814,7 +6097,7 @@ async function rollback2(installPlan, originals) {
|
|
|
5814
6097
|
const restored = [];
|
|
5815
6098
|
for (const [file, raw] of originals.entries()) {
|
|
5816
6099
|
try {
|
|
5817
|
-
await
|
|
6100
|
+
await import_node_fs24.promises.writeFile(file, raw, "utf8");
|
|
5818
6101
|
restored.push(file);
|
|
5819
6102
|
} catch {
|
|
5820
6103
|
}
|
|
@@ -5828,8 +6111,8 @@ async function rollback2(installPlan, originals) {
|
|
|
5828
6111
|
...restored.map((f) => `restored: ${f}`),
|
|
5829
6112
|
""
|
|
5830
6113
|
];
|
|
5831
|
-
const rollbackPath =
|
|
5832
|
-
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");
|
|
5833
6116
|
}
|
|
5834
6117
|
var pythonInstaller = {
|
|
5835
6118
|
name: "python",
|
|
@@ -5942,10 +6225,10 @@ var TransportError = class extends Error {
|
|
|
5942
6225
|
function createHttpClient(baseUrl) {
|
|
5943
6226
|
const root = baseUrl.replace(/\/$/, "");
|
|
5944
6227
|
return {
|
|
5945
|
-
async get(
|
|
6228
|
+
async get(path40) {
|
|
5946
6229
|
let res;
|
|
5947
6230
|
try {
|
|
5948
|
-
res = await fetch(`${root}${
|
|
6231
|
+
res = await fetch(`${root}${path40}`);
|
|
5949
6232
|
} catch (err) {
|
|
5950
6233
|
throw new TransportError(
|
|
5951
6234
|
`cannot reach neat-core at ${root}: ${err.message}`
|
|
@@ -5955,16 +6238,16 @@ function createHttpClient(baseUrl) {
|
|
|
5955
6238
|
const body = await res.text().catch(() => "");
|
|
5956
6239
|
throw new HttpError(
|
|
5957
6240
|
res.status,
|
|
5958
|
-
`${res.status} ${res.statusText} on GET ${
|
|
6241
|
+
`${res.status} ${res.statusText} on GET ${path40}: ${body}`,
|
|
5959
6242
|
body
|
|
5960
6243
|
);
|
|
5961
6244
|
}
|
|
5962
6245
|
return await res.json();
|
|
5963
6246
|
},
|
|
5964
|
-
async post(
|
|
6247
|
+
async post(path40, body) {
|
|
5965
6248
|
let res;
|
|
5966
6249
|
try {
|
|
5967
|
-
res = await fetch(`${root}${
|
|
6250
|
+
res = await fetch(`${root}${path40}`, {
|
|
5968
6251
|
method: "POST",
|
|
5969
6252
|
headers: { "content-type": "application/json" },
|
|
5970
6253
|
body: JSON.stringify(body)
|
|
@@ -5978,7 +6261,7 @@ function createHttpClient(baseUrl) {
|
|
|
5978
6261
|
const text = await res.text().catch(() => "");
|
|
5979
6262
|
throw new HttpError(
|
|
5980
6263
|
res.status,
|
|
5981
|
-
`${res.status} ${res.statusText} on POST ${
|
|
6264
|
+
`${res.status} ${res.statusText} on POST ${path40}: ${text}`,
|
|
5982
6265
|
text
|
|
5983
6266
|
);
|
|
5984
6267
|
}
|
|
@@ -5992,12 +6275,12 @@ function projectPath(project, suffix) {
|
|
|
5992
6275
|
}
|
|
5993
6276
|
async function runRootCause(client, input) {
|
|
5994
6277
|
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5995
|
-
const
|
|
6278
|
+
const path40 = projectPath(
|
|
5996
6279
|
input.project,
|
|
5997
6280
|
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5998
6281
|
);
|
|
5999
6282
|
try {
|
|
6000
|
-
const result = await client.get(
|
|
6283
|
+
const result = await client.get(path40);
|
|
6001
6284
|
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
6002
6285
|
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
6003
6286
|
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
@@ -6023,12 +6306,12 @@ async function runRootCause(client, input) {
|
|
|
6023
6306
|
}
|
|
6024
6307
|
async function runBlastRadius(client, input) {
|
|
6025
6308
|
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
6026
|
-
const
|
|
6309
|
+
const path40 = projectPath(
|
|
6027
6310
|
input.project,
|
|
6028
6311
|
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
6029
6312
|
);
|
|
6030
6313
|
try {
|
|
6031
|
-
const result = await client.get(
|
|
6314
|
+
const result = await client.get(path40);
|
|
6032
6315
|
if (result.totalAffected === 0) {
|
|
6033
6316
|
return {
|
|
6034
6317
|
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
@@ -6062,12 +6345,12 @@ function formatBlastEntry(n) {
|
|
|
6062
6345
|
}
|
|
6063
6346
|
async function runDependencies(client, input) {
|
|
6064
6347
|
const depth = input.depth ?? 3;
|
|
6065
|
-
const
|
|
6348
|
+
const path40 = projectPath(
|
|
6066
6349
|
input.project,
|
|
6067
6350
|
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
6068
6351
|
);
|
|
6069
6352
|
try {
|
|
6070
|
-
const result = await client.get(
|
|
6353
|
+
const result = await client.get(path40);
|
|
6071
6354
|
if (result.total === 0) {
|
|
6072
6355
|
return {
|
|
6073
6356
|
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
@@ -6148,9 +6431,9 @@ function formatDuration(ms) {
|
|
|
6148
6431
|
return `${Math.round(h / 24)}d`;
|
|
6149
6432
|
}
|
|
6150
6433
|
async function runIncidents(client, input) {
|
|
6151
|
-
const
|
|
6434
|
+
const path40 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
6152
6435
|
try {
|
|
6153
|
-
const body = await client.get(
|
|
6436
|
+
const body = await client.get(path40);
|
|
6154
6437
|
const events = body.events;
|
|
6155
6438
|
if (events.length === 0) {
|
|
6156
6439
|
return {
|
|
@@ -6665,7 +6948,7 @@ async function buildPatchSections(services) {
|
|
|
6665
6948
|
}
|
|
6666
6949
|
async function runInit(opts) {
|
|
6667
6950
|
const written = [];
|
|
6668
|
-
const stat = await
|
|
6951
|
+
const stat = await import_node_fs25.promises.stat(opts.scanPath).catch(() => null);
|
|
6669
6952
|
if (!stat || !stat.isDirectory()) {
|
|
6670
6953
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
6671
6954
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -6674,9 +6957,9 @@ async function runInit(opts) {
|
|
|
6674
6957
|
printDiscoveryReport(opts, services);
|
|
6675
6958
|
const sections = opts.noInstall ? [] : await buildPatchSections(services);
|
|
6676
6959
|
const patch = renderPatch(sections);
|
|
6677
|
-
const patchPath =
|
|
6960
|
+
const patchPath = import_node_path39.default.join(opts.scanPath, "neat.patch");
|
|
6678
6961
|
if (opts.dryRun) {
|
|
6679
|
-
await
|
|
6962
|
+
await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
|
|
6680
6963
|
written.push(patchPath);
|
|
6681
6964
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
6682
6965
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -6685,7 +6968,12 @@ async function runInit(opts) {
|
|
|
6685
6968
|
const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
|
|
6686
6969
|
resetGraph(graphKey);
|
|
6687
6970
|
const graph = getGraph(graphKey);
|
|
6688
|
-
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 });
|
|
6689
6977
|
await saveGraphToDisk(graph, opts.outPath);
|
|
6690
6978
|
written.push(opts.outPath);
|
|
6691
6979
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
@@ -6716,7 +7004,7 @@ async function runInit(opts) {
|
|
|
6716
7004
|
console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
6717
7005
|
}
|
|
6718
7006
|
} else {
|
|
6719
|
-
await
|
|
7007
|
+
await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
|
|
6720
7008
|
written.push(patchPath);
|
|
6721
7009
|
}
|
|
6722
7010
|
}
|
|
@@ -6730,6 +7018,10 @@ async function runInit(opts) {
|
|
|
6730
7018
|
console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
|
|
6731
7019
|
console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
|
|
6732
7020
|
console.log(summarise(nodes, edges));
|
|
7021
|
+
console.log(formatExtractionBanner(result.extractionErrors));
|
|
7022
|
+
if (result.extractionErrors > 0) {
|
|
7023
|
+
console.log(`errors: ${errorsPath}`);
|
|
7024
|
+
}
|
|
6733
7025
|
const incompatibilities = findIncompatibilities(nodes);
|
|
6734
7026
|
if (incompatibilities.length > 0) {
|
|
6735
7027
|
console.log("");
|
|
@@ -6740,6 +7032,9 @@ async function runInit(opts) {
|
|
|
6740
7032
|
}
|
|
6741
7033
|
}
|
|
6742
7034
|
}
|
|
7035
|
+
if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
|
|
7036
|
+
return { exitCode: 4, writtenFiles: written };
|
|
7037
|
+
}
|
|
6743
7038
|
return { exitCode: 0, writtenFiles: written };
|
|
6744
7039
|
}
|
|
6745
7040
|
var CLAUDE_SKILL_CONFIG = {
|
|
@@ -6756,9 +7051,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
6756
7051
|
};
|
|
6757
7052
|
function claudeConfigPath() {
|
|
6758
7053
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
6759
|
-
if (override && override.length > 0) return
|
|
7054
|
+
if (override && override.length > 0) return import_node_path39.default.resolve(override);
|
|
6760
7055
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
6761
|
-
return
|
|
7056
|
+
return import_node_path39.default.join(home, ".claude.json");
|
|
6762
7057
|
}
|
|
6763
7058
|
async function runSkill(opts) {
|
|
6764
7059
|
const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -6770,7 +7065,7 @@ async function runSkill(opts) {
|
|
|
6770
7065
|
const target = claudeConfigPath();
|
|
6771
7066
|
let existing = {};
|
|
6772
7067
|
try {
|
|
6773
|
-
existing = JSON.parse(await
|
|
7068
|
+
existing = JSON.parse(await import_node_fs25.promises.readFile(target, "utf8"));
|
|
6774
7069
|
} catch (err) {
|
|
6775
7070
|
if (err.code !== "ENOENT") {
|
|
6776
7071
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -6782,8 +7077,8 @@ async function runSkill(opts) {
|
|
|
6782
7077
|
...existing,
|
|
6783
7078
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
6784
7079
|
};
|
|
6785
|
-
await
|
|
6786
|
-
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");
|
|
6787
7082
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
6788
7083
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
6789
7084
|
return { exitCode: 0 };
|
|
@@ -6817,12 +7112,12 @@ async function main() {
|
|
|
6817
7112
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
6818
7113
|
process.exit(2);
|
|
6819
7114
|
}
|
|
6820
|
-
const scanPath =
|
|
7115
|
+
const scanPath = import_node_path39.default.resolve(target);
|
|
6821
7116
|
const projectExplicit = parsed.project !== null;
|
|
6822
|
-
const projectName = projectExplicit ? project :
|
|
7117
|
+
const projectName = projectExplicit ? project : import_node_path39.default.basename(scanPath);
|
|
6823
7118
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
6824
|
-
const fallback = pathsForProject(projectKey,
|
|
6825
|
-
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);
|
|
6826
7121
|
const result = await runInit({
|
|
6827
7122
|
scanPath,
|
|
6828
7123
|
outPath,
|
|
@@ -6842,21 +7137,21 @@ async function main() {
|
|
|
6842
7137
|
usage();
|
|
6843
7138
|
process.exit(2);
|
|
6844
7139
|
}
|
|
6845
|
-
const scanPath =
|
|
6846
|
-
const stat = await
|
|
7140
|
+
const scanPath = import_node_path39.default.resolve(target);
|
|
7141
|
+
const stat = await import_node_fs25.promises.stat(scanPath).catch(() => null);
|
|
6847
7142
|
if (!stat || !stat.isDirectory()) {
|
|
6848
7143
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
6849
7144
|
process.exit(2);
|
|
6850
7145
|
}
|
|
6851
|
-
const projectPaths = pathsForProject(project,
|
|
6852
|
-
const outPath =
|
|
6853
|
-
const errorsPath =
|
|
6854
|
-
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))
|
|
6855
7150
|
);
|
|
6856
|
-
const staleEventsPath =
|
|
6857
|
-
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))
|
|
6858
7153
|
);
|
|
6859
|
-
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;
|
|
6860
7155
|
const handle = await startWatch(getGraph(project), {
|
|
6861
7156
|
scanPath,
|
|
6862
7157
|
outPath,
|