@launchsecure/launch-kit 0.0.13 → 0.0.15
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/chart-client/assets/index-BpQPtTuo.js +441 -0
- package/dist/chart-client/assets/index-CbZ13AXL.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BCYw64M7.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +2069 -499
- package/dist/server/cli.js +5151 -3319
- package/dist/server/graph-mcp-entry.js +2600 -601
- package/package.json +1 -1
- package/dist/chart-client/assets/index-DIPKWwEJ.js +0 -404
- package/dist/chart-client/assets/index-DjnSR-Hf.css +0 -1
- package/dist/client/assets/index-BIpeUMYR.css +0 -32
- /package/dist/client/assets/{index-qzK1AD2G.js → index-3ENenBk-.js} +0 -0
|
@@ -155,7 +155,52 @@ var init_config = __esm({
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// src/server/graph/core/resolve-paths.ts
|
|
159
|
+
function detectDbDir(rootDir, config) {
|
|
160
|
+
if (config.paths?.dbDir) return (0, import_node_path3.join)(rootDir, config.paths.dbDir);
|
|
161
|
+
const prismaDir = (0, import_node_path3.join)(rootDir, "prisma");
|
|
162
|
+
if ((0, import_node_fs3.existsSync)(prismaDir)) return prismaDir;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function resolveProjectPaths(rootDir, config) {
|
|
166
|
+
const dbDir = detectDbDir(rootDir, config);
|
|
167
|
+
if (config.paths?.appDir) {
|
|
168
|
+
const appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
|
|
169
|
+
const srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
|
|
170
|
+
return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api"), dbDir };
|
|
171
|
+
}
|
|
172
|
+
const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
|
|
173
|
+
if ((0, import_node_fs3.existsSync)(srcApp)) {
|
|
174
|
+
return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api"), dbDir };
|
|
175
|
+
}
|
|
176
|
+
const rootApp = (0, import_node_path3.join)(rootDir, "app");
|
|
177
|
+
if ((0, import_node_fs3.existsSync)(rootApp)) {
|
|
178
|
+
return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api"), dbDir };
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
var import_node_fs3, import_node_path3;
|
|
183
|
+
var init_resolve_paths = __esm({
|
|
184
|
+
"src/server/graph/core/resolve-paths.ts"() {
|
|
185
|
+
"use strict";
|
|
186
|
+
import_node_fs3 = require("node:fs");
|
|
187
|
+
import_node_path3 = require("node:path");
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
158
191
|
// src/server/graph/core/ts-extractor.ts
|
|
192
|
+
var ts_extractor_exports = {};
|
|
193
|
+
__export(ts_extractor_exports, {
|
|
194
|
+
classifyFile: () => classifyFile,
|
|
195
|
+
createQuery: () => createQuery,
|
|
196
|
+
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
197
|
+
extractDbCallsTS: () => extractDbCallsTS,
|
|
198
|
+
extractDeep: () => extractDeep,
|
|
199
|
+
initTreeSitter: () => initTreeSitter,
|
|
200
|
+
parseCodeTS: () => parseCodeTS,
|
|
201
|
+
parseFileTS: () => parseFileTS,
|
|
202
|
+
setExtractorConfig: () => setExtractorConfig
|
|
203
|
+
});
|
|
159
204
|
async function initTreeSitter() {
|
|
160
205
|
if (initialized) return;
|
|
161
206
|
if (initPromise) return initPromise;
|
|
@@ -179,17 +224,25 @@ function getQuery(name) {
|
|
|
179
224
|
ensureInit();
|
|
180
225
|
const cached = queryCache.get(name);
|
|
181
226
|
if (cached) return cached;
|
|
182
|
-
const scmPath = (0,
|
|
183
|
-
const scm = (0,
|
|
227
|
+
const scmPath = (0, import_node_path4.join)(queriesDir, `${name}.scm`);
|
|
228
|
+
const scm = (0, import_node_fs4.readFileSync)(scmPath, "utf-8");
|
|
184
229
|
const query = tsxLanguage.query(scm);
|
|
185
230
|
queryCache.set(name, query);
|
|
186
231
|
return query;
|
|
187
232
|
}
|
|
188
233
|
function parseSource(absPath) {
|
|
189
234
|
ensureInit();
|
|
190
|
-
const content = (0,
|
|
235
|
+
const content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
|
|
191
236
|
return parserInstance.parse(content);
|
|
192
237
|
}
|
|
238
|
+
function parseCodeTS(code) {
|
|
239
|
+
ensureInit();
|
|
240
|
+
return parserInstance.parse(code);
|
|
241
|
+
}
|
|
242
|
+
function createQuery(pattern) {
|
|
243
|
+
ensureInit();
|
|
244
|
+
return tsxLanguage.query(pattern);
|
|
245
|
+
}
|
|
193
246
|
function setExtractorConfig(config) {
|
|
194
247
|
extraDbIdentifiers = config.dbIdentifiers ?? [];
|
|
195
248
|
extraMutationMethods = config.mutationMethods ?? [];
|
|
@@ -644,17 +697,17 @@ function extractDeep(absPath) {
|
|
|
644
697
|
}
|
|
645
698
|
return { elements, stateVars, conditions, variables, responses, params };
|
|
646
699
|
}
|
|
647
|
-
var
|
|
700
|
+
var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
|
|
648
701
|
var init_ts_extractor = __esm({
|
|
649
702
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
650
703
|
"use strict";
|
|
651
|
-
|
|
652
|
-
|
|
704
|
+
import_node_fs4 = require("node:fs");
|
|
705
|
+
import_node_path4 = require("node:path");
|
|
653
706
|
initialized = false;
|
|
654
707
|
queriesDir = (() => {
|
|
655
|
-
const srcPath = (0,
|
|
708
|
+
const srcPath = (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "..", "queries");
|
|
656
709
|
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
657
|
-
return (0,
|
|
710
|
+
return (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "graph", "queries");
|
|
658
711
|
})();
|
|
659
712
|
queryCache = /* @__PURE__ */ new Map();
|
|
660
713
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
@@ -674,15 +727,15 @@ var init_ts_extractor = __esm({
|
|
|
674
727
|
}
|
|
675
728
|
});
|
|
676
729
|
|
|
677
|
-
// src/server/graph/parsers/
|
|
730
|
+
// src/server/graph/parsers/ts/typescript-project.ts
|
|
678
731
|
function walk(dir, exts) {
|
|
679
732
|
const results = [];
|
|
680
|
-
if (!(0,
|
|
681
|
-
for (const entry of (0,
|
|
682
|
-
const full = (0,
|
|
733
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
734
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
735
|
+
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
683
736
|
if (entry.isDirectory()) {
|
|
684
737
|
results.push(...walk(full, exts));
|
|
685
|
-
} else if (exts.includes((0,
|
|
738
|
+
} else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
|
|
686
739
|
results.push(full);
|
|
687
740
|
}
|
|
688
741
|
}
|
|
@@ -690,33 +743,33 @@ function walk(dir, exts) {
|
|
|
690
743
|
}
|
|
691
744
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
692
745
|
const results = [];
|
|
693
|
-
if (!(0,
|
|
694
|
-
for (const entry of (0,
|
|
746
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
747
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
695
748
|
if (entry.isDirectory()) {
|
|
696
749
|
if (ignoreDirs.has(entry.name)) continue;
|
|
697
|
-
results.push(...walkWithIgnore((0,
|
|
698
|
-
} else if (exts.includes((0,
|
|
699
|
-
results.push((0,
|
|
750
|
+
results.push(...walkWithIgnore((0, import_node_path5.join)(dir, entry.name), exts, ignoreDirs));
|
|
751
|
+
} else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
|
|
752
|
+
results.push((0, import_node_path5.join)(dir, entry.name));
|
|
700
753
|
}
|
|
701
754
|
}
|
|
702
755
|
return results;
|
|
703
756
|
}
|
|
704
757
|
function toNodeId(srcDir, absPath) {
|
|
705
|
-
return (0,
|
|
758
|
+
return (0, import_node_path5.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
706
759
|
}
|
|
707
760
|
function resolveImport(srcDir, specifier) {
|
|
708
761
|
if (!specifier.startsWith("@/")) return null;
|
|
709
762
|
const rel = specifier.slice(2);
|
|
710
|
-
const base = (0,
|
|
711
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
712
|
-
if ((0,
|
|
763
|
+
const base = (0, import_node_path5.join)(srcDir, rel);
|
|
764
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path5.join)(base, "index.ts"), (0, import_node_path5.join)(base, "index.tsx")]) {
|
|
765
|
+
if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
|
|
713
766
|
}
|
|
714
767
|
return null;
|
|
715
768
|
}
|
|
716
769
|
function resolveRelativeImport(fromFile, specifier) {
|
|
717
|
-
const base = (0,
|
|
718
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
719
|
-
if ((0,
|
|
770
|
+
const base = (0, import_node_path5.join)((0, import_node_path5.dirname)(fromFile), specifier);
|
|
771
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path5.join)(base, "index.ts"), (0, import_node_path5.join)(base, "index.tsx")]) {
|
|
772
|
+
if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
|
|
720
773
|
}
|
|
721
774
|
return null;
|
|
722
775
|
}
|
|
@@ -737,7 +790,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
737
790
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
738
791
|
if (!resolved) continue;
|
|
739
792
|
if (re.isWildcard) {
|
|
740
|
-
const targetBn = (0,
|
|
793
|
+
const targetBn = (0, import_node_path5.basename)(resolved);
|
|
741
794
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
742
795
|
if (targetIsBarrel) {
|
|
743
796
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -764,12 +817,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
764
817
|
const barrels = /* @__PURE__ */ new Map();
|
|
765
818
|
const memo = /* @__PURE__ */ new Map();
|
|
766
819
|
for (const [absPath, parsed] of parsedByPath) {
|
|
767
|
-
const bn = (0,
|
|
820
|
+
const bn = (0, import_node_path5.basename)(absPath);
|
|
768
821
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
769
822
|
if (parsed.reExports.length === 0) continue;
|
|
770
823
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
771
824
|
if (map.size > 0) {
|
|
772
|
-
const barrelId = (0,
|
|
825
|
+
const barrelId = (0, import_node_path5.relative)(srcDir, (0, import_node_path5.dirname)(absPath)).replace(/\\/g, "/");
|
|
773
826
|
barrels.set(barrelId, map);
|
|
774
827
|
}
|
|
775
828
|
}
|
|
@@ -790,7 +843,18 @@ function extractRoute(id) {
|
|
|
790
843
|
return route || "/";
|
|
791
844
|
}
|
|
792
845
|
function nameFromFilename(absPath) {
|
|
793
|
-
return (0,
|
|
846
|
+
return (0, import_node_path5.basename)(absPath, (0, import_node_path5.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
847
|
+
}
|
|
848
|
+
function filePathToApiRoute(apiDir, absPath) {
|
|
849
|
+
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
850
|
+
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
851
|
+
route = route.replace(/\/+/g, "/");
|
|
852
|
+
if (route === "/") return "/api";
|
|
853
|
+
return "/api" + route;
|
|
854
|
+
}
|
|
855
|
+
function camelToPascal(s) {
|
|
856
|
+
if (!s) return s;
|
|
857
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
794
858
|
}
|
|
795
859
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
796
860
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -871,7 +935,7 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
871
935
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
872
936
|
return null;
|
|
873
937
|
}
|
|
874
|
-
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet,
|
|
938
|
+
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
|
|
875
939
|
const edges = [];
|
|
876
940
|
const flagged = [];
|
|
877
941
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -883,7 +947,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
883
947
|
if (label) edge.label = label;
|
|
884
948
|
edges.push(edge);
|
|
885
949
|
}
|
|
886
|
-
function edgeTypeFor(
|
|
950
|
+
function edgeTypeFor(isTypeOnlyImport, importedNames) {
|
|
887
951
|
if (isTypeOnlyImport) return "imports";
|
|
888
952
|
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
889
953
|
if (anyRendered) return "renders";
|
|
@@ -908,14 +972,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
908
972
|
}
|
|
909
973
|
for (const [targetId, targetNames] of byTarget) {
|
|
910
974
|
const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
|
|
911
|
-
addEdge(targetId, edgeTypeFor(
|
|
975
|
+
addEdge(targetId, edgeTypeFor(allType, targetNames));
|
|
912
976
|
}
|
|
913
977
|
} else {
|
|
914
978
|
const resolved = resolveImport(srcDir, specifier);
|
|
915
979
|
if (resolved) {
|
|
916
980
|
const targetId = toNodeId(srcDir, resolved);
|
|
917
981
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
918
|
-
addEdge(targetId, edgeTypeFor(
|
|
982
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
919
983
|
}
|
|
920
984
|
}
|
|
921
985
|
}
|
|
@@ -924,7 +988,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
924
988
|
if (resolved) {
|
|
925
989
|
const targetId = toNodeId(srcDir, resolved);
|
|
926
990
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
927
|
-
addEdge(targetId, edgeTypeFor(
|
|
991
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
928
992
|
}
|
|
929
993
|
}
|
|
930
994
|
}
|
|
@@ -966,74 +1030,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
966
1030
|
}
|
|
967
1031
|
return { edges, flagged };
|
|
968
1032
|
}
|
|
1033
|
+
function hasNextConfig(rootDir) {
|
|
1034
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.ts")) || (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.js")) || (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.mjs"));
|
|
1035
|
+
}
|
|
969
1036
|
function detect(rootDir) {
|
|
970
|
-
|
|
1037
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
1038
|
+
return paths !== null && hasNextConfig(rootDir);
|
|
971
1039
|
}
|
|
972
1040
|
function generate(rootDir) {
|
|
973
|
-
const
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
);
|
|
981
|
-
const
|
|
982
|
-
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
1041
|
+
const config = loadConfig(rootDir);
|
|
1042
|
+
const paths = resolveProjectPaths(rootDir, config);
|
|
1043
|
+
const srcDir = paths.srcDir;
|
|
1044
|
+
const apiDir = paths.apiDir;
|
|
1045
|
+
const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
|
|
1046
|
+
const clientFiles = walk((0, import_node_path5.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
1047
|
+
const serverFiles = walk((0, import_node_path5.join)(srcDir, "server"), [".ts", ".tsx"]);
|
|
1048
|
+
const libFiles = walk((0, import_node_path5.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
1049
|
+
const configFiles = walk((0, import_node_path5.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
983
1050
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
984
1051
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
985
1052
|
for (const absPath of allDiscovered) {
|
|
986
1053
|
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
987
1054
|
}
|
|
988
1055
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
989
|
-
const
|
|
990
|
-
const
|
|
1056
|
+
const uiNodes = [];
|
|
1057
|
+
const apiNodes = [];
|
|
991
1058
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
992
|
-
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
993
1059
|
const routeToNodeId = /* @__PURE__ */ new Map();
|
|
1060
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path5.basename)(f).startsWith("index."));
|
|
994
1061
|
for (const absPath of fileSet) {
|
|
995
1062
|
const id = toNodeId(srcDir, absPath);
|
|
996
1063
|
const type = classifyType(absPath, id);
|
|
1064
|
+
if (type === "test" || type === "story") continue;
|
|
997
1065
|
const parsed = parsedByPath.get(absPath);
|
|
998
1066
|
const name = parsed.name || nameFromFilename(absPath);
|
|
999
|
-
const
|
|
1000
|
-
const deep = extractDeep(absPath);
|
|
1001
|
-
nodes.push({
|
|
1002
|
-
id,
|
|
1003
|
-
type,
|
|
1004
|
-
name,
|
|
1005
|
-
route,
|
|
1006
|
-
exports: parsed.exports,
|
|
1007
|
-
elements: deep.elements,
|
|
1008
|
-
stateVars: deep.stateVars,
|
|
1009
|
-
conditions: deep.conditions,
|
|
1010
|
-
variables: deep.variables
|
|
1011
|
-
});
|
|
1067
|
+
const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
|
|
1012
1068
|
nodeIdSet.add(id);
|
|
1013
|
-
|
|
1014
|
-
|
|
1069
|
+
if (layer === "api") {
|
|
1070
|
+
const methods = [];
|
|
1071
|
+
for (const exp of parsed.exports) {
|
|
1072
|
+
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1073
|
+
}
|
|
1074
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1075
|
+
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1076
|
+
const deep = extractDeep(absPath);
|
|
1077
|
+
const routePath = (0, import_node_fs5.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
|
|
1078
|
+
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1079
|
+
const mutates = mutations.length > 0;
|
|
1080
|
+
const authStrategy = [...authWrappers];
|
|
1081
|
+
apiNodes.push({
|
|
1082
|
+
id,
|
|
1083
|
+
type: "endpoint",
|
|
1084
|
+
name: routePath,
|
|
1085
|
+
layer: "api",
|
|
1086
|
+
path: routePath,
|
|
1087
|
+
methods,
|
|
1088
|
+
handler: id,
|
|
1089
|
+
mutates,
|
|
1090
|
+
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1091
|
+
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1092
|
+
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1093
|
+
conditions: deep.conditions,
|
|
1094
|
+
variables: deep.variables,
|
|
1095
|
+
responses: deep.responses,
|
|
1096
|
+
params: deep.params,
|
|
1097
|
+
_dbCalls: dbCalls
|
|
1098
|
+
// temp: used for cross-ref building below
|
|
1099
|
+
});
|
|
1100
|
+
} else {
|
|
1101
|
+
const route = extractRoute(id);
|
|
1102
|
+
const deep = extractDeep(absPath);
|
|
1103
|
+
uiNodes.push({
|
|
1104
|
+
id,
|
|
1105
|
+
type,
|
|
1106
|
+
name,
|
|
1107
|
+
layer: "ui",
|
|
1108
|
+
route,
|
|
1109
|
+
exports: parsed.exports,
|
|
1110
|
+
elements: deep.elements,
|
|
1111
|
+
stateVars: deep.stateVars,
|
|
1112
|
+
conditions: deep.conditions,
|
|
1113
|
+
variables: deep.variables
|
|
1114
|
+
});
|
|
1115
|
+
if (route) routeToNodeId.set(route, id);
|
|
1116
|
+
}
|
|
1015
1117
|
}
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1118
|
+
const uiEdges = [];
|
|
1119
|
+
const uiFlagged = [];
|
|
1018
1120
|
for (const absPath of fileSet) {
|
|
1019
|
-
const
|
|
1121
|
+
const id = toNodeId(srcDir, absPath);
|
|
1122
|
+
if (!nodeIdSet.has(id)) continue;
|
|
1020
1123
|
const parsed = parsedByPath.get(absPath);
|
|
1021
1124
|
const { edges, flagged } = extractEdges(
|
|
1022
1125
|
srcDir,
|
|
1023
1126
|
absPath,
|
|
1024
|
-
|
|
1127
|
+
id,
|
|
1025
1128
|
parsed,
|
|
1026
1129
|
nodeIdSet,
|
|
1027
|
-
nodeTypeMap,
|
|
1028
1130
|
barrelMaps,
|
|
1029
1131
|
routeToNodeId
|
|
1030
1132
|
);
|
|
1031
|
-
|
|
1032
|
-
|
|
1133
|
+
uiEdges.push(...edges);
|
|
1134
|
+
uiFlagged.push(...flagged);
|
|
1033
1135
|
}
|
|
1034
1136
|
const fetchCallEntries = [];
|
|
1035
1137
|
for (const absPath of fileSet) {
|
|
1036
1138
|
const sourceId = toNodeId(srcDir, absPath);
|
|
1139
|
+
if (!nodeIdSet.has(sourceId)) continue;
|
|
1037
1140
|
const parsed = parsedByPath.get(absPath);
|
|
1038
1141
|
if (parsed.fetchCalls.length === 0) continue;
|
|
1039
1142
|
fetchCallEntries.push({
|
|
@@ -1048,7 +1151,7 @@ function generate(rootDir) {
|
|
|
1048
1151
|
});
|
|
1049
1152
|
}
|
|
1050
1153
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1051
|
-
const
|
|
1154
|
+
const IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
1052
1155
|
"node_modules",
|
|
1053
1156
|
".next",
|
|
1054
1157
|
"dist",
|
|
@@ -1061,7 +1164,7 @@ function generate(rootDir) {
|
|
|
1061
1164
|
"out",
|
|
1062
1165
|
".vercel"
|
|
1063
1166
|
]);
|
|
1064
|
-
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"],
|
|
1167
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS2);
|
|
1065
1168
|
for (const absPath of externalCandidates) {
|
|
1066
1169
|
const normalized = absPath.replace(/\\/g, "/");
|
|
1067
1170
|
if (externalScanned.has(normalized)) continue;
|
|
@@ -1071,11 +1174,11 @@ function generate(rootDir) {
|
|
|
1071
1174
|
} catch {
|
|
1072
1175
|
continue;
|
|
1073
1176
|
}
|
|
1074
|
-
const externalId = (0,
|
|
1177
|
+
const externalId = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1075
1178
|
const edgesFromThis = [];
|
|
1076
1179
|
const seen = /* @__PURE__ */ new Set();
|
|
1077
1180
|
for (const imp of parsed.imports) {
|
|
1078
|
-
const { specifier,
|
|
1181
|
+
const { specifier, names } = imp;
|
|
1079
1182
|
let resolved = null;
|
|
1080
1183
|
if (specifier.startsWith("@/")) {
|
|
1081
1184
|
const relToSrc = specifier.slice(2);
|
|
@@ -1101,25 +1204,52 @@ function generate(rootDir) {
|
|
|
1101
1204
|
const targetId = toNodeId(srcDir, resolved);
|
|
1102
1205
|
if (!nodeIdSet.has(targetId)) continue;
|
|
1103
1206
|
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
1104
|
-
const key = `${externalId}\u2192${targetId}
|
|
1207
|
+
const key = `${externalId}\u2192${targetId}`;
|
|
1105
1208
|
if (seen.has(key)) continue;
|
|
1106
1209
|
seen.add(key);
|
|
1107
1210
|
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
1108
1211
|
}
|
|
1109
1212
|
if (edgesFromThis.length === 0) continue;
|
|
1110
|
-
|
|
1213
|
+
uiNodes.push({
|
|
1111
1214
|
id: externalId,
|
|
1112
1215
|
type: "external",
|
|
1113
1216
|
name: parsed.name || nameFromFilename(absPath),
|
|
1217
|
+
layer: "ui",
|
|
1114
1218
|
route: null,
|
|
1115
1219
|
exports: parsed.exports
|
|
1116
1220
|
});
|
|
1117
1221
|
nodeIdSet.add(externalId);
|
|
1118
|
-
|
|
1119
|
-
|
|
1222
|
+
uiEdges.push(...edgesFromThis);
|
|
1223
|
+
}
|
|
1224
|
+
const apiCrossRefs = [];
|
|
1225
|
+
for (const node of apiNodes) {
|
|
1226
|
+
const dbCalls = node._dbCalls;
|
|
1227
|
+
if (!dbCalls) continue;
|
|
1228
|
+
const seenModels = /* @__PURE__ */ new Set();
|
|
1229
|
+
for (const call of dbCalls) {
|
|
1230
|
+
if (seenModels.has(call.model)) continue;
|
|
1231
|
+
seenModels.add(call.model);
|
|
1232
|
+
apiCrossRefs.push({
|
|
1233
|
+
source: node.id,
|
|
1234
|
+
target: camelToPascal(call.model),
|
|
1235
|
+
type: call.isMutation ? "mutates" : "reads",
|
|
1236
|
+
layer: "db"
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
delete node._dbCalls;
|
|
1240
|
+
}
|
|
1241
|
+
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1242
|
+
const apiEdges = [];
|
|
1243
|
+
const uiOnlyEdges = [];
|
|
1244
|
+
for (const edge of uiEdges) {
|
|
1245
|
+
if (apiNodeIds.has(edge.source)) {
|
|
1246
|
+
apiEdges.push(edge);
|
|
1247
|
+
} else {
|
|
1248
|
+
uiOnlyEdges.push(edge);
|
|
1249
|
+
}
|
|
1120
1250
|
}
|
|
1121
1251
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
1122
|
-
const dedupedFlagged =
|
|
1252
|
+
const dedupedFlagged = uiFlagged.filter((f) => {
|
|
1123
1253
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
1124
1254
|
if (flaggedSet.has(key)) return false;
|
|
1125
1255
|
flaggedSet.add(key);
|
|
@@ -1136,10 +1266,12 @@ function generate(rootDir) {
|
|
|
1136
1266
|
hook: 7,
|
|
1137
1267
|
lib: 8
|
|
1138
1268
|
};
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1269
|
+
uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
|
|
1270
|
+
uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1271
|
+
apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
|
|
1272
|
+
apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1273
|
+
const byType = (t) => uiNodes.filter((n) => n.type === t).length;
|
|
1274
|
+
const uiStats = {
|
|
1143
1275
|
total_pages: byType("page"),
|
|
1144
1276
|
total_layouts: byType("layout"),
|
|
1145
1277
|
total_components: byType("component"),
|
|
@@ -1150,196 +1282,122 @@ function generate(rootDir) {
|
|
|
1150
1282
|
total_utils: byType("util"),
|
|
1151
1283
|
total_libs: byType("lib"),
|
|
1152
1284
|
total_external: byType("external"),
|
|
1153
|
-
total_edges:
|
|
1285
|
+
total_edges: uiOnlyEdges.length,
|
|
1154
1286
|
total_flagged: dedupedFlagged.length
|
|
1155
1287
|
};
|
|
1156
|
-
|
|
1288
|
+
const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
|
|
1289
|
+
const result = /* @__PURE__ */ new Map();
|
|
1290
|
+
result.set("ui", {
|
|
1157
1291
|
metadata: {
|
|
1158
1292
|
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1159
1293
|
scope: "main-app-only",
|
|
1160
1294
|
app_root: "src/",
|
|
1161
1295
|
layer: "ui",
|
|
1162
|
-
parser: "
|
|
1163
|
-
...
|
|
1296
|
+
parser: "typescript-project",
|
|
1297
|
+
...uiStats,
|
|
1164
1298
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1165
1299
|
},
|
|
1166
|
-
nodes,
|
|
1167
|
-
edges:
|
|
1300
|
+
nodes: stripLayer(uiNodes),
|
|
1301
|
+
edges: uiOnlyEdges,
|
|
1168
1302
|
cross_refs: [],
|
|
1169
1303
|
contradictions: [],
|
|
1170
1304
|
warnings: [],
|
|
1171
1305
|
flagged_edges: dedupedFlagged,
|
|
1172
1306
|
patterns: {
|
|
1173
|
-
total_nodes:
|
|
1174
|
-
by_type:
|
|
1307
|
+
total_nodes: uiNodes.length,
|
|
1308
|
+
by_type: uiStats,
|
|
1175
1309
|
by_edge_type: {
|
|
1176
|
-
renders:
|
|
1177
|
-
imports:
|
|
1178
|
-
navigates:
|
|
1310
|
+
renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
|
|
1311
|
+
imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
|
|
1312
|
+
navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
|
|
1179
1313
|
},
|
|
1180
1314
|
fetch_calls: fetchCallEntries
|
|
1181
1315
|
}
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
generate
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
1201
|
-
function walk2(dir) {
|
|
1202
|
-
const results = [];
|
|
1203
|
-
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
1204
|
-
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
1205
|
-
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
1206
|
-
if (entry.isDirectory()) {
|
|
1207
|
-
results.push(...walk2(full));
|
|
1208
|
-
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
1209
|
-
results.push(full);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
return results;
|
|
1213
|
-
}
|
|
1214
|
-
function filePathToRoute(apiDir, absPath) {
|
|
1215
|
-
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
1216
|
-
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
1217
|
-
route = route.replace(/\/+/g, "/");
|
|
1218
|
-
if (route === "/") return "/api";
|
|
1219
|
-
return "/api" + route;
|
|
1220
|
-
}
|
|
1221
|
-
function camelToPascal(s) {
|
|
1222
|
-
if (!s) return s;
|
|
1223
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1224
|
-
}
|
|
1225
|
-
function detect2(rootDir) {
|
|
1226
|
-
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
|
|
1227
|
-
}
|
|
1228
|
-
function generate2(rootDir) {
|
|
1229
|
-
const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
|
|
1230
|
-
const routeFiles = walk2(apiDir);
|
|
1231
|
-
const nodes = [];
|
|
1232
|
-
const edges = [];
|
|
1233
|
-
const crossRefs = [];
|
|
1234
|
-
const mutatorCount = {};
|
|
1235
|
-
const authUsage = {};
|
|
1236
|
-
let endpointsWithAuth = 0;
|
|
1237
|
-
let endpointsWithDbAccess = 0;
|
|
1238
|
-
for (const absPath of routeFiles) {
|
|
1239
|
-
const parsed = parseFileTS(absPath);
|
|
1240
|
-
const dbCalls = extractDbCallsTS(absPath);
|
|
1241
|
-
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1242
|
-
const methods = [];
|
|
1243
|
-
for (const exp of parsed.exports) {
|
|
1244
|
-
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1245
|
-
}
|
|
1246
|
-
const routePath = filePathToRoute(apiDir, absPath);
|
|
1247
|
-
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1248
|
-
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1249
|
-
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1250
|
-
const mutates = mutations.length > 0;
|
|
1251
|
-
if (mutates) {
|
|
1252
|
-
for (const m of mutations) {
|
|
1253
|
-
mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
|
|
1316
|
+
});
|
|
1317
|
+
if (apiNodes.length > 0) {
|
|
1318
|
+
const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
|
|
1319
|
+
const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
|
|
1320
|
+
const authUsage = {};
|
|
1321
|
+
let endpointsWithAuth = 0;
|
|
1322
|
+
for (const n of apiNodes) {
|
|
1323
|
+
const auth = n.auth;
|
|
1324
|
+
if (auth.length > 0 && auth[0] !== "public") {
|
|
1325
|
+
endpointsWithAuth++;
|
|
1326
|
+
for (const w of auth) {
|
|
1327
|
+
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1328
|
+
}
|
|
1254
1329
|
}
|
|
1255
1330
|
}
|
|
1256
|
-
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1331
|
+
const mutatorCount = {};
|
|
1332
|
+
for (const n of apiNodes) {
|
|
1333
|
+
const ops = n.db_operations;
|
|
1334
|
+
for (const op of ops) {
|
|
1335
|
+
const method = op.split(".")[1];
|
|
1336
|
+
if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
|
|
1337
|
+
}
|
|
1261
1338
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1339
|
+
result.set("api", {
|
|
1340
|
+
metadata: {
|
|
1341
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1342
|
+
scope: "main-app-only",
|
|
1343
|
+
stack: "nextjs-app-router",
|
|
1344
|
+
layer: "api",
|
|
1345
|
+
parser: "typescript-project",
|
|
1346
|
+
total_endpoints: apiNodes.length,
|
|
1347
|
+
total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1348
|
+
endpoints_with_auth: endpointsWithAuth,
|
|
1349
|
+
endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
|
|
1350
|
+
mutator_endpoints: mutatorNodes,
|
|
1351
|
+
read_only_endpoints: readOnlyNodes
|
|
1352
|
+
},
|
|
1353
|
+
nodes: stripLayer(apiNodes),
|
|
1354
|
+
edges: apiEdges,
|
|
1355
|
+
cross_refs: apiCrossRefs,
|
|
1356
|
+
contradictions: [],
|
|
1357
|
+
warnings: [],
|
|
1358
|
+
flagged_edges: [],
|
|
1359
|
+
patterns: {
|
|
1360
|
+
total_endpoints: apiNodes.length,
|
|
1361
|
+
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1362
|
+
acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
|
|
1363
|
+
return acc;
|
|
1364
|
+
}, {}),
|
|
1365
|
+
auth_strategies: authUsage,
|
|
1366
|
+
mutation_operations: mutatorCount,
|
|
1367
|
+
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1368
|
+
}
|
|
1281
1369
|
});
|
|
1282
|
-
const seenModels = /* @__PURE__ */ new Set();
|
|
1283
|
-
for (const call of dbCalls) {
|
|
1284
|
-
if (seenModels.has(call.model)) continue;
|
|
1285
|
-
seenModels.add(call.model);
|
|
1286
|
-
crossRefs.push({
|
|
1287
|
-
source: relPath,
|
|
1288
|
-
target: camelToPascal(call.model),
|
|
1289
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1290
|
-
layer: "db"
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
1370
|
}
|
|
1294
|
-
|
|
1295
|
-
crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1296
|
-
const mutatorNodes = nodes.filter((n) => n.mutates).length;
|
|
1297
|
-
const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
|
|
1298
|
-
return {
|
|
1299
|
-
metadata: {
|
|
1300
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1301
|
-
scope: "main-app-only",
|
|
1302
|
-
stack: "nextjs-app-router",
|
|
1303
|
-
layer: "api",
|
|
1304
|
-
parser: "nextjs-routes-ast",
|
|
1305
|
-
total_endpoints: nodes.length,
|
|
1306
|
-
total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1307
|
-
endpoints_with_auth: endpointsWithAuth,
|
|
1308
|
-
endpoints_with_db_access: endpointsWithDbAccess,
|
|
1309
|
-
mutator_endpoints: mutatorNodes,
|
|
1310
|
-
read_only_endpoints: readOnlyNodes
|
|
1311
|
-
},
|
|
1312
|
-
nodes,
|
|
1313
|
-
edges,
|
|
1314
|
-
cross_refs: crossRefs,
|
|
1315
|
-
contradictions: [],
|
|
1316
|
-
warnings: [],
|
|
1317
|
-
flagged_edges: [],
|
|
1318
|
-
patterns: {
|
|
1319
|
-
total_endpoints: nodes.length,
|
|
1320
|
-
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1321
|
-
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
1322
|
-
return acc;
|
|
1323
|
-
}, {}),
|
|
1324
|
-
auth_strategies: authUsage,
|
|
1325
|
-
mutation_operations: mutatorCount,
|
|
1326
|
-
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1327
|
-
}
|
|
1328
|
-
};
|
|
1371
|
+
return result;
|
|
1329
1372
|
}
|
|
1330
|
-
var import_node_fs5, import_node_path5, HTTP_METHODS,
|
|
1331
|
-
var
|
|
1332
|
-
"src/server/graph/parsers/
|
|
1373
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS, CLASSIFICATION_TO_LAYER, typescriptProjectParser;
|
|
1374
|
+
var init_typescript_project = __esm({
|
|
1375
|
+
"src/server/graph/parsers/ts/typescript-project.ts"() {
|
|
1333
1376
|
"use strict";
|
|
1334
1377
|
import_node_fs5 = require("node:fs");
|
|
1335
1378
|
import_node_path5 = require("node:path");
|
|
1379
|
+
init_config();
|
|
1380
|
+
init_resolve_paths();
|
|
1336
1381
|
init_ts_extractor();
|
|
1337
1382
|
HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1383
|
+
CLASSIFICATION_TO_LAYER = {
|
|
1384
|
+
endpoint: "api",
|
|
1385
|
+
page: "ui",
|
|
1386
|
+
layout: "ui",
|
|
1387
|
+
component: "ui",
|
|
1388
|
+
ui: "ui",
|
|
1389
|
+
hook: "ui",
|
|
1390
|
+
context: "ui",
|
|
1391
|
+
config: "ui",
|
|
1392
|
+
lib: "ui",
|
|
1393
|
+
"mcp-tool": "ui",
|
|
1394
|
+
external: "ui"
|
|
1395
|
+
};
|
|
1396
|
+
typescriptProjectParser = {
|
|
1397
|
+
id: "typescript-project",
|
|
1398
|
+
layers: ["ui", "api"],
|
|
1399
|
+
detect,
|
|
1400
|
+
generate
|
|
1343
1401
|
};
|
|
1344
1402
|
}
|
|
1345
1403
|
});
|
|
@@ -1434,10 +1492,10 @@ function parseEnums(content) {
|
|
|
1434
1492
|
}
|
|
1435
1493
|
return nodes;
|
|
1436
1494
|
}
|
|
1437
|
-
function
|
|
1495
|
+
function detect2(rootDir) {
|
|
1438
1496
|
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
|
|
1439
1497
|
}
|
|
1440
|
-
function
|
|
1498
|
+
function generate2(rootDir) {
|
|
1441
1499
|
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1442
1500
|
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
1443
1501
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
@@ -1497,108 +1555,532 @@ var init_prisma_schema = __esm({
|
|
|
1497
1555
|
prismaSchemaParser = {
|
|
1498
1556
|
id: "prisma-schema",
|
|
1499
1557
|
layer: "db",
|
|
1500
|
-
detect:
|
|
1501
|
-
generate:
|
|
1558
|
+
detect: detect2,
|
|
1559
|
+
generate: generate2
|
|
1502
1560
|
};
|
|
1503
1561
|
}
|
|
1504
1562
|
});
|
|
1505
1563
|
|
|
1506
|
-
// src/server/graph/
|
|
1507
|
-
function
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
const path3 = n.path;
|
|
1511
|
-
if (!path3 || typeof path3 !== "string") continue;
|
|
1512
|
-
routes.push({
|
|
1513
|
-
path: path3,
|
|
1514
|
-
nodeId: n.id,
|
|
1515
|
-
segments: path3.split("/").filter(Boolean)
|
|
1516
|
-
});
|
|
1517
|
-
}
|
|
1518
|
-
return routes;
|
|
1519
|
-
}
|
|
1520
|
-
function buildApiPathMap(routes) {
|
|
1521
|
-
const map = /* @__PURE__ */ new Map();
|
|
1522
|
-
for (const r of routes) {
|
|
1523
|
-
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
1524
|
-
}
|
|
1525
|
-
return map;
|
|
1526
|
-
}
|
|
1527
|
-
function normalizeFetchUrl(raw) {
|
|
1528
|
-
let s = raw.replace(/^`|`$/g, "");
|
|
1529
|
-
const qIdx = s.indexOf("?");
|
|
1530
|
-
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
1531
|
-
const hIdx = s.indexOf("#");
|
|
1532
|
-
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
1533
|
-
let hadInterpolation = false;
|
|
1534
|
-
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1535
|
-
hadInterpolation = true;
|
|
1536
|
-
const cleaned = expr.trim();
|
|
1537
|
-
const last = cleaned.split(".").pop() ?? cleaned;
|
|
1538
|
-
const name = last.replace(/[^\w]/g, "") || "param";
|
|
1539
|
-
return ":" + name;
|
|
1540
|
-
});
|
|
1541
|
-
s = s.replace(/\/+/g, "/");
|
|
1542
|
-
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
1543
|
-
return { path: s || "/", hadInterpolation };
|
|
1564
|
+
// src/server/graph/parsers/db/sql-migrations.ts
|
|
1565
|
+
function pgTypeToPrisma(pgType) {
|
|
1566
|
+
const upper = pgType.toUpperCase().trim();
|
|
1567
|
+
return PG_TO_PRISMA[upper] ?? upper;
|
|
1544
1568
|
}
|
|
1545
|
-
function
|
|
1546
|
-
|
|
1547
|
-
let
|
|
1548
|
-
|
|
1549
|
-
const
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1569
|
+
function parseCreateTable(sql, state) {
|
|
1570
|
+
const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
|
|
1571
|
+
let m;
|
|
1572
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1573
|
+
const tableName = m[1];
|
|
1574
|
+
const body = m[2];
|
|
1575
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1576
|
+
let primaryCol = null;
|
|
1577
|
+
for (const line of body.split("\n")) {
|
|
1578
|
+
const trimmed = line.trim().replace(/,\s*$/, "");
|
|
1579
|
+
if (!trimmed || trimmed.startsWith("--")) continue;
|
|
1580
|
+
const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
|
|
1581
|
+
if (pkMatch) {
|
|
1582
|
+
primaryCol = pkMatch[1];
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
|
|
1586
|
+
const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
|
|
1587
|
+
if (!colMatch) continue;
|
|
1588
|
+
const colName = colMatch[1];
|
|
1589
|
+
let rest = colMatch[2];
|
|
1590
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1591
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
|
|
1592
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1593
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
|
|
1594
|
+
columns.set(colName, {
|
|
1595
|
+
name: colName,
|
|
1596
|
+
type: colType,
|
|
1597
|
+
nullable: !isNotNull,
|
|
1598
|
+
primary: false,
|
|
1599
|
+
unique: false,
|
|
1600
|
+
default: defaultVal
|
|
1601
|
+
});
|
|
1558
1602
|
}
|
|
1559
|
-
if (
|
|
1560
|
-
|
|
1561
|
-
continue;
|
|
1603
|
+
if (primaryCol && columns.has(primaryCol)) {
|
|
1604
|
+
columns.get(primaryCol).primary = true;
|
|
1562
1605
|
}
|
|
1563
|
-
|
|
1606
|
+
state.tables.set(tableName, { name: tableName, columns });
|
|
1564
1607
|
}
|
|
1565
|
-
return score;
|
|
1566
1608
|
}
|
|
1567
|
-
function
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1609
|
+
function parseCreateEnum(sql, state) {
|
|
1610
|
+
const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
|
|
1611
|
+
let m;
|
|
1612
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1613
|
+
const enumName = m[1];
|
|
1614
|
+
const valuesStr = m[2];
|
|
1615
|
+
const values = new Set(
|
|
1616
|
+
valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
|
|
1617
|
+
);
|
|
1618
|
+
state.enums.set(enumName, { name: enumName, values });
|
|
1574
1619
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1620
|
+
}
|
|
1621
|
+
function parseAlterTable(sql, state) {
|
|
1622
|
+
const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
|
|
1623
|
+
let m;
|
|
1624
|
+
while ((m = addColRe.exec(sql)) !== null) {
|
|
1625
|
+
const tableName = m[1];
|
|
1626
|
+
const colName = m[2];
|
|
1627
|
+
let rest = m[3];
|
|
1628
|
+
const table = state.tables.get(tableName);
|
|
1629
|
+
if (!table) continue;
|
|
1630
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1631
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
|
|
1632
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1633
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
|
|
1634
|
+
table.columns.set(colName, {
|
|
1635
|
+
name: colName,
|
|
1636
|
+
type: colType,
|
|
1637
|
+
nullable: !isNotNull,
|
|
1638
|
+
primary: false,
|
|
1639
|
+
unique: false,
|
|
1640
|
+
default: defaultVal
|
|
1641
|
+
});
|
|
1578
1642
|
}
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1643
|
+
const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
|
|
1644
|
+
while ((m = dropColRe.exec(sql)) !== null) {
|
|
1645
|
+
const table = state.tables.get(m[1]);
|
|
1646
|
+
if (table) table.columns.delete(m[2]);
|
|
1647
|
+
}
|
|
1648
|
+
const fkRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+CONSTRAINT\s+"([^"]+)"\s+FOREIGN\s+KEY\s*\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\("(\w+)"\)(?:\s+ON\s+DELETE\s+(\w+(?:\s+\w+)?))?/gi;
|
|
1649
|
+
while ((m = fkRe.exec(sql)) !== null) {
|
|
1650
|
+
state.fks.push({
|
|
1651
|
+
constraintName: m[2],
|
|
1652
|
+
sourceTable: m[1],
|
|
1653
|
+
sourceColumn: m[3],
|
|
1654
|
+
targetTable: m[4],
|
|
1655
|
+
targetColumn: m[5],
|
|
1656
|
+
onDelete: m[6] ?? null
|
|
1657
|
+
});
|
|
1582
1658
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
let
|
|
1587
|
-
|
|
1588
|
-
const
|
|
1589
|
-
if (
|
|
1590
|
-
bestScore = score;
|
|
1591
|
-
bestId = r.nodeId;
|
|
1592
|
-
}
|
|
1659
|
+
}
|
|
1660
|
+
function parseAlterEnum(sql, state) {
|
|
1661
|
+
const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
|
|
1662
|
+
let m;
|
|
1663
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1664
|
+
const en = state.enums.get(m[1]);
|
|
1665
|
+
if (en) en.values.add(m[2]);
|
|
1593
1666
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1667
|
+
}
|
|
1668
|
+
function parseDropTable(sql, state) {
|
|
1669
|
+
const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
|
|
1670
|
+
let m;
|
|
1671
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1672
|
+
state.tables.delete(m[1]);
|
|
1673
|
+
state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
|
|
1596
1674
|
}
|
|
1597
|
-
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1598
1675
|
}
|
|
1599
|
-
function
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1676
|
+
function parseUniqueIndex(sql, state) {
|
|
1677
|
+
const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
|
|
1678
|
+
let m;
|
|
1679
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1680
|
+
const table = state.tables.get(m[1]);
|
|
1681
|
+
const col = table?.columns.get(m[2]);
|
|
1682
|
+
if (col) col.unique = true;
|
|
1683
|
+
if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
|
|
1684
|
+
state.uniqueIndexes.get(m[1]).add(m[2]);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function parseMigrations(rootDir) {
|
|
1688
|
+
const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
|
|
1689
|
+
const state = {
|
|
1690
|
+
tables: /* @__PURE__ */ new Map(),
|
|
1691
|
+
enums: /* @__PURE__ */ new Map(),
|
|
1692
|
+
fks: [],
|
|
1693
|
+
uniqueIndexes: /* @__PURE__ */ new Map()
|
|
1694
|
+
};
|
|
1695
|
+
if (!(0, import_node_fs7.existsSync)(migrationsDir)) return state;
|
|
1696
|
+
const dirs = (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1697
|
+
for (const dir of dirs) {
|
|
1698
|
+
const sqlPath = (0, import_node_path7.join)(migrationsDir, dir, "migration.sql");
|
|
1699
|
+
if (!(0, import_node_fs7.existsSync)(sqlPath)) continue;
|
|
1700
|
+
const sql = (0, import_node_fs7.readFileSync)(sqlPath, "utf-8");
|
|
1701
|
+
parseCreateEnum(sql, state);
|
|
1702
|
+
parseCreateTable(sql, state);
|
|
1703
|
+
parseAlterTable(sql, state);
|
|
1704
|
+
parseAlterEnum(sql, state);
|
|
1705
|
+
parseDropTable(sql, state);
|
|
1706
|
+
parseUniqueIndex(sql, state);
|
|
1707
|
+
}
|
|
1708
|
+
return state;
|
|
1709
|
+
}
|
|
1710
|
+
function loadPrismaState(rootDir) {
|
|
1711
|
+
const schemaPath = (0, import_node_path7.join)(rootDir, "prisma", "schema.prisma");
|
|
1712
|
+
if (!(0, import_node_fs7.existsSync)(schemaPath)) return null;
|
|
1713
|
+
const content = (0, import_node_fs7.readFileSync)(schemaPath, "utf-8");
|
|
1714
|
+
const tables = /* @__PURE__ */ new Map();
|
|
1715
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1716
|
+
const relations = [];
|
|
1717
|
+
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1718
|
+
let m;
|
|
1719
|
+
while ((m = modelRe.exec(content)) !== null) {
|
|
1720
|
+
const modelName = m[1];
|
|
1721
|
+
const body = m[2];
|
|
1722
|
+
const cols = [];
|
|
1723
|
+
for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
|
|
1724
|
+
const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
|
|
1725
|
+
if (!fm) continue;
|
|
1726
|
+
const fieldName = fm[1];
|
|
1727
|
+
const fieldType = fm[2];
|
|
1728
|
+
const rest = fm[3] ?? "";
|
|
1729
|
+
const baseType = fieldType.replace(/[?\[\]]/g, "");
|
|
1730
|
+
const isRelation = rest.includes("@relation");
|
|
1731
|
+
if (isRelation) {
|
|
1732
|
+
const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
|
|
1733
|
+
const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
|
|
1734
|
+
if (fieldsMatch) {
|
|
1735
|
+
relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
|
|
1736
|
+
}
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
|
|
1740
|
+
if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
|
|
1741
|
+
}
|
|
1742
|
+
cols.push({
|
|
1743
|
+
name: fieldName,
|
|
1744
|
+
type: fieldType.replace("?", ""),
|
|
1745
|
+
nullable: fieldType.endsWith("?") || fieldType.includes("?"),
|
|
1746
|
+
primary: rest.includes("@id"),
|
|
1747
|
+
unique: rest.includes("@unique")
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
tables.set(modelName, { columns: cols });
|
|
1751
|
+
}
|
|
1752
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1753
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
1754
|
+
const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
1755
|
+
enums.set(m[1], values);
|
|
1756
|
+
}
|
|
1757
|
+
return { tables, enums, relations };
|
|
1758
|
+
}
|
|
1759
|
+
function verify(sqlState, prisma) {
|
|
1760
|
+
const contradictions = [];
|
|
1761
|
+
const flaggedEdges = [];
|
|
1762
|
+
for (const [name] of sqlState.tables) {
|
|
1763
|
+
if (!prisma.tables.has(name)) {
|
|
1764
|
+
contradictions.push({
|
|
1765
|
+
entity: name,
|
|
1766
|
+
source_a: "sql-migrations",
|
|
1767
|
+
source_b: "prisma-schema",
|
|
1768
|
+
detail: `Table "${name}" exists in migrations but not in schema.prisma`
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
for (const [name] of prisma.tables) {
|
|
1773
|
+
if (!sqlState.tables.has(name)) {
|
|
1774
|
+
contradictions.push({
|
|
1775
|
+
entity: name,
|
|
1776
|
+
source_a: "prisma-schema",
|
|
1777
|
+
source_b: "sql-migrations",
|
|
1778
|
+
detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
for (const [tableName, prismaTable] of prisma.tables) {
|
|
1783
|
+
const sqlTable = sqlState.tables.get(tableName);
|
|
1784
|
+
if (!sqlTable) continue;
|
|
1785
|
+
for (const prismaCol of prismaTable.columns) {
|
|
1786
|
+
const sqlCol = sqlTable.columns.get(prismaCol.name);
|
|
1787
|
+
if (!sqlCol) {
|
|
1788
|
+
contradictions.push({
|
|
1789
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1790
|
+
source_a: "prisma-schema",
|
|
1791
|
+
source_b: "sql-migrations",
|
|
1792
|
+
detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
|
|
1793
|
+
});
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
const mappedSqlType = pgTypeToPrisma(sqlCol.type);
|
|
1797
|
+
const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
|
|
1798
|
+
if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
|
|
1799
|
+
if (!sqlState.enums.has(prismaBaseType)) {
|
|
1800
|
+
contradictions.push({
|
|
1801
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1802
|
+
source_a: "sql-migrations",
|
|
1803
|
+
source_b: "prisma-schema",
|
|
1804
|
+
detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
if (sqlCol.nullable !== prismaCol.nullable) {
|
|
1809
|
+
contradictions.push({
|
|
1810
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1811
|
+
source_a: "sql-migrations",
|
|
1812
|
+
source_b: "prisma-schema",
|
|
1813
|
+
detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
for (const [colName] of sqlTable.columns) {
|
|
1818
|
+
const inPrisma = prismaTable.columns.some((c) => c.name === colName);
|
|
1819
|
+
if (!inPrisma) {
|
|
1820
|
+
contradictions.push({
|
|
1821
|
+
entity: `${tableName}.${colName}`,
|
|
1822
|
+
source_a: "sql-migrations",
|
|
1823
|
+
source_b: "prisma-schema",
|
|
1824
|
+
detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1830
|
+
const prismaValues = prisma.enums.get(name);
|
|
1831
|
+
if (!prismaValues) {
|
|
1832
|
+
contradictions.push({
|
|
1833
|
+
entity: name,
|
|
1834
|
+
source_a: "sql-migrations",
|
|
1835
|
+
source_b: "prisma-schema",
|
|
1836
|
+
detail: `Enum "${name}" exists in migrations but not in schema.prisma`
|
|
1837
|
+
});
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
const prismaSet = new Set(prismaValues);
|
|
1841
|
+
for (const val of sqlEnum.values) {
|
|
1842
|
+
if (!prismaSet.has(val)) {
|
|
1843
|
+
contradictions.push({
|
|
1844
|
+
entity: `${name}.${val}`,
|
|
1845
|
+
source_a: "sql-migrations",
|
|
1846
|
+
source_b: "prisma-schema",
|
|
1847
|
+
detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
for (const val of prismaValues) {
|
|
1852
|
+
if (!sqlEnum.values.has(val)) {
|
|
1853
|
+
contradictions.push({
|
|
1854
|
+
entity: `${name}.${val}`,
|
|
1855
|
+
source_a: "prisma-schema",
|
|
1856
|
+
source_b: "sql-migrations",
|
|
1857
|
+
detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
|
|
1863
|
+
for (const fk of sqlState.fks) {
|
|
1864
|
+
const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
|
|
1865
|
+
if (!prismaFkSet.has(key)) {
|
|
1866
|
+
flaggedEdges.push({
|
|
1867
|
+
source: fk.sourceTable,
|
|
1868
|
+
target: fk.targetTable,
|
|
1869
|
+
type: "belongs_to",
|
|
1870
|
+
label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
|
|
1871
|
+
confidence: "high"
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return { contradictions, flaggedEdges };
|
|
1876
|
+
}
|
|
1877
|
+
function detect3(rootDir) {
|
|
1878
|
+
const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
|
|
1879
|
+
if (!(0, import_node_fs7.existsSync)(migrationsDir)) return false;
|
|
1880
|
+
return (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).some((d) => d.isDirectory() && (0, import_node_fs7.existsSync)((0, import_node_path7.join)(migrationsDir, d.name, "migration.sql")));
|
|
1881
|
+
}
|
|
1882
|
+
function generate3(rootDir) {
|
|
1883
|
+
const sqlState = parseMigrations(rootDir);
|
|
1884
|
+
const prisma = loadPrismaState(rootDir);
|
|
1885
|
+
const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
|
|
1886
|
+
const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
|
|
1887
|
+
const nodes = [];
|
|
1888
|
+
for (const [name, table] of sqlState.tables) {
|
|
1889
|
+
if (prismaTableIds.has(name)) continue;
|
|
1890
|
+
nodes.push({
|
|
1891
|
+
id: name,
|
|
1892
|
+
type: "table",
|
|
1893
|
+
name,
|
|
1894
|
+
source: "sql",
|
|
1895
|
+
columns: [...table.columns.values()].map((c) => ({
|
|
1896
|
+
name: c.name,
|
|
1897
|
+
type: c.type,
|
|
1898
|
+
primary: c.primary,
|
|
1899
|
+
unique: c.unique,
|
|
1900
|
+
nullable: c.nullable,
|
|
1901
|
+
default: c.default,
|
|
1902
|
+
isRelation: false,
|
|
1903
|
+
comment: null
|
|
1904
|
+
}))
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1908
|
+
if (prismaEnumIds.has(name)) continue;
|
|
1909
|
+
nodes.push({
|
|
1910
|
+
id: name,
|
|
1911
|
+
type: "enum",
|
|
1912
|
+
name,
|
|
1913
|
+
source: "sql",
|
|
1914
|
+
values: [...sqlEnum.values]
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
|
|
1918
|
+
const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
|
|
1919
|
+
source: fk.sourceTable,
|
|
1920
|
+
target: fk.targetTable,
|
|
1921
|
+
type: "belongs_to",
|
|
1922
|
+
fk: fk.sourceColumn,
|
|
1923
|
+
onDelete: fk.onDelete
|
|
1924
|
+
}));
|
|
1925
|
+
const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
|
|
1926
|
+
return {
|
|
1927
|
+
metadata: {
|
|
1928
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1929
|
+
scope: "sql-migrations",
|
|
1930
|
+
source: "prisma/migrations/",
|
|
1931
|
+
layer: "db",
|
|
1932
|
+
sql_tables: sqlState.tables.size,
|
|
1933
|
+
sql_enums: sqlState.enums.size,
|
|
1934
|
+
sql_fks: sqlState.fks.length,
|
|
1935
|
+
additive_nodes: nodes.length,
|
|
1936
|
+
contradictions_found: contradictions.length,
|
|
1937
|
+
flagged_edges_found: flaggedEdges.length
|
|
1938
|
+
},
|
|
1939
|
+
nodes,
|
|
1940
|
+
edges,
|
|
1941
|
+
cross_refs: [],
|
|
1942
|
+
contradictions,
|
|
1943
|
+
warnings: [],
|
|
1944
|
+
flagged_edges: flaggedEdges
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
var import_node_fs7, import_node_path7, PG_TO_PRISMA, sqlMigrationsParser;
|
|
1948
|
+
var init_sql_migrations = __esm({
|
|
1949
|
+
"src/server/graph/parsers/db/sql-migrations.ts"() {
|
|
1950
|
+
"use strict";
|
|
1951
|
+
import_node_fs7 = require("node:fs");
|
|
1952
|
+
import_node_path7 = require("node:path");
|
|
1953
|
+
PG_TO_PRISMA = {
|
|
1954
|
+
"TEXT": "String",
|
|
1955
|
+
"VARCHAR": "String",
|
|
1956
|
+
"CHAR": "String",
|
|
1957
|
+
"INTEGER": "Int",
|
|
1958
|
+
"INT": "Int",
|
|
1959
|
+
"SMALLINT": "Int",
|
|
1960
|
+
"BIGINT": "BigInt",
|
|
1961
|
+
"SERIAL": "Int",
|
|
1962
|
+
"BOOLEAN": "Boolean",
|
|
1963
|
+
"BOOL": "Boolean",
|
|
1964
|
+
"TIMESTAMP(3)": "DateTime",
|
|
1965
|
+
"TIMESTAMP": "DateTime",
|
|
1966
|
+
"TIMESTAMPTZ": "DateTime",
|
|
1967
|
+
"DATE": "DateTime",
|
|
1968
|
+
"DOUBLE PRECISION": "Float",
|
|
1969
|
+
"FLOAT": "Float",
|
|
1970
|
+
"REAL": "Float",
|
|
1971
|
+
"DECIMAL": "Decimal",
|
|
1972
|
+
"NUMERIC": "Decimal",
|
|
1973
|
+
"JSONB": "Json",
|
|
1974
|
+
"JSON": "Json",
|
|
1975
|
+
"BYTEA": "Bytes",
|
|
1976
|
+
"UUID": "String",
|
|
1977
|
+
"TEXT[]": "String[]"
|
|
1978
|
+
};
|
|
1979
|
+
sqlMigrationsParser = {
|
|
1980
|
+
id: "sql-migrations",
|
|
1981
|
+
layer: "db",
|
|
1982
|
+
detect: detect3,
|
|
1983
|
+
generate: generate3
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
// src/server/graph/core/api-route-matching.ts
|
|
1989
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
1990
|
+
const routes = [];
|
|
1991
|
+
for (const n of apiOutput.nodes) {
|
|
1992
|
+
const path3 = n.path;
|
|
1993
|
+
if (!path3 || typeof path3 !== "string") continue;
|
|
1994
|
+
routes.push({
|
|
1995
|
+
path: path3,
|
|
1996
|
+
nodeId: n.id,
|
|
1997
|
+
segments: path3.split("/").filter(Boolean)
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
return routes;
|
|
2001
|
+
}
|
|
2002
|
+
function buildApiPathMap(routes) {
|
|
2003
|
+
const map = /* @__PURE__ */ new Map();
|
|
2004
|
+
for (const r of routes) {
|
|
2005
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
2006
|
+
}
|
|
2007
|
+
return map;
|
|
2008
|
+
}
|
|
2009
|
+
function normalizeFetchUrl(raw) {
|
|
2010
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
2011
|
+
const qIdx = s.indexOf("?");
|
|
2012
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
2013
|
+
const hIdx = s.indexOf("#");
|
|
2014
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
2015
|
+
let hadInterpolation = false;
|
|
2016
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
2017
|
+
hadInterpolation = true;
|
|
2018
|
+
const cleaned = expr.trim();
|
|
2019
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
2020
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
2021
|
+
return ":" + name;
|
|
2022
|
+
});
|
|
2023
|
+
s = s.replace(/\/+/g, "/");
|
|
2024
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
2025
|
+
return { path: s || "/", hadInterpolation };
|
|
2026
|
+
}
|
|
2027
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
2028
|
+
if (candidate.length !== known.length) return -1;
|
|
2029
|
+
let score = 0;
|
|
2030
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
2031
|
+
const a = candidate[i];
|
|
2032
|
+
const b = known[i];
|
|
2033
|
+
if (a === b) {
|
|
2034
|
+
score += 3;
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
2038
|
+
score += 2;
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
2042
|
+
score += 1;
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
return -1;
|
|
2046
|
+
}
|
|
2047
|
+
return score;
|
|
2048
|
+
}
|
|
2049
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
2050
|
+
const raw = call.url;
|
|
2051
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
2052
|
+
return { kind: "external", normalizedUrl: raw };
|
|
2053
|
+
}
|
|
2054
|
+
if (call.isConcat) {
|
|
2055
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
2056
|
+
}
|
|
2057
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
|
|
2058
|
+
if (!path3.startsWith("/")) {
|
|
2059
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
2060
|
+
}
|
|
2061
|
+
const segs = path3.split("/").filter(Boolean);
|
|
2062
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
2063
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
2064
|
+
}
|
|
2065
|
+
const exact = apiPathMap.get(path3);
|
|
2066
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
2067
|
+
let bestScore = -1;
|
|
2068
|
+
let bestId = null;
|
|
2069
|
+
for (const r of apiRoutes) {
|
|
2070
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
2071
|
+
if (score > bestScore) {
|
|
2072
|
+
bestScore = score;
|
|
2073
|
+
bestId = r.nodeId;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (bestId && bestScore > 0) {
|
|
2077
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
2078
|
+
}
|
|
2079
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
2080
|
+
}
|
|
2081
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
2082
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
2083
|
+
if (!path3.startsWith("/")) {
|
|
1602
2084
|
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1603
2085
|
}
|
|
1604
2086
|
const segs = path3.split("/").filter(Boolean);
|
|
@@ -1636,6 +2118,7 @@ var init_fetch_resolver = __esm({
|
|
|
1636
2118
|
fetchResolverParser = {
|
|
1637
2119
|
id: "fetch-resolver",
|
|
1638
2120
|
layer: "crosslayer",
|
|
2121
|
+
concern: "api-binding",
|
|
1639
2122
|
detect(_rootDir) {
|
|
1640
2123
|
return true;
|
|
1641
2124
|
},
|
|
@@ -1728,36 +2211,37 @@ var init_fetch_resolver = __esm({
|
|
|
1728
2211
|
});
|
|
1729
2212
|
|
|
1730
2213
|
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1731
|
-
function
|
|
1732
|
-
if (!(0,
|
|
2214
|
+
function walk2(dir, exts) {
|
|
2215
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1733
2216
|
const results = [];
|
|
1734
|
-
for (const entry of (0,
|
|
2217
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
1735
2218
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1736
|
-
const full = (0,
|
|
2219
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1737
2220
|
if (entry.isDirectory()) {
|
|
1738
|
-
results.push(...
|
|
1739
|
-
} else if (exts.includes((0,
|
|
2221
|
+
results.push(...walk2(full, exts));
|
|
2222
|
+
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
1740
2223
|
results.push(full);
|
|
1741
2224
|
}
|
|
1742
2225
|
}
|
|
1743
2226
|
return results;
|
|
1744
2227
|
}
|
|
1745
2228
|
function toNodeId2(srcDir, absPath) {
|
|
1746
|
-
return (0,
|
|
2229
|
+
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1747
2230
|
}
|
|
1748
|
-
var
|
|
2231
|
+
var import_node_fs8, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
|
|
1749
2232
|
var init_api_annotations = __esm({
|
|
1750
2233
|
"src/server/graph/parsers/crosslayer/api-annotations.ts"() {
|
|
1751
2234
|
"use strict";
|
|
1752
|
-
|
|
1753
|
-
|
|
2235
|
+
import_node_fs8 = require("node:fs");
|
|
2236
|
+
import_node_path8 = require("node:path");
|
|
1754
2237
|
init_api_route_matching();
|
|
1755
2238
|
API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1756
2239
|
apiAnnotationsParser = {
|
|
1757
2240
|
id: "api-annotations",
|
|
1758
2241
|
layer: "crosslayer",
|
|
2242
|
+
concern: "api-binding",
|
|
1759
2243
|
detect(rootDir) {
|
|
1760
|
-
return (0,
|
|
2244
|
+
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
|
|
1761
2245
|
},
|
|
1762
2246
|
generate(rootDir, layerOutputs) {
|
|
1763
2247
|
const apiOutput = layerOutputs.get("api");
|
|
@@ -1768,13 +2252,13 @@ var init_api_annotations = __esm({
|
|
|
1768
2252
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1769
2253
|
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1770
2254
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1771
|
-
const srcDir = (0,
|
|
1772
|
-
const files =
|
|
2255
|
+
const srcDir = (0, import_node_path8.join)(rootDir, "src");
|
|
2256
|
+
const files = walk2(srcDir, [".ts", ".tsx"]);
|
|
1773
2257
|
const crossRefs = [];
|
|
1774
2258
|
const flaggedEdges = [];
|
|
1775
2259
|
const seen = /* @__PURE__ */ new Set();
|
|
1776
2260
|
for (const absPath of files) {
|
|
1777
|
-
const content = (0,
|
|
2261
|
+
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
1778
2262
|
const sourceId = toNodeId2(srcDir, absPath);
|
|
1779
2263
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
1780
2264
|
let match;
|
|
@@ -1820,36 +2304,40 @@ var init_api_annotations = __esm({
|
|
|
1820
2304
|
});
|
|
1821
2305
|
|
|
1822
2306
|
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1823
|
-
function
|
|
1824
|
-
if (!(0,
|
|
2307
|
+
function walk3(dir, exts) {
|
|
2308
|
+
if (!(0, import_node_fs9.existsSync)(dir)) return [];
|
|
1825
2309
|
const results = [];
|
|
1826
|
-
for (const entry of (0,
|
|
2310
|
+
for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
|
|
1827
2311
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1828
|
-
const full = (0,
|
|
2312
|
+
const full = (0, import_node_path9.join)(dir, entry.name);
|
|
1829
2313
|
if (entry.isDirectory()) {
|
|
1830
|
-
results.push(...
|
|
1831
|
-
} else if (exts.includes((0,
|
|
2314
|
+
results.push(...walk3(full, exts));
|
|
2315
|
+
} else if (exts.includes((0, import_node_path9.extname)(entry.name))) {
|
|
1832
2316
|
results.push(full);
|
|
1833
2317
|
}
|
|
1834
2318
|
}
|
|
1835
2319
|
return results;
|
|
1836
2320
|
}
|
|
1837
2321
|
function toNodeId3(srcDir, absPath) {
|
|
1838
|
-
return (0,
|
|
2322
|
+
return (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1839
2323
|
}
|
|
1840
|
-
var
|
|
2324
|
+
var import_node_fs9, import_node_path9, URL_LITERAL_RE, urlLiteralScannerParser;
|
|
1841
2325
|
var init_url_literal_scanner = __esm({
|
|
1842
2326
|
"src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
|
|
1843
2327
|
"use strict";
|
|
1844
|
-
|
|
1845
|
-
|
|
2328
|
+
import_node_fs9 = require("node:fs");
|
|
2329
|
+
import_node_path9 = require("node:path");
|
|
1846
2330
|
init_api_route_matching();
|
|
2331
|
+
init_config();
|
|
2332
|
+
init_resolve_paths();
|
|
1847
2333
|
URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
1848
2334
|
urlLiteralScannerParser = {
|
|
1849
2335
|
id: "url-literal-scanner",
|
|
1850
2336
|
layer: "crosslayer",
|
|
2337
|
+
concern: "api-binding",
|
|
1851
2338
|
detect(rootDir) {
|
|
1852
|
-
|
|
2339
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2340
|
+
return paths !== null;
|
|
1853
2341
|
},
|
|
1854
2342
|
generate(rootDir, layerOutputs) {
|
|
1855
2343
|
const apiOutput = layerOutputs.get("api");
|
|
@@ -1860,19 +2348,19 @@ var init_url_literal_scanner = __esm({
|
|
|
1860
2348
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1861
2349
|
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1862
2350
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1863
|
-
const
|
|
1864
|
-
const
|
|
1865
|
-
const
|
|
2351
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2352
|
+
const srcDir = paths.srcDir;
|
|
2353
|
+
const clientDir = (0, import_node_path9.join)(srcDir, "client");
|
|
1866
2354
|
const files = [
|
|
1867
|
-
...
|
|
1868
|
-
...
|
|
2355
|
+
...walk3(clientDir, [".ts", ".tsx"]),
|
|
2356
|
+
...walk3(paths.appDir, [".ts", ".tsx"])
|
|
1869
2357
|
];
|
|
1870
2358
|
const crossRefs = [];
|
|
1871
2359
|
const seen = /* @__PURE__ */ new Set();
|
|
1872
2360
|
for (const absPath of files) {
|
|
1873
2361
|
const sourceId = toNodeId3(srcDir, absPath);
|
|
1874
2362
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
1875
|
-
const content = (0,
|
|
2363
|
+
const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
|
|
1876
2364
|
let match;
|
|
1877
2365
|
URL_LITERAL_RE.lastIndex = 0;
|
|
1878
2366
|
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
@@ -1904,15 +2392,689 @@ var init_url_literal_scanner = __esm({
|
|
|
1904
2392
|
}
|
|
1905
2393
|
});
|
|
1906
2394
|
|
|
2395
|
+
// src/server/graph/parsers/static/static-values.ts
|
|
2396
|
+
function tryLoadTreeSitter() {
|
|
2397
|
+
if (parseCode) return true;
|
|
2398
|
+
try {
|
|
2399
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2400
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
2401
|
+
parseCode = extractor.parseCodeTS;
|
|
2402
|
+
return true;
|
|
2403
|
+
}
|
|
2404
|
+
} catch {
|
|
2405
|
+
}
|
|
2406
|
+
return false;
|
|
2407
|
+
}
|
|
2408
|
+
function classifyScope(source, model) {
|
|
2409
|
+
if (source.includes("prisma/schema.prisma")) return "shared";
|
|
2410
|
+
if (source.includes("prisma/seed") && model) {
|
|
2411
|
+
if (SHARED_MODELS.has(model)) return "shared";
|
|
2412
|
+
if (DB_MODELS.has(model)) return "db";
|
|
2413
|
+
return "shared";
|
|
2414
|
+
}
|
|
2415
|
+
if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
|
|
2416
|
+
if (source.startsWith("src/server/")) return "be";
|
|
2417
|
+
if (source.startsWith("src/config/")) return "be";
|
|
2418
|
+
if (source.startsWith("src/lib/")) return "shared";
|
|
2419
|
+
return "shared";
|
|
2420
|
+
}
|
|
2421
|
+
function extractEnumValues(rootDir) {
|
|
2422
|
+
const nodes = [];
|
|
2423
|
+
const edges = [];
|
|
2424
|
+
const schemaPaths = [
|
|
2425
|
+
(0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"),
|
|
2426
|
+
(0, import_node_path10.join)(rootDir, "prisma", "schema")
|
|
2427
|
+
];
|
|
2428
|
+
let content = "";
|
|
2429
|
+
for (const p of schemaPaths) {
|
|
2430
|
+
if ((0, import_node_fs10.existsSync)(p)) {
|
|
2431
|
+
try {
|
|
2432
|
+
const stat = (0, import_node_fs10.statSync)(p);
|
|
2433
|
+
if (stat.isFile()) {
|
|
2434
|
+
content = (0, import_node_fs10.readFileSync)(p, "utf-8");
|
|
2435
|
+
} else if (stat.isDirectory()) {
|
|
2436
|
+
const files = (0, import_node_fs10.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
|
|
2437
|
+
content = files.map((f) => (0, import_node_fs10.readFileSync)((0, import_node_path10.join)(p, f), "utf-8")).join("\n");
|
|
2438
|
+
}
|
|
2439
|
+
} catch {
|
|
2440
|
+
continue;
|
|
2441
|
+
}
|
|
2442
|
+
break;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (!content) return { nodes, edges };
|
|
2446
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
2447
|
+
let m;
|
|
2448
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
2449
|
+
const enumName = m[1];
|
|
2450
|
+
const body = m[2];
|
|
2451
|
+
const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
2452
|
+
const scope = classifyScope("prisma/schema.prisma");
|
|
2453
|
+
for (const val of values) {
|
|
2454
|
+
const nodeId = `${enumName}.${val}`;
|
|
2455
|
+
nodes.push({
|
|
2456
|
+
id: nodeId,
|
|
2457
|
+
type: "enum_value",
|
|
2458
|
+
name: val,
|
|
2459
|
+
parentEnum: enumName,
|
|
2460
|
+
value: val,
|
|
2461
|
+
scope,
|
|
2462
|
+
source: "prisma/schema.prisma"
|
|
2463
|
+
});
|
|
2464
|
+
edges.push({
|
|
2465
|
+
source: nodeId,
|
|
2466
|
+
target: `enum:${enumName}`,
|
|
2467
|
+
type: "member_of"
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
nodes.push({
|
|
2471
|
+
id: `enum:${enumName}`,
|
|
2472
|
+
type: "enum_group",
|
|
2473
|
+
name: enumName,
|
|
2474
|
+
valueCount: values.length,
|
|
2475
|
+
scope,
|
|
2476
|
+
source: "prisma/schema.prisma"
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
return { nodes, edges };
|
|
2480
|
+
}
|
|
2481
|
+
function extractPropsFromObjectNode(node) {
|
|
2482
|
+
const props = {};
|
|
2483
|
+
for (const child of node.namedChildren) {
|
|
2484
|
+
if (child.type !== "pair") continue;
|
|
2485
|
+
const keyNode = child.childForFieldName("key");
|
|
2486
|
+
const valNode = child.childForFieldName("value");
|
|
2487
|
+
if (!keyNode || !valNode) continue;
|
|
2488
|
+
const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
|
|
2489
|
+
if (valNode.type === "string" || valNode.type === "template_string") {
|
|
2490
|
+
const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
|
|
2491
|
+
props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
|
|
2492
|
+
} else if (valNode.type === "number") {
|
|
2493
|
+
props[key] = valNode.text;
|
|
2494
|
+
} else if (valNode.type === "true" || valNode.type === "false") {
|
|
2495
|
+
props[key] = valNode.text;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
return props;
|
|
2499
|
+
}
|
|
2500
|
+
function extractStringArrayFromNode(node) {
|
|
2501
|
+
const values = [];
|
|
2502
|
+
for (const child of node.namedChildren) {
|
|
2503
|
+
if (child.type === "string") {
|
|
2504
|
+
const frag = child.namedChildren.find((c) => c.type === "string_fragment");
|
|
2505
|
+
if (frag) values.push(frag.text);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
return values;
|
|
2509
|
+
}
|
|
2510
|
+
function findArrayDecl(root, varName) {
|
|
2511
|
+
function walk5(node) {
|
|
2512
|
+
if (node.type === "variable_declarator") {
|
|
2513
|
+
const nameNode = node.childForFieldName("name");
|
|
2514
|
+
const valueNode = node.childForFieldName("value");
|
|
2515
|
+
if (nameNode?.text === varName && valueNode?.type === "array") {
|
|
2516
|
+
return valueNode;
|
|
2517
|
+
}
|
|
2518
|
+
if (nameNode?.text === varName && valueNode?.type === "as_expression") {
|
|
2519
|
+
const inner = valueNode.namedChildren.find((c) => c.type === "array");
|
|
2520
|
+
if (inner) return inner;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
for (const child of node.namedChildren) {
|
|
2524
|
+
const found = walk5(child);
|
|
2525
|
+
if (found) return found;
|
|
2526
|
+
}
|
|
2527
|
+
return null;
|
|
2528
|
+
}
|
|
2529
|
+
return walk5(root);
|
|
2530
|
+
}
|
|
2531
|
+
function extractObjectPropsRegex(objStr) {
|
|
2532
|
+
const props = {};
|
|
2533
|
+
const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
|
|
2534
|
+
let m;
|
|
2535
|
+
while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
|
|
2536
|
+
const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
|
|
2537
|
+
while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
|
|
2538
|
+
return props;
|
|
2539
|
+
}
|
|
2540
|
+
function extractStringArrayRegex(arrStr) {
|
|
2541
|
+
return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
|
|
2542
|
+
}
|
|
2543
|
+
function splitArrayObjectsRegex(arrayBody) {
|
|
2544
|
+
const objects = [];
|
|
2545
|
+
let depth = 0;
|
|
2546
|
+
let start = -1;
|
|
2547
|
+
for (let i = 0; i < arrayBody.length; i++) {
|
|
2548
|
+
if (arrayBody[i] === "{") {
|
|
2549
|
+
if (depth === 0) start = i;
|
|
2550
|
+
depth++;
|
|
2551
|
+
} else if (arrayBody[i] === "}") {
|
|
2552
|
+
depth--;
|
|
2553
|
+
if (depth === 0 && start >= 0) {
|
|
2554
|
+
objects.push(arrayBody.slice(start, i + 1));
|
|
2555
|
+
start = -1;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return objects;
|
|
2560
|
+
}
|
|
2561
|
+
function detectSeededArrays(content, sourceFile) {
|
|
2562
|
+
const results = [];
|
|
2563
|
+
const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
|
|
2564
|
+
let fm;
|
|
2565
|
+
while ((fm = forOfRe.exec(content)) !== null) {
|
|
2566
|
+
const arrayName = fm[1];
|
|
2567
|
+
const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
|
|
2568
|
+
const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
|
|
2569
|
+
if (!prismaMatch) continue;
|
|
2570
|
+
results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
|
|
2571
|
+
}
|
|
2572
|
+
return results;
|
|
2573
|
+
}
|
|
2574
|
+
function pickIdField(props) {
|
|
2575
|
+
for (const key of ["key", "slug", "id", "name", "templateId"]) {
|
|
2576
|
+
if (props[key]) return props[key];
|
|
2577
|
+
}
|
|
2578
|
+
return null;
|
|
2579
|
+
}
|
|
2580
|
+
function pickNameField(props) {
|
|
2581
|
+
for (const key of ["name", "slug", "key", "id"]) {
|
|
2582
|
+
if (props[key]) return props[key];
|
|
2583
|
+
}
|
|
2584
|
+
return null;
|
|
2585
|
+
}
|
|
2586
|
+
function modelToNodeType(model) {
|
|
2587
|
+
return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
|
|
2588
|
+
}
|
|
2589
|
+
function extractSeedData(rootDir) {
|
|
2590
|
+
const nodes = [];
|
|
2591
|
+
const edges = [];
|
|
2592
|
+
const seedFiles = [
|
|
2593
|
+
(0, import_node_path10.join)(rootDir, "prisma", "seed.ts"),
|
|
2594
|
+
(0, import_node_path10.join)(rootDir, "prisma", "seed.js"),
|
|
2595
|
+
(0, import_node_path10.join)(rootDir, "src", "server", "lib", "system-tags.ts")
|
|
2596
|
+
].filter(import_node_fs10.existsSync);
|
|
2597
|
+
const useTreeSitter = tryLoadTreeSitter();
|
|
2598
|
+
for (const filePath of seedFiles) {
|
|
2599
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2600
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2601
|
+
const seeded = detectSeededArrays(content, relPath);
|
|
2602
|
+
let astRoot = null;
|
|
2603
|
+
if (useTreeSitter && parseCode) {
|
|
2604
|
+
try {
|
|
2605
|
+
const tree = parseCode(content);
|
|
2606
|
+
astRoot = tree.rootNode;
|
|
2607
|
+
} catch {
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
for (const { arrayName, prismaModel, sourceFile } of seeded) {
|
|
2611
|
+
const nodeType = modelToNodeType(prismaModel);
|
|
2612
|
+
const scope = classifyScope(sourceFile, prismaModel);
|
|
2613
|
+
if (astRoot) {
|
|
2614
|
+
const arrayNode = findArrayDecl(astRoot, arrayName);
|
|
2615
|
+
if (!arrayNode) continue;
|
|
2616
|
+
for (const child of arrayNode.namedChildren) {
|
|
2617
|
+
if (child.type !== "object") continue;
|
|
2618
|
+
const props = extractPropsFromObjectNode(child);
|
|
2619
|
+
const idVal = pickIdField(props);
|
|
2620
|
+
if (!idVal) continue;
|
|
2621
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2622
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2623
|
+
const { scope: _seedScope, ...safeProps } = props;
|
|
2624
|
+
nodes.push({
|
|
2625
|
+
id: nodeId,
|
|
2626
|
+
type: nodeType,
|
|
2627
|
+
name: nameVal,
|
|
2628
|
+
value: idVal,
|
|
2629
|
+
model: prismaModel,
|
|
2630
|
+
...safeProps,
|
|
2631
|
+
seedScope: _seedScope ?? void 0,
|
|
2632
|
+
// preserve as seedScope
|
|
2633
|
+
scope,
|
|
2634
|
+
source: sourceFile
|
|
2635
|
+
});
|
|
2636
|
+
const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
|
|
2637
|
+
if (permsArrayNode) {
|
|
2638
|
+
const valNode = permsArrayNode.childForFieldName("value");
|
|
2639
|
+
if (valNode?.type === "array") {
|
|
2640
|
+
const permKeys = extractStringArrayFromNode(valNode);
|
|
2641
|
+
for (const pk of permKeys) {
|
|
2642
|
+
if (pk === "*") continue;
|
|
2643
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
} else {
|
|
2649
|
+
const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
|
|
2650
|
+
const cm = constRe.exec(content);
|
|
2651
|
+
if (!cm) continue;
|
|
2652
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2653
|
+
let depth = 1, i = bracketStart + 1;
|
|
2654
|
+
while (i < content.length && depth > 0) {
|
|
2655
|
+
if (content[i] === "[") depth++;
|
|
2656
|
+
else if (content[i] === "]") depth--;
|
|
2657
|
+
i++;
|
|
2658
|
+
}
|
|
2659
|
+
if (depth !== 0) continue;
|
|
2660
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2661
|
+
const objects = splitArrayObjectsRegex(body);
|
|
2662
|
+
for (const objStr of objects) {
|
|
2663
|
+
const props = extractObjectPropsRegex(objStr);
|
|
2664
|
+
const idVal = pickIdField(props);
|
|
2665
|
+
if (!idVal) continue;
|
|
2666
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2667
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2668
|
+
const { scope: _rScope, ...rSafeProps } = props;
|
|
2669
|
+
nodes.push({
|
|
2670
|
+
id: nodeId,
|
|
2671
|
+
type: nodeType,
|
|
2672
|
+
name: nameVal,
|
|
2673
|
+
value: idVal,
|
|
2674
|
+
model: prismaModel,
|
|
2675
|
+
...rSafeProps,
|
|
2676
|
+
seedScope: _rScope ?? void 0,
|
|
2677
|
+
scope,
|
|
2678
|
+
source: sourceFile
|
|
2679
|
+
});
|
|
2680
|
+
const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
|
|
2681
|
+
if (permArrayMatch) {
|
|
2682
|
+
for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
|
|
2683
|
+
if (pk === "*") continue;
|
|
2684
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return { nodes, edges };
|
|
2692
|
+
}
|
|
2693
|
+
function walkDir(dir, exts) {
|
|
2694
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
2695
|
+
const results = [];
|
|
2696
|
+
for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
|
|
2697
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2698
|
+
const full = (0, import_node_path10.join)(dir, entry.name);
|
|
2699
|
+
if (entry.isDirectory()) results.push(...walkDir(full, exts));
|
|
2700
|
+
else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
|
|
2701
|
+
}
|
|
2702
|
+
return results;
|
|
2703
|
+
}
|
|
2704
|
+
function extractConstants(rootDir) {
|
|
2705
|
+
const nodes = [];
|
|
2706
|
+
const srcDir = (0, import_node_path10.join)(rootDir, "src");
|
|
2707
|
+
if (!(0, import_node_fs10.existsSync)(srcDir)) return { nodes };
|
|
2708
|
+
for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
|
|
2709
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2710
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2711
|
+
const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
|
|
2712
|
+
let cm;
|
|
2713
|
+
while ((cm = constArrayRe.exec(content)) !== null) {
|
|
2714
|
+
const constName = cm[1];
|
|
2715
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2716
|
+
let depth = 1, i = bracketStart + 1;
|
|
2717
|
+
while (i < content.length && depth > 0) {
|
|
2718
|
+
if (content[i] === "[") depth++;
|
|
2719
|
+
else if (content[i] === "]") depth--;
|
|
2720
|
+
i++;
|
|
2721
|
+
}
|
|
2722
|
+
if (depth !== 0) continue;
|
|
2723
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2724
|
+
const stringValues = extractStringArrayRegex(body);
|
|
2725
|
+
const objectCount = splitArrayObjectsRegex(body).length;
|
|
2726
|
+
const valueCount = Math.max(stringValues.length, objectCount);
|
|
2727
|
+
if (valueCount < 2) continue;
|
|
2728
|
+
const scope = classifyScope(relPath);
|
|
2729
|
+
nodes.push({
|
|
2730
|
+
id: `const:${constName}`,
|
|
2731
|
+
type: "constant",
|
|
2732
|
+
name: constName,
|
|
2733
|
+
valueCount,
|
|
2734
|
+
values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
|
|
2735
|
+
scope,
|
|
2736
|
+
source: relPath
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
return { nodes };
|
|
2741
|
+
}
|
|
2742
|
+
function detect4(rootDir) {
|
|
2743
|
+
return (0, import_node_fs10.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs10.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
|
|
2744
|
+
}
|
|
2745
|
+
function generate4(rootDir) {
|
|
2746
|
+
const enumResult = extractEnumValues(rootDir);
|
|
2747
|
+
const seedResult = extractSeedData(rootDir);
|
|
2748
|
+
const constResult = extractConstants(rootDir);
|
|
2749
|
+
const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
|
|
2750
|
+
const allEdges = [...enumResult.edges, ...seedResult.edges];
|
|
2751
|
+
const typeOrder = {
|
|
2752
|
+
enum_group: 0,
|
|
2753
|
+
enum_value: 1,
|
|
2754
|
+
seed_permission: 2,
|
|
2755
|
+
seed_role: 3,
|
|
2756
|
+
seed_tag: 4,
|
|
2757
|
+
seed_plan: 5,
|
|
2758
|
+
seed_provider: 6,
|
|
2759
|
+
constant: 7
|
|
2760
|
+
};
|
|
2761
|
+
allNodes.sort((a, b) => {
|
|
2762
|
+
const ta = typeOrder[a.type] ?? 8;
|
|
2763
|
+
const tb = typeOrder[b.type] ?? 8;
|
|
2764
|
+
if (ta !== tb) return ta - tb;
|
|
2765
|
+
return a.name.localeCompare(b.name);
|
|
2766
|
+
});
|
|
2767
|
+
const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
|
|
2768
|
+
const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
|
|
2769
|
+
const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
|
|
2770
|
+
const constNodes = allNodes.filter((n) => n.type === "constant").length;
|
|
2771
|
+
const byScope = {};
|
|
2772
|
+
for (const n of allNodes) {
|
|
2773
|
+
const s = n.scope ?? "unknown";
|
|
2774
|
+
byScope[s] = (byScope[s] ?? 0) + 1;
|
|
2775
|
+
}
|
|
2776
|
+
return {
|
|
2777
|
+
metadata: {
|
|
2778
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2779
|
+
scope: "static-values",
|
|
2780
|
+
layer: "static",
|
|
2781
|
+
enum_groups: enumGroups,
|
|
2782
|
+
enum_values: enumValues,
|
|
2783
|
+
seed_records: seedNodes,
|
|
2784
|
+
constants: constNodes,
|
|
2785
|
+
total_nodes: allNodes.length,
|
|
2786
|
+
total_edges: allEdges.length,
|
|
2787
|
+
parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
|
|
2788
|
+
},
|
|
2789
|
+
nodes: allNodes,
|
|
2790
|
+
edges: allEdges,
|
|
2791
|
+
cross_refs: [],
|
|
2792
|
+
contradictions: [],
|
|
2793
|
+
warnings: [],
|
|
2794
|
+
flagged_edges: [],
|
|
2795
|
+
patterns: {
|
|
2796
|
+
by_type: {
|
|
2797
|
+
enum_group: enumGroups,
|
|
2798
|
+
enum_value: enumValues,
|
|
2799
|
+
seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
|
|
2800
|
+
seed_role: allNodes.filter((n) => n.type === "seed_role").length,
|
|
2801
|
+
seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
|
|
2802
|
+
seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
|
|
2803
|
+
seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
|
|
2804
|
+
constant: constNodes
|
|
2805
|
+
},
|
|
2806
|
+
by_scope: byScope
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
var import_node_fs10, import_node_path10, parseCode, SHARED_MODELS, DB_MODELS, staticValuesParser;
|
|
2811
|
+
var init_static_values = __esm({
|
|
2812
|
+
"src/server/graph/parsers/static/static-values.ts"() {
|
|
2813
|
+
"use strict";
|
|
2814
|
+
import_node_fs10 = require("node:fs");
|
|
2815
|
+
import_node_path10 = require("node:path");
|
|
2816
|
+
parseCode = null;
|
|
2817
|
+
SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
|
|
2818
|
+
DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
|
|
2819
|
+
staticValuesParser = {
|
|
2820
|
+
id: "static-values",
|
|
2821
|
+
layer: "static",
|
|
2822
|
+
detect: detect4,
|
|
2823
|
+
generate: generate4
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
|
|
2828
|
+
// src/server/graph/parsers/crosslayer/static-ref-scanner.ts
|
|
2829
|
+
function walk4(dir, exts) {
|
|
2830
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return [];
|
|
2831
|
+
const results = [];
|
|
2832
|
+
function recurse(d) {
|
|
2833
|
+
for (const entry of (0, import_node_fs11.readdirSync)(d, { withFileTypes: true })) {
|
|
2834
|
+
const full = (0, import_node_path11.join)(d, entry.name);
|
|
2835
|
+
if (entry.isDirectory()) {
|
|
2836
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2837
|
+
recurse(full);
|
|
2838
|
+
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
|
2839
|
+
results.push(full);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
recurse(dir);
|
|
2844
|
+
return results;
|
|
2845
|
+
}
|
|
2846
|
+
function isInCommentOrType(node) {
|
|
2847
|
+
let current = node.parent;
|
|
2848
|
+
while (current) {
|
|
2849
|
+
if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
|
|
2850
|
+
return true;
|
|
2851
|
+
}
|
|
2852
|
+
current = current.parent;
|
|
2853
|
+
}
|
|
2854
|
+
return false;
|
|
2855
|
+
}
|
|
2856
|
+
function collectStaticRefs(root, valueLookup) {
|
|
2857
|
+
const refs = [];
|
|
2858
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2859
|
+
function visit(node) {
|
|
2860
|
+
if (node.type === "string_fragment") {
|
|
2861
|
+
const val = node.text;
|
|
2862
|
+
if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
|
|
2863
|
+
const targets = valueLookup.get(val);
|
|
2864
|
+
if (targets && !isInCommentOrType(node)) {
|
|
2865
|
+
for (const t of targets) {
|
|
2866
|
+
const key = t;
|
|
2867
|
+
if (!seen.has(key)) {
|
|
2868
|
+
seen.add(key);
|
|
2869
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
if (node.type === "member_expression") {
|
|
2876
|
+
const objNode = node.namedChildren.find((c) => c.type === "identifier");
|
|
2877
|
+
const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
|
|
2878
|
+
if (objNode && propNode) {
|
|
2879
|
+
const combined = `${objNode.text}.${propNode.text}`;
|
|
2880
|
+
const targets = valueLookup.get(propNode.text);
|
|
2881
|
+
const directTarget = valueLookup.get(combined);
|
|
2882
|
+
if (directTarget && !isInCommentOrType(node)) {
|
|
2883
|
+
for (const t of directTarget) {
|
|
2884
|
+
if (!seen.has(t)) {
|
|
2885
|
+
seen.add(t);
|
|
2886
|
+
refs.push({ value: combined, targetIds: [t] });
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
} else if (targets && !isInCommentOrType(node)) {
|
|
2890
|
+
for (const t of targets) {
|
|
2891
|
+
if (!seen.has(t)) {
|
|
2892
|
+
seen.add(t);
|
|
2893
|
+
refs.push({ value: propNode.text, targetIds: [t] });
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
for (const child of node.namedChildren) {
|
|
2900
|
+
visit(child);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
visit(root);
|
|
2904
|
+
return refs;
|
|
2905
|
+
}
|
|
2906
|
+
function collectStaticRefsRegex(content, valueLookup, allValues) {
|
|
2907
|
+
const refs = [];
|
|
2908
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2909
|
+
const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2910
|
+
const pattern = new RegExp(
|
|
2911
|
+
`(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
|
|
2912
|
+
"g"
|
|
2913
|
+
);
|
|
2914
|
+
let match;
|
|
2915
|
+
pattern.lastIndex = 0;
|
|
2916
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
2917
|
+
const val = match[1] ?? match[3];
|
|
2918
|
+
if (!val) continue;
|
|
2919
|
+
const targets = valueLookup.get(val);
|
|
2920
|
+
if (!targets) continue;
|
|
2921
|
+
for (const t of targets) {
|
|
2922
|
+
if (!seen.has(t)) {
|
|
2923
|
+
seen.add(t);
|
|
2924
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
return refs;
|
|
2929
|
+
}
|
|
2930
|
+
var import_node_fs11, import_node_path11, MIN_VALUE_LENGTH, SKIP_VALUES, staticRefScannerParser;
|
|
2931
|
+
var init_static_ref_scanner = __esm({
|
|
2932
|
+
"src/server/graph/parsers/crosslayer/static-ref-scanner.ts"() {
|
|
2933
|
+
"use strict";
|
|
2934
|
+
import_node_fs11 = require("node:fs");
|
|
2935
|
+
import_node_path11 = require("node:path");
|
|
2936
|
+
init_config();
|
|
2937
|
+
init_resolve_paths();
|
|
2938
|
+
MIN_VALUE_LENGTH = 4;
|
|
2939
|
+
SKIP_VALUES = /* @__PURE__ */ new Set([
|
|
2940
|
+
"true",
|
|
2941
|
+
"false",
|
|
2942
|
+
"null",
|
|
2943
|
+
"undefined",
|
|
2944
|
+
"none",
|
|
2945
|
+
"default",
|
|
2946
|
+
"name",
|
|
2947
|
+
"type",
|
|
2948
|
+
"data",
|
|
2949
|
+
"text",
|
|
2950
|
+
"info",
|
|
2951
|
+
"error",
|
|
2952
|
+
"open",
|
|
2953
|
+
"read",
|
|
2954
|
+
"user",
|
|
2955
|
+
"test",
|
|
2956
|
+
"json",
|
|
2957
|
+
"form"
|
|
2958
|
+
]);
|
|
2959
|
+
staticRefScannerParser = {
|
|
2960
|
+
id: "static-ref-scanner",
|
|
2961
|
+
layer: "crosslayer",
|
|
2962
|
+
concern: "static-ref",
|
|
2963
|
+
detect(rootDir) {
|
|
2964
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2965
|
+
return paths !== null;
|
|
2966
|
+
},
|
|
2967
|
+
generate(rootDir, layerOutputs) {
|
|
2968
|
+
const staticOutput = layerOutputs.get("static");
|
|
2969
|
+
if (!staticOutput || staticOutput.nodes.length === 0) {
|
|
2970
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2971
|
+
}
|
|
2972
|
+
const valueLookup = /* @__PURE__ */ new Map();
|
|
2973
|
+
for (const node of staticOutput.nodes) {
|
|
2974
|
+
const type = node.type;
|
|
2975
|
+
let valueStr = null;
|
|
2976
|
+
if (type === "enum_value") {
|
|
2977
|
+
valueStr = node.value;
|
|
2978
|
+
const fullId = node.id;
|
|
2979
|
+
if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
|
|
2980
|
+
valueLookup.get(fullId).push(node.id);
|
|
2981
|
+
} else if (type.startsWith("seed_")) {
|
|
2982
|
+
valueStr = node.value;
|
|
2983
|
+
}
|
|
2984
|
+
if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
|
|
2985
|
+
if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
|
|
2986
|
+
valueLookup.get(valueStr).push(node.id);
|
|
2987
|
+
}
|
|
2988
|
+
const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
|
|
2989
|
+
if (allValues.length === 0) {
|
|
2990
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2991
|
+
}
|
|
2992
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2993
|
+
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2994
|
+
const srcDir = paths.srcDir;
|
|
2995
|
+
const files = [
|
|
2996
|
+
...walk4((0, import_node_path11.join)(srcDir, "client"), [".ts", ".tsx"]),
|
|
2997
|
+
...walk4(paths.appDir, [".ts", ".tsx"]),
|
|
2998
|
+
...walk4((0, import_node_path11.join)(srcDir, "server"), [".ts", ".tsx"]),
|
|
2999
|
+
...walk4((0, import_node_path11.join)(srcDir, "lib"), [".ts", ".tsx"]),
|
|
3000
|
+
...walk4((0, import_node_path11.join)(srcDir, "config"), [".ts", ".tsx"])
|
|
3001
|
+
];
|
|
3002
|
+
const uiOutput = layerOutputs.get("ui");
|
|
3003
|
+
const apiOutput = layerOutputs.get("api");
|
|
3004
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
3005
|
+
const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
|
|
3006
|
+
let parseCode2 = null;
|
|
3007
|
+
try {
|
|
3008
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
3009
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
3010
|
+
parseCode2 = extractor.parseCodeTS;
|
|
3011
|
+
}
|
|
3012
|
+
} catch {
|
|
3013
|
+
}
|
|
3014
|
+
const crossRefs = [];
|
|
3015
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3016
|
+
let filesScanned = 0;
|
|
3017
|
+
for (const absPath of files) {
|
|
3018
|
+
const sourceId = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
3019
|
+
const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
|
|
3020
|
+
if (!sourceLayer) continue;
|
|
3021
|
+
const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
|
|
3022
|
+
filesScanned++;
|
|
3023
|
+
let fileRefs;
|
|
3024
|
+
if (parseCode2) {
|
|
3025
|
+
try {
|
|
3026
|
+
const tree = parseCode2(content);
|
|
3027
|
+
fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
|
|
3028
|
+
} catch {
|
|
3029
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
3030
|
+
}
|
|
3031
|
+
} else {
|
|
3032
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
3033
|
+
}
|
|
3034
|
+
for (const ref of fileRefs) {
|
|
3035
|
+
for (const targetId of ref.targetIds) {
|
|
3036
|
+
const key = `${sourceId}|${targetId}`;
|
|
3037
|
+
if (seen.has(key)) continue;
|
|
3038
|
+
seen.add(key);
|
|
3039
|
+
crossRefs.push({
|
|
3040
|
+
source: sourceId,
|
|
3041
|
+
target: targetId,
|
|
3042
|
+
type: "references_static",
|
|
3043
|
+
layer: "static"
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
return {
|
|
3049
|
+
cross_refs: crossRefs,
|
|
3050
|
+
flagged_edges: [],
|
|
3051
|
+
warnings: [],
|
|
3052
|
+
patterns: {
|
|
3053
|
+
files_scanned: filesScanned,
|
|
3054
|
+
static_values_tracked: valueLookup.size,
|
|
3055
|
+
references_found: crossRefs.length,
|
|
3056
|
+
parser: parseCode2 ? "tree-sitter" : "regex-fallback"
|
|
3057
|
+
}
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
});
|
|
3063
|
+
|
|
1907
3064
|
// src/server/graph/core/parser-registry.ts
|
|
3065
|
+
function isMultiLayerParser(p) {
|
|
3066
|
+
return "layers" in p && Array.isArray(p.layers);
|
|
3067
|
+
}
|
|
1908
3068
|
function registerBuiltins(registry, disabled) {
|
|
1909
3069
|
const builtins = [
|
|
1910
|
-
|
|
1911
|
-
nextjsRoutesParser,
|
|
3070
|
+
typescriptProjectParser,
|
|
1912
3071
|
prismaSchemaParser,
|
|
3072
|
+
sqlMigrationsParser,
|
|
3073
|
+
staticValuesParser,
|
|
1913
3074
|
fetchResolverParser,
|
|
1914
3075
|
apiAnnotationsParser,
|
|
1915
|
-
urlLiteralScannerParser
|
|
3076
|
+
urlLiteralScannerParser,
|
|
3077
|
+
staticRefScannerParser
|
|
1916
3078
|
];
|
|
1917
3079
|
for (const parser of builtins) {
|
|
1918
3080
|
if (disabled.has(parser.id)) continue;
|
|
@@ -1922,16 +3084,19 @@ function registerBuiltins(registry, disabled) {
|
|
|
1922
3084
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1923
3085
|
for (const entry of config.parsers?.custom ?? []) {
|
|
1924
3086
|
try {
|
|
1925
|
-
const absPath = (0,
|
|
3087
|
+
const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
|
|
1926
3088
|
const mod = require(absPath);
|
|
1927
3089
|
const parser = "default" in mod ? mod.default : mod;
|
|
1928
3090
|
if (disabled.has(parser.id)) continue;
|
|
1929
|
-
if (parser.layer !== entry.layer) {
|
|
3091
|
+
if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
|
|
1930
3092
|
process.stderr.write(
|
|
1931
3093
|
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1932
3094
|
`
|
|
1933
3095
|
);
|
|
1934
3096
|
}
|
|
3097
|
+
if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
|
|
3098
|
+
parser.concern = entry.concern;
|
|
3099
|
+
}
|
|
1935
3100
|
registry.register(parser);
|
|
1936
3101
|
} catch (err2) {
|
|
1937
3102
|
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
|
|
@@ -1946,20 +3111,23 @@ function createRegistry(config, rootDir) {
|
|
|
1946
3111
|
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1947
3112
|
return registry;
|
|
1948
3113
|
}
|
|
1949
|
-
var
|
|
3114
|
+
var import_node_path12, ParserRegistry;
|
|
1950
3115
|
var init_parser_registry = __esm({
|
|
1951
3116
|
"src/server/graph/core/parser-registry.ts"() {
|
|
1952
3117
|
"use strict";
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
init_nextjs_routes();
|
|
3118
|
+
import_node_path12 = require("node:path");
|
|
3119
|
+
init_typescript_project();
|
|
1956
3120
|
init_prisma_schema();
|
|
3121
|
+
init_sql_migrations();
|
|
1957
3122
|
init_fetch_resolver();
|
|
1958
3123
|
init_api_annotations();
|
|
1959
3124
|
init_url_literal_scanner();
|
|
3125
|
+
init_static_values();
|
|
3126
|
+
init_static_ref_scanner();
|
|
1960
3127
|
ParserRegistry = class {
|
|
1961
3128
|
constructor() {
|
|
1962
|
-
this.
|
|
3129
|
+
this.singleLayerParsers = /* @__PURE__ */ new Map();
|
|
3130
|
+
this.multiLayerParsers = [];
|
|
1963
3131
|
this.ids = /* @__PURE__ */ new Set();
|
|
1964
3132
|
}
|
|
1965
3133
|
register(parser) {
|
|
@@ -1967,19 +3135,44 @@ var init_parser_registry = __esm({
|
|
|
1967
3135
|
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1968
3136
|
}
|
|
1969
3137
|
this.ids.add(parser.id);
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
3138
|
+
if (isMultiLayerParser(parser)) {
|
|
3139
|
+
this.multiLayerParsers.push(parser);
|
|
3140
|
+
} else {
|
|
3141
|
+
const list = this.singleLayerParsers.get(parser.layer) ?? [];
|
|
3142
|
+
list.push(parser);
|
|
3143
|
+
this.singleLayerParsers.set(parser.layer, list);
|
|
3144
|
+
}
|
|
1973
3145
|
}
|
|
3146
|
+
/** Get single-layer parsers for a specific layer. */
|
|
1974
3147
|
getParsers(layer) {
|
|
1975
|
-
return this.
|
|
3148
|
+
return this.singleLayerParsers.get(layer) ?? [];
|
|
3149
|
+
}
|
|
3150
|
+
/** Get multi-layer parsers that can produce output for the given layer. */
|
|
3151
|
+
getMultiLayerParsersFor(layer) {
|
|
3152
|
+
return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
|
|
3153
|
+
}
|
|
3154
|
+
/** Get all multi-layer parsers. */
|
|
3155
|
+
getMultiLayerParsers() {
|
|
3156
|
+
return [...this.multiLayerParsers];
|
|
1976
3157
|
}
|
|
1977
3158
|
getCrossLayerParsers() {
|
|
1978
|
-
return this.
|
|
3159
|
+
return this.singleLayerParsers.get("crosslayer") ?? [];
|
|
3160
|
+
}
|
|
3161
|
+
/** All layers that registered parsers can produce (single + multi). */
|
|
3162
|
+
getAvailableLayers() {
|
|
3163
|
+
const layers = /* @__PURE__ */ new Set();
|
|
3164
|
+
for (const key of this.singleLayerParsers.keys()) {
|
|
3165
|
+
if (key !== "crosslayer") layers.add(key);
|
|
3166
|
+
}
|
|
3167
|
+
for (const mp of this.multiLayerParsers) {
|
|
3168
|
+
for (const l of mp.layers) layers.add(l);
|
|
3169
|
+
}
|
|
3170
|
+
return [...layers];
|
|
1979
3171
|
}
|
|
1980
3172
|
getAll() {
|
|
1981
3173
|
const all = [];
|
|
1982
|
-
for (const list of this.
|
|
3174
|
+
for (const list of this.singleLayerParsers.values()) all.push(...list);
|
|
3175
|
+
all.push(...this.multiLayerParsers);
|
|
1983
3176
|
return all;
|
|
1984
3177
|
}
|
|
1985
3178
|
};
|
|
@@ -2068,44 +3261,21 @@ function dedupCrossRefs(refs) {
|
|
|
2068
3261
|
}
|
|
2069
3262
|
return result;
|
|
2070
3263
|
}
|
|
2071
|
-
function applyCrossLayerResults(uiOutput, results
|
|
2072
|
-
const allCrossRefs = [...uiOutput.cross_refs];
|
|
2073
|
-
const allFlagged = [...uiOutput.flagged_edges];
|
|
2074
|
-
const allWarnings = [...uiOutput.warnings];
|
|
2075
|
-
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
2076
|
-
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
2077
|
-
if (primaryResult) {
|
|
2078
|
-
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
2079
|
-
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
2080
|
-
allWarnings.push(...primaryResult.output.warnings);
|
|
2081
|
-
}
|
|
2082
|
-
const primarySet = new Set(
|
|
2083
|
-
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
2084
|
-
);
|
|
2085
|
-
for (const sec of secondaryResults) {
|
|
2086
|
-
for (const ref of sec.output.cross_refs) {
|
|
2087
|
-
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
2088
|
-
if (primarySet.has(key)) {
|
|
2089
|
-
allCrossRefs.push(ref);
|
|
2090
|
-
} else {
|
|
2091
|
-
allFlagged.push({
|
|
2092
|
-
source: ref.source,
|
|
2093
|
-
target: ref.target,
|
|
2094
|
-
type: "out_of_pattern",
|
|
2095
|
-
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
2096
|
-
confidence: "medium"
|
|
2097
|
-
});
|
|
2098
|
-
allCrossRefs.push(ref);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
allFlagged.push(...sec.output.flagged_edges);
|
|
2102
|
-
allWarnings.push(...sec.output.warnings);
|
|
2103
|
-
}
|
|
3264
|
+
function applyCrossLayerResults(uiOutput, results) {
|
|
2104
3265
|
return {
|
|
2105
3266
|
...uiOutput,
|
|
2106
|
-
cross_refs: dedupCrossRefs(
|
|
2107
|
-
|
|
2108
|
-
|
|
3267
|
+
cross_refs: dedupCrossRefs([
|
|
3268
|
+
...uiOutput.cross_refs,
|
|
3269
|
+
...results.flatMap((r) => r.output.cross_refs)
|
|
3270
|
+
]),
|
|
3271
|
+
flagged_edges: [
|
|
3272
|
+
...uiOutput.flagged_edges,
|
|
3273
|
+
...results.flatMap((r) => r.output.flagged_edges)
|
|
3274
|
+
],
|
|
3275
|
+
warnings: [
|
|
3276
|
+
...uiOutput.warnings,
|
|
3277
|
+
...results.flatMap((r) => r.output.warnings)
|
|
3278
|
+
]
|
|
2109
3279
|
};
|
|
2110
3280
|
}
|
|
2111
3281
|
var init_merge = __esm({
|
|
@@ -2116,10 +3286,10 @@ var init_merge = __esm({
|
|
|
2116
3286
|
|
|
2117
3287
|
// src/server/graph/core/graph-builder.ts
|
|
2118
3288
|
function readGraphFromDisk(rootDir, layer) {
|
|
2119
|
-
const filePath = (0,
|
|
2120
|
-
if (!(0,
|
|
3289
|
+
const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3290
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) return null;
|
|
2121
3291
|
try {
|
|
2122
|
-
return JSON.parse((0,
|
|
3292
|
+
return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
|
|
2123
3293
|
} catch {
|
|
2124
3294
|
return null;
|
|
2125
3295
|
}
|
|
@@ -2127,26 +3297,31 @@ function readGraphFromDisk(rootDir, layer) {
|
|
|
2127
3297
|
function generateLayer(rootDir, layer) {
|
|
2128
3298
|
const config = loadConfig(rootDir);
|
|
2129
3299
|
const registry = createRegistry(config, rootDir);
|
|
2130
|
-
const parsers = registry.getParsers(layer);
|
|
2131
3300
|
const outputs = [];
|
|
2132
|
-
for (const parser of
|
|
3301
|
+
for (const parser of registry.getParsers(layer)) {
|
|
2133
3302
|
if (!parser.detect(rootDir)) continue;
|
|
2134
3303
|
outputs.push(parser.generate(rootDir));
|
|
2135
3304
|
}
|
|
3305
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3306
|
+
if (!mp.detect(rootDir)) continue;
|
|
3307
|
+
const multiOutput = mp.generate(rootDir);
|
|
3308
|
+
const layerOutput = multiOutput.get(layer);
|
|
3309
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3310
|
+
}
|
|
2136
3311
|
if (outputs.length === 0) return null;
|
|
2137
3312
|
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
2138
3313
|
if (layer === "ui") {
|
|
2139
3314
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
2140
3315
|
layerOutputs.set("ui", merged);
|
|
2141
|
-
for (const otherLayer of
|
|
3316
|
+
for (const otherLayer of registry.getAvailableLayers()) {
|
|
3317
|
+
if (otherLayer === "ui") continue;
|
|
2142
3318
|
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
2143
3319
|
if (existing) layerOutputs.set(otherLayer, existing);
|
|
2144
3320
|
}
|
|
2145
3321
|
const crossParsers = registry.getCrossLayerParsers();
|
|
2146
|
-
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
2147
3322
|
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
2148
3323
|
if (crossResults.length > 0) {
|
|
2149
|
-
merged = applyCrossLayerResults(merged, crossResults
|
|
3324
|
+
merged = applyCrossLayerResults(merged, crossResults);
|
|
2150
3325
|
}
|
|
2151
3326
|
}
|
|
2152
3327
|
return {
|
|
@@ -2159,16 +3334,29 @@ function generateLayer(rootDir, layer) {
|
|
|
2159
3334
|
function generateAll(rootDir) {
|
|
2160
3335
|
const config = loadConfig(rootDir);
|
|
2161
3336
|
const registry = createRegistry(config, rootDir);
|
|
2162
|
-
const
|
|
3337
|
+
const allLayers = registry.getAvailableLayers();
|
|
3338
|
+
const layerOrder = [
|
|
3339
|
+
...allLayers.filter((l) => l !== "ui"),
|
|
3340
|
+
...allLayers.filter((l) => l === "ui")
|
|
3341
|
+
];
|
|
2163
3342
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
2164
3343
|
const results = [];
|
|
3344
|
+
const multiLayerResults = /* @__PURE__ */ new Map();
|
|
2165
3345
|
for (const layer of layerOrder) {
|
|
2166
|
-
const parsers = registry.getParsers(layer);
|
|
2167
3346
|
const outputs = [];
|
|
2168
|
-
for (const parser of
|
|
3347
|
+
for (const parser of registry.getParsers(layer)) {
|
|
2169
3348
|
if (!parser.detect(rootDir)) continue;
|
|
2170
3349
|
outputs.push(parser.generate(rootDir));
|
|
2171
3350
|
}
|
|
3351
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3352
|
+
if (!mp.detect(rootDir)) continue;
|
|
3353
|
+
if (!multiLayerResults.has(mp.id)) {
|
|
3354
|
+
multiLayerResults.set(mp.id, mp.generate(rootDir));
|
|
3355
|
+
}
|
|
3356
|
+
const cached = multiLayerResults.get(mp.id);
|
|
3357
|
+
const layerOutput = cached.get(layer);
|
|
3358
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3359
|
+
}
|
|
2172
3360
|
if (outputs.length === 0) continue;
|
|
2173
3361
|
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
2174
3362
|
layerOutputs.set(layer, merged);
|
|
@@ -2180,11 +3368,10 @@ function generateAll(rootDir) {
|
|
|
2180
3368
|
});
|
|
2181
3369
|
}
|
|
2182
3370
|
const crossParsers = registry.getCrossLayerParsers();
|
|
2183
|
-
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
2184
3371
|
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
2185
3372
|
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
2186
3373
|
const uiOutput = layerOutputs.get("ui");
|
|
2187
|
-
const merged = applyCrossLayerResults(uiOutput, crossResults
|
|
3374
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults);
|
|
2188
3375
|
layerOutputs.set("ui", merged);
|
|
2189
3376
|
const uiResult = results.find((r) => r.layer === "ui");
|
|
2190
3377
|
if (uiResult) {
|
|
@@ -2194,14 +3381,16 @@ function generateAll(rootDir) {
|
|
|
2194
3381
|
}
|
|
2195
3382
|
}
|
|
2196
3383
|
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
2197
|
-
|
|
3384
|
+
const wellKnownOrder = ["ui", "api", "db"];
|
|
3385
|
+
const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
|
|
3386
|
+
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
2198
3387
|
}
|
|
2199
|
-
var
|
|
3388
|
+
var import_node_fs12, import_node_path13;
|
|
2200
3389
|
var init_graph_builder = __esm({
|
|
2201
3390
|
"src/server/graph/core/graph-builder.ts"() {
|
|
2202
3391
|
"use strict";
|
|
2203
|
-
|
|
2204
|
-
|
|
3392
|
+
import_node_fs12 = require("node:fs");
|
|
3393
|
+
import_node_path13 = require("node:path");
|
|
2205
3394
|
init_config();
|
|
2206
3395
|
init_parser_registry();
|
|
2207
3396
|
init_merge();
|
|
@@ -2240,18 +3429,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
2240
3429
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
2241
3430
|
const searchDirs = [
|
|
2242
3431
|
rootDir,
|
|
2243
|
-
(0,
|
|
2244
|
-
(0,
|
|
2245
|
-
(0,
|
|
3432
|
+
(0, import_node_path14.join)(rootDir, "src"),
|
|
3433
|
+
(0, import_node_path14.join)(rootDir, "app"),
|
|
3434
|
+
(0, import_node_path14.join)(rootDir, "lib")
|
|
2246
3435
|
];
|
|
2247
3436
|
for (const base of searchDirs) {
|
|
2248
3437
|
for (const convention of conventionDirs) {
|
|
2249
|
-
const dir = (0,
|
|
2250
|
-
if (!(0,
|
|
3438
|
+
const dir = (0, import_node_path14.join)(base, convention);
|
|
3439
|
+
if (!(0, import_node_fs13.existsSync)(dir)) continue;
|
|
2251
3440
|
try {
|
|
2252
|
-
const stat = (0,
|
|
3441
|
+
const stat = (0, import_node_fs13.statSync)(dir);
|
|
2253
3442
|
if (!stat.isDirectory()) continue;
|
|
2254
|
-
const entries = (0,
|
|
3443
|
+
const entries = (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
2255
3444
|
if (entries.length > 0) {
|
|
2256
3445
|
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
2257
3446
|
result.set(relPath, entries);
|
|
@@ -2323,12 +3512,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
|
|
|
2323
3512
|
}
|
|
2324
3513
|
return "root";
|
|
2325
3514
|
}
|
|
2326
|
-
var
|
|
3515
|
+
var import_node_fs13, import_node_path14, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
2327
3516
|
var init_module_tagger = __esm({
|
|
2328
3517
|
"src/server/graph/taggers/module-tagger.ts"() {
|
|
2329
3518
|
"use strict";
|
|
2330
|
-
|
|
2331
|
-
|
|
3519
|
+
import_node_fs13 = require("node:fs");
|
|
3520
|
+
import_node_path14 = require("node:path");
|
|
2332
3521
|
CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
2333
3522
|
GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
2334
3523
|
// JS/TS
|
|
@@ -2540,7 +3729,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
2540
3729
|
for (const entry of config.taggers?.custom ?? []) {
|
|
2541
3730
|
if (disabled.has(entry.id)) continue;
|
|
2542
3731
|
try {
|
|
2543
|
-
const absPath = (0,
|
|
3732
|
+
const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
|
|
2544
3733
|
const mod = require(absPath);
|
|
2545
3734
|
const tagger = "default" in mod ? mod.default : mod;
|
|
2546
3735
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -2561,11 +3750,11 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
2561
3750
|
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
2562
3751
|
return registry;
|
|
2563
3752
|
}
|
|
2564
|
-
var
|
|
3753
|
+
var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
|
|
2565
3754
|
var init_tagger_registry = __esm({
|
|
2566
3755
|
"src/server/graph/core/tagger-registry.ts"() {
|
|
2567
3756
|
"use strict";
|
|
2568
|
-
|
|
3757
|
+
import_node_path15 = require("node:path");
|
|
2569
3758
|
init_module_tagger();
|
|
2570
3759
|
init_screen_tagger();
|
|
2571
3760
|
TaggerRegistry = class {
|
|
@@ -2593,18 +3782,18 @@ var init_tagger_registry = __esm({
|
|
|
2593
3782
|
|
|
2594
3783
|
// src/server/graph/core/tag-store.ts
|
|
2595
3784
|
function tagsFilePath(rootDir) {
|
|
2596
|
-
return (0,
|
|
3785
|
+
return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
2597
3786
|
}
|
|
2598
3787
|
function readTagStore(rootDir) {
|
|
2599
3788
|
const filePath = tagsFilePath(rootDir);
|
|
2600
|
-
if (!(0,
|
|
2601
|
-
const stat = (0,
|
|
3789
|
+
if (!(0, import_node_fs14.existsSync)(filePath)) return {};
|
|
3790
|
+
const stat = (0, import_node_fs14.statSync)(filePath);
|
|
2602
3791
|
const cached = tagCache.get(filePath);
|
|
2603
3792
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2604
3793
|
return cached.store;
|
|
2605
3794
|
}
|
|
2606
3795
|
try {
|
|
2607
|
-
const content = (0,
|
|
3796
|
+
const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
|
|
2608
3797
|
const store = JSON.parse(content);
|
|
2609
3798
|
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
2610
3799
|
return store;
|
|
@@ -2614,15 +3803,15 @@ function readTagStore(rootDir) {
|
|
|
2614
3803
|
}
|
|
2615
3804
|
function writeTagStore(rootDir, store) {
|
|
2616
3805
|
const filePath = tagsFilePath(rootDir);
|
|
2617
|
-
const dir = (0,
|
|
2618
|
-
(0,
|
|
3806
|
+
const dir = (0, import_node_path16.dirname)(filePath);
|
|
3807
|
+
(0, import_node_fs14.mkdirSync)(dir, { recursive: true });
|
|
2619
3808
|
const cleaned = {};
|
|
2620
3809
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
2621
3810
|
if (Object.keys(tags).length > 0) {
|
|
2622
3811
|
cleaned[nodeId] = tags;
|
|
2623
3812
|
}
|
|
2624
3813
|
}
|
|
2625
|
-
(0,
|
|
3814
|
+
(0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
2626
3815
|
tagCache.delete(filePath);
|
|
2627
3816
|
}
|
|
2628
3817
|
function setTag(rootDir, nodeId, key, value) {
|
|
@@ -2640,12 +3829,12 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
2640
3829
|
}
|
|
2641
3830
|
writeTagStore(rootDir, store);
|
|
2642
3831
|
}
|
|
2643
|
-
var
|
|
3832
|
+
var import_node_fs14, import_node_path16, TAGS_FILENAME, GRAPHS_DIR, tagCache;
|
|
2644
3833
|
var init_tag_store = __esm({
|
|
2645
3834
|
"src/server/graph/core/tag-store.ts"() {
|
|
2646
3835
|
"use strict";
|
|
2647
|
-
|
|
2648
|
-
|
|
3836
|
+
import_node_fs14 = require("node:fs");
|
|
3837
|
+
import_node_path16 = require("node:path");
|
|
2649
3838
|
TAGS_FILENAME = "tags.json";
|
|
2650
3839
|
GRAPHS_DIR = ".launchsecure/graphs";
|
|
2651
3840
|
tagCache = /* @__PURE__ */ new Map();
|
|
@@ -2653,18 +3842,23 @@ var init_tag_store = __esm({
|
|
|
2653
3842
|
});
|
|
2654
3843
|
|
|
2655
3844
|
// src/server/graph/index.ts
|
|
3845
|
+
function getAvailableLayers(rootDir) {
|
|
3846
|
+
const dir = (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
|
|
3847
|
+
if (!(0, import_node_fs15.existsSync)(dir)) return [];
|
|
3848
|
+
return (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
|
|
3849
|
+
}
|
|
2656
3850
|
function graphsDir(rootDir) {
|
|
2657
|
-
return (0,
|
|
3851
|
+
return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
|
|
2658
3852
|
}
|
|
2659
3853
|
function graphFilePath(rootDir, layer) {
|
|
2660
|
-
return (0,
|
|
3854
|
+
return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
|
|
2661
3855
|
}
|
|
2662
3856
|
function tagsFilePath2(rootDir) {
|
|
2663
|
-
return (0,
|
|
3857
|
+
return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
|
|
2664
3858
|
}
|
|
2665
3859
|
function getMtimeMs(filePath) {
|
|
2666
|
-
if (!(0,
|
|
2667
|
-
return (0,
|
|
3860
|
+
if (!(0, import_node_fs15.existsSync)(filePath)) return 0;
|
|
3861
|
+
return (0, import_node_fs15.statSync)(filePath).mtimeMs;
|
|
2668
3862
|
}
|
|
2669
3863
|
function invalidateCache(filePath) {
|
|
2670
3864
|
graphCache.delete(filePath);
|
|
@@ -2703,20 +3897,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
2703
3897
|
}
|
|
2704
3898
|
function readGraphRaw(rootDir, layer) {
|
|
2705
3899
|
const filePath = graphFilePath(rootDir, layer);
|
|
2706
|
-
if (!(0,
|
|
2707
|
-
const stat = (0,
|
|
3900
|
+
if (!(0, import_node_fs15.existsSync)(filePath)) return null;
|
|
3901
|
+
const stat = (0, import_node_fs15.statSync)(filePath);
|
|
2708
3902
|
const cached = graphCache.get(filePath);
|
|
2709
3903
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2710
3904
|
return cached.graph;
|
|
2711
3905
|
}
|
|
2712
|
-
const content = (0,
|
|
3906
|
+
const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
|
|
2713
3907
|
const graph = JSON.parse(content);
|
|
2714
3908
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
2715
3909
|
return graph;
|
|
2716
3910
|
}
|
|
2717
3911
|
function readGraph(rootDir, layer) {
|
|
2718
3912
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
2719
|
-
if (!(0,
|
|
3913
|
+
if (!(0, import_node_fs15.existsSync)(rawFilePath)) return null;
|
|
2720
3914
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
2721
3915
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
2722
3916
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -2732,7 +3926,7 @@ function readGraph(rootDir, layer) {
|
|
|
2732
3926
|
}
|
|
2733
3927
|
function readAllGraphs(rootDir) {
|
|
2734
3928
|
const result = {};
|
|
2735
|
-
for (const layer of
|
|
3929
|
+
for (const layer of getAvailableLayers(rootDir)) {
|
|
2736
3930
|
const graph = readGraph(rootDir, layer);
|
|
2737
3931
|
if (graph) result[layer] = graph;
|
|
2738
3932
|
}
|
|
@@ -2746,22 +3940,22 @@ async function generateGraph(rootDir, layer) {
|
|
|
2746
3940
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
2747
3941
|
});
|
|
2748
3942
|
const dir = graphsDir(rootDir);
|
|
2749
|
-
(0,
|
|
3943
|
+
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
2750
3944
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
2751
3945
|
for (const result of results) {
|
|
2752
3946
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
2753
|
-
(0,
|
|
3947
|
+
(0, import_node_fs15.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
2754
3948
|
invalidateCache(filePath);
|
|
2755
3949
|
invalidateTaggedCache(rootDir, result.layer);
|
|
2756
3950
|
}
|
|
2757
3951
|
return results;
|
|
2758
3952
|
}
|
|
2759
|
-
var
|
|
3953
|
+
var import_node_fs15, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
|
|
2760
3954
|
var init_graph = __esm({
|
|
2761
3955
|
"src/server/graph/index.ts"() {
|
|
2762
3956
|
"use strict";
|
|
2763
|
-
|
|
2764
|
-
|
|
3957
|
+
import_node_fs15 = require("node:fs");
|
|
3958
|
+
import_node_path17 = require("node:path");
|
|
2765
3959
|
init_graph_builder();
|
|
2766
3960
|
init_config();
|
|
2767
3961
|
init_tagger_registry();
|
|
@@ -2769,12 +3963,294 @@ var init_graph = __esm({
|
|
|
2769
3963
|
init_ts_extractor();
|
|
2770
3964
|
init_tag_store();
|
|
2771
3965
|
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2772
|
-
LAYERS = ["ui", "api", "db"];
|
|
2773
3966
|
graphCache = /* @__PURE__ */ new Map();
|
|
2774
3967
|
taggedCache = /* @__PURE__ */ new Map();
|
|
2775
3968
|
}
|
|
2776
3969
|
});
|
|
2777
3970
|
|
|
3971
|
+
// src/server/graph/core/audit-core.ts
|
|
3972
|
+
function readGraphFile(rootDir, layer) {
|
|
3973
|
+
const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3974
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return null;
|
|
3975
|
+
try {
|
|
3976
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
|
|
3977
|
+
} catch {
|
|
3978
|
+
return null;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
function checkSchemaDrift(rootDir) {
|
|
3982
|
+
const findings = [];
|
|
3983
|
+
const db = readGraphFile(rootDir, "db");
|
|
3984
|
+
if (!db) {
|
|
3985
|
+
findings.push({ id: "no-db-graph", severity: "info", category: "schema_drift", title: "No DB graph", detail: "Run generate_graph first to populate the DB layer." });
|
|
3986
|
+
return buildReport("db", "schema_drift", findings);
|
|
3987
|
+
}
|
|
3988
|
+
for (const c of db.contradictions ?? []) {
|
|
3989
|
+
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
3990
|
+
findings.push({
|
|
3991
|
+
id: `drift:${c.entity}`,
|
|
3992
|
+
severity: isTableLevel ? "error" : "warning",
|
|
3993
|
+
category: "schema_drift",
|
|
3994
|
+
title: c.entity,
|
|
3995
|
+
detail: c.detail
|
|
3996
|
+
});
|
|
3997
|
+
}
|
|
3998
|
+
return buildReport("db", "schema_drift", findings);
|
|
3999
|
+
}
|
|
4000
|
+
function checkOrphanFks(rootDir) {
|
|
4001
|
+
const findings = [];
|
|
4002
|
+
const db = readGraphFile(rootDir, "db");
|
|
4003
|
+
if (!db) return buildReport("db", "orphan_fks", findings);
|
|
4004
|
+
for (const f of db.flagged_edges ?? []) {
|
|
4005
|
+
findings.push({
|
|
4006
|
+
id: `fk:${f.source}->${f.target}`,
|
|
4007
|
+
severity: "warning",
|
|
4008
|
+
category: "orphan_fks",
|
|
4009
|
+
title: `${f.source} \u2192 ${f.target}`,
|
|
4010
|
+
detail: f.label
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
return buildReport("db", "orphan_fks", findings);
|
|
4014
|
+
}
|
|
4015
|
+
function checkUnprotectedRoutes(rootDir) {
|
|
4016
|
+
const findings = [];
|
|
4017
|
+
const api = readGraphFile(rootDir, "api");
|
|
4018
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4019
|
+
if (!api) return buildReport("api", "unprotected_routes", findings);
|
|
4020
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
4021
|
+
let routePermsContent = "";
|
|
4022
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
4023
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
4024
|
+
}
|
|
4025
|
+
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
4026
|
+
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
4027
|
+
let rm;
|
|
4028
|
+
while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
|
|
4029
|
+
registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
|
|
4030
|
+
}
|
|
4031
|
+
for (const node of api.nodes) {
|
|
4032
|
+
if (node.type !== "endpoint") continue;
|
|
4033
|
+
const route = node.route ?? "";
|
|
4034
|
+
if (!route) continue;
|
|
4035
|
+
const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
|
|
4036
|
+
const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
|
|
4037
|
+
if (!isRegistered) {
|
|
4038
|
+
const methods = node.methods ?? [];
|
|
4039
|
+
findings.push({
|
|
4040
|
+
id: `unprotected:${node.id}`,
|
|
4041
|
+
severity: "warning",
|
|
4042
|
+
category: "unprotected_routes",
|
|
4043
|
+
title: `${methods.join(",")} ${route}`,
|
|
4044
|
+
detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
|
|
4045
|
+
file: node.id
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
return buildReport("api", "unprotected_routes", findings);
|
|
4050
|
+
}
|
|
4051
|
+
function routeMatchesPattern(route, pattern) {
|
|
4052
|
+
const routeParts = route.split("/");
|
|
4053
|
+
const patternParts = pattern.split("/");
|
|
4054
|
+
if (routeParts.length !== patternParts.length) return false;
|
|
4055
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
4056
|
+
const rp = routeParts[i];
|
|
4057
|
+
const pp = patternParts[i];
|
|
4058
|
+
if (rp === pp) continue;
|
|
4059
|
+
if (pp.startsWith("[") || pp.startsWith(":")) continue;
|
|
4060
|
+
if (rp.startsWith("[") || rp.startsWith(":")) continue;
|
|
4061
|
+
return false;
|
|
4062
|
+
}
|
|
4063
|
+
return true;
|
|
4064
|
+
}
|
|
4065
|
+
function checkDeadScreens(rootDir) {
|
|
4066
|
+
const findings = [];
|
|
4067
|
+
const ui = readGraphFile(rootDir, "ui");
|
|
4068
|
+
if (!ui) return buildReport("ui", "dead_screens", findings);
|
|
4069
|
+
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
4070
|
+
const navTargets = /* @__PURE__ */ new Set();
|
|
4071
|
+
for (const e of ui.edges) {
|
|
4072
|
+
if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
|
|
4073
|
+
navTargets.add(e.target);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
for (const cr of ui.cross_refs ?? []) {
|
|
4077
|
+
navTargets.add(cr.target);
|
|
4078
|
+
}
|
|
4079
|
+
for (const page of pages) {
|
|
4080
|
+
if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
|
|
4081
|
+
if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
|
|
4082
|
+
if (!navTargets.has(page.id)) {
|
|
4083
|
+
findings.push({
|
|
4084
|
+
id: `dead:${page.id}`,
|
|
4085
|
+
severity: "info",
|
|
4086
|
+
category: "dead_screens",
|
|
4087
|
+
title: page.name,
|
|
4088
|
+
detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
|
|
4089
|
+
file: page.id
|
|
4090
|
+
});
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
return buildReport("ui", "dead_screens", findings);
|
|
4094
|
+
}
|
|
4095
|
+
function checkUnenforcedPermissions(rootDir) {
|
|
4096
|
+
const findings = [];
|
|
4097
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4098
|
+
if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
|
|
4099
|
+
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
4100
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
4101
|
+
let routePermsContent = "";
|
|
4102
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
4103
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
4104
|
+
}
|
|
4105
|
+
for (const perm of permissions) {
|
|
4106
|
+
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
4107
|
+
if (!regex.test(routePermsContent)) {
|
|
4108
|
+
findings.push({
|
|
4109
|
+
id: `unenforced:${perm.key}`,
|
|
4110
|
+
severity: "warning",
|
|
4111
|
+
category: "unenforced_permissions",
|
|
4112
|
+
title: `${perm.name} (${perm.key})`,
|
|
4113
|
+
detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
return buildReport("static", "unenforced_permissions", findings);
|
|
4118
|
+
}
|
|
4119
|
+
function checkHardcodedValues(rootDir) {
|
|
4120
|
+
const findings = [];
|
|
4121
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4122
|
+
if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
|
|
4123
|
+
const knownValues = /* @__PURE__ */ new Set();
|
|
4124
|
+
for (const n of staticGraph.nodes) {
|
|
4125
|
+
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4126
|
+
}
|
|
4127
|
+
const api = readGraphFile(rootDir, "api");
|
|
4128
|
+
if (!api) return buildReport("static", "hardcoded_values", findings);
|
|
4129
|
+
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4130
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4131
|
+
for (const node of api.nodes) {
|
|
4132
|
+
if (node.type !== "endpoint") continue;
|
|
4133
|
+
const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
|
|
4134
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) continue;
|
|
4135
|
+
const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
|
|
4136
|
+
let m;
|
|
4137
|
+
allCapsRe.lastIndex = 0;
|
|
4138
|
+
while ((m = allCapsRe.exec(content)) !== null) {
|
|
4139
|
+
const val = m[1];
|
|
4140
|
+
if (knownValues.has(val)) continue;
|
|
4141
|
+
if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
|
|
4142
|
+
const key = `${node.id}:${val}`;
|
|
4143
|
+
if (seen.has(key)) continue;
|
|
4144
|
+
seen.add(key);
|
|
4145
|
+
findings.push({
|
|
4146
|
+
id: `hardcoded:${key}`,
|
|
4147
|
+
severity: "info",
|
|
4148
|
+
category: "hardcoded_values",
|
|
4149
|
+
title: `"${val}" in ${node.id}`,
|
|
4150
|
+
detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
|
|
4151
|
+
file: node.id
|
|
4152
|
+
});
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
return buildReport("static", "hardcoded_values", findings);
|
|
4156
|
+
}
|
|
4157
|
+
function buildReport(layer, check, findings) {
|
|
4158
|
+
return {
|
|
4159
|
+
layer,
|
|
4160
|
+
check,
|
|
4161
|
+
findings,
|
|
4162
|
+
summary: {
|
|
4163
|
+
errors: findings.filter((f) => f.severity === "error").length,
|
|
4164
|
+
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4165
|
+
info: findings.filter((f) => f.severity === "info").length
|
|
4166
|
+
},
|
|
4167
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
4170
|
+
function getAvailableChecks() {
|
|
4171
|
+
const result = {};
|
|
4172
|
+
for (const [layer, checks] of Object.entries(CHECKS)) {
|
|
4173
|
+
result[layer] = Object.keys(checks);
|
|
4174
|
+
}
|
|
4175
|
+
return result;
|
|
4176
|
+
}
|
|
4177
|
+
function runAudit(rootDir, layer, check) {
|
|
4178
|
+
if (layer === "all") {
|
|
4179
|
+
const reports = [];
|
|
4180
|
+
for (const [l, checks] of Object.entries(CHECKS)) {
|
|
4181
|
+
for (const fn of Object.values(checks)) {
|
|
4182
|
+
reports.push(fn(rootDir));
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
return reports;
|
|
4186
|
+
}
|
|
4187
|
+
const layerChecks = CHECKS[layer];
|
|
4188
|
+
if (!layerChecks) {
|
|
4189
|
+
return [{
|
|
4190
|
+
layer,
|
|
4191
|
+
check: "unknown",
|
|
4192
|
+
findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
|
|
4193
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4194
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4195
|
+
}];
|
|
4196
|
+
}
|
|
4197
|
+
if (check) {
|
|
4198
|
+
const fn = layerChecks[check];
|
|
4199
|
+
if (!fn) {
|
|
4200
|
+
return [{
|
|
4201
|
+
layer,
|
|
4202
|
+
check,
|
|
4203
|
+
findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
|
|
4204
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4205
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4206
|
+
}];
|
|
4207
|
+
}
|
|
4208
|
+
return [fn(rootDir)];
|
|
4209
|
+
}
|
|
4210
|
+
return Object.values(layerChecks).map((fn) => fn(rootDir));
|
|
4211
|
+
}
|
|
4212
|
+
function formatAsPrompt(reports) {
|
|
4213
|
+
const lines = [];
|
|
4214
|
+
for (const report of reports) {
|
|
4215
|
+
if (report.findings.length === 0) continue;
|
|
4216
|
+
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
4217
|
+
lines.push("");
|
|
4218
|
+
for (const f of report.findings) {
|
|
4219
|
+
const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
|
|
4220
|
+
const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
|
|
4221
|
+
lines.push(`- [${tag}] ${f.title}${filePart}`);
|
|
4222
|
+
lines.push(` ${f.detail}`);
|
|
4223
|
+
}
|
|
4224
|
+
lines.push("");
|
|
4225
|
+
}
|
|
4226
|
+
if (lines.length === 0) return "No audit findings.";
|
|
4227
|
+
return lines.join("\n");
|
|
4228
|
+
}
|
|
4229
|
+
var import_node_fs16, import_node_path18, CHECKS;
|
|
4230
|
+
var init_audit_core = __esm({
|
|
4231
|
+
"src/server/graph/core/audit-core.ts"() {
|
|
4232
|
+
"use strict";
|
|
4233
|
+
import_node_fs16 = require("node:fs");
|
|
4234
|
+
import_node_path18 = require("node:path");
|
|
4235
|
+
CHECKS = {
|
|
4236
|
+
db: {
|
|
4237
|
+
schema_drift: checkSchemaDrift,
|
|
4238
|
+
orphan_fks: checkOrphanFks
|
|
4239
|
+
},
|
|
4240
|
+
api: {
|
|
4241
|
+
unprotected_routes: checkUnprotectedRoutes
|
|
4242
|
+
},
|
|
4243
|
+
ui: {
|
|
4244
|
+
dead_screens: checkDeadScreens
|
|
4245
|
+
},
|
|
4246
|
+
static: {
|
|
4247
|
+
unenforced_permissions: checkUnenforcedPermissions,
|
|
4248
|
+
hardcoded_values: checkHardcodedValues
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
});
|
|
4253
|
+
|
|
2778
4254
|
// src/server/chart-serve.ts
|
|
2779
4255
|
var chart_serve_exports = {};
|
|
2780
4256
|
__export(chart_serve_exports, {
|
|
@@ -2787,31 +4263,39 @@ function randomPort() {
|
|
|
2787
4263
|
function findProjectRoot(startDir) {
|
|
2788
4264
|
let dir = startDir;
|
|
2789
4265
|
for (let i = 0; i < 8; i++) {
|
|
2790
|
-
const graphsDir2 =
|
|
2791
|
-
if (
|
|
2792
|
-
const parent =
|
|
4266
|
+
const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
|
|
4267
|
+
if (import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "ui.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "api.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "db.json"))) return dir;
|
|
4268
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2793
4269
|
if (parent === dir) break;
|
|
2794
4270
|
dir = parent;
|
|
2795
4271
|
}
|
|
2796
4272
|
dir = startDir;
|
|
2797
4273
|
for (let i = 0; i < 8; i++) {
|
|
2798
|
-
if (
|
|
2799
|
-
const parent =
|
|
4274
|
+
if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
|
|
4275
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2800
4276
|
if (parent === dir) break;
|
|
2801
4277
|
dir = parent;
|
|
2802
4278
|
}
|
|
2803
4279
|
return startDir;
|
|
2804
4280
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
if (!
|
|
2808
|
-
|
|
2809
|
-
|
|
4281
|
+
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
4282
|
+
const projectParam = url.searchParams.get("project");
|
|
4283
|
+
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
4284
|
+
const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
|
|
4285
|
+
if (!resolved.startsWith(monorepoRoot)) {
|
|
4286
|
+
throw new Error("Project path outside monorepo root");
|
|
4287
|
+
}
|
|
4288
|
+
return resolved;
|
|
4289
|
+
}
|
|
4290
|
+
async function buildMergedGraph(root) {
|
|
4291
|
+
let graphs = readAllGraphs(root);
|
|
4292
|
+
if (Object.keys(graphs).length === 0) {
|
|
4293
|
+
await generateGraph(root);
|
|
4294
|
+
graphs = readAllGraphs(root);
|
|
2810
4295
|
}
|
|
2811
4296
|
const nodes = [];
|
|
2812
4297
|
const rawLinks = [];
|
|
2813
|
-
const
|
|
2814
|
-
for (const layer of LAYERS2) {
|
|
4298
|
+
for (const layer of Object.keys(graphs)) {
|
|
2815
4299
|
const g = graphs[layer];
|
|
2816
4300
|
if (!g) continue;
|
|
2817
4301
|
for (const n of g.nodes) {
|
|
@@ -2848,16 +4332,16 @@ async function buildMergedGraph(projectRoot) {
|
|
|
2848
4332
|
};
|
|
2849
4333
|
}
|
|
2850
4334
|
function serveStatic(res, filePath) {
|
|
2851
|
-
if (!
|
|
2852
|
-
const ext =
|
|
4335
|
+
if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
|
|
4336
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2853
4337
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2854
4338
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
2855
|
-
|
|
4339
|
+
import_node_fs17.default.createReadStream(filePath).pipe(res);
|
|
2856
4340
|
return true;
|
|
2857
4341
|
}
|
|
2858
4342
|
function serveIndex(res, clientDir) {
|
|
2859
|
-
const indexPath =
|
|
2860
|
-
if (!
|
|
4343
|
+
const indexPath = import_node_path19.default.join(clientDir, "index.html");
|
|
4344
|
+
if (!import_node_fs17.default.existsSync(indexPath)) {
|
|
2861
4345
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2862
4346
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
2863
4347
|
return;
|
|
@@ -2909,19 +4393,40 @@ async function startChartServer(opts = {}) {
|
|
|
2909
4393
|
}
|
|
2910
4394
|
return { port: existing.port, url: existing.url };
|
|
2911
4395
|
}
|
|
2912
|
-
const clientDir = opts.clientDir ??
|
|
4396
|
+
const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
|
|
4397
|
+
const rootConfig = loadConfig(projectRoot);
|
|
4398
|
+
const projects = rootConfig.projects ?? [];
|
|
2913
4399
|
const server = import_node_http.default.createServer((req, res) => {
|
|
2914
4400
|
try {
|
|
2915
4401
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
4402
|
+
let reqRoot;
|
|
4403
|
+
try {
|
|
4404
|
+
reqRoot = resolveRequestRoot(url2, projectRoot, projects);
|
|
4405
|
+
} catch (err2) {
|
|
4406
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4407
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4408
|
+
return;
|
|
4409
|
+
}
|
|
4410
|
+
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
4411
|
+
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
4412
|
+
const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
|
|
4413
|
+
const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
|
|
4414
|
+
const hasNextConfig2 = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.ts")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.js")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.mjs"));
|
|
4415
|
+
return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
|
|
4416
|
+
}) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
|
|
4417
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4418
|
+
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
2916
4421
|
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2917
4422
|
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2918
4423
|
(async () => {
|
|
2919
|
-
if (regenerate) await generateGraph(
|
|
2920
|
-
const merged = await buildMergedGraph(
|
|
4424
|
+
if (regenerate) await generateGraph(reqRoot);
|
|
4425
|
+
const merged = await buildMergedGraph(reqRoot);
|
|
2921
4426
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2922
4427
|
res.end(JSON.stringify({
|
|
2923
4428
|
...merged,
|
|
2924
|
-
debug: { cwd, projectRoot }
|
|
4429
|
+
debug: { cwd, projectRoot: reqRoot }
|
|
2925
4430
|
}));
|
|
2926
4431
|
})().catch((e) => {
|
|
2927
4432
|
res.writeHead(500);
|
|
@@ -2930,15 +4435,15 @@ async function startChartServer(opts = {}) {
|
|
|
2930
4435
|
return;
|
|
2931
4436
|
}
|
|
2932
4437
|
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
2933
|
-
const graphs = readAllGraphs(
|
|
4438
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2934
4439
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2935
4440
|
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
2936
4441
|
return;
|
|
2937
4442
|
}
|
|
2938
4443
|
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2939
4444
|
(async () => {
|
|
2940
|
-
await generateGraph(
|
|
2941
|
-
const graphs = readAllGraphs(
|
|
4445
|
+
await generateGraph(reqRoot);
|
|
4446
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2942
4447
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2943
4448
|
res.end(JSON.stringify({
|
|
2944
4449
|
ok: true,
|
|
@@ -2954,41 +4459,49 @@ async function startChartServer(opts = {}) {
|
|
|
2954
4459
|
}
|
|
2955
4460
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
2956
4461
|
const relPath = url2.searchParams.get("path");
|
|
2957
|
-
if (!relPath || relPath.includes("..") ||
|
|
4462
|
+
if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
|
|
2958
4463
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2959
4464
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
2960
4465
|
return;
|
|
2961
4466
|
}
|
|
2962
|
-
const filePath =
|
|
2963
|
-
if (!filePath.startsWith(
|
|
4467
|
+
const filePath = import_node_path19.default.join(reqRoot, relPath);
|
|
4468
|
+
if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
|
|
2964
4469
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2965
4470
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
2966
4471
|
return;
|
|
2967
4472
|
}
|
|
2968
|
-
const ext =
|
|
4473
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2969
4474
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
2970
|
-
const content =
|
|
4475
|
+
const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
2971
4476
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2972
4477
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
2973
4478
|
return;
|
|
2974
4479
|
}
|
|
2975
4480
|
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
2976
4481
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2977
|
-
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
4482
|
+
res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
|
|
2978
4483
|
return;
|
|
2979
4484
|
}
|
|
2980
4485
|
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2981
|
-
const config2 = loadConfig(
|
|
2982
|
-
const
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
4486
|
+
const config2 = loadConfig(reqRoot);
|
|
4487
|
+
const registry = createRegistry(config2, reqRoot);
|
|
4488
|
+
const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
4489
|
+
const detection = [];
|
|
4490
|
+
for (const parser of registry.getAll()) {
|
|
4491
|
+
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
4492
|
+
const mp = parser;
|
|
4493
|
+
detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
|
|
4494
|
+
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
4495
|
+
const sp = parser;
|
|
4496
|
+
detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
const crosslayerParsers = {};
|
|
4500
|
+
for (const p of registry.getCrossLayerParsers()) {
|
|
4501
|
+
const concern = p.concern ?? "api-binding";
|
|
4502
|
+
if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
|
|
4503
|
+
crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
|
|
4504
|
+
}
|
|
2992
4505
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2993
4506
|
res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
|
|
2994
4507
|
return;
|
|
@@ -3001,8 +4514,8 @@ async function startChartServer(opts = {}) {
|
|
|
3001
4514
|
req.on("end", () => {
|
|
3002
4515
|
try {
|
|
3003
4516
|
const newConfig = JSON.parse(body);
|
|
3004
|
-
const configPath =
|
|
3005
|
-
|
|
4517
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4518
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
3006
4519
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3007
4520
|
res.end(JSON.stringify({ ok: true }));
|
|
3008
4521
|
} catch (err2) {
|
|
@@ -3013,7 +4526,7 @@ async function startChartServer(opts = {}) {
|
|
|
3013
4526
|
return;
|
|
3014
4527
|
}
|
|
3015
4528
|
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
3016
|
-
const config2 = loadConfig(
|
|
4529
|
+
const config2 = loadConfig(reqRoot);
|
|
3017
4530
|
const builtinTaggers = [
|
|
3018
4531
|
{ id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
|
|
3019
4532
|
{ id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
|
|
@@ -3033,10 +4546,10 @@ async function startChartServer(opts = {}) {
|
|
|
3033
4546
|
req.on("end", () => {
|
|
3034
4547
|
try {
|
|
3035
4548
|
const taggerConfig = JSON.parse(body);
|
|
3036
|
-
const config2 = loadConfig(
|
|
4549
|
+
const config2 = loadConfig(reqRoot);
|
|
3037
4550
|
config2.taggers = taggerConfig;
|
|
3038
|
-
const configPath =
|
|
3039
|
-
|
|
4551
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4552
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
3040
4553
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3041
4554
|
res.end(JSON.stringify({ ok: true }));
|
|
3042
4555
|
} catch (err2) {
|
|
@@ -3047,7 +4560,7 @@ async function startChartServer(opts = {}) {
|
|
|
3047
4560
|
return;
|
|
3048
4561
|
}
|
|
3049
4562
|
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
3050
|
-
const store = readTagStore(
|
|
4563
|
+
const store = readTagStore(reqRoot);
|
|
3051
4564
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3052
4565
|
res.end(JSON.stringify(store));
|
|
3053
4566
|
return;
|
|
@@ -3065,7 +4578,7 @@ async function startChartServer(opts = {}) {
|
|
|
3065
4578
|
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
3066
4579
|
return;
|
|
3067
4580
|
}
|
|
3068
|
-
setTag(
|
|
4581
|
+
setTag(reqRoot, nodeId, key, value);
|
|
3069
4582
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3070
4583
|
res.end(JSON.stringify({ ok: true }));
|
|
3071
4584
|
} catch (err2) {
|
|
@@ -3088,7 +4601,81 @@ async function startChartServer(opts = {}) {
|
|
|
3088
4601
|
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
3089
4602
|
return;
|
|
3090
4603
|
}
|
|
3091
|
-
removeTag(
|
|
4604
|
+
removeTag(reqRoot, nodeId, key);
|
|
4605
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4606
|
+
res.end(JSON.stringify({ ok: true }));
|
|
4607
|
+
} catch (err2) {
|
|
4608
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4609
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4610
|
+
}
|
|
4611
|
+
});
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
|
|
4615
|
+
const config2 = loadConfig(reqRoot);
|
|
4616
|
+
const paths = resolveProjectPaths(reqRoot, config2);
|
|
4617
|
+
const overrides = {
|
|
4618
|
+
appDir: !!config2.paths?.appDir,
|
|
4619
|
+
dbDir: !!config2.paths?.dbDir
|
|
4620
|
+
};
|
|
4621
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4622
|
+
res.end(JSON.stringify({
|
|
4623
|
+
projectRoot: reqRoot,
|
|
4624
|
+
detected: paths ? {
|
|
4625
|
+
srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
|
|
4626
|
+
appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
|
|
4627
|
+
apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
|
|
4628
|
+
dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
|
|
4629
|
+
} : null,
|
|
4630
|
+
overrides,
|
|
4631
|
+
isOverride: overrides.appDir
|
|
4632
|
+
}));
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
4636
|
+
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
4637
|
+
const abs = import_node_path19.default.resolve(browsePath);
|
|
4638
|
+
const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
|
|
4639
|
+
if (!abs.startsWith(twoUp)) {
|
|
4640
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
4641
|
+
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
try {
|
|
4645
|
+
const entries = import_node_fs17.default.readdirSync(abs, { withFileTypes: true });
|
|
4646
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist" && e.name !== ".next").map((e) => e.name).sort();
|
|
4647
|
+
const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
|
|
4648
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4649
|
+
res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
|
|
4650
|
+
} catch (err2) {
|
|
4651
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4652
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4653
|
+
}
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
if (req.method === "GET" && url2.pathname === "/api/audit") {
|
|
4657
|
+
const layer = url2.searchParams.get("layer") ?? "all";
|
|
4658
|
+
const check = url2.searchParams.get("check") ?? void 0;
|
|
4659
|
+
const reports = runAudit(reqRoot, layer, check);
|
|
4660
|
+
const prompt = formatAsPrompt(reports);
|
|
4661
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4662
|
+
res.end(JSON.stringify({ reports, prompt, checks: getAvailableChecks() }));
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
if (req.method === "POST" && url2.pathname === "/api/projects") {
|
|
4666
|
+
let body = "";
|
|
4667
|
+
req.on("data", (chunk) => {
|
|
4668
|
+
body += chunk.toString();
|
|
4669
|
+
});
|
|
4670
|
+
req.on("end", () => {
|
|
4671
|
+
try {
|
|
4672
|
+
const { projects: newProjects } = JSON.parse(body);
|
|
4673
|
+
const config2 = loadConfig(projectRoot);
|
|
4674
|
+
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
4675
|
+
const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
|
|
4676
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
4677
|
+
projects.length = 0;
|
|
4678
|
+
if (config2.projects) projects.push(...config2.projects);
|
|
3092
4679
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3093
4680
|
res.end(JSON.stringify({ ok: true }));
|
|
3094
4681
|
} catch (err2) {
|
|
@@ -3099,7 +4686,7 @@ async function startChartServer(opts = {}) {
|
|
|
3099
4686
|
return;
|
|
3100
4687
|
}
|
|
3101
4688
|
if (url2.pathname !== "/") {
|
|
3102
|
-
const staticPath =
|
|
4689
|
+
const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
|
|
3103
4690
|
if (serveStatic(res, staticPath)) return;
|
|
3104
4691
|
}
|
|
3105
4692
|
serveIndex(res, clientDir);
|
|
@@ -3137,6 +4724,10 @@ async function startChartServer(opts = {}) {
|
|
|
3137
4724
|
`);
|
|
3138
4725
|
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
3139
4726
|
`);
|
|
4727
|
+
if (projects.length > 0) {
|
|
4728
|
+
process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
|
|
4729
|
+
`);
|
|
4730
|
+
}
|
|
3140
4731
|
}
|
|
3141
4732
|
return { port, url };
|
|
3142
4733
|
}
|
|
@@ -3155,19 +4746,19 @@ function runServeCli(argv) {
|
|
|
3155
4746
|
process.exit(1);
|
|
3156
4747
|
});
|
|
3157
4748
|
}
|
|
3158
|
-
var import_node_http,
|
|
4749
|
+
var import_node_http, import_node_fs17, import_node_path19, MAX_PORT_SCAN, MIME_TYPES;
|
|
3159
4750
|
var init_chart_serve = __esm({
|
|
3160
4751
|
"src/server/chart-serve.ts"() {
|
|
3161
4752
|
"use strict";
|
|
3162
4753
|
import_node_http = __toESM(require("node:http"));
|
|
3163
|
-
|
|
3164
|
-
|
|
4754
|
+
import_node_fs17 = __toESM(require("node:fs"));
|
|
4755
|
+
import_node_path19 = __toESM(require("node:path"));
|
|
3165
4756
|
init_graph();
|
|
3166
4757
|
init_lockfile();
|
|
3167
4758
|
init_config();
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
4759
|
+
init_parser_registry();
|
|
4760
|
+
init_resolve_paths();
|
|
4761
|
+
init_audit_core();
|
|
3171
4762
|
MAX_PORT_SCAN = 3;
|
|
3172
4763
|
MIME_TYPES = {
|
|
3173
4764
|
".html": "text/html; charset=utf-8",
|
|
@@ -3183,6 +4774,179 @@ var init_chart_serve = __esm({
|
|
|
3183
4774
|
}
|
|
3184
4775
|
});
|
|
3185
4776
|
|
|
4777
|
+
// src/server/graph/core/language-detection.ts
|
|
4778
|
+
function walkForExtensions(dir, extCounts, depth = 0) {
|
|
4779
|
+
if (depth > 10) return;
|
|
4780
|
+
if (!(0, import_node_fs18.existsSync)(dir)) return;
|
|
4781
|
+
let entries;
|
|
4782
|
+
try {
|
|
4783
|
+
entries = (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true });
|
|
4784
|
+
} catch {
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
for (const entry of entries) {
|
|
4788
|
+
if (entry.name.startsWith(".") && entry.isDirectory()) continue;
|
|
4789
|
+
if (entry.isDirectory()) {
|
|
4790
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
4791
|
+
walkForExtensions((0, import_node_path20.join)(dir, entry.name), extCounts, depth + 1);
|
|
4792
|
+
} else {
|
|
4793
|
+
const ext = (0, import_node_path20.extname)(entry.name).toLowerCase();
|
|
4794
|
+
if (ext && EXTENSION_TO_LANGUAGE[ext]) {
|
|
4795
|
+
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
function detectLanguages(rootDir, supportedLanguages) {
|
|
4801
|
+
const extCounts = /* @__PURE__ */ new Map();
|
|
4802
|
+
walkForExtensions(rootDir, extCounts);
|
|
4803
|
+
const langData = /* @__PURE__ */ new Map();
|
|
4804
|
+
for (const [ext, count] of extCounts) {
|
|
4805
|
+
const lang = EXTENSION_TO_LANGUAGE[ext];
|
|
4806
|
+
if (!lang) continue;
|
|
4807
|
+
if (!langData.has(lang)) {
|
|
4808
|
+
langData.set(lang, { extensions: /* @__PURE__ */ new Set(), fileCount: 0 });
|
|
4809
|
+
}
|
|
4810
|
+
const data = langData.get(lang);
|
|
4811
|
+
data.extensions.add(ext);
|
|
4812
|
+
data.fileCount += count;
|
|
4813
|
+
}
|
|
4814
|
+
const results = [];
|
|
4815
|
+
for (const [lang, data] of langData) {
|
|
4816
|
+
if (AUXILIARY_LANGUAGES.has(lang)) continue;
|
|
4817
|
+
const parserIds = supportedLanguages.get(lang) ?? [];
|
|
4818
|
+
results.push({
|
|
4819
|
+
id: lang,
|
|
4820
|
+
extensions: [...data.extensions].sort(),
|
|
4821
|
+
fileCount: data.fileCount,
|
|
4822
|
+
supported: parserIds.length > 0,
|
|
4823
|
+
parserIds
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
results.sort((a, b) => {
|
|
4827
|
+
if (a.supported !== b.supported) return a.supported ? -1 : 1;
|
|
4828
|
+
return b.fileCount - a.fileCount;
|
|
4829
|
+
});
|
|
4830
|
+
return results;
|
|
4831
|
+
}
|
|
4832
|
+
var import_node_fs18, import_node_path20, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
|
|
4833
|
+
var init_language_detection = __esm({
|
|
4834
|
+
"src/server/graph/core/language-detection.ts"() {
|
|
4835
|
+
"use strict";
|
|
4836
|
+
import_node_fs18 = require("node:fs");
|
|
4837
|
+
import_node_path20 = require("node:path");
|
|
4838
|
+
EXTENSION_TO_LANGUAGE = {
|
|
4839
|
+
// Web / Frontend
|
|
4840
|
+
".ts": "typescript",
|
|
4841
|
+
".tsx": "typescript",
|
|
4842
|
+
".js": "javascript",
|
|
4843
|
+
".jsx": "javascript",
|
|
4844
|
+
".mjs": "javascript",
|
|
4845
|
+
".cjs": "javascript",
|
|
4846
|
+
".vue": "vue",
|
|
4847
|
+
".svelte": "svelte",
|
|
4848
|
+
".html": "html",
|
|
4849
|
+
".css": "css",
|
|
4850
|
+
".scss": "scss",
|
|
4851
|
+
".less": "less",
|
|
4852
|
+
// Backend
|
|
4853
|
+
".py": "python",
|
|
4854
|
+
".go": "go",
|
|
4855
|
+
".rs": "rust",
|
|
4856
|
+
".rb": "ruby",
|
|
4857
|
+
".java": "java",
|
|
4858
|
+
".kt": "kotlin",
|
|
4859
|
+
".kts": "kotlin",
|
|
4860
|
+
".scala": "scala",
|
|
4861
|
+
".cs": "csharp",
|
|
4862
|
+
".fs": "fsharp",
|
|
4863
|
+
".php": "php",
|
|
4864
|
+
".ex": "elixir",
|
|
4865
|
+
".exs": "elixir",
|
|
4866
|
+
".erl": "erlang",
|
|
4867
|
+
".hs": "haskell",
|
|
4868
|
+
".lua": "lua",
|
|
4869
|
+
".pl": "perl",
|
|
4870
|
+
".r": "r",
|
|
4871
|
+
".R": "r",
|
|
4872
|
+
".jl": "julia",
|
|
4873
|
+
".dart": "dart",
|
|
4874
|
+
".clj": "clojure",
|
|
4875
|
+
".cljs": "clojure",
|
|
4876
|
+
// Systems
|
|
4877
|
+
".c": "c",
|
|
4878
|
+
".h": "c",
|
|
4879
|
+
".cpp": "cpp",
|
|
4880
|
+
".cc": "cpp",
|
|
4881
|
+
".cxx": "cpp",
|
|
4882
|
+
".hpp": "cpp",
|
|
4883
|
+
".swift": "swift",
|
|
4884
|
+
".m": "objective-c",
|
|
4885
|
+
".mm": "objective-c",
|
|
4886
|
+
".zig": "zig",
|
|
4887
|
+
".nim": "nim",
|
|
4888
|
+
// Shell / Config
|
|
4889
|
+
".sh": "shell",
|
|
4890
|
+
".bash": "shell",
|
|
4891
|
+
".zsh": "shell",
|
|
4892
|
+
".fish": "shell",
|
|
4893
|
+
// Data / Schema
|
|
4894
|
+
".prisma": "prisma",
|
|
4895
|
+
".sql": "sql",
|
|
4896
|
+
".graphql": "graphql",
|
|
4897
|
+
".gql": "graphql",
|
|
4898
|
+
".proto": "protobuf",
|
|
4899
|
+
// Config
|
|
4900
|
+
".json": "json",
|
|
4901
|
+
".yaml": "yaml",
|
|
4902
|
+
".yml": "yaml",
|
|
4903
|
+
".toml": "toml",
|
|
4904
|
+
".xml": "xml",
|
|
4905
|
+
// Docs
|
|
4906
|
+
".md": "markdown",
|
|
4907
|
+
".mdx": "mdx",
|
|
4908
|
+
".rst": "restructuredtext"
|
|
4909
|
+
};
|
|
4910
|
+
IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
4911
|
+
"node_modules",
|
|
4912
|
+
".next",
|
|
4913
|
+
"dist",
|
|
4914
|
+
".launchsecure",
|
|
4915
|
+
".git",
|
|
4916
|
+
"coverage",
|
|
4917
|
+
".turbo",
|
|
4918
|
+
"build",
|
|
4919
|
+
"out",
|
|
4920
|
+
".vercel",
|
|
4921
|
+
".cache",
|
|
4922
|
+
"__pycache__",
|
|
4923
|
+
".tox",
|
|
4924
|
+
"venv",
|
|
4925
|
+
".venv",
|
|
4926
|
+
"target",
|
|
4927
|
+
"vendor",
|
|
4928
|
+
"Pods",
|
|
4929
|
+
".gradle",
|
|
4930
|
+
".idea",
|
|
4931
|
+
".vscode"
|
|
4932
|
+
]);
|
|
4933
|
+
AUXILIARY_LANGUAGES = /* @__PURE__ */ new Set([
|
|
4934
|
+
"json",
|
|
4935
|
+
"yaml",
|
|
4936
|
+
"toml",
|
|
4937
|
+
"xml",
|
|
4938
|
+
"markdown",
|
|
4939
|
+
"mdx",
|
|
4940
|
+
"restructuredtext",
|
|
4941
|
+
"html",
|
|
4942
|
+
"css",
|
|
4943
|
+
"scss",
|
|
4944
|
+
"less",
|
|
4945
|
+
"shell"
|
|
4946
|
+
]);
|
|
4947
|
+
}
|
|
4948
|
+
});
|
|
4949
|
+
|
|
3186
4950
|
// src/server/graph-mcp.ts
|
|
3187
4951
|
var graph_mcp_exports = {};
|
|
3188
4952
|
__export(graph_mcp_exports, {
|
|
@@ -3269,8 +5033,8 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
3269
5033
|
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
3270
5034
|
if (srcIn && dstIn) projectedEdges++;
|
|
3271
5035
|
}
|
|
3272
|
-
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
|
|
3273
|
-
const projectedChars = projectedVisited * perNode + projectedEdges * EST_CHARS_PER_EDGE[layer];
|
|
5036
|
+
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] ?? DEFAULT_EST_NODE_MIN : EST_CHARS_PER_NODE_FULL[layer] ?? DEFAULT_EST_NODE_FULL;
|
|
5037
|
+
const projectedChars = projectedVisited * perNode + projectedEdges * (EST_CHARS_PER_EDGE[layer] ?? DEFAULT_EST_EDGE);
|
|
3274
5038
|
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
3275
5039
|
budgetExceeded = true;
|
|
3276
5040
|
break;
|
|
@@ -3284,6 +5048,125 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
3284
5048
|
const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
|
|
3285
5049
|
return { nodes, edges, budgetExceeded, stoppedAtHop };
|
|
3286
5050
|
}
|
|
5051
|
+
function reverseNeighborhood(graph, centerId, hops, direction) {
|
|
5052
|
+
const center = graph.nodes.find((n) => n.id === centerId);
|
|
5053
|
+
if (!center) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
|
|
5054
|
+
const visited = /* @__PURE__ */ new Map();
|
|
5055
|
+
visited.set(centerId, { node: center, hop: 0 });
|
|
5056
|
+
let frontier = /* @__PURE__ */ new Set([centerId]);
|
|
5057
|
+
for (let h = 0; h < hops; h++) {
|
|
5058
|
+
const next = /* @__PURE__ */ new Set();
|
|
5059
|
+
for (const edge of graph.edges) {
|
|
5060
|
+
if (direction === "reverse") {
|
|
5061
|
+
if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
|
|
5062
|
+
} else {
|
|
5063
|
+
if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
|
|
5064
|
+
if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
for (const id of next) {
|
|
5068
|
+
const node = graph.nodes.find((n) => n.id === id);
|
|
5069
|
+
if (node) visited.set(id, { node, hop: h + 1 });
|
|
5070
|
+
}
|
|
5071
|
+
frontier = next;
|
|
5072
|
+
if (frontier.size === 0) break;
|
|
5073
|
+
}
|
|
5074
|
+
const nodeIds = new Set(visited.keys());
|
|
5075
|
+
const edges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
|
|
5076
|
+
return { nodes: visited, edges };
|
|
5077
|
+
}
|
|
5078
|
+
function handleBlastPoints(args) {
|
|
5079
|
+
const rootDir = process.cwd();
|
|
5080
|
+
const nodeId = args.node_id;
|
|
5081
|
+
const requestedLayer = args.layer;
|
|
5082
|
+
const hops = args.hops ?? 2;
|
|
5083
|
+
const direction = args.direction ?? "reverse";
|
|
5084
|
+
let targetLayer = requestedLayer;
|
|
5085
|
+
if (!targetLayer) {
|
|
5086
|
+
const graphs = readAllGraphs(rootDir);
|
|
5087
|
+
for (const [layer, graph2] of Object.entries(graphs)) {
|
|
5088
|
+
if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
|
|
5089
|
+
targetLayer = layer;
|
|
5090
|
+
break;
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
if (!targetLayer) {
|
|
5094
|
+
return err(`Node "${nodeId}" not found in any layer. Available layers: ${getAvailableLayers(rootDir).join(", ")}`);
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
5097
|
+
const graph = readGraph(rootDir, targetLayer);
|
|
5098
|
+
if (!graph) {
|
|
5099
|
+
return err(`No graph for layer "${targetLayer}". Run generate_graph first.`);
|
|
5100
|
+
}
|
|
5101
|
+
const center = graph.nodes.find((n) => n.id === nodeId);
|
|
5102
|
+
if (!center) {
|
|
5103
|
+
return err(`Node "${nodeId}" not found in ${targetLayer} layer.`);
|
|
5104
|
+
}
|
|
5105
|
+
const result = reverseNeighborhood(graph, nodeId, hops, direction);
|
|
5106
|
+
const affected = [];
|
|
5107
|
+
for (const [id, { node, hop }] of result.nodes) {
|
|
5108
|
+
if (id === nodeId) continue;
|
|
5109
|
+
const tags = node.tags;
|
|
5110
|
+
affected.push({
|
|
5111
|
+
id: node.id,
|
|
5112
|
+
name: node.name,
|
|
5113
|
+
type: node.type,
|
|
5114
|
+
layer: targetLayer,
|
|
5115
|
+
hop,
|
|
5116
|
+
module: tags?.module
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
|
|
5120
|
+
for (const otherLayer of otherLayers) {
|
|
5121
|
+
const otherGraph = readGraph(rootDir, otherLayer);
|
|
5122
|
+
if (!otherGraph) continue;
|
|
5123
|
+
for (const edge of otherGraph.edges) {
|
|
5124
|
+
if (edge.target === nodeId || edge.source === nodeId) {
|
|
5125
|
+
const dependentId = edge.target === nodeId ? edge.source : edge.target;
|
|
5126
|
+
if (affected.some((a) => a.id === dependentId)) continue;
|
|
5127
|
+
const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
|
|
5128
|
+
if (depNode) {
|
|
5129
|
+
const tags = depNode.tags;
|
|
5130
|
+
affected.push({
|
|
5131
|
+
id: depNode.id,
|
|
5132
|
+
name: depNode.name,
|
|
5133
|
+
type: depNode.type,
|
|
5134
|
+
layer: otherLayer,
|
|
5135
|
+
hop: 1,
|
|
5136
|
+
module: tags?.module
|
|
5137
|
+
});
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
const byLayer = {};
|
|
5143
|
+
const byHop = {};
|
|
5144
|
+
const modulesSet = /* @__PURE__ */ new Set();
|
|
5145
|
+
for (const a of affected) {
|
|
5146
|
+
byLayer[a.layer] = (byLayer[a.layer] ?? 0) + 1;
|
|
5147
|
+
byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
|
|
5148
|
+
if (a.module) modulesSet.add(a.module);
|
|
5149
|
+
}
|
|
5150
|
+
const crossesLayers = Object.keys(byLayer).length > 1;
|
|
5151
|
+
const centerTags = center.tags;
|
|
5152
|
+
return okJson({
|
|
5153
|
+
center: {
|
|
5154
|
+
id: center.id,
|
|
5155
|
+
name: center.name,
|
|
5156
|
+
type: center.type,
|
|
5157
|
+
layer: targetLayer,
|
|
5158
|
+
module: centerTags?.module
|
|
5159
|
+
},
|
|
5160
|
+
affected,
|
|
5161
|
+
summary: {
|
|
5162
|
+
total: affected.length,
|
|
5163
|
+
by_layer: byLayer,
|
|
5164
|
+
by_hop: byHop,
|
|
5165
|
+
modules_touched: Array.from(modulesSet).sort(),
|
|
5166
|
+
crosses_layers: crossesLayers
|
|
5167
|
+
}
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
3287
5170
|
function layerSummary(graph) {
|
|
3288
5171
|
const typeCounts = {};
|
|
3289
5172
|
const moduleCounts = {};
|
|
@@ -3318,9 +5201,6 @@ function err(text) {
|
|
|
3318
5201
|
async function handleGenerateGraph(args) {
|
|
3319
5202
|
const rootDir = process.cwd();
|
|
3320
5203
|
const layer = args.layer;
|
|
3321
|
-
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
3322
|
-
return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
|
|
3323
|
-
}
|
|
3324
5204
|
const results = await generateGraph(rootDir, layer);
|
|
3325
5205
|
if (results.length === 0) {
|
|
3326
5206
|
return err(
|
|
@@ -3357,8 +5237,11 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
3357
5237
|
const offset = args.offset ?? 0;
|
|
3358
5238
|
const limit = args.limit;
|
|
3359
5239
|
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
3360
|
-
if (layer
|
|
3361
|
-
|
|
5240
|
+
if (layer) {
|
|
5241
|
+
const available = getAvailableLayers(rootDir);
|
|
5242
|
+
if (available.length > 0 && !available.includes(layer)) {
|
|
5243
|
+
return { error: `No graph found for layer "${layer}". Available: ${available.join(", ")}. Run generate_graph first.` };
|
|
5244
|
+
}
|
|
3362
5245
|
}
|
|
3363
5246
|
if (!layer) {
|
|
3364
5247
|
const graphs = readAllGraphs(rootDir);
|
|
@@ -3512,9 +5395,12 @@ function handleReadGraph(args) {
|
|
|
3512
5395
|
return okJson(result);
|
|
3513
5396
|
}
|
|
3514
5397
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
3515
|
-
if (layer === "ui") return (0,
|
|
3516
|
-
if (layer === "
|
|
3517
|
-
|
|
5398
|
+
if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
|
|
5399
|
+
if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
|
|
5400
|
+
const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
|
|
5401
|
+
if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
|
|
5402
|
+
const direct = (0, import_node_path21.join)(rootDir, nodeId);
|
|
5403
|
+
if ((0, import_node_fs19.existsSync)(direct)) return direct;
|
|
3518
5404
|
return null;
|
|
3519
5405
|
}
|
|
3520
5406
|
function handleInspectNode(args) {
|
|
@@ -3603,7 +5489,10 @@ function handleGrepNodes(args) {
|
|
|
3603
5489
|
const layer = args.layer;
|
|
3604
5490
|
if (!pattern) return err("pattern is required");
|
|
3605
5491
|
if (!layer) return err("layer is required (ui, api, or db)");
|
|
3606
|
-
|
|
5492
|
+
const grepAvailable = getAvailableLayers(rootDir);
|
|
5493
|
+
if (grepAvailable.length > 0 && !grepAvailable.includes(layer)) {
|
|
5494
|
+
return err(`No graph found for layer "${layer}". Available: ${grepAvailable.join(", ")}. Run generate_graph first.`);
|
|
5495
|
+
}
|
|
3607
5496
|
const hasFilter = !!(args.search || args.type || args.module || args.node_id);
|
|
3608
5497
|
if (!hasFilter) {
|
|
3609
5498
|
return err(
|
|
@@ -3654,11 +5543,11 @@ function handleGrepNodes(args) {
|
|
|
3654
5543
|
let filesSearched = 0;
|
|
3655
5544
|
let truncated = false;
|
|
3656
5545
|
for (const [filePath, nodeId] of filePaths) {
|
|
3657
|
-
if (!(0,
|
|
5546
|
+
if (!(0, import_node_fs19.existsSync)(filePath)) continue;
|
|
3658
5547
|
filesSearched++;
|
|
3659
5548
|
let content;
|
|
3660
5549
|
try {
|
|
3661
|
-
content = (0,
|
|
5550
|
+
content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
|
|
3662
5551
|
} catch {
|
|
3663
5552
|
continue;
|
|
3664
5553
|
}
|
|
@@ -3723,11 +5612,11 @@ function handleStartChartServer(args) {
|
|
|
3723
5612
|
});
|
|
3724
5613
|
}
|
|
3725
5614
|
const entryPath = process.argv[1];
|
|
3726
|
-
const logDir = (0,
|
|
3727
|
-
(0,
|
|
3728
|
-
const logPath = (0,
|
|
3729
|
-
const out = (0,
|
|
3730
|
-
const err2 = (0,
|
|
5615
|
+
const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
5616
|
+
(0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
|
|
5617
|
+
const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
|
|
5618
|
+
const out = (0, import_node_fs19.openSync)(logPath, "a");
|
|
5619
|
+
const err2 = (0, import_node_fs19.openSync)(logPath, "a");
|
|
3731
5620
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
3732
5621
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
3733
5622
|
detached: true,
|
|
@@ -3790,39 +5679,77 @@ function handleRemoveTag(args) {
|
|
|
3790
5679
|
removeTag(rootDir, nodeId, key);
|
|
3791
5680
|
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
3792
5681
|
}
|
|
5682
|
+
function handleAuditLayer(args) {
|
|
5683
|
+
const rootDir = process.cwd();
|
|
5684
|
+
const layer = args.layer;
|
|
5685
|
+
const check = args.check;
|
|
5686
|
+
if (!layer) return err("layer is required");
|
|
5687
|
+
const reports = runAudit(rootDir, layer, check);
|
|
5688
|
+
const totalFindings = reports.reduce((acc, r) => acc + r.findings.length, 0);
|
|
5689
|
+
const totalErrors = reports.reduce((acc, r) => acc + r.summary.errors, 0);
|
|
5690
|
+
const totalWarnings = reports.reduce((acc, r) => acc + r.summary.warnings, 0);
|
|
5691
|
+
const totalInfo = reports.reduce((acc, r) => acc + r.summary.info, 0);
|
|
5692
|
+
const lines = [];
|
|
5693
|
+
lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
|
|
5694
|
+
lines.push("");
|
|
5695
|
+
for (const report of reports) {
|
|
5696
|
+
if (report.findings.length === 0) {
|
|
5697
|
+
lines.push(`\u2713 ${report.check}: no issues found`);
|
|
5698
|
+
continue;
|
|
5699
|
+
}
|
|
5700
|
+
lines.push(`\u2500\u2500 ${report.check} (${report.findings.length}) \u2500\u2500`);
|
|
5701
|
+
for (const f of report.findings) {
|
|
5702
|
+
const tag = f.severity === "error" ? "\u2717" : f.severity === "warning" ? "!" : "\xB7";
|
|
5703
|
+
const filePart = f.file ? ` [${f.file}]` : "";
|
|
5704
|
+
lines.push(` ${tag} ${f.title}${filePart}`);
|
|
5705
|
+
lines.push(` ${f.detail}`);
|
|
5706
|
+
}
|
|
5707
|
+
lines.push("");
|
|
5708
|
+
}
|
|
5709
|
+
return okText(lines.join("\n"));
|
|
5710
|
+
}
|
|
3793
5711
|
function handleDetectProjectStack() {
|
|
3794
5712
|
const rootDir = process.cwd();
|
|
3795
|
-
const parsers = [
|
|
3796
|
-
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
3797
|
-
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
3798
|
-
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
3799
|
-
];
|
|
3800
5713
|
const config = loadConfig(rootDir);
|
|
3801
|
-
|
|
5714
|
+
const registry = createRegistry(config, rootDir);
|
|
5715
|
+
const parserResults = [];
|
|
5716
|
+
for (const parser of registry.getAll()) {
|
|
5717
|
+
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
5718
|
+
const mp = parser;
|
|
5719
|
+
parserResults.push({ id: mp.id, layers: mp.layers, detected: mp.detect(rootDir) });
|
|
5720
|
+
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
5721
|
+
const sp = parser;
|
|
5722
|
+
parserResults.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(rootDir) });
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
const availableLayers = /* @__PURE__ */ new Set();
|
|
5726
|
+
for (const p of parserResults) {
|
|
5727
|
+
if (p.detected) {
|
|
5728
|
+
for (const l of p.layers) availableLayers.add(l);
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
const stats = { calls_api: 0, references_api: 0, annotations: 0 };
|
|
3802
5732
|
const uiGraph = readGraph(rootDir, "ui");
|
|
3803
5733
|
if (uiGraph) {
|
|
3804
5734
|
for (const ref of uiGraph.cross_refs ?? []) {
|
|
3805
5735
|
if (ref.type === "calls_api") stats.calls_api++;
|
|
3806
5736
|
if (ref.type === "references_api") stats.references_api++;
|
|
3807
5737
|
}
|
|
3808
|
-
for (const f of uiGraph.flagged_edges ?? []) {
|
|
3809
|
-
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
3810
|
-
}
|
|
3811
5738
|
}
|
|
3812
|
-
const srcDir = (0,
|
|
3813
|
-
if ((0,
|
|
5739
|
+
const srcDir = (0, import_node_path21.join)(rootDir, "src");
|
|
5740
|
+
if ((0, import_node_fs19.existsSync)(srcDir)) {
|
|
3814
5741
|
const scanDir = (dir) => {
|
|
3815
|
-
if (!(0,
|
|
3816
|
-
for (const entry of (0,
|
|
5742
|
+
if (!(0, import_node_fs19.existsSync)(dir)) return;
|
|
5743
|
+
for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
|
|
3817
5744
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3818
|
-
const full = (0,
|
|
5745
|
+
const full = (0, import_node_path21.join)(dir, entry.name);
|
|
3819
5746
|
if (entry.isDirectory()) {
|
|
3820
5747
|
scanDir(full);
|
|
3821
5748
|
continue;
|
|
3822
5749
|
}
|
|
3823
|
-
if (![".ts", ".tsx"].includes((0,
|
|
5750
|
+
if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
|
|
3824
5751
|
try {
|
|
3825
|
-
const content = (0,
|
|
5752
|
+
const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
|
|
3826
5753
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
3827
5754
|
if (matches) stats.annotations += matches.length;
|
|
3828
5755
|
} catch {
|
|
@@ -3831,21 +5758,33 @@ function handleDetectProjectStack() {
|
|
|
3831
5758
|
};
|
|
3832
5759
|
scanDir(srcDir);
|
|
3833
5760
|
}
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
}
|
|
5761
|
+
const supportedLanguages = /* @__PURE__ */ new Map();
|
|
5762
|
+
supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
|
|
5763
|
+
supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
|
|
5764
|
+
const languages = detectLanguages(rootDir, supportedLanguages);
|
|
5765
|
+
const unsupported = languages.filter((l) => !l.supported);
|
|
5766
|
+
const unsupportedHint = unsupported.length > 0 ? unsupported.map((l) => `${l.id} (${l.fileCount} files)`).join(", ") + " \u2014 detected but not yet supported" : null;
|
|
3840
5767
|
return okJson({
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
5768
|
+
languages,
|
|
5769
|
+
parsers: parserResults,
|
|
5770
|
+
available_layers: [...availableLayers],
|
|
5771
|
+
crosslayer_parsers: (() => {
|
|
5772
|
+
const descriptions = {
|
|
5773
|
+
"fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
|
|
5774
|
+
"api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
|
|
5775
|
+
"url-literal-scanner": "Finds /api/... string literals as fallback detection",
|
|
5776
|
+
"static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
|
|
5777
|
+
};
|
|
5778
|
+
const grouped = {};
|
|
5779
|
+
for (const p of registry.getCrossLayerParsers()) {
|
|
5780
|
+
const concern = p.concern ?? "api-binding";
|
|
5781
|
+
if (!grouped[concern]) grouped[concern] = [];
|
|
5782
|
+
grouped[concern].push({ id: p.id, description: descriptions[p.id] ?? p.id });
|
|
5783
|
+
}
|
|
5784
|
+
return grouped;
|
|
5785
|
+
})(),
|
|
3847
5786
|
stats,
|
|
3848
|
-
|
|
5787
|
+
...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
|
|
3849
5788
|
current_config: Object.keys(config).length > 0 ? config : null,
|
|
3850
5789
|
config_path: ".launchchart.json"
|
|
3851
5790
|
});
|
|
@@ -3921,6 +5860,14 @@ async function handleMessage(msg) {
|
|
|
3921
5860
|
respond(id ?? null, handleRemoveTag(args));
|
|
3922
5861
|
return;
|
|
3923
5862
|
}
|
|
5863
|
+
if (toolName === "audit_layer") {
|
|
5864
|
+
respond(id ?? null, handleAuditLayer(args));
|
|
5865
|
+
return;
|
|
5866
|
+
}
|
|
5867
|
+
if (toolName === "blast_points") {
|
|
5868
|
+
respond(id ?? null, handleBlastPoints(args));
|
|
5869
|
+
return;
|
|
5870
|
+
}
|
|
3924
5871
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
3925
5872
|
return;
|
|
3926
5873
|
}
|
|
@@ -3956,20 +5903,20 @@ function startGraphMcpServer() {
|
|
|
3956
5903
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
3957
5904
|
`);
|
|
3958
5905
|
}
|
|
3959
|
-
var
|
|
5906
|
+
var import_node_fs19, import_node_path21, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, DEFAULT_EST_NODE_FULL, DEFAULT_EST_NODE_MIN, DEFAULT_EST_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
3960
5907
|
var init_graph_mcp = __esm({
|
|
3961
5908
|
"src/server/graph-mcp.ts"() {
|
|
3962
5909
|
"use strict";
|
|
3963
|
-
|
|
3964
|
-
|
|
5910
|
+
import_node_fs19 = require("node:fs");
|
|
5911
|
+
import_node_path21 = require("node:path");
|
|
3965
5912
|
import_node_child_process2 = require("node:child_process");
|
|
3966
5913
|
import_node_os2 = require("node:os");
|
|
3967
5914
|
init_graph();
|
|
3968
5915
|
init_lockfile();
|
|
3969
5916
|
init_config();
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
5917
|
+
init_parser_registry();
|
|
5918
|
+
init_language_detection();
|
|
5919
|
+
init_audit_core();
|
|
3973
5920
|
SERVER_INFO = {
|
|
3974
5921
|
name: "launchsecure-graph",
|
|
3975
5922
|
version: "0.0.1"
|
|
@@ -3977,14 +5924,13 @@ var init_graph_mcp = __esm({
|
|
|
3977
5924
|
TOOLS = [
|
|
3978
5925
|
{
|
|
3979
5926
|
name: "generate_graph",
|
|
3980
|
-
description: "Regenerate the structural project graph by scanning source code in the current working directory.
|
|
5927
|
+
description: "Regenerate the structural project graph by scanning source code in the current working directory. Auto-detects project languages and frameworks, then parses into layers (e.g. ui, api, db) based on content classification. Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
|
|
3981
5928
|
inputSchema: {
|
|
3982
5929
|
type: "object",
|
|
3983
5930
|
properties: {
|
|
3984
5931
|
layer: {
|
|
3985
5932
|
type: "string",
|
|
3986
|
-
|
|
3987
|
-
description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
|
|
5933
|
+
description: "Specific layer to regenerate (e.g. 'ui', 'api', 'db'). Omit to regenerate all detectable layers. Run detect_project_stack to see available layers."
|
|
3988
5934
|
}
|
|
3989
5935
|
}
|
|
3990
5936
|
}
|
|
@@ -3997,8 +5943,7 @@ var init_graph_mcp = __esm({
|
|
|
3997
5943
|
properties: {
|
|
3998
5944
|
layer: {
|
|
3999
5945
|
type: "string",
|
|
4000
|
-
|
|
4001
|
-
description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
|
|
5946
|
+
description: "Graph layer to query (e.g. 'ui', 'api', 'db'). Required if any filter is provided. Run detect_project_stack to see available layers."
|
|
4002
5947
|
},
|
|
4003
5948
|
search: {
|
|
4004
5949
|
type: "string",
|
|
@@ -4050,7 +5995,7 @@ var init_graph_mcp = __esm({
|
|
|
4050
5995
|
items: {
|
|
4051
5996
|
type: "object",
|
|
4052
5997
|
properties: {
|
|
4053
|
-
layer: { type: "string"
|
|
5998
|
+
layer: { type: "string" },
|
|
4054
5999
|
search: { type: "string" },
|
|
4055
6000
|
type: { type: "string" },
|
|
4056
6001
|
module: { type: "string" },
|
|
@@ -4092,8 +6037,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
4092
6037
|
properties: {
|
|
4093
6038
|
layer: {
|
|
4094
6039
|
type: "string",
|
|
4095
|
-
|
|
4096
|
-
description: "Graph layer to scope files (required)."
|
|
6040
|
+
description: "Graph layer to scope files (required, e.g. 'ui', 'api', 'db')."
|
|
4097
6041
|
},
|
|
4098
6042
|
pattern: {
|
|
4099
6043
|
type: "string",
|
|
@@ -4126,8 +6070,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
|
|
|
4126
6070
|
properties: {
|
|
4127
6071
|
layer: {
|
|
4128
6072
|
type: "string",
|
|
4129
|
-
|
|
4130
|
-
description: "Graph layer (required)."
|
|
6073
|
+
description: "Graph layer (required, e.g. 'ui', 'api', 'db')."
|
|
4131
6074
|
},
|
|
4132
6075
|
node_id: {
|
|
4133
6076
|
type: "string",
|
|
@@ -4187,7 +6130,7 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4187
6130
|
},
|
|
4188
6131
|
{
|
|
4189
6132
|
name: "detect_project_stack",
|
|
4190
|
-
description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify
|
|
6133
|
+
description: "Detect project languages, frameworks, available parsers, and recommend parser configuration. Scans the project to identify all languages present (TypeScript, Python, Go, etc.) and reports which are supported by registered parsers vs detected-but-unsupported. Also detects frameworks (Next.js, Prisma, React, etc.), provides cross-layer detection stats (fetch calls, @api annotations, URL literals), and returns available graph layers. \n\nUse this when setting up launch-chart for a new project, reviewing parser configuration, or checking what languages are in the project.",
|
|
4191
6134
|
inputSchema: {
|
|
4192
6135
|
type: "object",
|
|
4193
6136
|
properties: {}
|
|
@@ -4232,6 +6175,59 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4232
6175
|
},
|
|
4233
6176
|
required: ["node_id", "key"]
|
|
4234
6177
|
}
|
|
6178
|
+
},
|
|
6179
|
+
{
|
|
6180
|
+
name: "audit_layer",
|
|
6181
|
+
description: "Run audit checks on a graph layer. Surfaces schema drift, unprotected routes, hardcoded values, dead screens, and other discrepancies. Returns findings with severity and file locations.\n\nAvailable layers and checks:\n- db: schema_drift (Prisma vs SQL migration mismatches), orphan_fks (FKs without @relation)\n- api: unprotected_routes (endpoints missing from ROUTE_PERMISSIONS)\n- ui: dead_screens (pages with no incoming navigation)\n- static: unenforced_permissions (seed permissions with no route enforcement), hardcoded_values (ALL_CAPS strings not in inventory)\n- all: run every check across all layers",
|
|
6182
|
+
inputSchema: {
|
|
6183
|
+
type: "object",
|
|
6184
|
+
properties: {
|
|
6185
|
+
layer: {
|
|
6186
|
+
type: "string",
|
|
6187
|
+
description: "Layer to audit: 'db', 'api', 'ui', 'static', or 'all'."
|
|
6188
|
+
},
|
|
6189
|
+
check: {
|
|
6190
|
+
type: "string",
|
|
6191
|
+
description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
|
|
6192
|
+
}
|
|
6193
|
+
},
|
|
6194
|
+
required: ["layer"]
|
|
6195
|
+
}
|
|
6196
|
+
},
|
|
6197
|
+
{
|
|
6198
|
+
name: "blast_points",
|
|
6199
|
+
description: `Calculate the blast radius for a node \u2014 what depends on it across all project layers. Returns reverse dependencies aggregated with hop distance and summary stats.
|
|
6200
|
+
|
|
6201
|
+
USE THIS when assessing the impact of changing a file, table, or endpoint. Replaces multiple read_graph calls with a single query that:
|
|
6202
|
+
- Traverses REVERSE edges (who imports/depends on this node)
|
|
6203
|
+
- Searches across ALL layers if layer is omitted
|
|
6204
|
+
- Returns affected nodes with hop distance, type, layer, and module
|
|
6205
|
+
- Provides a summary with counts by layer, by hop, and risk assessment
|
|
6206
|
+
|
|
6207
|
+
Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 returns all files that import middleware.ts, and all files that import THOSE files.`,
|
|
6208
|
+
inputSchema: {
|
|
6209
|
+
type: "object",
|
|
6210
|
+
properties: {
|
|
6211
|
+
node_id: {
|
|
6212
|
+
type: "string",
|
|
6213
|
+
description: "The node to analyze (file path, table name, etc.)"
|
|
6214
|
+
},
|
|
6215
|
+
layer: {
|
|
6216
|
+
type: "string",
|
|
6217
|
+
description: "Layer the node lives in (e.g. 'ui', 'api', 'db'). Omit to auto-detect by searching all layers."
|
|
6218
|
+
},
|
|
6219
|
+
hops: {
|
|
6220
|
+
type: "number",
|
|
6221
|
+
description: "Max hops to traverse outward. Default 2."
|
|
6222
|
+
},
|
|
6223
|
+
direction: {
|
|
6224
|
+
type: "string",
|
|
6225
|
+
enum: ["reverse", "both"],
|
|
6226
|
+
description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
|
|
6227
|
+
}
|
|
6228
|
+
},
|
|
6229
|
+
required: ["node_id"]
|
|
6230
|
+
}
|
|
4235
6231
|
}
|
|
4236
6232
|
];
|
|
4237
6233
|
COMPACT_SCHEMA = {
|
|
@@ -4288,6 +6284,9 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4288
6284
|
api: 65,
|
|
4289
6285
|
db: 65
|
|
4290
6286
|
};
|
|
6287
|
+
DEFAULT_EST_NODE_FULL = 300;
|
|
6288
|
+
DEFAULT_EST_NODE_MIN = 150;
|
|
6289
|
+
DEFAULT_EST_EDGE = 65;
|
|
4291
6290
|
NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
4292
6291
|
BATCH_BUDGET_CHARS = 6e4;
|
|
4293
6292
|
}
|
|
@@ -4295,10 +6294,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4295
6294
|
|
|
4296
6295
|
// src/server/graph-mcp-entry.ts
|
|
4297
6296
|
var import_node_child_process3 = require("node:child_process");
|
|
4298
|
-
var
|
|
4299
|
-
var
|
|
6297
|
+
var import_node_fs20 = require("node:fs");
|
|
6298
|
+
var import_node_path22 = __toESM(require("node:path"));
|
|
4300
6299
|
var import_node_os3 = require("node:os");
|
|
4301
|
-
var
|
|
6300
|
+
var import_node_fs21 = require("node:fs");
|
|
4302
6301
|
init_lockfile();
|
|
4303
6302
|
function logStderr(msg) {
|
|
4304
6303
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -4314,11 +6313,11 @@ function maybeAutoServe() {
|
|
|
4314
6313
|
return;
|
|
4315
6314
|
}
|
|
4316
6315
|
try {
|
|
4317
|
-
const logDir =
|
|
4318
|
-
(0,
|
|
4319
|
-
const logPath =
|
|
4320
|
-
const out = (0,
|
|
4321
|
-
const err2 = (0,
|
|
6316
|
+
const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
6317
|
+
(0, import_node_fs21.mkdirSync)(logDir, { recursive: true });
|
|
6318
|
+
const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
|
|
6319
|
+
const out = (0, import_node_fs20.openSync)(logPath, "a");
|
|
6320
|
+
const err2 = (0, import_node_fs20.openSync)(logPath, "a");
|
|
4322
6321
|
const entryPath = process.argv[1];
|
|
4323
6322
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
4324
6323
|
detached: true,
|