@neat.is/core 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-66Z5IEFY.js → chunk-ICFH326H.js} +2 -2
- package/dist/{chunk-FIXKIYNF.js → chunk-VABXPLDT.js} +457 -206
- package/dist/chunk-VABXPLDT.js.map +1 -0
- package/dist/cli.cjs +555 -309
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +24 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +451 -221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +458 -228
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +420 -190
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-FIXKIYNF.js.map +0 -1
- /package/dist/{chunk-66Z5IEFY.js.map → chunk-ICFH326H.js.map} +0 -0
package/dist/server.cjs
CHANGED
|
@@ -99,8 +99,8 @@ function reshapeGrpcRequest(req) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
function resolveProtoRoot() {
|
|
102
|
-
const here =
|
|
103
|
-
return
|
|
102
|
+
const here = import_node_path33.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
103
|
+
return import_node_path33.default.resolve(here, "..", "proto");
|
|
104
104
|
}
|
|
105
105
|
function loadTraceService() {
|
|
106
106
|
const protoRoot = resolveProtoRoot();
|
|
@@ -155,13 +155,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
155
155
|
})
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
|
-
var import_node_url,
|
|
158
|
+
var import_node_url, import_node_path33, grpc, protoLoader;
|
|
159
159
|
var init_otel_grpc = __esm({
|
|
160
160
|
"src/otel-grpc.ts"() {
|
|
161
161
|
"use strict";
|
|
162
162
|
init_cjs_shims();
|
|
163
163
|
import_node_url = require("url");
|
|
164
|
-
|
|
164
|
+
import_node_path33 = __toESM(require("path"), 1);
|
|
165
165
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
166
166
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
167
167
|
init_otel();
|
|
@@ -258,10 +258,10 @@ function parseOtlpRequest(body) {
|
|
|
258
258
|
}
|
|
259
259
|
function loadProtobufDecoder() {
|
|
260
260
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
261
|
-
const here =
|
|
262
|
-
const protoRoot =
|
|
261
|
+
const here = import_node_path34.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
262
|
+
const protoRoot = import_node_path34.default.resolve(here, "..", "proto");
|
|
263
263
|
const root = new import_protobufjs.default.Root();
|
|
264
|
-
root.resolvePath = (_origin, target) =>
|
|
264
|
+
root.resolvePath = (_origin, target) => import_node_path34.default.resolve(protoRoot, target);
|
|
265
265
|
root.loadSync(
|
|
266
266
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
267
267
|
{ keepCase: true }
|
|
@@ -353,12 +353,12 @@ async function buildOtelReceiver(opts) {
|
|
|
353
353
|
};
|
|
354
354
|
return decorated;
|
|
355
355
|
}
|
|
356
|
-
var
|
|
356
|
+
var import_node_path34, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
357
357
|
var init_otel = __esm({
|
|
358
358
|
"src/otel.ts"() {
|
|
359
359
|
"use strict";
|
|
360
360
|
init_cjs_shims();
|
|
361
|
-
|
|
361
|
+
import_node_path34 = __toESM(require("path"), 1);
|
|
362
362
|
import_node_url2 = require("url");
|
|
363
363
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
364
364
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -368,7 +368,7 @@ var init_otel = __esm({
|
|
|
368
368
|
|
|
369
369
|
// src/server.ts
|
|
370
370
|
init_cjs_shims();
|
|
371
|
-
var
|
|
371
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
372
372
|
|
|
373
373
|
// src/graph.ts
|
|
374
374
|
init_cjs_shims();
|
|
@@ -392,7 +392,7 @@ function getGraph(project = DEFAULT_PROJECT) {
|
|
|
392
392
|
init_cjs_shims();
|
|
393
393
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
394
394
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
395
|
-
var
|
|
395
|
+
var import_types21 = require("@neat.is/types");
|
|
396
396
|
|
|
397
397
|
// src/divergences.ts
|
|
398
398
|
init_cjs_shims();
|
|
@@ -798,19 +798,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
798
798
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
799
799
|
let best = { path: [start], edges: [] };
|
|
800
800
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
801
|
-
function step(node,
|
|
802
|
-
if (
|
|
803
|
-
best = { path: [...
|
|
801
|
+
function step(node, path37, edges) {
|
|
802
|
+
if (path37.length > best.path.length) {
|
|
803
|
+
best = { path: [...path37], edges: [...edges] };
|
|
804
804
|
}
|
|
805
|
-
if (
|
|
805
|
+
if (path37.length - 1 >= maxDepth) return;
|
|
806
806
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
807
807
|
for (const [srcId, edge] of incoming) {
|
|
808
808
|
if (visited.has(srcId)) continue;
|
|
809
809
|
visited.add(srcId);
|
|
810
|
-
|
|
810
|
+
path37.push(srcId);
|
|
811
811
|
edges.push(edge);
|
|
812
|
-
step(srcId,
|
|
813
|
-
|
|
812
|
+
step(srcId, path37, edges);
|
|
813
|
+
path37.pop();
|
|
814
814
|
edges.pop();
|
|
815
815
|
visited.delete(srcId);
|
|
816
816
|
}
|
|
@@ -2086,8 +2086,8 @@ async function readErrorEvents(errorsPath) {
|
|
|
2086
2086
|
|
|
2087
2087
|
// src/extract/services.ts
|
|
2088
2088
|
init_cjs_shims();
|
|
2089
|
-
var
|
|
2090
|
-
var
|
|
2089
|
+
var import_node_fs8 = require("fs");
|
|
2090
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2091
2091
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
2092
2092
|
var import_minimatch2 = require("minimatch");
|
|
2093
2093
|
var import_types6 = require("@neat.is/types");
|
|
@@ -2111,9 +2111,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
2111
2111
|
function isConfigFile(name) {
|
|
2112
2112
|
const ext = import_node_path4.default.extname(name);
|
|
2113
2113
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2114
|
-
if (name === ".env" || name.startsWith(".env."))
|
|
2114
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
2115
|
+
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2116
|
+
return { match: true, fileType: "env" };
|
|
2117
|
+
}
|
|
2115
2118
|
return { match: false, fileType: "" };
|
|
2116
2119
|
}
|
|
2120
|
+
function isTestPath(filePath) {
|
|
2121
|
+
const normalised = filePath.replace(/\\/g, "/");
|
|
2122
|
+
const segments = normalised.split("/");
|
|
2123
|
+
for (const seg of segments) {
|
|
2124
|
+
if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
|
|
2125
|
+
return true;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
const base = segments[segments.length - 1] ?? "";
|
|
2129
|
+
return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
|
|
2130
|
+
}
|
|
2131
|
+
function isEnvTemplateFile(name) {
|
|
2132
|
+
if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
|
|
2133
|
+
return true;
|
|
2134
|
+
}
|
|
2135
|
+
return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
|
|
2136
|
+
}
|
|
2137
|
+
function maskCommentsInSource(src) {
|
|
2138
|
+
const len = src.length;
|
|
2139
|
+
const out = new Array(len);
|
|
2140
|
+
let i = 0;
|
|
2141
|
+
let inString = 0;
|
|
2142
|
+
let escaped = false;
|
|
2143
|
+
while (i < len) {
|
|
2144
|
+
const c = src[i];
|
|
2145
|
+
if (inString !== 0) {
|
|
2146
|
+
out[i] = c;
|
|
2147
|
+
if (escaped) {
|
|
2148
|
+
escaped = false;
|
|
2149
|
+
} else if (c === "\\") {
|
|
2150
|
+
escaped = true;
|
|
2151
|
+
} else if (c === inString) {
|
|
2152
|
+
inString = 0;
|
|
2153
|
+
}
|
|
2154
|
+
i++;
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (c === "/" && i + 1 < len) {
|
|
2158
|
+
const next = src[i + 1];
|
|
2159
|
+
if (next === "/") {
|
|
2160
|
+
out[i] = " ";
|
|
2161
|
+
out[i + 1] = " ";
|
|
2162
|
+
let j = i + 2;
|
|
2163
|
+
while (j < len && src[j] !== "\n") {
|
|
2164
|
+
out[j] = " ";
|
|
2165
|
+
j++;
|
|
2166
|
+
}
|
|
2167
|
+
i = j;
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
if (next === "*") {
|
|
2171
|
+
out[i] = " ";
|
|
2172
|
+
out[i + 1] = " ";
|
|
2173
|
+
let j = i + 2;
|
|
2174
|
+
while (j < len) {
|
|
2175
|
+
if (src[j] === "\n") {
|
|
2176
|
+
out[j] = "\n";
|
|
2177
|
+
j++;
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
|
|
2181
|
+
out[j] = " ";
|
|
2182
|
+
out[j + 1] = " ";
|
|
2183
|
+
j += 2;
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
out[j] = " ";
|
|
2187
|
+
j++;
|
|
2188
|
+
}
|
|
2189
|
+
i = j;
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
out[i] = c;
|
|
2194
|
+
if (c === "'" || c === '"' || c === "`") inString = c;
|
|
2195
|
+
i++;
|
|
2196
|
+
}
|
|
2197
|
+
return out.join("");
|
|
2198
|
+
}
|
|
2199
|
+
var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
|
|
2200
|
+
function urlMatchesHost(urlString, host) {
|
|
2201
|
+
if (typeof urlString !== "string" || urlString.length === 0) return false;
|
|
2202
|
+
if (!URL_LIKE.test(urlString)) return false;
|
|
2203
|
+
const [wantedHost, wantedPort] = host.split(":");
|
|
2204
|
+
let parsed;
|
|
2205
|
+
try {
|
|
2206
|
+
const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
|
|
2207
|
+
parsed = new URL(candidate);
|
|
2208
|
+
} catch {
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
|
|
2212
|
+
if (wantedPort && parsed.port !== wantedPort) return false;
|
|
2213
|
+
return true;
|
|
2214
|
+
}
|
|
2117
2215
|
function cleanVersion(raw) {
|
|
2118
2216
|
if (!raw) return void 0;
|
|
2119
2217
|
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
@@ -2269,6 +2367,33 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
|
2269
2367
|
return author ?? void 0;
|
|
2270
2368
|
}
|
|
2271
2369
|
|
|
2370
|
+
// src/extract/errors.ts
|
|
2371
|
+
init_cjs_shims();
|
|
2372
|
+
var import_node_fs7 = require("fs");
|
|
2373
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2374
|
+
var sink = [];
|
|
2375
|
+
function recordExtractionError(producer, file, err) {
|
|
2376
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2377
|
+
sink.push({
|
|
2378
|
+
producer,
|
|
2379
|
+
file,
|
|
2380
|
+
error: e.message,
|
|
2381
|
+
stack: e.stack,
|
|
2382
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2383
|
+
source: "extract"
|
|
2384
|
+
});
|
|
2385
|
+
console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
|
|
2386
|
+
}
|
|
2387
|
+
function drainExtractionErrors() {
|
|
2388
|
+
return sink.splice(0, sink.length);
|
|
2389
|
+
}
|
|
2390
|
+
async function writeExtractionErrors(errors, errorsPath) {
|
|
2391
|
+
if (errors.length === 0) return;
|
|
2392
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
|
|
2393
|
+
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2394
|
+
await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2272
2397
|
// src/extract/services.ts
|
|
2273
2398
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
2274
2399
|
function parseScanDepth() {
|
|
@@ -2285,21 +2410,21 @@ function workspaceGlobs(pkg) {
|
|
|
2285
2410
|
return null;
|
|
2286
2411
|
}
|
|
2287
2412
|
async function loadGitignore(scanPath) {
|
|
2288
|
-
const gitignorePath =
|
|
2413
|
+
const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
|
|
2289
2414
|
if (!await exists(gitignorePath)) return null;
|
|
2290
|
-
const raw = await
|
|
2415
|
+
const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
|
|
2291
2416
|
return (0, import_ignore.default)().add(raw);
|
|
2292
2417
|
}
|
|
2293
2418
|
async function walkDirs(start, scanPath, options, visit) {
|
|
2294
2419
|
async function recurse(current, depth) {
|
|
2295
2420
|
if (depth > options.maxDepth) return;
|
|
2296
|
-
const entries = await
|
|
2421
|
+
const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2297
2422
|
for (const entry of entries) {
|
|
2298
2423
|
if (!entry.isDirectory()) continue;
|
|
2299
2424
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2300
|
-
const child =
|
|
2425
|
+
const child = import_node_path8.default.join(current, entry.name);
|
|
2301
2426
|
if (options.ig) {
|
|
2302
|
-
const rel =
|
|
2427
|
+
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2303
2428
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2304
2429
|
}
|
|
2305
2430
|
await visit(child);
|
|
@@ -2314,8 +2439,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2314
2439
|
for (const raw of globs) {
|
|
2315
2440
|
const pattern = raw.replace(/^\.\//, "");
|
|
2316
2441
|
if (!pattern.includes("*")) {
|
|
2317
|
-
const candidate =
|
|
2318
|
-
if (await exists(
|
|
2442
|
+
const candidate = import_node_path8.default.join(scanPath, pattern);
|
|
2443
|
+
if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2319
2444
|
continue;
|
|
2320
2445
|
}
|
|
2321
2446
|
const segments = pattern.split("/");
|
|
@@ -2324,13 +2449,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2324
2449
|
if (seg.includes("*")) break;
|
|
2325
2450
|
staticSegments.push(seg);
|
|
2326
2451
|
}
|
|
2327
|
-
const start =
|
|
2452
|
+
const start = import_node_path8.default.join(scanPath, ...staticSegments);
|
|
2328
2453
|
if (!await exists(start)) continue;
|
|
2329
2454
|
const hasDoubleStar = pattern.includes("**");
|
|
2330
2455
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2331
2456
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2332
|
-
const rel =
|
|
2333
|
-
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(
|
|
2457
|
+
const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
|
|
2458
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2334
2459
|
found.add(dir);
|
|
2335
2460
|
}
|
|
2336
2461
|
});
|
|
@@ -2338,15 +2463,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2338
2463
|
return [...found];
|
|
2339
2464
|
}
|
|
2340
2465
|
async function discoverNodeService(scanPath, dir) {
|
|
2341
|
-
const pkgPath =
|
|
2466
|
+
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2342
2467
|
if (!await exists(pkgPath)) return null;
|
|
2343
2468
|
let pkg;
|
|
2344
2469
|
try {
|
|
2345
2470
|
pkg = await readJson(pkgPath);
|
|
2346
2471
|
} catch (err) {
|
|
2347
|
-
|
|
2348
|
-
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2349
|
-
);
|
|
2472
|
+
recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
|
|
2350
2473
|
return null;
|
|
2351
2474
|
}
|
|
2352
2475
|
if (!pkg.name) return null;
|
|
@@ -2357,7 +2480,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2357
2480
|
language: "javascript",
|
|
2358
2481
|
version: pkg.version,
|
|
2359
2482
|
dependencies: pkg.dependencies ?? {},
|
|
2360
|
-
repoPath:
|
|
2483
|
+
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2361
2484
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2362
2485
|
};
|
|
2363
2486
|
return { pkg, dir, node };
|
|
@@ -2373,19 +2496,21 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2373
2496
|
language: "python",
|
|
2374
2497
|
version: py.version,
|
|
2375
2498
|
dependencies: py.dependencies,
|
|
2376
|
-
repoPath:
|
|
2499
|
+
repoPath: import_node_path8.default.relative(scanPath, dir)
|
|
2377
2500
|
};
|
|
2378
2501
|
return { pkg, dir, node };
|
|
2379
2502
|
}
|
|
2380
2503
|
async function discoverServices(scanPath) {
|
|
2381
|
-
const rootPkgPath =
|
|
2504
|
+
const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
|
|
2382
2505
|
let rootPkg = null;
|
|
2383
2506
|
if (await exists(rootPkgPath)) {
|
|
2384
2507
|
try {
|
|
2385
2508
|
rootPkg = await readJson(rootPkgPath);
|
|
2386
2509
|
} catch (err) {
|
|
2387
|
-
|
|
2388
|
-
|
|
2510
|
+
recordExtractionError(
|
|
2511
|
+
"services workspaces",
|
|
2512
|
+
import_node_path8.default.relative(scanPath, rootPkgPath),
|
|
2513
|
+
err
|
|
2389
2514
|
);
|
|
2390
2515
|
}
|
|
2391
2516
|
}
|
|
@@ -2401,9 +2526,9 @@ async function discoverServices(scanPath) {
|
|
|
2401
2526
|
scanPath,
|
|
2402
2527
|
{ maxDepth: parseScanDepth(), ig },
|
|
2403
2528
|
async (dir) => {
|
|
2404
|
-
if (await exists(
|
|
2529
|
+
if (await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2405
2530
|
candidateDirs.push(dir);
|
|
2406
|
-
} else if (await exists(
|
|
2531
|
+
} 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"))) {
|
|
2407
2532
|
candidateDirs.push(dir);
|
|
2408
2533
|
}
|
|
2409
2534
|
}
|
|
@@ -2417,8 +2542,8 @@ async function discoverServices(scanPath) {
|
|
|
2417
2542
|
if (!service) continue;
|
|
2418
2543
|
const existingDir = seen.get(service.node.name);
|
|
2419
2544
|
if (existingDir !== void 0) {
|
|
2420
|
-
const a =
|
|
2421
|
-
const b =
|
|
2545
|
+
const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
|
|
2546
|
+
const b = import_node_path8.default.relative(scanPath, dir) || ".";
|
|
2422
2547
|
console.warn(
|
|
2423
2548
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2424
2549
|
);
|
|
@@ -2455,8 +2580,8 @@ function addServiceNodes(graph, services) {
|
|
|
2455
2580
|
|
|
2456
2581
|
// src/extract/aliases.ts
|
|
2457
2582
|
init_cjs_shims();
|
|
2458
|
-
var
|
|
2459
|
-
var
|
|
2583
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2584
|
+
var import_node_fs9 = require("fs");
|
|
2460
2585
|
var import_yaml2 = require("yaml");
|
|
2461
2586
|
var import_types7 = require("@neat.is/types");
|
|
2462
2587
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2483,14 +2608,14 @@ function indexServicesByName(services) {
|
|
|
2483
2608
|
const map = /* @__PURE__ */ new Map();
|
|
2484
2609
|
for (const s of services) {
|
|
2485
2610
|
map.set(s.node.name, s.node.id);
|
|
2486
|
-
map.set(
|
|
2611
|
+
map.set(import_node_path9.default.basename(s.dir), s.node.id);
|
|
2487
2612
|
}
|
|
2488
2613
|
return map;
|
|
2489
2614
|
}
|
|
2490
2615
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2491
2616
|
let composePath = null;
|
|
2492
2617
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2493
|
-
const abs =
|
|
2618
|
+
const abs = import_node_path9.default.join(scanPath, name);
|
|
2494
2619
|
if (await exists(abs)) {
|
|
2495
2620
|
composePath = abs;
|
|
2496
2621
|
break;
|
|
@@ -2501,8 +2626,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
2501
2626
|
try {
|
|
2502
2627
|
compose = await readYaml(composePath);
|
|
2503
2628
|
} catch (err) {
|
|
2504
|
-
|
|
2505
|
-
|
|
2629
|
+
recordExtractionError(
|
|
2630
|
+
"aliases compose",
|
|
2631
|
+
import_node_path9.default.relative(scanPath, composePath),
|
|
2632
|
+
err
|
|
2506
2633
|
);
|
|
2507
2634
|
return;
|
|
2508
2635
|
}
|
|
@@ -2544,15 +2671,13 @@ function parseDockerfileLabels(content) {
|
|
|
2544
2671
|
}
|
|
2545
2672
|
async function collectDockerfileAliases(graph, services) {
|
|
2546
2673
|
for (const service of services) {
|
|
2547
|
-
const dockerfilePath =
|
|
2674
|
+
const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
|
|
2548
2675
|
if (!await exists(dockerfilePath)) continue;
|
|
2549
2676
|
let content;
|
|
2550
2677
|
try {
|
|
2551
|
-
content = await
|
|
2678
|
+
content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
|
|
2552
2679
|
} catch (err) {
|
|
2553
|
-
|
|
2554
|
-
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2555
|
-
);
|
|
2680
|
+
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
2556
2681
|
continue;
|
|
2557
2682
|
}
|
|
2558
2683
|
const aliases = parseDockerfileLabels(content);
|
|
@@ -2562,13 +2687,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2562
2687
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2563
2688
|
if (depth > max) return [];
|
|
2564
2689
|
const out = [];
|
|
2565
|
-
const entries = await
|
|
2690
|
+
const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2566
2691
|
for (const entry of entries) {
|
|
2567
2692
|
if (entry.isDirectory()) {
|
|
2568
2693
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2569
|
-
out.push(...await walkYamlFiles(
|
|
2570
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2571
|
-
out.push(
|
|
2694
|
+
out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
|
|
2695
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
|
|
2696
|
+
out.push(import_node_path9.default.join(start, entry.name));
|
|
2572
2697
|
}
|
|
2573
2698
|
}
|
|
2574
2699
|
return out;
|
|
@@ -2595,7 +2720,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2595
2720
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2596
2721
|
const files = await walkYamlFiles(scanPath);
|
|
2597
2722
|
for (const file of files) {
|
|
2598
|
-
const content = await
|
|
2723
|
+
const content = await import_node_fs9.promises.readFile(file, "utf8");
|
|
2599
2724
|
let docs;
|
|
2600
2725
|
try {
|
|
2601
2726
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2620,14 +2745,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2620
2745
|
|
|
2621
2746
|
// src/extract/databases/index.ts
|
|
2622
2747
|
init_cjs_shims();
|
|
2623
|
-
var
|
|
2748
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2624
2749
|
var import_types8 = require("@neat.is/types");
|
|
2625
2750
|
|
|
2626
2751
|
// src/extract/databases/db-config-yaml.ts
|
|
2627
2752
|
init_cjs_shims();
|
|
2628
|
-
var
|
|
2753
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2629
2754
|
async function parse(serviceDir) {
|
|
2630
|
-
const yamlPath =
|
|
2755
|
+
const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
|
|
2631
2756
|
if (!await exists(yamlPath)) return [];
|
|
2632
2757
|
const raw = await readYaml(yamlPath);
|
|
2633
2758
|
return [
|
|
@@ -2645,13 +2770,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2645
2770
|
|
|
2646
2771
|
// src/extract/databases/dotenv.ts
|
|
2647
2772
|
init_cjs_shims();
|
|
2648
|
-
var
|
|
2649
|
-
var
|
|
2773
|
+
var import_node_fs11 = require("fs");
|
|
2774
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2650
2775
|
|
|
2651
2776
|
// src/extract/databases/shared.ts
|
|
2652
2777
|
init_cjs_shims();
|
|
2653
|
-
var
|
|
2654
|
-
var
|
|
2778
|
+
var import_node_fs10 = require("fs");
|
|
2779
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2655
2780
|
function schemeToEngine(scheme) {
|
|
2656
2781
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2657
2782
|
switch (s) {
|
|
@@ -2690,14 +2815,14 @@ function parseConnectionString(url) {
|
|
|
2690
2815
|
}
|
|
2691
2816
|
async function readIfExists(filePath) {
|
|
2692
2817
|
try {
|
|
2693
|
-
return await
|
|
2818
|
+
return await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2694
2819
|
} catch {
|
|
2695
2820
|
return null;
|
|
2696
2821
|
}
|
|
2697
2822
|
}
|
|
2698
2823
|
async function findFirst(serviceDir, candidates) {
|
|
2699
2824
|
for (const rel of candidates) {
|
|
2700
|
-
const abs =
|
|
2825
|
+
const abs = import_node_path11.default.join(serviceDir, rel);
|
|
2701
2826
|
const content = await readIfExists(abs);
|
|
2702
2827
|
if (content !== null) return abs;
|
|
2703
2828
|
}
|
|
@@ -2748,15 +2873,15 @@ function parseDotenvLine(line) {
|
|
|
2748
2873
|
return { key, value };
|
|
2749
2874
|
}
|
|
2750
2875
|
async function parse2(serviceDir) {
|
|
2751
|
-
const entries = await
|
|
2876
|
+
const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2752
2877
|
const configs = [];
|
|
2753
2878
|
const seen = /* @__PURE__ */ new Set();
|
|
2754
2879
|
for (const entry of entries) {
|
|
2755
2880
|
if (!entry.isFile()) continue;
|
|
2756
2881
|
const match = isConfigFile(entry.name);
|
|
2757
2882
|
if (!match.match || match.fileType !== "env") continue;
|
|
2758
|
-
const filePath =
|
|
2759
|
-
const content = await
|
|
2883
|
+
const filePath = import_node_path12.default.join(serviceDir, entry.name);
|
|
2884
|
+
const content = await import_node_fs11.promises.readFile(filePath, "utf8");
|
|
2760
2885
|
for (const line of content.split("\n")) {
|
|
2761
2886
|
const parsed = parseDotenvLine(line);
|
|
2762
2887
|
if (!parsed) continue;
|
|
@@ -2775,9 +2900,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2775
2900
|
|
|
2776
2901
|
// src/extract/databases/prisma.ts
|
|
2777
2902
|
init_cjs_shims();
|
|
2778
|
-
var
|
|
2903
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2779
2904
|
async function parse3(serviceDir) {
|
|
2780
|
-
const schemaPath =
|
|
2905
|
+
const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2781
2906
|
const content = await readIfExists(schemaPath);
|
|
2782
2907
|
if (!content) return [];
|
|
2783
2908
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2909,10 +3034,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2909
3034
|
|
|
2910
3035
|
// src/extract/databases/ormconfig.ts
|
|
2911
3036
|
init_cjs_shims();
|
|
2912
|
-
var
|
|
3037
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2913
3038
|
async function parse6(serviceDir) {
|
|
2914
3039
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2915
|
-
const abs =
|
|
3040
|
+
const abs = import_node_path14.default.join(serviceDir, candidate);
|
|
2916
3041
|
if (!await exists(abs)) continue;
|
|
2917
3042
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2918
3043
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2972,9 +3097,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2972
3097
|
|
|
2973
3098
|
// src/extract/databases/sequelize.ts
|
|
2974
3099
|
init_cjs_shims();
|
|
2975
|
-
var
|
|
3100
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2976
3101
|
async function parse8(serviceDir) {
|
|
2977
|
-
const configPath =
|
|
3102
|
+
const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
|
|
2978
3103
|
if (!await exists(configPath)) return [];
|
|
2979
3104
|
const raw = await readJson(configPath);
|
|
2980
3105
|
const out = [];
|
|
@@ -3001,7 +3126,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
3001
3126
|
|
|
3002
3127
|
// src/extract/databases/docker-compose.ts
|
|
3003
3128
|
init_cjs_shims();
|
|
3004
|
-
var
|
|
3129
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
3005
3130
|
function portFromService(svc) {
|
|
3006
3131
|
for (const raw of svc.ports ?? []) {
|
|
3007
3132
|
const str = String(raw);
|
|
@@ -3028,7 +3153,7 @@ function databaseFromEnv(svc) {
|
|
|
3028
3153
|
}
|
|
3029
3154
|
async function parse9(serviceDir) {
|
|
3030
3155
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3031
|
-
const abs =
|
|
3156
|
+
const abs = import_node_path16.default.join(serviceDir, name);
|
|
3032
3157
|
if (!await exists(abs)) continue;
|
|
3033
3158
|
const raw = await readYaml(abs);
|
|
3034
3159
|
if (!raw?.services) return [];
|
|
@@ -3206,11 +3331,12 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3206
3331
|
target: dbNode.id,
|
|
3207
3332
|
type: import_types8.EdgeType.CONNECTS_TO,
|
|
3208
3333
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3334
|
+
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
3335
|
+
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3336
|
+
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3337
|
+
evidence: {
|
|
3338
|
+
file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
|
|
3339
|
+
}
|
|
3214
3340
|
};
|
|
3215
3341
|
if (!graph.hasEdge(edge.id)) {
|
|
3216
3342
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3236,15 +3362,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3236
3362
|
|
|
3237
3363
|
// src/extract/configs.ts
|
|
3238
3364
|
init_cjs_shims();
|
|
3239
|
-
var
|
|
3240
|
-
var
|
|
3365
|
+
var import_node_fs12 = require("fs");
|
|
3366
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
3241
3367
|
var import_types9 = require("@neat.is/types");
|
|
3242
3368
|
async function walkConfigFiles(dir) {
|
|
3243
3369
|
const out = [];
|
|
3244
3370
|
async function walk(current) {
|
|
3245
|
-
const entries = await
|
|
3371
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
|
|
3246
3372
|
for (const entry of entries) {
|
|
3247
|
-
const full =
|
|
3373
|
+
const full = import_node_path18.default.join(current, entry.name);
|
|
3248
3374
|
if (entry.isDirectory()) {
|
|
3249
3375
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
3250
3376
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
@@ -3261,13 +3387,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3261
3387
|
for (const service of services) {
|
|
3262
3388
|
const configFiles = await walkConfigFiles(service.dir);
|
|
3263
3389
|
for (const file of configFiles) {
|
|
3264
|
-
const relPath =
|
|
3390
|
+
const relPath = import_node_path18.default.relative(scanPath, file);
|
|
3265
3391
|
const node = {
|
|
3266
3392
|
id: (0, import_types9.configId)(relPath),
|
|
3267
3393
|
type: import_types9.NodeType.ConfigNode,
|
|
3268
|
-
name:
|
|
3394
|
+
name: import_node_path18.default.basename(file),
|
|
3269
3395
|
path: relPath,
|
|
3270
|
-
fileType: isConfigFile(
|
|
3396
|
+
fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
|
|
3271
3397
|
};
|
|
3272
3398
|
if (!graph.hasNode(node.id)) {
|
|
3273
3399
|
graph.addNode(node.id, node);
|
|
@@ -3279,7 +3405,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3279
3405
|
target: node.id,
|
|
3280
3406
|
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
3281
3407
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3282
|
-
evidence: { file: relPath.split(
|
|
3408
|
+
evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
|
|
3283
3409
|
};
|
|
3284
3410
|
if (!graph.hasEdge(edge.id)) {
|
|
3285
3411
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3296,7 +3422,7 @@ var import_types15 = require("@neat.is/types");
|
|
|
3296
3422
|
|
|
3297
3423
|
// src/extract/calls/http.ts
|
|
3298
3424
|
init_cjs_shims();
|
|
3299
|
-
var
|
|
3425
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3300
3426
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3301
3427
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3302
3428
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -3304,17 +3430,17 @@ var import_types10 = require("@neat.is/types");
|
|
|
3304
3430
|
|
|
3305
3431
|
// src/extract/calls/shared.ts
|
|
3306
3432
|
init_cjs_shims();
|
|
3307
|
-
var
|
|
3308
|
-
var
|
|
3433
|
+
var import_node_fs13 = require("fs");
|
|
3434
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3309
3435
|
async function walkSourceFiles(dir) {
|
|
3310
3436
|
const out = [];
|
|
3311
3437
|
async function walk(current) {
|
|
3312
|
-
const entries = await
|
|
3438
|
+
const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3313
3439
|
for (const entry of entries) {
|
|
3314
|
-
const full =
|
|
3440
|
+
const full = import_node_path19.default.join(current, entry.name);
|
|
3315
3441
|
if (entry.isDirectory()) {
|
|
3316
3442
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
3317
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3443
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
|
|
3318
3444
|
out.push(full);
|
|
3319
3445
|
}
|
|
3320
3446
|
}
|
|
@@ -3327,7 +3453,7 @@ async function loadSourceFiles(dir) {
|
|
|
3327
3453
|
const out = [];
|
|
3328
3454
|
for (const p of paths) {
|
|
3329
3455
|
try {
|
|
3330
|
-
const content = await
|
|
3456
|
+
const content = await import_node_fs13.promises.readFile(p, "utf8");
|
|
3331
3457
|
out.push({ path: p, content });
|
|
3332
3458
|
} catch {
|
|
3333
3459
|
}
|
|
@@ -3346,8 +3472,27 @@ function snippet(text, line) {
|
|
|
3346
3472
|
|
|
3347
3473
|
// src/extract/calls/http.ts
|
|
3348
3474
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3475
|
+
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3476
|
+
function isInsideJsxExternalLink(node) {
|
|
3477
|
+
let cursor = node.parent;
|
|
3478
|
+
while (cursor) {
|
|
3479
|
+
if (cursor.type === "jsx_attribute") {
|
|
3480
|
+
let owner = cursor.parent;
|
|
3481
|
+
while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
|
|
3482
|
+
owner = owner.parent;
|
|
3483
|
+
}
|
|
3484
|
+
if (!owner) return false;
|
|
3485
|
+
const tagNode = owner.namedChild(0);
|
|
3486
|
+
const tagName = tagNode?.text ?? "";
|
|
3487
|
+
const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
|
|
3488
|
+
return JSX_EXTERNAL_LINK_TAGS.has(right);
|
|
3489
|
+
}
|
|
3490
|
+
cursor = cursor.parent;
|
|
3491
|
+
}
|
|
3492
|
+
return false;
|
|
3493
|
+
}
|
|
3349
3494
|
function collectStringLiterals(node, out) {
|
|
3350
|
-
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
|
|
3495
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
|
|
3351
3496
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3352
3497
|
const child = node.namedChild(i);
|
|
3353
3498
|
if (child) collectStringLiterals(child, out);
|
|
@@ -3359,8 +3504,9 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3359
3504
|
collectStringLiterals(tree.rootNode, literals);
|
|
3360
3505
|
const targets = /* @__PURE__ */ new Set();
|
|
3361
3506
|
for (const lit of literals) {
|
|
3507
|
+
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
3362
3508
|
for (const host of knownHosts) {
|
|
3363
|
-
if (lit.
|
|
3509
|
+
if (urlMatchesHost(lit.text, host)) {
|
|
3364
3510
|
targets.add(host);
|
|
3365
3511
|
}
|
|
3366
3512
|
}
|
|
@@ -3383,9 +3529,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3383
3529
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3384
3530
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3385
3531
|
for (const service of services) {
|
|
3386
|
-
knownHosts.add(
|
|
3532
|
+
knownHosts.add(import_node_path20.default.basename(service.dir));
|
|
3387
3533
|
knownHosts.add(service.pkg.name);
|
|
3388
|
-
hostToNodeId.set(
|
|
3534
|
+
hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
|
|
3389
3535
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3390
3536
|
}
|
|
3391
3537
|
let edgesAdded = 0;
|
|
@@ -3393,14 +3539,13 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3393
3539
|
const files = await loadSourceFiles(service.dir);
|
|
3394
3540
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3395
3541
|
for (const file of files) {
|
|
3396
|
-
|
|
3542
|
+
if (isTestPath(file.path)) continue;
|
|
3543
|
+
const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3397
3544
|
let targets;
|
|
3398
3545
|
try {
|
|
3399
3546
|
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3400
3547
|
} catch (err) {
|
|
3401
|
-
|
|
3402
|
-
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3403
|
-
);
|
|
3548
|
+
recordExtractionError("http call extraction", file.path, err);
|
|
3404
3549
|
continue;
|
|
3405
3550
|
}
|
|
3406
3551
|
for (const t of targets) {
|
|
@@ -3421,7 +3566,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3421
3566
|
type: import_types10.EdgeType.CALLS,
|
|
3422
3567
|
provenance: import_types10.Provenance.EXTRACTED,
|
|
3423
3568
|
evidence: {
|
|
3424
|
-
file:
|
|
3569
|
+
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3425
3570
|
line,
|
|
3426
3571
|
snippet: snippet(fileContent, line)
|
|
3427
3572
|
}
|
|
@@ -3437,7 +3582,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3437
3582
|
|
|
3438
3583
|
// src/extract/calls/kafka.ts
|
|
3439
3584
|
init_cjs_shims();
|
|
3440
|
-
var
|
|
3585
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3441
3586
|
var import_types11 = require("@neat.is/types");
|
|
3442
3587
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3443
3588
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3464,7 +3609,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3464
3609
|
kind: "kafka-topic",
|
|
3465
3610
|
edgeType,
|
|
3466
3611
|
evidence: {
|
|
3467
|
-
file:
|
|
3612
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3468
3613
|
line,
|
|
3469
3614
|
snippet: snippet(file.content, line)
|
|
3470
3615
|
}
|
|
@@ -3477,7 +3622,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3477
3622
|
|
|
3478
3623
|
// src/extract/calls/redis.ts
|
|
3479
3624
|
init_cjs_shims();
|
|
3480
|
-
var
|
|
3625
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3481
3626
|
var import_types12 = require("@neat.is/types");
|
|
3482
3627
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3483
3628
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3496,7 +3641,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3496
3641
|
kind: "redis",
|
|
3497
3642
|
edgeType: "CALLS",
|
|
3498
3643
|
evidence: {
|
|
3499
|
-
file:
|
|
3644
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3500
3645
|
line,
|
|
3501
3646
|
snippet: snippet(file.content, line)
|
|
3502
3647
|
}
|
|
@@ -3507,7 +3652,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3507
3652
|
|
|
3508
3653
|
// src/extract/calls/aws.ts
|
|
3509
3654
|
init_cjs_shims();
|
|
3510
|
-
var
|
|
3655
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3511
3656
|
var import_types13 = require("@neat.is/types");
|
|
3512
3657
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3513
3658
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3537,7 +3682,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3537
3682
|
kind,
|
|
3538
3683
|
edgeType: "CALLS",
|
|
3539
3684
|
evidence: {
|
|
3540
|
-
file:
|
|
3685
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3541
3686
|
line,
|
|
3542
3687
|
snippet: snippet(file.content, line)
|
|
3543
3688
|
}
|
|
@@ -3562,16 +3707,42 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3562
3707
|
|
|
3563
3708
|
// src/extract/calls/grpc.ts
|
|
3564
3709
|
init_cjs_shims();
|
|
3565
|
-
var
|
|
3710
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3566
3711
|
var import_types14 = require("@neat.is/types");
|
|
3567
3712
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3713
|
+
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3714
|
+
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3568
3715
|
function isLikelyAddress(value) {
|
|
3569
3716
|
if (!value) return false;
|
|
3570
3717
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3571
3718
|
}
|
|
3719
|
+
function normaliseForMatch(s) {
|
|
3720
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3721
|
+
}
|
|
3722
|
+
function readImports(content) {
|
|
3723
|
+
const awsSdkSuffixes = /* @__PURE__ */ new Map();
|
|
3724
|
+
AWS_SDK_IMPORT_RE.lastIndex = 0;
|
|
3725
|
+
let m;
|
|
3726
|
+
while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
|
|
3727
|
+
const raw = m[1];
|
|
3728
|
+
awsSdkSuffixes.set(normaliseForMatch(raw), raw);
|
|
3729
|
+
}
|
|
3730
|
+
return {
|
|
3731
|
+
awsSdkSuffixes,
|
|
3732
|
+
hasGrpcImport: GRPC_IMPORT_RE.test(content)
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
function classifyClient(symbol, ctx) {
|
|
3736
|
+
const key = normaliseForMatch(symbol);
|
|
3737
|
+
const awsRaw = ctx.awsSdkSuffixes.get(key);
|
|
3738
|
+
if (awsRaw) return { kind: `aws-${awsRaw}` };
|
|
3739
|
+
if (ctx.hasGrpcImport) return { kind: "grpc-service" };
|
|
3740
|
+
return null;
|
|
3741
|
+
}
|
|
3572
3742
|
function grpcEndpointsFromFile(file, serviceDir) {
|
|
3573
3743
|
const out = [];
|
|
3574
3744
|
const seen = /* @__PURE__ */ new Set();
|
|
3745
|
+
const ctx = readImports(file.content);
|
|
3575
3746
|
GRPC_CLIENT_RE.lastIndex = 0;
|
|
3576
3747
|
let m;
|
|
3577
3748
|
while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
|
|
@@ -3579,15 +3750,18 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3579
3750
|
const addr = m[2]?.trim();
|
|
3580
3751
|
const name = isLikelyAddress(addr) ? addr : symbol;
|
|
3581
3752
|
if (seen.has(name)) continue;
|
|
3753
|
+
const classified = classifyClient(symbol, ctx);
|
|
3754
|
+
if (!classified) continue;
|
|
3582
3755
|
seen.add(name);
|
|
3756
|
+
const { kind } = classified;
|
|
3583
3757
|
const line = lineOf(file.content, m[0]);
|
|
3584
3758
|
out.push({
|
|
3585
|
-
infraId: (0, import_types14.infraId)(
|
|
3759
|
+
infraId: (0, import_types14.infraId)(kind, name),
|
|
3586
3760
|
name,
|
|
3587
|
-
kind
|
|
3761
|
+
kind,
|
|
3588
3762
|
edgeType: "CALLS",
|
|
3589
3763
|
evidence: {
|
|
3590
|
-
file:
|
|
3764
|
+
file: import_node_path24.default.relative(serviceDir, file.path),
|
|
3591
3765
|
line,
|
|
3592
3766
|
snippet: snippet(file.content, line)
|
|
3593
3767
|
}
|
|
@@ -3607,6 +3781,9 @@ function edgeTypeFromEndpoint(ep) {
|
|
|
3607
3781
|
return import_types15.EdgeType.CALLS;
|
|
3608
3782
|
}
|
|
3609
3783
|
}
|
|
3784
|
+
function isAwsKind(kind) {
|
|
3785
|
+
return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
|
|
3786
|
+
}
|
|
3610
3787
|
async function addExternalEndpointEdges(graph, services) {
|
|
3611
3788
|
let nodesAdded = 0;
|
|
3612
3789
|
let edgesAdded = 0;
|
|
@@ -3614,10 +3791,13 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3614
3791
|
const files = await loadSourceFiles(service.dir);
|
|
3615
3792
|
const endpoints = [];
|
|
3616
3793
|
for (const file of files) {
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
endpoints.push(...
|
|
3794
|
+
if (isTestPath(file.path)) continue;
|
|
3795
|
+
const masked = maskCommentsInSource(file.content);
|
|
3796
|
+
const maskedFile = { path: file.path, content: masked };
|
|
3797
|
+
endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
|
|
3798
|
+
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
3799
|
+
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
3800
|
+
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
3621
3801
|
}
|
|
3622
3802
|
if (endpoints.length === 0) continue;
|
|
3623
3803
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -3627,7 +3807,10 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3627
3807
|
id: ep.infraId,
|
|
3628
3808
|
type: import_types15.NodeType.InfraNode,
|
|
3629
3809
|
name: ep.name,
|
|
3630
|
-
|
|
3810
|
+
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3811
|
+
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
3812
|
+
// bucket / table kinds from aws.ts.
|
|
3813
|
+
provider: isAwsKind(ep.kind) ? "aws" : "self",
|
|
3631
3814
|
kind: ep.kind
|
|
3632
3815
|
};
|
|
3633
3816
|
graph.addNode(node.id, node);
|
|
@@ -3667,7 +3850,7 @@ init_cjs_shims();
|
|
|
3667
3850
|
|
|
3668
3851
|
// src/extract/infra/docker-compose.ts
|
|
3669
3852
|
init_cjs_shims();
|
|
3670
|
-
var
|
|
3853
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3671
3854
|
var import_types17 = require("@neat.is/types");
|
|
3672
3855
|
|
|
3673
3856
|
// src/extract/infra/shared.ts
|
|
@@ -3705,7 +3888,7 @@ function dependsOnList(value) {
|
|
|
3705
3888
|
}
|
|
3706
3889
|
function serviceNameToServiceNode(name, services) {
|
|
3707
3890
|
for (const s of services) {
|
|
3708
|
-
if (s.node.name === name ||
|
|
3891
|
+
if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
|
|
3709
3892
|
}
|
|
3710
3893
|
return null;
|
|
3711
3894
|
}
|
|
@@ -3714,7 +3897,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3714
3897
|
let edgesAdded = 0;
|
|
3715
3898
|
let composePath = null;
|
|
3716
3899
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3717
|
-
const abs =
|
|
3900
|
+
const abs = import_node_path25.default.join(scanPath, name);
|
|
3718
3901
|
if (await exists(abs)) {
|
|
3719
3902
|
composePath = abs;
|
|
3720
3903
|
break;
|
|
@@ -3725,13 +3908,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3725
3908
|
try {
|
|
3726
3909
|
compose = await readYaml(composePath);
|
|
3727
3910
|
} catch (err) {
|
|
3728
|
-
|
|
3729
|
-
|
|
3911
|
+
recordExtractionError(
|
|
3912
|
+
"infra docker-compose",
|
|
3913
|
+
import_node_path25.default.relative(scanPath, composePath),
|
|
3914
|
+
err
|
|
3730
3915
|
);
|
|
3731
3916
|
return { nodesAdded, edgesAdded };
|
|
3732
3917
|
}
|
|
3733
3918
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3734
|
-
const evidenceFile =
|
|
3919
|
+
const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
|
|
3735
3920
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3736
3921
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3737
3922
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3772,8 +3957,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3772
3957
|
|
|
3773
3958
|
// src/extract/infra/dockerfile.ts
|
|
3774
3959
|
init_cjs_shims();
|
|
3775
|
-
var
|
|
3776
|
-
var
|
|
3960
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3961
|
+
var import_node_fs14 = require("fs");
|
|
3777
3962
|
var import_types18 = require("@neat.is/types");
|
|
3778
3963
|
function runtimeImage(content) {
|
|
3779
3964
|
const lines = content.split("\n");
|
|
@@ -3793,14 +3978,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3793
3978
|
let nodesAdded = 0;
|
|
3794
3979
|
let edgesAdded = 0;
|
|
3795
3980
|
for (const service of services) {
|
|
3796
|
-
const dockerfilePath =
|
|
3981
|
+
const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
|
|
3797
3982
|
if (!await exists(dockerfilePath)) continue;
|
|
3798
3983
|
let content;
|
|
3799
3984
|
try {
|
|
3800
|
-
content = await
|
|
3985
|
+
content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
|
|
3801
3986
|
} catch (err) {
|
|
3802
|
-
|
|
3803
|
-
|
|
3987
|
+
recordExtractionError(
|
|
3988
|
+
"infra dockerfile",
|
|
3989
|
+
import_node_path26.default.relative(scanPath, dockerfilePath),
|
|
3990
|
+
err
|
|
3804
3991
|
);
|
|
3805
3992
|
continue;
|
|
3806
3993
|
}
|
|
@@ -3820,7 +4007,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3820
4007
|
type: import_types18.EdgeType.RUNS_ON,
|
|
3821
4008
|
provenance: import_types18.Provenance.EXTRACTED,
|
|
3822
4009
|
evidence: {
|
|
3823
|
-
file:
|
|
4010
|
+
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
3824
4011
|
}
|
|
3825
4012
|
};
|
|
3826
4013
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3832,19 +4019,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3832
4019
|
|
|
3833
4020
|
// src/extract/infra/terraform.ts
|
|
3834
4021
|
init_cjs_shims();
|
|
3835
|
-
var
|
|
3836
|
-
var
|
|
4022
|
+
var import_node_fs15 = require("fs");
|
|
4023
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3837
4024
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3838
4025
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3839
4026
|
if (depth > max) return [];
|
|
3840
4027
|
const out = [];
|
|
3841
|
-
const entries = await
|
|
4028
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3842
4029
|
for (const entry of entries) {
|
|
3843
4030
|
if (entry.isDirectory()) {
|
|
3844
4031
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3845
|
-
out.push(...await walkTfFiles(
|
|
4032
|
+
out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
|
|
3846
4033
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3847
|
-
out.push(
|
|
4034
|
+
out.push(import_node_path27.default.join(start, entry.name));
|
|
3848
4035
|
}
|
|
3849
4036
|
}
|
|
3850
4037
|
return out;
|
|
@@ -3853,7 +4040,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3853
4040
|
let nodesAdded = 0;
|
|
3854
4041
|
const files = await walkTfFiles(scanPath);
|
|
3855
4042
|
for (const file of files) {
|
|
3856
|
-
const content = await
|
|
4043
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3857
4044
|
RESOURCE_RE.lastIndex = 0;
|
|
3858
4045
|
let m;
|
|
3859
4046
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3871,8 +4058,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3871
4058
|
|
|
3872
4059
|
// src/extract/infra/k8s.ts
|
|
3873
4060
|
init_cjs_shims();
|
|
3874
|
-
var
|
|
3875
|
-
var
|
|
4061
|
+
var import_node_fs16 = require("fs");
|
|
4062
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3876
4063
|
var import_yaml3 = require("yaml");
|
|
3877
4064
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3878
4065
|
Service: "k8s-service",
|
|
@@ -3886,13 +4073,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3886
4073
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3887
4074
|
if (depth > max) return [];
|
|
3888
4075
|
const out = [];
|
|
3889
|
-
const entries = await
|
|
4076
|
+
const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3890
4077
|
for (const entry of entries) {
|
|
3891
4078
|
if (entry.isDirectory()) {
|
|
3892
4079
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3893
|
-
out.push(...await walkYamlFiles2(
|
|
3894
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3895
|
-
out.push(
|
|
4080
|
+
out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
|
|
4081
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
|
|
4082
|
+
out.push(import_node_path28.default.join(start, entry.name));
|
|
3896
4083
|
}
|
|
3897
4084
|
}
|
|
3898
4085
|
return out;
|
|
@@ -3901,7 +4088,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3901
4088
|
let nodesAdded = 0;
|
|
3902
4089
|
const files = await walkYamlFiles2(scanPath);
|
|
3903
4090
|
for (const file of files) {
|
|
3904
|
-
const content = await
|
|
4091
|
+
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
3905
4092
|
let docs;
|
|
3906
4093
|
try {
|
|
3907
4094
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3935,9 +4122,34 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3935
4122
|
};
|
|
3936
4123
|
}
|
|
3937
4124
|
|
|
4125
|
+
// src/extract/retire.ts
|
|
4126
|
+
init_cjs_shims();
|
|
4127
|
+
var import_node_fs17 = require("fs");
|
|
4128
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
4129
|
+
var import_types19 = require("@neat.is/types");
|
|
4130
|
+
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
4131
|
+
const toDrop = [];
|
|
4132
|
+
const bases = [scanPath, ...serviceDirs];
|
|
4133
|
+
graph.forEachEdge((id, attrs) => {
|
|
4134
|
+
const edge = attrs;
|
|
4135
|
+
if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
|
|
4136
|
+
const evidenceFile = edge.evidence?.file;
|
|
4137
|
+
if (!evidenceFile) return;
|
|
4138
|
+
if (import_node_path29.default.isAbsolute(evidenceFile)) {
|
|
4139
|
+
if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
|
|
4143
|
+
if (!found) toDrop.push(id);
|
|
4144
|
+
});
|
|
4145
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
4146
|
+
return toDrop.length;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
3938
4149
|
// src/extract/index.ts
|
|
3939
4150
|
async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
3940
4151
|
await ensureCompatLoaded();
|
|
4152
|
+
drainExtractionErrors();
|
|
3941
4153
|
const services = await discoverServices(scanPath);
|
|
3942
4154
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
3943
4155
|
await addServiceAliases(graph, scanPath, services);
|
|
@@ -3945,12 +4157,30 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3945
4157
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
3946
4158
|
const phase4 = await addCallEdges(graph, services);
|
|
3947
4159
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
4160
|
+
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
4161
|
+
graph,
|
|
4162
|
+
scanPath,
|
|
4163
|
+
services.map((s) => s.dir)
|
|
4164
|
+
);
|
|
3948
4165
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3949
4166
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
4167
|
+
const errorEntries = drainExtractionErrors();
|
|
4168
|
+
if (opts.errorsPath && errorEntries.length > 0) {
|
|
4169
|
+
try {
|
|
4170
|
+
await writeExtractionErrors(errorEntries, opts.errorsPath);
|
|
4171
|
+
} catch (err) {
|
|
4172
|
+
console.warn(
|
|
4173
|
+
`[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
|
|
4174
|
+
);
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
3950
4177
|
const result = {
|
|
3951
4178
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3952
4179
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3953
|
-
frontiersPromoted
|
|
4180
|
+
frontiersPromoted,
|
|
4181
|
+
extractionErrors: errorEntries.length,
|
|
4182
|
+
errorEntries,
|
|
4183
|
+
ghostsRetired
|
|
3954
4184
|
};
|
|
3955
4185
|
emitNeatEvent({
|
|
3956
4186
|
type: "extraction-complete",
|
|
@@ -3967,7 +4197,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3967
4197
|
|
|
3968
4198
|
// src/diff.ts
|
|
3969
4199
|
init_cjs_shims();
|
|
3970
|
-
var
|
|
4200
|
+
var import_node_fs18 = require("fs");
|
|
3971
4201
|
async function loadSnapshotForDiff(target) {
|
|
3972
4202
|
if (/^https?:\/\//i.test(target)) {
|
|
3973
4203
|
const res = await fetch(target);
|
|
@@ -3976,7 +4206,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
3976
4206
|
}
|
|
3977
4207
|
return await res.json();
|
|
3978
4208
|
}
|
|
3979
|
-
const raw = await
|
|
4209
|
+
const raw = await import_node_fs18.promises.readFile(target, "utf8");
|
|
3980
4210
|
return JSON.parse(raw);
|
|
3981
4211
|
}
|
|
3982
4212
|
function indexEntries(entries) {
|
|
@@ -4044,23 +4274,23 @@ function canonicalJson(value) {
|
|
|
4044
4274
|
|
|
4045
4275
|
// src/projects.ts
|
|
4046
4276
|
init_cjs_shims();
|
|
4047
|
-
var
|
|
4277
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4048
4278
|
function pathsForProject(project, baseDir) {
|
|
4049
4279
|
if (project === DEFAULT_PROJECT) {
|
|
4050
4280
|
return {
|
|
4051
|
-
snapshotPath:
|
|
4052
|
-
errorsPath:
|
|
4053
|
-
staleEventsPath:
|
|
4054
|
-
embeddingsCachePath:
|
|
4055
|
-
policyViolationsPath:
|
|
4281
|
+
snapshotPath: import_node_path30.default.join(baseDir, "graph.json"),
|
|
4282
|
+
errorsPath: import_node_path30.default.join(baseDir, "errors.ndjson"),
|
|
4283
|
+
staleEventsPath: import_node_path30.default.join(baseDir, "stale-events.ndjson"),
|
|
4284
|
+
embeddingsCachePath: import_node_path30.default.join(baseDir, "embeddings.json"),
|
|
4285
|
+
policyViolationsPath: import_node_path30.default.join(baseDir, "policy-violations.ndjson")
|
|
4056
4286
|
};
|
|
4057
4287
|
}
|
|
4058
4288
|
return {
|
|
4059
|
-
snapshotPath:
|
|
4060
|
-
errorsPath:
|
|
4061
|
-
staleEventsPath:
|
|
4062
|
-
embeddingsCachePath:
|
|
4063
|
-
policyViolationsPath:
|
|
4289
|
+
snapshotPath: import_node_path30.default.join(baseDir, `${project}.json`),
|
|
4290
|
+
errorsPath: import_node_path30.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4291
|
+
staleEventsPath: import_node_path30.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4292
|
+
embeddingsCachePath: import_node_path30.default.join(baseDir, `embeddings.${project}.json`),
|
|
4293
|
+
policyViolationsPath: import_node_path30.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4064
4294
|
};
|
|
4065
4295
|
}
|
|
4066
4296
|
var Projects = class {
|
|
@@ -4100,23 +4330,23 @@ function parseExtraProjects(raw) {
|
|
|
4100
4330
|
|
|
4101
4331
|
// src/registry.ts
|
|
4102
4332
|
init_cjs_shims();
|
|
4103
|
-
var
|
|
4333
|
+
var import_node_fs19 = require("fs");
|
|
4104
4334
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4105
|
-
var
|
|
4106
|
-
var
|
|
4335
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4336
|
+
var import_types20 = require("@neat.is/types");
|
|
4107
4337
|
function neatHome() {
|
|
4108
4338
|
const override = process.env.NEAT_HOME;
|
|
4109
|
-
if (override && override.length > 0) return
|
|
4110
|
-
return
|
|
4339
|
+
if (override && override.length > 0) return import_node_path31.default.resolve(override);
|
|
4340
|
+
return import_node_path31.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4111
4341
|
}
|
|
4112
4342
|
function registryPath() {
|
|
4113
|
-
return
|
|
4343
|
+
return import_node_path31.default.join(neatHome(), "projects.json");
|
|
4114
4344
|
}
|
|
4115
4345
|
async function readRegistry() {
|
|
4116
4346
|
const file = registryPath();
|
|
4117
4347
|
let raw;
|
|
4118
4348
|
try {
|
|
4119
|
-
raw = await
|
|
4349
|
+
raw = await import_node_fs19.promises.readFile(file, "utf8");
|
|
4120
4350
|
} catch (err) {
|
|
4121
4351
|
if (err.code === "ENOENT") {
|
|
4122
4352
|
return { version: 1, projects: [] };
|
|
@@ -4124,7 +4354,7 @@ async function readRegistry() {
|
|
|
4124
4354
|
throw err;
|
|
4125
4355
|
}
|
|
4126
4356
|
const parsed = JSON.parse(raw);
|
|
4127
|
-
return
|
|
4357
|
+
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4128
4358
|
}
|
|
4129
4359
|
async function getProject(name) {
|
|
4130
4360
|
const reg = await readRegistry();
|
|
@@ -4314,11 +4544,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4314
4544
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4315
4545
|
const parsed = [];
|
|
4316
4546
|
for (const c of candidates) {
|
|
4317
|
-
const r =
|
|
4547
|
+
const r = import_types21.DivergenceTypeSchema.safeParse(c);
|
|
4318
4548
|
if (!r.success) {
|
|
4319
4549
|
return reply.code(400).send({
|
|
4320
4550
|
error: `unknown divergence type "${c}"`,
|
|
4321
|
-
allowed:
|
|
4551
|
+
allowed: import_types21.DivergenceTypeSchema.options
|
|
4322
4552
|
});
|
|
4323
4553
|
}
|
|
4324
4554
|
parsed.push(r.data);
|
|
@@ -4502,7 +4732,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4502
4732
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4503
4733
|
let violations = await log.readAll();
|
|
4504
4734
|
if (req.query.severity) {
|
|
4505
|
-
const sev =
|
|
4735
|
+
const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4506
4736
|
if (!sev.success) {
|
|
4507
4737
|
return reply.code(400).send({
|
|
4508
4738
|
error: "invalid severity",
|
|
@@ -4519,7 +4749,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4519
4749
|
scope.post("/policies/check", async (req, reply) => {
|
|
4520
4750
|
const proj = resolveProject(registry, req, reply);
|
|
4521
4751
|
if (!proj) return;
|
|
4522
|
-
const parsed =
|
|
4752
|
+
const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4523
4753
|
if (!parsed.success) {
|
|
4524
4754
|
return reply.code(400).send({
|
|
4525
4755
|
error: "invalid /policies/check body",
|
|
@@ -4619,8 +4849,8 @@ async function buildApi(opts) {
|
|
|
4619
4849
|
|
|
4620
4850
|
// src/persist.ts
|
|
4621
4851
|
init_cjs_shims();
|
|
4622
|
-
var
|
|
4623
|
-
var
|
|
4852
|
+
var import_node_fs20 = require("fs");
|
|
4853
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4624
4854
|
var SCHEMA_VERSION = 2;
|
|
4625
4855
|
function migrateV1ToV2(payload) {
|
|
4626
4856
|
const nodes = payload.graph.nodes;
|
|
@@ -4634,7 +4864,7 @@ function migrateV1ToV2(payload) {
|
|
|
4634
4864
|
return { ...payload, schemaVersion: 2 };
|
|
4635
4865
|
}
|
|
4636
4866
|
async function ensureDir(filePath) {
|
|
4637
|
-
await
|
|
4867
|
+
await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(filePath), { recursive: true });
|
|
4638
4868
|
}
|
|
4639
4869
|
async function saveGraphToDisk(graph, outPath) {
|
|
4640
4870
|
await ensureDir(outPath);
|
|
@@ -4644,13 +4874,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
4644
4874
|
graph: graph.export()
|
|
4645
4875
|
};
|
|
4646
4876
|
const tmp = `${outPath}.tmp`;
|
|
4647
|
-
await
|
|
4648
|
-
await
|
|
4877
|
+
await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4878
|
+
await import_node_fs20.promises.rename(tmp, outPath);
|
|
4649
4879
|
}
|
|
4650
4880
|
async function loadGraphFromDisk(graph, outPath) {
|
|
4651
4881
|
let raw;
|
|
4652
4882
|
try {
|
|
4653
|
-
raw = await
|
|
4883
|
+
raw = await import_node_fs20.promises.readFile(outPath, "utf8");
|
|
4654
4884
|
} catch (err) {
|
|
4655
4885
|
if (err.code === "ENOENT") return;
|
|
4656
4886
|
throw err;
|
|
@@ -4707,8 +4937,8 @@ init_otel_grpc();
|
|
|
4707
4937
|
|
|
4708
4938
|
// src/search.ts
|
|
4709
4939
|
init_cjs_shims();
|
|
4710
|
-
var
|
|
4711
|
-
var
|
|
4940
|
+
var import_node_fs21 = require("fs");
|
|
4941
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
4712
4942
|
var import_node_crypto = require("crypto");
|
|
4713
4943
|
var DEFAULT_LIMIT = 10;
|
|
4714
4944
|
var NOMIC_DIM = 768;
|
|
@@ -4838,7 +5068,7 @@ async function pickEmbedder() {
|
|
|
4838
5068
|
}
|
|
4839
5069
|
async function readCache(cachePath) {
|
|
4840
5070
|
try {
|
|
4841
|
-
const raw = await
|
|
5071
|
+
const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
|
|
4842
5072
|
const parsed = JSON.parse(raw);
|
|
4843
5073
|
if (parsed.version !== 1) return null;
|
|
4844
5074
|
return parsed;
|
|
@@ -4847,8 +5077,8 @@ async function readCache(cachePath) {
|
|
|
4847
5077
|
}
|
|
4848
5078
|
}
|
|
4849
5079
|
async function writeCache(cachePath, cache) {
|
|
4850
|
-
await
|
|
4851
|
-
await
|
|
5080
|
+
await import_node_fs21.promises.mkdir(import_node_path35.default.dirname(cachePath), { recursive: true });
|
|
5081
|
+
await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
4852
5082
|
}
|
|
4853
5083
|
var VectorIndex = class {
|
|
4854
5084
|
constructor(embedder, cachePath) {
|
|
@@ -5027,14 +5257,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
|
|
|
5027
5257
|
async function main() {
|
|
5028
5258
|
const baseDirEnv = process.env.NEAT_OUT_DIR;
|
|
5029
5259
|
const legacyOutPath = process.env.NEAT_OUT_PATH;
|
|
5030
|
-
const baseDir = baseDirEnv ?
|
|
5031
|
-
const defaultScanPath =
|
|
5260
|
+
const baseDir = baseDirEnv ? import_node_path36.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path36.default.resolve(import_node_path36.default.dirname(legacyOutPath)) : import_node_path36.default.resolve("./neat-out");
|
|
5261
|
+
const defaultScanPath = import_node_path36.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
|
|
5032
5262
|
const registry = new Projects();
|
|
5033
5263
|
await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
|
|
5034
5264
|
for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
|
|
5035
5265
|
const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
5036
5266
|
const projectScan = process.env[envKey];
|
|
5037
|
-
await bootProject(registry, name, projectScan ?
|
|
5267
|
+
await bootProject(registry, name, projectScan ? import_node_path36.default.resolve(projectScan) : void 0, baseDir);
|
|
5038
5268
|
}
|
|
5039
5269
|
const host = process.env.HOST ?? "0.0.0.0";
|
|
5040
5270
|
const port = Number(process.env.PORT ?? 8080);
|