@neat.is/core 0.3.2 → 0.3.4
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-FIXKIYNF.js → chunk-33ZZ2ZID.js} +606 -240
- package/dist/chunk-33ZZ2ZID.js.map +1 -0
- package/dist/{chunk-66Z5IEFY.js → chunk-T3A2GK3X.js} +2 -2
- package/dist/cli.cjs +674 -337
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +26 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +565 -249
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +572 -256
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +535 -219
- 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-T3A2GK3X.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
function resolveProtoRoot() {
|
|
103
|
-
const here =
|
|
104
|
-
return
|
|
103
|
+
const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
104
|
+
return import_node_path34.default.resolve(here, "..", "proto");
|
|
105
105
|
}
|
|
106
106
|
function loadTraceService() {
|
|
107
107
|
const protoRoot = resolveProtoRoot();
|
|
@@ -156,13 +156,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
156
156
|
})
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
|
-
var import_node_url,
|
|
159
|
+
var import_node_url, import_node_path34, grpc, protoLoader;
|
|
160
160
|
var init_otel_grpc = __esm({
|
|
161
161
|
"src/otel-grpc.ts"() {
|
|
162
162
|
"use strict";
|
|
163
163
|
init_cjs_shims();
|
|
164
164
|
import_node_url = require("url");
|
|
165
|
-
|
|
165
|
+
import_node_path34 = __toESM(require("path"), 1);
|
|
166
166
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
167
167
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
168
168
|
init_otel();
|
|
@@ -259,10 +259,10 @@ function parseOtlpRequest(body) {
|
|
|
259
259
|
}
|
|
260
260
|
function loadProtobufDecoder() {
|
|
261
261
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
262
|
-
const here =
|
|
263
|
-
const protoRoot =
|
|
262
|
+
const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
|
+
const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
|
|
264
264
|
const root = new import_protobufjs.default.Root();
|
|
265
|
-
root.resolvePath = (_origin, target) =>
|
|
265
|
+
root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
|
|
266
266
|
root.loadSync(
|
|
267
267
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
268
|
{ keepCase: true }
|
|
@@ -362,12 +362,12 @@ function logSpanHandler(span) {
|
|
|
362
362
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
|
-
var
|
|
365
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
366
366
|
var init_otel = __esm({
|
|
367
367
|
"src/otel.ts"() {
|
|
368
368
|
"use strict";
|
|
369
369
|
init_cjs_shims();
|
|
370
|
-
|
|
370
|
+
import_node_path35 = __toESM(require("path"), 1);
|
|
371
371
|
import_node_url2 = require("url");
|
|
372
372
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
373
373
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -876,19 +876,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
876
876
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
877
877
|
let best = { path: [start], edges: [] };
|
|
878
878
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
879
|
-
function step(node,
|
|
880
|
-
if (
|
|
881
|
-
best = { path: [...
|
|
879
|
+
function step(node, path37, edges) {
|
|
880
|
+
if (path37.length > best.path.length) {
|
|
881
|
+
best = { path: [...path37], edges: [...edges] };
|
|
882
882
|
}
|
|
883
|
-
if (
|
|
883
|
+
if (path37.length - 1 >= maxDepth) return;
|
|
884
884
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
885
885
|
for (const [srcId, edge] of incoming) {
|
|
886
886
|
if (visited.has(srcId)) continue;
|
|
887
887
|
visited.add(srcId);
|
|
888
|
-
|
|
888
|
+
path37.push(srcId);
|
|
889
889
|
edges.push(edge);
|
|
890
|
-
step(srcId,
|
|
891
|
-
|
|
890
|
+
step(srcId, path37, edges);
|
|
891
|
+
path37.pop();
|
|
892
892
|
edges.pop();
|
|
893
893
|
visited.delete(srcId);
|
|
894
894
|
}
|
|
@@ -1577,35 +1577,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1577
1577
|
const existing = graph.getEdgeAttributes(id);
|
|
1578
1578
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1579
1579
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1580
|
+
const newSignal = {
|
|
1581
|
+
spanCount: newSpanCount,
|
|
1582
|
+
errorCount: newErrorCount,
|
|
1583
|
+
lastObservedAgeMs: 0
|
|
1584
|
+
};
|
|
1580
1585
|
const updated = {
|
|
1581
1586
|
...existing,
|
|
1582
1587
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1583
1588
|
lastObserved: ts,
|
|
1584
1589
|
callCount: newSpanCount,
|
|
1585
|
-
signal:
|
|
1586
|
-
|
|
1587
|
-
errorCount: newErrorCount,
|
|
1588
|
-
lastObservedAgeMs: 0
|
|
1589
|
-
},
|
|
1590
|
-
confidence: 1
|
|
1590
|
+
signal: newSignal,
|
|
1591
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(newSignal)
|
|
1591
1592
|
};
|
|
1592
1593
|
graph.replaceEdgeAttributes(id, updated);
|
|
1593
1594
|
return { edge: updated, created: false };
|
|
1594
1595
|
}
|
|
1596
|
+
const signal = {
|
|
1597
|
+
spanCount: 1,
|
|
1598
|
+
errorCount: isError ? 1 : 0,
|
|
1599
|
+
lastObservedAgeMs: 0
|
|
1600
|
+
};
|
|
1595
1601
|
const edge = {
|
|
1596
1602
|
id,
|
|
1597
1603
|
source,
|
|
1598
1604
|
target,
|
|
1599
1605
|
type,
|
|
1600
1606
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1601
|
-
confidence:
|
|
1607
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(signal),
|
|
1602
1608
|
lastObserved: ts,
|
|
1603
1609
|
callCount: 1,
|
|
1604
|
-
signal
|
|
1605
|
-
spanCount: 1,
|
|
1606
|
-
errorCount: isError ? 1 : 0,
|
|
1607
|
-
lastObservedAgeMs: 0
|
|
1608
|
-
}
|
|
1610
|
+
signal
|
|
1609
1611
|
};
|
|
1610
1612
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1611
1613
|
return { edge, created: true };
|
|
@@ -1925,8 +1927,8 @@ async function readErrorEvents(errorsPath) {
|
|
|
1925
1927
|
|
|
1926
1928
|
// src/extract/services.ts
|
|
1927
1929
|
init_cjs_shims();
|
|
1928
|
-
var
|
|
1929
|
-
var
|
|
1930
|
+
var import_node_fs8 = require("fs");
|
|
1931
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1930
1932
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
1931
1933
|
var import_minimatch2 = require("minimatch");
|
|
1932
1934
|
var import_types5 = require("@neat.is/types");
|
|
@@ -1950,9 +1952,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1950
1952
|
function isConfigFile(name) {
|
|
1951
1953
|
const ext = import_node_path4.default.extname(name);
|
|
1952
1954
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
1953
|
-
if (name === ".env" || name.startsWith(".env."))
|
|
1955
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
1956
|
+
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
1957
|
+
return { match: true, fileType: "env" };
|
|
1958
|
+
}
|
|
1954
1959
|
return { match: false, fileType: "" };
|
|
1955
1960
|
}
|
|
1961
|
+
function isTestPath(filePath) {
|
|
1962
|
+
const normalised = filePath.replace(/\\/g, "/");
|
|
1963
|
+
const segments = normalised.split("/");
|
|
1964
|
+
for (const seg of segments) {
|
|
1965
|
+
if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
const base = segments[segments.length - 1] ?? "";
|
|
1970
|
+
return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
|
|
1971
|
+
}
|
|
1972
|
+
function isEnvTemplateFile(name) {
|
|
1973
|
+
if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
|
|
1977
|
+
}
|
|
1978
|
+
function maskCommentsInSource(src) {
|
|
1979
|
+
const len = src.length;
|
|
1980
|
+
const out = new Array(len);
|
|
1981
|
+
let i = 0;
|
|
1982
|
+
let inString = 0;
|
|
1983
|
+
let escaped = false;
|
|
1984
|
+
while (i < len) {
|
|
1985
|
+
const c = src[i];
|
|
1986
|
+
if (inString !== 0) {
|
|
1987
|
+
out[i] = c;
|
|
1988
|
+
if (escaped) {
|
|
1989
|
+
escaped = false;
|
|
1990
|
+
} else if (c === "\\") {
|
|
1991
|
+
escaped = true;
|
|
1992
|
+
} else if (c === inString) {
|
|
1993
|
+
inString = 0;
|
|
1994
|
+
}
|
|
1995
|
+
i++;
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
if (c === "/" && i + 1 < len) {
|
|
1999
|
+
const next = src[i + 1];
|
|
2000
|
+
if (next === "/") {
|
|
2001
|
+
out[i] = " ";
|
|
2002
|
+
out[i + 1] = " ";
|
|
2003
|
+
let j = i + 2;
|
|
2004
|
+
while (j < len && src[j] !== "\n") {
|
|
2005
|
+
out[j] = " ";
|
|
2006
|
+
j++;
|
|
2007
|
+
}
|
|
2008
|
+
i = j;
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
if (next === "*") {
|
|
2012
|
+
out[i] = " ";
|
|
2013
|
+
out[i + 1] = " ";
|
|
2014
|
+
let j = i + 2;
|
|
2015
|
+
while (j < len) {
|
|
2016
|
+
if (src[j] === "\n") {
|
|
2017
|
+
out[j] = "\n";
|
|
2018
|
+
j++;
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
|
|
2022
|
+
out[j] = " ";
|
|
2023
|
+
out[j + 1] = " ";
|
|
2024
|
+
j += 2;
|
|
2025
|
+
break;
|
|
2026
|
+
}
|
|
2027
|
+
out[j] = " ";
|
|
2028
|
+
j++;
|
|
2029
|
+
}
|
|
2030
|
+
i = j;
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
out[i] = c;
|
|
2035
|
+
if (c === "'" || c === '"' || c === "`") inString = c;
|
|
2036
|
+
i++;
|
|
2037
|
+
}
|
|
2038
|
+
return out.join("");
|
|
2039
|
+
}
|
|
2040
|
+
var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
|
|
2041
|
+
function urlMatchesHost(urlString, host) {
|
|
2042
|
+
if (typeof urlString !== "string" || urlString.length === 0) return false;
|
|
2043
|
+
if (!URL_LIKE.test(urlString)) return false;
|
|
2044
|
+
const [wantedHost, wantedPort] = host.split(":");
|
|
2045
|
+
let parsed;
|
|
2046
|
+
try {
|
|
2047
|
+
const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
|
|
2048
|
+
parsed = new URL(candidate);
|
|
2049
|
+
} catch {
|
|
2050
|
+
return false;
|
|
2051
|
+
}
|
|
2052
|
+
if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
|
|
2053
|
+
if (wantedPort && parsed.port !== wantedPort) return false;
|
|
2054
|
+
return true;
|
|
2055
|
+
}
|
|
1956
2056
|
function cleanVersion(raw) {
|
|
1957
2057
|
if (!raw) return void 0;
|
|
1958
2058
|
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
@@ -2108,6 +2208,50 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
|
2108
2208
|
return author ?? void 0;
|
|
2109
2209
|
}
|
|
2110
2210
|
|
|
2211
|
+
// src/extract/errors.ts
|
|
2212
|
+
init_cjs_shims();
|
|
2213
|
+
var import_node_fs7 = require("fs");
|
|
2214
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2215
|
+
var sink = [];
|
|
2216
|
+
function recordExtractionError(producer, file, err) {
|
|
2217
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2218
|
+
sink.push({
|
|
2219
|
+
producer,
|
|
2220
|
+
file,
|
|
2221
|
+
error: e.message,
|
|
2222
|
+
stack: e.stack,
|
|
2223
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2224
|
+
source: "extract"
|
|
2225
|
+
});
|
|
2226
|
+
console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
|
|
2227
|
+
}
|
|
2228
|
+
function drainExtractionErrors() {
|
|
2229
|
+
return sink.splice(0, sink.length);
|
|
2230
|
+
}
|
|
2231
|
+
async function writeExtractionErrors(errors, errorsPath) {
|
|
2232
|
+
if (errors.length === 0) return;
|
|
2233
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
|
|
2234
|
+
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2235
|
+
await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
|
|
2236
|
+
}
|
|
2237
|
+
var droppedSink = [];
|
|
2238
|
+
function noteExtractedDropped(edge) {
|
|
2239
|
+
droppedSink.push(edge);
|
|
2240
|
+
}
|
|
2241
|
+
function drainDroppedExtracted() {
|
|
2242
|
+
return droppedSink.splice(0, droppedSink.length);
|
|
2243
|
+
}
|
|
2244
|
+
function isRejectedLogEnabled() {
|
|
2245
|
+
const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
|
|
2246
|
+
return raw === "1" || raw === "true";
|
|
2247
|
+
}
|
|
2248
|
+
async function writeRejectedExtracted(drops, rejectedPath) {
|
|
2249
|
+
if (drops.length === 0) return;
|
|
2250
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
|
|
2251
|
+
const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
|
|
2252
|
+
await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2111
2255
|
// src/extract/services.ts
|
|
2112
2256
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
2113
2257
|
function parseScanDepth() {
|
|
@@ -2124,21 +2268,21 @@ function workspaceGlobs(pkg) {
|
|
|
2124
2268
|
return null;
|
|
2125
2269
|
}
|
|
2126
2270
|
async function loadGitignore(scanPath) {
|
|
2127
|
-
const gitignorePath =
|
|
2271
|
+
const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
|
|
2128
2272
|
if (!await exists(gitignorePath)) return null;
|
|
2129
|
-
const raw = await
|
|
2273
|
+
const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
|
|
2130
2274
|
return (0, import_ignore.default)().add(raw);
|
|
2131
2275
|
}
|
|
2132
2276
|
async function walkDirs(start, scanPath, options, visit) {
|
|
2133
2277
|
async function recurse(current, depth) {
|
|
2134
2278
|
if (depth > options.maxDepth) return;
|
|
2135
|
-
const entries = await
|
|
2279
|
+
const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2136
2280
|
for (const entry of entries) {
|
|
2137
2281
|
if (!entry.isDirectory()) continue;
|
|
2138
2282
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2139
|
-
const child =
|
|
2283
|
+
const child = import_node_path8.default.join(current, entry.name);
|
|
2140
2284
|
if (options.ig) {
|
|
2141
|
-
const rel =
|
|
2285
|
+
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2142
2286
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2143
2287
|
}
|
|
2144
2288
|
await visit(child);
|
|
@@ -2153,8 +2297,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2153
2297
|
for (const raw of globs) {
|
|
2154
2298
|
const pattern = raw.replace(/^\.\//, "");
|
|
2155
2299
|
if (!pattern.includes("*")) {
|
|
2156
|
-
const candidate =
|
|
2157
|
-
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);
|
|
2158
2302
|
continue;
|
|
2159
2303
|
}
|
|
2160
2304
|
const segments = pattern.split("/");
|
|
@@ -2163,13 +2307,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2163
2307
|
if (seg.includes("*")) break;
|
|
2164
2308
|
staticSegments.push(seg);
|
|
2165
2309
|
}
|
|
2166
|
-
const start =
|
|
2310
|
+
const start = import_node_path8.default.join(scanPath, ...staticSegments);
|
|
2167
2311
|
if (!await exists(start)) continue;
|
|
2168
2312
|
const hasDoubleStar = pattern.includes("**");
|
|
2169
2313
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2170
2314
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2171
|
-
const rel =
|
|
2172
|
-
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"))) {
|
|
2173
2317
|
found.add(dir);
|
|
2174
2318
|
}
|
|
2175
2319
|
});
|
|
@@ -2177,15 +2321,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2177
2321
|
return [...found];
|
|
2178
2322
|
}
|
|
2179
2323
|
async function discoverNodeService(scanPath, dir) {
|
|
2180
|
-
const pkgPath =
|
|
2324
|
+
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2181
2325
|
if (!await exists(pkgPath)) return null;
|
|
2182
2326
|
let pkg;
|
|
2183
2327
|
try {
|
|
2184
2328
|
pkg = await readJson(pkgPath);
|
|
2185
2329
|
} catch (err) {
|
|
2186
|
-
|
|
2187
|
-
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2188
|
-
);
|
|
2330
|
+
recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
|
|
2189
2331
|
return null;
|
|
2190
2332
|
}
|
|
2191
2333
|
if (!pkg.name) return null;
|
|
@@ -2196,7 +2338,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2196
2338
|
language: "javascript",
|
|
2197
2339
|
version: pkg.version,
|
|
2198
2340
|
dependencies: pkg.dependencies ?? {},
|
|
2199
|
-
repoPath:
|
|
2341
|
+
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2200
2342
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2201
2343
|
};
|
|
2202
2344
|
return { pkg, dir, node };
|
|
@@ -2212,19 +2354,21 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2212
2354
|
language: "python",
|
|
2213
2355
|
version: py.version,
|
|
2214
2356
|
dependencies: py.dependencies,
|
|
2215
|
-
repoPath:
|
|
2357
|
+
repoPath: import_node_path8.default.relative(scanPath, dir)
|
|
2216
2358
|
};
|
|
2217
2359
|
return { pkg, dir, node };
|
|
2218
2360
|
}
|
|
2219
2361
|
async function discoverServices(scanPath) {
|
|
2220
|
-
const rootPkgPath =
|
|
2362
|
+
const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
|
|
2221
2363
|
let rootPkg = null;
|
|
2222
2364
|
if (await exists(rootPkgPath)) {
|
|
2223
2365
|
try {
|
|
2224
2366
|
rootPkg = await readJson(rootPkgPath);
|
|
2225
2367
|
} catch (err) {
|
|
2226
|
-
|
|
2227
|
-
|
|
2368
|
+
recordExtractionError(
|
|
2369
|
+
"services workspaces",
|
|
2370
|
+
import_node_path8.default.relative(scanPath, rootPkgPath),
|
|
2371
|
+
err
|
|
2228
2372
|
);
|
|
2229
2373
|
}
|
|
2230
2374
|
}
|
|
@@ -2240,9 +2384,9 @@ async function discoverServices(scanPath) {
|
|
|
2240
2384
|
scanPath,
|
|
2241
2385
|
{ maxDepth: parseScanDepth(), ig },
|
|
2242
2386
|
async (dir) => {
|
|
2243
|
-
if (await exists(
|
|
2387
|
+
if (await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2244
2388
|
candidateDirs.push(dir);
|
|
2245
|
-
} 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"))) {
|
|
2246
2390
|
candidateDirs.push(dir);
|
|
2247
2391
|
}
|
|
2248
2392
|
}
|
|
@@ -2256,8 +2400,8 @@ async function discoverServices(scanPath) {
|
|
|
2256
2400
|
if (!service) continue;
|
|
2257
2401
|
const existingDir = seen.get(service.node.name);
|
|
2258
2402
|
if (existingDir !== void 0) {
|
|
2259
|
-
const a =
|
|
2260
|
-
const b =
|
|
2403
|
+
const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
|
|
2404
|
+
const b = import_node_path8.default.relative(scanPath, dir) || ".";
|
|
2261
2405
|
console.warn(
|
|
2262
2406
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2263
2407
|
);
|
|
@@ -2294,8 +2438,8 @@ function addServiceNodes(graph, services) {
|
|
|
2294
2438
|
|
|
2295
2439
|
// src/extract/aliases.ts
|
|
2296
2440
|
init_cjs_shims();
|
|
2297
|
-
var
|
|
2298
|
-
var
|
|
2441
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2442
|
+
var import_node_fs9 = require("fs");
|
|
2299
2443
|
var import_yaml2 = require("yaml");
|
|
2300
2444
|
var import_types6 = require("@neat.is/types");
|
|
2301
2445
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2322,14 +2466,14 @@ function indexServicesByName(services) {
|
|
|
2322
2466
|
const map = /* @__PURE__ */ new Map();
|
|
2323
2467
|
for (const s of services) {
|
|
2324
2468
|
map.set(s.node.name, s.node.id);
|
|
2325
|
-
map.set(
|
|
2469
|
+
map.set(import_node_path9.default.basename(s.dir), s.node.id);
|
|
2326
2470
|
}
|
|
2327
2471
|
return map;
|
|
2328
2472
|
}
|
|
2329
2473
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2330
2474
|
let composePath = null;
|
|
2331
2475
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2332
|
-
const abs =
|
|
2476
|
+
const abs = import_node_path9.default.join(scanPath, name);
|
|
2333
2477
|
if (await exists(abs)) {
|
|
2334
2478
|
composePath = abs;
|
|
2335
2479
|
break;
|
|
@@ -2340,8 +2484,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
2340
2484
|
try {
|
|
2341
2485
|
compose = await readYaml(composePath);
|
|
2342
2486
|
} catch (err) {
|
|
2343
|
-
|
|
2344
|
-
|
|
2487
|
+
recordExtractionError(
|
|
2488
|
+
"aliases compose",
|
|
2489
|
+
import_node_path9.default.relative(scanPath, composePath),
|
|
2490
|
+
err
|
|
2345
2491
|
);
|
|
2346
2492
|
return;
|
|
2347
2493
|
}
|
|
@@ -2383,15 +2529,13 @@ function parseDockerfileLabels(content) {
|
|
|
2383
2529
|
}
|
|
2384
2530
|
async function collectDockerfileAliases(graph, services) {
|
|
2385
2531
|
for (const service of services) {
|
|
2386
|
-
const dockerfilePath =
|
|
2532
|
+
const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
|
|
2387
2533
|
if (!await exists(dockerfilePath)) continue;
|
|
2388
2534
|
let content;
|
|
2389
2535
|
try {
|
|
2390
|
-
content = await
|
|
2536
|
+
content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
|
|
2391
2537
|
} catch (err) {
|
|
2392
|
-
|
|
2393
|
-
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2394
|
-
);
|
|
2538
|
+
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
2395
2539
|
continue;
|
|
2396
2540
|
}
|
|
2397
2541
|
const aliases = parseDockerfileLabels(content);
|
|
@@ -2401,13 +2545,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2401
2545
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2402
2546
|
if (depth > max) return [];
|
|
2403
2547
|
const out = [];
|
|
2404
|
-
const entries = await
|
|
2548
|
+
const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2405
2549
|
for (const entry of entries) {
|
|
2406
2550
|
if (entry.isDirectory()) {
|
|
2407
2551
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2408
|
-
out.push(...await walkYamlFiles(
|
|
2409
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2410
|
-
out.push(
|
|
2552
|
+
out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
|
|
2553
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
|
|
2554
|
+
out.push(import_node_path9.default.join(start, entry.name));
|
|
2411
2555
|
}
|
|
2412
2556
|
}
|
|
2413
2557
|
return out;
|
|
@@ -2434,7 +2578,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2434
2578
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2435
2579
|
const files = await walkYamlFiles(scanPath);
|
|
2436
2580
|
for (const file of files) {
|
|
2437
|
-
const content = await
|
|
2581
|
+
const content = await import_node_fs9.promises.readFile(file, "utf8");
|
|
2438
2582
|
let docs;
|
|
2439
2583
|
try {
|
|
2440
2584
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2459,14 +2603,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2459
2603
|
|
|
2460
2604
|
// src/extract/databases/index.ts
|
|
2461
2605
|
init_cjs_shims();
|
|
2462
|
-
var
|
|
2606
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2463
2607
|
var import_types7 = require("@neat.is/types");
|
|
2464
2608
|
|
|
2465
2609
|
// src/extract/databases/db-config-yaml.ts
|
|
2466
2610
|
init_cjs_shims();
|
|
2467
|
-
var
|
|
2611
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2468
2612
|
async function parse(serviceDir) {
|
|
2469
|
-
const yamlPath =
|
|
2613
|
+
const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
|
|
2470
2614
|
if (!await exists(yamlPath)) return [];
|
|
2471
2615
|
const raw = await readYaml(yamlPath);
|
|
2472
2616
|
return [
|
|
@@ -2484,13 +2628,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2484
2628
|
|
|
2485
2629
|
// src/extract/databases/dotenv.ts
|
|
2486
2630
|
init_cjs_shims();
|
|
2487
|
-
var
|
|
2488
|
-
var
|
|
2631
|
+
var import_node_fs11 = require("fs");
|
|
2632
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2489
2633
|
|
|
2490
2634
|
// src/extract/databases/shared.ts
|
|
2491
2635
|
init_cjs_shims();
|
|
2492
|
-
var
|
|
2493
|
-
var
|
|
2636
|
+
var import_node_fs10 = require("fs");
|
|
2637
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2494
2638
|
function schemeToEngine(scheme) {
|
|
2495
2639
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2496
2640
|
switch (s) {
|
|
@@ -2529,14 +2673,14 @@ function parseConnectionString(url) {
|
|
|
2529
2673
|
}
|
|
2530
2674
|
async function readIfExists(filePath) {
|
|
2531
2675
|
try {
|
|
2532
|
-
return await
|
|
2676
|
+
return await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2533
2677
|
} catch {
|
|
2534
2678
|
return null;
|
|
2535
2679
|
}
|
|
2536
2680
|
}
|
|
2537
2681
|
async function findFirst(serviceDir, candidates) {
|
|
2538
2682
|
for (const rel of candidates) {
|
|
2539
|
-
const abs =
|
|
2683
|
+
const abs = import_node_path11.default.join(serviceDir, rel);
|
|
2540
2684
|
const content = await readIfExists(abs);
|
|
2541
2685
|
if (content !== null) return abs;
|
|
2542
2686
|
}
|
|
@@ -2587,15 +2731,15 @@ function parseDotenvLine(line) {
|
|
|
2587
2731
|
return { key, value };
|
|
2588
2732
|
}
|
|
2589
2733
|
async function parse2(serviceDir) {
|
|
2590
|
-
const entries = await
|
|
2734
|
+
const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2591
2735
|
const configs = [];
|
|
2592
2736
|
const seen = /* @__PURE__ */ new Set();
|
|
2593
2737
|
for (const entry of entries) {
|
|
2594
2738
|
if (!entry.isFile()) continue;
|
|
2595
2739
|
const match = isConfigFile(entry.name);
|
|
2596
2740
|
if (!match.match || match.fileType !== "env") continue;
|
|
2597
|
-
const filePath =
|
|
2598
|
-
const content = await
|
|
2741
|
+
const filePath = import_node_path12.default.join(serviceDir, entry.name);
|
|
2742
|
+
const content = await import_node_fs11.promises.readFile(filePath, "utf8");
|
|
2599
2743
|
for (const line of content.split("\n")) {
|
|
2600
2744
|
const parsed = parseDotenvLine(line);
|
|
2601
2745
|
if (!parsed) continue;
|
|
@@ -2614,9 +2758,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2614
2758
|
|
|
2615
2759
|
// src/extract/databases/prisma.ts
|
|
2616
2760
|
init_cjs_shims();
|
|
2617
|
-
var
|
|
2761
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2618
2762
|
async function parse3(serviceDir) {
|
|
2619
|
-
const schemaPath =
|
|
2763
|
+
const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2620
2764
|
const content = await readIfExists(schemaPath);
|
|
2621
2765
|
if (!content) return [];
|
|
2622
2766
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2748,10 +2892,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2748
2892
|
|
|
2749
2893
|
// src/extract/databases/ormconfig.ts
|
|
2750
2894
|
init_cjs_shims();
|
|
2751
|
-
var
|
|
2895
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2752
2896
|
async function parse6(serviceDir) {
|
|
2753
2897
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2754
|
-
const abs =
|
|
2898
|
+
const abs = import_node_path14.default.join(serviceDir, candidate);
|
|
2755
2899
|
if (!await exists(abs)) continue;
|
|
2756
2900
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2757
2901
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2811,9 +2955,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2811
2955
|
|
|
2812
2956
|
// src/extract/databases/sequelize.ts
|
|
2813
2957
|
init_cjs_shims();
|
|
2814
|
-
var
|
|
2958
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2815
2959
|
async function parse8(serviceDir) {
|
|
2816
|
-
const configPath =
|
|
2960
|
+
const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
|
|
2817
2961
|
if (!await exists(configPath)) return [];
|
|
2818
2962
|
const raw = await readJson(configPath);
|
|
2819
2963
|
const out = [];
|
|
@@ -2840,7 +2984,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
2840
2984
|
|
|
2841
2985
|
// src/extract/databases/docker-compose.ts
|
|
2842
2986
|
init_cjs_shims();
|
|
2843
|
-
var
|
|
2987
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2844
2988
|
function portFromService(svc) {
|
|
2845
2989
|
for (const raw of svc.ports ?? []) {
|
|
2846
2990
|
const str = String(raw);
|
|
@@ -2867,7 +3011,7 @@ function databaseFromEnv(svc) {
|
|
|
2867
3011
|
}
|
|
2868
3012
|
async function parse9(serviceDir) {
|
|
2869
3013
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2870
|
-
const abs =
|
|
3014
|
+
const abs = import_node_path16.default.join(serviceDir, name);
|
|
2871
3015
|
if (!await exists(abs)) continue;
|
|
2872
3016
|
const raw = await readYaml(abs);
|
|
2873
3017
|
if (!raw?.services) return [];
|
|
@@ -3045,11 +3189,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3045
3189
|
target: dbNode.id,
|
|
3046
3190
|
type: import_types7.EdgeType.CONNECTS_TO,
|
|
3047
3191
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3192
|
+
confidence: (0, import_types7.confidenceForExtracted)("structural"),
|
|
3193
|
+
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
3194
|
+
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3195
|
+
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3196
|
+
evidence: {
|
|
3197
|
+
file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
|
|
3198
|
+
}
|
|
3053
3199
|
};
|
|
3054
3200
|
if (!graph.hasEdge(edge.id)) {
|
|
3055
3201
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3075,15 +3221,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3075
3221
|
|
|
3076
3222
|
// src/extract/configs.ts
|
|
3077
3223
|
init_cjs_shims();
|
|
3078
|
-
var
|
|
3079
|
-
var
|
|
3224
|
+
var import_node_fs12 = require("fs");
|
|
3225
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
3080
3226
|
var import_types8 = require("@neat.is/types");
|
|
3081
3227
|
async function walkConfigFiles(dir) {
|
|
3082
3228
|
const out = [];
|
|
3083
3229
|
async function walk(current) {
|
|
3084
|
-
const entries = await
|
|
3230
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
|
|
3085
3231
|
for (const entry of entries) {
|
|
3086
|
-
const full =
|
|
3232
|
+
const full = import_node_path18.default.join(current, entry.name);
|
|
3087
3233
|
if (entry.isDirectory()) {
|
|
3088
3234
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
3089
3235
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
@@ -3100,13 +3246,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3100
3246
|
for (const service of services) {
|
|
3101
3247
|
const configFiles = await walkConfigFiles(service.dir);
|
|
3102
3248
|
for (const file of configFiles) {
|
|
3103
|
-
const relPath =
|
|
3249
|
+
const relPath = import_node_path18.default.relative(scanPath, file);
|
|
3104
3250
|
const node = {
|
|
3105
3251
|
id: (0, import_types8.configId)(relPath),
|
|
3106
3252
|
type: import_types8.NodeType.ConfigNode,
|
|
3107
|
-
name:
|
|
3253
|
+
name: import_node_path18.default.basename(file),
|
|
3108
3254
|
path: relPath,
|
|
3109
|
-
fileType: isConfigFile(
|
|
3255
|
+
fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
|
|
3110
3256
|
};
|
|
3111
3257
|
if (!graph.hasNode(node.id)) {
|
|
3112
3258
|
graph.addNode(node.id, node);
|
|
@@ -3118,7 +3264,8 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3118
3264
|
target: node.id,
|
|
3119
3265
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
3120
3266
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
3121
|
-
|
|
3267
|
+
confidence: (0, import_types8.confidenceForExtracted)("structural"),
|
|
3268
|
+
evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
|
|
3122
3269
|
};
|
|
3123
3270
|
if (!graph.hasEdge(edge.id)) {
|
|
3124
3271
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3135,7 +3282,7 @@ var import_types14 = require("@neat.is/types");
|
|
|
3135
3282
|
|
|
3136
3283
|
// src/extract/calls/http.ts
|
|
3137
3284
|
init_cjs_shims();
|
|
3138
|
-
var
|
|
3285
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3139
3286
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3140
3287
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3141
3288
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -3143,17 +3290,17 @@ var import_types9 = require("@neat.is/types");
|
|
|
3143
3290
|
|
|
3144
3291
|
// src/extract/calls/shared.ts
|
|
3145
3292
|
init_cjs_shims();
|
|
3146
|
-
var
|
|
3147
|
-
var
|
|
3293
|
+
var import_node_fs13 = require("fs");
|
|
3294
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3148
3295
|
async function walkSourceFiles(dir) {
|
|
3149
3296
|
const out = [];
|
|
3150
3297
|
async function walk(current) {
|
|
3151
|
-
const entries = await
|
|
3298
|
+
const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3152
3299
|
for (const entry of entries) {
|
|
3153
|
-
const full =
|
|
3300
|
+
const full = import_node_path19.default.join(current, entry.name);
|
|
3154
3301
|
if (entry.isDirectory()) {
|
|
3155
3302
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
3156
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3303
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
|
|
3157
3304
|
out.push(full);
|
|
3158
3305
|
}
|
|
3159
3306
|
}
|
|
@@ -3166,7 +3313,7 @@ async function loadSourceFiles(dir) {
|
|
|
3166
3313
|
const out = [];
|
|
3167
3314
|
for (const p of paths) {
|
|
3168
3315
|
try {
|
|
3169
|
-
const content = await
|
|
3316
|
+
const content = await import_node_fs13.promises.readFile(p, "utf8");
|
|
3170
3317
|
out.push({ path: p, content });
|
|
3171
3318
|
} catch {
|
|
3172
3319
|
}
|
|
@@ -3185,8 +3332,27 @@ function snippet(text, line) {
|
|
|
3185
3332
|
|
|
3186
3333
|
// src/extract/calls/http.ts
|
|
3187
3334
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3335
|
+
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3336
|
+
function isInsideJsxExternalLink(node) {
|
|
3337
|
+
let cursor = node.parent;
|
|
3338
|
+
while (cursor) {
|
|
3339
|
+
if (cursor.type === "jsx_attribute") {
|
|
3340
|
+
let owner = cursor.parent;
|
|
3341
|
+
while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
|
|
3342
|
+
owner = owner.parent;
|
|
3343
|
+
}
|
|
3344
|
+
if (!owner) return false;
|
|
3345
|
+
const tagNode = owner.namedChild(0);
|
|
3346
|
+
const tagName = tagNode?.text ?? "";
|
|
3347
|
+
const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
|
|
3348
|
+
return JSX_EXTERNAL_LINK_TAGS.has(right);
|
|
3349
|
+
}
|
|
3350
|
+
cursor = cursor.parent;
|
|
3351
|
+
}
|
|
3352
|
+
return false;
|
|
3353
|
+
}
|
|
3188
3354
|
function collectStringLiterals(node, out) {
|
|
3189
|
-
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
|
|
3355
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
|
|
3190
3356
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3191
3357
|
const child = node.namedChild(i);
|
|
3192
3358
|
if (child) collectStringLiterals(child, out);
|
|
@@ -3198,8 +3364,9 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3198
3364
|
collectStringLiterals(tree.rootNode, literals);
|
|
3199
3365
|
const targets = /* @__PURE__ */ new Set();
|
|
3200
3366
|
for (const lit of literals) {
|
|
3367
|
+
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
3201
3368
|
for (const host of knownHosts) {
|
|
3202
|
-
if (lit.
|
|
3369
|
+
if (urlMatchesHost(lit.text, host)) {
|
|
3203
3370
|
targets.add(host);
|
|
3204
3371
|
}
|
|
3205
3372
|
}
|
|
@@ -3222,9 +3389,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3222
3389
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3223
3390
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3224
3391
|
for (const service of services) {
|
|
3225
|
-
knownHosts.add(
|
|
3392
|
+
knownHosts.add(import_node_path20.default.basename(service.dir));
|
|
3226
3393
|
knownHosts.add(service.pkg.name);
|
|
3227
|
-
hostToNodeId.set(
|
|
3394
|
+
hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
|
|
3228
3395
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3229
3396
|
}
|
|
3230
3397
|
let edgesAdded = 0;
|
|
@@ -3232,14 +3399,13 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3232
3399
|
const files = await loadSourceFiles(service.dir);
|
|
3233
3400
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3234
3401
|
for (const file of files) {
|
|
3235
|
-
|
|
3402
|
+
if (isTestPath(file.path)) continue;
|
|
3403
|
+
const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3236
3404
|
let targets;
|
|
3237
3405
|
try {
|
|
3238
3406
|
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3239
3407
|
} catch (err) {
|
|
3240
|
-
|
|
3241
|
-
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3242
|
-
);
|
|
3408
|
+
recordExtractionError("http call extraction", file.path, err);
|
|
3243
3409
|
continue;
|
|
3244
3410
|
}
|
|
3245
3411
|
for (const t of targets) {
|
|
@@ -3253,17 +3419,32 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3253
3419
|
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3254
3420
|
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3255
3421
|
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3422
|
+
const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
|
|
3423
|
+
const ev = {
|
|
3424
|
+
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3425
|
+
line,
|
|
3426
|
+
snippet: snippet(fileContent, line)
|
|
3427
|
+
};
|
|
3428
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
|
|
3429
|
+
if (!(0, import_types9.passesExtractedFloor)(confidence)) {
|
|
3430
|
+
noteExtractedDropped({
|
|
3431
|
+
source: service.node.id,
|
|
3432
|
+
target: targetId,
|
|
3433
|
+
type: import_types9.EdgeType.CALLS,
|
|
3434
|
+
confidence,
|
|
3435
|
+
confidenceKind: "hostname-shape-match",
|
|
3436
|
+
evidence: ev
|
|
3437
|
+
});
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3256
3440
|
const edge = {
|
|
3257
|
-
id:
|
|
3441
|
+
id: edgeId,
|
|
3258
3442
|
source: service.node.id,
|
|
3259
3443
|
target: targetId,
|
|
3260
3444
|
type: import_types9.EdgeType.CALLS,
|
|
3261
3445
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
line,
|
|
3265
|
-
snippet: snippet(fileContent, line)
|
|
3266
|
-
}
|
|
3446
|
+
confidence,
|
|
3447
|
+
evidence: ev
|
|
3267
3448
|
};
|
|
3268
3449
|
if (!graph.hasEdge(edge.id)) {
|
|
3269
3450
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3276,7 +3457,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3276
3457
|
|
|
3277
3458
|
// src/extract/calls/kafka.ts
|
|
3278
3459
|
init_cjs_shims();
|
|
3279
|
-
var
|
|
3460
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3280
3461
|
var import_types10 = require("@neat.is/types");
|
|
3281
3462
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3282
3463
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3302,8 +3483,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3302
3483
|
name: topic,
|
|
3303
3484
|
kind: "kafka-topic",
|
|
3304
3485
|
edgeType,
|
|
3486
|
+
// `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
|
|
3487
|
+
// framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
|
|
3488
|
+
// tier (ADR-066).
|
|
3489
|
+
confidenceKind: "verified-call-site",
|
|
3305
3490
|
evidence: {
|
|
3306
|
-
file:
|
|
3491
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3307
3492
|
line,
|
|
3308
3493
|
snippet: snippet(file.content, line)
|
|
3309
3494
|
}
|
|
@@ -3316,7 +3501,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3316
3501
|
|
|
3317
3502
|
// src/extract/calls/redis.ts
|
|
3318
3503
|
init_cjs_shims();
|
|
3319
|
-
var
|
|
3504
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3320
3505
|
var import_types11 = require("@neat.is/types");
|
|
3321
3506
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3322
3507
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3334,8 +3519,12 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3334
3519
|
name: host,
|
|
3335
3520
|
kind: "redis",
|
|
3336
3521
|
edgeType: "CALLS",
|
|
3522
|
+
// `redis://host` URL literal — the scheme is structural support, but no
|
|
3523
|
+
// call expression is verified to wire it through. URL-with-structural-
|
|
3524
|
+
// support tier (ADR-066).
|
|
3525
|
+
confidenceKind: "url-with-structural-support",
|
|
3337
3526
|
evidence: {
|
|
3338
|
-
file:
|
|
3527
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3339
3528
|
line,
|
|
3340
3529
|
snippet: snippet(file.content, line)
|
|
3341
3530
|
}
|
|
@@ -3346,7 +3535,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3346
3535
|
|
|
3347
3536
|
// src/extract/calls/aws.ts
|
|
3348
3537
|
init_cjs_shims();
|
|
3349
|
-
var
|
|
3538
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3350
3539
|
var import_types12 = require("@neat.is/types");
|
|
3351
3540
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3352
3541
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3375,8 +3564,12 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3375
3564
|
name,
|
|
3376
3565
|
kind,
|
|
3377
3566
|
edgeType: "CALLS",
|
|
3567
|
+
// SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
|
|
3568
|
+
// literal — framework-aware recognizer, verified-call-site tier
|
|
3569
|
+
// (ADR-066).
|
|
3570
|
+
confidenceKind: "verified-call-site",
|
|
3378
3571
|
evidence: {
|
|
3379
|
-
file:
|
|
3572
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3380
3573
|
line,
|
|
3381
3574
|
snippet: snippet(file.content, line)
|
|
3382
3575
|
}
|
|
@@ -3401,16 +3594,42 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3401
3594
|
|
|
3402
3595
|
// src/extract/calls/grpc.ts
|
|
3403
3596
|
init_cjs_shims();
|
|
3404
|
-
var
|
|
3597
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3405
3598
|
var import_types13 = require("@neat.is/types");
|
|
3406
3599
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3600
|
+
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3601
|
+
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3407
3602
|
function isLikelyAddress(value) {
|
|
3408
3603
|
if (!value) return false;
|
|
3409
3604
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3410
3605
|
}
|
|
3606
|
+
function normaliseForMatch(s) {
|
|
3607
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3608
|
+
}
|
|
3609
|
+
function readImports(content) {
|
|
3610
|
+
const awsSdkSuffixes = /* @__PURE__ */ new Map();
|
|
3611
|
+
AWS_SDK_IMPORT_RE.lastIndex = 0;
|
|
3612
|
+
let m;
|
|
3613
|
+
while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
|
|
3614
|
+
const raw = m[1];
|
|
3615
|
+
awsSdkSuffixes.set(normaliseForMatch(raw), raw);
|
|
3616
|
+
}
|
|
3617
|
+
return {
|
|
3618
|
+
awsSdkSuffixes,
|
|
3619
|
+
hasGrpcImport: GRPC_IMPORT_RE.test(content)
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
function classifyClient(symbol, ctx) {
|
|
3623
|
+
const key = normaliseForMatch(symbol);
|
|
3624
|
+
const awsRaw = ctx.awsSdkSuffixes.get(key);
|
|
3625
|
+
if (awsRaw) return { kind: `aws-${awsRaw}` };
|
|
3626
|
+
if (ctx.hasGrpcImport) return { kind: "grpc-service" };
|
|
3627
|
+
return null;
|
|
3628
|
+
}
|
|
3411
3629
|
function grpcEndpointsFromFile(file, serviceDir) {
|
|
3412
3630
|
const out = [];
|
|
3413
3631
|
const seen = /* @__PURE__ */ new Set();
|
|
3632
|
+
const ctx = readImports(file.content);
|
|
3414
3633
|
GRPC_CLIENT_RE.lastIndex = 0;
|
|
3415
3634
|
let m;
|
|
3416
3635
|
while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
|
|
@@ -3418,15 +3637,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3418
3637
|
const addr = m[2]?.trim();
|
|
3419
3638
|
const name = isLikelyAddress(addr) ? addr : symbol;
|
|
3420
3639
|
if (seen.has(name)) continue;
|
|
3640
|
+
const classified = classifyClient(symbol, ctx);
|
|
3641
|
+
if (!classified) continue;
|
|
3421
3642
|
seen.add(name);
|
|
3643
|
+
const { kind } = classified;
|
|
3422
3644
|
const line = lineOf(file.content, m[0]);
|
|
3423
3645
|
out.push({
|
|
3424
|
-
infraId: (0, import_types13.infraId)(
|
|
3646
|
+
infraId: (0, import_types13.infraId)(kind, name),
|
|
3425
3647
|
name,
|
|
3426
|
-
kind
|
|
3648
|
+
kind,
|
|
3427
3649
|
edgeType: "CALLS",
|
|
3650
|
+
// `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
|
|
3651
|
+
// context — import-aware classification per #238. Verified-call-site
|
|
3652
|
+
// tier (ADR-066).
|
|
3653
|
+
confidenceKind: "verified-call-site",
|
|
3428
3654
|
evidence: {
|
|
3429
|
-
file:
|
|
3655
|
+
file: import_node_path24.default.relative(serviceDir, file.path),
|
|
3430
3656
|
line,
|
|
3431
3657
|
snippet: snippet(file.content, line)
|
|
3432
3658
|
}
|
|
@@ -3446,6 +3672,9 @@ function edgeTypeFromEndpoint(ep) {
|
|
|
3446
3672
|
return import_types14.EdgeType.CALLS;
|
|
3447
3673
|
}
|
|
3448
3674
|
}
|
|
3675
|
+
function isAwsKind(kind) {
|
|
3676
|
+
return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
|
|
3677
|
+
}
|
|
3449
3678
|
async function addExternalEndpointEdges(graph, services) {
|
|
3450
3679
|
let nodesAdded = 0;
|
|
3451
3680
|
let edgesAdded = 0;
|
|
@@ -3453,10 +3682,13 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3453
3682
|
const files = await loadSourceFiles(service.dir);
|
|
3454
3683
|
const endpoints = [];
|
|
3455
3684
|
for (const file of files) {
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
endpoints.push(...
|
|
3685
|
+
if (isTestPath(file.path)) continue;
|
|
3686
|
+
const masked = maskCommentsInSource(file.content);
|
|
3687
|
+
const maskedFile = { path: file.path, content: masked };
|
|
3688
|
+
endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
|
|
3689
|
+
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
3690
|
+
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
3691
|
+
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
3460
3692
|
}
|
|
3461
3693
|
if (endpoints.length === 0) continue;
|
|
3462
3694
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -3466,7 +3698,10 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3466
3698
|
id: ep.infraId,
|
|
3467
3699
|
type: import_types14.NodeType.InfraNode,
|
|
3468
3700
|
name: ep.name,
|
|
3469
|
-
|
|
3701
|
+
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3702
|
+
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
3703
|
+
// bucket / table kinds from aws.ts.
|
|
3704
|
+
provider: isAwsKind(ep.kind) ? "aws" : "self",
|
|
3470
3705
|
kind: ep.kind
|
|
3471
3706
|
};
|
|
3472
3707
|
graph.addNode(node.id, node);
|
|
@@ -3476,6 +3711,18 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3476
3711
|
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
|
|
3477
3712
|
if (seenEdges.has(edgeId)) continue;
|
|
3478
3713
|
seenEdges.add(edgeId);
|
|
3714
|
+
const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
|
|
3715
|
+
if (!(0, import_types14.passesExtractedFloor)(confidence)) {
|
|
3716
|
+
noteExtractedDropped({
|
|
3717
|
+
source: service.node.id,
|
|
3718
|
+
target: ep.infraId,
|
|
3719
|
+
type: edgeType,
|
|
3720
|
+
confidence,
|
|
3721
|
+
confidenceKind: ep.confidenceKind,
|
|
3722
|
+
evidence: ep.evidence
|
|
3723
|
+
});
|
|
3724
|
+
continue;
|
|
3725
|
+
}
|
|
3479
3726
|
if (!graph.hasEdge(edgeId)) {
|
|
3480
3727
|
const edge = {
|
|
3481
3728
|
id: edgeId,
|
|
@@ -3483,6 +3730,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3483
3730
|
target: ep.infraId,
|
|
3484
3731
|
type: edgeType,
|
|
3485
3732
|
provenance: import_types14.Provenance.EXTRACTED,
|
|
3733
|
+
confidence,
|
|
3486
3734
|
evidence: ep.evidence
|
|
3487
3735
|
};
|
|
3488
3736
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3506,7 +3754,7 @@ init_cjs_shims();
|
|
|
3506
3754
|
|
|
3507
3755
|
// src/extract/infra/docker-compose.ts
|
|
3508
3756
|
init_cjs_shims();
|
|
3509
|
-
var
|
|
3757
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3510
3758
|
var import_types16 = require("@neat.is/types");
|
|
3511
3759
|
|
|
3512
3760
|
// src/extract/infra/shared.ts
|
|
@@ -3544,7 +3792,7 @@ function dependsOnList(value) {
|
|
|
3544
3792
|
}
|
|
3545
3793
|
function serviceNameToServiceNode(name, services) {
|
|
3546
3794
|
for (const s of services) {
|
|
3547
|
-
if (s.node.name === name ||
|
|
3795
|
+
if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
|
|
3548
3796
|
}
|
|
3549
3797
|
return null;
|
|
3550
3798
|
}
|
|
@@ -3553,7 +3801,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3553
3801
|
let edgesAdded = 0;
|
|
3554
3802
|
let composePath = null;
|
|
3555
3803
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3556
|
-
const abs =
|
|
3804
|
+
const abs = import_node_path25.default.join(scanPath, name);
|
|
3557
3805
|
if (await exists(abs)) {
|
|
3558
3806
|
composePath = abs;
|
|
3559
3807
|
break;
|
|
@@ -3564,13 +3812,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3564
3812
|
try {
|
|
3565
3813
|
compose = await readYaml(composePath);
|
|
3566
3814
|
} catch (err) {
|
|
3567
|
-
|
|
3568
|
-
|
|
3815
|
+
recordExtractionError(
|
|
3816
|
+
"infra docker-compose",
|
|
3817
|
+
import_node_path25.default.relative(scanPath, composePath),
|
|
3818
|
+
err
|
|
3569
3819
|
);
|
|
3570
3820
|
return { nodesAdded, edgesAdded };
|
|
3571
3821
|
}
|
|
3572
3822
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3573
|
-
const evidenceFile =
|
|
3823
|
+
const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
|
|
3574
3824
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3575
3825
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3576
3826
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3600,6 +3850,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3600
3850
|
target: targetId,
|
|
3601
3851
|
type: import_types16.EdgeType.DEPENDS_ON,
|
|
3602
3852
|
provenance: import_types16.Provenance.EXTRACTED,
|
|
3853
|
+
confidence: (0, import_types16.confidenceForExtracted)("structural"),
|
|
3603
3854
|
evidence: { file: evidenceFile }
|
|
3604
3855
|
};
|
|
3605
3856
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3611,8 +3862,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3611
3862
|
|
|
3612
3863
|
// src/extract/infra/dockerfile.ts
|
|
3613
3864
|
init_cjs_shims();
|
|
3614
|
-
var
|
|
3615
|
-
var
|
|
3865
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3866
|
+
var import_node_fs14 = require("fs");
|
|
3616
3867
|
var import_types17 = require("@neat.is/types");
|
|
3617
3868
|
function runtimeImage(content) {
|
|
3618
3869
|
const lines = content.split("\n");
|
|
@@ -3632,14 +3883,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3632
3883
|
let nodesAdded = 0;
|
|
3633
3884
|
let edgesAdded = 0;
|
|
3634
3885
|
for (const service of services) {
|
|
3635
|
-
const dockerfilePath =
|
|
3886
|
+
const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
|
|
3636
3887
|
if (!await exists(dockerfilePath)) continue;
|
|
3637
3888
|
let content;
|
|
3638
3889
|
try {
|
|
3639
|
-
content = await
|
|
3890
|
+
content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
|
|
3640
3891
|
} catch (err) {
|
|
3641
|
-
|
|
3642
|
-
|
|
3892
|
+
recordExtractionError(
|
|
3893
|
+
"infra dockerfile",
|
|
3894
|
+
import_node_path26.default.relative(scanPath, dockerfilePath),
|
|
3895
|
+
err
|
|
3643
3896
|
);
|
|
3644
3897
|
continue;
|
|
3645
3898
|
}
|
|
@@ -3658,8 +3911,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3658
3911
|
target: node.id,
|
|
3659
3912
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3660
3913
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3914
|
+
confidence: (0, import_types17.confidenceForExtracted)("structural"),
|
|
3661
3915
|
evidence: {
|
|
3662
|
-
file:
|
|
3916
|
+
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
3663
3917
|
}
|
|
3664
3918
|
};
|
|
3665
3919
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3671,19 +3925,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3671
3925
|
|
|
3672
3926
|
// src/extract/infra/terraform.ts
|
|
3673
3927
|
init_cjs_shims();
|
|
3674
|
-
var
|
|
3675
|
-
var
|
|
3928
|
+
var import_node_fs15 = require("fs");
|
|
3929
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3676
3930
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3677
3931
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3678
3932
|
if (depth > max) return [];
|
|
3679
3933
|
const out = [];
|
|
3680
|
-
const entries = await
|
|
3934
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3681
3935
|
for (const entry of entries) {
|
|
3682
3936
|
if (entry.isDirectory()) {
|
|
3683
3937
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3684
|
-
out.push(...await walkTfFiles(
|
|
3938
|
+
out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
|
|
3685
3939
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3686
|
-
out.push(
|
|
3940
|
+
out.push(import_node_path27.default.join(start, entry.name));
|
|
3687
3941
|
}
|
|
3688
3942
|
}
|
|
3689
3943
|
return out;
|
|
@@ -3692,7 +3946,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3692
3946
|
let nodesAdded = 0;
|
|
3693
3947
|
const files = await walkTfFiles(scanPath);
|
|
3694
3948
|
for (const file of files) {
|
|
3695
|
-
const content = await
|
|
3949
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3696
3950
|
RESOURCE_RE.lastIndex = 0;
|
|
3697
3951
|
let m;
|
|
3698
3952
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3710,8 +3964,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3710
3964
|
|
|
3711
3965
|
// src/extract/infra/k8s.ts
|
|
3712
3966
|
init_cjs_shims();
|
|
3713
|
-
var
|
|
3714
|
-
var
|
|
3967
|
+
var import_node_fs16 = require("fs");
|
|
3968
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3715
3969
|
var import_yaml3 = require("yaml");
|
|
3716
3970
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3717
3971
|
Service: "k8s-service",
|
|
@@ -3725,13 +3979,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3725
3979
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3726
3980
|
if (depth > max) return [];
|
|
3727
3981
|
const out = [];
|
|
3728
|
-
const entries = await
|
|
3982
|
+
const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3729
3983
|
for (const entry of entries) {
|
|
3730
3984
|
if (entry.isDirectory()) {
|
|
3731
3985
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3732
|
-
out.push(...await walkYamlFiles2(
|
|
3733
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3734
|
-
out.push(
|
|
3986
|
+
out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
|
|
3987
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
|
|
3988
|
+
out.push(import_node_path28.default.join(start, entry.name));
|
|
3735
3989
|
}
|
|
3736
3990
|
}
|
|
3737
3991
|
return out;
|
|
@@ -3740,7 +3994,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3740
3994
|
let nodesAdded = 0;
|
|
3741
3995
|
const files = await walkYamlFiles2(scanPath);
|
|
3742
3996
|
for (const file of files) {
|
|
3743
|
-
const content = await
|
|
3997
|
+
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
3744
3998
|
let docs;
|
|
3745
3999
|
try {
|
|
3746
4000
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3774,9 +4028,37 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3774
4028
|
};
|
|
3775
4029
|
}
|
|
3776
4030
|
|
|
4031
|
+
// src/extract/index.ts
|
|
4032
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4033
|
+
|
|
4034
|
+
// src/extract/retire.ts
|
|
4035
|
+
init_cjs_shims();
|
|
4036
|
+
var import_node_fs17 = require("fs");
|
|
4037
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
4038
|
+
var import_types18 = require("@neat.is/types");
|
|
4039
|
+
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
4040
|
+
const toDrop = [];
|
|
4041
|
+
const bases = [scanPath, ...serviceDirs];
|
|
4042
|
+
graph.forEachEdge((id, attrs) => {
|
|
4043
|
+
const edge = attrs;
|
|
4044
|
+
if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
|
|
4045
|
+
const evidenceFile = edge.evidence?.file;
|
|
4046
|
+
if (!evidenceFile) return;
|
|
4047
|
+
if (import_node_path29.default.isAbsolute(evidenceFile)) {
|
|
4048
|
+
if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
|
|
4052
|
+
if (!found) toDrop.push(id);
|
|
4053
|
+
});
|
|
4054
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
4055
|
+
return toDrop.length;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
3777
4058
|
// src/extract/index.ts
|
|
3778
4059
|
async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
3779
4060
|
await ensureCompatLoaded();
|
|
4061
|
+
drainExtractionErrors();
|
|
3780
4062
|
const services = await discoverServices(scanPath);
|
|
3781
4063
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
3782
4064
|
await addServiceAliases(graph, scanPath, services);
|
|
@@ -3784,12 +4066,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3784
4066
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
3785
4067
|
const phase4 = await addCallEdges(graph, services);
|
|
3786
4068
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
4069
|
+
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
4070
|
+
graph,
|
|
4071
|
+
scanPath,
|
|
4072
|
+
services.map((s) => s.dir)
|
|
4073
|
+
);
|
|
3787
4074
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3788
4075
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
4076
|
+
const errorEntries = drainExtractionErrors();
|
|
4077
|
+
if (opts.errorsPath && errorEntries.length > 0) {
|
|
4078
|
+
try {
|
|
4079
|
+
await writeExtractionErrors(errorEntries, opts.errorsPath);
|
|
4080
|
+
} catch (err) {
|
|
4081
|
+
console.warn(
|
|
4082
|
+
`[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
const droppedEntries = drainDroppedExtracted();
|
|
4087
|
+
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
4088
|
+
const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
4089
|
+
try {
|
|
4090
|
+
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
4091
|
+
} catch (err) {
|
|
4092
|
+
console.warn(
|
|
4093
|
+
`[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
3789
4097
|
const result = {
|
|
3790
4098
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3791
4099
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3792
|
-
frontiersPromoted
|
|
4100
|
+
frontiersPromoted,
|
|
4101
|
+
extractionErrors: errorEntries.length,
|
|
4102
|
+
errorEntries,
|
|
4103
|
+
ghostsRetired,
|
|
4104
|
+
extractedDropped: droppedEntries.length,
|
|
4105
|
+
droppedEntries
|
|
3793
4106
|
};
|
|
3794
4107
|
emitNeatEvent({
|
|
3795
4108
|
type: "extraction-complete",
|
|
@@ -3806,8 +4119,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3806
4119
|
|
|
3807
4120
|
// src/persist.ts
|
|
3808
4121
|
init_cjs_shims();
|
|
3809
|
-
var
|
|
3810
|
-
var
|
|
4122
|
+
var import_node_fs18 = require("fs");
|
|
4123
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
3811
4124
|
var SCHEMA_VERSION = 2;
|
|
3812
4125
|
function migrateV1ToV2(payload) {
|
|
3813
4126
|
const nodes = payload.graph.nodes;
|
|
@@ -3821,7 +4134,7 @@ function migrateV1ToV2(payload) {
|
|
|
3821
4134
|
return { ...payload, schemaVersion: 2 };
|
|
3822
4135
|
}
|
|
3823
4136
|
async function ensureDir(filePath) {
|
|
3824
|
-
await
|
|
4137
|
+
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
3825
4138
|
}
|
|
3826
4139
|
async function saveGraphToDisk(graph, outPath) {
|
|
3827
4140
|
await ensureDir(outPath);
|
|
@@ -3831,13 +4144,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3831
4144
|
graph: graph.export()
|
|
3832
4145
|
};
|
|
3833
4146
|
const tmp = `${outPath}.tmp`;
|
|
3834
|
-
await
|
|
3835
|
-
await
|
|
4147
|
+
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4148
|
+
await import_node_fs18.promises.rename(tmp, outPath);
|
|
3836
4149
|
}
|
|
3837
4150
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3838
4151
|
let raw;
|
|
3839
4152
|
try {
|
|
3840
|
-
raw = await
|
|
4153
|
+
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
3841
4154
|
} catch (err) {
|
|
3842
4155
|
if (err.code === "ENOENT") return;
|
|
3843
4156
|
throw err;
|
|
@@ -3892,11 +4205,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3892
4205
|
init_cjs_shims();
|
|
3893
4206
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3894
4207
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3895
|
-
var
|
|
4208
|
+
var import_types21 = require("@neat.is/types");
|
|
3896
4209
|
|
|
3897
4210
|
// src/divergences.ts
|
|
3898
4211
|
init_cjs_shims();
|
|
3899
|
-
var
|
|
4212
|
+
var import_types19 = require("@neat.is/types");
|
|
3900
4213
|
function bucketKey(source, target, type) {
|
|
3901
4214
|
return `${type}|${source}|${target}`;
|
|
3902
4215
|
}
|
|
@@ -3904,25 +4217,25 @@ function bucketEdges(graph) {
|
|
|
3904
4217
|
const buckets = /* @__PURE__ */ new Map();
|
|
3905
4218
|
graph.forEachEdge((id, attrs) => {
|
|
3906
4219
|
const e = attrs;
|
|
3907
|
-
const parsed = (0,
|
|
4220
|
+
const parsed = (0, import_types19.parseEdgeId)(id);
|
|
3908
4221
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
3909
4222
|
const key = bucketKey(e.source, e.target, e.type);
|
|
3910
4223
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3911
4224
|
switch (provenance) {
|
|
3912
|
-
case
|
|
4225
|
+
case import_types19.Provenance.EXTRACTED:
|
|
3913
4226
|
cur.extracted = e;
|
|
3914
4227
|
break;
|
|
3915
|
-
case
|
|
4228
|
+
case import_types19.Provenance.OBSERVED:
|
|
3916
4229
|
cur.observed = e;
|
|
3917
4230
|
break;
|
|
3918
|
-
case
|
|
4231
|
+
case import_types19.Provenance.INFERRED:
|
|
3919
4232
|
cur.inferred = e;
|
|
3920
4233
|
break;
|
|
3921
|
-
case
|
|
4234
|
+
case import_types19.Provenance.FRONTIER:
|
|
3922
4235
|
cur.frontier = e;
|
|
3923
4236
|
break;
|
|
3924
4237
|
default:
|
|
3925
|
-
if (e.provenance ===
|
|
4238
|
+
if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
|
|
3926
4239
|
}
|
|
3927
4240
|
buckets.set(key, cur);
|
|
3928
4241
|
});
|
|
@@ -3931,15 +4244,7 @@ function bucketEdges(graph) {
|
|
|
3931
4244
|
function nodeIsFrontier(graph, nodeId) {
|
|
3932
4245
|
if (!graph.hasNode(nodeId)) return false;
|
|
3933
4246
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
3934
|
-
return attrs.type ===
|
|
3935
|
-
}
|
|
3936
|
-
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3937
|
-
if (!graph.hasNode(sourceId)) return false;
|
|
3938
|
-
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3939
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
3940
|
-
if (e.provenance === import_types18.Provenance.OBSERVED) return true;
|
|
3941
|
-
}
|
|
3942
|
-
return false;
|
|
4247
|
+
return attrs.type === import_types19.NodeType.FrontierNode;
|
|
3943
4248
|
}
|
|
3944
4249
|
function clampConfidence(n) {
|
|
3945
4250
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -3954,32 +4259,34 @@ function reasonForMissingExtracted(source, target, type) {
|
|
|
3954
4259
|
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
3955
4260
|
var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
|
|
3956
4261
|
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
4262
|
+
function gradedConfidence(edge) {
|
|
4263
|
+
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
4264
|
+
return clampConfidence(confidenceForEdge(edge));
|
|
4265
|
+
}
|
|
3957
4266
|
function detectMissingDivergences(graph, bucket) {
|
|
3958
4267
|
const out = [];
|
|
3959
4268
|
if (bucket.extracted && !bucket.observed) {
|
|
3960
4269
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
3961
|
-
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
3962
4270
|
out.push({
|
|
3963
4271
|
type: "missing-observed",
|
|
3964
4272
|
source: bucket.source,
|
|
3965
4273
|
target: bucket.target,
|
|
3966
4274
|
edgeType: bucket.type,
|
|
3967
4275
|
extracted: bucket.extracted,
|
|
3968
|
-
confidence:
|
|
4276
|
+
confidence: gradedConfidence(bucket.extracted),
|
|
3969
4277
|
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
3970
4278
|
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
3971
4279
|
});
|
|
3972
4280
|
}
|
|
3973
4281
|
}
|
|
3974
4282
|
if (bucket.observed && !bucket.extracted) {
|
|
3975
|
-
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
3976
4283
|
out.push({
|
|
3977
4284
|
type: "missing-extracted",
|
|
3978
4285
|
source: bucket.source,
|
|
3979
4286
|
target: bucket.target,
|
|
3980
4287
|
edgeType: bucket.type,
|
|
3981
4288
|
observed: bucket.observed,
|
|
3982
|
-
confidence:
|
|
4289
|
+
confidence: gradedConfidence(bucket.observed),
|
|
3983
4290
|
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
3984
4291
|
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
3985
4292
|
});
|
|
@@ -3998,7 +4305,7 @@ function declaredHostFor(svc) {
|
|
|
3998
4305
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
3999
4306
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4000
4307
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4001
|
-
if (e.type ===
|
|
4308
|
+
if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
|
|
4002
4309
|
return true;
|
|
4003
4310
|
}
|
|
4004
4311
|
}
|
|
@@ -4011,10 +4318,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4011
4318
|
const out = [];
|
|
4012
4319
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4013
4320
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4014
|
-
if (edge.type !==
|
|
4015
|
-
if (edge.provenance !==
|
|
4321
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4322
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4016
4323
|
const target = graph.getNodeAttributes(edge.target);
|
|
4017
|
-
if (target.type !==
|
|
4324
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4018
4325
|
const observedHost = target.host?.trim();
|
|
4019
4326
|
if (!observedHost) continue;
|
|
4020
4327
|
if (observedHost === declaredHost) continue;
|
|
@@ -4036,10 +4343,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4036
4343
|
const deps = svc.dependencies ?? {};
|
|
4037
4344
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4038
4345
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4039
|
-
if (edge.type !==
|
|
4040
|
-
if (edge.provenance !==
|
|
4346
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4347
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
4041
4348
|
const target = graph.getNodeAttributes(edge.target);
|
|
4042
|
-
if (target.type !==
|
|
4349
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
4043
4350
|
for (const pair of compatPairs()) {
|
|
4044
4351
|
if (pair.engine !== target.engine) continue;
|
|
4045
4352
|
const declared = deps[pair.driver];
|
|
@@ -4100,7 +4407,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4100
4407
|
}
|
|
4101
4408
|
graph.forEachNode((nodeId, attrs) => {
|
|
4102
4409
|
const n = attrs;
|
|
4103
|
-
if (n.type !==
|
|
4410
|
+
if (n.type !== import_types19.NodeType.ServiceNode) return;
|
|
4104
4411
|
const svc = n;
|
|
4105
4412
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4106
4413
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4118,13 +4425,22 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4118
4425
|
const target = opts.node;
|
|
4119
4426
|
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4120
4427
|
}
|
|
4428
|
+
const TYPE_LEADERSHIP = {
|
|
4429
|
+
"missing-extracted": 0,
|
|
4430
|
+
"missing-observed": 1,
|
|
4431
|
+
"version-mismatch": 2,
|
|
4432
|
+
"host-mismatch": 3,
|
|
4433
|
+
"compat-violation": 4
|
|
4434
|
+
};
|
|
4121
4435
|
filtered.sort((a, b) => {
|
|
4122
4436
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4437
|
+
const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
|
|
4438
|
+
if (lead !== 0) return lead;
|
|
4123
4439
|
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4124
4440
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4125
4441
|
return a.target.localeCompare(b.target);
|
|
4126
4442
|
});
|
|
4127
|
-
return
|
|
4443
|
+
return import_types19.DivergenceResultSchema.parse({
|
|
4128
4444
|
divergences: filtered,
|
|
4129
4445
|
totalAffected: filtered.length,
|
|
4130
4446
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4133,7 +4449,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4133
4449
|
|
|
4134
4450
|
// src/diff.ts
|
|
4135
4451
|
init_cjs_shims();
|
|
4136
|
-
var
|
|
4452
|
+
var import_node_fs19 = require("fs");
|
|
4137
4453
|
async function loadSnapshotForDiff(target) {
|
|
4138
4454
|
if (/^https?:\/\//i.test(target)) {
|
|
4139
4455
|
const res = await fetch(target);
|
|
@@ -4142,7 +4458,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
4142
4458
|
}
|
|
4143
4459
|
return await res.json();
|
|
4144
4460
|
}
|
|
4145
|
-
const raw = await
|
|
4461
|
+
const raw = await import_node_fs19.promises.readFile(target, "utf8");
|
|
4146
4462
|
return JSON.parse(raw);
|
|
4147
4463
|
}
|
|
4148
4464
|
function indexEntries(entries) {
|
|
@@ -4210,23 +4526,23 @@ function canonicalJson(value) {
|
|
|
4210
4526
|
|
|
4211
4527
|
// src/projects.ts
|
|
4212
4528
|
init_cjs_shims();
|
|
4213
|
-
var
|
|
4529
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4214
4530
|
function pathsForProject(project, baseDir) {
|
|
4215
4531
|
if (project === DEFAULT_PROJECT) {
|
|
4216
4532
|
return {
|
|
4217
|
-
snapshotPath:
|
|
4218
|
-
errorsPath:
|
|
4219
|
-
staleEventsPath:
|
|
4220
|
-
embeddingsCachePath:
|
|
4221
|
-
policyViolationsPath:
|
|
4533
|
+
snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
|
|
4534
|
+
errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
|
|
4535
|
+
staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
|
|
4536
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
|
|
4537
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
|
|
4222
4538
|
};
|
|
4223
4539
|
}
|
|
4224
4540
|
return {
|
|
4225
|
-
snapshotPath:
|
|
4226
|
-
errorsPath:
|
|
4227
|
-
staleEventsPath:
|
|
4228
|
-
embeddingsCachePath:
|
|
4229
|
-
policyViolationsPath:
|
|
4541
|
+
snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
|
|
4542
|
+
errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4543
|
+
staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4544
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
|
|
4545
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4230
4546
|
};
|
|
4231
4547
|
}
|
|
4232
4548
|
var Projects = class {
|
|
@@ -4262,49 +4578,49 @@ var Projects = class {
|
|
|
4262
4578
|
|
|
4263
4579
|
// src/registry.ts
|
|
4264
4580
|
init_cjs_shims();
|
|
4265
|
-
var
|
|
4581
|
+
var import_node_fs20 = require("fs");
|
|
4266
4582
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4267
|
-
var
|
|
4268
|
-
var
|
|
4583
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4584
|
+
var import_types20 = require("@neat.is/types");
|
|
4269
4585
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4270
4586
|
var LOCK_RETRY_MS = 50;
|
|
4271
4587
|
function neatHome() {
|
|
4272
4588
|
const override = process.env.NEAT_HOME;
|
|
4273
|
-
if (override && override.length > 0) return
|
|
4274
|
-
return
|
|
4589
|
+
if (override && override.length > 0) return import_node_path33.default.resolve(override);
|
|
4590
|
+
return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4275
4591
|
}
|
|
4276
4592
|
function registryPath() {
|
|
4277
|
-
return
|
|
4593
|
+
return import_node_path33.default.join(neatHome(), "projects.json");
|
|
4278
4594
|
}
|
|
4279
4595
|
function registryLockPath() {
|
|
4280
|
-
return
|
|
4596
|
+
return import_node_path33.default.join(neatHome(), "projects.json.lock");
|
|
4281
4597
|
}
|
|
4282
4598
|
async function normalizeProjectPath(input) {
|
|
4283
|
-
const resolved =
|
|
4599
|
+
const resolved = import_node_path33.default.resolve(input);
|
|
4284
4600
|
try {
|
|
4285
|
-
return await
|
|
4601
|
+
return await import_node_fs20.promises.realpath(resolved);
|
|
4286
4602
|
} catch {
|
|
4287
4603
|
return resolved;
|
|
4288
4604
|
}
|
|
4289
4605
|
}
|
|
4290
4606
|
async function writeAtomically(target, contents) {
|
|
4291
|
-
await
|
|
4607
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
|
|
4292
4608
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4293
|
-
const fd = await
|
|
4609
|
+
const fd = await import_node_fs20.promises.open(tmp, "w");
|
|
4294
4610
|
try {
|
|
4295
4611
|
await fd.writeFile(contents, "utf8");
|
|
4296
4612
|
await fd.sync();
|
|
4297
4613
|
} finally {
|
|
4298
4614
|
await fd.close();
|
|
4299
4615
|
}
|
|
4300
|
-
await
|
|
4616
|
+
await import_node_fs20.promises.rename(tmp, target);
|
|
4301
4617
|
}
|
|
4302
4618
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4303
4619
|
const deadline = Date.now() + timeoutMs;
|
|
4304
|
-
await
|
|
4620
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
4305
4621
|
while (true) {
|
|
4306
4622
|
try {
|
|
4307
|
-
const fd = await
|
|
4623
|
+
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
4308
4624
|
await fd.close();
|
|
4309
4625
|
return;
|
|
4310
4626
|
} catch (err) {
|
|
@@ -4320,7 +4636,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
4320
4636
|
}
|
|
4321
4637
|
}
|
|
4322
4638
|
async function releaseLock(lockPath) {
|
|
4323
|
-
await
|
|
4639
|
+
await import_node_fs20.promises.unlink(lockPath).catch(() => {
|
|
4324
4640
|
});
|
|
4325
4641
|
}
|
|
4326
4642
|
async function withLock(fn) {
|
|
@@ -4336,7 +4652,7 @@ async function readRegistry() {
|
|
|
4336
4652
|
const file = registryPath();
|
|
4337
4653
|
let raw;
|
|
4338
4654
|
try {
|
|
4339
|
-
raw = await
|
|
4655
|
+
raw = await import_node_fs20.promises.readFile(file, "utf8");
|
|
4340
4656
|
} catch (err) {
|
|
4341
4657
|
if (err.code === "ENOENT") {
|
|
4342
4658
|
return { version: 1, projects: [] };
|
|
@@ -4344,10 +4660,10 @@ async function readRegistry() {
|
|
|
4344
4660
|
throw err;
|
|
4345
4661
|
}
|
|
4346
4662
|
const parsed = JSON.parse(raw);
|
|
4347
|
-
return
|
|
4663
|
+
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4348
4664
|
}
|
|
4349
4665
|
async function writeRegistry(reg) {
|
|
4350
|
-
const validated =
|
|
4666
|
+
const validated = import_types20.RegistryFileSchema.parse(reg);
|
|
4351
4667
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4352
4668
|
}
|
|
4353
4669
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4607,11 +4923,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4607
4923
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4608
4924
|
const parsed = [];
|
|
4609
4925
|
for (const c of candidates) {
|
|
4610
|
-
const r =
|
|
4926
|
+
const r = import_types21.DivergenceTypeSchema.safeParse(c);
|
|
4611
4927
|
if (!r.success) {
|
|
4612
4928
|
return reply.code(400).send({
|
|
4613
4929
|
error: `unknown divergence type "${c}"`,
|
|
4614
|
-
allowed:
|
|
4930
|
+
allowed: import_types21.DivergenceTypeSchema.options
|
|
4615
4931
|
});
|
|
4616
4932
|
}
|
|
4617
4933
|
parsed.push(r.data);
|
|
@@ -4795,7 +5111,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4795
5111
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4796
5112
|
let violations = await log.readAll();
|
|
4797
5113
|
if (req.query.severity) {
|
|
4798
|
-
const sev =
|
|
5114
|
+
const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4799
5115
|
if (!sev.success) {
|
|
4800
5116
|
return reply.code(400).send({
|
|
4801
5117
|
error: "invalid severity",
|
|
@@ -4812,7 +5128,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4812
5128
|
scope.post("/policies/check", async (req, reply) => {
|
|
4813
5129
|
const proj = resolveProject(registry, req, reply);
|
|
4814
5130
|
if (!proj) return;
|
|
4815
|
-
const parsed =
|
|
5131
|
+
const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4816
5132
|
if (!parsed.success) {
|
|
4817
5133
|
return reply.code(400).send({
|
|
4818
5134
|
error: "invalid /policies/check body",
|
|
@@ -4916,15 +5232,15 @@ init_otel_grpc();
|
|
|
4916
5232
|
|
|
4917
5233
|
// src/daemon.ts
|
|
4918
5234
|
init_cjs_shims();
|
|
4919
|
-
var
|
|
4920
|
-
var
|
|
5235
|
+
var import_node_fs21 = require("fs");
|
|
5236
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
4921
5237
|
init_otel();
|
|
4922
5238
|
function neatHomeFor(opts) {
|
|
4923
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
5239
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
|
|
4924
5240
|
const env = process.env.NEAT_HOME;
|
|
4925
|
-
if (env && env.length > 0) return
|
|
5241
|
+
if (env && env.length > 0) return import_node_path36.default.resolve(env);
|
|
4926
5242
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4927
|
-
return
|
|
5243
|
+
return import_node_path36.default.join(home, ".neat");
|
|
4928
5244
|
}
|
|
4929
5245
|
function routeSpanToProject(serviceName, projects) {
|
|
4930
5246
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -4937,9 +5253,9 @@ function routeSpanToProject(serviceName, projects) {
|
|
|
4937
5253
|
return DEFAULT_PROJECT;
|
|
4938
5254
|
}
|
|
4939
5255
|
async function bootstrapProject(entry) {
|
|
4940
|
-
const paths = pathsForProject(entry.name,
|
|
5256
|
+
const paths = pathsForProject(entry.name, import_node_path36.default.join(entry.path, "neat-out"));
|
|
4941
5257
|
try {
|
|
4942
|
-
const stat = await
|
|
5258
|
+
const stat = await import_node_fs21.promises.stat(entry.path);
|
|
4943
5259
|
if (!stat.isDirectory()) {
|
|
4944
5260
|
throw new Error(`registered path ${entry.path} is not a directory`);
|
|
4945
5261
|
}
|
|
@@ -5004,13 +5320,13 @@ async function startDaemon(opts = {}) {
|
|
|
5004
5320
|
const home = neatHomeFor(opts);
|
|
5005
5321
|
const regPath = registryPath();
|
|
5006
5322
|
try {
|
|
5007
|
-
await
|
|
5323
|
+
await import_node_fs21.promises.access(regPath);
|
|
5008
5324
|
} catch {
|
|
5009
5325
|
throw new Error(
|
|
5010
5326
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
5011
5327
|
);
|
|
5012
5328
|
}
|
|
5013
|
-
const pidPath =
|
|
5329
|
+
const pidPath = import_node_path36.default.join(home, "neatd.pid");
|
|
5014
5330
|
await writeAtomically(pidPath, `${process.pid}
|
|
5015
5331
|
`);
|
|
5016
5332
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -5079,7 +5395,7 @@ async function startDaemon(opts = {}) {
|
|
|
5079
5395
|
}
|
|
5080
5396
|
if (restApp) await restApp.close().catch(() => {
|
|
5081
5397
|
});
|
|
5082
|
-
await
|
|
5398
|
+
await import_node_fs21.promises.unlink(pidPath).catch(() => {
|
|
5083
5399
|
});
|
|
5084
5400
|
throw new Error(
|
|
5085
5401
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
@@ -5124,7 +5440,7 @@ async function startDaemon(opts = {}) {
|
|
|
5124
5440
|
});
|
|
5125
5441
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
5126
5442
|
});
|
|
5127
|
-
await
|
|
5443
|
+
await import_node_fs21.promises.unlink(pidPath).catch(() => {
|
|
5128
5444
|
});
|
|
5129
5445
|
throw new Error(
|
|
5130
5446
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
@@ -5164,7 +5480,7 @@ async function startDaemon(opts = {}) {
|
|
|
5164
5480
|
} catch {
|
|
5165
5481
|
}
|
|
5166
5482
|
}
|
|
5167
|
-
await
|
|
5483
|
+
await import_node_fs21.promises.unlink(pidPath).catch(() => {
|
|
5168
5484
|
});
|
|
5169
5485
|
};
|
|
5170
5486
|
return { slots, reload, stop, pidPath, restAddress, otlpAddress };
|