@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/server.cjs
CHANGED
|
@@ -99,8 +99,8 @@ function reshapeGrpcRequest(req) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
function resolveProtoRoot() {
|
|
102
|
-
const here =
|
|
103
|
-
return
|
|
102
|
+
const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
103
|
+
return import_node_path34.default.resolve(here, "..", "proto");
|
|
104
104
|
}
|
|
105
105
|
function loadTraceService() {
|
|
106
106
|
const protoRoot = resolveProtoRoot();
|
|
@@ -155,13 +155,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
155
155
|
})
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
|
-
var import_node_url,
|
|
158
|
+
var import_node_url, import_node_path34, grpc, protoLoader;
|
|
159
159
|
var init_otel_grpc = __esm({
|
|
160
160
|
"src/otel-grpc.ts"() {
|
|
161
161
|
"use strict";
|
|
162
162
|
init_cjs_shims();
|
|
163
163
|
import_node_url = require("url");
|
|
164
|
-
|
|
164
|
+
import_node_path34 = __toESM(require("path"), 1);
|
|
165
165
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
166
166
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
167
167
|
init_otel();
|
|
@@ -258,10 +258,10 @@ function parseOtlpRequest(body) {
|
|
|
258
258
|
}
|
|
259
259
|
function loadProtobufDecoder() {
|
|
260
260
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
261
|
-
const here =
|
|
262
|
-
const protoRoot =
|
|
261
|
+
const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
262
|
+
const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
|
|
263
263
|
const root = new import_protobufjs.default.Root();
|
|
264
|
-
root.resolvePath = (_origin, target) =>
|
|
264
|
+
root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
|
|
265
265
|
root.loadSync(
|
|
266
266
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
267
267
|
{ keepCase: true }
|
|
@@ -353,12 +353,12 @@ async function buildOtelReceiver(opts) {
|
|
|
353
353
|
};
|
|
354
354
|
return decorated;
|
|
355
355
|
}
|
|
356
|
-
var
|
|
356
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
357
357
|
var init_otel = __esm({
|
|
358
358
|
"src/otel.ts"() {
|
|
359
359
|
"use strict";
|
|
360
360
|
init_cjs_shims();
|
|
361
|
-
|
|
361
|
+
import_node_path35 = __toESM(require("path"), 1);
|
|
362
362
|
import_node_url2 = require("url");
|
|
363
363
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
364
364
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -368,7 +368,7 @@ var init_otel = __esm({
|
|
|
368
368
|
|
|
369
369
|
// src/server.ts
|
|
370
370
|
init_cjs_shims();
|
|
371
|
-
var
|
|
371
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
372
372
|
|
|
373
373
|
// src/graph.ts
|
|
374
374
|
init_cjs_shims();
|
|
@@ -392,7 +392,7 @@ function getGraph(project = DEFAULT_PROJECT) {
|
|
|
392
392
|
init_cjs_shims();
|
|
393
393
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
394
394
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
395
|
-
var
|
|
395
|
+
var import_types21 = require("@neat.is/types");
|
|
396
396
|
|
|
397
397
|
// src/divergences.ts
|
|
398
398
|
init_cjs_shims();
|
|
@@ -798,19 +798,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
798
798
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
799
799
|
let best = { path: [start], edges: [] };
|
|
800
800
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
801
|
-
function step(node,
|
|
802
|
-
if (
|
|
803
|
-
best = { path: [...
|
|
801
|
+
function step(node, path38, edges) {
|
|
802
|
+
if (path38.length > best.path.length) {
|
|
803
|
+
best = { path: [...path38], edges: [...edges] };
|
|
804
804
|
}
|
|
805
|
-
if (
|
|
805
|
+
if (path38.length - 1 >= maxDepth) return;
|
|
806
806
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
807
807
|
for (const [srcId, edge] of incoming) {
|
|
808
808
|
if (visited.has(srcId)) continue;
|
|
809
809
|
visited.add(srcId);
|
|
810
|
-
|
|
810
|
+
path38.push(srcId);
|
|
811
811
|
edges.push(edge);
|
|
812
|
-
step(srcId,
|
|
813
|
-
|
|
812
|
+
step(srcId, path38, edges);
|
|
813
|
+
path38.pop();
|
|
814
814
|
edges.pop();
|
|
815
815
|
visited.delete(srcId);
|
|
816
816
|
}
|
|
@@ -1029,14 +1029,6 @@ function nodeIsFrontier(graph, nodeId) {
|
|
|
1029
1029
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
1030
1030
|
return attrs.type === import_types2.NodeType.FrontierNode;
|
|
1031
1031
|
}
|
|
1032
|
-
function hasAnyObservedFromSource(graph, sourceId) {
|
|
1033
|
-
if (!graph.hasNode(sourceId)) return false;
|
|
1034
|
-
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
1035
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
1036
|
-
if (e.provenance === import_types2.Provenance.OBSERVED) return true;
|
|
1037
|
-
}
|
|
1038
|
-
return false;
|
|
1039
|
-
}
|
|
1040
1032
|
function clampConfidence(n) {
|
|
1041
1033
|
if (!Number.isFinite(n)) return 0;
|
|
1042
1034
|
return Math.max(0, Math.min(1, n));
|
|
@@ -1050,32 +1042,34 @@ function reasonForMissingExtracted(source, target, type) {
|
|
|
1050
1042
|
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
1051
1043
|
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.";
|
|
1052
1044
|
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
1045
|
+
function gradedConfidence(edge) {
|
|
1046
|
+
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
1047
|
+
return clampConfidence(confidenceForEdge(edge));
|
|
1048
|
+
}
|
|
1053
1049
|
function detectMissingDivergences(graph, bucket) {
|
|
1054
1050
|
const out = [];
|
|
1055
1051
|
if (bucket.extracted && !bucket.observed) {
|
|
1056
1052
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
1057
|
-
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
1058
1053
|
out.push({
|
|
1059
1054
|
type: "missing-observed",
|
|
1060
1055
|
source: bucket.source,
|
|
1061
1056
|
target: bucket.target,
|
|
1062
1057
|
edgeType: bucket.type,
|
|
1063
1058
|
extracted: bucket.extracted,
|
|
1064
|
-
confidence:
|
|
1059
|
+
confidence: gradedConfidence(bucket.extracted),
|
|
1065
1060
|
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
1066
1061
|
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
1067
1062
|
});
|
|
1068
1063
|
}
|
|
1069
1064
|
}
|
|
1070
1065
|
if (bucket.observed && !bucket.extracted) {
|
|
1071
|
-
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
1072
1066
|
out.push({
|
|
1073
1067
|
type: "missing-extracted",
|
|
1074
1068
|
source: bucket.source,
|
|
1075
1069
|
target: bucket.target,
|
|
1076
1070
|
edgeType: bucket.type,
|
|
1077
1071
|
observed: bucket.observed,
|
|
1078
|
-
confidence:
|
|
1072
|
+
confidence: gradedConfidence(bucket.observed),
|
|
1079
1073
|
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
1080
1074
|
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
1081
1075
|
});
|
|
@@ -1214,8 +1208,17 @@ function computeDivergences(graph, opts = {}) {
|
|
|
1214
1208
|
const target = opts.node;
|
|
1215
1209
|
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
1216
1210
|
}
|
|
1211
|
+
const TYPE_LEADERSHIP = {
|
|
1212
|
+
"missing-extracted": 0,
|
|
1213
|
+
"missing-observed": 1,
|
|
1214
|
+
"version-mismatch": 2,
|
|
1215
|
+
"host-mismatch": 3,
|
|
1216
|
+
"compat-violation": 4
|
|
1217
|
+
};
|
|
1217
1218
|
filtered.sort((a, b) => {
|
|
1218
1219
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
1220
|
+
const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
|
|
1221
|
+
if (lead !== 0) return lead;
|
|
1219
1222
|
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
1220
1223
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
1221
1224
|
return a.target.localeCompare(b.target);
|
|
@@ -1761,35 +1764,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1761
1764
|
const existing = graph.getEdgeAttributes(id);
|
|
1762
1765
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1763
1766
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1767
|
+
const newSignal = {
|
|
1768
|
+
spanCount: newSpanCount,
|
|
1769
|
+
errorCount: newErrorCount,
|
|
1770
|
+
lastObservedAgeMs: 0
|
|
1771
|
+
};
|
|
1764
1772
|
const updated = {
|
|
1765
1773
|
...existing,
|
|
1766
1774
|
provenance: import_types4.Provenance.OBSERVED,
|
|
1767
1775
|
lastObserved: ts,
|
|
1768
1776
|
callCount: newSpanCount,
|
|
1769
|
-
signal:
|
|
1770
|
-
|
|
1771
|
-
errorCount: newErrorCount,
|
|
1772
|
-
lastObservedAgeMs: 0
|
|
1773
|
-
},
|
|
1774
|
-
confidence: 1
|
|
1777
|
+
signal: newSignal,
|
|
1778
|
+
confidence: (0, import_types4.confidenceForObservedSignal)(newSignal)
|
|
1775
1779
|
};
|
|
1776
1780
|
graph.replaceEdgeAttributes(id, updated);
|
|
1777
1781
|
return { edge: updated, created: false };
|
|
1778
1782
|
}
|
|
1783
|
+
const signal = {
|
|
1784
|
+
spanCount: 1,
|
|
1785
|
+
errorCount: isError ? 1 : 0,
|
|
1786
|
+
lastObservedAgeMs: 0
|
|
1787
|
+
};
|
|
1779
1788
|
const edge = {
|
|
1780
1789
|
id,
|
|
1781
1790
|
source,
|
|
1782
1791
|
target,
|
|
1783
1792
|
type,
|
|
1784
1793
|
provenance: import_types4.Provenance.OBSERVED,
|
|
1785
|
-
confidence:
|
|
1794
|
+
confidence: (0, import_types4.confidenceForObservedSignal)(signal),
|
|
1786
1795
|
lastObserved: ts,
|
|
1787
1796
|
callCount: 1,
|
|
1788
|
-
signal
|
|
1789
|
-
spanCount: 1,
|
|
1790
|
-
errorCount: isError ? 1 : 0,
|
|
1791
|
-
lastObservedAgeMs: 0
|
|
1792
|
-
}
|
|
1797
|
+
signal
|
|
1793
1798
|
};
|
|
1794
1799
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1795
1800
|
return { edge, created: true };
|
|
@@ -2086,8 +2091,8 @@ async function readErrorEvents(errorsPath) {
|
|
|
2086
2091
|
|
|
2087
2092
|
// src/extract/services.ts
|
|
2088
2093
|
init_cjs_shims();
|
|
2089
|
-
var
|
|
2090
|
-
var
|
|
2094
|
+
var import_node_fs8 = require("fs");
|
|
2095
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2091
2096
|
var import_ignore = __toESM(require("ignore"), 1);
|
|
2092
2097
|
var import_minimatch2 = require("minimatch");
|
|
2093
2098
|
var import_types6 = require("@neat.is/types");
|
|
@@ -2111,9 +2116,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
2111
2116
|
function isConfigFile(name) {
|
|
2112
2117
|
const ext = import_node_path4.default.extname(name);
|
|
2113
2118
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2114
|
-
if (name === ".env" || name.startsWith(".env."))
|
|
2119
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
2120
|
+
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2121
|
+
return { match: true, fileType: "env" };
|
|
2122
|
+
}
|
|
2115
2123
|
return { match: false, fileType: "" };
|
|
2116
2124
|
}
|
|
2125
|
+
function isTestPath(filePath) {
|
|
2126
|
+
const normalised = filePath.replace(/\\/g, "/");
|
|
2127
|
+
const segments = normalised.split("/");
|
|
2128
|
+
for (const seg of segments) {
|
|
2129
|
+
if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
const base = segments[segments.length - 1] ?? "";
|
|
2134
|
+
return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
|
|
2135
|
+
}
|
|
2136
|
+
function isEnvTemplateFile(name) {
|
|
2137
|
+
if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
|
|
2138
|
+
return true;
|
|
2139
|
+
}
|
|
2140
|
+
return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
|
|
2141
|
+
}
|
|
2142
|
+
function maskCommentsInSource(src) {
|
|
2143
|
+
const len = src.length;
|
|
2144
|
+
const out = new Array(len);
|
|
2145
|
+
let i = 0;
|
|
2146
|
+
let inString = 0;
|
|
2147
|
+
let escaped = false;
|
|
2148
|
+
while (i < len) {
|
|
2149
|
+
const c = src[i];
|
|
2150
|
+
if (inString !== 0) {
|
|
2151
|
+
out[i] = c;
|
|
2152
|
+
if (escaped) {
|
|
2153
|
+
escaped = false;
|
|
2154
|
+
} else if (c === "\\") {
|
|
2155
|
+
escaped = true;
|
|
2156
|
+
} else if (c === inString) {
|
|
2157
|
+
inString = 0;
|
|
2158
|
+
}
|
|
2159
|
+
i++;
|
|
2160
|
+
continue;
|
|
2161
|
+
}
|
|
2162
|
+
if (c === "/" && i + 1 < len) {
|
|
2163
|
+
const next = src[i + 1];
|
|
2164
|
+
if (next === "/") {
|
|
2165
|
+
out[i] = " ";
|
|
2166
|
+
out[i + 1] = " ";
|
|
2167
|
+
let j = i + 2;
|
|
2168
|
+
while (j < len && src[j] !== "\n") {
|
|
2169
|
+
out[j] = " ";
|
|
2170
|
+
j++;
|
|
2171
|
+
}
|
|
2172
|
+
i = j;
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
if (next === "*") {
|
|
2176
|
+
out[i] = " ";
|
|
2177
|
+
out[i + 1] = " ";
|
|
2178
|
+
let j = i + 2;
|
|
2179
|
+
while (j < len) {
|
|
2180
|
+
if (src[j] === "\n") {
|
|
2181
|
+
out[j] = "\n";
|
|
2182
|
+
j++;
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
|
|
2186
|
+
out[j] = " ";
|
|
2187
|
+
out[j + 1] = " ";
|
|
2188
|
+
j += 2;
|
|
2189
|
+
break;
|
|
2190
|
+
}
|
|
2191
|
+
out[j] = " ";
|
|
2192
|
+
j++;
|
|
2193
|
+
}
|
|
2194
|
+
i = j;
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
out[i] = c;
|
|
2199
|
+
if (c === "'" || c === '"' || c === "`") inString = c;
|
|
2200
|
+
i++;
|
|
2201
|
+
}
|
|
2202
|
+
return out.join("");
|
|
2203
|
+
}
|
|
2204
|
+
var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
|
|
2205
|
+
function urlMatchesHost(urlString, host) {
|
|
2206
|
+
if (typeof urlString !== "string" || urlString.length === 0) return false;
|
|
2207
|
+
if (!URL_LIKE.test(urlString)) return false;
|
|
2208
|
+
const [wantedHost, wantedPort] = host.split(":");
|
|
2209
|
+
let parsed;
|
|
2210
|
+
try {
|
|
2211
|
+
const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
|
|
2212
|
+
parsed = new URL(candidate);
|
|
2213
|
+
} catch {
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
|
|
2217
|
+
if (wantedPort && parsed.port !== wantedPort) return false;
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2117
2220
|
function cleanVersion(raw) {
|
|
2118
2221
|
if (!raw) return void 0;
|
|
2119
2222
|
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
@@ -2269,6 +2372,50 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
|
|
|
2269
2372
|
return author ?? void 0;
|
|
2270
2373
|
}
|
|
2271
2374
|
|
|
2375
|
+
// src/extract/errors.ts
|
|
2376
|
+
init_cjs_shims();
|
|
2377
|
+
var import_node_fs7 = require("fs");
|
|
2378
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2379
|
+
var sink = [];
|
|
2380
|
+
function recordExtractionError(producer, file, err) {
|
|
2381
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
2382
|
+
sink.push({
|
|
2383
|
+
producer,
|
|
2384
|
+
file,
|
|
2385
|
+
error: e.message,
|
|
2386
|
+
stack: e.stack,
|
|
2387
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2388
|
+
source: "extract"
|
|
2389
|
+
});
|
|
2390
|
+
console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
|
|
2391
|
+
}
|
|
2392
|
+
function drainExtractionErrors() {
|
|
2393
|
+
return sink.splice(0, sink.length);
|
|
2394
|
+
}
|
|
2395
|
+
async function writeExtractionErrors(errors, errorsPath) {
|
|
2396
|
+
if (errors.length === 0) return;
|
|
2397
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
|
|
2398
|
+
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2399
|
+
await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
|
|
2400
|
+
}
|
|
2401
|
+
var droppedSink = [];
|
|
2402
|
+
function noteExtractedDropped(edge) {
|
|
2403
|
+
droppedSink.push(edge);
|
|
2404
|
+
}
|
|
2405
|
+
function drainDroppedExtracted() {
|
|
2406
|
+
return droppedSink.splice(0, droppedSink.length);
|
|
2407
|
+
}
|
|
2408
|
+
function isRejectedLogEnabled() {
|
|
2409
|
+
const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
|
|
2410
|
+
return raw === "1" || raw === "true";
|
|
2411
|
+
}
|
|
2412
|
+
async function writeRejectedExtracted(drops, rejectedPath) {
|
|
2413
|
+
if (drops.length === 0) return;
|
|
2414
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
|
|
2415
|
+
const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
|
|
2416
|
+
await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2272
2419
|
// src/extract/services.ts
|
|
2273
2420
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
2274
2421
|
function parseScanDepth() {
|
|
@@ -2285,21 +2432,21 @@ function workspaceGlobs(pkg) {
|
|
|
2285
2432
|
return null;
|
|
2286
2433
|
}
|
|
2287
2434
|
async function loadGitignore(scanPath) {
|
|
2288
|
-
const gitignorePath =
|
|
2435
|
+
const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
|
|
2289
2436
|
if (!await exists(gitignorePath)) return null;
|
|
2290
|
-
const raw = await
|
|
2437
|
+
const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
|
|
2291
2438
|
return (0, import_ignore.default)().add(raw);
|
|
2292
2439
|
}
|
|
2293
2440
|
async function walkDirs(start, scanPath, options, visit) {
|
|
2294
2441
|
async function recurse(current, depth) {
|
|
2295
2442
|
if (depth > options.maxDepth) return;
|
|
2296
|
-
const entries = await
|
|
2443
|
+
const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2297
2444
|
for (const entry of entries) {
|
|
2298
2445
|
if (!entry.isDirectory()) continue;
|
|
2299
2446
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2300
|
-
const child =
|
|
2447
|
+
const child = import_node_path8.default.join(current, entry.name);
|
|
2301
2448
|
if (options.ig) {
|
|
2302
|
-
const rel =
|
|
2449
|
+
const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
|
|
2303
2450
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2304
2451
|
}
|
|
2305
2452
|
await visit(child);
|
|
@@ -2314,8 +2461,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2314
2461
|
for (const raw of globs) {
|
|
2315
2462
|
const pattern = raw.replace(/^\.\//, "");
|
|
2316
2463
|
if (!pattern.includes("*")) {
|
|
2317
|
-
const candidate =
|
|
2318
|
-
if (await exists(
|
|
2464
|
+
const candidate = import_node_path8.default.join(scanPath, pattern);
|
|
2465
|
+
if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2319
2466
|
continue;
|
|
2320
2467
|
}
|
|
2321
2468
|
const segments = pattern.split("/");
|
|
@@ -2324,13 +2471,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2324
2471
|
if (seg.includes("*")) break;
|
|
2325
2472
|
staticSegments.push(seg);
|
|
2326
2473
|
}
|
|
2327
|
-
const start =
|
|
2474
|
+
const start = import_node_path8.default.join(scanPath, ...staticSegments);
|
|
2328
2475
|
if (!await exists(start)) continue;
|
|
2329
2476
|
const hasDoubleStar = pattern.includes("**");
|
|
2330
2477
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2331
2478
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2332
|
-
const rel =
|
|
2333
|
-
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(
|
|
2479
|
+
const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
|
|
2480
|
+
if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2334
2481
|
found.add(dir);
|
|
2335
2482
|
}
|
|
2336
2483
|
});
|
|
@@ -2338,15 +2485,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
2338
2485
|
return [...found];
|
|
2339
2486
|
}
|
|
2340
2487
|
async function discoverNodeService(scanPath, dir) {
|
|
2341
|
-
const pkgPath =
|
|
2488
|
+
const pkgPath = import_node_path8.default.join(dir, "package.json");
|
|
2342
2489
|
if (!await exists(pkgPath)) return null;
|
|
2343
2490
|
let pkg;
|
|
2344
2491
|
try {
|
|
2345
2492
|
pkg = await readJson(pkgPath);
|
|
2346
2493
|
} catch (err) {
|
|
2347
|
-
|
|
2348
|
-
`[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
|
|
2349
|
-
);
|
|
2494
|
+
recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
|
|
2350
2495
|
return null;
|
|
2351
2496
|
}
|
|
2352
2497
|
if (!pkg.name) return null;
|
|
@@ -2357,7 +2502,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
2357
2502
|
language: "javascript",
|
|
2358
2503
|
version: pkg.version,
|
|
2359
2504
|
dependencies: pkg.dependencies ?? {},
|
|
2360
|
-
repoPath:
|
|
2505
|
+
repoPath: import_node_path8.default.relative(scanPath, dir),
|
|
2361
2506
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2362
2507
|
};
|
|
2363
2508
|
return { pkg, dir, node };
|
|
@@ -2373,19 +2518,21 @@ async function discoverPyService(scanPath, dir) {
|
|
|
2373
2518
|
language: "python",
|
|
2374
2519
|
version: py.version,
|
|
2375
2520
|
dependencies: py.dependencies,
|
|
2376
|
-
repoPath:
|
|
2521
|
+
repoPath: import_node_path8.default.relative(scanPath, dir)
|
|
2377
2522
|
};
|
|
2378
2523
|
return { pkg, dir, node };
|
|
2379
2524
|
}
|
|
2380
2525
|
async function discoverServices(scanPath) {
|
|
2381
|
-
const rootPkgPath =
|
|
2526
|
+
const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
|
|
2382
2527
|
let rootPkg = null;
|
|
2383
2528
|
if (await exists(rootPkgPath)) {
|
|
2384
2529
|
try {
|
|
2385
2530
|
rootPkg = await readJson(rootPkgPath);
|
|
2386
2531
|
} catch (err) {
|
|
2387
|
-
|
|
2388
|
-
|
|
2532
|
+
recordExtractionError(
|
|
2533
|
+
"services workspaces",
|
|
2534
|
+
import_node_path8.default.relative(scanPath, rootPkgPath),
|
|
2535
|
+
err
|
|
2389
2536
|
);
|
|
2390
2537
|
}
|
|
2391
2538
|
}
|
|
@@ -2401,9 +2548,9 @@ async function discoverServices(scanPath) {
|
|
|
2401
2548
|
scanPath,
|
|
2402
2549
|
{ maxDepth: parseScanDepth(), ig },
|
|
2403
2550
|
async (dir) => {
|
|
2404
|
-
if (await exists(
|
|
2551
|
+
if (await exists(import_node_path8.default.join(dir, "package.json"))) {
|
|
2405
2552
|
candidateDirs.push(dir);
|
|
2406
|
-
} else if (await exists(
|
|
2553
|
+
} else if (await exists(import_node_path8.default.join(dir, "pyproject.toml")) || await exists(import_node_path8.default.join(dir, "requirements.txt")) || await exists(import_node_path8.default.join(dir, "setup.py"))) {
|
|
2407
2554
|
candidateDirs.push(dir);
|
|
2408
2555
|
}
|
|
2409
2556
|
}
|
|
@@ -2417,8 +2564,8 @@ async function discoverServices(scanPath) {
|
|
|
2417
2564
|
if (!service) continue;
|
|
2418
2565
|
const existingDir = seen.get(service.node.name);
|
|
2419
2566
|
if (existingDir !== void 0) {
|
|
2420
|
-
const a =
|
|
2421
|
-
const b =
|
|
2567
|
+
const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
|
|
2568
|
+
const b = import_node_path8.default.relative(scanPath, dir) || ".";
|
|
2422
2569
|
console.warn(
|
|
2423
2570
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2424
2571
|
);
|
|
@@ -2455,8 +2602,8 @@ function addServiceNodes(graph, services) {
|
|
|
2455
2602
|
|
|
2456
2603
|
// src/extract/aliases.ts
|
|
2457
2604
|
init_cjs_shims();
|
|
2458
|
-
var
|
|
2459
|
-
var
|
|
2605
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2606
|
+
var import_node_fs9 = require("fs");
|
|
2460
2607
|
var import_yaml2 = require("yaml");
|
|
2461
2608
|
var import_types7 = require("@neat.is/types");
|
|
2462
2609
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -2483,14 +2630,14 @@ function indexServicesByName(services) {
|
|
|
2483
2630
|
const map = /* @__PURE__ */ new Map();
|
|
2484
2631
|
for (const s of services) {
|
|
2485
2632
|
map.set(s.node.name, s.node.id);
|
|
2486
|
-
map.set(
|
|
2633
|
+
map.set(import_node_path9.default.basename(s.dir), s.node.id);
|
|
2487
2634
|
}
|
|
2488
2635
|
return map;
|
|
2489
2636
|
}
|
|
2490
2637
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2491
2638
|
let composePath = null;
|
|
2492
2639
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2493
|
-
const abs =
|
|
2640
|
+
const abs = import_node_path9.default.join(scanPath, name);
|
|
2494
2641
|
if (await exists(abs)) {
|
|
2495
2642
|
composePath = abs;
|
|
2496
2643
|
break;
|
|
@@ -2501,8 +2648,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
2501
2648
|
try {
|
|
2502
2649
|
compose = await readYaml(composePath);
|
|
2503
2650
|
} catch (err) {
|
|
2504
|
-
|
|
2505
|
-
|
|
2651
|
+
recordExtractionError(
|
|
2652
|
+
"aliases compose",
|
|
2653
|
+
import_node_path9.default.relative(scanPath, composePath),
|
|
2654
|
+
err
|
|
2506
2655
|
);
|
|
2507
2656
|
return;
|
|
2508
2657
|
}
|
|
@@ -2544,15 +2693,13 @@ function parseDockerfileLabels(content) {
|
|
|
2544
2693
|
}
|
|
2545
2694
|
async function collectDockerfileAliases(graph, services) {
|
|
2546
2695
|
for (const service of services) {
|
|
2547
|
-
const dockerfilePath =
|
|
2696
|
+
const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
|
|
2548
2697
|
if (!await exists(dockerfilePath)) continue;
|
|
2549
2698
|
let content;
|
|
2550
2699
|
try {
|
|
2551
|
-
content = await
|
|
2700
|
+
content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
|
|
2552
2701
|
} catch (err) {
|
|
2553
|
-
|
|
2554
|
-
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2555
|
-
);
|
|
2702
|
+
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
2556
2703
|
continue;
|
|
2557
2704
|
}
|
|
2558
2705
|
const aliases = parseDockerfileLabels(content);
|
|
@@ -2562,13 +2709,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2562
2709
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2563
2710
|
if (depth > max) return [];
|
|
2564
2711
|
const out = [];
|
|
2565
|
-
const entries = await
|
|
2712
|
+
const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2566
2713
|
for (const entry of entries) {
|
|
2567
2714
|
if (entry.isDirectory()) {
|
|
2568
2715
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2569
|
-
out.push(...await walkYamlFiles(
|
|
2570
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2571
|
-
out.push(
|
|
2716
|
+
out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
|
|
2717
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
|
|
2718
|
+
out.push(import_node_path9.default.join(start, entry.name));
|
|
2572
2719
|
}
|
|
2573
2720
|
}
|
|
2574
2721
|
return out;
|
|
@@ -2595,7 +2742,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2595
2742
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2596
2743
|
const files = await walkYamlFiles(scanPath);
|
|
2597
2744
|
for (const file of files) {
|
|
2598
|
-
const content = await
|
|
2745
|
+
const content = await import_node_fs9.promises.readFile(file, "utf8");
|
|
2599
2746
|
let docs;
|
|
2600
2747
|
try {
|
|
2601
2748
|
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -2620,14 +2767,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2620
2767
|
|
|
2621
2768
|
// src/extract/databases/index.ts
|
|
2622
2769
|
init_cjs_shims();
|
|
2623
|
-
var
|
|
2770
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2624
2771
|
var import_types8 = require("@neat.is/types");
|
|
2625
2772
|
|
|
2626
2773
|
// src/extract/databases/db-config-yaml.ts
|
|
2627
2774
|
init_cjs_shims();
|
|
2628
|
-
var
|
|
2775
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2629
2776
|
async function parse(serviceDir) {
|
|
2630
|
-
const yamlPath =
|
|
2777
|
+
const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
|
|
2631
2778
|
if (!await exists(yamlPath)) return [];
|
|
2632
2779
|
const raw = await readYaml(yamlPath);
|
|
2633
2780
|
return [
|
|
@@ -2645,13 +2792,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
|
2645
2792
|
|
|
2646
2793
|
// src/extract/databases/dotenv.ts
|
|
2647
2794
|
init_cjs_shims();
|
|
2648
|
-
var
|
|
2649
|
-
var
|
|
2795
|
+
var import_node_fs11 = require("fs");
|
|
2796
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2650
2797
|
|
|
2651
2798
|
// src/extract/databases/shared.ts
|
|
2652
2799
|
init_cjs_shims();
|
|
2653
|
-
var
|
|
2654
|
-
var
|
|
2800
|
+
var import_node_fs10 = require("fs");
|
|
2801
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2655
2802
|
function schemeToEngine(scheme) {
|
|
2656
2803
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2657
2804
|
switch (s) {
|
|
@@ -2690,14 +2837,14 @@ function parseConnectionString(url) {
|
|
|
2690
2837
|
}
|
|
2691
2838
|
async function readIfExists(filePath) {
|
|
2692
2839
|
try {
|
|
2693
|
-
return await
|
|
2840
|
+
return await import_node_fs10.promises.readFile(filePath, "utf8");
|
|
2694
2841
|
} catch {
|
|
2695
2842
|
return null;
|
|
2696
2843
|
}
|
|
2697
2844
|
}
|
|
2698
2845
|
async function findFirst(serviceDir, candidates) {
|
|
2699
2846
|
for (const rel of candidates) {
|
|
2700
|
-
const abs =
|
|
2847
|
+
const abs = import_node_path11.default.join(serviceDir, rel);
|
|
2701
2848
|
const content = await readIfExists(abs);
|
|
2702
2849
|
if (content !== null) return abs;
|
|
2703
2850
|
}
|
|
@@ -2748,15 +2895,15 @@ function parseDotenvLine(line) {
|
|
|
2748
2895
|
return { key, value };
|
|
2749
2896
|
}
|
|
2750
2897
|
async function parse2(serviceDir) {
|
|
2751
|
-
const entries = await
|
|
2898
|
+
const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2752
2899
|
const configs = [];
|
|
2753
2900
|
const seen = /* @__PURE__ */ new Set();
|
|
2754
2901
|
for (const entry of entries) {
|
|
2755
2902
|
if (!entry.isFile()) continue;
|
|
2756
2903
|
const match = isConfigFile(entry.name);
|
|
2757
2904
|
if (!match.match || match.fileType !== "env") continue;
|
|
2758
|
-
const filePath =
|
|
2759
|
-
const content = await
|
|
2905
|
+
const filePath = import_node_path12.default.join(serviceDir, entry.name);
|
|
2906
|
+
const content = await import_node_fs11.promises.readFile(filePath, "utf8");
|
|
2760
2907
|
for (const line of content.split("\n")) {
|
|
2761
2908
|
const parsed = parseDotenvLine(line);
|
|
2762
2909
|
if (!parsed) continue;
|
|
@@ -2775,9 +2922,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
|
|
|
2775
2922
|
|
|
2776
2923
|
// src/extract/databases/prisma.ts
|
|
2777
2924
|
init_cjs_shims();
|
|
2778
|
-
var
|
|
2925
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2779
2926
|
async function parse3(serviceDir) {
|
|
2780
|
-
const schemaPath =
|
|
2927
|
+
const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2781
2928
|
const content = await readIfExists(schemaPath);
|
|
2782
2929
|
if (!content) return [];
|
|
2783
2930
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2909,10 +3056,10 @@ var knexParser = { name: "knex", parse: parse5 };
|
|
|
2909
3056
|
|
|
2910
3057
|
// src/extract/databases/ormconfig.ts
|
|
2911
3058
|
init_cjs_shims();
|
|
2912
|
-
var
|
|
3059
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2913
3060
|
async function parse6(serviceDir) {
|
|
2914
3061
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2915
|
-
const abs =
|
|
3062
|
+
const abs = import_node_path14.default.join(serviceDir, candidate);
|
|
2916
3063
|
if (!await exists(abs)) continue;
|
|
2917
3064
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2918
3065
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2972,9 +3119,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
|
|
|
2972
3119
|
|
|
2973
3120
|
// src/extract/databases/sequelize.ts
|
|
2974
3121
|
init_cjs_shims();
|
|
2975
|
-
var
|
|
3122
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2976
3123
|
async function parse8(serviceDir) {
|
|
2977
|
-
const configPath =
|
|
3124
|
+
const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
|
|
2978
3125
|
if (!await exists(configPath)) return [];
|
|
2979
3126
|
const raw = await readJson(configPath);
|
|
2980
3127
|
const out = [];
|
|
@@ -3001,7 +3148,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
|
3001
3148
|
|
|
3002
3149
|
// src/extract/databases/docker-compose.ts
|
|
3003
3150
|
init_cjs_shims();
|
|
3004
|
-
var
|
|
3151
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
3005
3152
|
function portFromService(svc) {
|
|
3006
3153
|
for (const raw of svc.ports ?? []) {
|
|
3007
3154
|
const str = String(raw);
|
|
@@ -3028,7 +3175,7 @@ function databaseFromEnv(svc) {
|
|
|
3028
3175
|
}
|
|
3029
3176
|
async function parse9(serviceDir) {
|
|
3030
3177
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3031
|
-
const abs =
|
|
3178
|
+
const abs = import_node_path16.default.join(serviceDir, name);
|
|
3032
3179
|
if (!await exists(abs)) continue;
|
|
3033
3180
|
const raw = await readYaml(abs);
|
|
3034
3181
|
if (!raw?.services) return [];
|
|
@@ -3206,11 +3353,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3206
3353
|
target: dbNode.id,
|
|
3207
3354
|
type: import_types8.EdgeType.CONNECTS_TO,
|
|
3208
3355
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3356
|
+
confidence: (0, import_types8.confidenceForExtracted)("structural"),
|
|
3357
|
+
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
3358
|
+
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3359
|
+
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3360
|
+
evidence: {
|
|
3361
|
+
file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
|
|
3362
|
+
}
|
|
3214
3363
|
};
|
|
3215
3364
|
if (!graph.hasEdge(edge.id)) {
|
|
3216
3365
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3236,15 +3385,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3236
3385
|
|
|
3237
3386
|
// src/extract/configs.ts
|
|
3238
3387
|
init_cjs_shims();
|
|
3239
|
-
var
|
|
3240
|
-
var
|
|
3388
|
+
var import_node_fs12 = require("fs");
|
|
3389
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
3241
3390
|
var import_types9 = require("@neat.is/types");
|
|
3242
3391
|
async function walkConfigFiles(dir) {
|
|
3243
3392
|
const out = [];
|
|
3244
3393
|
async function walk(current) {
|
|
3245
|
-
const entries = await
|
|
3394
|
+
const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
|
|
3246
3395
|
for (const entry of entries) {
|
|
3247
|
-
const full =
|
|
3396
|
+
const full = import_node_path18.default.join(current, entry.name);
|
|
3248
3397
|
if (entry.isDirectory()) {
|
|
3249
3398
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
3250
3399
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
@@ -3261,13 +3410,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3261
3410
|
for (const service of services) {
|
|
3262
3411
|
const configFiles = await walkConfigFiles(service.dir);
|
|
3263
3412
|
for (const file of configFiles) {
|
|
3264
|
-
const relPath =
|
|
3413
|
+
const relPath = import_node_path18.default.relative(scanPath, file);
|
|
3265
3414
|
const node = {
|
|
3266
3415
|
id: (0, import_types9.configId)(relPath),
|
|
3267
3416
|
type: import_types9.NodeType.ConfigNode,
|
|
3268
|
-
name:
|
|
3417
|
+
name: import_node_path18.default.basename(file),
|
|
3269
3418
|
path: relPath,
|
|
3270
|
-
fileType: isConfigFile(
|
|
3419
|
+
fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
|
|
3271
3420
|
};
|
|
3272
3421
|
if (!graph.hasNode(node.id)) {
|
|
3273
3422
|
graph.addNode(node.id, node);
|
|
@@ -3279,7 +3428,8 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3279
3428
|
target: node.id,
|
|
3280
3429
|
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
3281
3430
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3282
|
-
|
|
3431
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
3432
|
+
evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
|
|
3283
3433
|
};
|
|
3284
3434
|
if (!graph.hasEdge(edge.id)) {
|
|
3285
3435
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3296,7 +3446,7 @@ var import_types15 = require("@neat.is/types");
|
|
|
3296
3446
|
|
|
3297
3447
|
// src/extract/calls/http.ts
|
|
3298
3448
|
init_cjs_shims();
|
|
3299
|
-
var
|
|
3449
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3300
3450
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3301
3451
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3302
3452
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
@@ -3304,17 +3454,17 @@ var import_types10 = require("@neat.is/types");
|
|
|
3304
3454
|
|
|
3305
3455
|
// src/extract/calls/shared.ts
|
|
3306
3456
|
init_cjs_shims();
|
|
3307
|
-
var
|
|
3308
|
-
var
|
|
3457
|
+
var import_node_fs13 = require("fs");
|
|
3458
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3309
3459
|
async function walkSourceFiles(dir) {
|
|
3310
3460
|
const out = [];
|
|
3311
3461
|
async function walk(current) {
|
|
3312
|
-
const entries = await
|
|
3462
|
+
const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3313
3463
|
for (const entry of entries) {
|
|
3314
|
-
const full =
|
|
3464
|
+
const full = import_node_path19.default.join(current, entry.name);
|
|
3315
3465
|
if (entry.isDirectory()) {
|
|
3316
3466
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
3317
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
3467
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
|
|
3318
3468
|
out.push(full);
|
|
3319
3469
|
}
|
|
3320
3470
|
}
|
|
@@ -3327,7 +3477,7 @@ async function loadSourceFiles(dir) {
|
|
|
3327
3477
|
const out = [];
|
|
3328
3478
|
for (const p of paths) {
|
|
3329
3479
|
try {
|
|
3330
|
-
const content = await
|
|
3480
|
+
const content = await import_node_fs13.promises.readFile(p, "utf8");
|
|
3331
3481
|
out.push({ path: p, content });
|
|
3332
3482
|
} catch {
|
|
3333
3483
|
}
|
|
@@ -3346,8 +3496,27 @@ function snippet(text, line) {
|
|
|
3346
3496
|
|
|
3347
3497
|
// src/extract/calls/http.ts
|
|
3348
3498
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3499
|
+
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3500
|
+
function isInsideJsxExternalLink(node) {
|
|
3501
|
+
let cursor = node.parent;
|
|
3502
|
+
while (cursor) {
|
|
3503
|
+
if (cursor.type === "jsx_attribute") {
|
|
3504
|
+
let owner = cursor.parent;
|
|
3505
|
+
while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
|
|
3506
|
+
owner = owner.parent;
|
|
3507
|
+
}
|
|
3508
|
+
if (!owner) return false;
|
|
3509
|
+
const tagNode = owner.namedChild(0);
|
|
3510
|
+
const tagName = tagNode?.text ?? "";
|
|
3511
|
+
const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
|
|
3512
|
+
return JSX_EXTERNAL_LINK_TAGS.has(right);
|
|
3513
|
+
}
|
|
3514
|
+
cursor = cursor.parent;
|
|
3515
|
+
}
|
|
3516
|
+
return false;
|
|
3517
|
+
}
|
|
3349
3518
|
function collectStringLiterals(node, out) {
|
|
3350
|
-
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
|
|
3519
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
|
|
3351
3520
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3352
3521
|
const child = node.namedChild(i);
|
|
3353
3522
|
if (child) collectStringLiterals(child, out);
|
|
@@ -3359,8 +3528,9 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3359
3528
|
collectStringLiterals(tree.rootNode, literals);
|
|
3360
3529
|
const targets = /* @__PURE__ */ new Set();
|
|
3361
3530
|
for (const lit of literals) {
|
|
3531
|
+
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
3362
3532
|
for (const host of knownHosts) {
|
|
3363
|
-
if (lit.
|
|
3533
|
+
if (urlMatchesHost(lit.text, host)) {
|
|
3364
3534
|
targets.add(host);
|
|
3365
3535
|
}
|
|
3366
3536
|
}
|
|
@@ -3383,9 +3553,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3383
3553
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3384
3554
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3385
3555
|
for (const service of services) {
|
|
3386
|
-
knownHosts.add(
|
|
3556
|
+
knownHosts.add(import_node_path20.default.basename(service.dir));
|
|
3387
3557
|
knownHosts.add(service.pkg.name);
|
|
3388
|
-
hostToNodeId.set(
|
|
3558
|
+
hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
|
|
3389
3559
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3390
3560
|
}
|
|
3391
3561
|
let edgesAdded = 0;
|
|
@@ -3393,14 +3563,13 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3393
3563
|
const files = await loadSourceFiles(service.dir);
|
|
3394
3564
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
3395
3565
|
for (const file of files) {
|
|
3396
|
-
|
|
3566
|
+
if (isTestPath(file.path)) continue;
|
|
3567
|
+
const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3397
3568
|
let targets;
|
|
3398
3569
|
try {
|
|
3399
3570
|
targets = callsFromSource(file.content, parser, knownHosts);
|
|
3400
3571
|
} catch (err) {
|
|
3401
|
-
|
|
3402
|
-
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
3403
|
-
);
|
|
3572
|
+
recordExtractionError("http call extraction", file.path, err);
|
|
3404
3573
|
continue;
|
|
3405
3574
|
}
|
|
3406
3575
|
for (const t of targets) {
|
|
@@ -3414,17 +3583,32 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3414
3583
|
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3415
3584
|
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3416
3585
|
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3586
|
+
const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
|
|
3587
|
+
const ev = {
|
|
3588
|
+
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3589
|
+
line,
|
|
3590
|
+
snippet: snippet(fileContent, line)
|
|
3591
|
+
};
|
|
3592
|
+
const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, targetId, import_types10.EdgeType.CALLS);
|
|
3593
|
+
if (!(0, import_types10.passesExtractedFloor)(confidence)) {
|
|
3594
|
+
noteExtractedDropped({
|
|
3595
|
+
source: service.node.id,
|
|
3596
|
+
target: targetId,
|
|
3597
|
+
type: import_types10.EdgeType.CALLS,
|
|
3598
|
+
confidence,
|
|
3599
|
+
confidenceKind: "hostname-shape-match",
|
|
3600
|
+
evidence: ev
|
|
3601
|
+
});
|
|
3602
|
+
continue;
|
|
3603
|
+
}
|
|
3417
3604
|
const edge = {
|
|
3418
|
-
id:
|
|
3605
|
+
id: edgeId,
|
|
3419
3606
|
source: service.node.id,
|
|
3420
3607
|
target: targetId,
|
|
3421
3608
|
type: import_types10.EdgeType.CALLS,
|
|
3422
3609
|
provenance: import_types10.Provenance.EXTRACTED,
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
line,
|
|
3426
|
-
snippet: snippet(fileContent, line)
|
|
3427
|
-
}
|
|
3610
|
+
confidence,
|
|
3611
|
+
evidence: ev
|
|
3428
3612
|
};
|
|
3429
3613
|
if (!graph.hasEdge(edge.id)) {
|
|
3430
3614
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3437,7 +3621,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3437
3621
|
|
|
3438
3622
|
// src/extract/calls/kafka.ts
|
|
3439
3623
|
init_cjs_shims();
|
|
3440
|
-
var
|
|
3624
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3441
3625
|
var import_types11 = require("@neat.is/types");
|
|
3442
3626
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3443
3627
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3463,8 +3647,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3463
3647
|
name: topic,
|
|
3464
3648
|
kind: "kafka-topic",
|
|
3465
3649
|
edgeType,
|
|
3650
|
+
// `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
|
|
3651
|
+
// framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
|
|
3652
|
+
// tier (ADR-066).
|
|
3653
|
+
confidenceKind: "verified-call-site",
|
|
3466
3654
|
evidence: {
|
|
3467
|
-
file:
|
|
3655
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3468
3656
|
line,
|
|
3469
3657
|
snippet: snippet(file.content, line)
|
|
3470
3658
|
}
|
|
@@ -3477,7 +3665,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3477
3665
|
|
|
3478
3666
|
// src/extract/calls/redis.ts
|
|
3479
3667
|
init_cjs_shims();
|
|
3480
|
-
var
|
|
3668
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3481
3669
|
var import_types12 = require("@neat.is/types");
|
|
3482
3670
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3483
3671
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3495,8 +3683,12 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3495
3683
|
name: host,
|
|
3496
3684
|
kind: "redis",
|
|
3497
3685
|
edgeType: "CALLS",
|
|
3686
|
+
// `redis://host` URL literal — the scheme is structural support, but no
|
|
3687
|
+
// call expression is verified to wire it through. URL-with-structural-
|
|
3688
|
+
// support tier (ADR-066).
|
|
3689
|
+
confidenceKind: "url-with-structural-support",
|
|
3498
3690
|
evidence: {
|
|
3499
|
-
file:
|
|
3691
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3500
3692
|
line,
|
|
3501
3693
|
snippet: snippet(file.content, line)
|
|
3502
3694
|
}
|
|
@@ -3507,7 +3699,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3507
3699
|
|
|
3508
3700
|
// src/extract/calls/aws.ts
|
|
3509
3701
|
init_cjs_shims();
|
|
3510
|
-
var
|
|
3702
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3511
3703
|
var import_types13 = require("@neat.is/types");
|
|
3512
3704
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3513
3705
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3536,8 +3728,12 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3536
3728
|
name,
|
|
3537
3729
|
kind,
|
|
3538
3730
|
edgeType: "CALLS",
|
|
3731
|
+
// SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
|
|
3732
|
+
// literal — framework-aware recognizer, verified-call-site tier
|
|
3733
|
+
// (ADR-066).
|
|
3734
|
+
confidenceKind: "verified-call-site",
|
|
3539
3735
|
evidence: {
|
|
3540
|
-
file:
|
|
3736
|
+
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3541
3737
|
line,
|
|
3542
3738
|
snippet: snippet(file.content, line)
|
|
3543
3739
|
}
|
|
@@ -3562,16 +3758,42 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3562
3758
|
|
|
3563
3759
|
// src/extract/calls/grpc.ts
|
|
3564
3760
|
init_cjs_shims();
|
|
3565
|
-
var
|
|
3761
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3566
3762
|
var import_types14 = require("@neat.is/types");
|
|
3567
3763
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3764
|
+
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3765
|
+
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3568
3766
|
function isLikelyAddress(value) {
|
|
3569
3767
|
if (!value) return false;
|
|
3570
3768
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3571
3769
|
}
|
|
3770
|
+
function normaliseForMatch(s) {
|
|
3771
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3772
|
+
}
|
|
3773
|
+
function readImports(content) {
|
|
3774
|
+
const awsSdkSuffixes = /* @__PURE__ */ new Map();
|
|
3775
|
+
AWS_SDK_IMPORT_RE.lastIndex = 0;
|
|
3776
|
+
let m;
|
|
3777
|
+
while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
|
|
3778
|
+
const raw = m[1];
|
|
3779
|
+
awsSdkSuffixes.set(normaliseForMatch(raw), raw);
|
|
3780
|
+
}
|
|
3781
|
+
return {
|
|
3782
|
+
awsSdkSuffixes,
|
|
3783
|
+
hasGrpcImport: GRPC_IMPORT_RE.test(content)
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
function classifyClient(symbol, ctx) {
|
|
3787
|
+
const key = normaliseForMatch(symbol);
|
|
3788
|
+
const awsRaw = ctx.awsSdkSuffixes.get(key);
|
|
3789
|
+
if (awsRaw) return { kind: `aws-${awsRaw}` };
|
|
3790
|
+
if (ctx.hasGrpcImport) return { kind: "grpc-service" };
|
|
3791
|
+
return null;
|
|
3792
|
+
}
|
|
3572
3793
|
function grpcEndpointsFromFile(file, serviceDir) {
|
|
3573
3794
|
const out = [];
|
|
3574
3795
|
const seen = /* @__PURE__ */ new Set();
|
|
3796
|
+
const ctx = readImports(file.content);
|
|
3575
3797
|
GRPC_CLIENT_RE.lastIndex = 0;
|
|
3576
3798
|
let m;
|
|
3577
3799
|
while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
|
|
@@ -3579,15 +3801,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3579
3801
|
const addr = m[2]?.trim();
|
|
3580
3802
|
const name = isLikelyAddress(addr) ? addr : symbol;
|
|
3581
3803
|
if (seen.has(name)) continue;
|
|
3804
|
+
const classified = classifyClient(symbol, ctx);
|
|
3805
|
+
if (!classified) continue;
|
|
3582
3806
|
seen.add(name);
|
|
3807
|
+
const { kind } = classified;
|
|
3583
3808
|
const line = lineOf(file.content, m[0]);
|
|
3584
3809
|
out.push({
|
|
3585
|
-
infraId: (0, import_types14.infraId)(
|
|
3810
|
+
infraId: (0, import_types14.infraId)(kind, name),
|
|
3586
3811
|
name,
|
|
3587
|
-
kind
|
|
3812
|
+
kind,
|
|
3588
3813
|
edgeType: "CALLS",
|
|
3814
|
+
// `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
|
|
3815
|
+
// context — import-aware classification per #238. Verified-call-site
|
|
3816
|
+
// tier (ADR-066).
|
|
3817
|
+
confidenceKind: "verified-call-site",
|
|
3589
3818
|
evidence: {
|
|
3590
|
-
file:
|
|
3819
|
+
file: import_node_path24.default.relative(serviceDir, file.path),
|
|
3591
3820
|
line,
|
|
3592
3821
|
snippet: snippet(file.content, line)
|
|
3593
3822
|
}
|
|
@@ -3607,6 +3836,9 @@ function edgeTypeFromEndpoint(ep) {
|
|
|
3607
3836
|
return import_types15.EdgeType.CALLS;
|
|
3608
3837
|
}
|
|
3609
3838
|
}
|
|
3839
|
+
function isAwsKind(kind) {
|
|
3840
|
+
return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
|
|
3841
|
+
}
|
|
3610
3842
|
async function addExternalEndpointEdges(graph, services) {
|
|
3611
3843
|
let nodesAdded = 0;
|
|
3612
3844
|
let edgesAdded = 0;
|
|
@@ -3614,10 +3846,13 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3614
3846
|
const files = await loadSourceFiles(service.dir);
|
|
3615
3847
|
const endpoints = [];
|
|
3616
3848
|
for (const file of files) {
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
endpoints.push(...
|
|
3849
|
+
if (isTestPath(file.path)) continue;
|
|
3850
|
+
const masked = maskCommentsInSource(file.content);
|
|
3851
|
+
const maskedFile = { path: file.path, content: masked };
|
|
3852
|
+
endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
|
|
3853
|
+
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
3854
|
+
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
3855
|
+
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
3621
3856
|
}
|
|
3622
3857
|
if (endpoints.length === 0) continue;
|
|
3623
3858
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -3627,7 +3862,10 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3627
3862
|
id: ep.infraId,
|
|
3628
3863
|
type: import_types15.NodeType.InfraNode,
|
|
3629
3864
|
name: ep.name,
|
|
3630
|
-
|
|
3865
|
+
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3866
|
+
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
3867
|
+
// bucket / table kinds from aws.ts.
|
|
3868
|
+
provider: isAwsKind(ep.kind) ? "aws" : "self",
|
|
3631
3869
|
kind: ep.kind
|
|
3632
3870
|
};
|
|
3633
3871
|
graph.addNode(node.id, node);
|
|
@@ -3637,6 +3875,18 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3637
3875
|
const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
|
|
3638
3876
|
if (seenEdges.has(edgeId)) continue;
|
|
3639
3877
|
seenEdges.add(edgeId);
|
|
3878
|
+
const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
|
|
3879
|
+
if (!(0, import_types15.passesExtractedFloor)(confidence)) {
|
|
3880
|
+
noteExtractedDropped({
|
|
3881
|
+
source: service.node.id,
|
|
3882
|
+
target: ep.infraId,
|
|
3883
|
+
type: edgeType,
|
|
3884
|
+
confidence,
|
|
3885
|
+
confidenceKind: ep.confidenceKind,
|
|
3886
|
+
evidence: ep.evidence
|
|
3887
|
+
});
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3640
3890
|
if (!graph.hasEdge(edgeId)) {
|
|
3641
3891
|
const edge = {
|
|
3642
3892
|
id: edgeId,
|
|
@@ -3644,6 +3894,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3644
3894
|
target: ep.infraId,
|
|
3645
3895
|
type: edgeType,
|
|
3646
3896
|
provenance: import_types15.Provenance.EXTRACTED,
|
|
3897
|
+
confidence,
|
|
3647
3898
|
evidence: ep.evidence
|
|
3648
3899
|
};
|
|
3649
3900
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3667,7 +3918,7 @@ init_cjs_shims();
|
|
|
3667
3918
|
|
|
3668
3919
|
// src/extract/infra/docker-compose.ts
|
|
3669
3920
|
init_cjs_shims();
|
|
3670
|
-
var
|
|
3921
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3671
3922
|
var import_types17 = require("@neat.is/types");
|
|
3672
3923
|
|
|
3673
3924
|
// src/extract/infra/shared.ts
|
|
@@ -3705,7 +3956,7 @@ function dependsOnList(value) {
|
|
|
3705
3956
|
}
|
|
3706
3957
|
function serviceNameToServiceNode(name, services) {
|
|
3707
3958
|
for (const s of services) {
|
|
3708
|
-
if (s.node.name === name ||
|
|
3959
|
+
if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
|
|
3709
3960
|
}
|
|
3710
3961
|
return null;
|
|
3711
3962
|
}
|
|
@@ -3714,7 +3965,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3714
3965
|
let edgesAdded = 0;
|
|
3715
3966
|
let composePath = null;
|
|
3716
3967
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3717
|
-
const abs =
|
|
3968
|
+
const abs = import_node_path25.default.join(scanPath, name);
|
|
3718
3969
|
if (await exists(abs)) {
|
|
3719
3970
|
composePath = abs;
|
|
3720
3971
|
break;
|
|
@@ -3725,13 +3976,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3725
3976
|
try {
|
|
3726
3977
|
compose = await readYaml(composePath);
|
|
3727
3978
|
} catch (err) {
|
|
3728
|
-
|
|
3729
|
-
|
|
3979
|
+
recordExtractionError(
|
|
3980
|
+
"infra docker-compose",
|
|
3981
|
+
import_node_path25.default.relative(scanPath, composePath),
|
|
3982
|
+
err
|
|
3730
3983
|
);
|
|
3731
3984
|
return { nodesAdded, edgesAdded };
|
|
3732
3985
|
}
|
|
3733
3986
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3734
|
-
const evidenceFile =
|
|
3987
|
+
const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
|
|
3735
3988
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3736
3989
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3737
3990
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3761,6 +4014,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3761
4014
|
target: targetId,
|
|
3762
4015
|
type: import_types17.EdgeType.DEPENDS_ON,
|
|
3763
4016
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
4017
|
+
confidence: (0, import_types17.confidenceForExtracted)("structural"),
|
|
3764
4018
|
evidence: { file: evidenceFile }
|
|
3765
4019
|
};
|
|
3766
4020
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3772,8 +4026,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3772
4026
|
|
|
3773
4027
|
// src/extract/infra/dockerfile.ts
|
|
3774
4028
|
init_cjs_shims();
|
|
3775
|
-
var
|
|
3776
|
-
var
|
|
4029
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
4030
|
+
var import_node_fs14 = require("fs");
|
|
3777
4031
|
var import_types18 = require("@neat.is/types");
|
|
3778
4032
|
function runtimeImage(content) {
|
|
3779
4033
|
const lines = content.split("\n");
|
|
@@ -3793,14 +4047,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3793
4047
|
let nodesAdded = 0;
|
|
3794
4048
|
let edgesAdded = 0;
|
|
3795
4049
|
for (const service of services) {
|
|
3796
|
-
const dockerfilePath =
|
|
4050
|
+
const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
|
|
3797
4051
|
if (!await exists(dockerfilePath)) continue;
|
|
3798
4052
|
let content;
|
|
3799
4053
|
try {
|
|
3800
|
-
content = await
|
|
4054
|
+
content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
|
|
3801
4055
|
} catch (err) {
|
|
3802
|
-
|
|
3803
|
-
|
|
4056
|
+
recordExtractionError(
|
|
4057
|
+
"infra dockerfile",
|
|
4058
|
+
import_node_path26.default.relative(scanPath, dockerfilePath),
|
|
4059
|
+
err
|
|
3804
4060
|
);
|
|
3805
4061
|
continue;
|
|
3806
4062
|
}
|
|
@@ -3819,8 +4075,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3819
4075
|
target: node.id,
|
|
3820
4076
|
type: import_types18.EdgeType.RUNS_ON,
|
|
3821
4077
|
provenance: import_types18.Provenance.EXTRACTED,
|
|
4078
|
+
confidence: (0, import_types18.confidenceForExtracted)("structural"),
|
|
3822
4079
|
evidence: {
|
|
3823
|
-
file:
|
|
4080
|
+
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
3824
4081
|
}
|
|
3825
4082
|
};
|
|
3826
4083
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3832,19 +4089,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3832
4089
|
|
|
3833
4090
|
// src/extract/infra/terraform.ts
|
|
3834
4091
|
init_cjs_shims();
|
|
3835
|
-
var
|
|
3836
|
-
var
|
|
4092
|
+
var import_node_fs15 = require("fs");
|
|
4093
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3837
4094
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3838
4095
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3839
4096
|
if (depth > max) return [];
|
|
3840
4097
|
const out = [];
|
|
3841
|
-
const entries = await
|
|
4098
|
+
const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3842
4099
|
for (const entry of entries) {
|
|
3843
4100
|
if (entry.isDirectory()) {
|
|
3844
4101
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3845
|
-
out.push(...await walkTfFiles(
|
|
4102
|
+
out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
|
|
3846
4103
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3847
|
-
out.push(
|
|
4104
|
+
out.push(import_node_path27.default.join(start, entry.name));
|
|
3848
4105
|
}
|
|
3849
4106
|
}
|
|
3850
4107
|
return out;
|
|
@@ -3853,7 +4110,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3853
4110
|
let nodesAdded = 0;
|
|
3854
4111
|
const files = await walkTfFiles(scanPath);
|
|
3855
4112
|
for (const file of files) {
|
|
3856
|
-
const content = await
|
|
4113
|
+
const content = await import_node_fs15.promises.readFile(file, "utf8");
|
|
3857
4114
|
RESOURCE_RE.lastIndex = 0;
|
|
3858
4115
|
let m;
|
|
3859
4116
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3871,8 +4128,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3871
4128
|
|
|
3872
4129
|
// src/extract/infra/k8s.ts
|
|
3873
4130
|
init_cjs_shims();
|
|
3874
|
-
var
|
|
3875
|
-
var
|
|
4131
|
+
var import_node_fs16 = require("fs");
|
|
4132
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3876
4133
|
var import_yaml3 = require("yaml");
|
|
3877
4134
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3878
4135
|
Service: "k8s-service",
|
|
@@ -3886,13 +4143,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3886
4143
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3887
4144
|
if (depth > max) return [];
|
|
3888
4145
|
const out = [];
|
|
3889
|
-
const entries = await
|
|
4146
|
+
const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3890
4147
|
for (const entry of entries) {
|
|
3891
4148
|
if (entry.isDirectory()) {
|
|
3892
4149
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3893
|
-
out.push(...await walkYamlFiles2(
|
|
3894
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3895
|
-
out.push(
|
|
4150
|
+
out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
|
|
4151
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
|
|
4152
|
+
out.push(import_node_path28.default.join(start, entry.name));
|
|
3896
4153
|
}
|
|
3897
4154
|
}
|
|
3898
4155
|
return out;
|
|
@@ -3901,7 +4158,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3901
4158
|
let nodesAdded = 0;
|
|
3902
4159
|
const files = await walkYamlFiles2(scanPath);
|
|
3903
4160
|
for (const file of files) {
|
|
3904
|
-
const content = await
|
|
4161
|
+
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
3905
4162
|
let docs;
|
|
3906
4163
|
try {
|
|
3907
4164
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -3935,9 +4192,37 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3935
4192
|
};
|
|
3936
4193
|
}
|
|
3937
4194
|
|
|
4195
|
+
// src/extract/index.ts
|
|
4196
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4197
|
+
|
|
4198
|
+
// src/extract/retire.ts
|
|
4199
|
+
init_cjs_shims();
|
|
4200
|
+
var import_node_fs17 = require("fs");
|
|
4201
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
4202
|
+
var import_types19 = require("@neat.is/types");
|
|
4203
|
+
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
4204
|
+
const toDrop = [];
|
|
4205
|
+
const bases = [scanPath, ...serviceDirs];
|
|
4206
|
+
graph.forEachEdge((id, attrs) => {
|
|
4207
|
+
const edge = attrs;
|
|
4208
|
+
if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
|
|
4209
|
+
const evidenceFile = edge.evidence?.file;
|
|
4210
|
+
if (!evidenceFile) return;
|
|
4211
|
+
if (import_node_path29.default.isAbsolute(evidenceFile)) {
|
|
4212
|
+
if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
|
|
4216
|
+
if (!found) toDrop.push(id);
|
|
4217
|
+
});
|
|
4218
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
4219
|
+
return toDrop.length;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
3938
4222
|
// src/extract/index.ts
|
|
3939
4223
|
async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
3940
4224
|
await ensureCompatLoaded();
|
|
4225
|
+
drainExtractionErrors();
|
|
3941
4226
|
const services = await discoverServices(scanPath);
|
|
3942
4227
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
3943
4228
|
await addServiceAliases(graph, scanPath, services);
|
|
@@ -3945,12 +4230,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3945
4230
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
3946
4231
|
const phase4 = await addCallEdges(graph, services);
|
|
3947
4232
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
4233
|
+
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
4234
|
+
graph,
|
|
4235
|
+
scanPath,
|
|
4236
|
+
services.map((s) => s.dir)
|
|
4237
|
+
);
|
|
3948
4238
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3949
4239
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
4240
|
+
const errorEntries = drainExtractionErrors();
|
|
4241
|
+
if (opts.errorsPath && errorEntries.length > 0) {
|
|
4242
|
+
try {
|
|
4243
|
+
await writeExtractionErrors(errorEntries, opts.errorsPath);
|
|
4244
|
+
} catch (err) {
|
|
4245
|
+
console.warn(
|
|
4246
|
+
`[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
|
|
4247
|
+
);
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
const droppedEntries = drainDroppedExtracted();
|
|
4251
|
+
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
4252
|
+
const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
4253
|
+
try {
|
|
4254
|
+
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
4255
|
+
} catch (err) {
|
|
4256
|
+
console.warn(
|
|
4257
|
+
`[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
3950
4261
|
const result = {
|
|
3951
4262
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3952
4263
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3953
|
-
frontiersPromoted
|
|
4264
|
+
frontiersPromoted,
|
|
4265
|
+
extractionErrors: errorEntries.length,
|
|
4266
|
+
errorEntries,
|
|
4267
|
+
ghostsRetired,
|
|
4268
|
+
extractedDropped: droppedEntries.length,
|
|
4269
|
+
droppedEntries
|
|
3954
4270
|
};
|
|
3955
4271
|
emitNeatEvent({
|
|
3956
4272
|
type: "extraction-complete",
|
|
@@ -3967,7 +4283,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3967
4283
|
|
|
3968
4284
|
// src/diff.ts
|
|
3969
4285
|
init_cjs_shims();
|
|
3970
|
-
var
|
|
4286
|
+
var import_node_fs18 = require("fs");
|
|
3971
4287
|
async function loadSnapshotForDiff(target) {
|
|
3972
4288
|
if (/^https?:\/\//i.test(target)) {
|
|
3973
4289
|
const res = await fetch(target);
|
|
@@ -3976,7 +4292,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
3976
4292
|
}
|
|
3977
4293
|
return await res.json();
|
|
3978
4294
|
}
|
|
3979
|
-
const raw = await
|
|
4295
|
+
const raw = await import_node_fs18.promises.readFile(target, "utf8");
|
|
3980
4296
|
return JSON.parse(raw);
|
|
3981
4297
|
}
|
|
3982
4298
|
function indexEntries(entries) {
|
|
@@ -4044,23 +4360,23 @@ function canonicalJson(value) {
|
|
|
4044
4360
|
|
|
4045
4361
|
// src/projects.ts
|
|
4046
4362
|
init_cjs_shims();
|
|
4047
|
-
var
|
|
4363
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4048
4364
|
function pathsForProject(project, baseDir) {
|
|
4049
4365
|
if (project === DEFAULT_PROJECT) {
|
|
4050
4366
|
return {
|
|
4051
|
-
snapshotPath:
|
|
4052
|
-
errorsPath:
|
|
4053
|
-
staleEventsPath:
|
|
4054
|
-
embeddingsCachePath:
|
|
4055
|
-
policyViolationsPath:
|
|
4367
|
+
snapshotPath: import_node_path31.default.join(baseDir, "graph.json"),
|
|
4368
|
+
errorsPath: import_node_path31.default.join(baseDir, "errors.ndjson"),
|
|
4369
|
+
staleEventsPath: import_node_path31.default.join(baseDir, "stale-events.ndjson"),
|
|
4370
|
+
embeddingsCachePath: import_node_path31.default.join(baseDir, "embeddings.json"),
|
|
4371
|
+
policyViolationsPath: import_node_path31.default.join(baseDir, "policy-violations.ndjson")
|
|
4056
4372
|
};
|
|
4057
4373
|
}
|
|
4058
4374
|
return {
|
|
4059
|
-
snapshotPath:
|
|
4060
|
-
errorsPath:
|
|
4061
|
-
staleEventsPath:
|
|
4062
|
-
embeddingsCachePath:
|
|
4063
|
-
policyViolationsPath:
|
|
4375
|
+
snapshotPath: import_node_path31.default.join(baseDir, `${project}.json`),
|
|
4376
|
+
errorsPath: import_node_path31.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4377
|
+
staleEventsPath: import_node_path31.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4378
|
+
embeddingsCachePath: import_node_path31.default.join(baseDir, `embeddings.${project}.json`),
|
|
4379
|
+
policyViolationsPath: import_node_path31.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4064
4380
|
};
|
|
4065
4381
|
}
|
|
4066
4382
|
var Projects = class {
|
|
@@ -4100,23 +4416,23 @@ function parseExtraProjects(raw) {
|
|
|
4100
4416
|
|
|
4101
4417
|
// src/registry.ts
|
|
4102
4418
|
init_cjs_shims();
|
|
4103
|
-
var
|
|
4419
|
+
var import_node_fs19 = require("fs");
|
|
4104
4420
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4105
|
-
var
|
|
4106
|
-
var
|
|
4421
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4422
|
+
var import_types20 = require("@neat.is/types");
|
|
4107
4423
|
function neatHome() {
|
|
4108
4424
|
const override = process.env.NEAT_HOME;
|
|
4109
|
-
if (override && override.length > 0) return
|
|
4110
|
-
return
|
|
4425
|
+
if (override && override.length > 0) return import_node_path32.default.resolve(override);
|
|
4426
|
+
return import_node_path32.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4111
4427
|
}
|
|
4112
4428
|
function registryPath() {
|
|
4113
|
-
return
|
|
4429
|
+
return import_node_path32.default.join(neatHome(), "projects.json");
|
|
4114
4430
|
}
|
|
4115
4431
|
async function readRegistry() {
|
|
4116
4432
|
const file = registryPath();
|
|
4117
4433
|
let raw;
|
|
4118
4434
|
try {
|
|
4119
|
-
raw = await
|
|
4435
|
+
raw = await import_node_fs19.promises.readFile(file, "utf8");
|
|
4120
4436
|
} catch (err) {
|
|
4121
4437
|
if (err.code === "ENOENT") {
|
|
4122
4438
|
return { version: 1, projects: [] };
|
|
@@ -4124,7 +4440,7 @@ async function readRegistry() {
|
|
|
4124
4440
|
throw err;
|
|
4125
4441
|
}
|
|
4126
4442
|
const parsed = JSON.parse(raw);
|
|
4127
|
-
return
|
|
4443
|
+
return import_types20.RegistryFileSchema.parse(parsed);
|
|
4128
4444
|
}
|
|
4129
4445
|
async function getProject(name) {
|
|
4130
4446
|
const reg = await readRegistry();
|
|
@@ -4314,11 +4630,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4314
4630
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4315
4631
|
const parsed = [];
|
|
4316
4632
|
for (const c of candidates) {
|
|
4317
|
-
const r =
|
|
4633
|
+
const r = import_types21.DivergenceTypeSchema.safeParse(c);
|
|
4318
4634
|
if (!r.success) {
|
|
4319
4635
|
return reply.code(400).send({
|
|
4320
4636
|
error: `unknown divergence type "${c}"`,
|
|
4321
|
-
allowed:
|
|
4637
|
+
allowed: import_types21.DivergenceTypeSchema.options
|
|
4322
4638
|
});
|
|
4323
4639
|
}
|
|
4324
4640
|
parsed.push(r.data);
|
|
@@ -4502,7 +4818,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4502
4818
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4503
4819
|
let violations = await log.readAll();
|
|
4504
4820
|
if (req.query.severity) {
|
|
4505
|
-
const sev =
|
|
4821
|
+
const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4506
4822
|
if (!sev.success) {
|
|
4507
4823
|
return reply.code(400).send({
|
|
4508
4824
|
error: "invalid severity",
|
|
@@ -4519,7 +4835,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4519
4835
|
scope.post("/policies/check", async (req, reply) => {
|
|
4520
4836
|
const proj = resolveProject(registry, req, reply);
|
|
4521
4837
|
if (!proj) return;
|
|
4522
|
-
const parsed =
|
|
4838
|
+
const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4523
4839
|
if (!parsed.success) {
|
|
4524
4840
|
return reply.code(400).send({
|
|
4525
4841
|
error: "invalid /policies/check body",
|
|
@@ -4619,8 +4935,8 @@ async function buildApi(opts) {
|
|
|
4619
4935
|
|
|
4620
4936
|
// src/persist.ts
|
|
4621
4937
|
init_cjs_shims();
|
|
4622
|
-
var
|
|
4623
|
-
var
|
|
4938
|
+
var import_node_fs20 = require("fs");
|
|
4939
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4624
4940
|
var SCHEMA_VERSION = 2;
|
|
4625
4941
|
function migrateV1ToV2(payload) {
|
|
4626
4942
|
const nodes = payload.graph.nodes;
|
|
@@ -4634,7 +4950,7 @@ function migrateV1ToV2(payload) {
|
|
|
4634
4950
|
return { ...payload, schemaVersion: 2 };
|
|
4635
4951
|
}
|
|
4636
4952
|
async function ensureDir(filePath) {
|
|
4637
|
-
await
|
|
4953
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
|
|
4638
4954
|
}
|
|
4639
4955
|
async function saveGraphToDisk(graph, outPath) {
|
|
4640
4956
|
await ensureDir(outPath);
|
|
@@ -4644,13 +4960,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
4644
4960
|
graph: graph.export()
|
|
4645
4961
|
};
|
|
4646
4962
|
const tmp = `${outPath}.tmp`;
|
|
4647
|
-
await
|
|
4648
|
-
await
|
|
4963
|
+
await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4964
|
+
await import_node_fs20.promises.rename(tmp, outPath);
|
|
4649
4965
|
}
|
|
4650
4966
|
async function loadGraphFromDisk(graph, outPath) {
|
|
4651
4967
|
let raw;
|
|
4652
4968
|
try {
|
|
4653
|
-
raw = await
|
|
4969
|
+
raw = await import_node_fs20.promises.readFile(outPath, "utf8");
|
|
4654
4970
|
} catch (err) {
|
|
4655
4971
|
if (err.code === "ENOENT") return;
|
|
4656
4972
|
throw err;
|
|
@@ -4707,8 +5023,8 @@ init_otel_grpc();
|
|
|
4707
5023
|
|
|
4708
5024
|
// src/search.ts
|
|
4709
5025
|
init_cjs_shims();
|
|
4710
|
-
var
|
|
4711
|
-
var
|
|
5026
|
+
var import_node_fs21 = require("fs");
|
|
5027
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
4712
5028
|
var import_node_crypto = require("crypto");
|
|
4713
5029
|
var DEFAULT_LIMIT = 10;
|
|
4714
5030
|
var NOMIC_DIM = 768;
|
|
@@ -4838,7 +5154,7 @@ async function pickEmbedder() {
|
|
|
4838
5154
|
}
|
|
4839
5155
|
async function readCache(cachePath) {
|
|
4840
5156
|
try {
|
|
4841
|
-
const raw = await
|
|
5157
|
+
const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
|
|
4842
5158
|
const parsed = JSON.parse(raw);
|
|
4843
5159
|
if (parsed.version !== 1) return null;
|
|
4844
5160
|
return parsed;
|
|
@@ -4847,8 +5163,8 @@ async function readCache(cachePath) {
|
|
|
4847
5163
|
}
|
|
4848
5164
|
}
|
|
4849
5165
|
async function writeCache(cachePath, cache) {
|
|
4850
|
-
await
|
|
4851
|
-
await
|
|
5166
|
+
await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(cachePath), { recursive: true });
|
|
5167
|
+
await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
4852
5168
|
}
|
|
4853
5169
|
var VectorIndex = class {
|
|
4854
5170
|
constructor(embedder, cachePath) {
|
|
@@ -5027,14 +5343,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
|
|
|
5027
5343
|
async function main() {
|
|
5028
5344
|
const baseDirEnv = process.env.NEAT_OUT_DIR;
|
|
5029
5345
|
const legacyOutPath = process.env.NEAT_OUT_PATH;
|
|
5030
|
-
const baseDir = baseDirEnv ?
|
|
5031
|
-
const defaultScanPath =
|
|
5346
|
+
const baseDir = baseDirEnv ? import_node_path37.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path37.default.resolve(import_node_path37.default.dirname(legacyOutPath)) : import_node_path37.default.resolve("./neat-out");
|
|
5347
|
+
const defaultScanPath = import_node_path37.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
|
|
5032
5348
|
const registry = new Projects();
|
|
5033
5349
|
await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
|
|
5034
5350
|
for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
|
|
5035
5351
|
const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
5036
5352
|
const projectScan = process.env[envKey];
|
|
5037
|
-
await bootProject(registry, name, projectScan ?
|
|
5353
|
+
await bootProject(registry, name, projectScan ? import_node_path37.default.resolve(projectScan) : void 0, baseDir);
|
|
5038
5354
|
}
|
|
5039
5355
|
const host = process.env.HOST ?? "0.0.0.0";
|
|
5040
5356
|
const port = Number(process.env.PORT ?? 8080);
|