@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/neatd.cjs
CHANGED
|
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req2) {
|
|
|
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 }
|
|
@@ -354,12 +354,12 @@ async function buildOtelReceiver(opts) {
|
|
|
354
354
|
};
|
|
355
355
|
return decorated;
|
|
356
356
|
}
|
|
357
|
-
var
|
|
357
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
358
358
|
var init_otel = __esm({
|
|
359
359
|
"src/otel.ts"() {
|
|
360
360
|
"use strict";
|
|
361
361
|
init_cjs_shims();
|
|
362
|
-
|
|
362
|
+
import_node_path35 = __toESM(require("path"), 1);
|
|
363
363
|
import_node_url2 = require("url");
|
|
364
364
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
365
365
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -369,13 +369,13 @@ var init_otel = __esm({
|
|
|
369
369
|
|
|
370
370
|
// src/neatd.ts
|
|
371
371
|
init_cjs_shims();
|
|
372
|
-
var
|
|
373
|
-
var
|
|
372
|
+
var import_node_fs22 = require("fs");
|
|
373
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
374
374
|
|
|
375
375
|
// src/daemon.ts
|
|
376
376
|
init_cjs_shims();
|
|
377
|
-
var
|
|
378
|
-
var
|
|
377
|
+
var import_node_fs21 = require("fs");
|
|
378
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
379
379
|
|
|
380
380
|
// src/graph.ts
|
|
381
381
|
init_cjs_shims();
|
|
@@ -831,19 +831,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
831
831
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
832
832
|
let best = { path: [start], edges: [] };
|
|
833
833
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
834
|
-
function step(node,
|
|
835
|
-
if (
|
|
836
|
-
best = { path: [...
|
|
834
|
+
function step(node, path39, edges) {
|
|
835
|
+
if (path39.length > best.path.length) {
|
|
836
|
+
best = { path: [...path39], edges: [...edges] };
|
|
837
837
|
}
|
|
838
|
-
if (
|
|
838
|
+
if (path39.length - 1 >= maxDepth) return;
|
|
839
839
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
840
840
|
for (const [srcId, edge] of incoming) {
|
|
841
841
|
if (visited.has(srcId)) continue;
|
|
842
842
|
visited.add(srcId);
|
|
843
|
-
|
|
843
|
+
path39.push(srcId);
|
|
844
844
|
edges.push(edge);
|
|
845
|
-
step(srcId,
|
|
846
|
-
|
|
845
|
+
step(srcId, path39, edges);
|
|
846
|
+
path39.pop();
|
|
847
847
|
edges.pop();
|
|
848
848
|
visited.delete(srcId);
|
|
849
849
|
}
|
|
@@ -1510,35 +1510,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1510
1510
|
const existing = graph.getEdgeAttributes(id);
|
|
1511
1511
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1512
1512
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1513
|
+
const newSignal = {
|
|
1514
|
+
spanCount: newSpanCount,
|
|
1515
|
+
errorCount: newErrorCount,
|
|
1516
|
+
lastObservedAgeMs: 0
|
|
1517
|
+
};
|
|
1513
1518
|
const updated = {
|
|
1514
1519
|
...existing,
|
|
1515
1520
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1516
1521
|
lastObserved: ts,
|
|
1517
1522
|
callCount: newSpanCount,
|
|
1518
|
-
signal:
|
|
1519
|
-
|
|
1520
|
-
errorCount: newErrorCount,
|
|
1521
|
-
lastObservedAgeMs: 0
|
|
1522
|
-
},
|
|
1523
|
-
confidence: 1
|
|
1523
|
+
signal: newSignal,
|
|
1524
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(newSignal)
|
|
1524
1525
|
};
|
|
1525
1526
|
graph.replaceEdgeAttributes(id, updated);
|
|
1526
1527
|
return { edge: updated, created: false };
|
|
1527
1528
|
}
|
|
1529
|
+
const signal = {
|
|
1530
|
+
spanCount: 1,
|
|
1531
|
+
errorCount: isError ? 1 : 0,
|
|
1532
|
+
lastObservedAgeMs: 0
|
|
1533
|
+
};
|
|
1528
1534
|
const edge = {
|
|
1529
1535
|
id,
|
|
1530
1536
|
source,
|
|
1531
1537
|
target,
|
|
1532
1538
|
type,
|
|
1533
1539
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1534
|
-
confidence:
|
|
1540
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(signal),
|
|
1535
1541
|
lastObserved: ts,
|
|
1536
1542
|
callCount: 1,
|
|
1537
|
-
signal
|
|
1538
|
-
spanCount: 1,
|
|
1539
|
-
errorCount: isError ? 1 : 0,
|
|
1540
|
-
lastObservedAgeMs: 0
|
|
1541
|
-
}
|
|
1543
|
+
signal
|
|
1542
1544
|
};
|
|
1543
1545
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1544
1546
|
return { edge, created: true };
|
|
@@ -1785,8 +1787,8 @@ async function readErrorEvents(errorsPath) {
|
|
|
1785
1787
|
|
|
1786
1788
|
// src/extract/services.ts
|
|
1787
1789
|
init_cjs_shims();
|
|
1788
|
-
var
|
|
1789
|
-
var
|
|
1790
|
+
var import_node_fs8 = require("fs");
|
|
1791
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1790
1792
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
1791
1793
|
var import_minimatch2 = require("minimatch");
|
|
1792
1794
|
var import_types5 = require("@neat.is/types");
|
|
@@ -1810,9 +1812,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1810
1812
|
function isConfigFile(name) {
|
|
1811
1813
|
const ext = import_node_path4.default.extname(name);
|
|
1812
1814
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
1813
|
-
if (name === ".env" || name.startsWith(".env."))
|
|
1815
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
1816
|
+
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
1817
|
+
return { match: true, fileType: "env" };
|
|
1818
|
+
}
|
|
1814
1819
|
return { match: false, fileType: "" };
|
|
1815
1820
|
}
|
|
1821
|
+
function isTestPath(filePath) {
|
|
1822
|
+
const normalised = filePath.replace(/\\/g, "/");
|
|
1823
|
+
const segments = normalised.split("/");
|
|
1824
|
+
for (const seg of segments) {
|
|
1825
|
+
if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
|
|
1826
|
+
return true;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const base = segments[segments.length - 1] ?? "";
|
|
1830
|
+
return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
|
|
1831
|
+
}
|
|
1832
|
+
function isEnvTemplateFile(name) {
|
|
1833
|
+
if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
|
|
1834
|
+
return true;
|
|
1835
|
+
}
|
|
1836
|
+
return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
|
|
1837
|
+
}
|
|
1838
|
+
function maskCommentsInSource(src) {
|
|
1839
|
+
const len = src.length;
|
|
1840
|
+
const out = new Array(len);
|
|
1841
|
+
let i = 0;
|
|
1842
|
+
let inString = 0;
|
|
1843
|
+
let escaped = false;
|
|
1844
|
+
while (i < len) {
|
|
1845
|
+
const c = src[i];
|
|
1846
|
+
if (inString !== 0) {
|
|
1847
|
+
out[i] = c;
|
|
1848
|
+
if (escaped) {
|
|
1849
|
+
escaped = false;
|
|
1850
|
+
} else if (c === "\\") {
|
|
1851
|
+
escaped = true;
|
|
1852
|
+
} else if (c === inString) {
|
|
1853
|
+
inString = 0;
|
|
1854
|
+
}
|
|
1855
|
+
i++;
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
if (c === "/" && i + 1 < len) {
|
|
1859
|
+
const next = src[i + 1];
|
|
1860
|
+
if (next === "/") {
|
|
1861
|
+
out[i] = " ";
|
|
1862
|
+
out[i + 1] = " ";
|
|
1863
|
+
let j = i + 2;
|
|
1864
|
+
while (j < len && src[j] !== "\n") {
|
|
1865
|
+
out[j] = " ";
|
|
1866
|
+
j++;
|
|
1867
|
+
}
|
|
1868
|
+
i = j;
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
if (next === "*") {
|
|
1872
|
+
out[i] = " ";
|
|
1873
|
+
out[i + 1] = " ";
|
|
1874
|
+
let j = i + 2;
|
|
1875
|
+
while (j < len) {
|
|
1876
|
+
if (src[j] === "\n") {
|
|
1877
|
+
out[j] = "\n";
|
|
1878
|
+
j++;
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
|
|
1882
|
+
out[j] = " ";
|
|
1883
|
+
out[j + 1] = " ";
|
|
1884
|
+
j += 2;
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
out[j] = " ";
|
|
1888
|
+
j++;
|
|
1889
|
+
}
|
|
1890
|
+
i = j;
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
out[i] = c;
|
|
1895
|
+
if (c === "'" || c === '"' || c === "`") inString = c;
|
|
1896
|
+
i++;
|
|
1897
|
+
}
|
|
1898
|
+
return out.join("");
|
|
1899
|
+
}
|
|
1900
|
+
var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
|
|
1901
|
+
function urlMatchesHost(urlString, host) {
|
|
1902
|
+
if (typeof urlString !== "string" || urlString.length === 0) return false;
|
|
1903
|
+
if (!URL_LIKE.test(urlString)) return false;
|
|
1904
|
+
const [wantedHost, wantedPort] = host.split(":");
|
|
1905
|
+
let parsed;
|
|
1906
|
+
try {
|
|
1907
|
+
const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
|
|
1908
|
+
parsed = new URL(candidate);
|
|
1909
|
+
} catch {
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
|
|
1913
|
+
if (wantedPort && parsed.port !== wantedPort) return false;
|
|
1914
|
+
return true;
|
|
1915
|
+
}
|
|
1816
1916
|
function cleanVersion(raw) {
|
|
1817
1917
|
if (!raw) return void 0;
|
|
1818
1918
|
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
@@ -1968,6 +2068,50 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
|
1968
2068
|
return author ?? void 0;
|
|
1969
2069
|
}
|
|
1970
2070
|
|
|
2071
|
+
// src/extract/errors.ts
|
|
2072
|
+
init_cjs_shims();
|
|
2073
|
+
var import_node_fs7 = require("fs");
|
|
2074
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2075
|
+
var sink = [];
|
|
2076
|
+
function recordExtractionError(producer, file, err) {
|
|
2077
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2078
|
+
sink.push({
|
|
2079
|
+
producer,
|
|
2080
|
+
file,
|
|
2081
|
+
error: e.message,
|
|
2082
|
+
stack: e.stack,
|
|
2083
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2084
|
+
source: "extract"
|
|
2085
|
+
});
|
|
2086
|
+
console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
|
|
2087
|
+
}
|
|
2088
|
+
function drainExtractionErrors() {
|
|
2089
|
+
return sink.splice(0, sink.length);
|
|
2090
|
+
}
|
|
2091
|
+
async function writeExtractionErrors(errors, errorsPath) {
|
|
2092
|
+
if (errors.length === 0) return;
|
|
2093
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
|
|
2094
|
+
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2095
|
+
await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
|
|
2096
|
+
}
|
|
2097
|
+
var droppedSink = [];
|
|
2098
|
+
function noteExtractedDropped(edge) {
|
|
2099
|
+
droppedSink.push(edge);
|
|
2100
|
+
}
|
|
2101
|
+
function drainDroppedExtracted() {
|
|
2102
|
+
return droppedSink.splice(0, droppedSink.length);
|
|
2103
|
+
}
|
|
2104
|
+
function isRejectedLogEnabled() {
|
|
2105
|
+
const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
|
|
2106
|
+
return raw === "1" || raw === "true";
|
|
2107
|
+
}
|
|
2108
|
+
async function writeRejectedExtracted(drops, rejectedPath) {
|
|
2109
|
+
if (drops.length === 0) return;
|
|
2110
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
|
|
2111
|
+
const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
|
|
2112
|
+
await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1971
2115
|
// src/extract/services.ts
|
|
1972
2116
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
1973
2117
|
function parseScanDepth() {
|
|
@@ -1984,21 +2128,21 @@ function workspaceGlobs(pkg) {
|
|
|
1984
2128
|
return null;
|
|
1985
2129
|
}
|
|
1986
2130
|
async function loadGitignore(scanPath) {
|
|
1987
|
-
const gitignorePath =
|
|
2131
|
+
const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
|
|
1988
2132
|
if (!await exists(gitignorePath)) return null;
|
|
1989
|
-
const raw = await
|
|
2133
|
+
const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
|
|
1990
2134
|
return (0, import_ignore.default)().add(raw);
|
|
1991
2135
|
}
|
|
1992
2136
|
async function walkDirs(start, scanPath, options, visit) {
|
|
1993
2137
|
async function recurse(current, depth) {
|
|
1994
2138
|
if (depth > options.maxDepth) return;
|
|
1995
|
-
const entries = await
|
|
2139
|
+
const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1996
2140
|
for (const entry2 of entries) {
|
|
1997
2141
|
if (!entry2.isDirectory()) continue;
|
|
1998
2142
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
1999
|
-
const child =
|
|
2143
|
+
const child = import_node_path8.default.join(current, entry2.name);
|
|
2000
2144
|
if (options.ig) {
|
|
2001
|
-
const rel =
|
|
2145
|
+
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2002
2146
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2003
2147
|
}
|
|
2004
2148
|
await visit(child);
|
|
@@ -2013,8 +2157,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2013
2157
|
for (const raw of globs) {
|
|
2014
2158
|
const pattern = raw.replace(/^\.\//, "");
|
|
2015
2159
|
if (!pattern.includes("*")) {
|
|
2016
|
-
const candidate =
|
|
2017
|
-
if (await exists(
|
|
2160
|
+
const candidate = import_node_path8.default.join(scanPath, pattern);
|
|
2161
|
+
if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2018
2162
|
continue;
|
|
2019
2163
|
}
|
|
2020
2164
|
const segments = pattern.split("/");
|
|
@@ -2023,13 +2167,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2023
2167
|
if (seg.includes("*")) break;
|
|
2024
2168
|
staticSegments.push(seg);
|
|
2025
2169
|
}
|
|
2026
|
-
const start =
|
|
2170
|
+
const start = import_node_path8.default.join(scanPath, ...staticSegments);
|
|
2027
2171
|
if (!await exists(start)) continue;
|
|
2028
2172
|
const hasDoubleStar = pattern.includes("**");
|
|
2029
2173
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2030
2174
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2031
|
-
const rel =
|
|
2032
|
-
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(
|
|
2175
|
+
const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
|
|
2176
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2033
2177
|
found.add(dir);
|
|
2034
2178
|
}
|
|
2035
2179
|
});
|
|
@@ -2037,15 +2181,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2037
2181
|
return [...found];
|
|
2038
2182
|
}
|
|
2039
2183
|
async function discoverNodeService(scanPath, dir) {
|
|
2040
|
-
const pkgPath =
|
|
2184
|
+
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2041
2185
|
if (!await exists(pkgPath)) return null;
|
|
2042
2186
|
let pkg;
|
|
2043
2187
|
try {
|
|
2044
2188
|
pkg = await readJson(pkgPath);
|
|
2045
2189
|
} catch (err) {
|
|
2046
|
-
|
|
2047
|
-
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2048
|
-
);
|
|
2190
|
+
recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
|
|
2049
2191
|
return null;
|
|
2050
2192
|
}
|
|
2051
2193
|
if (!pkg.name) return null;
|
|
@@ -2056,7 +2198,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2056
2198
|
language: "javascript",
|
|
2057
2199
|
version: pkg.version,
|
|
2058
2200
|
dependencies: pkg.dependencies ?? {},
|
|
2059
|
-
repoPath:
|
|
2201
|
+
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2060
2202
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2061
2203
|
};
|
|
2062
2204
|
return { pkg, dir, node };
|
|
@@ -2072,19 +2214,21 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2072
2214
|
language: "python",
|
|
2073
2215
|
version: py.version,
|
|
2074
2216
|
dependencies: py.dependencies,
|
|
2075
|
-
repoPath:
|
|
2217
|
+
repoPath: import_node_path8.default.relative(scanPath, dir)
|
|
2076
2218
|
};
|
|
2077
2219
|
return { pkg, dir, node };
|
|
2078
2220
|
}
|
|
2079
2221
|
async function discoverServices(scanPath) {
|
|
2080
|
-
const rootPkgPath =
|
|
2222
|
+
const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
|
|
2081
2223
|
let rootPkg = null;
|
|
2082
2224
|
if (await exists(rootPkgPath)) {
|
|
2083
2225
|
try {
|
|
2084
2226
|
rootPkg = await readJson(rootPkgPath);
|
|
2085
2227
|
} catch (err) {
|
|
2086
|
-
|
|
2087
|
-
|
|
2228
|
+
recordExtractionError(
|
|
2229
|
+
"services workspaces",
|
|
2230
|
+
import_node_path8.default.relative(scanPath, rootPkgPath),
|
|
2231
|
+
err
|
|
2088
2232
|
);
|
|
2089
2233
|
}
|
|
2090
2234
|
}
|
|
@@ -2100,9 +2244,9 @@ async function discoverServices(scanPath) {
|
|
|
2100
2244
|
scanPath,
|
|
2101
2245
|
{ maxDepth: parseScanDepth(), ig },
|
|
2102
2246
|
async (dir) => {
|
|
2103
|
-
if (await exists(
|
|
2247
|
+
if (await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2104
2248
|
candidateDirs.push(dir);
|
|
2105
|
-
} else if (await exists(
|
|
2249
|
+
} 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"))) {
|
|
2106
2250
|
candidateDirs.push(dir);
|
|
2107
2251
|
}
|
|
2108
2252
|
}
|
|
@@ -2116,8 +2260,8 @@ async function discoverServices(scanPath) {
|
|
|
2116
2260
|
if (!service) continue;
|
|
2117
2261
|
const existingDir = seen.get(service.node.name);
|
|
2118
2262
|
if (existingDir !== void 0) {
|
|
2119
|
-
const a =
|
|
2120
|
-
const b =
|
|
2263
|
+
const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
|
|
2264
|
+
const b = import_node_path8.default.relative(scanPath, dir) || ".";
|
|
2121
2265
|
console.warn(
|
|
2122
2266
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2123
2267
|
);
|
|
@@ -2154,8 +2298,8 @@ function addServiceNodes(graph, services) {
|
|
|
2154
2298
|
|
|
2155
2299
|
// src/extract/aliases.ts
|
|
2156
2300
|
init_cjs_shims();
|
|
2157
|
-
var
|
|
2158
|
-
var
|
|
2301
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2302
|
+
var import_node_fs9 = require("fs");
|
|
2159
2303
|
var import_yaml2 = require("yaml");
|
|
2160
2304
|
var import_types6 = require("@neat.is/types");
|
|
2161
2305
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2182,14 +2326,14 @@ function indexServicesByName(services) {
|
|
|
2182
2326
|
const map = /* @__PURE__ */ new Map();
|
|
2183
2327
|
for (const s of services) {
|
|
2184
2328
|
map.set(s.node.name, s.node.id);
|
|
2185
|
-
map.set(
|
|
2329
|
+
map.set(import_node_path9.default.basename(s.dir), s.node.id);
|
|
2186
2330
|
}
|
|
2187
2331
|
return map;
|
|
2188
2332
|
}
|
|
2189
2333
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2190
2334
|
let composePath = null;
|
|
2191
2335
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2192
|
-
const abs =
|
|
2336
|
+
const abs = import_node_path9.default.join(scanPath, name);
|
|
2193
2337
|
if (await exists(abs)) {
|
|
2194
2338
|
composePath = abs;
|
|
2195
2339
|
break;
|
|
@@ -2200,8 +2344,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
2200
2344
|
try {
|
|
2201
2345
|
compose = await readYaml(composePath);
|
|
2202
2346
|
} catch (err) {
|
|
2203
|
-
|
|
2204
|
-
|
|
2347
|
+
recordExtractionError(
|
|
2348
|
+
"aliases compose",
|
|
2349
|
+
import_node_path9.default.relative(scanPath, composePath),
|
|
2350
|
+
err
|
|
2205
2351
|
);
|
|
2206
2352
|
return;
|
|
2207
2353
|
}
|
|
@@ -2243,15 +2389,13 @@ function parseDockerfileLabels(content) {
|
|
|
2243
2389
|
}
|
|
2244
2390
|
async function collectDockerfileAliases(graph, services) {
|
|
2245
2391
|
for (const service of services) {
|
|
2246
|
-
const dockerfilePath =
|
|
2392
|
+
const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
|
|
2247
2393
|
if (!await exists(dockerfilePath)) continue;
|
|
2248
2394
|
let content;
|
|
2249
2395
|
try {
|
|
2250
|
-
content = await
|
|
2396
|
+
content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
|
|
2251
2397
|
} catch (err) {
|
|
2252
|
-
|
|
2253
|
-
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2254
|
-
);
|
|
2398
|
+
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
2255
2399
|
continue;
|
|
2256
2400
|
}
|
|
2257
2401
|
const aliases = parseDockerfileLabels(content);
|
|
@@ -2261,13 +2405,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2261
2405
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2262
2406
|
if (depth > max) return [];
|
|
2263
2407
|
const out = [];
|
|
2264
|
-
const entries = await
|
|
2408
|
+
const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2265
2409
|
for (const entry2 of entries) {
|
|
2266
2410
|
if (entry2.isDirectory()) {
|
|
2267
2411
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
2268
|
-
out.push(...await walkYamlFiles(
|
|
2269
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2270
|
-
out.push(
|
|
2412
|
+
out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
|
|
2413
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
|
|
2414
|
+
out.push(import_node_path9.default.join(start, entry2.name));
|
|
2271
2415
|
}
|
|
2272
2416
|
}
|
|
2273
2417
|
return out;
|
|
@@ -2294,7 +2438,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2294
2438
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2295
2439
|
const files = await walkYamlFiles(scanPath);
|
|
2296
2440
|
for (const file of files) {
|
|
2297
|
-
const content = await
|
|
2441
|
+
const content = await import_node_fs9.promises.readFile(file, "utf8");
|
|
2298
2442
|
let docs;
|
|
2299
2443
|
try {
|
|
2300
2444
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2319,14 +2463,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2319
2463
|
|
|
2320
2464
|
// src/extract/databases/index.ts
|
|
2321
2465
|
init_cjs_shims();
|
|
2322
|
-
var
|
|
2466
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2323
2467
|
var import_types7 = require("@neat.is/types");
|
|
2324
2468
|
|
|
2325
2469
|
// src/extract/databases/db-config-yaml.ts
|
|
2326
2470
|
init_cjs_shims();
|
|
2327
|
-
var
|
|
2471
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2328
2472
|
async function parse(serviceDir) {
|
|
2329
|
-
const yamlPath =
|
|
2473
|
+
const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
|
|
2330
2474
|
if (!await exists(yamlPath)) return [];
|
|
2331
2475
|
const raw = await readYaml(yamlPath);
|
|
2332
2476
|
return [
|
|
@@ -2344,13 +2488,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2344
2488
|
|
|
2345
2489
|
// src/extract/databases/dotenv.ts
|
|
2346
2490
|
init_cjs_shims();
|
|
2347
|
-
var
|
|
2348
|
-
var
|
|
2491
|
+
var import_node_fs11 = require("fs");
|
|
2492
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2349
2493
|
|
|
2350
2494
|
// src/extract/databases/shared.ts
|
|
2351
2495
|
init_cjs_shims();
|
|
2352
|
-
var
|
|
2353
|
-
var
|
|
2496
|
+
var import_node_fs10 = require("fs");
|
|
2497
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2354
2498
|
function schemeToEngine(scheme) {
|
|
2355
2499
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2356
2500
|
switch (s) {
|
|
@@ -2389,14 +2533,14 @@ function parseConnectionString(url) {
|
|
|
2389
2533
|
}
|
|
2390
2534
|
async function readIfExists(filePath) {
|
|
2391
2535
|
try {
|
|
2392
|
-
return await
|
|
2536
|
+
return await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2393
2537
|
} catch {
|
|
2394
2538
|
return null;
|
|
2395
2539
|
}
|
|
2396
2540
|
}
|
|
2397
2541
|
async function findFirst(serviceDir, candidates) {
|
|
2398
2542
|
for (const rel of candidates) {
|
|
2399
|
-
const abs =
|
|
2543
|
+
const abs = import_node_path11.default.join(serviceDir, rel);
|
|
2400
2544
|
const content = await readIfExists(abs);
|
|
2401
2545
|
if (content !== null) return abs;
|
|
2402
2546
|
}
|
|
@@ -2447,15 +2591,15 @@ function parseDotenvLine(line) {
|
|
|
2447
2591
|
return { key, value };
|
|
2448
2592
|
}
|
|
2449
2593
|
async function parse2(serviceDir) {
|
|
2450
|
-
const entries = await
|
|
2594
|
+
const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2451
2595
|
const configs = [];
|
|
2452
2596
|
const seen = /* @__PURE__ */ new Set();
|
|
2453
2597
|
for (const entry2 of entries) {
|
|
2454
2598
|
if (!entry2.isFile()) continue;
|
|
2455
2599
|
const match = isConfigFile(entry2.name);
|
|
2456
2600
|
if (!match.match || match.fileType !== "env") continue;
|
|
2457
|
-
const filePath =
|
|
2458
|
-
const content = await
|
|
2601
|
+
const filePath = import_node_path12.default.join(serviceDir, entry2.name);
|
|
2602
|
+
const content = await import_node_fs11.promises.readFile(filePath, "utf8");
|
|
2459
2603
|
for (const line of content.split("\n")) {
|
|
2460
2604
|
const parsed = parseDotenvLine(line);
|
|
2461
2605
|
if (!parsed) continue;
|
|
@@ -2474,9 +2618,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2474
2618
|
|
|
2475
2619
|
// src/extract/databases/prisma.ts
|
|
2476
2620
|
init_cjs_shims();
|
|
2477
|
-
var
|
|
2621
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2478
2622
|
async function parse3(serviceDir) {
|
|
2479
|
-
const schemaPath =
|
|
2623
|
+
const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2480
2624
|
const content = await readIfExists(schemaPath);
|
|
2481
2625
|
if (!content) return [];
|
|
2482
2626
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2608,10 +2752,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2608
2752
|
|
|
2609
2753
|
// src/extract/databases/ormconfig.ts
|
|
2610
2754
|
init_cjs_shims();
|
|
2611
|
-
var
|
|
2755
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2612
2756
|
async function parse6(serviceDir) {
|
|
2613
2757
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2614
|
-
const abs =
|
|
2758
|
+
const abs = import_node_path14.default.join(serviceDir, candidate);
|
|
2615
2759
|
if (!await exists(abs)) continue;
|
|
2616
2760
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2617
2761
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2671,9 +2815,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2671
2815
|
|
|
2672
2816
|
// src/extract/databases/sequelize.ts
|
|
2673
2817
|
init_cjs_shims();
|
|
2674
|
-
var
|
|
2818
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2675
2819
|
async function parse8(serviceDir) {
|
|
2676
|
-
const configPath =
|
|
2820
|
+
const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
|
|
2677
2821
|
if (!await exists(configPath)) return [];
|
|
2678
2822
|
const raw = await readJson(configPath);
|
|
2679
2823
|
const out = [];
|
|
@@ -2700,7 +2844,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
2700
2844
|
|
|
2701
2845
|
// src/extract/databases/docker-compose.ts
|
|
2702
2846
|
init_cjs_shims();
|
|
2703
|
-
var
|
|
2847
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2704
2848
|
function portFromService(svc) {
|
|
2705
2849
|
for (const raw of svc.ports ?? []) {
|
|
2706
2850
|
const str = String(raw);
|
|
@@ -2727,7 +2871,7 @@ function databaseFromEnv(svc) {
|
|
|
2727
2871
|
}
|
|
2728
2872
|
async function parse9(serviceDir) {
|
|
2729
2873
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2730
|
-
const abs =
|
|
2874
|
+
const abs = import_node_path16.default.join(serviceDir, name);
|
|
2731
2875
|
if (!await exists(abs)) continue;
|
|
2732
2876
|
const raw = await readYaml(abs);
|
|
2733
2877
|
if (!raw?.services) return [];
|
|
@@ -2905,11 +3049,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2905
3049
|
target: dbNode.id,
|
|
2906
3050
|
type: import_types7.EdgeType.CONNECTS_TO,
|
|
2907
3051
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
3052
|
+
confidence: (0, import_types7.confidenceForExtracted)("structural"),
|
|
3053
|
+
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
3054
|
+
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3055
|
+
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3056
|
+
evidence: {
|
|
3057
|
+
file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
|
|
3058
|
+
}
|
|
2913
3059
|
};
|
|
2914
3060
|
if (!graph.hasEdge(edge.id)) {
|
|
2915
3061
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2935,15 +3081,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2935
3081
|
|
|
2936
3082
|
// src/extract/configs.ts
|
|
2937
3083
|
init_cjs_shims();
|
|
2938
|
-
var
|
|
2939
|
-
var
|
|
3084
|
+
var import_node_fs12 = require("fs");
|
|
3085
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
2940
3086
|
var import_types8 = require("@neat.is/types");
|
|
2941
3087
|
async function walkConfigFiles(dir) {
|
|
2942
3088
|
const out = [];
|
|
2943
3089
|
async function walk(current) {
|
|
2944
|
-
const entries = await
|
|
3090
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
|
|
2945
3091
|
for (const entry2 of entries) {
|
|
2946
|
-
const full =
|
|
3092
|
+
const full = import_node_path18.default.join(current, entry2.name);
|
|
2947
3093
|
if (entry2.isDirectory()) {
|
|
2948
3094
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
2949
3095
|
} else if (entry2.isFile() && isConfigFile(entry2.name).match) {
|
|
@@ -2960,13 +3106,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2960
3106
|
for (const service of services) {
|
|
2961
3107
|
const configFiles = await walkConfigFiles(service.dir);
|
|
2962
3108
|
for (const file of configFiles) {
|
|
2963
|
-
const relPath =
|
|
3109
|
+
const relPath = import_node_path18.default.relative(scanPath, file);
|
|
2964
3110
|
const node = {
|
|
2965
3111
|
id: (0, import_types8.configId)(relPath),
|
|
2966
3112
|
type: import_types8.NodeType.ConfigNode,
|
|
2967
|
-
name:
|
|
3113
|
+
name: import_node_path18.default.basename(file),
|
|
2968
3114
|
path: relPath,
|
|
2969
|
-
fileType: isConfigFile(
|
|
3115
|
+
fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
|
|
2970
3116
|
};
|
|
2971
3117
|
if (!graph.hasNode(node.id)) {
|
|
2972
3118
|
graph.addNode(node.id, node);
|
|
@@ -2978,7 +3124,8 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2978
3124
|
target: node.id,
|
|
2979
3125
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
2980
3126
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
2981
|
-
|
|
3127
|
+
confidence: (0, import_types8.confidenceForExtracted)("structural"),
|
|
3128
|
+
evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
|
|
2982
3129
|
};
|
|
2983
3130
|
if (!graph.hasEdge(edge.id)) {
|
|
2984
3131
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2995,7 +3142,7 @@ var import_types14 = require("@neat.is/types");
|
|
|
2995
3142
|
|
|
2996
3143
|
// src/extract/calls/http.ts
|
|
2997
3144
|
init_cjs_shims();
|
|
2998
|
-
var
|
|
3145
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
2999
3146
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3000
3147
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3001
3148
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -3003,17 +3150,17 @@ var import_types9 = require("@neat.is/types");
|
|
|
3003
3150
|
|
|
3004
3151
|
// src/extract/calls/shared.ts
|
|
3005
3152
|
init_cjs_shims();
|
|
3006
|
-
var
|
|
3007
|
-
var
|
|
3153
|
+
var import_node_fs13 = require("fs");
|
|
3154
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3008
3155
|
async function walkSourceFiles(dir) {
|
|
3009
3156
|
const out = [];
|
|
3010
3157
|
async function walk(current) {
|
|
3011
|
-
const entries = await
|
|
3158
|
+
const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3012
3159
|
for (const entry2 of entries) {
|
|
3013
|
-
const full =
|
|
3160
|
+
const full = import_node_path19.default.join(current, entry2.name);
|
|
3014
3161
|
if (entry2.isDirectory()) {
|
|
3015
3162
|
if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
|
|
3016
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3163
|
+
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
|
|
3017
3164
|
out.push(full);
|
|
3018
3165
|
}
|
|
3019
3166
|
}
|
|
@@ -3026,7 +3173,7 @@ async function loadSourceFiles(dir) {
|
|
|
3026
3173
|
const out = [];
|
|
3027
3174
|
for (const p of paths) {
|
|
3028
3175
|
try {
|
|
3029
|
-
const content = await
|
|
3176
|
+
const content = await import_node_fs13.promises.readFile(p, "utf8");
|
|
3030
3177
|
out.push({ path: p, content });
|
|
3031
3178
|
} catch {
|
|
3032
3179
|
}
|
|
@@ -3045,8 +3192,27 @@ function snippet(text, line) {
|
|
|
3045
3192
|
|
|
3046
3193
|
// src/extract/calls/http.ts
|
|
3047
3194
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3195
|
+
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3196
|
+
function isInsideJsxExternalLink(node) {
|
|
3197
|
+
let cursor = node.parent;
|
|
3198
|
+
while (cursor) {
|
|
3199
|
+
if (cursor.type === "jsx_attribute") {
|
|
3200
|
+
let owner = cursor.parent;
|
|
3201
|
+
while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
|
|
3202
|
+
owner = owner.parent;
|
|
3203
|
+
}
|
|
3204
|
+
if (!owner) return false;
|
|
3205
|
+
const tagNode = owner.namedChild(0);
|
|
3206
|
+
const tagName = tagNode?.text ?? "";
|
|
3207
|
+
const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
|
|
3208
|
+
return JSX_EXTERNAL_LINK_TAGS.has(right);
|
|
3209
|
+
}
|
|
3210
|
+
cursor = cursor.parent;
|
|
3211
|
+
}
|
|
3212
|
+
return false;
|
|
3213
|
+
}
|
|
3048
3214
|
function collectStringLiterals(node, out) {
|
|
3049
|
-
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
|
|
3215
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
|
|
3050
3216
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3051
3217
|
const child = node.namedChild(i);
|
|
3052
3218
|
if (child) collectStringLiterals(child, out);
|
|
@@ -3058,8 +3224,9 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3058
3224
|
collectStringLiterals(tree.rootNode, literals);
|
|
3059
3225
|
const targets = /* @__PURE__ */ new Set();
|
|
3060
3226
|
for (const lit of literals) {
|
|
3227
|
+
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
3061
3228
|
for (const host of knownHosts) {
|
|
3062
|
-
if (lit.
|
|
3229
|
+
if (urlMatchesHost(lit.text, host)) {
|
|
3063
3230
|
targets.add(host);
|
|
3064
3231
|
}
|
|
3065
3232
|
}
|
|
@@ -3082,9 +3249,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3082
3249
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3083
3250
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3084
3251
|
for (const service of services) {
|
|
3085
|
-
knownHosts.add(
|
|
3252
|
+
knownHosts.add(import_node_path20.default.basename(service.dir));
|
|
3086
3253
|
knownHosts.add(service.pkg.name);
|
|
3087
|
-
hostToNodeId.set(
|
|
3254
|
+
hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
|
|
3088
3255
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3089
3256
|
}
|
|
3090
3257
|
let edgesAdded = 0;
|
|
@@ -3092,14 +3259,13 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3092
3259
|
const files = await loadSourceFiles(service.dir);
|
|
3093
3260
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3094
3261
|
for (const file of files) {
|
|
3095
|
-
|
|
3262
|
+
if (isTestPath(file.path)) continue;
|
|
3263
|
+
const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3096
3264
|
let targets;
|
|
3097
3265
|
try {
|
|
3098
3266
|
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3099
3267
|
} catch (err) {
|
|
3100
|
-
|
|
3101
|
-
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3102
|
-
);
|
|
3268
|
+
recordExtractionError("http call extraction", file.path, err);
|
|
3103
3269
|
continue;
|
|
3104
3270
|
}
|
|
3105
3271
|
for (const t of targets) {
|
|
@@ -3113,17 +3279,32 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3113
3279
|
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3114
3280
|
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3115
3281
|
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3282
|
+
const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
|
|
3283
|
+
const ev = {
|
|
3284
|
+
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3285
|
+
line,
|
|
3286
|
+
snippet: snippet(fileContent, line)
|
|
3287
|
+
};
|
|
3288
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
|
|
3289
|
+
if (!(0, import_types9.passesExtractedFloor)(confidence)) {
|
|
3290
|
+
noteExtractedDropped({
|
|
3291
|
+
source: service.node.id,
|
|
3292
|
+
target: targetId,
|
|
3293
|
+
type: import_types9.EdgeType.CALLS,
|
|
3294
|
+
confidence,
|
|
3295
|
+
confidenceKind: "hostname-shape-match",
|
|
3296
|
+
evidence: ev
|
|
3297
|
+
});
|
|
3298
|
+
continue;
|
|
3299
|
+
}
|
|
3116
3300
|
const edge = {
|
|
3117
|
-
id:
|
|
3301
|
+
id: edgeId,
|
|
3118
3302
|
source: service.node.id,
|
|
3119
3303
|
target: targetId,
|
|
3120
3304
|
type: import_types9.EdgeType.CALLS,
|
|
3121
3305
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
line,
|
|
3125
|
-
snippet: snippet(fileContent, line)
|
|
3126
|
-
}
|
|
3306
|
+
confidence,
|
|
3307
|
+
evidence: ev
|
|
3127
3308
|
};
|
|
3128
3309
|
if (!graph.hasEdge(edge.id)) {
|
|
3129
3310
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3136,7 +3317,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3136
3317
|
|
|
3137
3318
|
// src/extract/calls/kafka.ts
|
|
3138
3319
|
init_cjs_shims();
|
|
3139
|
-
var
|
|
3320
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3140
3321
|
var import_types10 = require("@neat.is/types");
|
|
3141
3322
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3142
3323
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3162,8 +3343,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3162
3343
|
name: topic,
|
|
3163
3344
|
kind: "kafka-topic",
|
|
3164
3345
|
edgeType,
|
|
3346
|
+
// `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
|
|
3347
|
+
// framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
|
|
3348
|
+
// tier (ADR-066).
|
|
3349
|
+
confidenceKind: "verified-call-site",
|
|
3165
3350
|
evidence: {
|
|
3166
|
-
file:
|
|
3351
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3167
3352
|
line,
|
|
3168
3353
|
snippet: snippet(file.content, line)
|
|
3169
3354
|
}
|
|
@@ -3176,7 +3361,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3176
3361
|
|
|
3177
3362
|
// src/extract/calls/redis.ts
|
|
3178
3363
|
init_cjs_shims();
|
|
3179
|
-
var
|
|
3364
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3180
3365
|
var import_types11 = require("@neat.is/types");
|
|
3181
3366
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3182
3367
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3194,8 +3379,12 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3194
3379
|
name: host,
|
|
3195
3380
|
kind: "redis",
|
|
3196
3381
|
edgeType: "CALLS",
|
|
3382
|
+
// `redis://host` URL literal — the scheme is structural support, but no
|
|
3383
|
+
// call expression is verified to wire it through. URL-with-structural-
|
|
3384
|
+
// support tier (ADR-066).
|
|
3385
|
+
confidenceKind: "url-with-structural-support",
|
|
3197
3386
|
evidence: {
|
|
3198
|
-
file:
|
|
3387
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3199
3388
|
line,
|
|
3200
3389
|
snippet: snippet(file.content, line)
|
|
3201
3390
|
}
|
|
@@ -3206,7 +3395,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3206
3395
|
|
|
3207
3396
|
// src/extract/calls/aws.ts
|
|
3208
3397
|
init_cjs_shims();
|
|
3209
|
-
var
|
|
3398
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3210
3399
|
var import_types12 = require("@neat.is/types");
|
|
3211
3400
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3212
3401
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3235,8 +3424,12 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3235
3424
|
name,
|
|
3236
3425
|
kind,
|
|
3237
3426
|
edgeType: "CALLS",
|
|
3427
|
+
// SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
|
|
3428
|
+
// literal — framework-aware recognizer, verified-call-site tier
|
|
3429
|
+
// (ADR-066).
|
|
3430
|
+
confidenceKind: "verified-call-site",
|
|
3238
3431
|
evidence: {
|
|
3239
|
-
file:
|
|
3432
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3240
3433
|
line,
|
|
3241
3434
|
snippet: snippet(file.content, line)
|
|
3242
3435
|
}
|
|
@@ -3261,16 +3454,42 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3261
3454
|
|
|
3262
3455
|
// src/extract/calls/grpc.ts
|
|
3263
3456
|
init_cjs_shims();
|
|
3264
|
-
var
|
|
3457
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3265
3458
|
var import_types13 = require("@neat.is/types");
|
|
3266
3459
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3460
|
+
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3461
|
+
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3267
3462
|
function isLikelyAddress(value) {
|
|
3268
3463
|
if (!value) return false;
|
|
3269
3464
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3270
3465
|
}
|
|
3466
|
+
function normaliseForMatch(s) {
|
|
3467
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3468
|
+
}
|
|
3469
|
+
function readImports(content) {
|
|
3470
|
+
const awsSdkSuffixes = /* @__PURE__ */ new Map();
|
|
3471
|
+
AWS_SDK_IMPORT_RE.lastIndex = 0;
|
|
3472
|
+
let m;
|
|
3473
|
+
while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
|
|
3474
|
+
const raw = m[1];
|
|
3475
|
+
awsSdkSuffixes.set(normaliseForMatch(raw), raw);
|
|
3476
|
+
}
|
|
3477
|
+
return {
|
|
3478
|
+
awsSdkSuffixes,
|
|
3479
|
+
hasGrpcImport: GRPC_IMPORT_RE.test(content)
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
function classifyClient(symbol, ctx) {
|
|
3483
|
+
const key = normaliseForMatch(symbol);
|
|
3484
|
+
const awsRaw = ctx.awsSdkSuffixes.get(key);
|
|
3485
|
+
if (awsRaw) return { kind: `aws-${awsRaw}` };
|
|
3486
|
+
if (ctx.hasGrpcImport) return { kind: "grpc-service" };
|
|
3487
|
+
return null;
|
|
3488
|
+
}
|
|
3271
3489
|
function grpcEndpointsFromFile(file, serviceDir) {
|
|
3272
3490
|
const out = [];
|
|
3273
3491
|
const seen = /* @__PURE__ */ new Set();
|
|
3492
|
+
const ctx = readImports(file.content);
|
|
3274
3493
|
GRPC_CLIENT_RE.lastIndex = 0;
|
|
3275
3494
|
let m;
|
|
3276
3495
|
while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
|
|
@@ -3278,15 +3497,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3278
3497
|
const addr = m[2]?.trim();
|
|
3279
3498
|
const name = isLikelyAddress(addr) ? addr : symbol;
|
|
3280
3499
|
if (seen.has(name)) continue;
|
|
3500
|
+
const classified = classifyClient(symbol, ctx);
|
|
3501
|
+
if (!classified) continue;
|
|
3281
3502
|
seen.add(name);
|
|
3503
|
+
const { kind } = classified;
|
|
3282
3504
|
const line = lineOf(file.content, m[0]);
|
|
3283
3505
|
out.push({
|
|
3284
|
-
infraId: (0, import_types13.infraId)(
|
|
3506
|
+
infraId: (0, import_types13.infraId)(kind, name),
|
|
3285
3507
|
name,
|
|
3286
|
-
kind
|
|
3508
|
+
kind,
|
|
3287
3509
|
edgeType: "CALLS",
|
|
3510
|
+
// `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
|
|
3511
|
+
// context — import-aware classification per #238. Verified-call-site
|
|
3512
|
+
// tier (ADR-066).
|
|
3513
|
+
confidenceKind: "verified-call-site",
|
|
3288
3514
|
evidence: {
|
|
3289
|
-
file:
|
|
3515
|
+
file: import_node_path24.default.relative(serviceDir, file.path),
|
|
3290
3516
|
line,
|
|
3291
3517
|
snippet: snippet(file.content, line)
|
|
3292
3518
|
}
|
|
@@ -3306,6 +3532,9 @@ function edgeTypeFromEndpoint(ep) {
|
|
|
3306
3532
|
return import_types14.EdgeType.CALLS;
|
|
3307
3533
|
}
|
|
3308
3534
|
}
|
|
3535
|
+
function isAwsKind(kind) {
|
|
3536
|
+
return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
|
|
3537
|
+
}
|
|
3309
3538
|
async function addExternalEndpointEdges(graph, services) {
|
|
3310
3539
|
let nodesAdded = 0;
|
|
3311
3540
|
let edgesAdded = 0;
|
|
@@ -3313,10 +3542,13 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3313
3542
|
const files = await loadSourceFiles(service.dir);
|
|
3314
3543
|
const endpoints = [];
|
|
3315
3544
|
for (const file of files) {
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
endpoints.push(...
|
|
3545
|
+
if (isTestPath(file.path)) continue;
|
|
3546
|
+
const masked = maskCommentsInSource(file.content);
|
|
3547
|
+
const maskedFile = { path: file.path, content: masked };
|
|
3548
|
+
endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
|
|
3549
|
+
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
3550
|
+
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
3551
|
+
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
3320
3552
|
}
|
|
3321
3553
|
if (endpoints.length === 0) continue;
|
|
3322
3554
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -3326,7 +3558,10 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3326
3558
|
id: ep.infraId,
|
|
3327
3559
|
type: import_types14.NodeType.InfraNode,
|
|
3328
3560
|
name: ep.name,
|
|
3329
|
-
|
|
3561
|
+
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3562
|
+
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
3563
|
+
// bucket / table kinds from aws.ts.
|
|
3564
|
+
provider: isAwsKind(ep.kind) ? "aws" : "self",
|
|
3330
3565
|
kind: ep.kind
|
|
3331
3566
|
};
|
|
3332
3567
|
graph.addNode(node.id, node);
|
|
@@ -3336,6 +3571,18 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3336
3571
|
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
|
|
3337
3572
|
if (seenEdges.has(edgeId)) continue;
|
|
3338
3573
|
seenEdges.add(edgeId);
|
|
3574
|
+
const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
|
|
3575
|
+
if (!(0, import_types14.passesExtractedFloor)(confidence)) {
|
|
3576
|
+
noteExtractedDropped({
|
|
3577
|
+
source: service.node.id,
|
|
3578
|
+
target: ep.infraId,
|
|
3579
|
+
type: edgeType,
|
|
3580
|
+
confidence,
|
|
3581
|
+
confidenceKind: ep.confidenceKind,
|
|
3582
|
+
evidence: ep.evidence
|
|
3583
|
+
});
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3339
3586
|
if (!graph.hasEdge(edgeId)) {
|
|
3340
3587
|
const edge = {
|
|
3341
3588
|
id: edgeId,
|
|
@@ -3343,6 +3590,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3343
3590
|
target: ep.infraId,
|
|
3344
3591
|
type: edgeType,
|
|
3345
3592
|
provenance: import_types14.Provenance.EXTRACTED,
|
|
3593
|
+
confidence,
|
|
3346
3594
|
evidence: ep.evidence
|
|
3347
3595
|
};
|
|
3348
3596
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3366,7 +3614,7 @@ init_cjs_shims();
|
|
|
3366
3614
|
|
|
3367
3615
|
// src/extract/infra/docker-compose.ts
|
|
3368
3616
|
init_cjs_shims();
|
|
3369
|
-
var
|
|
3617
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3370
3618
|
var import_types16 = require("@neat.is/types");
|
|
3371
3619
|
|
|
3372
3620
|
// src/extract/infra/shared.ts
|
|
@@ -3404,7 +3652,7 @@ function dependsOnList(value) {
|
|
|
3404
3652
|
}
|
|
3405
3653
|
function serviceNameToServiceNode(name, services) {
|
|
3406
3654
|
for (const s of services) {
|
|
3407
|
-
if (s.node.name === name ||
|
|
3655
|
+
if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
|
|
3408
3656
|
}
|
|
3409
3657
|
return null;
|
|
3410
3658
|
}
|
|
@@ -3413,7 +3661,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3413
3661
|
let edgesAdded = 0;
|
|
3414
3662
|
let composePath = null;
|
|
3415
3663
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3416
|
-
const abs =
|
|
3664
|
+
const abs = import_node_path25.default.join(scanPath, name);
|
|
3417
3665
|
if (await exists(abs)) {
|
|
3418
3666
|
composePath = abs;
|
|
3419
3667
|
break;
|
|
@@ -3424,13 +3672,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3424
3672
|
try {
|
|
3425
3673
|
compose = await readYaml(composePath);
|
|
3426
3674
|
} catch (err) {
|
|
3427
|
-
|
|
3428
|
-
|
|
3675
|
+
recordExtractionError(
|
|
3676
|
+
"infra docker-compose",
|
|
3677
|
+
import_node_path25.default.relative(scanPath, composePath),
|
|
3678
|
+
err
|
|
3429
3679
|
);
|
|
3430
3680
|
return { nodesAdded, edgesAdded };
|
|
3431
3681
|
}
|
|
3432
3682
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3433
|
-
const evidenceFile =
|
|
3683
|
+
const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
|
|
3434
3684
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3435
3685
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3436
3686
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3460,6 +3710,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3460
3710
|
target: targetId,
|
|
3461
3711
|
type: import_types16.EdgeType.DEPENDS_ON,
|
|
3462
3712
|
provenance: import_types16.Provenance.EXTRACTED,
|
|
3713
|
+
confidence: (0, import_types16.confidenceForExtracted)("structural"),
|
|
3463
3714
|
evidence: { file: evidenceFile }
|
|
3464
3715
|
};
|
|
3465
3716
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3471,8 +3722,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3471
3722
|
|
|
3472
3723
|
// src/extract/infra/dockerfile.ts
|
|
3473
3724
|
init_cjs_shims();
|
|
3474
|
-
var
|
|
3475
|
-
var
|
|
3725
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3726
|
+
var import_node_fs14 = require("fs");
|
|
3476
3727
|
var import_types17 = require("@neat.is/types");
|
|
3477
3728
|
function runtimeImage(content) {
|
|
3478
3729
|
const lines = content.split("\n");
|
|
@@ -3492,14 +3743,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3492
3743
|
let nodesAdded = 0;
|
|
3493
3744
|
let edgesAdded = 0;
|
|
3494
3745
|
for (const service of services) {
|
|
3495
|
-
const dockerfilePath =
|
|
3746
|
+
const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
|
|
3496
3747
|
if (!await exists(dockerfilePath)) continue;
|
|
3497
3748
|
let content;
|
|
3498
3749
|
try {
|
|
3499
|
-
content = await
|
|
3750
|
+
content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
|
|
3500
3751
|
} catch (err) {
|
|
3501
|
-
|
|
3502
|
-
|
|
3752
|
+
recordExtractionError(
|
|
3753
|
+
"infra dockerfile",
|
|
3754
|
+
import_node_path26.default.relative(scanPath, dockerfilePath),
|
|
3755
|
+
err
|
|
3503
3756
|
);
|
|
3504
3757
|
continue;
|
|
3505
3758
|
}
|
|
@@ -3518,8 +3771,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3518
3771
|
target: node.id,
|
|
3519
3772
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3520
3773
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3774
|
+
confidence: (0, import_types17.confidenceForExtracted)("structural"),
|
|
3521
3775
|
evidence: {
|
|
3522
|
-
file:
|
|
3776
|
+
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
3523
3777
|
}
|
|
3524
3778
|
};
|
|
3525
3779
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3531,19 +3785,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3531
3785
|
|
|
3532
3786
|
// src/extract/infra/terraform.ts
|
|
3533
3787
|
init_cjs_shims();
|
|
3534
|
-
var
|
|
3535
|
-
var
|
|
3788
|
+
var import_node_fs15 = require("fs");
|
|
3789
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3536
3790
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3537
3791
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3538
3792
|
if (depth > max) return [];
|
|
3539
3793
|
const out = [];
|
|
3540
|
-
const entries = await
|
|
3794
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3541
3795
|
for (const entry2 of entries) {
|
|
3542
3796
|
if (entry2.isDirectory()) {
|
|
3543
3797
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
3544
|
-
out.push(...await walkTfFiles(
|
|
3798
|
+
out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
|
|
3545
3799
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
3546
|
-
out.push(
|
|
3800
|
+
out.push(import_node_path27.default.join(start, entry2.name));
|
|
3547
3801
|
}
|
|
3548
3802
|
}
|
|
3549
3803
|
return out;
|
|
@@ -3552,7 +3806,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3552
3806
|
let nodesAdded = 0;
|
|
3553
3807
|
const files = await walkTfFiles(scanPath);
|
|
3554
3808
|
for (const file of files) {
|
|
3555
|
-
const content = await
|
|
3809
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3556
3810
|
RESOURCE_RE.lastIndex = 0;
|
|
3557
3811
|
let m;
|
|
3558
3812
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3570,8 +3824,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3570
3824
|
|
|
3571
3825
|
// src/extract/infra/k8s.ts
|
|
3572
3826
|
init_cjs_shims();
|
|
3573
|
-
var
|
|
3574
|
-
var
|
|
3827
|
+
var import_node_fs16 = require("fs");
|
|
3828
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3575
3829
|
var import_yaml3 = require("yaml");
|
|
3576
3830
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3577
3831
|
Service: "k8s-service",
|
|
@@ -3585,13 +3839,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3585
3839
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3586
3840
|
if (depth > max) return [];
|
|
3587
3841
|
const out = [];
|
|
3588
|
-
const entries = await
|
|
3842
|
+
const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3589
3843
|
for (const entry2 of entries) {
|
|
3590
3844
|
if (entry2.isDirectory()) {
|
|
3591
3845
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3592
|
-
out.push(...await walkYamlFiles2(
|
|
3593
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3594
|
-
out.push(
|
|
3846
|
+
out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
|
|
3847
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
|
|
3848
|
+
out.push(import_node_path28.default.join(start, entry2.name));
|
|
3595
3849
|
}
|
|
3596
3850
|
}
|
|
3597
3851
|
return out;
|
|
@@ -3600,7 +3854,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3600
3854
|
let nodesAdded = 0;
|
|
3601
3855
|
const files = await walkYamlFiles2(scanPath);
|
|
3602
3856
|
for (const file of files) {
|
|
3603
|
-
const content = await
|
|
3857
|
+
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
3604
3858
|
let docs;
|
|
3605
3859
|
try {
|
|
3606
3860
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3634,9 +3888,37 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3634
3888
|
};
|
|
3635
3889
|
}
|
|
3636
3890
|
|
|
3891
|
+
// src/extract/index.ts
|
|
3892
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
3893
|
+
|
|
3894
|
+
// src/extract/retire.ts
|
|
3895
|
+
init_cjs_shims();
|
|
3896
|
+
var import_node_fs17 = require("fs");
|
|
3897
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
3898
|
+
var import_types18 = require("@neat.is/types");
|
|
3899
|
+
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
3900
|
+
const toDrop = [];
|
|
3901
|
+
const bases = [scanPath, ...serviceDirs];
|
|
3902
|
+
graph.forEachEdge((id, attrs) => {
|
|
3903
|
+
const edge = attrs;
|
|
3904
|
+
if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
|
|
3905
|
+
const evidenceFile = edge.evidence?.file;
|
|
3906
|
+
if (!evidenceFile) return;
|
|
3907
|
+
if (import_node_path29.default.isAbsolute(evidenceFile)) {
|
|
3908
|
+
if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
|
|
3912
|
+
if (!found) toDrop.push(id);
|
|
3913
|
+
});
|
|
3914
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
3915
|
+
return toDrop.length;
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3637
3918
|
// src/extract/index.ts
|
|
3638
3919
|
async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
3639
3920
|
await ensureCompatLoaded();
|
|
3921
|
+
drainExtractionErrors();
|
|
3640
3922
|
const services = await discoverServices(scanPath);
|
|
3641
3923
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
3642
3924
|
await addServiceAliases(graph, scanPath, services);
|
|
@@ -3644,12 +3926,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3644
3926
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
3645
3927
|
const phase4 = await addCallEdges(graph, services);
|
|
3646
3928
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3929
|
+
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
3930
|
+
graph,
|
|
3931
|
+
scanPath,
|
|
3932
|
+
services.map((s) => s.dir)
|
|
3933
|
+
);
|
|
3647
3934
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3648
3935
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3936
|
+
const errorEntries = drainExtractionErrors();
|
|
3937
|
+
if (opts.errorsPath && errorEntries.length > 0) {
|
|
3938
|
+
try {
|
|
3939
|
+
await writeExtractionErrors(errorEntries, opts.errorsPath);
|
|
3940
|
+
} catch (err) {
|
|
3941
|
+
console.warn(
|
|
3942
|
+
`[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
const droppedEntries = drainDroppedExtracted();
|
|
3947
|
+
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
3948
|
+
const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
3949
|
+
try {
|
|
3950
|
+
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
3951
|
+
} catch (err) {
|
|
3952
|
+
console.warn(
|
|
3953
|
+
`[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3649
3957
|
const result = {
|
|
3650
3958
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3651
3959
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3652
|
-
frontiersPromoted
|
|
3960
|
+
frontiersPromoted,
|
|
3961
|
+
extractionErrors: errorEntries.length,
|
|
3962
|
+
errorEntries,
|
|
3963
|
+
ghostsRetired,
|
|
3964
|
+
extractedDropped: droppedEntries.length,
|
|
3965
|
+
droppedEntries
|
|
3653
3966
|
};
|
|
3654
3967
|
emitNeatEvent({
|
|
3655
3968
|
type: "extraction-complete",
|
|
@@ -3666,8 +3979,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3666
3979
|
|
|
3667
3980
|
// src/persist.ts
|
|
3668
3981
|
init_cjs_shims();
|
|
3669
|
-
var
|
|
3670
|
-
var
|
|
3982
|
+
var import_node_fs18 = require("fs");
|
|
3983
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
3671
3984
|
var SCHEMA_VERSION = 2;
|
|
3672
3985
|
function migrateV1ToV2(payload) {
|
|
3673
3986
|
const nodes = payload.graph.nodes;
|
|
@@ -3681,7 +3994,7 @@ function migrateV1ToV2(payload) {
|
|
|
3681
3994
|
return { ...payload, schemaVersion: 2 };
|
|
3682
3995
|
}
|
|
3683
3996
|
async function ensureDir(filePath) {
|
|
3684
|
-
await
|
|
3997
|
+
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
3685
3998
|
}
|
|
3686
3999
|
async function saveGraphToDisk(graph, outPath) {
|
|
3687
4000
|
await ensureDir(outPath);
|
|
@@ -3691,13 +4004,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3691
4004
|
graph: graph.export()
|
|
3692
4005
|
};
|
|
3693
4006
|
const tmp = `${outPath}.tmp`;
|
|
3694
|
-
await
|
|
3695
|
-
await
|
|
4007
|
+
await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4008
|
+
await import_node_fs18.promises.rename(tmp, outPath);
|
|
3696
4009
|
}
|
|
3697
4010
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3698
4011
|
let raw;
|
|
3699
4012
|
try {
|
|
3700
|
-
raw = await
|
|
4013
|
+
raw = await import_node_fs18.promises.readFile(outPath, "utf8");
|
|
3701
4014
|
} catch (err) {
|
|
3702
4015
|
if (err.code === "ENOENT") return;
|
|
3703
4016
|
throw err;
|
|
@@ -3750,23 +4063,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3750
4063
|
|
|
3751
4064
|
// src/projects.ts
|
|
3752
4065
|
init_cjs_shims();
|
|
3753
|
-
var
|
|
4066
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
3754
4067
|
function pathsForProject(project, baseDir) {
|
|
3755
4068
|
if (project === DEFAULT_PROJECT) {
|
|
3756
4069
|
return {
|
|
3757
|
-
snapshotPath:
|
|
3758
|
-
errorsPath:
|
|
3759
|
-
staleEventsPath:
|
|
3760
|
-
embeddingsCachePath:
|
|
3761
|
-
policyViolationsPath:
|
|
4070
|
+
snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
|
|
4071
|
+
errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
|
|
4072
|
+
staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
|
|
4073
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
|
|
4074
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
|
|
3762
4075
|
};
|
|
3763
4076
|
}
|
|
3764
4077
|
return {
|
|
3765
|
-
snapshotPath:
|
|
3766
|
-
errorsPath:
|
|
3767
|
-
staleEventsPath:
|
|
3768
|
-
embeddingsCachePath:
|
|
3769
|
-
policyViolationsPath:
|
|
4078
|
+
snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
|
|
4079
|
+
errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4080
|
+
staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4081
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
|
|
4082
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
3770
4083
|
};
|
|
3771
4084
|
}
|
|
3772
4085
|
var Projects = class {
|
|
@@ -3804,11 +4117,11 @@ var Projects = class {
|
|
|
3804
4117
|
init_cjs_shims();
|
|
3805
4118
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3806
4119
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3807
|
-
var
|
|
4120
|
+
var import_types21 = require("@neat.is/types");
|
|
3808
4121
|
|
|
3809
4122
|
// src/divergences.ts
|
|
3810
4123
|
init_cjs_shims();
|
|
3811
|
-
var
|
|
4124
|
+
var import_types19 = require("@neat.is/types");
|
|
3812
4125
|
function bucketKey(source, target, type) {
|
|
3813
4126
|
return `${type}|${source}|${target}`;
|
|
3814
4127
|
}
|
|
@@ -3816,25 +4129,25 @@ function bucketEdges(graph) {
|
|
|
3816
4129
|
const buckets = /* @__PURE__ */ new Map();
|
|
3817
4130
|
graph.forEachEdge((id, attrs) => {
|
|
3818
4131
|
const e = attrs;
|
|
3819
|
-
const parsed = (0,
|
|
4132
|
+
const parsed = (0, import_types19.parseEdgeId)(id);
|
|
3820
4133
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
3821
4134
|
const key = bucketKey(e.source, e.target, e.type);
|
|
3822
4135
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3823
4136
|
switch (provenance) {
|
|
3824
|
-
case
|
|
4137
|
+
case import_types19.Provenance.EXTRACTED:
|
|
3825
4138
|
cur.extracted = e;
|
|
3826
4139
|
break;
|
|
3827
|
-
case
|
|
4140
|
+
case import_types19.Provenance.OBSERVED:
|
|
3828
4141
|
cur.observed = e;
|
|
3829
4142
|
break;
|
|
3830
|
-
case
|
|
4143
|
+
case import_types19.Provenance.INFERRED:
|
|
3831
4144
|
cur.inferred = e;
|
|
3832
4145
|
break;
|
|
3833
|
-
case
|
|
4146
|
+
case import_types19.Provenance.FRONTIER:
|
|
3834
4147
|
cur.frontier = e;
|
|
3835
4148
|
break;
|
|
3836
4149
|
default:
|
|
3837
|
-
if (e.provenance ===
|
|
4150
|
+
if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
|
|
3838
4151
|
}
|
|
3839
4152
|
buckets.set(key, cur);
|
|
3840
4153
|
});
|
|
@@ -3843,15 +4156,7 @@ function bucketEdges(graph) {
|
|
|
3843
4156
|
function nodeIsFrontier(graph, nodeId) {
|
|
3844
4157
|
if (!graph.hasNode(nodeId)) return false;
|
|
3845
4158
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
3846
|
-
return attrs.type ===
|
|
3847
|
-
}
|
|
3848
|
-
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3849
|
-
if (!graph.hasNode(sourceId)) return false;
|
|
3850
|
-
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3851
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
3852
|
-
if (e.provenance === import_types18.Provenance.OBSERVED) return true;
|
|
3853
|
-
}
|
|
3854
|
-
return false;
|
|
4159
|
+
return attrs.type === import_types19.NodeType.FrontierNode;
|
|
3855
4160
|
}
|
|
3856
4161
|
function clampConfidence(n) {
|
|
3857
4162
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -3866,32 +4171,34 @@ function reasonForMissingExtracted(source, target, type) {
|
|
|
3866
4171
|
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
3867
4172
|
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.";
|
|
3868
4173
|
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
4174
|
+
function gradedConfidence(edge) {
|
|
4175
|
+
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
4176
|
+
return clampConfidence(confidenceForEdge(edge));
|
|
4177
|
+
}
|
|
3869
4178
|
function detectMissingDivergences(graph, bucket) {
|
|
3870
4179
|
const out = [];
|
|
3871
4180
|
if (bucket.extracted && !bucket.observed) {
|
|
3872
4181
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
3873
|
-
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
3874
4182
|
out.push({
|
|
3875
4183
|
type: "missing-observed",
|
|
3876
4184
|
source: bucket.source,
|
|
3877
4185
|
target: bucket.target,
|
|
3878
4186
|
edgeType: bucket.type,
|
|
3879
4187
|
extracted: bucket.extracted,
|
|
3880
|
-
confidence:
|
|
4188
|
+
confidence: gradedConfidence(bucket.extracted),
|
|
3881
4189
|
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
3882
4190
|
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
3883
4191
|
});
|
|
3884
4192
|
}
|
|
3885
4193
|
}
|
|
3886
4194
|
if (bucket.observed && !bucket.extracted) {
|
|
3887
|
-
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
3888
4195
|
out.push({
|
|
3889
4196
|
type: "missing-extracted",
|
|
3890
4197
|
source: bucket.source,
|
|
3891
4198
|
target: bucket.target,
|
|
3892
4199
|
edgeType: bucket.type,
|
|
3893
4200
|
observed: bucket.observed,
|
|
3894
|
-
confidence:
|
|
4201
|
+
confidence: gradedConfidence(bucket.observed),
|
|
3895
4202
|
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
3896
4203
|
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
3897
4204
|
});
|
|
@@ -3910,7 +4217,7 @@ function declaredHostFor(svc) {
|
|
|
3910
4217
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
3911
4218
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3912
4219
|
const e = graph.getEdgeAttributes(edgeId);
|
|
3913
|
-
if (e.type ===
|
|
4220
|
+
if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
|
|
3914
4221
|
return true;
|
|
3915
4222
|
}
|
|
3916
4223
|
}
|
|
@@ -3923,10 +4230,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
3923
4230
|
const out = [];
|
|
3924
4231
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3925
4232
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3926
|
-
if (edge.type !==
|
|
3927
|
-
if (edge.provenance !==
|
|
4233
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4234
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
3928
4235
|
const target = graph.getNodeAttributes(edge.target);
|
|
3929
|
-
if (target.type !==
|
|
4236
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
3930
4237
|
const observedHost = target.host?.trim();
|
|
3931
4238
|
if (!observedHost) continue;
|
|
3932
4239
|
if (observedHost === declaredHost) continue;
|
|
@@ -3948,10 +4255,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
3948
4255
|
const deps = svc.dependencies ?? {};
|
|
3949
4256
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3950
4257
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3951
|
-
if (edge.type !==
|
|
3952
|
-
if (edge.provenance !==
|
|
4258
|
+
if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
|
|
4259
|
+
if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
|
|
3953
4260
|
const target = graph.getNodeAttributes(edge.target);
|
|
3954
|
-
if (target.type !==
|
|
4261
|
+
if (target.type !== import_types19.NodeType.DatabaseNode) continue;
|
|
3955
4262
|
for (const pair of compatPairs()) {
|
|
3956
4263
|
if (pair.engine !== target.engine) continue;
|
|
3957
4264
|
const declared = deps[pair.driver];
|
|
@@ -4012,7 +4319,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4012
4319
|
}
|
|
4013
4320
|
graph.forEachNode((nodeId, attrs) => {
|
|
4014
4321
|
const n = attrs;
|
|
4015
|
-
if (n.type !==
|
|
4322
|
+
if (n.type !== import_types19.NodeType.ServiceNode) return;
|
|
4016
4323
|
const svc = n;
|
|
4017
4324
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4018
4325
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4030,13 +4337,22 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4030
4337
|
const target = opts.node;
|
|
4031
4338
|
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4032
4339
|
}
|
|
4340
|
+
const TYPE_LEADERSHIP = {
|
|
4341
|
+
"missing-extracted": 0,
|
|
4342
|
+
"missing-observed": 1,
|
|
4343
|
+
"version-mismatch": 2,
|
|
4344
|
+
"host-mismatch": 3,
|
|
4345
|
+
"compat-violation": 4
|
|
4346
|
+
};
|
|
4033
4347
|
filtered.sort((a, b) => {
|
|
4034
4348
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4349
|
+
const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
|
|
4350
|
+
if (lead !== 0) return lead;
|
|
4035
4351
|
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4036
4352
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4037
4353
|
return a.target.localeCompare(b.target);
|
|
4038
4354
|
});
|
|
4039
|
-
return
|
|
4355
|
+
return import_types19.DivergenceResultSchema.parse({
|
|
4040
4356
|
divergences: filtered,
|
|
4041
4357
|
totalAffected: filtered.length,
|
|
4042
4358
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4045,7 +4361,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4045
4361
|
|
|
4046
4362
|
// src/diff.ts
|
|
4047
4363
|
init_cjs_shims();
|
|
4048
|
-
var
|
|
4364
|
+
var import_node_fs19 = require("fs");
|
|
4049
4365
|
async function loadSnapshotForDiff(target) {
|
|
4050
4366
|
if (/^https?:\/\//i.test(target)) {
|
|
4051
4367
|
const res = await fetch(target);
|
|
@@ -4054,7 +4370,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
4054
4370
|
}
|
|
4055
4371
|
return await res.json();
|
|
4056
4372
|
}
|
|
4057
|
-
const raw = await
|
|
4373
|
+
const raw = await import_node_fs19.promises.readFile(target, "utf8");
|
|
4058
4374
|
return JSON.parse(raw);
|
|
4059
4375
|
}
|
|
4060
4376
|
function indexEntries(entries) {
|
|
@@ -4122,41 +4438,41 @@ function canonicalJson(value) {
|
|
|
4122
4438
|
|
|
4123
4439
|
// src/registry.ts
|
|
4124
4440
|
init_cjs_shims();
|
|
4125
|
-
var
|
|
4441
|
+
var import_node_fs20 = require("fs");
|
|
4126
4442
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4127
|
-
var
|
|
4128
|
-
var
|
|
4443
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4444
|
+
var import_types20 = require("@neat.is/types");
|
|
4129
4445
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4130
4446
|
var LOCK_RETRY_MS = 50;
|
|
4131
4447
|
function neatHome() {
|
|
4132
4448
|
const override = process.env.NEAT_HOME;
|
|
4133
|
-
if (override && override.length > 0) return
|
|
4134
|
-
return
|
|
4449
|
+
if (override && override.length > 0) return import_node_path33.default.resolve(override);
|
|
4450
|
+
return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4135
4451
|
}
|
|
4136
4452
|
function registryPath() {
|
|
4137
|
-
return
|
|
4453
|
+
return import_node_path33.default.join(neatHome(), "projects.json");
|
|
4138
4454
|
}
|
|
4139
4455
|
function registryLockPath() {
|
|
4140
|
-
return
|
|
4456
|
+
return import_node_path33.default.join(neatHome(), "projects.json.lock");
|
|
4141
4457
|
}
|
|
4142
4458
|
async function writeAtomically(target, contents) {
|
|
4143
|
-
await
|
|
4459
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
|
|
4144
4460
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4145
|
-
const fd = await
|
|
4461
|
+
const fd = await import_node_fs20.promises.open(tmp, "w");
|
|
4146
4462
|
try {
|
|
4147
4463
|
await fd.writeFile(contents, "utf8");
|
|
4148
4464
|
await fd.sync();
|
|
4149
4465
|
} finally {
|
|
4150
4466
|
await fd.close();
|
|
4151
4467
|
}
|
|
4152
|
-
await
|
|
4468
|
+
await import_node_fs20.promises.rename(tmp, target);
|
|
4153
4469
|
}
|
|
4154
4470
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4155
4471
|
const deadline = Date.now() + timeoutMs;
|
|
4156
|
-
await
|
|
4472
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
4157
4473
|
while (true) {
|
|
4158
4474
|
try {
|
|
4159
|
-
const fd = await
|
|
4475
|
+
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
4160
4476
|
await fd.close();
|
|
4161
4477
|
return;
|
|
4162
4478
|
} catch (err) {
|
|
@@ -4172,7 +4488,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
4172
4488
|
}
|
|
4173
4489
|
}
|
|
4174
4490
|
async function releaseLock(lockPath) {
|
|
4175
|
-
await
|
|
4491
|
+
await import_node_fs20.promises.unlink(lockPath).catch(() => {
|
|
4176
4492
|
});
|
|
4177
4493
|
}
|
|
4178
4494
|
async function withLock(fn) {
|
|
@@ -4188,7 +4504,7 @@ async function readRegistry() {
|
|
|
4188
4504
|
const file = registryPath();
|
|
4189
4505
|
let raw;
|
|
4190
4506
|
try {
|
|
4191
|
-
raw = await
|
|
4507
|
+
raw = await import_node_fs20.promises.readFile(file, "utf8");
|
|
4192
4508
|
} catch (err) {
|
|
4193
4509
|
if (err.code === "ENOENT") {
|
|
4194
4510
|
return { version: 1, projects: [] };
|
|
@@ -4196,10 +4512,10 @@ async function readRegistry() {
|
|
|
4196
4512
|
throw err;
|
|
4197
4513
|
}
|
|
4198
4514
|
const parsed = JSON.parse(raw);
|
|
4199
|
-
return
|
|
4515
|
+
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4200
4516
|
}
|
|
4201
4517
|
async function writeRegistry(reg) {
|
|
4202
|
-
const validated =
|
|
4518
|
+
const validated = import_types20.RegistryFileSchema.parse(reg);
|
|
4203
4519
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4204
4520
|
}
|
|
4205
4521
|
async function getProject(name) {
|
|
@@ -4409,11 +4725,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4409
4725
|
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4410
4726
|
const parsed = [];
|
|
4411
4727
|
for (const c of candidates) {
|
|
4412
|
-
const r =
|
|
4728
|
+
const r = import_types21.DivergenceTypeSchema.safeParse(c);
|
|
4413
4729
|
if (!r.success) {
|
|
4414
4730
|
return reply.code(400).send({
|
|
4415
4731
|
error: `unknown divergence type "${c}"`,
|
|
4416
|
-
allowed:
|
|
4732
|
+
allowed: import_types21.DivergenceTypeSchema.options
|
|
4417
4733
|
});
|
|
4418
4734
|
}
|
|
4419
4735
|
parsed.push(r.data);
|
|
@@ -4597,7 +4913,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4597
4913
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4598
4914
|
let violations = await log.readAll();
|
|
4599
4915
|
if (req2.query.severity) {
|
|
4600
|
-
const sev =
|
|
4916
|
+
const sev = import_types21.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
4601
4917
|
if (!sev.success) {
|
|
4602
4918
|
return reply.code(400).send({
|
|
4603
4919
|
error: "invalid severity",
|
|
@@ -4614,7 +4930,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4614
4930
|
scope.post("/policies/check", async (req2, reply) => {
|
|
4615
4931
|
const proj = resolveProject(registry, req2, reply);
|
|
4616
4932
|
if (!proj) return;
|
|
4617
|
-
const parsed =
|
|
4933
|
+
const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
4618
4934
|
if (!parsed.success) {
|
|
4619
4935
|
return reply.code(400).send({
|
|
4620
4936
|
error: "invalid /policies/check body",
|
|
@@ -4715,11 +5031,11 @@ async function buildApi(opts) {
|
|
|
4715
5031
|
// src/daemon.ts
|
|
4716
5032
|
init_otel();
|
|
4717
5033
|
function neatHomeFor(opts) {
|
|
4718
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
5034
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
|
|
4719
5035
|
const env = process.env.NEAT_HOME;
|
|
4720
|
-
if (env && env.length > 0) return
|
|
5036
|
+
if (env && env.length > 0) return import_node_path36.default.resolve(env);
|
|
4721
5037
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4722
|
-
return
|
|
5038
|
+
return import_node_path36.default.join(home, ".neat");
|
|
4723
5039
|
}
|
|
4724
5040
|
function routeSpanToProject(serviceName, projects) {
|
|
4725
5041
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -4732,9 +5048,9 @@ function routeSpanToProject(serviceName, projects) {
|
|
|
4732
5048
|
return DEFAULT_PROJECT;
|
|
4733
5049
|
}
|
|
4734
5050
|
async function bootstrapProject(entry2) {
|
|
4735
|
-
const paths = pathsForProject(entry2.name,
|
|
5051
|
+
const paths = pathsForProject(entry2.name, import_node_path36.default.join(entry2.path, "neat-out"));
|
|
4736
5052
|
try {
|
|
4737
|
-
const stat = await
|
|
5053
|
+
const stat = await import_node_fs21.promises.stat(entry2.path);
|
|
4738
5054
|
if (!stat.isDirectory()) {
|
|
4739
5055
|
throw new Error(`registered path ${entry2.path} is not a directory`);
|
|
4740
5056
|
}
|
|
@@ -4799,13 +5115,13 @@ async function startDaemon(opts = {}) {
|
|
|
4799
5115
|
const home = neatHomeFor(opts);
|
|
4800
5116
|
const regPath = registryPath();
|
|
4801
5117
|
try {
|
|
4802
|
-
await
|
|
5118
|
+
await import_node_fs21.promises.access(regPath);
|
|
4803
5119
|
} catch {
|
|
4804
5120
|
throw new Error(
|
|
4805
5121
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
4806
5122
|
);
|
|
4807
5123
|
}
|
|
4808
|
-
const pidPath =
|
|
5124
|
+
const pidPath = import_node_path36.default.join(home, "neatd.pid");
|
|
4809
5125
|
await writeAtomically(pidPath, `${process.pid}
|
|
4810
5126
|
`);
|
|
4811
5127
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -4874,7 +5190,7 @@ async function startDaemon(opts = {}) {
|
|
|
4874
5190
|
}
|
|
4875
5191
|
if (restApp) await restApp.close().catch(() => {
|
|
4876
5192
|
});
|
|
4877
|
-
await
|
|
5193
|
+
await import_node_fs21.promises.unlink(pidPath).catch(() => {
|
|
4878
5194
|
});
|
|
4879
5195
|
throw new Error(
|
|
4880
5196
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
@@ -4919,7 +5235,7 @@ async function startDaemon(opts = {}) {
|
|
|
4919
5235
|
});
|
|
4920
5236
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
4921
5237
|
});
|
|
4922
|
-
await
|
|
5238
|
+
await import_node_fs21.promises.unlink(pidPath).catch(() => {
|
|
4923
5239
|
});
|
|
4924
5240
|
throw new Error(
|
|
4925
5241
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
@@ -4959,7 +5275,7 @@ async function startDaemon(opts = {}) {
|
|
|
4959
5275
|
} catch {
|
|
4960
5276
|
}
|
|
4961
5277
|
}
|
|
4962
|
-
await
|
|
5278
|
+
await import_node_fs21.promises.unlink(pidPath).catch(() => {
|
|
4963
5279
|
});
|
|
4964
5280
|
};
|
|
4965
5281
|
return { slots, reload, stop, pidPath, restAddress, otlpAddress };
|
|
@@ -4969,7 +5285,7 @@ async function startDaemon(opts = {}) {
|
|
|
4969
5285
|
init_cjs_shims();
|
|
4970
5286
|
var import_node_child_process = require("child_process");
|
|
4971
5287
|
var import_node_net = __toESM(require("net"), 1);
|
|
4972
|
-
var
|
|
5288
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
4973
5289
|
var DEFAULT_WEB_PORT = 6328;
|
|
4974
5290
|
async function assertPortFree(port) {
|
|
4975
5291
|
await new Promise((resolve, reject) => {
|
|
@@ -4997,10 +5313,10 @@ function resolveWebPackageDir() {
|
|
|
4997
5313
|
eval("require")
|
|
4998
5314
|
);
|
|
4999
5315
|
const pkgJsonPath = req.resolve("@neat.is/web/package.json");
|
|
5000
|
-
return
|
|
5316
|
+
return import_node_path37.default.dirname(pkgJsonPath);
|
|
5001
5317
|
}
|
|
5002
5318
|
function resolveStandaloneServerEntry(webDir) {
|
|
5003
|
-
return
|
|
5319
|
+
return import_node_path37.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
5004
5320
|
}
|
|
5005
5321
|
async function spawnWebUI(restPort) {
|
|
5006
5322
|
const portRaw = process.env.NEAT_WEB_PORT;
|
|
@@ -5025,7 +5341,7 @@ async function spawnWebUI(restPort) {
|
|
|
5025
5341
|
NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
|
|
5026
5342
|
};
|
|
5027
5343
|
const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
|
|
5028
|
-
cwd:
|
|
5344
|
+
cwd: import_node_path37.default.dirname(serverEntry),
|
|
5029
5345
|
env,
|
|
5030
5346
|
stdio: ["ignore", "inherit", "inherit"],
|
|
5031
5347
|
detached: false
|
|
@@ -5062,14 +5378,14 @@ async function spawnWebUI(restPort) {
|
|
|
5062
5378
|
// src/neatd.ts
|
|
5063
5379
|
function neatHome2() {
|
|
5064
5380
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
5065
|
-
return
|
|
5381
|
+
return import_node_path38.default.resolve(process.env.NEAT_HOME);
|
|
5066
5382
|
}
|
|
5067
5383
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5068
|
-
return
|
|
5384
|
+
return import_node_path38.default.join(home, ".neat");
|
|
5069
5385
|
}
|
|
5070
5386
|
async function readPid() {
|
|
5071
5387
|
try {
|
|
5072
|
-
const raw = await
|
|
5388
|
+
const raw = await import_node_fs22.promises.readFile(import_node_path38.default.join(neatHome2(), "neatd.pid"), "utf8");
|
|
5073
5389
|
const n = Number.parseInt(raw.trim(), 10);
|
|
5074
5390
|
return Number.isFinite(n) ? n : null;
|
|
5075
5391
|
} catch {
|