@neat.is/core 0.10.5 → 0.10.6-dev.20261007
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-DVCGDMTZ.js → chunk-KTJK2PAH.js} +1443 -603
- package/dist/chunk-KTJK2PAH.js.map +1 -0
- package/dist/{chunk-Q2SL2SVH.js → chunk-VL7ANDPI.js} +35 -218
- package/dist/chunk-VL7ANDPI.js.map +1 -0
- package/dist/cli.cjs +1766 -1105
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.cjs +1596 -938
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +1609 -951
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +1307 -637
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-DVCGDMTZ.js.map +0 -1
- package/dist/chunk-Q2SL2SVH.js.map +0 -1
|
@@ -343,13 +343,35 @@ function deprecatedApis() {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
// src/ingest.ts
|
|
346
|
-
import { promises as
|
|
347
|
-
import
|
|
348
|
-
import * as sourceMapJs from "source-map-js";
|
|
346
|
+
import { promises as fs9, existsSync, readFileSync } from "fs";
|
|
347
|
+
import path10 from "path";
|
|
349
348
|
|
|
350
|
-
// src/
|
|
349
|
+
// src/durable-incident.ts
|
|
351
350
|
import { promises as fs2 } from "fs";
|
|
352
351
|
import path2 from "path";
|
|
352
|
+
async function appendRuntimeIncident(errorsPath, project, event, options = {}) {
|
|
353
|
+
const token = options.token ?? process.env.NEAT_INCIDENT_DURABLE_TOKEN;
|
|
354
|
+
if (token !== void 0) {
|
|
355
|
+
if (Buffer.byteLength(token, "utf8") < 32) throw new Error("durable incident sink unavailable");
|
|
356
|
+
const response = await (options.fetchImpl ?? fetch)("http://127.0.0.1:8081/incident-append", {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
359
|
+
body: JSON.stringify({ project, event }),
|
|
360
|
+
signal: AbortSignal.timeout(3e4)
|
|
361
|
+
});
|
|
362
|
+
if (response.status !== 204) throw new Error("durable incident sink unavailable");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
await fs2.mkdir(path2.dirname(errorsPath), { recursive: true });
|
|
366
|
+
await fs2.appendFile(errorsPath, JSON.stringify(event) + "\n", "utf8");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/ingest.ts
|
|
370
|
+
import * as sourceMapJs from "source-map-js";
|
|
371
|
+
|
|
372
|
+
// src/policy.ts
|
|
373
|
+
import { promises as fs3 } from "fs";
|
|
374
|
+
import path3 from "path";
|
|
353
375
|
import {
|
|
354
376
|
EdgeType,
|
|
355
377
|
NodeType,
|
|
@@ -871,7 +893,7 @@ function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
|
871
893
|
async function loadPolicyFile(policyPath) {
|
|
872
894
|
let raw;
|
|
873
895
|
try {
|
|
874
|
-
raw = await
|
|
896
|
+
raw = await fs3.readFile(policyPath, "utf8");
|
|
875
897
|
} catch (err) {
|
|
876
898
|
if (err.code === "ENOENT") return [];
|
|
877
899
|
throw err;
|
|
@@ -892,8 +914,8 @@ var PolicyViolationsLog = class {
|
|
|
892
914
|
if (!this.seen) await this.hydrate();
|
|
893
915
|
if (this.seen.has(v.id)) return false;
|
|
894
916
|
this.seen.add(v.id);
|
|
895
|
-
await
|
|
896
|
-
await
|
|
917
|
+
await fs3.mkdir(path3.dirname(this.path), { recursive: true });
|
|
918
|
+
await fs3.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
897
919
|
emitNeatEvent({
|
|
898
920
|
type: "policy-violation",
|
|
899
921
|
project: this.project,
|
|
@@ -903,7 +925,7 @@ var PolicyViolationsLog = class {
|
|
|
903
925
|
}
|
|
904
926
|
async readAll() {
|
|
905
927
|
try {
|
|
906
|
-
const raw = await
|
|
928
|
+
const raw = await fs3.readFile(this.path, "utf8");
|
|
907
929
|
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
908
930
|
} catch (err) {
|
|
909
931
|
if (err.code === "ENOENT") return [];
|
|
@@ -943,7 +965,7 @@ import {
|
|
|
943
965
|
} from "@neat.is/types";
|
|
944
966
|
|
|
945
967
|
// src/extract/routes.ts
|
|
946
|
-
import
|
|
968
|
+
import path9 from "path";
|
|
947
969
|
import Parser2 from "tree-sitter";
|
|
948
970
|
import JavaScript2 from "tree-sitter-javascript";
|
|
949
971
|
import Python2 from "tree-sitter-python";
|
|
@@ -961,8 +983,8 @@ import {
|
|
|
961
983
|
} from "@neat.is/types";
|
|
962
984
|
|
|
963
985
|
// src/extract/shared.ts
|
|
964
|
-
import { promises as
|
|
965
|
-
import
|
|
986
|
+
import { promises as fs4 } from "fs";
|
|
987
|
+
import path4 from "path";
|
|
966
988
|
import { parse as parseYaml } from "yaml";
|
|
967
989
|
import { extractedEdgeId } from "@neat.is/types";
|
|
968
990
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php", ".cs", ".java", ".kt", ".rs", ".cpp", ".cc", ".cxx", ".c++", ".hpp", ".hh", ".hxx", ".h++"]);
|
|
@@ -1006,7 +1028,7 @@ async function isPythonVenvDir(dir) {
|
|
|
1006
1028
|
const cached = PYVENV_MARKER_CACHE.get(dir);
|
|
1007
1029
|
if (cached !== void 0) return cached;
|
|
1008
1030
|
try {
|
|
1009
|
-
const stat = await
|
|
1031
|
+
const stat = await fs4.stat(path4.join(dir, "pyvenv.cfg"));
|
|
1010
1032
|
const ok = stat.isFile();
|
|
1011
1033
|
PYVENV_MARKER_CACHE.set(dir, ok);
|
|
1012
1034
|
return ok;
|
|
@@ -1016,7 +1038,7 @@ async function isPythonVenvDir(dir) {
|
|
|
1016
1038
|
}
|
|
1017
1039
|
}
|
|
1018
1040
|
function isConfigFile(name) {
|
|
1019
|
-
const ext =
|
|
1041
|
+
const ext = path4.extname(name);
|
|
1020
1042
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
1021
1043
|
if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
|
|
1022
1044
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
@@ -1133,16 +1155,16 @@ function cleanVersion(raw) {
|
|
|
1133
1155
|
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
1134
1156
|
}
|
|
1135
1157
|
async function readJson(filePath) {
|
|
1136
|
-
const raw = await
|
|
1158
|
+
const raw = await fs4.readFile(filePath, "utf8");
|
|
1137
1159
|
return JSON.parse(raw);
|
|
1138
1160
|
}
|
|
1139
1161
|
async function readYaml(filePath) {
|
|
1140
|
-
const raw = await
|
|
1162
|
+
const raw = await fs4.readFile(filePath, "utf8");
|
|
1141
1163
|
return parseYaml(raw);
|
|
1142
1164
|
}
|
|
1143
1165
|
async function exists(p) {
|
|
1144
1166
|
try {
|
|
1145
|
-
await
|
|
1167
|
+
await fs4.access(p);
|
|
1146
1168
|
return true;
|
|
1147
1169
|
} catch {
|
|
1148
1170
|
return false;
|
|
@@ -1150,8 +1172,8 @@ async function exists(p) {
|
|
|
1150
1172
|
}
|
|
1151
1173
|
|
|
1152
1174
|
// src/extract/errors.ts
|
|
1153
|
-
import { promises as
|
|
1154
|
-
import
|
|
1175
|
+
import { promises as fs5 } from "fs";
|
|
1176
|
+
import path5 from "path";
|
|
1155
1177
|
var sink = [];
|
|
1156
1178
|
function recordExtractionError(producer, file, err) {
|
|
1157
1179
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
@@ -1170,14 +1192,14 @@ function drainExtractionErrors() {
|
|
|
1170
1192
|
}
|
|
1171
1193
|
async function writeExtractionErrors(errors, errorsPath) {
|
|
1172
1194
|
if (errors.length === 0) return;
|
|
1173
|
-
await
|
|
1195
|
+
await fs5.mkdir(path5.dirname(errorsPath), { recursive: true });
|
|
1174
1196
|
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
1175
|
-
await
|
|
1197
|
+
await fs5.appendFile(errorsPath, lines, "utf8");
|
|
1176
1198
|
}
|
|
1177
1199
|
function extractionHealthPathFor(errorsPath) {
|
|
1178
|
-
const dir =
|
|
1179
|
-
const suffix =
|
|
1180
|
-
return
|
|
1200
|
+
const dir = path5.dirname(errorsPath);
|
|
1201
|
+
const suffix = path5.basename(errorsPath).replace(/^errors/, "").replace(/\.ndjson$/, "");
|
|
1202
|
+
return path5.join(dir, `extraction-health${suffix}.json`);
|
|
1181
1203
|
}
|
|
1182
1204
|
var HEALTH_FILE_SAMPLE = 100;
|
|
1183
1205
|
async function writeExtractionHealth(errors, healthPath, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
@@ -1189,13 +1211,13 @@ async function writeExtractionHealth(errors, healthPath, now = (/* @__PURE__ */
|
|
|
1189
1211
|
files: errors.slice(0, HEALTH_FILE_SAMPLE).map((e) => e.file),
|
|
1190
1212
|
updatedAt: now
|
|
1191
1213
|
};
|
|
1192
|
-
await
|
|
1193
|
-
await
|
|
1214
|
+
await fs5.mkdir(path5.dirname(healthPath), { recursive: true });
|
|
1215
|
+
await fs5.writeFile(healthPath, JSON.stringify(coverage), "utf8");
|
|
1194
1216
|
}
|
|
1195
1217
|
async function readExtractionHealth(healthPath) {
|
|
1196
1218
|
let raw;
|
|
1197
1219
|
try {
|
|
1198
|
-
raw = await
|
|
1220
|
+
raw = await fs5.readFile(healthPath, "utf8");
|
|
1199
1221
|
} catch {
|
|
1200
1222
|
return void 0;
|
|
1201
1223
|
}
|
|
@@ -1244,9 +1266,9 @@ function isRejectedLogEnabled() {
|
|
|
1244
1266
|
}
|
|
1245
1267
|
async function writeRejectedExtracted(drops, rejectedPath) {
|
|
1246
1268
|
if (drops.length === 0) return;
|
|
1247
|
-
await
|
|
1269
|
+
await fs5.mkdir(path5.dirname(rejectedPath), { recursive: true });
|
|
1248
1270
|
const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
|
|
1249
|
-
await
|
|
1271
|
+
await fs5.appendFile(rejectedPath, lines, "utf8");
|
|
1250
1272
|
}
|
|
1251
1273
|
function formatPrecisionFloorBanner(count) {
|
|
1252
1274
|
if (count === 1) return `[neat] 1 extracted edge dropped below precision floor`;
|
|
@@ -1254,8 +1276,8 @@ function formatPrecisionFloorBanner(count) {
|
|
|
1254
1276
|
}
|
|
1255
1277
|
|
|
1256
1278
|
// src/extract/calls/shared.ts
|
|
1257
|
-
import { promises as
|
|
1258
|
-
import
|
|
1279
|
+
import { promises as fs7 } from "fs";
|
|
1280
|
+
import path7 from "path";
|
|
1259
1281
|
import {
|
|
1260
1282
|
EdgeType as EdgeType2,
|
|
1261
1283
|
NodeType as NodeType2,
|
|
@@ -1266,8 +1288,8 @@ import {
|
|
|
1266
1288
|
} from "@neat.is/types";
|
|
1267
1289
|
|
|
1268
1290
|
// src/extract/gitignore.ts
|
|
1269
|
-
import { promises as
|
|
1270
|
-
import
|
|
1291
|
+
import { promises as fs6 } from "fs";
|
|
1292
|
+
import path6 from "path";
|
|
1271
1293
|
import ignore from "ignore";
|
|
1272
1294
|
var LAYER_CACHE = /* @__PURE__ */ new Map();
|
|
1273
1295
|
async function layerFor(dir) {
|
|
@@ -1275,7 +1297,7 @@ async function layerFor(dir) {
|
|
|
1275
1297
|
if (cached !== void 0) return cached;
|
|
1276
1298
|
let layer = null;
|
|
1277
1299
|
try {
|
|
1278
|
-
const raw = await
|
|
1300
|
+
const raw = await fs6.readFile(path6.join(dir, ".gitignore"), "utf8");
|
|
1279
1301
|
layer = { dir, ig: ignore().add(raw) };
|
|
1280
1302
|
} catch {
|
|
1281
1303
|
layer = null;
|
|
@@ -1285,7 +1307,7 @@ async function layerFor(dir) {
|
|
|
1285
1307
|
}
|
|
1286
1308
|
async function isRepoRoot(dir) {
|
|
1287
1309
|
try {
|
|
1288
|
-
await
|
|
1310
|
+
await fs6.stat(path6.join(dir, ".git"));
|
|
1289
1311
|
return true;
|
|
1290
1312
|
} catch {
|
|
1291
1313
|
return false;
|
|
@@ -1293,11 +1315,11 @@ async function isRepoRoot(dir) {
|
|
|
1293
1315
|
}
|
|
1294
1316
|
async function loadIgnoreChain(startDir) {
|
|
1295
1317
|
const dirs = [];
|
|
1296
|
-
let current =
|
|
1318
|
+
let current = path6.resolve(startDir);
|
|
1297
1319
|
for (; ; ) {
|
|
1298
1320
|
dirs.push(current);
|
|
1299
1321
|
if (await isRepoRoot(current)) break;
|
|
1300
|
-
const parent =
|
|
1322
|
+
const parent = path6.dirname(current);
|
|
1301
1323
|
if (parent === current) break;
|
|
1302
1324
|
current = parent;
|
|
1303
1325
|
}
|
|
@@ -1310,7 +1332,7 @@ async function loadIgnoreChain(startDir) {
|
|
|
1310
1332
|
return layers;
|
|
1311
1333
|
}
|
|
1312
1334
|
async function extendIgnoreChain(chain, dir) {
|
|
1313
|
-
const layer = await layerFor(
|
|
1335
|
+
const layer = await layerFor(path6.resolve(dir));
|
|
1314
1336
|
return layer ? [...chain, layer] : chain;
|
|
1315
1337
|
}
|
|
1316
1338
|
function isIgnored(chain, fullPath, isDir) {
|
|
@@ -1318,14 +1340,14 @@ function isIgnored(chain, fullPath, isDir) {
|
|
|
1318
1340
|
}
|
|
1319
1341
|
function decide(chain, fullPath, isDir) {
|
|
1320
1342
|
if (chain.length === 0) return { ignored: false, subtree: chain };
|
|
1321
|
-
const abs =
|
|
1343
|
+
const abs = path6.resolve(fullPath);
|
|
1322
1344
|
let ignored = false;
|
|
1323
1345
|
let unignoredAt = -1;
|
|
1324
1346
|
for (let i = 0; i < chain.length; i++) {
|
|
1325
1347
|
const layer = chain[i];
|
|
1326
|
-
const rel =
|
|
1327
|
-
if (!rel || rel.startsWith("..") ||
|
|
1328
|
-
const candidate = rel.split(
|
|
1348
|
+
const rel = path6.relative(layer.dir, abs);
|
|
1349
|
+
if (!rel || rel.startsWith("..") || path6.isAbsolute(rel)) continue;
|
|
1350
|
+
const candidate = rel.split(path6.sep).join("/") + (isDir ? "/" : "");
|
|
1329
1351
|
const result = layer.ig.test(candidate);
|
|
1330
1352
|
if (result.ignored) ignored = true;
|
|
1331
1353
|
else if (result.unignored) {
|
|
@@ -1344,7 +1366,7 @@ function buildServiceHostIndex(services) {
|
|
|
1344
1366
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
1345
1367
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
1346
1368
|
for (const service of services) {
|
|
1347
|
-
const base =
|
|
1369
|
+
const base = path7.basename(service.dir);
|
|
1348
1370
|
knownHosts.add(base);
|
|
1349
1371
|
knownHosts.add(service.pkg.name);
|
|
1350
1372
|
hostToNodeId.set(base, service.node.id);
|
|
@@ -1353,29 +1375,29 @@ function buildServiceHostIndex(services) {
|
|
|
1353
1375
|
return { knownHosts, hostToNodeId };
|
|
1354
1376
|
}
|
|
1355
1377
|
async function walkSourceFiles(dir, excludeDirs = []) {
|
|
1356
|
-
const excluded = new Set(excludeDirs.map((d) =>
|
|
1378
|
+
const excluded = new Set(excludeDirs.map((d) => path7.resolve(d)));
|
|
1357
1379
|
const out = [];
|
|
1358
1380
|
const rootChain = await loadIgnoreChain(dir);
|
|
1359
1381
|
async function walk11(current, inherited) {
|
|
1360
|
-
const entries = await
|
|
1382
|
+
const entries = await fs7.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1361
1383
|
const chain = entries.some((e) => e.isFile() && e.name === ".gitignore") ? await extendIgnoreChain(inherited, current) : inherited;
|
|
1362
1384
|
for (const entry of entries) {
|
|
1363
|
-
const full =
|
|
1385
|
+
const full = path7.join(current, entry.name);
|
|
1364
1386
|
if (entry.isDirectory()) {
|
|
1365
1387
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
1366
|
-
if (excluded.has(
|
|
1388
|
+
if (excluded.has(path7.resolve(full))) continue;
|
|
1367
1389
|
const verdict = decide(chain, full, true);
|
|
1368
1390
|
if (verdict.ignored) continue;
|
|
1369
1391
|
if (await isPythonVenvDir(full)) continue;
|
|
1370
1392
|
await walk11(full, verdict.subtree);
|
|
1371
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
1393
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path7.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
1372
1394
|
// would attribute our instrumentation imports to the user's service.
|
|
1373
1395
|
!isNeatAuthoredSourceFile(entry.name) && !isIgnored(chain, full, false)) {
|
|
1374
1396
|
out.push(full);
|
|
1375
1397
|
}
|
|
1376
1398
|
}
|
|
1377
1399
|
}
|
|
1378
|
-
await walk11(dir, rootChain.filter((l) =>
|
|
1400
|
+
await walk11(dir, rootChain.filter((l) => path7.resolve(l.dir) !== path7.resolve(dir)));
|
|
1379
1401
|
return out;
|
|
1380
1402
|
}
|
|
1381
1403
|
var MINIFIED_LINE_CHARS = 5e3;
|
|
@@ -1403,7 +1425,7 @@ async function loadSourceFiles(dir, excludeDirs = []) {
|
|
|
1403
1425
|
const out = [];
|
|
1404
1426
|
for (const p of paths) {
|
|
1405
1427
|
try {
|
|
1406
|
-
const content = await
|
|
1428
|
+
const content = await fs7.readFile(p, "utf8");
|
|
1407
1429
|
const verdict = isMinifiedSource(p, content);
|
|
1408
1430
|
if (verdict.minified) {
|
|
1409
1431
|
noteSkippedFile({ path: p, reason: "minified", detail: verdict.detail });
|
|
@@ -1431,7 +1453,7 @@ function toPosix(p) {
|
|
|
1431
1453
|
return p.split("\\").join("/");
|
|
1432
1454
|
}
|
|
1433
1455
|
function languageForPath(relPath) {
|
|
1434
|
-
switch (
|
|
1456
|
+
switch (path7.extname(relPath).toLowerCase()) {
|
|
1435
1457
|
case ".py":
|
|
1436
1458
|
return "python";
|
|
1437
1459
|
case ".go":
|
|
@@ -1514,8 +1536,8 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
|
1514
1536
|
}
|
|
1515
1537
|
|
|
1516
1538
|
// src/extract/imports.ts
|
|
1517
|
-
import
|
|
1518
|
-
import { promises as
|
|
1539
|
+
import path8 from "path";
|
|
1540
|
+
import { promises as fs8 } from "fs";
|
|
1519
1541
|
import Parser from "tree-sitter";
|
|
1520
1542
|
import JavaScript from "tree-sitter-javascript";
|
|
1521
1543
|
import Python from "tree-sitter-python";
|
|
@@ -1666,15 +1688,15 @@ function collectPyImports(node, out) {
|
|
|
1666
1688
|
}
|
|
1667
1689
|
async function fileExists(p) {
|
|
1668
1690
|
try {
|
|
1669
|
-
await
|
|
1691
|
+
await fs8.access(p);
|
|
1670
1692
|
return true;
|
|
1671
1693
|
} catch {
|
|
1672
1694
|
return false;
|
|
1673
1695
|
}
|
|
1674
1696
|
}
|
|
1675
1697
|
function isWithinServiceDir(candidate, serviceDir) {
|
|
1676
|
-
const rel =
|
|
1677
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
1698
|
+
const rel = path8.relative(serviceDir, candidate);
|
|
1699
|
+
return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
|
|
1678
1700
|
}
|
|
1679
1701
|
var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
1680
1702
|
var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
|
|
@@ -1682,22 +1704,22 @@ async function firstExistingCandidate(base, serviceDir) {
|
|
|
1682
1704
|
for (const ext of JS_EXTENSIONS) {
|
|
1683
1705
|
const candidate = base + ext;
|
|
1684
1706
|
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
1685
|
-
return toPosix(
|
|
1707
|
+
return toPosix(path8.relative(serviceDir, candidate));
|
|
1686
1708
|
}
|
|
1687
1709
|
}
|
|
1688
1710
|
for (const indexFile of JS_INDEX_FILES) {
|
|
1689
|
-
const candidate =
|
|
1711
|
+
const candidate = path8.join(base, indexFile);
|
|
1690
1712
|
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
1691
|
-
return toPosix(
|
|
1713
|
+
return toPosix(path8.relative(serviceDir, candidate));
|
|
1692
1714
|
}
|
|
1693
1715
|
}
|
|
1694
1716
|
return null;
|
|
1695
1717
|
}
|
|
1696
1718
|
async function loadTsPathConfig(serviceDir) {
|
|
1697
|
-
const tsconfigPath =
|
|
1719
|
+
const tsconfigPath = path8.join(serviceDir, "tsconfig.json");
|
|
1698
1720
|
let raw;
|
|
1699
1721
|
try {
|
|
1700
|
-
raw = await
|
|
1722
|
+
raw = await fs8.readFile(tsconfigPath, "utf8");
|
|
1701
1723
|
} catch {
|
|
1702
1724
|
return null;
|
|
1703
1725
|
}
|
|
@@ -1706,7 +1728,7 @@ async function loadTsPathConfig(serviceDir) {
|
|
|
1706
1728
|
const paths = parsed.compilerOptions?.paths;
|
|
1707
1729
|
if (!paths || Object.keys(paths).length === 0) return null;
|
|
1708
1730
|
const baseUrl = parsed.compilerOptions?.baseUrl;
|
|
1709
|
-
return { paths, baseDir: baseUrl ?
|
|
1731
|
+
return { paths, baseDir: baseUrl ? path8.resolve(serviceDir, baseUrl) : serviceDir };
|
|
1710
1732
|
} catch (err) {
|
|
1711
1733
|
recordExtractionError("import alias resolution", tsconfigPath, err);
|
|
1712
1734
|
return null;
|
|
@@ -1724,11 +1746,11 @@ async function resolveTsAlias(specifier, config, serviceDir) {
|
|
|
1724
1746
|
if (suffix === null) continue;
|
|
1725
1747
|
for (const target of targets) {
|
|
1726
1748
|
const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
|
|
1727
|
-
const resolvedBase =
|
|
1749
|
+
const resolvedBase = path8.resolve(config.baseDir, targetBase, suffix);
|
|
1728
1750
|
const hit = await firstExistingCandidate(resolvedBase, serviceDir);
|
|
1729
1751
|
if (hit) return hit;
|
|
1730
1752
|
if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
|
|
1731
|
-
return toPosix(
|
|
1753
|
+
return toPosix(path8.relative(serviceDir, resolvedBase));
|
|
1732
1754
|
}
|
|
1733
1755
|
}
|
|
1734
1756
|
}
|
|
@@ -1737,18 +1759,18 @@ async function resolveTsAlias(specifier, config, serviceDir) {
|
|
|
1737
1759
|
async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
1738
1760
|
if (!specifier) return null;
|
|
1739
1761
|
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
1740
|
-
const base =
|
|
1741
|
-
const ext =
|
|
1762
|
+
const base = path8.resolve(importerDir, specifier);
|
|
1763
|
+
const ext = path8.extname(specifier);
|
|
1742
1764
|
if (ext) {
|
|
1743
1765
|
if (ext === ".js" || ext === ".jsx") {
|
|
1744
1766
|
const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
|
|
1745
1767
|
const tsSibling = base.slice(0, -ext.length) + tsExt;
|
|
1746
1768
|
if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
|
|
1747
|
-
return toPosix(
|
|
1769
|
+
return toPosix(path8.relative(serviceDir, tsSibling));
|
|
1748
1770
|
}
|
|
1749
1771
|
}
|
|
1750
1772
|
if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
|
|
1751
|
-
return toPosix(
|
|
1773
|
+
return toPosix(path8.relative(serviceDir, base));
|
|
1752
1774
|
}
|
|
1753
1775
|
if (!JS_EXTENSIONS.includes(ext)) {
|
|
1754
1776
|
return firstExistingCandidate(base, serviceDir);
|
|
@@ -1763,30 +1785,30 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
1763
1785
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
1764
1786
|
let baseDir;
|
|
1765
1787
|
if (imp.level > 0) {
|
|
1766
|
-
baseDir =
|
|
1767
|
-
for (let i = 1; i < imp.level; i++) baseDir =
|
|
1788
|
+
baseDir = path8.dirname(importerPath);
|
|
1789
|
+
for (let i = 1; i < imp.level; i++) baseDir = path8.dirname(baseDir);
|
|
1768
1790
|
} else {
|
|
1769
1791
|
baseDir = serviceDir;
|
|
1770
1792
|
}
|
|
1771
|
-
const moduleBase = imp.modulePath ?
|
|
1793
|
+
const moduleBase = imp.modulePath ? path8.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
1772
1794
|
const resolved = /* @__PURE__ */ new Set();
|
|
1773
1795
|
let needModuleFile = imp.names.length === 0;
|
|
1774
1796
|
for (const name of imp.names) {
|
|
1775
|
-
const submoduleFile =
|
|
1776
|
-
const subpackageInit =
|
|
1797
|
+
const submoduleFile = path8.join(moduleBase, `${name}.py`);
|
|
1798
|
+
const subpackageInit = path8.join(moduleBase, name, "__init__.py");
|
|
1777
1799
|
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
1778
|
-
resolved.add(toPosix(
|
|
1800
|
+
resolved.add(toPosix(path8.relative(serviceDir, submoduleFile)));
|
|
1779
1801
|
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
1780
|
-
resolved.add(toPosix(
|
|
1802
|
+
resolved.add(toPosix(path8.relative(serviceDir, subpackageInit)));
|
|
1781
1803
|
} else {
|
|
1782
1804
|
needModuleFile = true;
|
|
1783
1805
|
}
|
|
1784
1806
|
}
|
|
1785
1807
|
if (needModuleFile) {
|
|
1786
|
-
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`,
|
|
1808
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, path8.join(moduleBase, "__init__.py")] : [path8.join(moduleBase, "__init__.py")];
|
|
1787
1809
|
for (const candidate of moduleFileCandidates) {
|
|
1788
1810
|
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
1789
|
-
resolved.add(toPosix(
|
|
1811
|
+
resolved.add(toPosix(path8.relative(serviceDir, candidate)));
|
|
1790
1812
|
break;
|
|
1791
1813
|
}
|
|
1792
1814
|
}
|
|
@@ -1796,11 +1818,11 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
1796
1818
|
async function resolveGoImport(specifier, modulePath, serviceDir) {
|
|
1797
1819
|
if (specifier !== modulePath && !specifier.startsWith(`${modulePath}/`)) return null;
|
|
1798
1820
|
const suffix = specifier === modulePath ? "" : specifier.slice(modulePath.length + 1);
|
|
1799
|
-
const dir =
|
|
1800
|
-
const entries = await
|
|
1801
|
-
const candidates = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".go") && !entry.name.endsWith("_test.go")).map((entry) =>
|
|
1821
|
+
const dir = path8.join(serviceDir, suffix);
|
|
1822
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1823
|
+
const candidates = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".go") && !entry.name.endsWith("_test.go")).map((entry) => path8.join(dir, entry.name));
|
|
1802
1824
|
if (candidates.length !== 1) return null;
|
|
1803
|
-
return toPosix(
|
|
1825
|
+
return toPosix(path8.relative(serviceDir, candidates[0]));
|
|
1804
1826
|
}
|
|
1805
1827
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
1806
1828
|
const importeeFileId = fileId2(serviceName, importeeRelPath);
|
|
@@ -1830,12 +1852,12 @@ async function addImports(graph, services) {
|
|
|
1830
1852
|
const files = await loadSourceFiles(service.dir, service.excludeDirs);
|
|
1831
1853
|
for (const file of files) {
|
|
1832
1854
|
if (isTestPath(file.path)) continue;
|
|
1833
|
-
const relFile = toPosix(
|
|
1855
|
+
const relFile = toPosix(path8.relative(service.dir, file.path));
|
|
1834
1856
|
const importerFileId = fileId2(service.pkg.name, relFile);
|
|
1835
|
-
const isPython =
|
|
1836
|
-
const isGo =
|
|
1837
|
-
if (
|
|
1838
|
-
if (
|
|
1857
|
+
const isPython = path8.extname(file.path) === ".py";
|
|
1858
|
+
const isGo = path8.extname(file.path) === ".go";
|
|
1859
|
+
if (path8.extname(file.path) === ".rb") continue;
|
|
1860
|
+
if (path8.extname(file.path) === ".php") continue;
|
|
1839
1861
|
if (isGo) {
|
|
1840
1862
|
let goImports = [];
|
|
1841
1863
|
try {
|
|
@@ -1845,7 +1867,7 @@ async function addImports(graph, services) {
|
|
|
1845
1867
|
recordExtractionError("import extraction", file.path, err);
|
|
1846
1868
|
continue;
|
|
1847
1869
|
}
|
|
1848
|
-
const goMod = await
|
|
1870
|
+
const goMod = await fs8.readFile(path8.join(service.dir, "go.mod"), "utf8").catch(() => "");
|
|
1849
1871
|
const modulePath = goMod.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
|
|
1850
1872
|
if (!modulePath) continue;
|
|
1851
1873
|
for (const imp of goImports) {
|
|
@@ -1889,7 +1911,7 @@ async function addImports(graph, services) {
|
|
|
1889
1911
|
continue;
|
|
1890
1912
|
}
|
|
1891
1913
|
for (const imp of jsImports) {
|
|
1892
|
-
const resolved = await resolveJsImport(imp.specifier,
|
|
1914
|
+
const resolved = await resolveJsImport(imp.specifier, path8.dirname(file.path), service.dir, tsPaths);
|
|
1893
1915
|
if (!resolved) continue;
|
|
1894
1916
|
edgesAdded += emitImportEdge(
|
|
1895
1917
|
graph,
|
|
@@ -2014,8 +2036,8 @@ function chiRoutesFromSource(source, parser) {
|
|
|
2014
2036
|
chiWalk(tree.rootNode, "", out);
|
|
2015
2037
|
return out;
|
|
2016
2038
|
}
|
|
2017
|
-
function stripChiRegex(
|
|
2018
|
-
return
|
|
2039
|
+
function stripChiRegex(path77) {
|
|
2040
|
+
return path77.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
|
|
2019
2041
|
}
|
|
2020
2042
|
function chiWalk(node, prefix, out) {
|
|
2021
2043
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -2355,7 +2377,7 @@ function isNextPagesApiFile(relFile) {
|
|
|
2355
2377
|
if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
|
|
2356
2378
|
const base = segs[segs.length - 1] ?? "";
|
|
2357
2379
|
if (/^_(app|document|middleware)\./.test(base)) return false;
|
|
2358
|
-
return JS_ROUTE_EXTENSIONS.has(
|
|
2380
|
+
return JS_ROUTE_EXTENSIONS.has(path9.extname(base));
|
|
2359
2381
|
}
|
|
2360
2382
|
function nextSegment(seg) {
|
|
2361
2383
|
if (seg.startsWith("(") && seg.endsWith(")")) return null;
|
|
@@ -2687,9 +2709,9 @@ function rubyRocketRoute(args) {
|
|
|
2687
2709
|
if (!pair || pair.type !== "pair") continue;
|
|
2688
2710
|
const k = pair.childForFieldName("key");
|
|
2689
2711
|
if (k?.type !== "string") continue;
|
|
2690
|
-
const
|
|
2691
|
-
if (
|
|
2692
|
-
return { path:
|
|
2712
|
+
const path77 = rubyLiteral(k);
|
|
2713
|
+
if (path77 === null) continue;
|
|
2714
|
+
return { path: path77, target: rubyLiteral(pair.childForFieldName("value")) };
|
|
2693
2715
|
}
|
|
2694
2716
|
return null;
|
|
2695
2717
|
}
|
|
@@ -2779,23 +2801,23 @@ function railsInferHandler(rawPath) {
|
|
|
2779
2801
|
if (segs.length === 1) return `${segs[0]}#index`;
|
|
2780
2802
|
return `${segs.slice(0, -1).join("/")}#${segs[segs.length - 1]}`;
|
|
2781
2803
|
}
|
|
2782
|
-
function railsResource(call, ctx, out,
|
|
2804
|
+
function railsResource(call, ctx, out, plural2) {
|
|
2783
2805
|
if (ctx.depth >= 2) return;
|
|
2784
2806
|
const args = rubyArgs(call);
|
|
2785
2807
|
const name = rubyLiteral(rubyPositional(args)[0]);
|
|
2786
2808
|
if (!name) return;
|
|
2787
2809
|
const base = `${ctx.path}/${name}`;
|
|
2788
|
-
const controller = ctx.module + (
|
|
2810
|
+
const controller = ctx.module + (plural2 ? name : railsPluralize(name));
|
|
2789
2811
|
const allow = railsActionFilter(args);
|
|
2790
2812
|
const line = call.startPosition.row + 1;
|
|
2791
|
-
for (const row of
|
|
2813
|
+
for (const row of plural2 ? RAILS_PLURAL_ROWS : RAILS_SINGULAR_ROWS) {
|
|
2792
2814
|
if (!allow(row.action)) continue;
|
|
2793
2815
|
for (const m of row.methods) railsEmit(out, m, base + row.suffix, `${controller}#${row.action}`, line);
|
|
2794
2816
|
}
|
|
2795
2817
|
const block = call.childForFieldName("block");
|
|
2796
2818
|
if (!block) return;
|
|
2797
|
-
const memberBase =
|
|
2798
|
-
const nestedPath =
|
|
2819
|
+
const memberBase = plural2 ? `${base}/:id` : base;
|
|
2820
|
+
const nestedPath = plural2 ? `${base}/:${railsSingularize(name)}_id` : base;
|
|
2799
2821
|
for (const child of rubyBlockCalls(block)) {
|
|
2800
2822
|
const cm = child.childForFieldName("method")?.text;
|
|
2801
2823
|
if (cm === "member") railsBlockVerbs(child, memberBase, controller, out);
|
|
@@ -3537,12 +3559,12 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
|
|
|
3537
3559
|
const jsParser = makeJsParser2();
|
|
3538
3560
|
const fileInfo = /* @__PURE__ */ new Map();
|
|
3539
3561
|
for (const f of files) {
|
|
3540
|
-
if (!JS_ROUTE_EXTENSIONS.has(
|
|
3562
|
+
if (!JS_ROUTE_EXTENSIONS.has(path9.extname(f.path))) continue;
|
|
3541
3563
|
if (isTestPath(f.path)) continue;
|
|
3542
|
-
const rel = toPosix(
|
|
3564
|
+
const rel = toPosix(path9.relative(serviceDir, f.path));
|
|
3543
3565
|
try {
|
|
3544
3566
|
const tree = parseSource2(jsParser, f.content);
|
|
3545
|
-
fileInfo.set(rel, analyzeExpressFile(tree.rootNode,
|
|
3567
|
+
fileInfo.set(rel, analyzeExpressFile(tree.rootNode, path9.dirname(f.path)));
|
|
3546
3568
|
} catch {
|
|
3547
3569
|
}
|
|
3548
3570
|
}
|
|
@@ -3641,14 +3663,14 @@ async function addRoutes(graph, services) {
|
|
|
3641
3663
|
const mountPrefixes = hasExpress ? await expressMountPrefixes(files, service.dir, await loadTsPathConfig(service.dir)) : /* @__PURE__ */ new Map();
|
|
3642
3664
|
for (const file of files) {
|
|
3643
3665
|
if (isTestPath(file.path)) continue;
|
|
3644
|
-
const ext =
|
|
3666
|
+
const ext = path9.extname(file.path);
|
|
3645
3667
|
const isPy = ext === ".py";
|
|
3646
3668
|
const isGo = ext === ".go";
|
|
3647
3669
|
const isRb = ext === ".rb";
|
|
3648
3670
|
const isPhp = ext === ".php";
|
|
3649
3671
|
const isRs = ext === ".rs";
|
|
3650
3672
|
if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy && !isGo && !isRb && !isPhp && !isRs) continue;
|
|
3651
|
-
const relFile = toPosix(
|
|
3673
|
+
const relFile = toPosix(path9.relative(service.dir, file.path));
|
|
3652
3674
|
let routes;
|
|
3653
3675
|
try {
|
|
3654
3676
|
if (isPhp) {
|
|
@@ -4110,20 +4132,20 @@ var CODE_LINENO_ATTR = "code.lineno";
|
|
|
4110
4132
|
var CODE_FUNCTION_NAME_ATTR = "code.function.name";
|
|
4111
4133
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
4112
4134
|
function codeFilepathOf(attrs) {
|
|
4113
|
-
const
|
|
4114
|
-
if (typeof
|
|
4135
|
+
const stable2 = attrs[CODE_FILE_PATH_ATTR];
|
|
4136
|
+
if (typeof stable2 === "string" && stable2.length > 0) return stable2;
|
|
4115
4137
|
const prior = attrs[CODE_FILEPATH_ATTR];
|
|
4116
4138
|
return typeof prior === "string" && prior.length > 0 ? prior : void 0;
|
|
4117
4139
|
}
|
|
4118
4140
|
function codeLinenoOf(attrs) {
|
|
4119
|
-
const
|
|
4120
|
-
if (typeof
|
|
4141
|
+
const stable2 = attrs[CODE_LINE_NUMBER_ATTR];
|
|
4142
|
+
if (typeof stable2 === "number" && Number.isFinite(stable2)) return stable2;
|
|
4121
4143
|
const prior = attrs[CODE_LINENO_ATTR];
|
|
4122
4144
|
return typeof prior === "number" && Number.isFinite(prior) ? prior : void 0;
|
|
4123
4145
|
}
|
|
4124
4146
|
function codeFunctionOf(attrs) {
|
|
4125
|
-
const
|
|
4126
|
-
if (typeof
|
|
4147
|
+
const stable2 = attrs[CODE_FUNCTION_NAME_ATTR];
|
|
4148
|
+
if (typeof stable2 === "string" && stable2.length > 0) return stable2;
|
|
4127
4149
|
const prior = attrs[CODE_FUNCTION_ATTR];
|
|
4128
4150
|
return typeof prior === "string" && prior.length > 0 ? prior : void 0;
|
|
4129
4151
|
}
|
|
@@ -4153,7 +4175,7 @@ function languageForExt(relPath) {
|
|
|
4153
4175
|
function relPathForRuntimeFile(filepath, serviceNode, scanPath) {
|
|
4154
4176
|
let p = toPosix2(filepath).replace(/^file:\/\//, "");
|
|
4155
4177
|
if (scanPath && scanPath.length > 0) {
|
|
4156
|
-
const absRoot = toPosix2(
|
|
4178
|
+
const absRoot = toPosix2(path10.resolve(scanPath, serviceNode?.repoPath ?? ""));
|
|
4157
4179
|
const anchor = absRoot.endsWith("/") ? absRoot : `${absRoot}/`;
|
|
4158
4180
|
if (p.startsWith(anchor)) return p.slice(anchor.length);
|
|
4159
4181
|
}
|
|
@@ -4184,7 +4206,7 @@ function resolveDistToSrc(absFilepath, line) {
|
|
|
4184
4206
|
if (existsSync(mapPath)) {
|
|
4185
4207
|
const raw = JSON.parse(readFileSync(mapPath, "utf8"));
|
|
4186
4208
|
const consumer = new sourceMapJs.SourceMapConsumer(raw);
|
|
4187
|
-
entry = { consumer, dir:
|
|
4209
|
+
entry = { consumer, dir: path10.dirname(mapPath) };
|
|
4188
4210
|
}
|
|
4189
4211
|
} catch {
|
|
4190
4212
|
entry = null;
|
|
@@ -4204,7 +4226,7 @@ function resolveDistToSrc(absFilepath, line) {
|
|
|
4204
4226
|
}
|
|
4205
4227
|
if (!pos || !pos.source) return null;
|
|
4206
4228
|
const root = entry.consumer.sourceRoot ?? "";
|
|
4207
|
-
const resolved =
|
|
4229
|
+
const resolved = path10.resolve(entry.dir, root, pos.source);
|
|
4208
4230
|
return { filepath: resolved, ...pos.line ? { line: pos.line } : {} };
|
|
4209
4231
|
} catch {
|
|
4210
4232
|
return null;
|
|
@@ -4748,8 +4770,7 @@ function emitIncidentEvent(project, ev) {
|
|
|
4748
4770
|
});
|
|
4749
4771
|
}
|
|
4750
4772
|
async function appendErrorEvent(ctx, ev) {
|
|
4751
|
-
await
|
|
4752
|
-
await fs8.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4773
|
+
await appendRuntimeIncident(ctx.errorsPath, ctx.project ?? DEFAULT_PROJECT, ev);
|
|
4753
4774
|
emitIncidentEvent(ctx.project ?? DEFAULT_PROJECT, ev);
|
|
4754
4775
|
}
|
|
4755
4776
|
async function appendConnectorIncident(errorsPath, input, project) {
|
|
@@ -4764,8 +4785,7 @@ async function appendConnectorIncident(errorsPath, input, project) {
|
|
|
4764
4785
|
...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
|
|
4765
4786
|
affectedNode: input.affectedNode
|
|
4766
4787
|
};
|
|
4767
|
-
await
|
|
4768
|
-
await fs8.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4788
|
+
await appendRuntimeIncident(errorsPath, project ?? DEFAULT_PROJECT, ev);
|
|
4769
4789
|
if (project) emitIncidentEvent(project, ev);
|
|
4770
4790
|
}
|
|
4771
4791
|
function landIncidentCallSite(span, callSite, trusted, graph) {
|
|
@@ -4887,8 +4907,7 @@ function makeErrorSpanWriter(errorsPath, graph, scanPath, project = DEFAULT_PROJ
|
|
|
4887
4907
|
return async (span) => {
|
|
4888
4908
|
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
4889
4909
|
if (!ev) return;
|
|
4890
|
-
await
|
|
4891
|
-
await fs8.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
4910
|
+
await appendRuntimeIncident(errorsPath, project, ev);
|
|
4892
4911
|
emitIncidentEvent(project, ev);
|
|
4893
4912
|
};
|
|
4894
4913
|
}
|
|
@@ -5391,13 +5410,13 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
5391
5410
|
return { count: events.length, events };
|
|
5392
5411
|
}
|
|
5393
5412
|
async function appendStaleEvents(staleEventsPath, events) {
|
|
5394
|
-
await
|
|
5413
|
+
await fs9.mkdir(path10.dirname(staleEventsPath), { recursive: true });
|
|
5395
5414
|
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
5396
|
-
await
|
|
5415
|
+
await fs9.appendFile(staleEventsPath, lines, "utf8");
|
|
5397
5416
|
}
|
|
5398
5417
|
async function readStaleEvents(staleEventsPath) {
|
|
5399
5418
|
try {
|
|
5400
|
-
const raw = await
|
|
5419
|
+
const raw = await fs9.readFile(staleEventsPath, "utf8");
|
|
5401
5420
|
return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
5402
5421
|
} catch (err) {
|
|
5403
5422
|
if (err.code === "ENOENT") return [];
|
|
@@ -5433,7 +5452,7 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
5433
5452
|
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
5434
5453
|
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
5435
5454
|
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
5436
|
-
const handle = await
|
|
5455
|
+
const handle = await fs9.open(errorsPath, "r");
|
|
5437
5456
|
try {
|
|
5438
5457
|
const { size } = await handle.stat();
|
|
5439
5458
|
if (size <= maxBytes) {
|
|
@@ -5469,6 +5488,23 @@ async function readErrorEvents(errorsPath, opts) {
|
|
|
5469
5488
|
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
5470
5489
|
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
5471
5490
|
}
|
|
5491
|
+
async function readErrorEventById(errorsPath, id) {
|
|
5492
|
+
let raw;
|
|
5493
|
+
try {
|
|
5494
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
5495
|
+
} catch (err) {
|
|
5496
|
+
if (err.code === "ENOENT") return void 0;
|
|
5497
|
+
throw err;
|
|
5498
|
+
}
|
|
5499
|
+
const lines = raw.split("\n");
|
|
5500
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
5501
|
+
const line = lines[i];
|
|
5502
|
+
if (!line) continue;
|
|
5503
|
+
const event = JSON.parse(line);
|
|
5504
|
+
if (event.id === id) return redactPersistedAttributes(event);
|
|
5505
|
+
}
|
|
5506
|
+
return void 0;
|
|
5507
|
+
}
|
|
5472
5508
|
function isSynthesizedHttpIncident(ev) {
|
|
5473
5509
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
5474
5510
|
if (ev.errorType) return false;
|
|
@@ -5690,19 +5726,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
5690
5726
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
5691
5727
|
let best = { path: [start], edges: [] };
|
|
5692
5728
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
5693
|
-
function step(node,
|
|
5694
|
-
if (
|
|
5695
|
-
best = { path: [...
|
|
5729
|
+
function step(node, path77, edges) {
|
|
5730
|
+
if (path77.length > best.path.length) {
|
|
5731
|
+
best = { path: [...path77], edges: [...edges] };
|
|
5696
5732
|
}
|
|
5697
|
-
if (
|
|
5733
|
+
if (path77.length - 1 >= maxDepth) return;
|
|
5698
5734
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
5699
5735
|
for (const [srcId, edge] of incoming) {
|
|
5700
5736
|
if (visited.has(srcId)) continue;
|
|
5701
5737
|
visited.add(srcId);
|
|
5702
|
-
|
|
5738
|
+
path77.push(srcId);
|
|
5703
5739
|
edges.push(edge);
|
|
5704
|
-
step(srcId,
|
|
5705
|
-
|
|
5740
|
+
step(srcId, path77, edges);
|
|
5741
|
+
path77.pop();
|
|
5706
5742
|
edges.pop();
|
|
5707
5743
|
visited.delete(srcId);
|
|
5708
5744
|
}
|
|
@@ -5917,20 +5953,20 @@ function dominantFailingCall(graph, serviceId17, visited) {
|
|
|
5917
5953
|
return best;
|
|
5918
5954
|
}
|
|
5919
5955
|
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
5920
|
-
const
|
|
5956
|
+
const path77 = [originServiceId];
|
|
5921
5957
|
const edges = [];
|
|
5922
5958
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
5923
5959
|
let current = originServiceId;
|
|
5924
5960
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
5925
5961
|
const hop = dominantFailingCall(graph, current, visited);
|
|
5926
5962
|
if (!hop) break;
|
|
5927
|
-
|
|
5963
|
+
path77.push(hop.nextService);
|
|
5928
5964
|
edges.push(hop.edge);
|
|
5929
5965
|
visited.add(hop.nextService);
|
|
5930
5966
|
current = hop.nextService;
|
|
5931
5967
|
}
|
|
5932
5968
|
if (edges.length === 0) return null;
|
|
5933
|
-
return { path:
|
|
5969
|
+
return { path: path77, edges, culprit: current };
|
|
5934
5970
|
}
|
|
5935
5971
|
function isStaleCallEdge(e) {
|
|
5936
5972
|
return e.type === EdgeType6.CALLS && e.provenance === Provenance6.STALE;
|
|
@@ -5966,26 +6002,26 @@ function dominantStaleCall(graph, serviceId17, visited) {
|
|
|
5966
6002
|
return best;
|
|
5967
6003
|
}
|
|
5968
6004
|
function followStaleCallChain(graph, originServiceId, maxDepth) {
|
|
5969
|
-
const
|
|
6005
|
+
const path77 = [originServiceId];
|
|
5970
6006
|
const edges = [];
|
|
5971
6007
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
5972
6008
|
let current = originServiceId;
|
|
5973
6009
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
5974
6010
|
const hop = dominantStaleCall(graph, current, visited);
|
|
5975
6011
|
if (!hop) break;
|
|
5976
|
-
|
|
6012
|
+
path77.push(hop.nextService);
|
|
5977
6013
|
edges.push(hop.edge);
|
|
5978
6014
|
visited.add(hop.nextService);
|
|
5979
6015
|
current = hop.nextService;
|
|
5980
6016
|
}
|
|
5981
6017
|
if (edges.length === 0) return null;
|
|
5982
|
-
return { path:
|
|
6018
|
+
return { path: path77, edges, culprit: current };
|
|
5983
6019
|
}
|
|
5984
6020
|
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
5985
6021
|
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
5986
6022
|
if (!chain) return null;
|
|
5987
6023
|
const culprit = chain.culprit;
|
|
5988
|
-
const
|
|
6024
|
+
const path77 = [...chain.path];
|
|
5989
6025
|
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
5990
6026
|
const baseConfidence = confidenceFromMix(chain.edges);
|
|
5991
6027
|
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
@@ -5993,14 +6029,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
5993
6029
|
if (loc) {
|
|
5994
6030
|
let rootCauseNode = culprit;
|
|
5995
6031
|
if (loc.fileNode) {
|
|
5996
|
-
|
|
6032
|
+
path77.push(loc.fileNode);
|
|
5997
6033
|
edgeProvenances.push(Provenance6.OBSERVED);
|
|
5998
6034
|
rootCauseNode = loc.fileNode;
|
|
5999
6035
|
}
|
|
6000
6036
|
return RootCauseResultSchema.parse({
|
|
6001
6037
|
rootCauseNode,
|
|
6002
6038
|
rootCauseReason: loc.rootCauseReason,
|
|
6003
|
-
traversalPath:
|
|
6039
|
+
traversalPath: path77,
|
|
6004
6040
|
edgeProvenances,
|
|
6005
6041
|
confidence,
|
|
6006
6042
|
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
@@ -6012,7 +6048,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
6012
6048
|
return RootCauseResultSchema.parse({
|
|
6013
6049
|
rootCauseNode: culprit,
|
|
6014
6050
|
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
6015
|
-
traversalPath:
|
|
6051
|
+
traversalPath: path77,
|
|
6016
6052
|
edgeProvenances,
|
|
6017
6053
|
confidence,
|
|
6018
6054
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -6665,10 +6701,10 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
6665
6701
|
traversalPath = staleChain.path;
|
|
6666
6702
|
edgeProvenances = staleChain.edges.map((e) => e.provenance);
|
|
6667
6703
|
} else if (top.node !== seedNode) {
|
|
6668
|
-
const
|
|
6669
|
-
if (
|
|
6670
|
-
traversalPath =
|
|
6671
|
-
edgeProvenances =
|
|
6704
|
+
const path77 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
6705
|
+
if (path77) {
|
|
6706
|
+
traversalPath = path77.nodes;
|
|
6707
|
+
edgeProvenances = path77.edges.map((e) => e.provenance);
|
|
6672
6708
|
} else {
|
|
6673
6709
|
traversalPath = [errorNodeId, top.node];
|
|
6674
6710
|
edgeProvenances = [top.provenance ?? Provenance6.OBSERVED];
|
|
@@ -6703,15 +6739,15 @@ function fixRecommendationForTop(top, seedNode, legacy) {
|
|
|
6703
6739
|
}
|
|
6704
6740
|
|
|
6705
6741
|
// src/extract/services.ts
|
|
6706
|
-
import { promises as
|
|
6707
|
-
import
|
|
6742
|
+
import { promises as fs20 } from "fs";
|
|
6743
|
+
import path22 from "path";
|
|
6708
6744
|
import ignore2 from "ignore";
|
|
6709
6745
|
import { minimatch as minimatch2 } from "minimatch";
|
|
6710
6746
|
import { NodeType as NodeType15, serviceId as serviceId11 } from "@neat.is/types";
|
|
6711
6747
|
|
|
6712
6748
|
// src/extract/python.ts
|
|
6713
|
-
import { promises as
|
|
6714
|
-
import
|
|
6749
|
+
import { promises as fs10 } from "fs";
|
|
6750
|
+
import path11 from "path";
|
|
6715
6751
|
import { parse as parseToml } from "smol-toml";
|
|
6716
6752
|
var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
|
|
6717
6753
|
function parseRequirementsTxt(content) {
|
|
@@ -6744,25 +6780,25 @@ function depsFromPyProject(pyproject) {
|
|
|
6744
6780
|
return out;
|
|
6745
6781
|
}
|
|
6746
6782
|
async function discoverPythonService(serviceDir) {
|
|
6747
|
-
const pyprojectPath =
|
|
6748
|
-
const requirementsPath =
|
|
6749
|
-
const setupPath =
|
|
6783
|
+
const pyprojectPath = path11.join(serviceDir, "pyproject.toml");
|
|
6784
|
+
const requirementsPath = path11.join(serviceDir, "requirements.txt");
|
|
6785
|
+
const setupPath = path11.join(serviceDir, "setup.py");
|
|
6750
6786
|
const hasPyproject = await exists(pyprojectPath);
|
|
6751
6787
|
const hasRequirements = await exists(requirementsPath);
|
|
6752
6788
|
const hasSetup = await exists(setupPath);
|
|
6753
6789
|
if (!hasPyproject && !hasRequirements && !hasSetup) return null;
|
|
6754
|
-
let name =
|
|
6790
|
+
let name = path11.basename(serviceDir);
|
|
6755
6791
|
let version;
|
|
6756
6792
|
const dependencies = {};
|
|
6757
6793
|
if (hasPyproject) {
|
|
6758
|
-
const raw = await
|
|
6794
|
+
const raw = await fs10.readFile(pyprojectPath, "utf8");
|
|
6759
6795
|
const pyproject = parseToml(raw);
|
|
6760
6796
|
name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
|
|
6761
6797
|
version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
|
|
6762
6798
|
Object.assign(dependencies, depsFromPyProject(pyproject));
|
|
6763
6799
|
}
|
|
6764
6800
|
if (hasRequirements) {
|
|
6765
|
-
const raw = await
|
|
6801
|
+
const raw = await fs10.readFile(requirementsPath, "utf8");
|
|
6766
6802
|
Object.assign(dependencies, parseRequirementsTxt(raw));
|
|
6767
6803
|
}
|
|
6768
6804
|
return { name, version, dependencies };
|
|
@@ -6776,8 +6812,8 @@ function pythonToPackage(service) {
|
|
|
6776
6812
|
}
|
|
6777
6813
|
|
|
6778
6814
|
// src/extract/go.ts
|
|
6779
|
-
import { promises as
|
|
6780
|
-
import
|
|
6815
|
+
import { promises as fs11 } from "fs";
|
|
6816
|
+
import path12 from "path";
|
|
6781
6817
|
import { NodeType as NodeType6, serviceId as serviceId2 } from "@neat.is/types";
|
|
6782
6818
|
function parseGoMod(source) {
|
|
6783
6819
|
const module = source.match(/^\s*module\s+(\S+)\s*$/m)?.[1];
|
|
@@ -6804,7 +6840,7 @@ function goFramework(deps) {
|
|
|
6804
6840
|
async function discoverGoService(scanPath, dir) {
|
|
6805
6841
|
let raw;
|
|
6806
6842
|
try {
|
|
6807
|
-
raw = await
|
|
6843
|
+
raw = await fs11.readFile(path12.join(dir, "go.mod"), "utf8");
|
|
6808
6844
|
} catch {
|
|
6809
6845
|
return null;
|
|
6810
6846
|
}
|
|
@@ -6819,15 +6855,15 @@ async function discoverGoService(scanPath, dir) {
|
|
|
6819
6855
|
name,
|
|
6820
6856
|
language: "go",
|
|
6821
6857
|
dependencies: mod.dependencies,
|
|
6822
|
-
repoPath:
|
|
6858
|
+
repoPath: path12.relative(scanPath, dir),
|
|
6823
6859
|
...framework ? { framework } : {}
|
|
6824
6860
|
};
|
|
6825
6861
|
return { pkg, dir, node };
|
|
6826
6862
|
}
|
|
6827
6863
|
|
|
6828
6864
|
// src/extract/ruby.ts
|
|
6829
|
-
import { promises as
|
|
6830
|
-
import
|
|
6865
|
+
import { promises as fs12 } from "fs";
|
|
6866
|
+
import path13 from "path";
|
|
6831
6867
|
import { NodeType as NodeType7, serviceId as serviceId3 } from "@neat.is/types";
|
|
6832
6868
|
function parseGemfile(source) {
|
|
6833
6869
|
const dependencies = {};
|
|
@@ -6844,12 +6880,12 @@ function parseGemfile(source) {
|
|
|
6844
6880
|
async function discoverRubyService(scanPath, dir) {
|
|
6845
6881
|
let raw;
|
|
6846
6882
|
try {
|
|
6847
|
-
raw = await
|
|
6883
|
+
raw = await fs12.readFile(path13.join(dir, "Gemfile"), "utf8");
|
|
6848
6884
|
} catch {
|
|
6849
6885
|
return null;
|
|
6850
6886
|
}
|
|
6851
6887
|
const gemfile = parseGemfile(raw);
|
|
6852
|
-
const name =
|
|
6888
|
+
const name = path13.basename(dir);
|
|
6853
6889
|
const pkg = { name, dependencies: gemfile.dependencies };
|
|
6854
6890
|
const node = {
|
|
6855
6891
|
id: serviceId3(name),
|
|
@@ -6857,14 +6893,14 @@ async function discoverRubyService(scanPath, dir) {
|
|
|
6857
6893
|
name,
|
|
6858
6894
|
language: "ruby",
|
|
6859
6895
|
dependencies: gemfile.dependencies,
|
|
6860
|
-
repoPath:
|
|
6896
|
+
repoPath: path13.relative(scanPath, dir),
|
|
6861
6897
|
...gemfile.dependencies["rails"] !== void 0 ? { framework: "rails" } : {}
|
|
6862
6898
|
};
|
|
6863
6899
|
return { pkg, dir, node };
|
|
6864
6900
|
}
|
|
6865
6901
|
|
|
6866
6902
|
// src/extract/php.ts
|
|
6867
|
-
import
|
|
6903
|
+
import path14 from "path";
|
|
6868
6904
|
import { NodeType as NodeType8, serviceId as serviceId4 } from "@neat.is/types";
|
|
6869
6905
|
function parseComposerJson(composer) {
|
|
6870
6906
|
return {
|
|
@@ -6877,12 +6913,12 @@ function parseComposerJson(composer) {
|
|
|
6877
6913
|
async function discoverPhpService(scanPath, dir) {
|
|
6878
6914
|
let composer;
|
|
6879
6915
|
try {
|
|
6880
|
-
composer = await readJson(
|
|
6916
|
+
composer = await readJson(path14.join(dir, "composer.json"));
|
|
6881
6917
|
} catch {
|
|
6882
6918
|
return null;
|
|
6883
6919
|
}
|
|
6884
6920
|
const { dependencies } = parseComposerJson(composer);
|
|
6885
|
-
const name =
|
|
6921
|
+
const name = path14.basename(dir);
|
|
6886
6922
|
const pkg = { name, dependencies };
|
|
6887
6923
|
const node = {
|
|
6888
6924
|
id: serviceId4(name),
|
|
@@ -6890,30 +6926,30 @@ async function discoverPhpService(scanPath, dir) {
|
|
|
6890
6926
|
name,
|
|
6891
6927
|
language: "php",
|
|
6892
6928
|
dependencies,
|
|
6893
|
-
repoPath:
|
|
6929
|
+
repoPath: path14.relative(scanPath, dir),
|
|
6894
6930
|
...dependencies["laravel/framework"] !== void 0 ? { framework: "laravel" } : {}
|
|
6895
6931
|
};
|
|
6896
6932
|
return { pkg, dir, node };
|
|
6897
6933
|
}
|
|
6898
6934
|
|
|
6899
6935
|
// src/extract/csharp.ts
|
|
6900
|
-
import { promises as
|
|
6901
|
-
import
|
|
6936
|
+
import { promises as fs13 } from "fs";
|
|
6937
|
+
import path15 from "path";
|
|
6902
6938
|
import { NodeType as NodeType9, serviceId as serviceId5 } from "@neat.is/types";
|
|
6903
6939
|
var CSHARP_PROJECT_EXTS = /* @__PURE__ */ new Set([".csproj", ".sln"]);
|
|
6904
6940
|
async function projectFiles(dir) {
|
|
6905
|
-
const entries = await
|
|
6941
|
+
const entries = await fs13.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
6906
6942
|
const out = [];
|
|
6907
6943
|
for (const entry of entries) {
|
|
6908
6944
|
if (!entry.isFile()) continue;
|
|
6909
|
-
if (CSHARP_PROJECT_EXTS.has(
|
|
6945
|
+
if (CSHARP_PROJECT_EXTS.has(path15.extname(entry.name).toLowerCase())) out.push(entry.name);
|
|
6910
6946
|
}
|
|
6911
6947
|
return out.sort();
|
|
6912
6948
|
}
|
|
6913
6949
|
var NESTED_PROJECT_DIR = "src";
|
|
6914
6950
|
async function resolveProjectDir(dir) {
|
|
6915
6951
|
if ((await projectFiles(dir)).length > 0) return dir;
|
|
6916
|
-
const nested =
|
|
6952
|
+
const nested = path15.join(dir, NESTED_PROJECT_DIR);
|
|
6917
6953
|
if ((await projectFiles(nested)).length > 0) return nested;
|
|
6918
6954
|
return null;
|
|
6919
6955
|
}
|
|
@@ -6938,10 +6974,10 @@ async function discoverCsharpService(scanPath, dir) {
|
|
|
6938
6974
|
if (projectDir === null) return null;
|
|
6939
6975
|
const projects = await projectFiles(projectDir);
|
|
6940
6976
|
const csproj = projects.find((p) => p.toLowerCase().endsWith(".csproj"));
|
|
6941
|
-
const name = csproj ?
|
|
6977
|
+
const name = csproj ? path15.basename(csproj, path15.extname(csproj)) : path15.basename(dir);
|
|
6942
6978
|
let dependencies = {};
|
|
6943
6979
|
if (csproj) {
|
|
6944
|
-
const raw = await
|
|
6980
|
+
const raw = await fs13.readFile(path15.join(projectDir, csproj), "utf8").catch(() => "");
|
|
6945
6981
|
dependencies = parseCsproj(raw).dependencies;
|
|
6946
6982
|
}
|
|
6947
6983
|
const pkg = { name, dependencies };
|
|
@@ -6953,19 +6989,19 @@ async function discoverCsharpService(scanPath, dir) {
|
|
|
6953
6989
|
dependencies,
|
|
6954
6990
|
// Anchor on the directory that holds the project and its `.cs` files, so file
|
|
6955
6991
|
// attribution reaches the source whether it sits at the root or under `src/`.
|
|
6956
|
-
repoPath:
|
|
6992
|
+
repoPath: path15.relative(scanPath, projectDir)
|
|
6957
6993
|
};
|
|
6958
6994
|
return { pkg, dir: projectDir, node };
|
|
6959
6995
|
}
|
|
6960
6996
|
|
|
6961
6997
|
// src/extract/java.ts
|
|
6962
|
-
import { promises as
|
|
6963
|
-
import
|
|
6998
|
+
import { promises as fs14 } from "fs";
|
|
6999
|
+
import path16 from "path";
|
|
6964
7000
|
import { NodeType as NodeType10, serviceId as serviceId6 } from "@neat.is/types";
|
|
6965
7001
|
var JAVA_MANIFESTS = ["pom.xml", "build.gradle", "build.gradle.kts"];
|
|
6966
7002
|
async function hasJavaManifest(dir) {
|
|
6967
7003
|
for (const manifest of JAVA_MANIFESTS) {
|
|
6968
|
-
if (await exists(
|
|
7004
|
+
if (await exists(path16.join(dir, manifest))) return true;
|
|
6969
7005
|
}
|
|
6970
7006
|
return false;
|
|
6971
7007
|
}
|
|
@@ -7000,19 +7036,19 @@ function parseGradle(source) {
|
|
|
7000
7036
|
return { dependencies };
|
|
7001
7037
|
}
|
|
7002
7038
|
async function discoverJavaService(scanPath, dir) {
|
|
7003
|
-
const pomPath =
|
|
7004
|
-
const gradlePath =
|
|
7005
|
-
const gradleKtsPath =
|
|
7039
|
+
const pomPath = path16.join(dir, "pom.xml");
|
|
7040
|
+
const gradlePath = path16.join(dir, "build.gradle");
|
|
7041
|
+
const gradleKtsPath = path16.join(dir, "build.gradle.kts");
|
|
7006
7042
|
let manifest = null;
|
|
7007
7043
|
if (await exists(pomPath)) {
|
|
7008
|
-
manifest = parsePomXml(await
|
|
7044
|
+
manifest = parsePomXml(await fs14.readFile(pomPath, "utf8").catch(() => ""));
|
|
7009
7045
|
} else if (await exists(gradlePath)) {
|
|
7010
|
-
manifest = parseGradle(await
|
|
7046
|
+
manifest = parseGradle(await fs14.readFile(gradlePath, "utf8").catch(() => ""));
|
|
7011
7047
|
} else if (await exists(gradleKtsPath)) {
|
|
7012
|
-
manifest = parseGradle(await
|
|
7048
|
+
manifest = parseGradle(await fs14.readFile(gradleKtsPath, "utf8").catch(() => ""));
|
|
7013
7049
|
}
|
|
7014
7050
|
if (!manifest) return null;
|
|
7015
|
-
const name = manifest.name?.trim() ||
|
|
7051
|
+
const name = manifest.name?.trim() || path16.basename(dir);
|
|
7016
7052
|
const dependencies = manifest.dependencies;
|
|
7017
7053
|
const pkg = { name, dependencies };
|
|
7018
7054
|
const node = {
|
|
@@ -7021,14 +7057,14 @@ async function discoverJavaService(scanPath, dir) {
|
|
|
7021
7057
|
name,
|
|
7022
7058
|
language: "java",
|
|
7023
7059
|
dependencies,
|
|
7024
|
-
repoPath:
|
|
7060
|
+
repoPath: path16.relative(scanPath, dir)
|
|
7025
7061
|
};
|
|
7026
7062
|
return { pkg, dir, node };
|
|
7027
7063
|
}
|
|
7028
7064
|
|
|
7029
7065
|
// src/extract/kotlin.ts
|
|
7030
|
-
import { promises as
|
|
7031
|
-
import
|
|
7066
|
+
import { promises as fs15 } from "fs";
|
|
7067
|
+
import path17 from "path";
|
|
7032
7068
|
import { NodeType as NodeType11, serviceId as serviceId7 } from "@neat.is/types";
|
|
7033
7069
|
var KOTLIN_SOURCE_EXT = ".kt";
|
|
7034
7070
|
var JAVA_SOURCE_EXT = ".java";
|
|
@@ -7038,15 +7074,15 @@ async function dominantJvmSourceLanguage(dir) {
|
|
|
7038
7074
|
let java = 0;
|
|
7039
7075
|
async function recurse(current, depth) {
|
|
7040
7076
|
if (depth > SOURCE_PROBE_MAX_DEPTH) return;
|
|
7041
|
-
const entries = await
|
|
7077
|
+
const entries = await fs15.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
7042
7078
|
for (const entry of entries) {
|
|
7043
7079
|
if (entry.isDirectory()) {
|
|
7044
7080
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
7045
|
-
await recurse(
|
|
7081
|
+
await recurse(path17.join(current, entry.name), depth + 1);
|
|
7046
7082
|
continue;
|
|
7047
7083
|
}
|
|
7048
7084
|
if (!entry.isFile()) continue;
|
|
7049
|
-
const ext =
|
|
7085
|
+
const ext = path17.extname(entry.name).toLowerCase();
|
|
7050
7086
|
if (ext === KOTLIN_SOURCE_EXT) kt++;
|
|
7051
7087
|
else if (ext === JAVA_SOURCE_EXT) java++;
|
|
7052
7088
|
}
|
|
@@ -7057,17 +7093,17 @@ async function dominantJvmSourceLanguage(dir) {
|
|
|
7057
7093
|
return void 0;
|
|
7058
7094
|
}
|
|
7059
7095
|
async function readJvmManifest(dir) {
|
|
7060
|
-
const pomPath =
|
|
7061
|
-
const gradlePath =
|
|
7062
|
-
const gradleKtsPath =
|
|
7096
|
+
const pomPath = path17.join(dir, JAVA_MANIFESTS[0]);
|
|
7097
|
+
const gradlePath = path17.join(dir, JAVA_MANIFESTS[1]);
|
|
7098
|
+
const gradleKtsPath = path17.join(dir, JAVA_MANIFESTS[2]);
|
|
7063
7099
|
if (await exists(pomPath)) {
|
|
7064
|
-
return parsePomXml(await
|
|
7100
|
+
return parsePomXml(await fs15.readFile(pomPath, "utf8").catch(() => ""));
|
|
7065
7101
|
}
|
|
7066
7102
|
if (await exists(gradlePath)) {
|
|
7067
|
-
return parseGradle(await
|
|
7103
|
+
return parseGradle(await fs15.readFile(gradlePath, "utf8").catch(() => ""));
|
|
7068
7104
|
}
|
|
7069
7105
|
if (await exists(gradleKtsPath)) {
|
|
7070
|
-
return parseGradle(await
|
|
7106
|
+
return parseGradle(await fs15.readFile(gradleKtsPath, "utf8").catch(() => ""));
|
|
7071
7107
|
}
|
|
7072
7108
|
return null;
|
|
7073
7109
|
}
|
|
@@ -7076,7 +7112,7 @@ async function discoverKotlinService(scanPath, dir) {
|
|
|
7076
7112
|
if (await dominantJvmSourceLanguage(dir) !== "kotlin") return null;
|
|
7077
7113
|
const manifest = await readJvmManifest(dir);
|
|
7078
7114
|
if (!manifest) return null;
|
|
7079
|
-
const name = manifest.name?.trim() ||
|
|
7115
|
+
const name = manifest.name?.trim() || path17.basename(dir);
|
|
7080
7116
|
const dependencies = manifest.dependencies;
|
|
7081
7117
|
const pkg = { name, dependencies };
|
|
7082
7118
|
const node = {
|
|
@@ -7085,19 +7121,19 @@ async function discoverKotlinService(scanPath, dir) {
|
|
|
7085
7121
|
name,
|
|
7086
7122
|
language: "kotlin",
|
|
7087
7123
|
dependencies,
|
|
7088
|
-
repoPath:
|
|
7124
|
+
repoPath: path17.relative(scanPath, dir)
|
|
7089
7125
|
};
|
|
7090
7126
|
return { pkg, dir, node };
|
|
7091
7127
|
}
|
|
7092
7128
|
|
|
7093
7129
|
// src/extract/rust.ts
|
|
7094
|
-
import { promises as
|
|
7095
|
-
import
|
|
7130
|
+
import { promises as fs16 } from "fs";
|
|
7131
|
+
import path18 from "path";
|
|
7096
7132
|
import { parse as parseToml2 } from "smol-toml";
|
|
7097
7133
|
import { NodeType as NodeType12, serviceId as serviceId8 } from "@neat.is/types";
|
|
7098
7134
|
var CARGO_MANIFEST = "Cargo.toml";
|
|
7099
7135
|
async function hasRustManifest(dir) {
|
|
7100
|
-
return exists(
|
|
7136
|
+
return exists(path18.join(dir, CARGO_MANIFEST));
|
|
7101
7137
|
}
|
|
7102
7138
|
function parseCargoToml(source) {
|
|
7103
7139
|
const dependencies = {};
|
|
@@ -7122,10 +7158,10 @@ function parseCargoToml(source) {
|
|
|
7122
7158
|
return { name, dependencies };
|
|
7123
7159
|
}
|
|
7124
7160
|
async function discoverRustService(scanPath, dir) {
|
|
7125
|
-
const cargoPath =
|
|
7161
|
+
const cargoPath = path18.join(dir, CARGO_MANIFEST);
|
|
7126
7162
|
if (!await exists(cargoPath)) return null;
|
|
7127
|
-
const manifest = parseCargoToml(await
|
|
7128
|
-
const name = manifest.name?.trim() ||
|
|
7163
|
+
const manifest = parseCargoToml(await fs16.readFile(cargoPath, "utf8").catch(() => ""));
|
|
7164
|
+
const name = manifest.name?.trim() || path18.basename(dir);
|
|
7129
7165
|
const dependencies = manifest.dependencies;
|
|
7130
7166
|
const pkg = { name, dependencies };
|
|
7131
7167
|
const node = {
|
|
@@ -7134,18 +7170,18 @@ async function discoverRustService(scanPath, dir) {
|
|
|
7134
7170
|
name,
|
|
7135
7171
|
language: "rust",
|
|
7136
7172
|
dependencies,
|
|
7137
|
-
repoPath:
|
|
7173
|
+
repoPath: path18.relative(scanPath, dir)
|
|
7138
7174
|
};
|
|
7139
7175
|
return { pkg, dir, node };
|
|
7140
7176
|
}
|
|
7141
7177
|
|
|
7142
7178
|
// src/extract/cpp.ts
|
|
7143
|
-
import { promises as
|
|
7144
|
-
import
|
|
7179
|
+
import { promises as fs17 } from "fs";
|
|
7180
|
+
import path19 from "path";
|
|
7145
7181
|
import { NodeType as NodeType13, serviceId as serviceId9 } from "@neat.is/types";
|
|
7146
7182
|
var CMAKE_MANIFEST = "CMakeLists.txt";
|
|
7147
7183
|
async function hasCppManifest(dir) {
|
|
7148
|
-
return exists(
|
|
7184
|
+
return exists(path19.join(dir, CMAKE_MANIFEST));
|
|
7149
7185
|
}
|
|
7150
7186
|
function parseCMakeProjectName(source) {
|
|
7151
7187
|
const withoutComments = source.replace(/#.*$/gm, "");
|
|
@@ -7153,10 +7189,10 @@ function parseCMakeProjectName(source) {
|
|
|
7153
7189
|
return m?.[1];
|
|
7154
7190
|
}
|
|
7155
7191
|
async function discoverCppService(scanPath, dir) {
|
|
7156
|
-
const cmakePath =
|
|
7192
|
+
const cmakePath = path19.join(dir, CMAKE_MANIFEST);
|
|
7157
7193
|
if (!await exists(cmakePath)) return null;
|
|
7158
|
-
const source = await
|
|
7159
|
-
const name = parseCMakeProjectName(source)?.trim() ||
|
|
7194
|
+
const source = await fs17.readFile(cmakePath, "utf8").catch(() => "");
|
|
7195
|
+
const name = parseCMakeProjectName(source)?.trim() || path19.basename(dir);
|
|
7160
7196
|
const dependencies = {};
|
|
7161
7197
|
const pkg = { name, dependencies };
|
|
7162
7198
|
const node = {
|
|
@@ -7165,14 +7201,14 @@ async function discoverCppService(scanPath, dir) {
|
|
|
7165
7201
|
name,
|
|
7166
7202
|
language: "cpp",
|
|
7167
7203
|
dependencies,
|
|
7168
|
-
repoPath:
|
|
7204
|
+
repoPath: path19.relative(scanPath, dir)
|
|
7169
7205
|
};
|
|
7170
7206
|
return { pkg, dir, node };
|
|
7171
7207
|
}
|
|
7172
7208
|
|
|
7173
7209
|
// src/extract/dockerfile-service.ts
|
|
7174
|
-
import { promises as
|
|
7175
|
-
import
|
|
7210
|
+
import { promises as fs18 } from "fs";
|
|
7211
|
+
import path20 from "path";
|
|
7176
7212
|
import { NodeType as NodeType14, serviceId as serviceId10 } from "@neat.is/types";
|
|
7177
7213
|
function languageForSourceExt(ext) {
|
|
7178
7214
|
switch (ext) {
|
|
@@ -7214,12 +7250,12 @@ function languageForSourceExt(ext) {
|
|
|
7214
7250
|
}
|
|
7215
7251
|
var LANGUAGE_PRECEDENCE = ["typescript", "javascript", "python", "go", "ruby", "php", "csharp", "java", "kotlin", "rust", "cpp"];
|
|
7216
7252
|
async function inferServiceLanguage(dir) {
|
|
7217
|
-
const entries = await
|
|
7253
|
+
const entries = await fs18.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
7218
7254
|
const counts = /* @__PURE__ */ new Map();
|
|
7219
7255
|
for (const entry of entries) {
|
|
7220
7256
|
if (!entry.isFile()) continue;
|
|
7221
7257
|
if (isNeatAuthoredSourceFile(entry.name)) continue;
|
|
7222
|
-
const ext =
|
|
7258
|
+
const ext = path20.extname(entry.name).toLowerCase();
|
|
7223
7259
|
if (!SERVICE_FILE_EXTENSIONS.has(ext)) continue;
|
|
7224
7260
|
const lang = languageForSourceExt(ext);
|
|
7225
7261
|
if (!lang) continue;
|
|
@@ -7238,7 +7274,7 @@ async function inferServiceLanguage(dir) {
|
|
|
7238
7274
|
return best;
|
|
7239
7275
|
}
|
|
7240
7276
|
async function hasLanguageManifest(dir) {
|
|
7241
|
-
return await exists(
|
|
7277
|
+
return await exists(path20.join(dir, "package.json")) || await exists(path20.join(dir, "pyproject.toml")) || await exists(path20.join(dir, "requirements.txt")) || await exists(path20.join(dir, "setup.py")) || await exists(path20.join(dir, "go.mod")) || await exists(path20.join(dir, "Gemfile")) || await exists(path20.join(dir, "composer.json")) || // C#'s marker is a glob (`*.csproj` / `*.sln`), so it reads the directory
|
|
7242
7278
|
// rather than probing a fixed filename (ADR-196).
|
|
7243
7279
|
await hasCsharpProject(dir) || // Java (and its JVM sibling Kotlin, ADR-199) key on the same Maven / Gradle
|
|
7244
7280
|
// build manifest (ADR-197), so this one check stands the Dockerfile fallback
|
|
@@ -7248,15 +7284,15 @@ async function hasLanguageManifest(dir) {
|
|
|
7248
7284
|
await hasCppManifest(dir);
|
|
7249
7285
|
}
|
|
7250
7286
|
async function hasDockerfileDeclaredService(dir) {
|
|
7251
|
-
if (!await exists(
|
|
7287
|
+
if (!await exists(path20.join(dir, "Dockerfile"))) return false;
|
|
7252
7288
|
return await inferServiceLanguage(dir) !== null;
|
|
7253
7289
|
}
|
|
7254
7290
|
async function discoverDockerfileService(scanPath, dir) {
|
|
7255
|
-
if (!await exists(
|
|
7291
|
+
if (!await exists(path20.join(dir, "Dockerfile"))) return null;
|
|
7256
7292
|
if (await hasLanguageManifest(dir)) return null;
|
|
7257
7293
|
const language = await inferServiceLanguage(dir);
|
|
7258
7294
|
if (!language) return null;
|
|
7259
|
-
const name =
|
|
7295
|
+
const name = path20.basename(dir);
|
|
7260
7296
|
const pkg = { name, dependencies: {} };
|
|
7261
7297
|
const node = {
|
|
7262
7298
|
id: serviceId10(name),
|
|
@@ -7264,23 +7300,23 @@ async function discoverDockerfileService(scanPath, dir) {
|
|
|
7264
7300
|
name,
|
|
7265
7301
|
language,
|
|
7266
7302
|
dependencies: {},
|
|
7267
|
-
repoPath:
|
|
7303
|
+
repoPath: path20.relative(scanPath, dir)
|
|
7268
7304
|
};
|
|
7269
7305
|
return { pkg, dir, node };
|
|
7270
7306
|
}
|
|
7271
7307
|
|
|
7272
7308
|
// src/extract/owners.ts
|
|
7273
|
-
import { promises as
|
|
7274
|
-
import
|
|
7309
|
+
import { promises as fs19 } from "fs";
|
|
7310
|
+
import path21 from "path";
|
|
7275
7311
|
import { minimatch } from "minimatch";
|
|
7276
7312
|
async function loadCodeowners(scanPath) {
|
|
7277
7313
|
const candidates = [
|
|
7278
|
-
|
|
7279
|
-
|
|
7314
|
+
path21.join(scanPath, "CODEOWNERS"),
|
|
7315
|
+
path21.join(scanPath, ".github", "CODEOWNERS")
|
|
7280
7316
|
];
|
|
7281
7317
|
for (const file of candidates) {
|
|
7282
7318
|
if (await exists(file)) {
|
|
7283
|
-
const raw = await
|
|
7319
|
+
const raw = await fs19.readFile(file, "utf8");
|
|
7284
7320
|
return parseCodeowners(raw);
|
|
7285
7321
|
}
|
|
7286
7322
|
}
|
|
@@ -7298,7 +7334,7 @@ function parseCodeowners(raw) {
|
|
|
7298
7334
|
return { rules };
|
|
7299
7335
|
}
|
|
7300
7336
|
function matchOwner(file, repoPath) {
|
|
7301
|
-
const normalized = repoPath.split(
|
|
7337
|
+
const normalized = repoPath.split(path21.sep).join("/");
|
|
7302
7338
|
for (const rule of file.rules) {
|
|
7303
7339
|
if (matchesPattern(rule.pattern, normalized)) return rule.owners;
|
|
7304
7340
|
}
|
|
@@ -7314,7 +7350,7 @@ function matchesPattern(rawPattern, repoPath) {
|
|
|
7314
7350
|
return false;
|
|
7315
7351
|
}
|
|
7316
7352
|
async function readPackageJsonAuthor(serviceDir) {
|
|
7317
|
-
const pkgPath =
|
|
7353
|
+
const pkgPath = path21.join(serviceDir, "package.json");
|
|
7318
7354
|
if (!await exists(pkgPath)) return null;
|
|
7319
7355
|
try {
|
|
7320
7356
|
const pkg = await readJson(pkgPath);
|
|
@@ -7351,36 +7387,36 @@ function workspaceGlobs(pkg) {
|
|
|
7351
7387
|
return null;
|
|
7352
7388
|
}
|
|
7353
7389
|
async function hasPythonManifest(dir) {
|
|
7354
|
-
return await exists(
|
|
7390
|
+
return await exists(path22.join(dir, "pyproject.toml")) || await exists(path22.join(dir, "requirements.txt")) || await exists(path22.join(dir, "setup.py"));
|
|
7355
7391
|
}
|
|
7356
7392
|
async function hasGoManifest(dir) {
|
|
7357
|
-
return exists(
|
|
7393
|
+
return exists(path22.join(dir, "go.mod"));
|
|
7358
7394
|
}
|
|
7359
7395
|
async function hasRubyManifest(dir) {
|
|
7360
|
-
return exists(
|
|
7396
|
+
return exists(path22.join(dir, "Gemfile"));
|
|
7361
7397
|
}
|
|
7362
7398
|
async function hasPhpManifest(dir) {
|
|
7363
|
-
return exists(
|
|
7399
|
+
return exists(path22.join(dir, "composer.json"));
|
|
7364
7400
|
}
|
|
7365
7401
|
async function hasCsharpManifest(dir) {
|
|
7366
7402
|
return hasCsharpProject(dir);
|
|
7367
7403
|
}
|
|
7368
7404
|
async function loadGitignore(scanPath) {
|
|
7369
|
-
const gitignorePath =
|
|
7405
|
+
const gitignorePath = path22.join(scanPath, ".gitignore");
|
|
7370
7406
|
if (!await exists(gitignorePath)) return null;
|
|
7371
|
-
const raw = await
|
|
7407
|
+
const raw = await fs20.readFile(gitignorePath, "utf8");
|
|
7372
7408
|
return ignore2().add(raw);
|
|
7373
7409
|
}
|
|
7374
7410
|
async function walkDirs(start, scanPath, options, visit) {
|
|
7375
7411
|
async function recurse(current, depth) {
|
|
7376
7412
|
if (depth > options.maxDepth) return;
|
|
7377
|
-
const entries = await
|
|
7413
|
+
const entries = await fs20.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
7378
7414
|
for (const entry of entries) {
|
|
7379
7415
|
if (!entry.isDirectory()) continue;
|
|
7380
7416
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
7381
|
-
const child =
|
|
7417
|
+
const child = path22.join(current, entry.name);
|
|
7382
7418
|
if (options.ig) {
|
|
7383
|
-
const rel =
|
|
7419
|
+
const rel = path22.relative(scanPath, child).split(path22.sep).join("/");
|
|
7384
7420
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
7385
7421
|
}
|
|
7386
7422
|
if (await isPythonVenvDir(child)) continue;
|
|
@@ -7396,8 +7432,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
7396
7432
|
for (const raw of globs) {
|
|
7397
7433
|
const pattern = raw.replace(/^\.\//, "");
|
|
7398
7434
|
if (!pattern.includes("*")) {
|
|
7399
|
-
const candidate =
|
|
7400
|
-
if (await exists(
|
|
7435
|
+
const candidate = path22.join(scanPath, pattern);
|
|
7436
|
+
if (await exists(path22.join(candidate, "package.json"))) found.add(candidate);
|
|
7401
7437
|
continue;
|
|
7402
7438
|
}
|
|
7403
7439
|
const segments = pattern.split("/");
|
|
@@ -7406,13 +7442,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
7406
7442
|
if (seg.includes("*")) break;
|
|
7407
7443
|
staticSegments.push(seg);
|
|
7408
7444
|
}
|
|
7409
|
-
const start =
|
|
7445
|
+
const start = path22.join(scanPath, ...staticSegments);
|
|
7410
7446
|
if (!await exists(start)) continue;
|
|
7411
7447
|
const hasDoubleStar = pattern.includes("**");
|
|
7412
7448
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
7413
7449
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
7414
|
-
const rel =
|
|
7415
|
-
if (minimatch2(rel, pattern) && await exists(
|
|
7450
|
+
const rel = path22.relative(scanPath, dir).split(path22.sep).join("/");
|
|
7451
|
+
if (minimatch2(rel, pattern) && await exists(path22.join(dir, "package.json"))) {
|
|
7416
7452
|
found.add(dir);
|
|
7417
7453
|
}
|
|
7418
7454
|
});
|
|
@@ -7435,18 +7471,18 @@ function detectJsFramework(pkg) {
|
|
|
7435
7471
|
async function detectJsServiceLanguage(dir, pkg) {
|
|
7436
7472
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
7437
7473
|
if (deps["typescript"] !== void 0) return "typescript";
|
|
7438
|
-
const entries = await
|
|
7474
|
+
const entries = await fs20.readdir(dir).catch(() => []);
|
|
7439
7475
|
if (entries.some((name) => /^tsconfig(\..+)?\.json$/.test(name))) return "typescript";
|
|
7440
7476
|
return "javascript";
|
|
7441
7477
|
}
|
|
7442
7478
|
async function discoverNodeService(scanPath, dir) {
|
|
7443
|
-
const pkgPath =
|
|
7479
|
+
const pkgPath = path22.join(dir, "package.json");
|
|
7444
7480
|
if (!await exists(pkgPath)) return null;
|
|
7445
7481
|
let pkg;
|
|
7446
7482
|
try {
|
|
7447
7483
|
pkg = await readJson(pkgPath);
|
|
7448
7484
|
} catch (err) {
|
|
7449
|
-
recordExtractionError("services",
|
|
7485
|
+
recordExtractionError("services", path22.relative(scanPath, pkgPath), err);
|
|
7450
7486
|
return null;
|
|
7451
7487
|
}
|
|
7452
7488
|
if (!pkg.name) return null;
|
|
@@ -7459,7 +7495,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
7459
7495
|
language,
|
|
7460
7496
|
version: pkg.version,
|
|
7461
7497
|
dependencies: pkg.dependencies ?? {},
|
|
7462
|
-
repoPath:
|
|
7498
|
+
repoPath: path22.relative(scanPath, dir),
|
|
7463
7499
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {},
|
|
7464
7500
|
...framework ? { framework } : {}
|
|
7465
7501
|
};
|
|
@@ -7476,12 +7512,12 @@ async function discoverPyService(scanPath, dir) {
|
|
|
7476
7512
|
language: "python",
|
|
7477
7513
|
version: py.version,
|
|
7478
7514
|
dependencies: py.dependencies,
|
|
7479
|
-
repoPath:
|
|
7515
|
+
repoPath: path22.relative(scanPath, dir)
|
|
7480
7516
|
};
|
|
7481
7517
|
return { pkg, dir, node };
|
|
7482
7518
|
}
|
|
7483
7519
|
async function discoverServices(scanPath) {
|
|
7484
|
-
const rootPkgPath =
|
|
7520
|
+
const rootPkgPath = path22.join(scanPath, "package.json");
|
|
7485
7521
|
let rootPkg = null;
|
|
7486
7522
|
if (await exists(rootPkgPath)) {
|
|
7487
7523
|
try {
|
|
@@ -7489,7 +7525,7 @@ async function discoverServices(scanPath) {
|
|
|
7489
7525
|
} catch (err) {
|
|
7490
7526
|
recordExtractionError(
|
|
7491
7527
|
"services workspaces",
|
|
7492
|
-
|
|
7528
|
+
path22.relative(scanPath, rootPkgPath),
|
|
7493
7529
|
err
|
|
7494
7530
|
);
|
|
7495
7531
|
}
|
|
@@ -7510,7 +7546,7 @@ async function discoverServices(scanPath) {
|
|
|
7510
7546
|
scanPath,
|
|
7511
7547
|
{ maxDepth: parseScanDepth(), ig },
|
|
7512
7548
|
async (dir) => {
|
|
7513
|
-
if (await exists(
|
|
7549
|
+
if (await exists(path22.join(dir, "package.json"))) {
|
|
7514
7550
|
candidateDirs.push(dir);
|
|
7515
7551
|
} else if (await hasPythonManifest(dir) || await hasGoManifest(dir) || await hasRubyManifest(dir) || await hasPhpManifest(dir) || await hasCsharpManifest(dir) || await hasJavaManifest(dir) || await hasRustManifest(dir) || await hasCppManifest(dir)) {
|
|
7516
7552
|
candidateDirs.push(dir);
|
|
@@ -7538,8 +7574,8 @@ async function discoverServices(scanPath) {
|
|
|
7538
7574
|
seenDirs.add(service.dir);
|
|
7539
7575
|
const existingDir = seen.get(service.node.name);
|
|
7540
7576
|
if (existingDir !== void 0) {
|
|
7541
|
-
const a =
|
|
7542
|
-
const b =
|
|
7577
|
+
const a = path22.relative(scanPath, existingDir) || ".";
|
|
7578
|
+
const b = path22.relative(scanPath, dir) || ".";
|
|
7543
7579
|
console.warn(
|
|
7544
7580
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
7545
7581
|
);
|
|
@@ -7553,14 +7589,14 @@ async function discoverServices(scanPath) {
|
|
|
7553
7589
|
const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
|
|
7554
7590
|
if (owner !== void 0) service.node.owner = owner;
|
|
7555
7591
|
}
|
|
7556
|
-
const resolvedDirs = out.map((s) =>
|
|
7592
|
+
const resolvedDirs = out.map((s) => path22.resolve(s.dir));
|
|
7557
7593
|
for (let i = 0; i < out.length; i++) {
|
|
7558
7594
|
const self = resolvedDirs[i];
|
|
7559
7595
|
const nested = [];
|
|
7560
7596
|
for (let j = 0; j < out.length; j++) {
|
|
7561
7597
|
if (i === j) continue;
|
|
7562
7598
|
const other = resolvedDirs[j];
|
|
7563
|
-
if (other.startsWith(self +
|
|
7599
|
+
if (other.startsWith(self + path22.sep)) nested.push(other);
|
|
7564
7600
|
}
|
|
7565
7601
|
out[i].excludeDirs = nested;
|
|
7566
7602
|
}
|
|
@@ -7586,8 +7622,8 @@ function addServiceNodes(graph, services) {
|
|
|
7586
7622
|
}
|
|
7587
7623
|
|
|
7588
7624
|
// src/extract/aliases.ts
|
|
7589
|
-
import
|
|
7590
|
-
import { promises as
|
|
7625
|
+
import path23 from "path";
|
|
7626
|
+
import { promises as fs21 } from "fs";
|
|
7591
7627
|
import { parseAllDocuments } from "yaml";
|
|
7592
7628
|
import { NodeType as NodeType16 } from "@neat.is/types";
|
|
7593
7629
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -7614,14 +7650,14 @@ function indexServicesByName(services) {
|
|
|
7614
7650
|
const map = /* @__PURE__ */ new Map();
|
|
7615
7651
|
for (const s of services) {
|
|
7616
7652
|
map.set(s.node.name, s.node.id);
|
|
7617
|
-
map.set(
|
|
7653
|
+
map.set(path23.basename(s.dir), s.node.id);
|
|
7618
7654
|
}
|
|
7619
7655
|
return map;
|
|
7620
7656
|
}
|
|
7621
7657
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
7622
7658
|
let composePath = null;
|
|
7623
7659
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
7624
|
-
const abs =
|
|
7660
|
+
const abs = path23.join(scanPath, name);
|
|
7625
7661
|
if (await exists(abs)) {
|
|
7626
7662
|
composePath = abs;
|
|
7627
7663
|
break;
|
|
@@ -7634,7 +7670,7 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
7634
7670
|
} catch (err) {
|
|
7635
7671
|
recordExtractionError(
|
|
7636
7672
|
"aliases compose",
|
|
7637
|
-
|
|
7673
|
+
path23.relative(scanPath, composePath),
|
|
7638
7674
|
err
|
|
7639
7675
|
);
|
|
7640
7676
|
return;
|
|
@@ -7677,11 +7713,11 @@ function parseDockerfileLabels(content) {
|
|
|
7677
7713
|
}
|
|
7678
7714
|
async function collectDockerfileAliases(graph, services) {
|
|
7679
7715
|
for (const service of services) {
|
|
7680
|
-
const dockerfilePath =
|
|
7716
|
+
const dockerfilePath = path23.join(service.dir, "Dockerfile");
|
|
7681
7717
|
if (!await exists(dockerfilePath)) continue;
|
|
7682
7718
|
let content;
|
|
7683
7719
|
try {
|
|
7684
|
-
content = await
|
|
7720
|
+
content = await fs21.readFile(dockerfilePath, "utf8");
|
|
7685
7721
|
} catch (err) {
|
|
7686
7722
|
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
7687
7723
|
continue;
|
|
@@ -7693,15 +7729,15 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
7693
7729
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
7694
7730
|
if (depth > max) return [];
|
|
7695
7731
|
const out = [];
|
|
7696
|
-
const entries = await
|
|
7732
|
+
const entries = await fs21.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
7697
7733
|
for (const entry of entries) {
|
|
7698
7734
|
if (entry.isDirectory()) {
|
|
7699
7735
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
7700
|
-
const child =
|
|
7736
|
+
const child = path23.join(start, entry.name);
|
|
7701
7737
|
if (await isPythonVenvDir(child)) continue;
|
|
7702
7738
|
out.push(...await walkYamlFiles(child, depth + 1, max));
|
|
7703
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
7704
|
-
out.push(
|
|
7739
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path23.extname(entry.name))) {
|
|
7740
|
+
out.push(path23.join(start, entry.name));
|
|
7705
7741
|
}
|
|
7706
7742
|
}
|
|
7707
7743
|
return out;
|
|
@@ -7728,7 +7764,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
7728
7764
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
7729
7765
|
const files = await walkYamlFiles(scanPath);
|
|
7730
7766
|
for (const file of files) {
|
|
7731
|
-
const content = await
|
|
7767
|
+
const content = await fs21.readFile(file, "utf8");
|
|
7732
7768
|
let docs;
|
|
7733
7769
|
try {
|
|
7734
7770
|
docs = parseAllDocuments(content).map((d) => d.toJSON());
|
|
@@ -7752,14 +7788,14 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
7752
7788
|
}
|
|
7753
7789
|
|
|
7754
7790
|
// src/extract/files.ts
|
|
7755
|
-
import
|
|
7791
|
+
import path24 from "path";
|
|
7756
7792
|
async function addFiles(graph, services) {
|
|
7757
7793
|
let nodesAdded = 0;
|
|
7758
7794
|
let edgesAdded = 0;
|
|
7759
7795
|
for (const service of services) {
|
|
7760
7796
|
const filePaths = await walkSourceFiles(service.dir, service.excludeDirs);
|
|
7761
7797
|
for (const filePath of filePaths) {
|
|
7762
|
-
const relPath = toPosix(
|
|
7798
|
+
const relPath = toPosix(path24.relative(service.dir, filePath));
|
|
7763
7799
|
const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
7764
7800
|
graph,
|
|
7765
7801
|
service.pkg.name,
|
|
@@ -7774,7 +7810,7 @@ async function addFiles(graph, services) {
|
|
|
7774
7810
|
}
|
|
7775
7811
|
|
|
7776
7812
|
// src/extract/symbols.ts
|
|
7777
|
-
import
|
|
7813
|
+
import path25 from "path";
|
|
7778
7814
|
import Parser3 from "tree-sitter";
|
|
7779
7815
|
import JavaScript3 from "tree-sitter-javascript";
|
|
7780
7816
|
import TypeScript from "tree-sitter-typescript";
|
|
@@ -8533,10 +8569,10 @@ async function addSymbols(graph, services) {
|
|
|
8533
8569
|
for (const service of services) {
|
|
8534
8570
|
const files = await loadSourceFiles(service.dir, service.excludeDirs);
|
|
8535
8571
|
for (const file of files) {
|
|
8536
|
-
const ext =
|
|
8572
|
+
const ext = path25.extname(file.path);
|
|
8537
8573
|
const parser = parserForExt5(ext);
|
|
8538
8574
|
if (!parser) continue;
|
|
8539
|
-
const relPath = toPosix(
|
|
8575
|
+
const relPath = toPosix(path25.relative(service.dir, file.path));
|
|
8540
8576
|
let defs;
|
|
8541
8577
|
try {
|
|
8542
8578
|
const tree = parseSource3(parser, file.content);
|
|
@@ -8595,7 +8631,7 @@ async function addSymbols(graph, services) {
|
|
|
8595
8631
|
}
|
|
8596
8632
|
|
|
8597
8633
|
// src/extract/symbol-edges.ts
|
|
8598
|
-
import
|
|
8634
|
+
import path26 from "path";
|
|
8599
8635
|
import Parser4 from "tree-sitter";
|
|
8600
8636
|
import {
|
|
8601
8637
|
EdgeType as EdgeType8,
|
|
@@ -8709,10 +8745,10 @@ async function addSymbolEdges(graph, services) {
|
|
|
8709
8745
|
const tsPaths = await loadTsPathConfig(service.dir);
|
|
8710
8746
|
const files = await loadSourceFiles(service.dir, service.excludeDirs);
|
|
8711
8747
|
for (const file of files) {
|
|
8712
|
-
const parser = parserForExt5(
|
|
8748
|
+
const parser = parserForExt5(path26.extname(file.path));
|
|
8713
8749
|
if (!parser) continue;
|
|
8714
|
-
const relPath = toPosix(
|
|
8715
|
-
const fileDir =
|
|
8750
|
+
const relPath = toPosix(path26.relative(service.dir, file.path));
|
|
8751
|
+
const fileDir = path26.dirname(file.path);
|
|
8716
8752
|
let root;
|
|
8717
8753
|
try {
|
|
8718
8754
|
root = parseSource3(parser, file.content).rootNode;
|
|
@@ -8846,7 +8882,7 @@ async function addSymbolEdges(graph, services) {
|
|
|
8846
8882
|
}
|
|
8847
8883
|
|
|
8848
8884
|
// src/extract/actions.ts
|
|
8849
|
-
import
|
|
8885
|
+
import path27 from "path";
|
|
8850
8886
|
import Parser5 from "tree-sitter";
|
|
8851
8887
|
import {
|
|
8852
8888
|
EdgeType as EdgeType9,
|
|
@@ -9000,9 +9036,9 @@ async function addServerActions(graph, services) {
|
|
|
9000
9036
|
const files = await loadSourceFiles(service.dir, service.excludeDirs);
|
|
9001
9037
|
for (const file of files) {
|
|
9002
9038
|
if (isTestPath(file.path)) continue;
|
|
9003
|
-
const parser = parserForExt5(
|
|
9039
|
+
const parser = parserForExt5(path27.extname(file.path));
|
|
9004
9040
|
if (!parser) continue;
|
|
9005
|
-
const relPath = toPosix(
|
|
9041
|
+
const relPath = toPosix(path27.relative(service.dir, file.path));
|
|
9006
9042
|
let root;
|
|
9007
9043
|
try {
|
|
9008
9044
|
root = parseSource3(parser, file.content).rootNode;
|
|
@@ -9063,10 +9099,10 @@ async function addServerActions(graph, services) {
|
|
|
9063
9099
|
}
|
|
9064
9100
|
for (const file of files) {
|
|
9065
9101
|
if (isTestPath(file.path)) continue;
|
|
9066
|
-
const parser = parserForExt5(
|
|
9102
|
+
const parser = parserForExt5(path27.extname(file.path));
|
|
9067
9103
|
if (!parser) continue;
|
|
9068
|
-
const relPath = toPosix(
|
|
9069
|
-
const fileDir =
|
|
9104
|
+
const relPath = toPosix(path27.relative(service.dir, file.path));
|
|
9105
|
+
const fileDir = path27.dirname(file.path);
|
|
9070
9106
|
let root;
|
|
9071
9107
|
try {
|
|
9072
9108
|
root = parseSource3(parser, file.content).rootNode;
|
|
@@ -9118,7 +9154,7 @@ async function addServerActions(graph, services) {
|
|
|
9118
9154
|
}
|
|
9119
9155
|
|
|
9120
9156
|
// src/extract/databases/index.ts
|
|
9121
|
-
import
|
|
9157
|
+
import path36 from "path";
|
|
9122
9158
|
import {
|
|
9123
9159
|
EdgeType as EdgeType10,
|
|
9124
9160
|
NodeType as NodeType20,
|
|
@@ -9129,9 +9165,9 @@ import {
|
|
|
9129
9165
|
} from "@neat.is/types";
|
|
9130
9166
|
|
|
9131
9167
|
// src/extract/databases/db-config-yaml.ts
|
|
9132
|
-
import
|
|
9168
|
+
import path28 from "path";
|
|
9133
9169
|
async function parse(serviceDir) {
|
|
9134
|
-
const yamlPath =
|
|
9170
|
+
const yamlPath = path28.join(serviceDir, "db-config.yaml");
|
|
9135
9171
|
if (!await exists(yamlPath)) return [];
|
|
9136
9172
|
const raw = await readYaml(yamlPath);
|
|
9137
9173
|
return [
|
|
@@ -9148,12 +9184,12 @@ async function parse(serviceDir) {
|
|
|
9148
9184
|
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
9149
9185
|
|
|
9150
9186
|
// src/extract/databases/dotenv.ts
|
|
9151
|
-
import { promises as
|
|
9152
|
-
import
|
|
9187
|
+
import { promises as fs23 } from "fs";
|
|
9188
|
+
import path30 from "path";
|
|
9153
9189
|
|
|
9154
9190
|
// src/extract/databases/shared.ts
|
|
9155
|
-
import { promises as
|
|
9156
|
-
import
|
|
9191
|
+
import { promises as fs22 } from "fs";
|
|
9192
|
+
import path29 from "path";
|
|
9157
9193
|
function schemeToEngine(scheme) {
|
|
9158
9194
|
const s = scheme.toLowerCase().split("+")[0];
|
|
9159
9195
|
switch (s) {
|
|
@@ -9192,18 +9228,18 @@ function parseConnectionString(url) {
|
|
|
9192
9228
|
}
|
|
9193
9229
|
async function readIfExists(filePath) {
|
|
9194
9230
|
try {
|
|
9195
|
-
return await
|
|
9231
|
+
return await fs22.readFile(filePath, "utf8");
|
|
9196
9232
|
} catch {
|
|
9197
9233
|
return null;
|
|
9198
9234
|
}
|
|
9199
9235
|
}
|
|
9200
9236
|
async function resolveEnvVar(serviceDir, name) {
|
|
9201
|
-
const entries = await
|
|
9237
|
+
const entries = await fs22.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
9202
9238
|
const envNames = entries.filter((e) => e.isFile() && (e.name === ".env" || e.name.startsWith(".env."))).map((e) => e.name);
|
|
9203
9239
|
const rank = (n) => n === ".env.local" ? 0 : n === ".env" ? 1 : 2;
|
|
9204
9240
|
envNames.sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
|
|
9205
9241
|
for (const fileName of envNames) {
|
|
9206
|
-
const content = await readIfExists(
|
|
9242
|
+
const content = await readIfExists(path29.join(serviceDir, fileName));
|
|
9207
9243
|
if (!content) continue;
|
|
9208
9244
|
for (const line of content.split("\n")) {
|
|
9209
9245
|
const trimmed = line.trim();
|
|
@@ -9222,7 +9258,7 @@ async function resolveEnvVar(serviceDir, name) {
|
|
|
9222
9258
|
}
|
|
9223
9259
|
async function findFirst(serviceDir, candidates) {
|
|
9224
9260
|
for (const rel of candidates) {
|
|
9225
|
-
const abs =
|
|
9261
|
+
const abs = path29.join(serviceDir, rel);
|
|
9226
9262
|
const content = await readIfExists(abs);
|
|
9227
9263
|
if (content !== null) return abs;
|
|
9228
9264
|
}
|
|
@@ -9280,15 +9316,15 @@ function parseDotenvLine(line) {
|
|
|
9280
9316
|
return { key, value };
|
|
9281
9317
|
}
|
|
9282
9318
|
async function parse2(serviceDir) {
|
|
9283
|
-
const entries = await
|
|
9319
|
+
const entries = await fs23.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
9284
9320
|
const configs = [];
|
|
9285
9321
|
const seen = /* @__PURE__ */ new Set();
|
|
9286
9322
|
for (const entry of entries) {
|
|
9287
9323
|
if (!entry.isFile()) continue;
|
|
9288
9324
|
const match = isConfigFile(entry.name);
|
|
9289
9325
|
if (!match.match || match.fileType !== "env") continue;
|
|
9290
|
-
const filePath =
|
|
9291
|
-
const content = await
|
|
9326
|
+
const filePath = path30.join(serviceDir, entry.name);
|
|
9327
|
+
const content = await fs23.readFile(filePath, "utf8");
|
|
9292
9328
|
for (const line of content.split("\n")) {
|
|
9293
9329
|
const parsed = parseDotenvLine(line);
|
|
9294
9330
|
if (!parsed) continue;
|
|
@@ -9306,9 +9342,9 @@ async function parse2(serviceDir) {
|
|
|
9306
9342
|
var dotenvParser = { name: ".env", parse: parse2 };
|
|
9307
9343
|
|
|
9308
9344
|
// src/extract/databases/prisma.ts
|
|
9309
|
-
import
|
|
9345
|
+
import path31 from "path";
|
|
9310
9346
|
async function parse3(serviceDir) {
|
|
9311
|
-
const schemaPath =
|
|
9347
|
+
const schemaPath = path31.join(serviceDir, "prisma", "schema.prisma");
|
|
9312
9348
|
const content = await readIfExists(schemaPath);
|
|
9313
9349
|
if (!content) return [];
|
|
9314
9350
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -9463,10 +9499,10 @@ async function parse5(serviceDir) {
|
|
|
9463
9499
|
var knexParser = { name: "knex", parse: parse5 };
|
|
9464
9500
|
|
|
9465
9501
|
// src/extract/databases/ormconfig.ts
|
|
9466
|
-
import
|
|
9502
|
+
import path32 from "path";
|
|
9467
9503
|
async function parse6(serviceDir) {
|
|
9468
9504
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
9469
|
-
const abs =
|
|
9505
|
+
const abs = path32.join(serviceDir, candidate);
|
|
9470
9506
|
if (!await exists(abs)) continue;
|
|
9471
9507
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
9472
9508
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -9524,9 +9560,9 @@ async function parse7(serviceDir) {
|
|
|
9524
9560
|
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
9525
9561
|
|
|
9526
9562
|
// src/extract/databases/sequelize.ts
|
|
9527
|
-
import
|
|
9563
|
+
import path33 from "path";
|
|
9528
9564
|
async function parse8(serviceDir) {
|
|
9529
|
-
const configPath =
|
|
9565
|
+
const configPath = path33.join(serviceDir, "config", "config.json");
|
|
9530
9566
|
if (!await exists(configPath)) return [];
|
|
9531
9567
|
const raw = await readJson(configPath);
|
|
9532
9568
|
const out = [];
|
|
@@ -9552,8 +9588,8 @@ async function parse8(serviceDir) {
|
|
|
9552
9588
|
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
9553
9589
|
|
|
9554
9590
|
// src/extract/databases/csharp.ts
|
|
9555
|
-
import { promises as
|
|
9556
|
-
import
|
|
9591
|
+
import { promises as fs24 } from "fs";
|
|
9592
|
+
import path34 from "path";
|
|
9557
9593
|
var CS_EXT = ".cs";
|
|
9558
9594
|
var NPGSQL_GATE = /\bUseNpgsql\b|\bNpgsql\b/;
|
|
9559
9595
|
var REDIS_GATE = /\bConnectionMultiplexer\b|\bStackExchange\.Redis\b|\bConfigurationOptions\.Parse\b|\bAddStackExchangeRedisCache\b/;
|
|
@@ -9600,12 +9636,12 @@ function parseRedisEndpoint(raw) {
|
|
|
9600
9636
|
return { host, port, database: "", engine: "redis", engineVersion: "unknown" };
|
|
9601
9637
|
}
|
|
9602
9638
|
async function resolveEnvUpTree(startDir, name) {
|
|
9603
|
-
let dir =
|
|
9639
|
+
let dir = path34.resolve(startDir);
|
|
9604
9640
|
for (let depth = 0; depth < 12; depth++) {
|
|
9605
9641
|
const value = await resolveEnvVar(dir, name);
|
|
9606
9642
|
if (value !== null) return value;
|
|
9607
|
-
const atRepoRoot = await
|
|
9608
|
-
const parent =
|
|
9643
|
+
const atRepoRoot = await fs24.access(path34.join(dir, ".git")).then(() => true).catch(() => false);
|
|
9644
|
+
const parent = path34.dirname(dir);
|
|
9609
9645
|
if (atRepoRoot || parent === dir) break;
|
|
9610
9646
|
dir = parent;
|
|
9611
9647
|
}
|
|
@@ -9660,12 +9696,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
|
|
|
9660
9696
|
}
|
|
9661
9697
|
async function parse9(serviceDir) {
|
|
9662
9698
|
const files = (await walkSourceFiles(serviceDir).catch(() => [])).filter(
|
|
9663
|
-
(f) =>
|
|
9699
|
+
(f) => path34.extname(f) === CS_EXT
|
|
9664
9700
|
);
|
|
9665
9701
|
if (files.length === 0) return [];
|
|
9666
9702
|
const sources = [];
|
|
9667
9703
|
for (const file of files) {
|
|
9668
|
-
const content = await
|
|
9704
|
+
const content = await fs24.readFile(file, "utf8").catch(() => null);
|
|
9669
9705
|
if (content !== null) sources.push({ file, content });
|
|
9670
9706
|
}
|
|
9671
9707
|
let pgGateFile = null;
|
|
@@ -9705,7 +9741,7 @@ async function parse9(serviceDir) {
|
|
|
9705
9741
|
var csharpParser = { name: "csharp", parse: parse9 };
|
|
9706
9742
|
|
|
9707
9743
|
// src/extract/databases/docker-compose.ts
|
|
9708
|
-
import
|
|
9744
|
+
import path35 from "path";
|
|
9709
9745
|
function portFromService(svc) {
|
|
9710
9746
|
for (const raw of svc.ports ?? []) {
|
|
9711
9747
|
const str = String(raw);
|
|
@@ -9732,7 +9768,7 @@ function databaseFromEnv(svc) {
|
|
|
9732
9768
|
}
|
|
9733
9769
|
async function parse10(serviceDir) {
|
|
9734
9770
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
9735
|
-
const abs =
|
|
9771
|
+
const abs = path35.join(serviceDir, name);
|
|
9736
9772
|
if (!await exists(abs)) continue;
|
|
9737
9773
|
const raw = await readYaml(abs);
|
|
9738
9774
|
if (!raw?.services) return [];
|
|
@@ -9905,7 +9941,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
9905
9941
|
discoveredVia: mergedDiscoveredVia
|
|
9906
9942
|
});
|
|
9907
9943
|
}
|
|
9908
|
-
const relConfigFile = toPosix(
|
|
9944
|
+
const relConfigFile = toPosix(path36.relative(service.dir, config.sourceFile));
|
|
9909
9945
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
9910
9946
|
graph,
|
|
9911
9947
|
service.pkg.name,
|
|
@@ -9914,7 +9950,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
9914
9950
|
);
|
|
9915
9951
|
nodesAdded += fn;
|
|
9916
9952
|
edgesAdded += fe;
|
|
9917
|
-
const evidenceFile = toPosix(
|
|
9953
|
+
const evidenceFile = toPosix(path36.relative(scanPath, config.sourceFile));
|
|
9918
9954
|
const edge = {
|
|
9919
9955
|
id: extractedEdgeId(fileNodeId, dbNode.id, EdgeType10.CONNECTS_TO),
|
|
9920
9956
|
source: fileNodeId,
|
|
@@ -9935,15 +9971,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
9935
9971
|
if (allConfigs.length === 1) {
|
|
9936
9972
|
const primary = allConfigs[0];
|
|
9937
9973
|
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
9938
|
-
const relPath =
|
|
9974
|
+
const relPath = path36.relative(scanPath, primary.sourceFile);
|
|
9939
9975
|
const cfgId = configId(relPath);
|
|
9940
9976
|
if (!graph.hasNode(cfgId)) {
|
|
9941
9977
|
const cfgNode = {
|
|
9942
9978
|
id: cfgId,
|
|
9943
9979
|
type: NodeType20.ConfigNode,
|
|
9944
|
-
name:
|
|
9980
|
+
name: path36.basename(primary.sourceFile),
|
|
9945
9981
|
path: relPath,
|
|
9946
|
-
fileType: isConfigFile(
|
|
9982
|
+
fileType: isConfigFile(path36.basename(primary.sourceFile)).fileType || "config"
|
|
9947
9983
|
};
|
|
9948
9984
|
graph.addNode(cfgId, cfgNode);
|
|
9949
9985
|
nodesAdded++;
|
|
@@ -9983,8 +10019,8 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
9983
10019
|
}
|
|
9984
10020
|
|
|
9985
10021
|
// src/extract/configs.ts
|
|
9986
|
-
import { promises as
|
|
9987
|
-
import
|
|
10022
|
+
import { promises as fs25 } from "fs";
|
|
10023
|
+
import path37 from "path";
|
|
9988
10024
|
import {
|
|
9989
10025
|
EdgeType as EdgeType11,
|
|
9990
10026
|
NodeType as NodeType21,
|
|
@@ -9993,15 +10029,15 @@ import {
|
|
|
9993
10029
|
confidenceForExtracted as confidenceForExtracted8
|
|
9994
10030
|
} from "@neat.is/types";
|
|
9995
10031
|
async function walkConfigFiles(dir, excludeDirs = []) {
|
|
9996
|
-
const excluded = new Set(excludeDirs.map((d) =>
|
|
10032
|
+
const excluded = new Set(excludeDirs.map((d) => path37.resolve(d)));
|
|
9997
10033
|
const out = [];
|
|
9998
10034
|
async function walk11(current) {
|
|
9999
|
-
const entries = await
|
|
10035
|
+
const entries = await fs25.readdir(current, { withFileTypes: true });
|
|
10000
10036
|
for (const entry of entries) {
|
|
10001
|
-
const full =
|
|
10037
|
+
const full = path37.join(current, entry.name);
|
|
10002
10038
|
if (entry.isDirectory()) {
|
|
10003
10039
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
10004
|
-
if (excluded.has(
|
|
10040
|
+
if (excluded.has(path37.resolve(full))) continue;
|
|
10005
10041
|
if (await isPythonVenvDir(full)) continue;
|
|
10006
10042
|
await walk11(full);
|
|
10007
10043
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
@@ -10018,19 +10054,19 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
10018
10054
|
for (const service of services) {
|
|
10019
10055
|
const configFiles = await walkConfigFiles(service.dir, service.excludeDirs);
|
|
10020
10056
|
for (const file of configFiles) {
|
|
10021
|
-
const relPath =
|
|
10057
|
+
const relPath = path37.relative(scanPath, file);
|
|
10022
10058
|
const node = {
|
|
10023
10059
|
id: configId2(relPath),
|
|
10024
10060
|
type: NodeType21.ConfigNode,
|
|
10025
|
-
name:
|
|
10061
|
+
name: path37.basename(file),
|
|
10026
10062
|
path: relPath,
|
|
10027
|
-
fileType: isConfigFile(
|
|
10063
|
+
fileType: isConfigFile(path37.basename(file)).fileType
|
|
10028
10064
|
};
|
|
10029
10065
|
if (!graph.hasNode(node.id)) {
|
|
10030
10066
|
graph.addNode(node.id, node);
|
|
10031
10067
|
nodesAdded++;
|
|
10032
10068
|
}
|
|
10033
|
-
const relToService = toPosix(
|
|
10069
|
+
const relToService = toPosix(path37.relative(service.dir, file));
|
|
10034
10070
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
10035
10071
|
graph,
|
|
10036
10072
|
service.pkg.name,
|
|
@@ -10046,7 +10082,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
10046
10082
|
type: EdgeType11.CONFIGURED_BY,
|
|
10047
10083
|
provenance: Provenance11.EXTRACTED,
|
|
10048
10084
|
confidence: confidenceForExtracted8("structural"),
|
|
10049
|
-
evidence: { file: relPath.split(
|
|
10085
|
+
evidence: { file: relPath.split(path37.sep).join("/") }
|
|
10050
10086
|
};
|
|
10051
10087
|
if (!graph.hasEdge(edge.id)) {
|
|
10052
10088
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -10058,8 +10094,8 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
10058
10094
|
}
|
|
10059
10095
|
|
|
10060
10096
|
// src/extract/proto.ts
|
|
10061
|
-
import { promises as
|
|
10062
|
-
import
|
|
10097
|
+
import { promises as fs26 } from "fs";
|
|
10098
|
+
import path38 from "path";
|
|
10063
10099
|
import {
|
|
10064
10100
|
EdgeType as EdgeType12,
|
|
10065
10101
|
NodeType as NodeType22,
|
|
@@ -10093,10 +10129,10 @@ function grpcMethodsFromProto(content, fqPackage) {
|
|
|
10093
10129
|
}
|
|
10094
10130
|
const body = content.slice(bodyStart, i - 1);
|
|
10095
10131
|
const rpcRe = /\brpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
|
|
10096
|
-
let
|
|
10097
|
-
while ((
|
|
10098
|
-
const rpcMethod =
|
|
10099
|
-
const line = lineAt(content, bodyStart +
|
|
10132
|
+
let rm2;
|
|
10133
|
+
while ((rm2 = rpcRe.exec(body)) !== null) {
|
|
10134
|
+
const rpcMethod = rm2[1];
|
|
10135
|
+
const line = lineAt(content, bodyStart + rm2.index);
|
|
10100
10136
|
out.push({ rpcService, rpcMethod, line });
|
|
10101
10137
|
}
|
|
10102
10138
|
serviceRe.lastIndex = i;
|
|
@@ -10104,18 +10140,18 @@ function grpcMethodsFromProto(content, fqPackage) {
|
|
|
10104
10140
|
return out;
|
|
10105
10141
|
}
|
|
10106
10142
|
async function walkProtoFiles(dir, excludeDirs = []) {
|
|
10107
|
-
const excluded = new Set(excludeDirs.map((d) =>
|
|
10143
|
+
const excluded = new Set(excludeDirs.map((d) => path38.resolve(d)));
|
|
10108
10144
|
const out = [];
|
|
10109
10145
|
async function walk11(current) {
|
|
10110
|
-
const entries = await
|
|
10146
|
+
const entries = await fs26.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
10111
10147
|
for (const entry of entries) {
|
|
10112
|
-
const full =
|
|
10148
|
+
const full = path38.join(current, entry.name);
|
|
10113
10149
|
if (entry.isDirectory()) {
|
|
10114
10150
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
10115
|
-
if (excluded.has(
|
|
10151
|
+
if (excluded.has(path38.resolve(full))) continue;
|
|
10116
10152
|
if (await isPythonVenvDir(full)) continue;
|
|
10117
10153
|
await walk11(full);
|
|
10118
|
-
} else if (entry.isFile() &&
|
|
10154
|
+
} else if (entry.isFile() && path38.extname(entry.name) === PROTO_EXTENSION) {
|
|
10119
10155
|
out.push(full);
|
|
10120
10156
|
}
|
|
10121
10157
|
}
|
|
@@ -10130,10 +10166,10 @@ async function addGrpcMethods(graph, services) {
|
|
|
10130
10166
|
const protoPaths = await walkProtoFiles(service.dir, service.excludeDirs);
|
|
10131
10167
|
for (const protoPath of protoPaths) {
|
|
10132
10168
|
if (isTestPath(protoPath)) continue;
|
|
10133
|
-
const relFile = toPosix(
|
|
10169
|
+
const relFile = toPosix(path38.relative(service.dir, protoPath));
|
|
10134
10170
|
let content;
|
|
10135
10171
|
try {
|
|
10136
|
-
content = await
|
|
10172
|
+
content = await fs26.readFile(protoPath, "utf8");
|
|
10137
10173
|
} catch (err) {
|
|
10138
10174
|
recordExtractionError("proto extraction", protoPath, err);
|
|
10139
10175
|
continue;
|
|
@@ -10196,7 +10232,7 @@ import {
|
|
|
10196
10232
|
} from "@neat.is/types";
|
|
10197
10233
|
|
|
10198
10234
|
// src/extract/calls/http.ts
|
|
10199
|
-
import
|
|
10235
|
+
import path39 from "path";
|
|
10200
10236
|
import Parser6 from "tree-sitter";
|
|
10201
10237
|
import JavaScript4 from "tree-sitter-javascript";
|
|
10202
10238
|
import TypeScript2 from "tree-sitter-typescript";
|
|
@@ -10284,7 +10320,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
10284
10320
|
const seen = /* @__PURE__ */ new Set();
|
|
10285
10321
|
for (const file of files) {
|
|
10286
10322
|
if (isTestPath(file.path)) continue;
|
|
10287
|
-
const parser = parserForExt(
|
|
10323
|
+
const parser = parserForExt(path39.extname(file.path), parserCache);
|
|
10288
10324
|
let sites;
|
|
10289
10325
|
try {
|
|
10290
10326
|
sites = callsFromSource(file.content, parser, knownHosts);
|
|
@@ -10293,7 +10329,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
10293
10329
|
continue;
|
|
10294
10330
|
}
|
|
10295
10331
|
if (sites.length === 0) continue;
|
|
10296
|
-
const relFile = toPosix(
|
|
10332
|
+
const relFile = toPosix(path39.relative(service.dir, file.path));
|
|
10297
10333
|
for (const site of sites) {
|
|
10298
10334
|
const targetId = hostToNodeId.get(site.host);
|
|
10299
10335
|
if (!targetId || targetId === service.node.id) continue;
|
|
@@ -10346,7 +10382,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
10346
10382
|
}
|
|
10347
10383
|
|
|
10348
10384
|
// src/extract/calls/route-match.ts
|
|
10349
|
-
import
|
|
10385
|
+
import path40 from "path";
|
|
10350
10386
|
import Parser7 from "tree-sitter";
|
|
10351
10387
|
import JavaScript5 from "tree-sitter-javascript";
|
|
10352
10388
|
import {
|
|
@@ -10631,7 +10667,7 @@ async function addRouteCallEdges(graph, services) {
|
|
|
10631
10667
|
const seen = /* @__PURE__ */ new Set();
|
|
10632
10668
|
for (const file of files) {
|
|
10633
10669
|
if (isTestPath(file.path)) continue;
|
|
10634
|
-
if (!JS_CLIENT_EXTENSIONS.has(
|
|
10670
|
+
if (!JS_CLIENT_EXTENSIONS.has(path40.extname(file.path))) continue;
|
|
10635
10671
|
let sites;
|
|
10636
10672
|
try {
|
|
10637
10673
|
sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
|
|
@@ -10640,7 +10676,7 @@ async function addRouteCallEdges(graph, services) {
|
|
|
10640
10676
|
continue;
|
|
10641
10677
|
}
|
|
10642
10678
|
if (sites.length === 0) continue;
|
|
10643
|
-
const relFile = toPosix(
|
|
10679
|
+
const relFile = toPosix(path40.relative(service.dir, file.path));
|
|
10644
10680
|
for (const site of sites) {
|
|
10645
10681
|
if (site.host === null) {
|
|
10646
10682
|
const dedupKey2 = `${relFile}|unresolved-host`;
|
|
@@ -10732,7 +10768,7 @@ async function addRouteCallEdges(graph, services) {
|
|
|
10732
10768
|
}
|
|
10733
10769
|
|
|
10734
10770
|
// src/extract/calls/kafka.ts
|
|
10735
|
-
import
|
|
10771
|
+
import path41 from "path";
|
|
10736
10772
|
import { infraId as infraId2 } from "@neat.is/types";
|
|
10737
10773
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
10738
10774
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -10820,13 +10856,13 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
10820
10856
|
// call sites — verified-call-site tier (ADR-066).
|
|
10821
10857
|
confidenceKind: "verified-call-site",
|
|
10822
10858
|
evidence: {
|
|
10823
|
-
file:
|
|
10859
|
+
file: path41.relative(serviceDir, file.path),
|
|
10824
10860
|
line,
|
|
10825
10861
|
snippet: snippet(file.content, line)
|
|
10826
10862
|
}
|
|
10827
10863
|
});
|
|
10828
10864
|
};
|
|
10829
|
-
if (
|
|
10865
|
+
if (path41.extname(file.path) === ".go") {
|
|
10830
10866
|
goSaramaEndpoints(file.content, make);
|
|
10831
10867
|
} else {
|
|
10832
10868
|
for (const { topic } of findAll(PRODUCER_TOPIC_RE, file.content)) make(topic, "PUBLISHES_TO");
|
|
@@ -10836,7 +10872,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
10836
10872
|
}
|
|
10837
10873
|
|
|
10838
10874
|
// src/extract/calls/redis.ts
|
|
10839
|
-
import
|
|
10875
|
+
import path42 from "path";
|
|
10840
10876
|
import { infraId as infraId3 } from "@neat.is/types";
|
|
10841
10877
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
10842
10878
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -10859,7 +10895,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
10859
10895
|
// support tier (ADR-066).
|
|
10860
10896
|
confidenceKind: "url-with-structural-support",
|
|
10861
10897
|
evidence: {
|
|
10862
|
-
file:
|
|
10898
|
+
file: path42.relative(serviceDir, file.path),
|
|
10863
10899
|
line,
|
|
10864
10900
|
snippet: snippet(file.content, line)
|
|
10865
10901
|
}
|
|
@@ -10869,7 +10905,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
10869
10905
|
}
|
|
10870
10906
|
|
|
10871
10907
|
// src/extract/calls/aws.ts
|
|
10872
|
-
import
|
|
10908
|
+
import path43 from "path";
|
|
10873
10909
|
import { infraId as infraId4 } from "@neat.is/types";
|
|
10874
10910
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
10875
10911
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -10903,7 +10939,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
10903
10939
|
// (ADR-066).
|
|
10904
10940
|
confidenceKind: "verified-call-site",
|
|
10905
10941
|
evidence: {
|
|
10906
|
-
file:
|
|
10942
|
+
file: path43.relative(serviceDir, file.path),
|
|
10907
10943
|
line,
|
|
10908
10944
|
snippet: snippet(file.content, line)
|
|
10909
10945
|
}
|
|
@@ -10927,7 +10963,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
10927
10963
|
}
|
|
10928
10964
|
|
|
10929
10965
|
// src/extract/calls/grpc.ts
|
|
10930
|
-
import
|
|
10966
|
+
import path44 from "path";
|
|
10931
10967
|
import { infraId as infraId5 } from "@neat.is/types";
|
|
10932
10968
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
10933
10969
|
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
@@ -10986,7 +11022,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
10986
11022
|
// tier (ADR-066).
|
|
10987
11023
|
confidenceKind: "verified-call-site",
|
|
10988
11024
|
evidence: {
|
|
10989
|
-
file:
|
|
11025
|
+
file: path44.relative(serviceDir, file.path),
|
|
10990
11026
|
line,
|
|
10991
11027
|
snippet: snippet(file.content, line)
|
|
10992
11028
|
}
|
|
@@ -10996,7 +11032,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
10996
11032
|
}
|
|
10997
11033
|
|
|
10998
11034
|
// src/extract/calls/supabase.ts
|
|
10999
|
-
import
|
|
11035
|
+
import path45 from "path";
|
|
11000
11036
|
import { infraId as infraId6 } from "@neat.is/types";
|
|
11001
11037
|
var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
|
|
11002
11038
|
var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
|
|
@@ -11055,7 +11091,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
11055
11091
|
// tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
|
|
11056
11092
|
confidenceKind: "verified-call-site",
|
|
11057
11093
|
evidence: {
|
|
11058
|
-
file:
|
|
11094
|
+
file: path45.relative(serviceDir, file.path),
|
|
11059
11095
|
line,
|
|
11060
11096
|
snippet: snippet(file.content, line)
|
|
11061
11097
|
}
|
|
@@ -11082,7 +11118,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
11082
11118
|
edgeType: "CALLS",
|
|
11083
11119
|
confidenceKind: "verified-call-site",
|
|
11084
11120
|
evidence: {
|
|
11085
|
-
file:
|
|
11121
|
+
file: path45.relative(serviceDir, file.path),
|
|
11086
11122
|
line,
|
|
11087
11123
|
snippet: snippet(file.content, line)
|
|
11088
11124
|
}
|
|
@@ -11093,7 +11129,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
11093
11129
|
}
|
|
11094
11130
|
|
|
11095
11131
|
// src/extract/calls/firestore.ts
|
|
11096
|
-
import
|
|
11132
|
+
import path46 from "path";
|
|
11097
11133
|
import Parser8 from "tree-sitter";
|
|
11098
11134
|
import JavaScript6 from "tree-sitter-javascript";
|
|
11099
11135
|
import { infraId as infraId7 } from "@neat.is/types";
|
|
@@ -11266,7 +11302,7 @@ function firestoreEndpointsFromFile(file, serviceDir, imported) {
|
|
|
11266
11302
|
if (!hasClient && !hasAdmin && importedNames.size === 0) return [];
|
|
11267
11303
|
const ownSdk = hasClient && !hasAdmin ? "client" : hasAdmin && !hasClient ? "admin" : null;
|
|
11268
11304
|
const fileSdk = !hasClient && !hasAdmin ? imported?.sdk ?? null : ownSdk;
|
|
11269
|
-
const tree = parseSource3(parserForExt2(
|
|
11305
|
+
const tree = parseSource3(parserForExt2(path46.extname(file.path)), file.content);
|
|
11270
11306
|
const clientVars = firestoreClientVars(tree.rootNode);
|
|
11271
11307
|
for (const n of importedNames) clientVars.add(n);
|
|
11272
11308
|
const collLine = /* @__PURE__ */ new Map();
|
|
@@ -11371,7 +11407,7 @@ function firestoreEndpointsFromFile(file, serviceDir, imported) {
|
|
|
11371
11407
|
...columnSet.size > 0 ? { columns: [...columnSet] } : {},
|
|
11372
11408
|
...sdkWrites ? { sdkWrites } : {},
|
|
11373
11409
|
evidence: {
|
|
11374
|
-
file:
|
|
11410
|
+
file: path46.relative(serviceDir, file.path),
|
|
11375
11411
|
line,
|
|
11376
11412
|
snippet: snippet(file.content, line)
|
|
11377
11413
|
}
|
|
@@ -11431,7 +11467,7 @@ function sdkOf(content) {
|
|
|
11431
11467
|
return hasClient && !hasAdmin ? "client" : hasAdmin && !hasClient ? "admin" : null;
|
|
11432
11468
|
}
|
|
11433
11469
|
function parseOrNull(file) {
|
|
11434
|
-
const ext =
|
|
11470
|
+
const ext = path46.extname(file.path);
|
|
11435
11471
|
if (!(ext in GRAMMAR_BY_EXT)) return null;
|
|
11436
11472
|
try {
|
|
11437
11473
|
return parseSource3(parserForExt2(ext), file.content).rootNode;
|
|
@@ -11447,7 +11483,7 @@ async function firestoreCrossFileEndpoints(files, serviceDir) {
|
|
|
11447
11483
|
if (!root) continue;
|
|
11448
11484
|
const names = exportedFirestoreClients(root);
|
|
11449
11485
|
if (names.size === 0) continue;
|
|
11450
|
-
registry.set(toPosix(
|
|
11486
|
+
registry.set(toPosix(path46.relative(serviceDir, f.path)), { names, sdk: sdkOf(f.content) });
|
|
11451
11487
|
}
|
|
11452
11488
|
if (registry.size === 0) return [];
|
|
11453
11489
|
const out = [];
|
|
@@ -11460,7 +11496,7 @@ async function firestoreCrossFileEndpoints(files, serviceDir) {
|
|
|
11460
11496
|
const names = /* @__PURE__ */ new Set();
|
|
11461
11497
|
let sdk = null;
|
|
11462
11498
|
for (const b of bindings) {
|
|
11463
|
-
const rel = await resolveJsImport(b.specifier,
|
|
11499
|
+
const rel = await resolveJsImport(b.specifier, path46.dirname(f.path), serviceDir, null);
|
|
11464
11500
|
if (!rel) continue;
|
|
11465
11501
|
const entry = registry.get(rel);
|
|
11466
11502
|
if (!entry || !entry.names.has(b.imported)) continue;
|
|
@@ -11474,7 +11510,7 @@ async function firestoreCrossFileEndpoints(files, serviceDir) {
|
|
|
11474
11510
|
}
|
|
11475
11511
|
|
|
11476
11512
|
// src/extract/calls/mongoose.ts
|
|
11477
|
-
import
|
|
11513
|
+
import path47 from "path";
|
|
11478
11514
|
import { infraId as infraId8 } from "@neat.is/types";
|
|
11479
11515
|
var MONGOOSE_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])mongoose['"`]/;
|
|
11480
11516
|
var MONGODB_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])mongodb['"`]/;
|
|
@@ -11625,7 +11661,7 @@ function endpoint(r, file, serviceDir, matchText) {
|
|
|
11625
11661
|
kind: r.kind,
|
|
11626
11662
|
edgeType: "CALLS",
|
|
11627
11663
|
confidenceKind: "verified-call-site",
|
|
11628
|
-
evidence: { file:
|
|
11664
|
+
evidence: { file: path47.relative(serviceDir, file.path), line, snippet: snippet(file.content, line) }
|
|
11629
11665
|
};
|
|
11630
11666
|
}
|
|
11631
11667
|
function mongooseEndpointsFromFile(file, serviceDir) {
|
|
@@ -11715,7 +11751,7 @@ async function mongooseCrossFileEndpoints(files, serviceDir) {
|
|
|
11715
11751
|
const registry = /* @__PURE__ */ new Map();
|
|
11716
11752
|
for (const f of mongooseFiles) {
|
|
11717
11753
|
const fx = fileExportsOf(f.content, pluralizeOn);
|
|
11718
|
-
if (fx) registry.set(toPosix(
|
|
11754
|
+
if (fx) registry.set(toPosix(path47.relative(serviceDir, f.path)), fx);
|
|
11719
11755
|
}
|
|
11720
11756
|
if (registry.size === 0) return [];
|
|
11721
11757
|
const out = [];
|
|
@@ -11726,7 +11762,7 @@ async function mongooseCrossFileEndpoints(files, serviceDir) {
|
|
|
11726
11762
|
const directColl = /* @__PURE__ */ new Map();
|
|
11727
11763
|
const nsExports = /* @__PURE__ */ new Map();
|
|
11728
11764
|
for (const b of bindings) {
|
|
11729
|
-
const resolvedRel = await resolveJsImport(b.specifier,
|
|
11765
|
+
const resolvedRel = await resolveJsImport(b.specifier, path47.dirname(f.path), serviceDir, null);
|
|
11730
11766
|
if (!resolvedRel) continue;
|
|
11731
11767
|
const fx = registry.get(resolvedRel);
|
|
11732
11768
|
if (!fx) continue;
|
|
@@ -11769,7 +11805,7 @@ async function mongooseCrossFileEndpoints(files, serviceDir) {
|
|
|
11769
11805
|
}
|
|
11770
11806
|
|
|
11771
11807
|
// src/extract/calls/sqlalchemy.ts
|
|
11772
|
-
import
|
|
11808
|
+
import path48 from "path";
|
|
11773
11809
|
import Parser9 from "tree-sitter";
|
|
11774
11810
|
import Python5 from "tree-sitter-python";
|
|
11775
11811
|
import { infraId as infraId9 } from "@neat.is/types";
|
|
@@ -11908,7 +11944,7 @@ function sqlalchemyForeignKeys(file, serviceDir) {
|
|
|
11908
11944
|
childTable,
|
|
11909
11945
|
parentTable,
|
|
11910
11946
|
evidence: {
|
|
11911
|
-
file:
|
|
11947
|
+
file: path48.relative(serviceDir, file.path),
|
|
11912
11948
|
line,
|
|
11913
11949
|
snippet: snippet(file.content, line)
|
|
11914
11950
|
}
|
|
@@ -11933,7 +11969,7 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
|
|
|
11933
11969
|
confidenceKind: "verified-call-site",
|
|
11934
11970
|
...columns && columns.length > 0 ? { columns } : {},
|
|
11935
11971
|
evidence: {
|
|
11936
|
-
file:
|
|
11972
|
+
file: path48.relative(serviceDir, file.path),
|
|
11937
11973
|
line,
|
|
11938
11974
|
snippet: snippet(file.content, line)
|
|
11939
11975
|
}
|
|
@@ -12040,7 +12076,7 @@ function pythonOrmCrossFileEndpoints(files, serviceDir) {
|
|
|
12040
12076
|
edgeType: "CALLS",
|
|
12041
12077
|
confidenceKind: "verified-call-site",
|
|
12042
12078
|
evidence: {
|
|
12043
|
-
file:
|
|
12079
|
+
file: path48.relative(serviceDir, file.path),
|
|
12044
12080
|
line,
|
|
12045
12081
|
snippet: snippet(file.content, line)
|
|
12046
12082
|
}
|
|
@@ -12051,7 +12087,7 @@ function pythonOrmCrossFileEndpoints(files, serviceDir) {
|
|
|
12051
12087
|
}
|
|
12052
12088
|
|
|
12053
12089
|
// src/extract/calls/socket.ts
|
|
12054
|
-
import
|
|
12090
|
+
import path49 from "path";
|
|
12055
12091
|
import Parser10 from "tree-sitter";
|
|
12056
12092
|
import Python6 from "tree-sitter-python";
|
|
12057
12093
|
import { infraId as infraId10 } from "@neat.is/types";
|
|
@@ -12127,7 +12163,7 @@ function socketEndpointsFromFile(file, serviceDir) {
|
|
|
12127
12163
|
// A verified `socket.socket(...)` + `.connect((host, port))` call site.
|
|
12128
12164
|
confidenceKind: "verified-call-site",
|
|
12129
12165
|
evidence: {
|
|
12130
|
-
file:
|
|
12166
|
+
file: path49.relative(serviceDir, file.path),
|
|
12131
12167
|
line,
|
|
12132
12168
|
snippet: snippet(file.content, line)
|
|
12133
12169
|
}
|
|
@@ -12137,7 +12173,7 @@ function socketEndpointsFromFile(file, serviceDir) {
|
|
|
12137
12173
|
}
|
|
12138
12174
|
|
|
12139
12175
|
// src/extract/calls/django-orm.ts
|
|
12140
|
-
import
|
|
12176
|
+
import path50 from "path";
|
|
12141
12177
|
import Parser11 from "tree-sitter";
|
|
12142
12178
|
import Python7 from "tree-sitter-python";
|
|
12143
12179
|
import { infraId as infraId11 } from "@neat.is/types";
|
|
@@ -12207,7 +12243,7 @@ function djangoOrmEndpointsFromFile(file, serviceDir) {
|
|
|
12207
12243
|
const tree = parseSource8(makePyParser5(), file.content);
|
|
12208
12244
|
const out = [];
|
|
12209
12245
|
const seen = /* @__PURE__ */ new Set();
|
|
12210
|
-
const defaultAppLabel =
|
|
12246
|
+
const defaultAppLabel = path50.basename(path50.dirname(file.path));
|
|
12211
12247
|
walk5(tree.rootNode, (node) => {
|
|
12212
12248
|
if (node.type !== "class_definition") return;
|
|
12213
12249
|
if (!extendsDjangoModel(node)) return;
|
|
@@ -12225,14 +12261,14 @@ function djangoOrmEndpointsFromFile(file, serviceDir) {
|
|
|
12225
12261
|
kind: "sql-table",
|
|
12226
12262
|
edgeType: "CALLS",
|
|
12227
12263
|
confidenceKind: "verified-call-site",
|
|
12228
|
-
evidence: { file:
|
|
12264
|
+
evidence: { file: path50.relative(serviceDir, file.path), line, snippet: snippet(file.content, line) }
|
|
12229
12265
|
});
|
|
12230
12266
|
});
|
|
12231
12267
|
return out;
|
|
12232
12268
|
}
|
|
12233
12269
|
|
|
12234
12270
|
// src/extract/calls/drizzle.ts
|
|
12235
|
-
import
|
|
12271
|
+
import path51 from "path";
|
|
12236
12272
|
import Parser12 from "tree-sitter";
|
|
12237
12273
|
import JavaScript7 from "tree-sitter-javascript";
|
|
12238
12274
|
import { infraId as infraId12 } from "@neat.is/types";
|
|
@@ -12310,7 +12346,7 @@ function columnsFromObject(obj) {
|
|
|
12310
12346
|
}
|
|
12311
12347
|
function drizzleEndpointsFromFile(file, serviceDir) {
|
|
12312
12348
|
if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
|
|
12313
|
-
const tree = parseSource3(parserForExt3(
|
|
12349
|
+
const tree = parseSource3(parserForExt3(path51.extname(file.path)), file.content);
|
|
12314
12350
|
const out = [];
|
|
12315
12351
|
const seen = /* @__PURE__ */ new Set();
|
|
12316
12352
|
const walk11 = (node) => {
|
|
@@ -12333,7 +12369,7 @@ function drizzleEndpointsFromFile(file, serviceDir) {
|
|
|
12333
12369
|
confidenceKind: "structural",
|
|
12334
12370
|
columns,
|
|
12335
12371
|
evidence: {
|
|
12336
|
-
file:
|
|
12372
|
+
file: path51.relative(serviceDir, file.path),
|
|
12337
12373
|
line,
|
|
12338
12374
|
snippet: snippet(file.content, line)
|
|
12339
12375
|
}
|
|
@@ -12399,7 +12435,7 @@ function referencesTargetVar(call) {
|
|
|
12399
12435
|
}
|
|
12400
12436
|
function drizzleForeignKeys(file, serviceDir) {
|
|
12401
12437
|
if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
|
|
12402
|
-
const tree = parseSource3(parserForExt3(
|
|
12438
|
+
const tree = parseSource3(parserForExt3(path51.extname(file.path)), file.content);
|
|
12403
12439
|
const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
|
|
12404
12440
|
const out = [];
|
|
12405
12441
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -12418,7 +12454,7 @@ function drizzleForeignKeys(file, serviceDir) {
|
|
|
12418
12454
|
childTable: table.tableName,
|
|
12419
12455
|
parentTable,
|
|
12420
12456
|
evidence: {
|
|
12421
|
-
file:
|
|
12457
|
+
file: path51.relative(serviceDir, file.path),
|
|
12422
12458
|
line,
|
|
12423
12459
|
snippet: snippet(file.content, line)
|
|
12424
12460
|
}
|
|
@@ -12434,7 +12470,7 @@ function drizzleForeignKeys(file, serviceDir) {
|
|
|
12434
12470
|
}
|
|
12435
12471
|
|
|
12436
12472
|
// src/extract/calls/prisma.ts
|
|
12437
|
-
import
|
|
12473
|
+
import path52 from "path";
|
|
12438
12474
|
import { infraId as infraId13 } from "@neat.is/types";
|
|
12439
12475
|
var SCALAR_TYPES = /* @__PURE__ */ new Set([
|
|
12440
12476
|
"Int",
|
|
@@ -12490,7 +12526,7 @@ function prismaColumnsFromSchema(file, serviceDir) {
|
|
|
12490
12526
|
confidenceKind: "structural",
|
|
12491
12527
|
columns: b.columns,
|
|
12492
12528
|
evidence: {
|
|
12493
|
-
file:
|
|
12529
|
+
file: path52.relative(serviceDir, file.path),
|
|
12494
12530
|
line: b.startLine,
|
|
12495
12531
|
snippet: snippet(content, b.startLine)
|
|
12496
12532
|
}
|
|
@@ -12551,7 +12587,7 @@ function prismaColumnsFromSchema(file, serviceDir) {
|
|
|
12551
12587
|
}
|
|
12552
12588
|
async function prismaColumnEndpoints(serviceDir) {
|
|
12553
12589
|
const schemaPath = await findFirst(serviceDir, [
|
|
12554
|
-
|
|
12590
|
+
path52.join("prisma", "schema.prisma"),
|
|
12555
12591
|
"schema.prisma"
|
|
12556
12592
|
]);
|
|
12557
12593
|
if (!schemaPath) return [];
|
|
@@ -12626,7 +12662,7 @@ function prismaForeignKeysFromSchema(file, serviceDir) {
|
|
|
12626
12662
|
childTable: current.table,
|
|
12627
12663
|
parentTable,
|
|
12628
12664
|
evidence: {
|
|
12629
|
-
file:
|
|
12665
|
+
file: path52.relative(serviceDir, file.path),
|
|
12630
12666
|
line: lineNo,
|
|
12631
12667
|
snippet: snippet(content, lineNo)
|
|
12632
12668
|
}
|
|
@@ -12641,7 +12677,7 @@ function prismaForeignKeysFromSchema(file, serviceDir) {
|
|
|
12641
12677
|
}
|
|
12642
12678
|
async function prismaForeignKeys(serviceDir) {
|
|
12643
12679
|
const schemaPath = await findFirst(serviceDir, [
|
|
12644
|
-
|
|
12680
|
+
path52.join("prisma", "schema.prisma"),
|
|
12645
12681
|
"schema.prisma"
|
|
12646
12682
|
]);
|
|
12647
12683
|
if (!schemaPath) return [];
|
|
@@ -12651,7 +12687,7 @@ async function prismaForeignKeys(serviceDir) {
|
|
|
12651
12687
|
}
|
|
12652
12688
|
|
|
12653
12689
|
// src/extract/calls/activerecord.ts
|
|
12654
|
-
import
|
|
12690
|
+
import path53 from "path";
|
|
12655
12691
|
import Parser13 from "tree-sitter";
|
|
12656
12692
|
import Ruby3 from "tree-sitter-ruby";
|
|
12657
12693
|
import { infraId as infraId14 } from "@neat.is/types";
|
|
@@ -12859,7 +12895,7 @@ function railsSchemaEndpointsFromFile(file, serviceDir) {
|
|
|
12859
12895
|
confidenceKind: "structural",
|
|
12860
12896
|
...table.columns.length > 0 ? { columns: table.columns } : {},
|
|
12861
12897
|
evidence: {
|
|
12862
|
-
file:
|
|
12898
|
+
file: path53.relative(serviceDir, file.path),
|
|
12863
12899
|
line: table.line,
|
|
12864
12900
|
snippet: snippet(file.content, table.line)
|
|
12865
12901
|
}
|
|
@@ -12881,7 +12917,7 @@ function railsSchemaForeignKeys(file, serviceDir) {
|
|
|
12881
12917
|
childTable,
|
|
12882
12918
|
parentTable,
|
|
12883
12919
|
evidence: {
|
|
12884
|
-
file:
|
|
12920
|
+
file: path53.relative(serviceDir, file.path),
|
|
12885
12921
|
line,
|
|
12886
12922
|
snippet: snippet(file.content, line)
|
|
12887
12923
|
}
|
|
@@ -12964,7 +13000,7 @@ function railsModelEndpointsFromFile(file, serviceDir) {
|
|
|
12964
13000
|
edgeType: "CALLS",
|
|
12965
13001
|
confidenceKind: "verified-call-site",
|
|
12966
13002
|
evidence: {
|
|
12967
|
-
file:
|
|
13003
|
+
file: path53.relative(serviceDir, file.path),
|
|
12968
13004
|
line,
|
|
12969
13005
|
snippet: snippet(file.content, line)
|
|
12970
13006
|
}
|
|
@@ -13001,7 +13037,7 @@ function railsModelForeignKeys(file, serviceDir) {
|
|
|
13001
13037
|
childTable,
|
|
13002
13038
|
parentTable,
|
|
13003
13039
|
evidence: {
|
|
13004
|
-
file:
|
|
13040
|
+
file: path53.relative(serviceDir, file.path),
|
|
13005
13041
|
line,
|
|
13006
13042
|
snippet: snippet(file.content, line)
|
|
13007
13043
|
}
|
|
@@ -13012,7 +13048,7 @@ function railsModelForeignKeys(file, serviceDir) {
|
|
|
13012
13048
|
}
|
|
13013
13049
|
|
|
13014
13050
|
// src/extract/calls/eloquent.ts
|
|
13015
|
-
import
|
|
13051
|
+
import path54 from "path";
|
|
13016
13052
|
import Parser14 from "tree-sitter";
|
|
13017
13053
|
import Php3 from "tree-sitter-php";
|
|
13018
13054
|
import { infraId as infraId15 } from "@neat.is/types";
|
|
@@ -13269,7 +13305,7 @@ function laravelMigrationEndpointsFromFile(file, serviceDir) {
|
|
|
13269
13305
|
confidenceKind: "structural",
|
|
13270
13306
|
...columns.length > 0 ? { columns } : {},
|
|
13271
13307
|
evidence: {
|
|
13272
|
-
file:
|
|
13308
|
+
file: path54.relative(serviceDir, file.path),
|
|
13273
13309
|
line: bp.line,
|
|
13274
13310
|
snippet: snippet(file.content, bp.line)
|
|
13275
13311
|
}
|
|
@@ -13291,7 +13327,7 @@ function laravelMigrationForeignKeys(file, serviceDir) {
|
|
|
13291
13327
|
childTable,
|
|
13292
13328
|
parentTable,
|
|
13293
13329
|
evidence: {
|
|
13294
|
-
file:
|
|
13330
|
+
file: path54.relative(serviceDir, file.path),
|
|
13295
13331
|
line,
|
|
13296
13332
|
snippet: snippet(file.content, line)
|
|
13297
13333
|
}
|
|
@@ -13411,7 +13447,7 @@ function laravelModelEndpointsFromFile(file, serviceDir) {
|
|
|
13411
13447
|
edgeType: "CALLS",
|
|
13412
13448
|
confidenceKind: "verified-call-site",
|
|
13413
13449
|
evidence: {
|
|
13414
|
-
file:
|
|
13450
|
+
file: path54.relative(serviceDir, file.path),
|
|
13415
13451
|
line,
|
|
13416
13452
|
snippet: snippet(file.content, line)
|
|
13417
13453
|
}
|
|
@@ -13447,7 +13483,7 @@ function laravelModelForeignKeys(file, serviceDir) {
|
|
|
13447
13483
|
childTable,
|
|
13448
13484
|
parentTable,
|
|
13449
13485
|
evidence: {
|
|
13450
|
-
file:
|
|
13486
|
+
file: path54.relative(serviceDir, file.path),
|
|
13451
13487
|
line,
|
|
13452
13488
|
snippet: snippet(file.content, line)
|
|
13453
13489
|
}
|
|
@@ -13458,7 +13494,7 @@ function laravelModelForeignKeys(file, serviceDir) {
|
|
|
13458
13494
|
}
|
|
13459
13495
|
|
|
13460
13496
|
// src/extract/calls/go.ts
|
|
13461
|
-
import
|
|
13497
|
+
import path55 from "path";
|
|
13462
13498
|
import Parser15 from "tree-sitter";
|
|
13463
13499
|
import Go4 from "tree-sitter-go";
|
|
13464
13500
|
import { infraId as infraId16 } from "@neat.is/types";
|
|
@@ -13532,7 +13568,7 @@ function firstStringLiteralArg(argsNode) {
|
|
|
13532
13568
|
return null;
|
|
13533
13569
|
}
|
|
13534
13570
|
function goSqlEndpointsFromFile(file, serviceDir) {
|
|
13535
|
-
if (
|
|
13571
|
+
if (path55.extname(file.path) !== ".go") return [];
|
|
13536
13572
|
if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
|
|
13537
13573
|
const tree = parseSource11(makeGoParser3(), file.content);
|
|
13538
13574
|
const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
|
|
@@ -13561,7 +13597,7 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
13561
13597
|
confidenceKind: "verified-call-site",
|
|
13562
13598
|
...columns.length > 0 ? { columns } : {},
|
|
13563
13599
|
evidence: {
|
|
13564
|
-
file: toPosix(
|
|
13600
|
+
file: toPosix(path55.relative(serviceDir, file.path)),
|
|
13565
13601
|
line,
|
|
13566
13602
|
snippet: snippet(file.content, line)
|
|
13567
13603
|
}
|
|
@@ -13571,7 +13607,7 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
13571
13607
|
}
|
|
13572
13608
|
|
|
13573
13609
|
// src/extract/calls/gorm.ts
|
|
13574
|
-
import
|
|
13610
|
+
import path56 from "path";
|
|
13575
13611
|
import Parser16 from "tree-sitter";
|
|
13576
13612
|
import Go5 from "tree-sitter-go";
|
|
13577
13613
|
import { infraId as infraId17 } from "@neat.is/types";
|
|
@@ -14004,7 +14040,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
|
|
|
14004
14040
|
seen.delete(struct.name);
|
|
14005
14041
|
}
|
|
14006
14042
|
function gormEndpointsFromFile(file, serviceDir) {
|
|
14007
|
-
if (
|
|
14043
|
+
if (path56.extname(file.path) !== ".go") return [];
|
|
14008
14044
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
14009
14045
|
const tree = parseSource12(makeGoParser4(), file.content);
|
|
14010
14046
|
const { structs, models, tableFor } = analyze(tree);
|
|
@@ -14026,7 +14062,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
14026
14062
|
confidenceKind: "structural",
|
|
14027
14063
|
...columns.length > 0 ? { columns } : {},
|
|
14028
14064
|
evidence: {
|
|
14029
|
-
file: toPosix(
|
|
14065
|
+
file: toPosix(path56.relative(serviceDir, file.path)),
|
|
14030
14066
|
line: struct.line,
|
|
14031
14067
|
snippet: snippet(file.content, struct.line)
|
|
14032
14068
|
}
|
|
@@ -14035,7 +14071,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
14035
14071
|
return out;
|
|
14036
14072
|
}
|
|
14037
14073
|
function gormForeignKeys(file, serviceDir) {
|
|
14038
|
-
if (
|
|
14074
|
+
if (path56.extname(file.path) !== ".go") return [];
|
|
14039
14075
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
14040
14076
|
const tree = parseSource12(makeGoParser4(), file.content);
|
|
14041
14077
|
const { structs, models, tableFor } = analyze(tree);
|
|
@@ -14050,7 +14086,7 @@ function gormForeignKeys(file, serviceDir) {
|
|
|
14050
14086
|
childTable,
|
|
14051
14087
|
parentTable,
|
|
14052
14088
|
evidence: {
|
|
14053
|
-
file: toPosix(
|
|
14089
|
+
file: toPosix(path56.relative(serviceDir, file.path)),
|
|
14054
14090
|
line,
|
|
14055
14091
|
snippet: snippet(file.content, line)
|
|
14056
14092
|
}
|
|
@@ -14086,7 +14122,7 @@ function gormForeignKeys(file, serviceDir) {
|
|
|
14086
14122
|
}
|
|
14087
14123
|
|
|
14088
14124
|
// src/extract/calls/efcore.ts
|
|
14089
|
-
import
|
|
14125
|
+
import path57 from "path";
|
|
14090
14126
|
import Parser17 from "tree-sitter";
|
|
14091
14127
|
import CSharp2 from "tree-sitter-c-sharp";
|
|
14092
14128
|
import { infraId as infraId18 } from "@neat.is/types";
|
|
@@ -14165,7 +14201,7 @@ function tableFromToTable(call) {
|
|
|
14165
14201
|
return value ? csStringLiteral(value) : null;
|
|
14166
14202
|
}
|
|
14167
14203
|
function efcoreEndpointsFromFile(file, serviceDir) {
|
|
14168
|
-
if (
|
|
14204
|
+
if (path57.extname(file.path) !== ".cs") return [];
|
|
14169
14205
|
if (!EFCORE_GATE.test(file.content)) return [];
|
|
14170
14206
|
const tree = parseSource13(makeCsParser(), file.content);
|
|
14171
14207
|
const out = [];
|
|
@@ -14180,7 +14216,7 @@ function efcoreEndpointsFromFile(file, serviceDir) {
|
|
|
14180
14216
|
edgeType: "CALLS",
|
|
14181
14217
|
confidenceKind: "structural",
|
|
14182
14218
|
evidence: {
|
|
14183
|
-
file: toPosix(
|
|
14219
|
+
file: toPosix(path57.relative(serviceDir, file.path)),
|
|
14184
14220
|
line,
|
|
14185
14221
|
snippet: snippet(file.content, line)
|
|
14186
14222
|
}
|
|
@@ -14437,7 +14473,7 @@ function ensureTableNode(graph, id, name) {
|
|
|
14437
14473
|
}
|
|
14438
14474
|
|
|
14439
14475
|
// src/extract/infra/docker-compose.ts
|
|
14440
|
-
import
|
|
14476
|
+
import path58 from "path";
|
|
14441
14477
|
import { EdgeType as EdgeType17, Provenance as Provenance18, confidenceForExtracted as confidenceForExtracted15 } from "@neat.is/types";
|
|
14442
14478
|
|
|
14443
14479
|
// src/extract/infra/shared.ts
|
|
@@ -14507,7 +14543,7 @@ function dependsOnList(value) {
|
|
|
14507
14543
|
}
|
|
14508
14544
|
function serviceNameToServiceNode(name, services) {
|
|
14509
14545
|
for (const s of services) {
|
|
14510
|
-
if (s.node.name === name ||
|
|
14546
|
+
if (s.node.name === name || path58.basename(s.dir) === name) return s.node.id;
|
|
14511
14547
|
}
|
|
14512
14548
|
return null;
|
|
14513
14549
|
}
|
|
@@ -14516,7 +14552,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
14516
14552
|
let edgesAdded = 0;
|
|
14517
14553
|
let composePath = null;
|
|
14518
14554
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
14519
|
-
const abs =
|
|
14555
|
+
const abs = path58.join(scanPath, name);
|
|
14520
14556
|
if (await exists(abs)) {
|
|
14521
14557
|
composePath = abs;
|
|
14522
14558
|
break;
|
|
@@ -14529,13 +14565,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
14529
14565
|
} catch (err) {
|
|
14530
14566
|
recordExtractionError(
|
|
14531
14567
|
"infra docker-compose",
|
|
14532
|
-
|
|
14568
|
+
path58.relative(scanPath, composePath),
|
|
14533
14569
|
err
|
|
14534
14570
|
);
|
|
14535
14571
|
return { nodesAdded, edgesAdded };
|
|
14536
14572
|
}
|
|
14537
14573
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
14538
|
-
const evidenceFile =
|
|
14574
|
+
const evidenceFile = path58.relative(scanPath, composePath).split(path58.sep).join("/");
|
|
14539
14575
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
14540
14576
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
14541
14577
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -14576,8 +14612,8 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
14576
14612
|
}
|
|
14577
14613
|
|
|
14578
14614
|
// src/extract/infra/dockerfile.ts
|
|
14579
|
-
import
|
|
14580
|
-
import { promises as
|
|
14615
|
+
import path59 from "path";
|
|
14616
|
+
import { promises as fs27 } from "fs";
|
|
14581
14617
|
import { EdgeType as EdgeType18, Provenance as Provenance19, confidenceForExtracted as confidenceForExtracted16 } from "@neat.is/types";
|
|
14582
14618
|
function readDockerfile(content) {
|
|
14583
14619
|
let image = null;
|
|
@@ -14607,15 +14643,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
14607
14643
|
let nodesAdded = 0;
|
|
14608
14644
|
let edgesAdded = 0;
|
|
14609
14645
|
for (const service of services) {
|
|
14610
|
-
const dockerfilePath =
|
|
14646
|
+
const dockerfilePath = path59.join(service.dir, "Dockerfile");
|
|
14611
14647
|
if (!await exists(dockerfilePath)) continue;
|
|
14612
14648
|
let content;
|
|
14613
14649
|
try {
|
|
14614
|
-
content = await
|
|
14650
|
+
content = await fs27.readFile(dockerfilePath, "utf8");
|
|
14615
14651
|
} catch (err) {
|
|
14616
14652
|
recordExtractionError(
|
|
14617
14653
|
"infra dockerfile",
|
|
14618
|
-
|
|
14654
|
+
path59.relative(scanPath, dockerfilePath),
|
|
14619
14655
|
err
|
|
14620
14656
|
);
|
|
14621
14657
|
continue;
|
|
@@ -14627,8 +14663,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
14627
14663
|
graph.addNode(node.id, node);
|
|
14628
14664
|
nodesAdded++;
|
|
14629
14665
|
}
|
|
14630
|
-
const relDockerfile = toPosix(
|
|
14631
|
-
const evidenceFile = toPosix(
|
|
14666
|
+
const relDockerfile = toPosix(path59.relative(service.dir, dockerfilePath));
|
|
14667
|
+
const evidenceFile = toPosix(path59.relative(scanPath, dockerfilePath));
|
|
14632
14668
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
14633
14669
|
graph,
|
|
14634
14670
|
service.pkg.name,
|
|
@@ -14679,23 +14715,23 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
14679
14715
|
}
|
|
14680
14716
|
|
|
14681
14717
|
// src/extract/infra/terraform.ts
|
|
14682
|
-
import { promises as
|
|
14683
|
-
import
|
|
14718
|
+
import { promises as fs28 } from "fs";
|
|
14719
|
+
import path60 from "path";
|
|
14684
14720
|
import { EdgeType as EdgeType19, Provenance as Provenance20, confidenceForExtracted as confidenceForExtracted17 } from "@neat.is/types";
|
|
14685
14721
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
14686
14722
|
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
14687
14723
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
14688
14724
|
if (depth > max) return [];
|
|
14689
14725
|
const out = [];
|
|
14690
|
-
const entries = await
|
|
14726
|
+
const entries = await fs28.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
14691
14727
|
for (const entry of entries) {
|
|
14692
14728
|
if (entry.isDirectory()) {
|
|
14693
14729
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
14694
|
-
const child =
|
|
14730
|
+
const child = path60.join(start, entry.name);
|
|
14695
14731
|
if (await isPythonVenvDir(child)) continue;
|
|
14696
14732
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
14697
14733
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
14698
|
-
out.push(
|
|
14734
|
+
out.push(path60.join(start, entry.name));
|
|
14699
14735
|
}
|
|
14700
14736
|
}
|
|
14701
14737
|
return out;
|
|
@@ -14726,8 +14762,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
14726
14762
|
let edgesAdded = 0;
|
|
14727
14763
|
const files = await walkTfFiles(scanPath);
|
|
14728
14764
|
for (const file of files) {
|
|
14729
|
-
const content = await
|
|
14730
|
-
const evidenceFile = toPosix(
|
|
14765
|
+
const content = await fs28.readFile(file, "utf8");
|
|
14766
|
+
const evidenceFile = toPosix(path60.relative(scanPath, file));
|
|
14731
14767
|
const resources = [];
|
|
14732
14768
|
const byKey = /* @__PURE__ */ new Map();
|
|
14733
14769
|
RESOURCE_RE.lastIndex = 0;
|
|
@@ -14783,8 +14819,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
14783
14819
|
}
|
|
14784
14820
|
|
|
14785
14821
|
// src/extract/infra/k8s.ts
|
|
14786
|
-
import { promises as
|
|
14787
|
-
import
|
|
14822
|
+
import { promises as fs29 } from "fs";
|
|
14823
|
+
import path61 from "path";
|
|
14788
14824
|
import { parseAllDocuments as parseAllDocuments2 } from "yaml";
|
|
14789
14825
|
import { EdgeType as EdgeType20, serviceId as serviceId13 } from "@neat.is/types";
|
|
14790
14826
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
@@ -14800,15 +14836,15 @@ var WORKLOAD_KINDS = /* @__PURE__ */ new Set(["Deployment", "StatefulSet", "Daem
|
|
|
14800
14836
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
14801
14837
|
if (depth > max) return [];
|
|
14802
14838
|
const out = [];
|
|
14803
|
-
const entries = await
|
|
14839
|
+
const entries = await fs29.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
14804
14840
|
for (const entry of entries) {
|
|
14805
14841
|
if (entry.isDirectory()) {
|
|
14806
14842
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
14807
|
-
const child =
|
|
14843
|
+
const child = path61.join(start, entry.name);
|
|
14808
14844
|
if (await isPythonVenvDir(child)) continue;
|
|
14809
14845
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
14810
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
14811
|
-
out.push(
|
|
14846
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path61.extname(entry.name))) {
|
|
14847
|
+
out.push(path61.join(start, entry.name));
|
|
14812
14848
|
}
|
|
14813
14849
|
}
|
|
14814
14850
|
return out;
|
|
@@ -14818,14 +14854,14 @@ async function addK8sResources(graph, scanPath) {
|
|
|
14818
14854
|
let edgesAdded = 0;
|
|
14819
14855
|
const files = await walkYamlFiles2(scanPath);
|
|
14820
14856
|
for (const file of files) {
|
|
14821
|
-
const content = await
|
|
14857
|
+
const content = await fs29.readFile(file, "utf8");
|
|
14822
14858
|
let docs;
|
|
14823
14859
|
try {
|
|
14824
14860
|
docs = parseAllDocuments2(content).map((d) => d.toJSON());
|
|
14825
14861
|
} catch {
|
|
14826
14862
|
continue;
|
|
14827
14863
|
}
|
|
14828
|
-
const relFile = toPosix(
|
|
14864
|
+
const relFile = toPosix(path61.relative(scanPath, file));
|
|
14829
14865
|
for (const doc of docs) {
|
|
14830
14866
|
if (!doc?.kind || !doc.metadata?.name) continue;
|
|
14831
14867
|
const infraKind = K8S_KIND_TO_INFRA_KIND[doc.kind];
|
|
@@ -14873,16 +14909,16 @@ async function addK8sResources(graph, scanPath) {
|
|
|
14873
14909
|
}
|
|
14874
14910
|
|
|
14875
14911
|
// src/extract/infra/cloudflare.ts
|
|
14876
|
-
import { promises as
|
|
14877
|
-
import
|
|
14912
|
+
import { promises as fs30 } from "fs";
|
|
14913
|
+
import path62 from "path";
|
|
14878
14914
|
import { parse as parseToml3 } from "smol-toml";
|
|
14879
14915
|
import { EdgeType as EdgeType21, Provenance as Provenance21, confidenceForExtracted as confidenceForExtracted18 } from "@neat.is/types";
|
|
14880
14916
|
var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
|
|
14881
14917
|
async function readWranglerConfig(dir) {
|
|
14882
14918
|
for (const filename of WRANGLER_FILENAMES) {
|
|
14883
|
-
const abs =
|
|
14919
|
+
const abs = path62.join(dir, filename);
|
|
14884
14920
|
if (!await exists(abs)) continue;
|
|
14885
|
-
const raw = await
|
|
14921
|
+
const raw = await fs30.readFile(abs, "utf8");
|
|
14886
14922
|
const config = filename === "wrangler.toml" ? parseToml3(raw) : JSON.parse(maskCommentsInSource(raw));
|
|
14887
14923
|
return { config, relFile: filename, raw };
|
|
14888
14924
|
}
|
|
@@ -14943,11 +14979,11 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
14943
14979
|
try {
|
|
14944
14980
|
read = await readWranglerConfig(service.dir);
|
|
14945
14981
|
} catch (err) {
|
|
14946
|
-
recordExtractionError("infra cloudflare",
|
|
14982
|
+
recordExtractionError("infra cloudflare", path62.relative(scanPath, service.dir), err);
|
|
14947
14983
|
continue;
|
|
14948
14984
|
}
|
|
14949
14985
|
if (!read || !read.config.name) continue;
|
|
14950
|
-
const evidenceFile = toPosix(
|
|
14986
|
+
const evidenceFile = toPosix(path62.relative(scanPath, path62.join(service.dir, read.relFile)));
|
|
14951
14987
|
discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
|
|
14952
14988
|
}
|
|
14953
14989
|
for (const worker of discovered) {
|
|
@@ -14959,7 +14995,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
14959
14995
|
}
|
|
14960
14996
|
let anchorId = service.node.id;
|
|
14961
14997
|
if (config.main) {
|
|
14962
|
-
const entryRelPath = toPosix(
|
|
14998
|
+
const entryRelPath = toPosix(path62.normalize(config.main));
|
|
14963
14999
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
14964
15000
|
graph,
|
|
14965
15001
|
service.pkg.name,
|
|
@@ -15105,24 +15141,24 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
15105
15141
|
}
|
|
15106
15142
|
|
|
15107
15143
|
// src/extract/infra/vercel.ts
|
|
15108
|
-
import { promises as
|
|
15109
|
-
import
|
|
15144
|
+
import { promises as fs31 } from "fs";
|
|
15145
|
+
import path63 from "path";
|
|
15110
15146
|
import { EdgeType as EdgeType22 } from "@neat.is/types";
|
|
15111
15147
|
var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
|
|
15112
15148
|
async function readVercelConfig(dir) {
|
|
15113
15149
|
for (const filename of VERCEL_CONFIG_FILENAMES) {
|
|
15114
|
-
const abs =
|
|
15150
|
+
const abs = path63.join(dir, filename);
|
|
15115
15151
|
if (!await exists(abs)) continue;
|
|
15116
|
-
const raw = await
|
|
15152
|
+
const raw = await fs31.readFile(abs, "utf8");
|
|
15117
15153
|
const config = JSON.parse(maskCommentsInSource(raw));
|
|
15118
15154
|
return { config, relFile: filename, raw };
|
|
15119
15155
|
}
|
|
15120
15156
|
return null;
|
|
15121
15157
|
}
|
|
15122
15158
|
async function readLinkedProjectName(dir) {
|
|
15123
|
-
const abs =
|
|
15159
|
+
const abs = path63.join(dir, ".vercel", "project.json");
|
|
15124
15160
|
if (!await exists(abs)) return void 0;
|
|
15125
|
-
const parsed = JSON.parse(await
|
|
15161
|
+
const parsed = JSON.parse(await fs31.readFile(abs, "utf8"));
|
|
15126
15162
|
return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
|
|
15127
15163
|
}
|
|
15128
15164
|
function routeSource(route) {
|
|
@@ -15138,7 +15174,7 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
15138
15174
|
read = await readVercelConfig(service.dir);
|
|
15139
15175
|
projectName = await readLinkedProjectName(service.dir);
|
|
15140
15176
|
} catch (err) {
|
|
15141
|
-
recordExtractionError("infra vercel",
|
|
15177
|
+
recordExtractionError("infra vercel", path63.relative(scanPath, service.dir), err);
|
|
15142
15178
|
continue;
|
|
15143
15179
|
}
|
|
15144
15180
|
if (!read && !projectName) continue;
|
|
@@ -15154,7 +15190,7 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
15154
15190
|
const anchorId = service.node.id;
|
|
15155
15191
|
if (!read) continue;
|
|
15156
15192
|
const { config, relFile, raw } = read;
|
|
15157
|
-
const evidenceFile = toPosix(
|
|
15193
|
+
const evidenceFile = toPosix(path63.relative(scanPath, path63.join(service.dir, relFile)));
|
|
15158
15194
|
const add = (edgeType, kind, name) => {
|
|
15159
15195
|
if (!name) return;
|
|
15160
15196
|
const result = emitPlatformResourceEdge(
|
|
@@ -15182,16 +15218,16 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
15182
15218
|
}
|
|
15183
15219
|
|
|
15184
15220
|
// src/extract/infra/railway.ts
|
|
15185
|
-
import { promises as
|
|
15186
|
-
import
|
|
15221
|
+
import { promises as fs32 } from "fs";
|
|
15222
|
+
import path64 from "path";
|
|
15187
15223
|
import { parse as parseToml4 } from "smol-toml";
|
|
15188
15224
|
import { EdgeType as EdgeType23 } from "@neat.is/types";
|
|
15189
15225
|
var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
|
|
15190
15226
|
async function readRailwayConfig(dir) {
|
|
15191
15227
|
for (const filename of RAILWAY_FILENAMES) {
|
|
15192
|
-
const abs =
|
|
15228
|
+
const abs = path64.join(dir, filename);
|
|
15193
15229
|
if (!await exists(abs)) continue;
|
|
15194
|
-
const raw = await
|
|
15230
|
+
const raw = await fs32.readFile(abs, "utf8");
|
|
15195
15231
|
const config = filename === "railway.toml" ? parseToml4(raw) : JSON.parse(maskCommentsInSource(raw));
|
|
15196
15232
|
return { config, relFile: filename, raw };
|
|
15197
15233
|
}
|
|
@@ -15205,7 +15241,7 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
15205
15241
|
try {
|
|
15206
15242
|
read = await readRailwayConfig(service.dir);
|
|
15207
15243
|
} catch (err) {
|
|
15208
|
-
recordExtractionError("infra railway",
|
|
15244
|
+
recordExtractionError("infra railway", path64.relative(scanPath, service.dir), err);
|
|
15209
15245
|
continue;
|
|
15210
15246
|
}
|
|
15211
15247
|
if (!read) continue;
|
|
@@ -15215,7 +15251,7 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
15215
15251
|
}
|
|
15216
15252
|
const anchorId = service.node.id;
|
|
15217
15253
|
const { config, relFile, raw } = read;
|
|
15218
|
-
const evidenceFile = toPosix(
|
|
15254
|
+
const evidenceFile = toPosix(path64.relative(scanPath, path64.join(service.dir, relFile)));
|
|
15219
15255
|
const add = (edgeType, kind, name) => {
|
|
15220
15256
|
if (!name) return;
|
|
15221
15257
|
const result = emitPlatformResourceEdge(
|
|
@@ -15239,15 +15275,15 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
15239
15275
|
}
|
|
15240
15276
|
|
|
15241
15277
|
// src/extract/infra/supabase.ts
|
|
15242
|
-
import { promises as
|
|
15243
|
-
import
|
|
15278
|
+
import { promises as fs33 } from "fs";
|
|
15279
|
+
import path65 from "path";
|
|
15244
15280
|
import { parse as parseToml5 } from "smol-toml";
|
|
15245
15281
|
import { EdgeType as EdgeType24 } from "@neat.is/types";
|
|
15246
15282
|
async function readSupabaseConfig(dir) {
|
|
15247
|
-
const relFile =
|
|
15248
|
-
const abs =
|
|
15283
|
+
const relFile = path65.join("supabase", "config.toml");
|
|
15284
|
+
const abs = path65.join(dir, relFile);
|
|
15249
15285
|
if (!await exists(abs)) return null;
|
|
15250
|
-
const raw = await
|
|
15286
|
+
const raw = await fs33.readFile(abs, "utf8");
|
|
15251
15287
|
const config = parseToml5(raw);
|
|
15252
15288
|
return { config, relFile, raw };
|
|
15253
15289
|
}
|
|
@@ -15259,7 +15295,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
|
|
|
15259
15295
|
try {
|
|
15260
15296
|
read = await readSupabaseConfig(service.dir);
|
|
15261
15297
|
} catch (err) {
|
|
15262
|
-
recordExtractionError("infra supabase",
|
|
15298
|
+
recordExtractionError("infra supabase", path65.relative(scanPath, service.dir), err);
|
|
15263
15299
|
continue;
|
|
15264
15300
|
}
|
|
15265
15301
|
if (!read) continue;
|
|
@@ -15274,7 +15310,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
|
|
|
15274
15310
|
});
|
|
15275
15311
|
}
|
|
15276
15312
|
const anchorId = service.node.id;
|
|
15277
|
-
const evidenceFile = toPosix(
|
|
15313
|
+
const evidenceFile = toPosix(path65.relative(scanPath, path65.join(service.dir, relFile)));
|
|
15278
15314
|
const add = (edgeType, kind, name) => {
|
|
15279
15315
|
if (!name) return;
|
|
15280
15316
|
const result = emitPlatformResourceEdge(
|
|
@@ -15315,7 +15351,7 @@ async function addInfra(graph, scanPath, services) {
|
|
|
15315
15351
|
}
|
|
15316
15352
|
|
|
15317
15353
|
// src/extract/zod-shapes.ts
|
|
15318
|
-
import
|
|
15354
|
+
import path66 from "path";
|
|
15319
15355
|
import Parser18 from "tree-sitter";
|
|
15320
15356
|
import JavaScript8 from "tree-sitter-javascript";
|
|
15321
15357
|
import {
|
|
@@ -15417,7 +15453,7 @@ function topLevelSchemas(root) {
|
|
|
15417
15453
|
}
|
|
15418
15454
|
function zodShapesFromFile(file, serviceDir) {
|
|
15419
15455
|
if (!ZOD_IMPORT_RE.test(file.content)) return [];
|
|
15420
|
-
const tree = parseSource3(parserForExt4(
|
|
15456
|
+
const tree = parseSource3(parserForExt4(path66.extname(file.path)), file.content);
|
|
15421
15457
|
const out = [];
|
|
15422
15458
|
const seen = /* @__PURE__ */ new Set();
|
|
15423
15459
|
for (const { name, call } of topLevelSchemas(tree.rootNode)) {
|
|
@@ -15435,7 +15471,7 @@ function zodShapesFromFile(file, serviceDir) {
|
|
|
15435
15471
|
name,
|
|
15436
15472
|
fields,
|
|
15437
15473
|
evidence: {
|
|
15438
|
-
file:
|
|
15474
|
+
file: path66.relative(serviceDir, file.path),
|
|
15439
15475
|
line,
|
|
15440
15476
|
snippet: snippet(file.content, line)
|
|
15441
15477
|
}
|
|
@@ -15702,11 +15738,11 @@ async function addFirestoreRules(graph, services) {
|
|
|
15702
15738
|
}
|
|
15703
15739
|
|
|
15704
15740
|
// src/extract/index.ts
|
|
15705
|
-
import
|
|
15741
|
+
import path68 from "path";
|
|
15706
15742
|
|
|
15707
15743
|
// src/extract/retire.ts
|
|
15708
15744
|
import { existsSync as existsSync2 } from "fs";
|
|
15709
|
-
import
|
|
15745
|
+
import path67 from "path";
|
|
15710
15746
|
import { NodeType as NodeType29, Provenance as Provenance23 } from "@neat.is/types";
|
|
15711
15747
|
function dropOrphanedFileNodes(graph, source) {
|
|
15712
15748
|
const orphans = [];
|
|
@@ -15753,11 +15789,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = [], so
|
|
|
15753
15789
|
if (outOfScope(graph, edge, source)) return;
|
|
15754
15790
|
const evidenceFile = edge.evidence?.file;
|
|
15755
15791
|
if (!evidenceFile) return;
|
|
15756
|
-
if (
|
|
15792
|
+
if (path67.isAbsolute(evidenceFile)) {
|
|
15757
15793
|
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
15758
15794
|
return;
|
|
15759
15795
|
}
|
|
15760
|
-
const found = bases.some((base) => existsSync2(
|
|
15796
|
+
const found = bases.some((base) => existsSync2(path67.join(base, evidenceFile)));
|
|
15761
15797
|
if (!found) toDrop.push(id);
|
|
15762
15798
|
});
|
|
15763
15799
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -15766,7 +15802,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = [], so
|
|
|
15766
15802
|
}
|
|
15767
15803
|
|
|
15768
15804
|
// src/extract/index.ts
|
|
15769
|
-
|
|
15805
|
+
var passQueue = Promise.resolve();
|
|
15806
|
+
function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
15807
|
+
const pass = passQueue.then(() => runExtractionPass(graph, scanPath, opts));
|
|
15808
|
+
passQueue = pass.catch(() => {
|
|
15809
|
+
});
|
|
15810
|
+
return pass;
|
|
15811
|
+
}
|
|
15812
|
+
async function runExtractionPass(graph, scanPath, opts) {
|
|
15770
15813
|
await ensureCompatLoaded();
|
|
15771
15814
|
drainExtractionErrors();
|
|
15772
15815
|
drainSkippedFiles();
|
|
@@ -15818,7 +15861,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
15818
15861
|
const droppedEntries = drainDroppedExtracted();
|
|
15819
15862
|
const skippedEntries = drainSkippedFiles();
|
|
15820
15863
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
15821
|
-
const rejectedPath =
|
|
15864
|
+
const rejectedPath = path68.join(path68.dirname(opts.errorsPath), "rejected.ndjson");
|
|
15822
15865
|
try {
|
|
15823
15866
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
15824
15867
|
} catch (err) {
|
|
@@ -15840,7 +15883,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
15840
15883
|
skippedFiles: skippedEntries.length,
|
|
15841
15884
|
skippedEntries
|
|
15842
15885
|
};
|
|
15843
|
-
emitNeatEvent({
|
|
15886
|
+
if (opts.announce !== false) emitNeatEvent({
|
|
15844
15887
|
type: "extraction-complete",
|
|
15845
15888
|
project: opts.project ?? DEFAULT_PROJECT,
|
|
15846
15889
|
payload: {
|
|
@@ -16540,8 +16583,8 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16540
16583
|
}
|
|
16541
16584
|
|
|
16542
16585
|
// src/persist.ts
|
|
16543
|
-
import { promises as
|
|
16544
|
-
import
|
|
16586
|
+
import { promises as fs34 } from "fs";
|
|
16587
|
+
import path69 from "path";
|
|
16545
16588
|
import { NodeType as NodeType31, Provenance as Provenance25, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
16546
16589
|
var SCHEMA_VERSION = 7;
|
|
16547
16590
|
function migrateV1ToV2(payload) {
|
|
@@ -16596,7 +16639,7 @@ function migrateV2ToV3(payload) {
|
|
|
16596
16639
|
return { ...payload, schemaVersion: 3 };
|
|
16597
16640
|
}
|
|
16598
16641
|
async function ensureDir(filePath) {
|
|
16599
|
-
await
|
|
16642
|
+
await fs34.mkdir(path69.dirname(filePath), { recursive: true });
|
|
16600
16643
|
}
|
|
16601
16644
|
var WRITE_FLUSH_CHARS = 4 * 1024 * 1024;
|
|
16602
16645
|
function jsonOrUndefined(value) {
|
|
@@ -16642,7 +16685,7 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
16642
16685
|
graph: graph.export()
|
|
16643
16686
|
};
|
|
16644
16687
|
const tmp = `${outPath}.tmp`;
|
|
16645
|
-
const handle = await
|
|
16688
|
+
const handle = await fs34.open(tmp, "w");
|
|
16646
16689
|
try {
|
|
16647
16690
|
let buffered = "";
|
|
16648
16691
|
for (const chunk of serializeGraphChunks(payload)) {
|
|
@@ -16656,7 +16699,7 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
16656
16699
|
} finally {
|
|
16657
16700
|
await handle.close();
|
|
16658
16701
|
}
|
|
16659
|
-
await
|
|
16702
|
+
await fs34.rename(tmp, outPath);
|
|
16660
16703
|
}
|
|
16661
16704
|
function migrateSnapshot(input) {
|
|
16662
16705
|
let payload = input;
|
|
@@ -16688,7 +16731,7 @@ function migrateSnapshot(input) {
|
|
|
16688
16731
|
async function loadGraphFromDisk(graph, outPath) {
|
|
16689
16732
|
let raw;
|
|
16690
16733
|
try {
|
|
16691
|
-
raw = await
|
|
16734
|
+
raw = await fs34.readFile(outPath, "utf8");
|
|
16692
16735
|
} catch (err) {
|
|
16693
16736
|
if (err.code === "ENOENT") return;
|
|
16694
16737
|
throw err;
|
|
@@ -16738,7 +16781,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
16738
16781
|
}
|
|
16739
16782
|
|
|
16740
16783
|
// src/diff.ts
|
|
16741
|
-
import { promises as
|
|
16784
|
+
import { promises as fs35 } from "fs";
|
|
16742
16785
|
async function loadSnapshotForDiff(target) {
|
|
16743
16786
|
if (/^https?:\/\//i.test(target)) {
|
|
16744
16787
|
const res = await fetch(target);
|
|
@@ -16747,7 +16790,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
16747
16790
|
}
|
|
16748
16791
|
return await res.json();
|
|
16749
16792
|
}
|
|
16750
|
-
const raw = await
|
|
16793
|
+
const raw = await fs35.readFile(target, "utf8");
|
|
16751
16794
|
return JSON.parse(raw);
|
|
16752
16795
|
}
|
|
16753
16796
|
function indexEntries(entries) {
|
|
@@ -16814,23 +16857,23 @@ function canonicalJson(value) {
|
|
|
16814
16857
|
}
|
|
16815
16858
|
|
|
16816
16859
|
// src/projects.ts
|
|
16817
|
-
import
|
|
16860
|
+
import path70 from "path";
|
|
16818
16861
|
function pathsForProject(project, baseDir) {
|
|
16819
16862
|
if (project === DEFAULT_PROJECT) {
|
|
16820
16863
|
return {
|
|
16821
|
-
snapshotPath:
|
|
16822
|
-
errorsPath:
|
|
16823
|
-
staleEventsPath:
|
|
16824
|
-
embeddingsCachePath:
|
|
16825
|
-
policyViolationsPath:
|
|
16864
|
+
snapshotPath: path70.join(baseDir, "graph.json"),
|
|
16865
|
+
errorsPath: path70.join(baseDir, "errors.ndjson"),
|
|
16866
|
+
staleEventsPath: path70.join(baseDir, "stale-events.ndjson"),
|
|
16867
|
+
embeddingsCachePath: path70.join(baseDir, "embeddings.json"),
|
|
16868
|
+
policyViolationsPath: path70.join(baseDir, "policy-violations.ndjson")
|
|
16826
16869
|
};
|
|
16827
16870
|
}
|
|
16828
16871
|
return {
|
|
16829
|
-
snapshotPath:
|
|
16830
|
-
errorsPath:
|
|
16831
|
-
staleEventsPath:
|
|
16832
|
-
embeddingsCachePath:
|
|
16833
|
-
policyViolationsPath:
|
|
16872
|
+
snapshotPath: path70.join(baseDir, `${project}.json`),
|
|
16873
|
+
errorsPath: path70.join(baseDir, `errors.${project}.ndjson`),
|
|
16874
|
+
staleEventsPath: path70.join(baseDir, `stale-events.${project}.ndjson`),
|
|
16875
|
+
embeddingsCachePath: path70.join(baseDir, `embeddings.${project}.json`),
|
|
16876
|
+
policyViolationsPath: path70.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
16834
16877
|
};
|
|
16835
16878
|
}
|
|
16836
16879
|
var Projects = class {
|
|
@@ -16869,9 +16912,9 @@ function parseExtraProjects(raw) {
|
|
|
16869
16912
|
}
|
|
16870
16913
|
|
|
16871
16914
|
// src/registry.ts
|
|
16872
|
-
import { promises as
|
|
16915
|
+
import { promises as fs36 } from "fs";
|
|
16873
16916
|
import os2 from "os";
|
|
16874
|
-
import
|
|
16917
|
+
import path71 from "path";
|
|
16875
16918
|
import {
|
|
16876
16919
|
RegistryFileSchema
|
|
16877
16920
|
} from "@neat.is/types";
|
|
@@ -16879,20 +16922,20 @@ var LOCK_TIMEOUT_MS = 5e3;
|
|
|
16879
16922
|
var LOCK_RETRY_MS = 50;
|
|
16880
16923
|
function neatHome() {
|
|
16881
16924
|
const override = process.env.NEAT_HOME;
|
|
16882
|
-
if (override && override.length > 0) return
|
|
16883
|
-
return
|
|
16925
|
+
if (override && override.length > 0) return path71.resolve(override);
|
|
16926
|
+
return path71.join(os2.homedir(), ".neat");
|
|
16884
16927
|
}
|
|
16885
16928
|
function registryPath() {
|
|
16886
|
-
return
|
|
16929
|
+
return path71.join(neatHome(), "projects.json");
|
|
16887
16930
|
}
|
|
16888
16931
|
function registryLockPath() {
|
|
16889
|
-
return
|
|
16932
|
+
return path71.join(neatHome(), "projects.json.lock");
|
|
16890
16933
|
}
|
|
16891
16934
|
function daemonPidPath() {
|
|
16892
|
-
return
|
|
16935
|
+
return path71.join(neatHome(), "neatd.pid");
|
|
16893
16936
|
}
|
|
16894
16937
|
function daemonsDir() {
|
|
16895
|
-
return
|
|
16938
|
+
return path71.join(neatHome(), "daemons");
|
|
16896
16939
|
}
|
|
16897
16940
|
function isFiniteInt(v) {
|
|
16898
16941
|
return typeof v === "number" && Number.isFinite(v);
|
|
@@ -16925,7 +16968,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
16925
16968
|
const dir = daemonsDir();
|
|
16926
16969
|
let names;
|
|
16927
16970
|
try {
|
|
16928
|
-
names = await
|
|
16971
|
+
names = await fs36.readdir(dir);
|
|
16929
16972
|
} catch (err) {
|
|
16930
16973
|
if (err.code === "ENOENT") return [];
|
|
16931
16974
|
throw err;
|
|
@@ -16933,10 +16976,10 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
16933
16976
|
const out = [];
|
|
16934
16977
|
for (const name of names) {
|
|
16935
16978
|
if (!name.endsWith(".json")) continue;
|
|
16936
|
-
const file =
|
|
16979
|
+
const file = path71.join(dir, name);
|
|
16937
16980
|
let raw;
|
|
16938
16981
|
try {
|
|
16939
|
-
raw = await
|
|
16982
|
+
raw = await fs36.readFile(file, "utf8");
|
|
16940
16983
|
} catch {
|
|
16941
16984
|
continue;
|
|
16942
16985
|
}
|
|
@@ -16949,7 +16992,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
16949
16992
|
return out;
|
|
16950
16993
|
}
|
|
16951
16994
|
async function removeDaemonRecord(source) {
|
|
16952
|
-
await
|
|
16995
|
+
await fs36.unlink(source).catch(() => {
|
|
16953
16996
|
});
|
|
16954
16997
|
}
|
|
16955
16998
|
async function listMachineProjects(probe = defaultDiscoveryProbe) {
|
|
@@ -17006,7 +17049,7 @@ function isPidAliveDefault(pid) {
|
|
|
17006
17049
|
}
|
|
17007
17050
|
async function readPidFile(file) {
|
|
17008
17051
|
try {
|
|
17009
|
-
const raw = await
|
|
17052
|
+
const raw = await fs36.readFile(file, "utf8");
|
|
17010
17053
|
const pid = Number.parseInt(raw.trim(), 10);
|
|
17011
17054
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
17012
17055
|
} catch {
|
|
@@ -17052,32 +17095,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
17052
17095
|
}
|
|
17053
17096
|
}
|
|
17054
17097
|
async function normalizeProjectPath(input) {
|
|
17055
|
-
const resolved =
|
|
17098
|
+
const resolved = path71.resolve(input);
|
|
17056
17099
|
try {
|
|
17057
|
-
return await
|
|
17100
|
+
return await fs36.realpath(resolved);
|
|
17058
17101
|
} catch {
|
|
17059
17102
|
return resolved;
|
|
17060
17103
|
}
|
|
17061
17104
|
}
|
|
17062
17105
|
async function writeAtomically(target, contents) {
|
|
17063
|
-
await
|
|
17106
|
+
await fs36.mkdir(path71.dirname(target), { recursive: true });
|
|
17064
17107
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
17065
|
-
const fd = await
|
|
17108
|
+
const fd = await fs36.open(tmp, "w");
|
|
17066
17109
|
try {
|
|
17067
17110
|
await fd.writeFile(contents, "utf8");
|
|
17068
17111
|
await fd.sync();
|
|
17069
17112
|
} finally {
|
|
17070
17113
|
await fd.close();
|
|
17071
17114
|
}
|
|
17072
|
-
await
|
|
17115
|
+
await fs36.rename(tmp, target);
|
|
17073
17116
|
}
|
|
17074
17117
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
17075
17118
|
const deadline = Date.now() + timeoutMs;
|
|
17076
|
-
await
|
|
17119
|
+
await fs36.mkdir(path71.dirname(lockPath), { recursive: true });
|
|
17077
17120
|
let probedHolder = false;
|
|
17078
17121
|
while (true) {
|
|
17079
17122
|
try {
|
|
17080
|
-
const fd = await
|
|
17123
|
+
const fd = await fs36.open(lockPath, "wx");
|
|
17081
17124
|
try {
|
|
17082
17125
|
await fd.writeFile(`${process.pid}
|
|
17083
17126
|
`, "utf8");
|
|
@@ -17095,7 +17138,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
|
|
|
17095
17138
|
throw new RegistryLockError(lockHolderMessage(holder, lockPath, timeoutMs));
|
|
17096
17139
|
}
|
|
17097
17140
|
if (holder.kind === "stale" && await isReclaimableLock(lockPath)) {
|
|
17098
|
-
await
|
|
17141
|
+
await fs36.unlink(lockPath).catch(() => {
|
|
17099
17142
|
});
|
|
17100
17143
|
continue;
|
|
17101
17144
|
}
|
|
@@ -17113,14 +17156,14 @@ async function isReclaimableLock(lockPath) {
|
|
|
17113
17156
|
const pid = await readLockPid(lockPath);
|
|
17114
17157
|
if (pid !== void 0) return true;
|
|
17115
17158
|
try {
|
|
17116
|
-
const stat = await
|
|
17159
|
+
const stat = await fs36.stat(lockPath);
|
|
17117
17160
|
return Date.now() - stat.mtimeMs > LOCK_UNSTAMPED_GRACE_MS;
|
|
17118
17161
|
} catch {
|
|
17119
17162
|
return false;
|
|
17120
17163
|
}
|
|
17121
17164
|
}
|
|
17122
17165
|
async function releaseLock(lockPath) {
|
|
17123
|
-
await
|
|
17166
|
+
await fs36.unlink(lockPath).catch(() => {
|
|
17124
17167
|
});
|
|
17125
17168
|
}
|
|
17126
17169
|
async function withLock(fn) {
|
|
@@ -17136,7 +17179,7 @@ async function readRegistry() {
|
|
|
17136
17179
|
const file = registryPath();
|
|
17137
17180
|
let raw;
|
|
17138
17181
|
try {
|
|
17139
|
-
raw = await
|
|
17182
|
+
raw = await fs36.readFile(file, "utf8");
|
|
17140
17183
|
} catch (err) {
|
|
17141
17184
|
if (err.code === "ENOENT") {
|
|
17142
17185
|
return { version: 1, projects: [] };
|
|
@@ -17211,7 +17254,7 @@ async function findProjectByPath(dir) {
|
|
|
17211
17254
|
const { projects } = await readRegistry();
|
|
17212
17255
|
let best;
|
|
17213
17256
|
for (const entry of projects) {
|
|
17214
|
-
const inside = here === entry.path || here.startsWith(entry.path +
|
|
17257
|
+
const inside = here === entry.path || here.startsWith(entry.path + path71.sep);
|
|
17215
17258
|
if (inside && (!best || entry.path.length > best.path.length)) best = entry;
|
|
17216
17259
|
}
|
|
17217
17260
|
return best;
|
|
@@ -17263,7 +17306,7 @@ function pruneTtlMs() {
|
|
|
17263
17306
|
}
|
|
17264
17307
|
async function statPathStatus(p) {
|
|
17265
17308
|
try {
|
|
17266
|
-
const stat = await
|
|
17309
|
+
const stat = await fs36.stat(p);
|
|
17267
17310
|
return stat.isDirectory() ? "present" : "unknown";
|
|
17268
17311
|
} catch (err) {
|
|
17269
17312
|
return err.code === "ENOENT" ? "gone" : "unknown";
|
|
@@ -17304,18 +17347,19 @@ async function pruneRegistry(opts = {}) {
|
|
|
17304
17347
|
|
|
17305
17348
|
// src/api.ts
|
|
17306
17349
|
import Fastify from "fastify";
|
|
17350
|
+
import { timingSafeEqual } from "crypto";
|
|
17307
17351
|
import cors from "@fastify/cors";
|
|
17308
17352
|
import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
|
|
17309
17353
|
|
|
17310
17354
|
// src/extend/index.ts
|
|
17311
|
-
import { promises as
|
|
17312
|
-
import
|
|
17355
|
+
import { promises as fs38 } from "fs";
|
|
17356
|
+
import path73 from "path";
|
|
17313
17357
|
import os3 from "os";
|
|
17314
17358
|
import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
|
|
17315
17359
|
|
|
17316
17360
|
// src/installers/package-manager.ts
|
|
17317
|
-
import { promises as
|
|
17318
|
-
import
|
|
17361
|
+
import { promises as fs37 } from "fs";
|
|
17362
|
+
import path72 from "path";
|
|
17319
17363
|
import { spawn } from "child_process";
|
|
17320
17364
|
var LOCKFILE_PRIORITY = [
|
|
17321
17365
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -17330,29 +17374,29 @@ var LOCKFILE_PRIORITY = [
|
|
|
17330
17374
|
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
17331
17375
|
async function exists2(p) {
|
|
17332
17376
|
try {
|
|
17333
|
-
await
|
|
17377
|
+
await fs37.access(p);
|
|
17334
17378
|
return true;
|
|
17335
17379
|
} catch {
|
|
17336
17380
|
return false;
|
|
17337
17381
|
}
|
|
17338
17382
|
}
|
|
17339
17383
|
async function detectPackageManager(serviceDir) {
|
|
17340
|
-
let dir =
|
|
17384
|
+
let dir = path72.resolve(serviceDir);
|
|
17341
17385
|
const stops = /* @__PURE__ */ new Set();
|
|
17342
17386
|
for (let i = 0; i < 64; i++) {
|
|
17343
17387
|
if (stops.has(dir)) break;
|
|
17344
17388
|
stops.add(dir);
|
|
17345
17389
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
17346
|
-
const lockPath =
|
|
17390
|
+
const lockPath = path72.join(dir, candidate.lockfile);
|
|
17347
17391
|
if (await exists2(lockPath)) {
|
|
17348
17392
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
17349
17393
|
}
|
|
17350
17394
|
}
|
|
17351
|
-
const parent =
|
|
17395
|
+
const parent = path72.dirname(dir);
|
|
17352
17396
|
if (parent === dir) break;
|
|
17353
17397
|
dir = parent;
|
|
17354
17398
|
}
|
|
17355
|
-
return { pm: "npm", cwd:
|
|
17399
|
+
return { pm: "npm", cwd: path72.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
17356
17400
|
}
|
|
17357
17401
|
async function runPackageManagerInstall(cmd) {
|
|
17358
17402
|
return new Promise((resolve) => {
|
|
@@ -17394,15 +17438,15 @@ ${err.message}`
|
|
|
17394
17438
|
// src/extend/index.ts
|
|
17395
17439
|
async function fileExists2(p) {
|
|
17396
17440
|
try {
|
|
17397
|
-
await
|
|
17441
|
+
await fs38.access(p);
|
|
17398
17442
|
return true;
|
|
17399
17443
|
} catch {
|
|
17400
17444
|
return false;
|
|
17401
17445
|
}
|
|
17402
17446
|
}
|
|
17403
17447
|
async function readPackageJson(scanPath) {
|
|
17404
|
-
const pkgPath =
|
|
17405
|
-
const raw = await
|
|
17448
|
+
const pkgPath = path73.join(scanPath, "package.json");
|
|
17449
|
+
const raw = await fs38.readFile(pkgPath, "utf8");
|
|
17406
17450
|
return JSON.parse(raw);
|
|
17407
17451
|
}
|
|
17408
17452
|
var HOOK_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -17416,15 +17460,15 @@ var HOOK_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
17416
17460
|
async function findHookFiles(scanPath) {
|
|
17417
17461
|
const found = [];
|
|
17418
17462
|
const walk11 = async (dir) => {
|
|
17419
|
-
const entries = await
|
|
17463
|
+
const entries = await fs38.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
17420
17464
|
for (const entry of entries) {
|
|
17421
17465
|
if (entry.isDirectory()) {
|
|
17422
17466
|
if (entry.name.startsWith(".") || HOOK_WALK_SKIP_DIRS.has(entry.name)) continue;
|
|
17423
|
-
await walk11(
|
|
17467
|
+
await walk11(path73.join(dir, entry.name));
|
|
17424
17468
|
} else if (entry.isFile()) {
|
|
17425
17469
|
if ((entry.name.startsWith("instrumentation") || entry.name.startsWith("otel-init")) && /\.(ts|js|cjs|mjs)$/.test(entry.name)) {
|
|
17426
|
-
const rel =
|
|
17427
|
-
found.push(rel.split(
|
|
17470
|
+
const rel = path73.relative(scanPath, path73.join(dir, entry.name));
|
|
17471
|
+
found.push(rel.split(path73.sep).join("/"));
|
|
17428
17472
|
}
|
|
17429
17473
|
}
|
|
17430
17474
|
}
|
|
@@ -17435,7 +17479,7 @@ async function findHookFiles(scanPath) {
|
|
|
17435
17479
|
async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
|
|
17436
17480
|
let fallback = null;
|
|
17437
17481
|
for (const file of hookFiles) {
|
|
17438
|
-
const content = await
|
|
17482
|
+
const content = await fs38.readFile(path73.join(scanPath, file), "utf8");
|
|
17439
17483
|
const patched = splicedContent(content, snippet2);
|
|
17440
17484
|
if (patched !== null) return { file, content, patched };
|
|
17441
17485
|
if (fallback === null) fallback = { file, content };
|
|
@@ -17443,12 +17487,12 @@ async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
|
|
|
17443
17487
|
return { file: fallback.file, content: fallback.content, patched: null };
|
|
17444
17488
|
}
|
|
17445
17489
|
function extendLogPath() {
|
|
17446
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
17490
|
+
return process.env.NEAT_EXTEND_LOG ?? path73.join(os3.homedir(), ".neat", "extend-log.ndjson");
|
|
17447
17491
|
}
|
|
17448
17492
|
async function appendExtendLog(entry) {
|
|
17449
17493
|
const logPath = extendLogPath();
|
|
17450
|
-
await
|
|
17451
|
-
await
|
|
17494
|
+
await fs38.mkdir(path73.dirname(logPath), { recursive: true });
|
|
17495
|
+
await fs38.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
17452
17496
|
}
|
|
17453
17497
|
function splicedContent(fileContent, snippet2) {
|
|
17454
17498
|
if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
|
|
@@ -17506,7 +17550,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
17506
17550
|
}
|
|
17507
17551
|
async function describeProjectInstrumentation(ctx) {
|
|
17508
17552
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
17509
|
-
const envNeat = await fileExists2(
|
|
17553
|
+
const envNeat = await fileExists2(path73.join(ctx.scanPath, ".env.neat"));
|
|
17510
17554
|
const registryInstrPackages = new Set(
|
|
17511
17555
|
registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
17512
17556
|
);
|
|
@@ -17528,7 +17572,7 @@ async function applyExtension(ctx, args, options) {
|
|
|
17528
17572
|
);
|
|
17529
17573
|
}
|
|
17530
17574
|
for (const file of hookFiles) {
|
|
17531
|
-
const content = await
|
|
17575
|
+
const content = await fs38.readFile(path73.join(ctx.scanPath, file), "utf8");
|
|
17532
17576
|
if (content.includes(args.registration_snippet)) {
|
|
17533
17577
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
17534
17578
|
}
|
|
@@ -17540,18 +17584,18 @@ async function applyExtension(ctx, args, options) {
|
|
|
17540
17584
|
);
|
|
17541
17585
|
}
|
|
17542
17586
|
const primaryFile = primary.file;
|
|
17543
|
-
const primaryPath =
|
|
17587
|
+
const primaryPath = path73.join(ctx.scanPath, primaryFile);
|
|
17544
17588
|
const filesTouched = [];
|
|
17545
17589
|
const depsAdded = [];
|
|
17546
|
-
const pkgPath =
|
|
17590
|
+
const pkgPath = path73.join(ctx.scanPath, "package.json");
|
|
17547
17591
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
17548
17592
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
17549
17593
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
17550
|
-
await
|
|
17594
|
+
await fs38.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
17551
17595
|
filesTouched.push("package.json");
|
|
17552
17596
|
depsAdded.push(`${args.instrumentation_package}@${args.version}`);
|
|
17553
17597
|
}
|
|
17554
|
-
await
|
|
17598
|
+
await fs38.writeFile(primaryPath, primary.patched, "utf8");
|
|
17555
17599
|
filesTouched.push(primaryFile);
|
|
17556
17600
|
const cmd = await detectPackageManager(ctx.scanPath);
|
|
17557
17601
|
const installer = options?.runInstall ?? runPackageManagerInstall;
|
|
@@ -17582,7 +17626,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
17582
17626
|
};
|
|
17583
17627
|
}
|
|
17584
17628
|
for (const file of hookFiles) {
|
|
17585
|
-
const content = await
|
|
17629
|
+
const content = await fs38.readFile(path73.join(ctx.scanPath, file), "utf8");
|
|
17586
17630
|
if (content.includes(args.registration_snippet)) {
|
|
17587
17631
|
return {
|
|
17588
17632
|
library: args.library,
|
|
@@ -17617,28 +17661,28 @@ async function rollbackExtension(ctx, args) {
|
|
|
17617
17661
|
if (!await fileExists2(logPath)) {
|
|
17618
17662
|
return { undone: false, message: "no apply found for library" };
|
|
17619
17663
|
}
|
|
17620
|
-
const raw = await
|
|
17664
|
+
const raw = await fs38.readFile(logPath, "utf8");
|
|
17621
17665
|
const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
17622
17666
|
const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
|
|
17623
17667
|
if (!match) {
|
|
17624
17668
|
return { undone: false, message: "no apply found for library" };
|
|
17625
17669
|
}
|
|
17626
|
-
const pkgPath =
|
|
17670
|
+
const pkgPath = path73.join(ctx.scanPath, "package.json");
|
|
17627
17671
|
if (await fileExists2(pkgPath)) {
|
|
17628
17672
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
17629
17673
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
17630
17674
|
const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
|
|
17631
17675
|
pkg.dependencies = rest;
|
|
17632
|
-
await
|
|
17676
|
+
await fs38.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
17633
17677
|
}
|
|
17634
17678
|
}
|
|
17635
17679
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
17636
17680
|
for (const file of hookFiles) {
|
|
17637
|
-
const filePath =
|
|
17638
|
-
const content = await
|
|
17681
|
+
const filePath = path73.join(ctx.scanPath, file);
|
|
17682
|
+
const content = await fs38.readFile(filePath, "utf8");
|
|
17639
17683
|
if (content.includes(match.registration_snippet)) {
|
|
17640
17684
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
17641
|
-
await
|
|
17685
|
+
await fs38.writeFile(filePath, filtered, "utf8");
|
|
17642
17686
|
break;
|
|
17643
17687
|
}
|
|
17644
17688
|
}
|
|
@@ -18598,8 +18642,64 @@ async function askGraph(graph, question, opts = {}) {
|
|
|
18598
18642
|
}
|
|
18599
18643
|
|
|
18600
18644
|
// src/streaming.ts
|
|
18645
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
18646
|
+
import { z } from "zod";
|
|
18647
|
+
import { ErrorEventSchema, IncidentEventPayloadSchema, incidentKindOf as incidentKindOf3 } from "@neat.is/types";
|
|
18601
18648
|
var SSE_HEARTBEAT_MS = 3e4;
|
|
18602
18649
|
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
18650
|
+
var INCIDENT_TRIGGER_STREAM_SCOPE = "incident-only-v1";
|
|
18651
|
+
var IncidentTriggerSchema = IncidentEventPayloadSchema.extend({
|
|
18652
|
+
incidentId: z.string().regex(/^[A-Za-z0-9:_-]{1,100}$/),
|
|
18653
|
+
affectedNode: z.string().min(1).max(512),
|
|
18654
|
+
service: z.string().min(1).max(256),
|
|
18655
|
+
at: z.string().datetime()
|
|
18656
|
+
});
|
|
18657
|
+
var MAX_REPLAY_BYTES = 16 * 1024 * 1024;
|
|
18658
|
+
var MAX_REPLAY_EVENTS = 1e4;
|
|
18659
|
+
function isExtractionFailure(row) {
|
|
18660
|
+
const keys = Object.keys(row);
|
|
18661
|
+
return keys.every((key) => ["source", "producer", "file", "error", "stack", "ts"].includes(key)) && typeof row.producer === "string" && typeof row.file === "string" && typeof row.error === "string" && typeof row.ts === "string" && (row.stack === void 0 || typeof row.stack === "string") && !Number.isNaN(Date.parse(row.ts));
|
|
18662
|
+
}
|
|
18663
|
+
function loadIncidentReplay(errorsPath, cursor) {
|
|
18664
|
+
if (cursor !== void 0 && !/^[A-Za-z0-9:_-]{1,100}$/.test(cursor)) throw new Error("incident replay unavailable");
|
|
18665
|
+
try {
|
|
18666
|
+
if (statSync(errorsPath).size > MAX_REPLAY_BYTES) throw new Error("incident replay unavailable");
|
|
18667
|
+
const raw = readFileSync2(errorsPath, "utf8");
|
|
18668
|
+
if (raw && !raw.endsWith("\n")) throw new Error("incident replay unavailable");
|
|
18669
|
+
const seen = /* @__PURE__ */ new Map();
|
|
18670
|
+
const events = [];
|
|
18671
|
+
for (const line of raw.split("\n")) {
|
|
18672
|
+
if (!line) continue;
|
|
18673
|
+
if (events.length >= MAX_REPLAY_EVENTS) throw new Error("incident replay unavailable");
|
|
18674
|
+
const row = JSON.parse(line);
|
|
18675
|
+
if (row !== null && typeof row === "object" && !Array.isArray(row) && "source" in row && row.source === "extract") {
|
|
18676
|
+
if (!isExtractionFailure(row)) throw new Error("incident replay unavailable");
|
|
18677
|
+
continue;
|
|
18678
|
+
}
|
|
18679
|
+
const event = ErrorEventSchema.parse(row);
|
|
18680
|
+
const trigger = IncidentTriggerSchema.parse({
|
|
18681
|
+
incidentId: event.id,
|
|
18682
|
+
affectedNode: event.affectedNode,
|
|
18683
|
+
service: event.service,
|
|
18684
|
+
incidentKind: incidentKindOf3(event),
|
|
18685
|
+
at: event.timestamp
|
|
18686
|
+
});
|
|
18687
|
+
const prior = seen.get(trigger.incidentId), encoded = JSON.stringify(trigger);
|
|
18688
|
+
if (prior !== void 0) {
|
|
18689
|
+
if (prior !== encoded) throw new Error("incident replay unavailable");
|
|
18690
|
+
continue;
|
|
18691
|
+
}
|
|
18692
|
+
seen.set(trigger.incidentId, encoded);
|
|
18693
|
+
events.push(trigger);
|
|
18694
|
+
}
|
|
18695
|
+
if (cursor === void 0) return events;
|
|
18696
|
+
const at = events.findIndex((event) => event.incidentId === cursor);
|
|
18697
|
+
if (at < 0) throw new Error("incident replay unavailable");
|
|
18698
|
+
return events.slice(at + 1);
|
|
18699
|
+
} catch {
|
|
18700
|
+
throw new Error("incident replay unavailable");
|
|
18701
|
+
}
|
|
18702
|
+
}
|
|
18603
18703
|
function handleSse(req, reply, opts) {
|
|
18604
18704
|
const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
|
|
18605
18705
|
const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
|
|
@@ -18607,8 +18707,20 @@ function handleSse(req, reply, opts) {
|
|
|
18607
18707
|
reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
|
|
18608
18708
|
reply.raw.setHeader("Connection", "keep-alive");
|
|
18609
18709
|
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
18710
|
+
if (opts.incidentOnly) {
|
|
18711
|
+
reply.raw.setHeader("X-NEAT-Event-Scope", INCIDENT_TRIGGER_STREAM_SCOPE);
|
|
18712
|
+
reply.raw.setHeader("X-NEAT-Project", opts.project);
|
|
18713
|
+
if (opts.incidentReplay) reply.raw.setHeader("X-NEAT-Replay-Complete", "1");
|
|
18714
|
+
}
|
|
18610
18715
|
reply.raw.flushHeaders?.();
|
|
18611
18716
|
reply.raw.write(":open\n\n");
|
|
18717
|
+
for (const trigger of opts.incidentReplay ?? []) {
|
|
18718
|
+
reply.raw.write(`id: ${trigger.incidentId}
|
|
18719
|
+
event: incident
|
|
18720
|
+
data: ${JSON.stringify(trigger)}
|
|
18721
|
+
|
|
18722
|
+
`);
|
|
18723
|
+
}
|
|
18612
18724
|
let pending = 0;
|
|
18613
18725
|
let dropped = false;
|
|
18614
18726
|
const closeConnection = () => {
|
|
@@ -18636,6 +18748,17 @@ data: ${JSON.stringify({ reason: "backpressure" })}
|
|
|
18636
18748
|
};
|
|
18637
18749
|
const listener = (envelope) => {
|
|
18638
18750
|
if (envelope.project !== opts.project) return;
|
|
18751
|
+
if (opts.incidentOnly) {
|
|
18752
|
+
if (envelope.type !== "incident") return;
|
|
18753
|
+
const trigger = IncidentTriggerSchema.safeParse(envelope.payload);
|
|
18754
|
+
if (!trigger.success) return;
|
|
18755
|
+
writeFrame(`id: ${trigger.data.incidentId}
|
|
18756
|
+
event: incident
|
|
18757
|
+
data: ${JSON.stringify(trigger.data)}
|
|
18758
|
+
|
|
18759
|
+
`);
|
|
18760
|
+
return;
|
|
18761
|
+
}
|
|
18639
18762
|
writeFrame(`event: ${envelope.type}
|
|
18640
18763
|
data: ${JSON.stringify(envelope.payload)}
|
|
18641
18764
|
|
|
@@ -18652,10 +18775,671 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
18652
18775
|
reply.raw.on("error", closeConnection);
|
|
18653
18776
|
}
|
|
18654
18777
|
|
|
18655
|
-
// src/
|
|
18778
|
+
// src/pr-verdict.ts
|
|
18779
|
+
import { createHash } from "crypto";
|
|
18780
|
+
import { promises as fs39 } from "fs";
|
|
18781
|
+
import os5 from "os";
|
|
18782
|
+
import path75 from "path";
|
|
18783
|
+
|
|
18784
|
+
// ../action/src/graph.mjs
|
|
18785
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
18786
|
+
var MARKER = "<!-- neat-action:graph-impact -->";
|
|
18787
|
+
var shortId = (id) => String(id).split(":").slice(-1)[0];
|
|
18788
|
+
var fence = (s) => "`" + s + "`";
|
|
18789
|
+
function graphFromExport(raw) {
|
|
18790
|
+
const g = raw.graph ?? raw;
|
|
18791
|
+
const nodes = new Map((g.nodes ?? []).map((n) => [n.key, n.attributes ?? {}]));
|
|
18792
|
+
const edges = (g.edges ?? []).map((e) => ({
|
|
18793
|
+
key: e.key,
|
|
18794
|
+
source: e.source,
|
|
18795
|
+
target: e.target,
|
|
18796
|
+
...e.attributes ?? {}
|
|
18797
|
+
}));
|
|
18798
|
+
return { nodes, edges };
|
|
18799
|
+
}
|
|
18800
|
+
var namesByType = (entries, type) => entries.filter(([, a]) => a.type === type).map(([k, a]) => a.name ?? shortId(k));
|
|
18801
|
+
function diffGraphs(base, head) {
|
|
18802
|
+
const added = [...head.nodes].filter(([k]) => !base.nodes.has(k));
|
|
18803
|
+
const removed = [...base.nodes].filter(([k]) => !head.nodes.has(k));
|
|
18804
|
+
return {
|
|
18805
|
+
routesAdded: namesByType(added, "RouteNode"),
|
|
18806
|
+
routesRemoved: namesByType(removed, "RouteNode"),
|
|
18807
|
+
tablesAdded: namesByType(added, "InfraNode"),
|
|
18808
|
+
tablesRemoved: namesByType(removed, "InfraNode")
|
|
18809
|
+
};
|
|
18810
|
+
}
|
|
18811
|
+
function changedFileNodeIds(graph, changedPaths) {
|
|
18812
|
+
const ids = [];
|
|
18813
|
+
for (const [key, attrs] of graph.nodes) {
|
|
18814
|
+
if (attrs.type !== "FileNode" || !attrs.path) continue;
|
|
18815
|
+
if (changedPaths.some((c) => c === attrs.path || c.endsWith("/" + attrs.path))) ids.push(key);
|
|
18816
|
+
}
|
|
18817
|
+
return ids;
|
|
18818
|
+
}
|
|
18819
|
+
function changedNodeIds(base, head) {
|
|
18820
|
+
if (!base) return [...head.nodes.keys()];
|
|
18821
|
+
return [
|
|
18822
|
+
...[...head.nodes.keys()].filter((k) => !base.nodes.has(k)),
|
|
18823
|
+
...[...base.nodes.keys()].filter((k) => !head.nodes.has(k))
|
|
18824
|
+
];
|
|
18825
|
+
}
|
|
18826
|
+
function formatDivergences(data, changedIds) {
|
|
18827
|
+
const findings = Array.isArray(data) ? data : data?.divergences ?? data?.findings ?? data?.items ?? [];
|
|
18828
|
+
const changed = new Set(changedIds);
|
|
18829
|
+
return findings.filter((f) => changed.has(f.source) || changed.has(f.target) || changed.has(f.nodeId)).slice(0, 10).map((f) => {
|
|
18830
|
+
const kind = f.kind || f.type || "divergence";
|
|
18831
|
+
const s = shortId(f.source ?? f.nodeId ?? "");
|
|
18832
|
+
const t = f.target ? " \u2192 `" + shortId(f.target) + "`" : "";
|
|
18833
|
+
const why = f.reason || f.message || "";
|
|
18834
|
+
return `${kind}: \`${s}\`${t}${why ? " \u2014 " + why : ""}`;
|
|
18835
|
+
});
|
|
18836
|
+
}
|
|
18837
|
+
function observedBreakFrom(node, data) {
|
|
18838
|
+
const observed = Boolean(data?.observed);
|
|
18839
|
+
const dependentCount = Number(data?.inboundObservedCount ?? 0);
|
|
18840
|
+
const callCount = Array.isArray(data?.dependencies) ? data.dependencies.length : 0;
|
|
18841
|
+
if (!observed && dependentCount === 0 && callCount === 0) return null;
|
|
18842
|
+
const inboundVolume = typeof data.inboundVolume === "number" ? data.inboundVolume : void 0;
|
|
18843
|
+
const window = typeof data.window === "string" ? data.window : void 0;
|
|
18844
|
+
const inboundLastObserved = typeof data.inboundLastObserved === "string" ? data.inboundLastObserved : void 0;
|
|
18845
|
+
return {
|
|
18846
|
+
id: node.id,
|
|
18847
|
+
type: node.type,
|
|
18848
|
+
label: node.label,
|
|
18849
|
+
change: node.change,
|
|
18850
|
+
dependentCount,
|
|
18851
|
+
callCount,
|
|
18852
|
+
...inboundVolume !== void 0 ? { inboundVolume } : {},
|
|
18853
|
+
...window !== void 0 ? { window } : {},
|
|
18854
|
+
...inboundLastObserved !== void 0 ? { inboundLastObserved } : {}
|
|
18855
|
+
};
|
|
18856
|
+
}
|
|
18857
|
+
function blastRadius(graph, nodeId, maxDepth = 10) {
|
|
18858
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
18859
|
+
for (const e of graph.edges) {
|
|
18860
|
+
if (!inbound.has(e.target)) inbound.set(e.target, []);
|
|
18861
|
+
inbound.get(e.target).push({ source: e.source, type: e.type });
|
|
18862
|
+
}
|
|
18863
|
+
const seen = /* @__PURE__ */ new Set([nodeId]);
|
|
18864
|
+
const dependents = [];
|
|
18865
|
+
let frontier = [nodeId];
|
|
18866
|
+
for (let d = 0; d < maxDepth && frontier.length; d++) {
|
|
18867
|
+
const next = [];
|
|
18868
|
+
for (const n of frontier) {
|
|
18869
|
+
for (const { source } of inbound.get(n) ?? []) {
|
|
18870
|
+
if (seen.has(source)) continue;
|
|
18871
|
+
seen.add(source);
|
|
18872
|
+
dependents.push(source);
|
|
18873
|
+
next.push(source);
|
|
18874
|
+
}
|
|
18875
|
+
}
|
|
18876
|
+
frontier = next;
|
|
18877
|
+
}
|
|
18878
|
+
return dependents;
|
|
18879
|
+
}
|
|
18880
|
+
var stable = (obj) => JSON.stringify(obj, Object.keys(obj ?? {}).sort());
|
|
18881
|
+
function changedNodesForObservedScan(base, head, { limit = 25 } = {}) {
|
|
18882
|
+
const out = [];
|
|
18883
|
+
for (const [key, attrs] of base.nodes) {
|
|
18884
|
+
if (!head.nodes.has(key)) {
|
|
18885
|
+
out.push({ id: key, type: attrs.type, label: attrs.name ?? shortId(key), change: "removed" });
|
|
18886
|
+
}
|
|
18887
|
+
}
|
|
18888
|
+
for (const [key, attrs] of head.nodes) {
|
|
18889
|
+
const b = base.nodes.get(key);
|
|
18890
|
+
if (b && stable(b) !== stable(attrs)) {
|
|
18891
|
+
out.push({ id: key, type: attrs.type, label: attrs.name ?? shortId(key), change: "changed" });
|
|
18892
|
+
}
|
|
18893
|
+
}
|
|
18894
|
+
return out.slice(0, limit);
|
|
18895
|
+
}
|
|
18896
|
+
var plural = (n) => n === 1 ? "" : "s";
|
|
18897
|
+
function formatAge(iso) {
|
|
18898
|
+
const t = Date.parse(iso);
|
|
18899
|
+
if (!Number.isFinite(t)) return null;
|
|
18900
|
+
const ms = Date.now() - t;
|
|
18901
|
+
if (ms < 0) return "just now";
|
|
18902
|
+
const s = Math.round(ms / 1e3);
|
|
18903
|
+
if (s < 60) return `${s}s ago`;
|
|
18904
|
+
const m = Math.round(s / 60);
|
|
18905
|
+
if (m < 60) return `${m}m ago`;
|
|
18906
|
+
const h = Math.round(m / 60);
|
|
18907
|
+
if (h < 48) return `${h}h ago`;
|
|
18908
|
+
return `${Math.round(h / 24)}d ago`;
|
|
18909
|
+
}
|
|
18910
|
+
function windowPhrase(window) {
|
|
18911
|
+
if (window === "7d") return "in 7d";
|
|
18912
|
+
if (window === "lifetime") return "(lifetime)";
|
|
18913
|
+
return "";
|
|
18914
|
+
}
|
|
18915
|
+
function observedTraffic(b) {
|
|
18916
|
+
if (typeof b.inboundVolume !== "number") return null;
|
|
18917
|
+
const win = windowPhrase(b.window);
|
|
18918
|
+
const served = `served ${b.inboundVolume.toLocaleString("en-US")}\xD7${win ? " " + win : ""}`;
|
|
18919
|
+
const age = b.inboundLastObserved ? formatAge(b.inboundLastObserved) : null;
|
|
18920
|
+
return age ? `${served}, last seen ${age}` : served;
|
|
18921
|
+
}
|
|
18922
|
+
var FUSION_NOTE = "<sub>Fused view \u2014 the static graph (EXTRACTED) weighted against production traffic (OBSERVED) from the connected NEAT host. Observed-break checks ran live against that host.</sub>";
|
|
18923
|
+
var TONE_COPY = {
|
|
18924
|
+
loud: {
|
|
18925
|
+
green: "## \u2705 NEAT \u2014 all clear",
|
|
18926
|
+
yellow: "## \u26A0\uFE0F NEAT \u2014 a few minor problems",
|
|
18927
|
+
red: "## \u{1F6D1} NEAT \u2014 FUCK NO. Here are the problems:"
|
|
18928
|
+
},
|
|
18929
|
+
professional: {
|
|
18930
|
+
green: "## \u2705 NEAT \u2014 all clear",
|
|
18931
|
+
yellow: "## \u26A0\uFE0F NEAT \u2014 Minor issues to review",
|
|
18932
|
+
red: "## \u{1F6D1} NEAT \u2014 Critical: production will break"
|
|
18933
|
+
}
|
|
18934
|
+
};
|
|
18935
|
+
function breakHeadline(b) {
|
|
18936
|
+
const verb = b.change === "removed" ? "deletes" : "changes";
|
|
18937
|
+
const what = fence(b.label) + (b.type === "InfraNode" ? " (data store)" : "");
|
|
18938
|
+
const consequence = b.type === "RouteNode" ? "Live callers 404." : b.type === "InfraNode" ? "Live readers and writers fail." : "Live callers break.";
|
|
18939
|
+
const traffic = observedTraffic(b);
|
|
18940
|
+
if (traffic) {
|
|
18941
|
+
return `**\u{1F534} Production will break** \u2014 this PR ${verb} ${what}, ${traffic} (OBSERVED). ${consequence}`;
|
|
18942
|
+
}
|
|
18943
|
+
if (b.dependentCount > 0) {
|
|
18944
|
+
return `**\u{1F534} Production will break** \u2014 this PR ${verb} ${what}, ${b.dependentCount} observed dependent${plural(b.dependentCount)} (OBSERVED). ${consequence}`;
|
|
18945
|
+
}
|
|
18946
|
+
return `**\u{1F534} Production will break** \u2014 this PR ${verb} ${what}, seen live in production making ${b.callCount} observed call${plural(b.callCount)} (OBSERVED). Runtime behavior changes.`;
|
|
18947
|
+
}
|
|
18948
|
+
function breakDetail(b) {
|
|
18949
|
+
return "- " + fence(b.label) + ` \u2014 ${b.change} ${b.type ?? "node"}; ${b.dependentCount} observed inbound dependent${plural(b.dependentCount)}, ${b.callCount} observed outbound call${plural(b.callCount)}. Provenance: OBSERVED (live traffic on the connected host).`;
|
|
18950
|
+
}
|
|
18951
|
+
function renderVerdict({
|
|
18952
|
+
graph,
|
|
18953
|
+
delta,
|
|
18954
|
+
changedFiles = [],
|
|
18955
|
+
divergences = [],
|
|
18956
|
+
observedBreaks = [],
|
|
18957
|
+
tone = "loud",
|
|
18958
|
+
sniperDispatchUrl = "",
|
|
18959
|
+
graphDiffUrl = ""
|
|
18960
|
+
}) {
|
|
18961
|
+
const copy = TONE_COPY[tone] ?? TONE_COPY.loud;
|
|
18962
|
+
const brokenRoutes = new Set(
|
|
18963
|
+
observedBreaks.filter((b) => b.type === "RouteNode").map((b) => b.label)
|
|
18964
|
+
);
|
|
18965
|
+
const brokenTables = new Set(
|
|
18966
|
+
observedBreaks.filter((b) => b.type === "InfraNode").map((b) => b.label)
|
|
18967
|
+
);
|
|
18968
|
+
const staticConcerns = [];
|
|
18969
|
+
for (const r of delta.routesRemoved ?? []) {
|
|
18970
|
+
if (brokenRoutes.has(r)) continue;
|
|
18971
|
+
staticConcerns.push({
|
|
18972
|
+
headline: `**\u{1F7E0} Removed route** \u2014 ${fence(r)} no longer exists; confirm nothing in-repo still calls it.`,
|
|
18973
|
+
detail: `- Route ${fence(r)} removed. The connected host observed no production traffic on it \u2014 static-only concern.`
|
|
18974
|
+
});
|
|
18975
|
+
}
|
|
18976
|
+
for (const t of delta.tablesRemoved ?? []) {
|
|
18977
|
+
if (brokenTables.has(t)) continue;
|
|
18978
|
+
staticConcerns.push({
|
|
18979
|
+
headline: `**\u{1F7E0} Removed reference** \u2014 ${fence(t)} no longer exists; confirm nothing in-repo still reads it.`,
|
|
18980
|
+
detail: `- Table/infra ${fence(t)} removed. The connected host observed no production traffic on it \u2014 static-only concern.`
|
|
18981
|
+
});
|
|
18982
|
+
}
|
|
18983
|
+
const findings = [
|
|
18984
|
+
...observedBreaks.map((b) => ({ headline: breakHeadline(b), detail: breakDetail(b) })),
|
|
18985
|
+
...divergences.map((d) => ({
|
|
18986
|
+
headline: `**\u{1F7E0} Divergence** \u2014 ${d}`,
|
|
18987
|
+
detail: `- ${d}`
|
|
18988
|
+
})),
|
|
18989
|
+
...staticConcerns
|
|
18990
|
+
];
|
|
18991
|
+
const tier = observedBreaks.length > 0 ? "red" : findings.length > 0 ? "yellow" : "green";
|
|
18992
|
+
const actionLinks = [];
|
|
18993
|
+
if (sniperDispatchUrl)
|
|
18994
|
+
actionLinks.push(`**[\u26A1 Dispatch Sniper to fix these \u2192](${sniperDispatchUrl})**`);
|
|
18995
|
+
if (graphDiffUrl) actionLinks.push(`[Full graph diff \u2192](${graphDiffUrl})`);
|
|
18996
|
+
const L = [MARKER, ""];
|
|
18997
|
+
if (tier === "green") {
|
|
18998
|
+
L.push(
|
|
18999
|
+
copy.green,
|
|
19000
|
+
"",
|
|
19001
|
+
"Nothing this PR touches is observed in production, and it introduces no new divergences.",
|
|
19002
|
+
"",
|
|
19003
|
+
FUSION_NOTE
|
|
19004
|
+
);
|
|
19005
|
+
return { marker: MARKER, body: L.join("\n"), tier };
|
|
19006
|
+
}
|
|
19007
|
+
if (tier === "yellow") {
|
|
19008
|
+
L.push(
|
|
19009
|
+
copy.yellow,
|
|
19010
|
+
"",
|
|
19011
|
+
`${findings.length} minor problem${plural(findings.length)} \u2014 read ${findings.length === 1 ? "it" : "them"} below.`,
|
|
19012
|
+
"",
|
|
19013
|
+
"<details><summary>Read them \u2192</summary>",
|
|
19014
|
+
""
|
|
19015
|
+
);
|
|
19016
|
+
findings.forEach((f, i) => L.push(`${i + 1}. ${f.headline}`));
|
|
19017
|
+
L.push("", ...findings.map((f) => f.detail));
|
|
19018
|
+
L.push("", "</details>");
|
|
19019
|
+
if (actionLinks.length) L.push("", ...actionLinks);
|
|
19020
|
+
L.push("", FUSION_NOTE);
|
|
19021
|
+
return { marker: MARKER, body: L.join("\n"), tier };
|
|
19022
|
+
}
|
|
19023
|
+
L.push(copy.red, "");
|
|
19024
|
+
findings.forEach((f, i) => L.push(`${i + 1}. ${f.headline}`));
|
|
19025
|
+
L.push("", "<details><summary>Full detail \u2192</summary>", "");
|
|
19026
|
+
L.push(...findings.map((f) => f.detail));
|
|
19027
|
+
const anyDelta = (delta.routesAdded?.length ?? 0) + (delta.routesRemoved?.length ?? 0) + (delta.tablesAdded?.length ?? 0) + (delta.tablesRemoved?.length ?? 0);
|
|
19028
|
+
if (anyDelta) {
|
|
19029
|
+
L.push("", "**Graph delta**");
|
|
19030
|
+
if (delta.routesAdded?.length) L.push("- \u2795 routes: " + delta.routesAdded.map(fence).join(", "));
|
|
19031
|
+
if (delta.routesRemoved?.length)
|
|
19032
|
+
L.push("- \u2796 routes: " + delta.routesRemoved.map(fence).join(", "));
|
|
19033
|
+
if (delta.tablesAdded?.length) L.push("- \u2795 tables: " + delta.tablesAdded.map(fence).join(", "));
|
|
19034
|
+
if (delta.tablesRemoved?.length)
|
|
19035
|
+
L.push("- \u2796 tables: " + delta.tablesRemoved.map(fence).join(", "));
|
|
19036
|
+
}
|
|
19037
|
+
const filesInGraph = (changedFiles ?? []).filter((f) => graph?.nodes?.has(f));
|
|
19038
|
+
if (filesInGraph.length) {
|
|
19039
|
+
L.push("", "**Blast radius (in-repo)**");
|
|
19040
|
+
for (const f of filesInGraph) {
|
|
19041
|
+
const deps = blastRadius(graph, f).filter((d) => d.startsWith("file:")).map(shortId);
|
|
19042
|
+
L.push(
|
|
19043
|
+
"- " + fence(shortId(f)) + " \u2192 depended on by " + (deps.length ? deps.map(fence).join(", ") : "_nothing else in-repo_")
|
|
19044
|
+
);
|
|
19045
|
+
}
|
|
19046
|
+
}
|
|
19047
|
+
L.push("", "</details>");
|
|
19048
|
+
if (actionLinks.length) L.push("", ...actionLinks);
|
|
19049
|
+
L.push("", FUSION_NOTE);
|
|
19050
|
+
return { marker: MARKER, body: L.join("\n"), tier };
|
|
19051
|
+
}
|
|
19052
|
+
|
|
19053
|
+
// src/pr-verdict.ts
|
|
19054
|
+
import { PrVerdictBodySchema } from "@neat.is/types";
|
|
19055
|
+
|
|
19056
|
+
// src/connectors/hosted-repos.ts
|
|
18656
19057
|
import os4 from "os";
|
|
18657
|
-
import
|
|
18658
|
-
import {
|
|
19058
|
+
import path74 from "path";
|
|
19059
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
19060
|
+
var CP_REQUEST_TIMEOUT_MS = 1e4;
|
|
19061
|
+
var DEFAULT_SYNC_INTERVAL_MS = 5 * 6e4;
|
|
19062
|
+
var CLONE_TIMEOUT_MS = 5 * 6e4;
|
|
19063
|
+
function scrubToken(s) {
|
|
19064
|
+
return s.replace(/x-access-token:[^@\s]+@/g, "x-access-token:***@");
|
|
19065
|
+
}
|
|
19066
|
+
async function cpGet(pathname, deps) {
|
|
19067
|
+
const f = deps.fetchImpl ?? fetch;
|
|
19068
|
+
const res = await f(`${deps.cpUrl.replace(/\/+$/, "")}${pathname}`, {
|
|
19069
|
+
headers: { authorization: `Bearer ${deps.daemonToken}`, accept: "application/json" },
|
|
19070
|
+
signal: AbortSignal.timeout(CP_REQUEST_TIMEOUT_MS)
|
|
19071
|
+
});
|
|
19072
|
+
if (!res.ok) throw new Error(`control plane ${pathname} \u2192 HTTP ${res.status}`);
|
|
19073
|
+
return await res.json();
|
|
19074
|
+
}
|
|
19075
|
+
async function cpPostStatus(deps, owner, name, body) {
|
|
19076
|
+
const f = deps.fetchImpl ?? fetch;
|
|
19077
|
+
const url = `${deps.cpUrl.replace(/\/+$/, "")}/internal/projects/${deps.projectId}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/status`;
|
|
19078
|
+
const res = await f(url, {
|
|
19079
|
+
method: "POST",
|
|
19080
|
+
headers: { authorization: `Bearer ${deps.daemonToken}`, "content-type": "application/json" },
|
|
19081
|
+
body: JSON.stringify(body),
|
|
19082
|
+
signal: AbortSignal.timeout(CP_REQUEST_TIMEOUT_MS)
|
|
19083
|
+
});
|
|
19084
|
+
if (!res.ok && res.status !== 404) throw new Error(`control plane status POST \u2192 HTTP ${res.status}`);
|
|
19085
|
+
}
|
|
19086
|
+
var defaultCloneRepo = async (cloneUrl, ref, destDir) => {
|
|
19087
|
+
const [{ default: git }, httpMod, fs41] = await Promise.all([
|
|
19088
|
+
import("isomorphic-git"),
|
|
19089
|
+
import("isomorphic-git/http/node"),
|
|
19090
|
+
import("fs")
|
|
19091
|
+
]);
|
|
19092
|
+
const http = httpMod.default ?? httpMod;
|
|
19093
|
+
const parsed = new URL(cloneUrl);
|
|
19094
|
+
const password = parsed.password || parsed.username;
|
|
19095
|
+
const username = parsed.password ? parsed.username : "x-access-token";
|
|
19096
|
+
const cleanUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
19097
|
+
const clone = git.clone({
|
|
19098
|
+
fs: fs41,
|
|
19099
|
+
http,
|
|
19100
|
+
dir: destDir,
|
|
19101
|
+
url: cleanUrl,
|
|
19102
|
+
// Absent ref → isomorphic-git clones the remote's default branch.
|
|
19103
|
+
...ref ? { ref } : {},
|
|
19104
|
+
singleBranch: true,
|
|
19105
|
+
depth: 1,
|
|
19106
|
+
onAuth: () => ({ username, password })
|
|
19107
|
+
});
|
|
19108
|
+
let timer;
|
|
19109
|
+
const timeout = new Promise((_, reject) => {
|
|
19110
|
+
timer = setTimeout(() => reject(new Error(`clone timed out after ${CLONE_TIMEOUT_MS}ms`)), CLONE_TIMEOUT_MS);
|
|
19111
|
+
});
|
|
19112
|
+
try {
|
|
19113
|
+
await Promise.race([clone, timeout]);
|
|
19114
|
+
} finally {
|
|
19115
|
+
if (timer) clearTimeout(timer);
|
|
19116
|
+
}
|
|
19117
|
+
};
|
|
19118
|
+
var defaultCloneCommit = async (cloneUrl, sha, destDir) => {
|
|
19119
|
+
const [{ default: git }, httpMod, fs41] = await Promise.all([
|
|
19120
|
+
import("isomorphic-git"),
|
|
19121
|
+
import("isomorphic-git/http/node"),
|
|
19122
|
+
import("fs")
|
|
19123
|
+
]);
|
|
19124
|
+
const http = httpMod.default ?? httpMod;
|
|
19125
|
+
const parsed = new URL(cloneUrl);
|
|
19126
|
+
const password = parsed.password || parsed.username;
|
|
19127
|
+
const username = parsed.password ? parsed.username : "x-access-token";
|
|
19128
|
+
const cleanUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
19129
|
+
const clone = (async () => {
|
|
19130
|
+
await git.init({ fs: fs41, dir: destDir });
|
|
19131
|
+
await git.addRemote({ fs: fs41, dir: destDir, remote: "origin", url: cleanUrl });
|
|
19132
|
+
await git.fetch({
|
|
19133
|
+
fs: fs41,
|
|
19134
|
+
http,
|
|
19135
|
+
dir: destDir,
|
|
19136
|
+
remote: "origin",
|
|
19137
|
+
// A full SHA is fetched as itself; the remote doesn't have to advertise a ref that points at it.
|
|
19138
|
+
ref: sha,
|
|
19139
|
+
remoteRef: sha,
|
|
19140
|
+
singleBranch: true,
|
|
19141
|
+
depth: 1,
|
|
19142
|
+
tags: false,
|
|
19143
|
+
...password ? { onAuth: () => ({ username, password }) } : {}
|
|
19144
|
+
});
|
|
19145
|
+
await git.checkout({ fs: fs41, dir: destDir, ref: sha, force: true });
|
|
19146
|
+
})();
|
|
19147
|
+
clone.catch(() => {
|
|
19148
|
+
});
|
|
19149
|
+
let timer;
|
|
19150
|
+
const timeout = new Promise((_, reject) => {
|
|
19151
|
+
timer = setTimeout(() => reject(new Error(`clone timed out after ${CLONE_TIMEOUT_MS}ms`)), CLONE_TIMEOUT_MS);
|
|
19152
|
+
});
|
|
19153
|
+
try {
|
|
19154
|
+
await Promise.race([clone, timeout]);
|
|
19155
|
+
} finally {
|
|
19156
|
+
if (timer) clearTimeout(timer);
|
|
19157
|
+
}
|
|
19158
|
+
};
|
|
19159
|
+
function scrubCloneToken(s) {
|
|
19160
|
+
return scrubToken(s);
|
|
19161
|
+
}
|
|
19162
|
+
function needsSync(r) {
|
|
19163
|
+
return r.syncStatus === void 0 || r.syncStatus === "syncing";
|
|
19164
|
+
}
|
|
19165
|
+
async function syncOneRepo(r, input) {
|
|
19166
|
+
const { deps, graph } = input;
|
|
19167
|
+
const cloneRepo = input.cloneRepo ?? defaultCloneRepo;
|
|
19168
|
+
const extract = input.extract ?? extractFromDirectory;
|
|
19169
|
+
const label = `${r.owner}/${r.name}`;
|
|
19170
|
+
const tmpRoot = input.tmpRoot ?? os4.tmpdir();
|
|
19171
|
+
let dir;
|
|
19172
|
+
try {
|
|
19173
|
+
dir = await mkdtemp(path74.join(tmpRoot, "neat-repo-"));
|
|
19174
|
+
await cpPostStatus(deps, r.owner, r.name, { syncStatus: "syncing", detail: "cloning" }).catch(() => {
|
|
19175
|
+
});
|
|
19176
|
+
await cloneRepo(r.cloneUrl, r.defaultBranch, dir);
|
|
19177
|
+
const extracted = await extract(graph, dir, { source: label });
|
|
19178
|
+
const nodes = extracted?.nodesAdded ?? 0;
|
|
19179
|
+
const edges = extracted?.edgesAdded ?? 0;
|
|
19180
|
+
await cpPostStatus(deps, r.owner, r.name, {
|
|
19181
|
+
syncStatus: "synced",
|
|
19182
|
+
detail: `extracted ${nodes} node${nodes === 1 ? "" : "s"}, ${edges} edge${edges === 1 ? "" : "s"}`,
|
|
19183
|
+
lastSyncAt: new Date(input.now?.() ?? Date.now()).toISOString()
|
|
19184
|
+
});
|
|
19185
|
+
return true;
|
|
19186
|
+
} catch (err) {
|
|
19187
|
+
input.onError?.(label, err);
|
|
19188
|
+
await cpPostStatus(deps, r.owner, r.name, {
|
|
19189
|
+
syncStatus: "failed",
|
|
19190
|
+
detail: scrubToken(err.message).slice(0, 300)
|
|
19191
|
+
}).catch(() => {
|
|
19192
|
+
});
|
|
19193
|
+
return false;
|
|
19194
|
+
} finally {
|
|
19195
|
+
if (dir) await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
19196
|
+
});
|
|
19197
|
+
}
|
|
19198
|
+
}
|
|
19199
|
+
async function runRepoSyncPassCounted(input) {
|
|
19200
|
+
const counts = { listed: false, synced: 0, failed: 0 };
|
|
19201
|
+
let repos;
|
|
19202
|
+
try {
|
|
19203
|
+
repos = await cpGet(`/internal/projects/${input.deps.projectId}/repos`, input.deps);
|
|
19204
|
+
} catch (err) {
|
|
19205
|
+
input.onSkip?.("(all)", `control plane repo list unreadable \u2014 ${err.message}`);
|
|
19206
|
+
return counts;
|
|
19207
|
+
}
|
|
19208
|
+
if (!Array.isArray(repos)) return counts;
|
|
19209
|
+
counts.listed = true;
|
|
19210
|
+
for (const r of repos) {
|
|
19211
|
+
if (!input.forceResync && !needsSync(r)) continue;
|
|
19212
|
+
if (await syncOneRepo(r, input)) counts.synced++;
|
|
19213
|
+
else counts.failed++;
|
|
19214
|
+
}
|
|
19215
|
+
return counts;
|
|
19216
|
+
}
|
|
19217
|
+
async function startRepoSync(input) {
|
|
19218
|
+
const intervalMs = input.intervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
|
|
19219
|
+
const now = () => input.now?.() ?? Date.now();
|
|
19220
|
+
let stopped = false;
|
|
19221
|
+
let inFlight = null;
|
|
19222
|
+
let again = false;
|
|
19223
|
+
let lastPass;
|
|
19224
|
+
let bootResyncDone = false;
|
|
19225
|
+
const run = async () => {
|
|
19226
|
+
try {
|
|
19227
|
+
do {
|
|
19228
|
+
again = false;
|
|
19229
|
+
const startedAt = new Date(now()).toISOString();
|
|
19230
|
+
const counts = await runRepoSyncPassCounted({ ...input, forceResync: !bootResyncDone });
|
|
19231
|
+
if (counts.listed) bootResyncDone = true;
|
|
19232
|
+
lastPass = { ...counts, startedAt, finishedAt: new Date(now()).toISOString() };
|
|
19233
|
+
} while (again && !stopped);
|
|
19234
|
+
} finally {
|
|
19235
|
+
inFlight = null;
|
|
19236
|
+
}
|
|
19237
|
+
};
|
|
19238
|
+
const tick = () => {
|
|
19239
|
+
if (stopped || inFlight) return;
|
|
19240
|
+
inFlight = run();
|
|
19241
|
+
};
|
|
19242
|
+
tick();
|
|
19243
|
+
const timer = setInterval(tick, intervalMs);
|
|
19244
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
19245
|
+
const stop = () => {
|
|
19246
|
+
stopped = true;
|
|
19247
|
+
clearInterval(timer);
|
|
19248
|
+
};
|
|
19249
|
+
return Object.assign(stop, {
|
|
19250
|
+
syncNow: () => {
|
|
19251
|
+
const summary = lastPass ? { lastPass } : {};
|
|
19252
|
+
if (inFlight) {
|
|
19253
|
+
again = true;
|
|
19254
|
+
return { status: "queued", ...summary };
|
|
19255
|
+
}
|
|
19256
|
+
inFlight = run();
|
|
19257
|
+
return { status: "started", ...summary };
|
|
19258
|
+
},
|
|
19259
|
+
settled: async () => {
|
|
19260
|
+
while (inFlight) await inFlight;
|
|
19261
|
+
}
|
|
19262
|
+
});
|
|
19263
|
+
}
|
|
19264
|
+
async function maybeStartRepoSync(input) {
|
|
19265
|
+
const env = input.env ?? process.env;
|
|
19266
|
+
const cpUrl = env.NEAT_CP_URL;
|
|
19267
|
+
const projectId = env.NEAT_CP_PROJECT_ID;
|
|
19268
|
+
const daemonToken = env.NEAT_AUTH_TOKEN;
|
|
19269
|
+
if (!cpUrl || !projectId || !daemonToken) return () => {
|
|
19270
|
+
};
|
|
19271
|
+
return startRepoSync({
|
|
19272
|
+
deps: { cpUrl, projectId, daemonToken, ...input.fetchImpl ? { fetchImpl: input.fetchImpl } : {} },
|
|
19273
|
+
graph: input.graph,
|
|
19274
|
+
project: input.project,
|
|
19275
|
+
...input.cloneRepo ? { cloneRepo: input.cloneRepo } : {},
|
|
19276
|
+
...input.extract ? { extract: input.extract } : {},
|
|
19277
|
+
...input.intervalMs !== void 0 ? { intervalMs: input.intervalMs } : {},
|
|
19278
|
+
...input.tmpRoot ? { tmpRoot: input.tmpRoot } : {},
|
|
19279
|
+
...input.onSkip ? { onSkip: input.onSkip } : {},
|
|
19280
|
+
...input.onError ? { onError: input.onError } : {}
|
|
19281
|
+
});
|
|
19282
|
+
}
|
|
19283
|
+
|
|
19284
|
+
// src/pr-verdict.ts
|
|
19285
|
+
var DEFAULT_PR_VERDICT_TIMEOUT_MS = 24e4;
|
|
19286
|
+
var MAX_CHANGED_FILES = 3e3;
|
|
19287
|
+
var PrVerdictError = class extends Error {
|
|
19288
|
+
constructor(message, status, stage, details) {
|
|
19289
|
+
super(message);
|
|
19290
|
+
this.status = status;
|
|
19291
|
+
this.stage = stage;
|
|
19292
|
+
this.details = details;
|
|
19293
|
+
this.name = "PrVerdictError";
|
|
19294
|
+
}
|
|
19295
|
+
status;
|
|
19296
|
+
stage;
|
|
19297
|
+
details;
|
|
19298
|
+
};
|
|
19299
|
+
function parsePrVerdictRequest(body) {
|
|
19300
|
+
const parsed = PrVerdictBodySchema.safeParse(body);
|
|
19301
|
+
if (!parsed.success) {
|
|
19302
|
+
throw new PrVerdictError("invalid pr-verdict body", 400, void 0, parsed.error.issues);
|
|
19303
|
+
}
|
|
19304
|
+
const { changedFiles, tone, ...rest } = parsed.data;
|
|
19305
|
+
return {
|
|
19306
|
+
...rest,
|
|
19307
|
+
...changedFiles ? { changedFiles: changedFiles.slice(0, MAX_CHANGED_FILES) } : {},
|
|
19308
|
+
...tone ? { tone } : {}
|
|
19309
|
+
};
|
|
19310
|
+
}
|
|
19311
|
+
function createPrVerdictRunner(deps = {}) {
|
|
19312
|
+
let busy = false;
|
|
19313
|
+
return async (req, ctx) => {
|
|
19314
|
+
if (busy) throw new PrVerdictError("a verdict is already being computed on this daemon", 429);
|
|
19315
|
+
busy = true;
|
|
19316
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_PR_VERDICT_TIMEOUT_MS;
|
|
19317
|
+
const deadline = (deps.now ?? Date.now)() + timeoutMs;
|
|
19318
|
+
const work = computePrVerdict(req, ctx, deps, deadline).finally(() => {
|
|
19319
|
+
busy = false;
|
|
19320
|
+
});
|
|
19321
|
+
let timer;
|
|
19322
|
+
const timeout = new Promise((_, reject) => {
|
|
19323
|
+
timer = setTimeout(
|
|
19324
|
+
() => reject(new PrVerdictError(`verdict did not finish within ${timeoutMs}ms`, 504)),
|
|
19325
|
+
timeoutMs
|
|
19326
|
+
);
|
|
19327
|
+
});
|
|
19328
|
+
try {
|
|
19329
|
+
return await Promise.race([work, timeout]);
|
|
19330
|
+
} finally {
|
|
19331
|
+
if (timer) clearTimeout(timer);
|
|
19332
|
+
work.catch(() => {
|
|
19333
|
+
});
|
|
19334
|
+
}
|
|
19335
|
+
};
|
|
19336
|
+
}
|
|
19337
|
+
async function computePrVerdict(req, ctx, deps, deadline) {
|
|
19338
|
+
const now = deps.now ?? Date.now;
|
|
19339
|
+
const startedAt = now();
|
|
19340
|
+
const cloneCommit = deps.cloneCommit ?? defaultCloneCommit;
|
|
19341
|
+
const extract = deps.extract ?? extractFromDirectory;
|
|
19342
|
+
const root = await fs39.mkdtemp(path75.join(deps.tmpRoot ?? os5.tmpdir(), "neat-pr-"));
|
|
19343
|
+
const baseDir = path75.join(root, "base");
|
|
19344
|
+
const headDir = path75.join(root, "head");
|
|
19345
|
+
const stage = async (name, run) => {
|
|
19346
|
+
const left = deadline - now();
|
|
19347
|
+
if (left <= 0) throw new PrVerdictError(`verdict ran out of time before ${name}`, 504, name);
|
|
19348
|
+
const work = run();
|
|
19349
|
+
work.catch(() => {
|
|
19350
|
+
});
|
|
19351
|
+
let timer;
|
|
19352
|
+
const expired = new Promise((_, reject) => {
|
|
19353
|
+
timer = setTimeout(() => reject(new PrVerdictError(`verdict ran out of time during ${name}`, 504, name)), left);
|
|
19354
|
+
});
|
|
19355
|
+
try {
|
|
19356
|
+
return await Promise.race([work, expired]);
|
|
19357
|
+
} catch (err) {
|
|
19358
|
+
if (err instanceof PrVerdictError) throw err;
|
|
19359
|
+
const why = scrubCloneToken(err.message ?? String(err)).slice(0, 300);
|
|
19360
|
+
throw new PrVerdictError(`${name} failed \u2014 ${why}`, 422, name);
|
|
19361
|
+
} finally {
|
|
19362
|
+
if (timer) clearTimeout(timer);
|
|
19363
|
+
}
|
|
19364
|
+
};
|
|
19365
|
+
const extractOpts = { source: `${req.owner}/${req.name}`, announce: false };
|
|
19366
|
+
try {
|
|
19367
|
+
await fs39.mkdir(baseDir);
|
|
19368
|
+
await fs39.mkdir(headDir);
|
|
19369
|
+
await stage("clone-base", () => cloneCommit(req.cloneUrl, req.baseSha, baseDir));
|
|
19370
|
+
await stage("clone-head", () => cloneCommit(req.cloneUrl, req.headSha, headDir));
|
|
19371
|
+
const baseGraph = makeGraph();
|
|
19372
|
+
const headGraph = makeGraph();
|
|
19373
|
+
await stage("extract-base", () => extract(baseGraph, baseDir, extractOpts));
|
|
19374
|
+
await stage("extract-head", () => extract(headGraph, headDir, extractOpts));
|
|
19375
|
+
const base = graphFromExport(baseGraph.export());
|
|
19376
|
+
const head = graphFromExport(headGraph.export());
|
|
19377
|
+
const delta = diffGraphs(base, head);
|
|
19378
|
+
const changedPaths = req.changedFiles ?? await changedPathsBetween(baseDir, headDir);
|
|
19379
|
+
const changedFiles = changedFileNodeIds(head, changedPaths);
|
|
19380
|
+
const divergences = formatDivergences(
|
|
19381
|
+
computeDivergences(ctx.liveGraph, { incidents: ctx.incidents ?? [] }),
|
|
19382
|
+
changedNodeIds(base, head)
|
|
19383
|
+
);
|
|
19384
|
+
const observedBreaks = [];
|
|
19385
|
+
for (const node of changedNodesForObservedScan(base, head)) {
|
|
19386
|
+
if (!ctx.liveGraph.hasNode(node.id)) continue;
|
|
19387
|
+
const found = observedBreakFrom(node, getObservedDependencies(ctx.liveGraph, node.id));
|
|
19388
|
+
if (found) observedBreaks.push(found);
|
|
19389
|
+
}
|
|
19390
|
+
const { marker, body } = renderVerdict({
|
|
19391
|
+
graph: head,
|
|
19392
|
+
delta,
|
|
19393
|
+
changedFiles,
|
|
19394
|
+
divergences,
|
|
19395
|
+
observedBreaks,
|
|
19396
|
+
tone: req.tone ?? "loud"
|
|
19397
|
+
});
|
|
19398
|
+
return {
|
|
19399
|
+
marker,
|
|
19400
|
+
body,
|
|
19401
|
+
base: { sha: req.baseSha, nodes: baseGraph.order, edges: baseGraph.size },
|
|
19402
|
+
head: { sha: req.headSha, nodes: headGraph.order, edges: headGraph.size },
|
|
19403
|
+
changedFiles: changedFiles.length,
|
|
19404
|
+
observedBreaks: observedBreaks.length,
|
|
19405
|
+
divergences: divergences.length,
|
|
19406
|
+
durationMs: now() - startedAt
|
|
19407
|
+
};
|
|
19408
|
+
} finally {
|
|
19409
|
+
await fs39.rm(root, { recursive: true, force: true }).catch(() => {
|
|
19410
|
+
});
|
|
19411
|
+
}
|
|
19412
|
+
}
|
|
19413
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "neat-out"]);
|
|
19414
|
+
async function hashTree(dir) {
|
|
19415
|
+
const out = /* @__PURE__ */ new Map();
|
|
19416
|
+
const walk11 = async (rel) => {
|
|
19417
|
+
const entries = await fs39.readdir(path75.join(dir, rel), { withFileTypes: true }).catch(() => []);
|
|
19418
|
+
for (const entry of entries) {
|
|
19419
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
19420
|
+
if (entry.isDirectory()) {
|
|
19421
|
+
if (!SKIP_DIRS.has(entry.name)) await walk11(child);
|
|
19422
|
+
} else if (entry.isFile()) {
|
|
19423
|
+
const bytes = await fs39.readFile(path75.join(dir, child)).catch(() => null);
|
|
19424
|
+
if (bytes) out.set(child, createHash("sha1").update(bytes).digest("hex"));
|
|
19425
|
+
}
|
|
19426
|
+
}
|
|
19427
|
+
};
|
|
19428
|
+
await walk11("");
|
|
19429
|
+
return out;
|
|
19430
|
+
}
|
|
19431
|
+
async function changedPathsBetween(baseDir, headDir) {
|
|
19432
|
+
const [base, head] = await Promise.all([hashTree(baseDir), hashTree(headDir)]);
|
|
19433
|
+
const changed = [];
|
|
19434
|
+
for (const [file, hash] of head) if (base.get(file) !== hash) changed.push(file);
|
|
19435
|
+
for (const file of base.keys()) if (!head.has(file)) changed.push(file);
|
|
19436
|
+
return changed.sort().slice(0, MAX_CHANGED_FILES);
|
|
19437
|
+
}
|
|
19438
|
+
|
|
19439
|
+
// src/connectors-config.ts
|
|
19440
|
+
import os6 from "os";
|
|
19441
|
+
import path76 from "path";
|
|
19442
|
+
import { promises as fs40 } from "fs";
|
|
18659
19443
|
var CONNECTORS_CONFIG_VERSION = 1;
|
|
18660
19444
|
var EnvRefUnsetError = class extends Error {
|
|
18661
19445
|
ref;
|
|
@@ -18669,17 +19453,17 @@ var EnvRefUnsetError = class extends Error {
|
|
|
18669
19453
|
};
|
|
18670
19454
|
function neatHome2() {
|
|
18671
19455
|
const override = process.env.NEAT_HOME;
|
|
18672
|
-
if (override && override.length > 0) return
|
|
18673
|
-
return
|
|
19456
|
+
if (override && override.length > 0) return path76.resolve(override);
|
|
19457
|
+
return path76.join(os6.homedir(), ".neat");
|
|
18674
19458
|
}
|
|
18675
19459
|
function connectorsConfigPath(home = neatHome2()) {
|
|
18676
|
-
return
|
|
19460
|
+
return path76.join(home, "connectors.json");
|
|
18677
19461
|
}
|
|
18678
19462
|
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
18679
19463
|
async function warnIfModeLooserThan0600(file) {
|
|
18680
19464
|
if (process.platform === "win32") return;
|
|
18681
19465
|
try {
|
|
18682
|
-
const stat = await
|
|
19466
|
+
const stat = await fs40.stat(file);
|
|
18683
19467
|
if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
|
|
18684
19468
|
const mode = (stat.mode & 511).toString(8).padStart(3, "0");
|
|
18685
19469
|
console.warn(
|
|
@@ -18693,7 +19477,7 @@ async function readConnectorsConfig(home = neatHome2()) {
|
|
|
18693
19477
|
const file = connectorsConfigPath(home);
|
|
18694
19478
|
let raw;
|
|
18695
19479
|
try {
|
|
18696
|
-
raw = await
|
|
19480
|
+
raw = await fs40.readFile(file, "utf8");
|
|
18697
19481
|
} catch (err) {
|
|
18698
19482
|
if (err.code === "ENOENT") {
|
|
18699
19483
|
return { version: CONNECTORS_CONFIG_VERSION, connectors: [] };
|
|
@@ -18804,7 +19588,7 @@ function connectorMatchesProject(entry, project) {
|
|
|
18804
19588
|
var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
|
|
18805
19589
|
var CONNECTORS_LOCK_RETRY_MS = 50;
|
|
18806
19590
|
function connectorsConfigLockPath(home = neatHome2()) {
|
|
18807
|
-
return
|
|
19591
|
+
return path76.join(home, "connectors.json.lock");
|
|
18808
19592
|
}
|
|
18809
19593
|
function isEnvRef(value) {
|
|
18810
19594
|
return value.length > 1 && value.startsWith("$");
|
|
@@ -18817,9 +19601,9 @@ function redactCredentialRef(ref) {
|
|
|
18817
19601
|
return out;
|
|
18818
19602
|
}
|
|
18819
19603
|
async function writeConfigAtomically0600(file, contents) {
|
|
18820
|
-
await
|
|
19604
|
+
await fs40.mkdir(path76.dirname(file), { recursive: true });
|
|
18821
19605
|
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
18822
|
-
const fd = await
|
|
19606
|
+
const fd = await fs40.open(tmp, "w", 384);
|
|
18823
19607
|
try {
|
|
18824
19608
|
await fd.writeFile(contents, "utf8");
|
|
18825
19609
|
await fd.chmod(384);
|
|
@@ -18827,14 +19611,14 @@ async function writeConfigAtomically0600(file, contents) {
|
|
|
18827
19611
|
} finally {
|
|
18828
19612
|
await fd.close();
|
|
18829
19613
|
}
|
|
18830
|
-
await
|
|
19614
|
+
await fs40.rename(tmp, file);
|
|
18831
19615
|
}
|
|
18832
19616
|
async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
|
|
18833
19617
|
const deadline = Date.now() + timeoutMs;
|
|
18834
|
-
await
|
|
19618
|
+
await fs40.mkdir(path76.dirname(lockPath), { recursive: true });
|
|
18835
19619
|
for (; ; ) {
|
|
18836
19620
|
try {
|
|
18837
|
-
const fd = await
|
|
19621
|
+
const fd = await fs40.open(lockPath, "wx");
|
|
18838
19622
|
try {
|
|
18839
19623
|
await fd.writeFile(`${process.pid}
|
|
18840
19624
|
`, "utf8");
|
|
@@ -18854,7 +19638,7 @@ async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEO
|
|
|
18854
19638
|
}
|
|
18855
19639
|
}
|
|
18856
19640
|
async function releaseConnectorsLock(lockPath) {
|
|
18857
|
-
await
|
|
19641
|
+
await fs40.unlink(lockPath).catch(() => {
|
|
18858
19642
|
});
|
|
18859
19643
|
}
|
|
18860
19644
|
async function withConnectorsLock(home, fn) {
|
|
@@ -19594,10 +20378,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
|
|
|
19594
20378
|
// src/connectors/supabase/map.ts
|
|
19595
20379
|
var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
|
|
19596
20380
|
var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
|
|
19597
|
-
function targetFromRestPath(
|
|
19598
|
-
const rpcMatch = REST_RPC_PATH_RE.exec(
|
|
20381
|
+
function targetFromRestPath(path77) {
|
|
20382
|
+
const rpcMatch = REST_RPC_PATH_RE.exec(path77);
|
|
19599
20383
|
if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
|
|
19600
|
-
const tableMatch = REST_TABLE_PATH_RE.exec(
|
|
20384
|
+
const tableMatch = REST_TABLE_PATH_RE.exec(path77);
|
|
19601
20385
|
if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
|
|
19602
20386
|
return null;
|
|
19603
20387
|
}
|
|
@@ -20192,9 +20976,9 @@ function parseFirebaseTargetName(targetName) {
|
|
|
20192
20976
|
const secondSep = rest.indexOf(FIELD_SEP);
|
|
20193
20977
|
if (secondSep === -1) return null;
|
|
20194
20978
|
const method = rest.slice(0, secondSep);
|
|
20195
|
-
const
|
|
20196
|
-
if (!resourceName || !method || !
|
|
20197
|
-
return { resourceName, method, path:
|
|
20979
|
+
const path77 = rest.slice(secondSep + 1);
|
|
20980
|
+
if (!resourceName || !method || !path77) return null;
|
|
20981
|
+
return { resourceName, method, path: path77 };
|
|
20198
20982
|
}
|
|
20199
20983
|
function resourceNameFor(type, labels) {
|
|
20200
20984
|
if (!labels) return null;
|
|
@@ -20232,14 +21016,14 @@ function mapLogEntryToSignal(entry) {
|
|
|
20232
21016
|
if (!req) return null;
|
|
20233
21017
|
if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
|
|
20234
21018
|
const method = req.requestMethod.toUpperCase();
|
|
20235
|
-
const
|
|
20236
|
-
if (
|
|
21019
|
+
const path77 = pathFromRequestUrl(req.requestUrl);
|
|
21020
|
+
if (path77 === null) return null;
|
|
20237
21021
|
const timestamp = entry.timestamp;
|
|
20238
21022
|
if (typeof timestamp !== "string" || timestamp.length === 0) return null;
|
|
20239
21023
|
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
|
|
20240
21024
|
return {
|
|
20241
21025
|
targetKind: resourceType,
|
|
20242
|
-
targetName: packFirebaseTargetName({ resourceName, method, path:
|
|
21026
|
+
targetName: packFirebaseTargetName({ resourceName, method, path: path77 }),
|
|
20243
21027
|
callCount: 1,
|
|
20244
21028
|
errorCount: isError ? 1 : 0,
|
|
20245
21029
|
lastObservedIso: timestamp
|
|
@@ -20262,9 +21046,9 @@ import { NodeType as NodeType35 } from "@neat.is/types";
|
|
|
20262
21046
|
function normalizeName(name) {
|
|
20263
21047
|
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
20264
21048
|
}
|
|
20265
|
-
function inferServiceName(graph, resourceName, method,
|
|
21049
|
+
function inferServiceName(graph, resourceName, method, path77) {
|
|
20266
21050
|
const wantedMethod = method.toUpperCase();
|
|
20267
|
-
const wantedPath = normalizePathTemplate(
|
|
21051
|
+
const wantedPath = normalizePathTemplate(path77);
|
|
20268
21052
|
const services = [];
|
|
20269
21053
|
const owners = /* @__PURE__ */ new Set();
|
|
20270
21054
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -20472,7 +21256,7 @@ function mapEventToSignal(event) {
|
|
|
20472
21256
|
if (Number.isNaN(observedAt.getTime())) return null;
|
|
20473
21257
|
const statusCode = metadata?.statusCode;
|
|
20474
21258
|
const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
|
|
20475
|
-
const
|
|
21259
|
+
const path77 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
|
|
20476
21260
|
return {
|
|
20477
21261
|
targetKind: CLOUDFLARE_TARGET_KIND,
|
|
20478
21262
|
targetName: scriptName,
|
|
@@ -20480,7 +21264,7 @@ function mapEventToSignal(event) {
|
|
|
20480
21264
|
errorCount: isError ? 1 : 0,
|
|
20481
21265
|
lastObservedIso: observedAt.toISOString(),
|
|
20482
21266
|
method,
|
|
20483
|
-
...
|
|
21267
|
+
...path77 ? { path: path77 } : {},
|
|
20484
21268
|
...typeof statusCode === "number" ? { statusCode } : {},
|
|
20485
21269
|
...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
|
|
20486
21270
|
};
|
|
@@ -20526,8 +21310,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
|
|
|
20526
21310
|
});
|
|
20527
21311
|
return found;
|
|
20528
21312
|
}
|
|
20529
|
-
function findMatchingRouteNode(graph, serviceName, method,
|
|
20530
|
-
const normalizedPath = normalizePathTemplate(
|
|
21313
|
+
function findMatchingRouteNode(graph, serviceName, method, path77) {
|
|
21314
|
+
const normalizedPath = normalizePathTemplate(path77);
|
|
20531
21315
|
let found = null;
|
|
20532
21316
|
graph.forEachNode((id, attrs) => {
|
|
20533
21317
|
if (found) return;
|
|
@@ -20544,10 +21328,10 @@ function createCloudflareResolveTarget(config, graph) {
|
|
|
20544
21328
|
return (signal) => {
|
|
20545
21329
|
if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
|
|
20546
21330
|
const scriptName = signal.targetName;
|
|
20547
|
-
const { method, path:
|
|
21331
|
+
const { method, path: path77 } = signal;
|
|
20548
21332
|
const resolveRouteGrain = (serviceName, wholeFileId) => {
|
|
20549
|
-
if (!method || !
|
|
20550
|
-
return findMatchingRouteNode(graph, serviceName, method,
|
|
21333
|
+
if (!method || !path77) return wholeFileId;
|
|
21334
|
+
return findMatchingRouteNode(graph, serviceName, method, path77) ?? wholeFileId;
|
|
20551
21335
|
};
|
|
20552
21336
|
const mapping = config.workers?.[scriptName];
|
|
20553
21337
|
if (mapping) {
|
|
@@ -20877,9 +21661,9 @@ function parseCloudRunTargetName(targetName) {
|
|
|
20877
21661
|
const secondSep = rest.indexOf(FIELD_SEP2);
|
|
20878
21662
|
if (secondSep === -1) return null;
|
|
20879
21663
|
const method = rest.slice(0, secondSep);
|
|
20880
|
-
const
|
|
20881
|
-
if (!serviceName || !method || !
|
|
20882
|
-
return { serviceName, method, path:
|
|
21664
|
+
const path77 = rest.slice(secondSep + 1);
|
|
21665
|
+
if (!serviceName || !method || !path77) return null;
|
|
21666
|
+
return { serviceName, method, path: path77 };
|
|
20883
21667
|
}
|
|
20884
21668
|
|
|
20885
21669
|
// src/connectors/cloud-run/map.ts
|
|
@@ -20908,14 +21692,14 @@ function mapLogEntryToSignal2(entry) {
|
|
|
20908
21692
|
if (!req) return null;
|
|
20909
21693
|
if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
|
|
20910
21694
|
const method = req.requestMethod.toUpperCase();
|
|
20911
|
-
const
|
|
20912
|
-
if (
|
|
21695
|
+
const path77 = pathFromRequestUrl2(req.requestUrl);
|
|
21696
|
+
if (path77 === null) return null;
|
|
20913
21697
|
const timestamp = entry.timestamp;
|
|
20914
21698
|
if (typeof timestamp !== "string" || timestamp.length === 0) return null;
|
|
20915
21699
|
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
|
|
20916
21700
|
return {
|
|
20917
21701
|
targetKind: CLOUD_RUN_TARGET_KIND,
|
|
20918
|
-
targetName: packCloudRunTargetName({ serviceName, method, path:
|
|
21702
|
+
targetName: packCloudRunTargetName({ serviceName, method, path: path77 }),
|
|
20919
21703
|
callCount: 1,
|
|
20920
21704
|
errorCount: isError ? 1 : 0,
|
|
20921
21705
|
lastObservedIso: timestamp
|
|
@@ -20953,14 +21737,14 @@ function createCloudRunResolveTarget(graph, config) {
|
|
|
20953
21737
|
if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
|
|
20954
21738
|
const identity = parseCloudRunTargetName(signal.targetName);
|
|
20955
21739
|
if (!identity) return null;
|
|
20956
|
-
const { serviceName: gcpServiceName, method, path:
|
|
20957
|
-
const mappedService = config.serviceMap?.[gcpServiceName] ?? (config.inferServices ? inferServiceName(graph, gcpServiceName, method,
|
|
21740
|
+
const { serviceName: gcpServiceName, method, path: path77 } = identity;
|
|
21741
|
+
const mappedService = config.serviceMap?.[gcpServiceName] ?? (config.inferServices ? inferServiceName(graph, gcpServiceName, method, path77) ?? void 0 : void 0);
|
|
20958
21742
|
if (mappedService) {
|
|
20959
21743
|
const routeNodeId = findMatchingRouteNode2(
|
|
20960
21744
|
graph,
|
|
20961
21745
|
mappedService,
|
|
20962
21746
|
method,
|
|
20963
|
-
normalizePathTemplate(
|
|
21747
|
+
normalizePathTemplate(path77)
|
|
20964
21748
|
);
|
|
20965
21749
|
if (routeNodeId) {
|
|
20966
21750
|
return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: EdgeType32.CALLS };
|
|
@@ -21088,9 +21872,9 @@ function parseGcpLbTargetName(targetName) {
|
|
|
21088
21872
|
const secondSep = rest.indexOf(FIELD_SEP3);
|
|
21089
21873
|
if (secondSep === -1) return null;
|
|
21090
21874
|
const method = rest.slice(0, secondSep);
|
|
21091
|
-
const
|
|
21092
|
-
if (!backendServiceName || !method || !
|
|
21093
|
-
return { backendServiceName, method, path:
|
|
21875
|
+
const path77 = rest.slice(secondSep + 1);
|
|
21876
|
+
if (!backendServiceName || !method || !path77) return null;
|
|
21877
|
+
return { backendServiceName, method, path: path77 };
|
|
21094
21878
|
}
|
|
21095
21879
|
|
|
21096
21880
|
// src/connectors/gcp-lb/map.ts
|
|
@@ -21119,14 +21903,14 @@ function mapLogEntryToSignal3(entry) {
|
|
|
21119
21903
|
if (!req) return null;
|
|
21120
21904
|
if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
|
|
21121
21905
|
const method = req.requestMethod.toUpperCase();
|
|
21122
|
-
const
|
|
21123
|
-
if (
|
|
21906
|
+
const path77 = pathFromRequestUrl3(req.requestUrl);
|
|
21907
|
+
if (path77 === null) return null;
|
|
21124
21908
|
const timestamp = entry.timestamp;
|
|
21125
21909
|
if (typeof timestamp !== "string" || timestamp.length === 0) return null;
|
|
21126
21910
|
const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD5;
|
|
21127
21911
|
return {
|
|
21128
21912
|
targetKind: GCP_LB_TARGET_KIND,
|
|
21129
|
-
targetName: packGcpLbTargetName({ backendServiceName, method, path:
|
|
21913
|
+
targetName: packGcpLbTargetName({ backendServiceName, method, path: path77 }),
|
|
21130
21914
|
callCount: 1,
|
|
21131
21915
|
errorCount: isError ? 1 : 0,
|
|
21132
21916
|
lastObservedIso: timestamp
|
|
@@ -21164,14 +21948,14 @@ function createGcpLbResolveTarget(graph, config) {
|
|
|
21164
21948
|
if (signal.targetKind !== GCP_LB_TARGET_KIND) return null;
|
|
21165
21949
|
const identity = parseGcpLbTargetName(signal.targetName);
|
|
21166
21950
|
if (!identity) return null;
|
|
21167
|
-
const { backendServiceName, method, path:
|
|
21168
|
-
const mappedService = config.backendServiceMap?.[backendServiceName] ?? (config.inferServices ? inferServiceName(graph, backendServiceName, method,
|
|
21951
|
+
const { backendServiceName, method, path: path77 } = identity;
|
|
21952
|
+
const mappedService = config.backendServiceMap?.[backendServiceName] ?? (config.inferServices ? inferServiceName(graph, backendServiceName, method, path77) ?? void 0 : void 0);
|
|
21169
21953
|
if (mappedService) {
|
|
21170
21954
|
const routeNodeId = findMatchingRouteNode3(
|
|
21171
21955
|
graph,
|
|
21172
21956
|
mappedService,
|
|
21173
21957
|
method,
|
|
21174
|
-
normalizePathTemplate(
|
|
21958
|
+
normalizePathTemplate(path77)
|
|
21175
21959
|
);
|
|
21176
21960
|
if (routeNodeId) {
|
|
21177
21961
|
return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: EdgeType33.CALLS };
|
|
@@ -22618,6 +23402,35 @@ function registerRoutes(scope, ctx) {
|
|
|
22618
23402
|
if (!proj) return;
|
|
22619
23403
|
handleSse(req, reply, { project: proj.name });
|
|
22620
23404
|
});
|
|
23405
|
+
scope.get(
|
|
23406
|
+
"/incident-triggers",
|
|
23407
|
+
{ config: BEARER_DELEGATED },
|
|
23408
|
+
(req, reply) => {
|
|
23409
|
+
const token = ctx.incidentStreamToken;
|
|
23410
|
+
if (!token || Buffer.byteLength(token, "utf8") < 32 || token === ctx.generalAuthToken || !ctx.incidentReplayDurable) {
|
|
23411
|
+
return reply.code(503).send({ error: "incident trigger stream unavailable" });
|
|
23412
|
+
}
|
|
23413
|
+
const header = req.headers.authorization;
|
|
23414
|
+
const supplied = typeof header === "string" && header.startsWith("Bearer ") ? Buffer.from(header.slice("Bearer ".length), "utf8") : void 0;
|
|
23415
|
+
const expected = Buffer.from(token, "utf8");
|
|
23416
|
+
if (!supplied || supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) {
|
|
23417
|
+
return reply.code(401).send({ error: "unauthorized" });
|
|
23418
|
+
}
|
|
23419
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
23420
|
+
if (!proj) return;
|
|
23421
|
+
const errorsPath = errorsPathFor(proj);
|
|
23422
|
+
if (!errorsPath) return reply.code(503).send({ error: "incident replay unavailable" });
|
|
23423
|
+
let replay;
|
|
23424
|
+
try {
|
|
23425
|
+
const cursor = req.headers["last-event-id"];
|
|
23426
|
+
if (Array.isArray(cursor)) throw new Error("invalid cursor");
|
|
23427
|
+
replay = loadIncidentReplay(errorsPath, cursor);
|
|
23428
|
+
} catch {
|
|
23429
|
+
return reply.code(409).send({ error: "incident replay unavailable" });
|
|
23430
|
+
}
|
|
23431
|
+
handleSse(req, reply, { project: proj.name, incidentOnly: true, incidentReplay: replay });
|
|
23432
|
+
}
|
|
23433
|
+
);
|
|
22621
23434
|
if (ctx.scope === "project") {
|
|
22622
23435
|
scope.get("/health", async (req, reply) => {
|
|
22623
23436
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -22903,7 +23716,7 @@ function registerRoutes(scope, ctx) {
|
|
|
22903
23716
|
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
22904
23717
|
let errorEvent;
|
|
22905
23718
|
if (req.query.errorId) {
|
|
22906
|
-
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
23719
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId) ?? (epath ? await readErrorEventById(epath, req.query.errorId) : void 0);
|
|
22907
23720
|
if (!errorEvent) {
|
|
22908
23721
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
22909
23722
|
}
|
|
@@ -23089,6 +23902,26 @@ function registerRoutes(scope, ctx) {
|
|
|
23089
23902
|
}
|
|
23090
23903
|
return reply.code(202).send({ project: proj.name, ...syncNow() });
|
|
23091
23904
|
});
|
|
23905
|
+
scope.post("/pr-verdict", async (req, reply) => {
|
|
23906
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
23907
|
+
if (!proj) return;
|
|
23908
|
+
try {
|
|
23909
|
+
const request = parsePrVerdictRequest(req.body);
|
|
23910
|
+
const epath = errorsPathFor(proj);
|
|
23911
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
23912
|
+
const verdict = await ctx.prVerdict(request, { liveGraph: proj.graph, incidents });
|
|
23913
|
+
return { project: proj.name, ...verdict };
|
|
23914
|
+
} catch (err) {
|
|
23915
|
+
if (!(err instanceof PrVerdictError)) throw err;
|
|
23916
|
+
if (err.status === 429) void reply.header("retry-after", "30");
|
|
23917
|
+
return reply.code(err.status).send({
|
|
23918
|
+
error: err.message,
|
|
23919
|
+
project: proj.name,
|
|
23920
|
+
...err.stage ? { stage: err.stage } : {},
|
|
23921
|
+
...err.details !== void 0 ? { details: err.details } : {}
|
|
23922
|
+
});
|
|
23923
|
+
}
|
|
23924
|
+
});
|
|
23092
23925
|
scope.post("/graph/scan", async (req, reply) => {
|
|
23093
23926
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
23094
23927
|
if (!proj) return;
|
|
@@ -23376,6 +24209,8 @@ async function buildApi(opts) {
|
|
|
23376
24209
|
const authToken = opts.authToken ?? env.authToken;
|
|
23377
24210
|
const trustProxy = opts.trustProxy ?? env.trustProxy;
|
|
23378
24211
|
const publicRead = opts.publicRead ?? env.publicRead;
|
|
24212
|
+
const incidentStreamToken = opts.incidentStreamToken ?? process.env.NEAT_INCIDENT_STREAM_TOKEN;
|
|
24213
|
+
const incidentReplayDurable = opts.incidentReplayDurable ?? (process.env.NEAT_INCIDENT_REPLAY_DURABLE === "1" && Buffer.byteLength(process.env.NEAT_INCIDENT_DURABLE_TOKEN ?? "", "utf8") >= 32);
|
|
23379
24214
|
mountBearerAuth(app, {
|
|
23380
24215
|
token: authToken,
|
|
23381
24216
|
trustProxy,
|
|
@@ -23411,6 +24246,9 @@ async function buildApi(opts) {
|
|
|
23411
24246
|
const routeCtx = {
|
|
23412
24247
|
registry,
|
|
23413
24248
|
startedAt,
|
|
24249
|
+
incidentStreamToken,
|
|
24250
|
+
incidentReplayDurable,
|
|
24251
|
+
generalAuthToken: authToken,
|
|
23414
24252
|
scope: "root",
|
|
23415
24253
|
errorsPathFor,
|
|
23416
24254
|
staleEventsPathFor,
|
|
@@ -23419,6 +24257,7 @@ async function buildApi(opts) {
|
|
|
23419
24257
|
singleProject: opts.singleProject?.name,
|
|
23420
24258
|
connectorsHome: opts.connectorsHome,
|
|
23421
24259
|
repoSync: opts.repoSync,
|
|
24260
|
+
prVerdict: opts.prVerdict ?? createPrVerdictRunner(),
|
|
23422
24261
|
runPoll: opts.runPoll ?? runConnectorPoll
|
|
23423
24262
|
};
|
|
23424
24263
|
app.get("/health", async () => {
|
|
@@ -23596,6 +24435,7 @@ export {
|
|
|
23596
24435
|
touchLastSeen,
|
|
23597
24436
|
removeProject,
|
|
23598
24437
|
pruneRegistry,
|
|
24438
|
+
maybeStartRepoSync,
|
|
23599
24439
|
readConnectorsConfig,
|
|
23600
24440
|
resolveCredential,
|
|
23601
24441
|
isEnvRef,
|
|
@@ -23617,4 +24457,4 @@ export {
|
|
|
23617
24457
|
deprovisionConnector,
|
|
23618
24458
|
buildApi
|
|
23619
24459
|
};
|
|
23620
|
-
//# sourceMappingURL=chunk-
|
|
24460
|
+
//# sourceMappingURL=chunk-KTJK2PAH.js.map
|