@launchsecure/launch-kit 0.0.12 → 0.0.14
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-Dm6IBkiC.js +441 -0
- package/dist/chart-client/assets/index-l-yyLDX5.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-rlw8dmPR.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +2055 -481
- package/dist/server/cli.js +5018 -3329
- package/dist/server/graph/queries/classify.scm +41 -0
- package/dist/server/graph-mcp-entry.js +2351 -506
- 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-CyML1UiJ.js} +0 -0
|
@@ -155,7 +155,45 @@ var init_config = __esm({
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// src/server/graph/core/resolve-paths.ts
|
|
159
|
+
function resolveProjectPaths(rootDir, config) {
|
|
160
|
+
if (config.paths?.appDir) {
|
|
161
|
+
const appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
|
|
162
|
+
const srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
|
|
163
|
+
return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api") };
|
|
164
|
+
}
|
|
165
|
+
const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
|
|
166
|
+
if ((0, import_node_fs3.existsSync)(srcApp)) {
|
|
167
|
+
return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api") };
|
|
168
|
+
}
|
|
169
|
+
const rootApp = (0, import_node_path3.join)(rootDir, "app");
|
|
170
|
+
if ((0, import_node_fs3.existsSync)(rootApp)) {
|
|
171
|
+
return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api") };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
var import_node_fs3, import_node_path3;
|
|
176
|
+
var init_resolve_paths = __esm({
|
|
177
|
+
"src/server/graph/core/resolve-paths.ts"() {
|
|
178
|
+
"use strict";
|
|
179
|
+
import_node_fs3 = require("node:fs");
|
|
180
|
+
import_node_path3 = require("node:path");
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
158
184
|
// src/server/graph/core/ts-extractor.ts
|
|
185
|
+
var ts_extractor_exports = {};
|
|
186
|
+
__export(ts_extractor_exports, {
|
|
187
|
+
classifyFile: () => classifyFile,
|
|
188
|
+
createQuery: () => createQuery,
|
|
189
|
+
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
190
|
+
extractDbCallsTS: () => extractDbCallsTS,
|
|
191
|
+
extractDeep: () => extractDeep,
|
|
192
|
+
initTreeSitter: () => initTreeSitter,
|
|
193
|
+
parseCodeTS: () => parseCodeTS,
|
|
194
|
+
parseFileTS: () => parseFileTS,
|
|
195
|
+
setExtractorConfig: () => setExtractorConfig
|
|
196
|
+
});
|
|
159
197
|
async function initTreeSitter() {
|
|
160
198
|
if (initialized) return;
|
|
161
199
|
if (initPromise) return initPromise;
|
|
@@ -179,17 +217,25 @@ function getQuery(name) {
|
|
|
179
217
|
ensureInit();
|
|
180
218
|
const cached = queryCache.get(name);
|
|
181
219
|
if (cached) return cached;
|
|
182
|
-
const scmPath = (0,
|
|
183
|
-
const scm = (0,
|
|
220
|
+
const scmPath = (0, import_node_path4.join)(queriesDir, `${name}.scm`);
|
|
221
|
+
const scm = (0, import_node_fs4.readFileSync)(scmPath, "utf-8");
|
|
184
222
|
const query = tsxLanguage.query(scm);
|
|
185
223
|
queryCache.set(name, query);
|
|
186
224
|
return query;
|
|
187
225
|
}
|
|
188
226
|
function parseSource(absPath) {
|
|
189
227
|
ensureInit();
|
|
190
|
-
const content = (0,
|
|
228
|
+
const content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
|
|
191
229
|
return parserInstance.parse(content);
|
|
192
230
|
}
|
|
231
|
+
function parseCodeTS(code) {
|
|
232
|
+
ensureInit();
|
|
233
|
+
return parserInstance.parse(code);
|
|
234
|
+
}
|
|
235
|
+
function createQuery(pattern) {
|
|
236
|
+
ensureInit();
|
|
237
|
+
return tsxLanguage.query(pattern);
|
|
238
|
+
}
|
|
193
239
|
function setExtractorConfig(config) {
|
|
194
240
|
extraDbIdentifiers = config.dbIdentifiers ?? [];
|
|
195
241
|
extraMutationMethods = config.mutationMethods ?? [];
|
|
@@ -456,6 +502,24 @@ function extractDbCallsTS(absPath) {
|
|
|
456
502
|
}
|
|
457
503
|
return calls;
|
|
458
504
|
}
|
|
505
|
+
function classifyFile(absPath) {
|
|
506
|
+
const fileName = require("path").basename(absPath);
|
|
507
|
+
if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
|
|
508
|
+
if (fileName.includes(".stories.")) return "story";
|
|
509
|
+
const tree = parseSource(absPath);
|
|
510
|
+
const root = tree.rootNode;
|
|
511
|
+
const classifyQuery = getQuery("classify");
|
|
512
|
+
const captures = classifyQuery.captures(root);
|
|
513
|
+
const capNames = new Set(captures.map((c) => c.name));
|
|
514
|
+
if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
|
|
515
|
+
if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
|
|
516
|
+
if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
|
|
517
|
+
if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
|
|
518
|
+
if (capNames.has("has_jsx")) return "component";
|
|
519
|
+
if (capNames.has("hook_decl") || capNames.has("hook_const")) return "hook";
|
|
520
|
+
if (fileName.includes("config") || fileName.includes(".config.")) return "config";
|
|
521
|
+
return "lib";
|
|
522
|
+
}
|
|
459
523
|
function extractAuthWrappersTS(absPath) {
|
|
460
524
|
const tree = parseSource(absPath);
|
|
461
525
|
const root = tree.rootNode;
|
|
@@ -626,17 +690,17 @@ function extractDeep(absPath) {
|
|
|
626
690
|
}
|
|
627
691
|
return { elements, stateVars, conditions, variables, responses, params };
|
|
628
692
|
}
|
|
629
|
-
var
|
|
693
|
+
var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
|
|
630
694
|
var init_ts_extractor = __esm({
|
|
631
695
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
632
696
|
"use strict";
|
|
633
|
-
|
|
634
|
-
|
|
697
|
+
import_node_fs4 = require("node:fs");
|
|
698
|
+
import_node_path4 = require("node:path");
|
|
635
699
|
initialized = false;
|
|
636
700
|
queriesDir = (() => {
|
|
637
|
-
const srcPath = (0,
|
|
701
|
+
const srcPath = (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "..", "queries");
|
|
638
702
|
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
639
|
-
return (0,
|
|
703
|
+
return (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "graph", "queries");
|
|
640
704
|
})();
|
|
641
705
|
queryCache = /* @__PURE__ */ new Map();
|
|
642
706
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
@@ -656,15 +720,15 @@ var init_ts_extractor = __esm({
|
|
|
656
720
|
}
|
|
657
721
|
});
|
|
658
722
|
|
|
659
|
-
// src/server/graph/parsers/
|
|
723
|
+
// src/server/graph/parsers/ts/typescript-project.ts
|
|
660
724
|
function walk(dir, exts) {
|
|
661
725
|
const results = [];
|
|
662
|
-
if (!(0,
|
|
663
|
-
for (const entry of (0,
|
|
664
|
-
const full = (0,
|
|
726
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
727
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
728
|
+
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
665
729
|
if (entry.isDirectory()) {
|
|
666
730
|
results.push(...walk(full, exts));
|
|
667
|
-
} else if (exts.includes((0,
|
|
731
|
+
} else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
|
|
668
732
|
results.push(full);
|
|
669
733
|
}
|
|
670
734
|
}
|
|
@@ -672,33 +736,33 @@ function walk(dir, exts) {
|
|
|
672
736
|
}
|
|
673
737
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
674
738
|
const results = [];
|
|
675
|
-
if (!(0,
|
|
676
|
-
for (const entry of (0,
|
|
739
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
740
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
677
741
|
if (entry.isDirectory()) {
|
|
678
742
|
if (ignoreDirs.has(entry.name)) continue;
|
|
679
|
-
results.push(...walkWithIgnore((0,
|
|
680
|
-
} else if (exts.includes((0,
|
|
681
|
-
results.push((0,
|
|
743
|
+
results.push(...walkWithIgnore((0, import_node_path5.join)(dir, entry.name), exts, ignoreDirs));
|
|
744
|
+
} else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
|
|
745
|
+
results.push((0, import_node_path5.join)(dir, entry.name));
|
|
682
746
|
}
|
|
683
747
|
}
|
|
684
748
|
return results;
|
|
685
749
|
}
|
|
686
750
|
function toNodeId(srcDir, absPath) {
|
|
687
|
-
return (0,
|
|
751
|
+
return (0, import_node_path5.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
688
752
|
}
|
|
689
753
|
function resolveImport(srcDir, specifier) {
|
|
690
754
|
if (!specifier.startsWith("@/")) return null;
|
|
691
755
|
const rel = specifier.slice(2);
|
|
692
|
-
const base = (0,
|
|
693
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
694
|
-
if ((0,
|
|
756
|
+
const base = (0, import_node_path5.join)(srcDir, rel);
|
|
757
|
+
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")]) {
|
|
758
|
+
if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
|
|
695
759
|
}
|
|
696
760
|
return null;
|
|
697
761
|
}
|
|
698
762
|
function resolveRelativeImport(fromFile, specifier) {
|
|
699
|
-
const base = (0,
|
|
700
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
701
|
-
if ((0,
|
|
763
|
+
const base = (0, import_node_path5.join)((0, import_node_path5.dirname)(fromFile), specifier);
|
|
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;
|
|
702
766
|
}
|
|
703
767
|
return null;
|
|
704
768
|
}
|
|
@@ -719,7 +783,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
719
783
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
720
784
|
if (!resolved) continue;
|
|
721
785
|
if (re.isWildcard) {
|
|
722
|
-
const targetBn = (0,
|
|
786
|
+
const targetBn = (0, import_node_path5.basename)(resolved);
|
|
723
787
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
724
788
|
if (targetIsBarrel) {
|
|
725
789
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -746,31 +810,21 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
746
810
|
const barrels = /* @__PURE__ */ new Map();
|
|
747
811
|
const memo = /* @__PURE__ */ new Map();
|
|
748
812
|
for (const [absPath, parsed] of parsedByPath) {
|
|
749
|
-
const bn = (0,
|
|
813
|
+
const bn = (0, import_node_path5.basename)(absPath);
|
|
750
814
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
751
815
|
if (parsed.reExports.length === 0) continue;
|
|
752
816
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
753
817
|
if (map.size > 0) {
|
|
754
|
-
const barrelId = (0,
|
|
818
|
+
const barrelId = (0, import_node_path5.relative)(srcDir, (0, import_node_path5.dirname)(absPath)).replace(/\\/g, "/");
|
|
755
819
|
barrels.set(barrelId, map);
|
|
756
820
|
}
|
|
757
821
|
}
|
|
758
822
|
return barrels;
|
|
759
823
|
}
|
|
760
|
-
function classifyType(id) {
|
|
761
|
-
|
|
762
|
-
if (id.
|
|
763
|
-
|
|
764
|
-
if (id.startsWith("client/components/")) return "component";
|
|
765
|
-
if (id.startsWith("client/hooks/")) return "hook";
|
|
766
|
-
if (/client\/lib\/.*-context\./.test(id)) return "context";
|
|
767
|
-
if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
|
|
768
|
-
if (id.startsWith("client/api/")) return "util";
|
|
769
|
-
if (id.startsWith("server/mcp/")) return "mcp-tool";
|
|
770
|
-
if (id.startsWith("server/lib/")) return "lib";
|
|
771
|
-
if (id.startsWith("server/")) return "lib";
|
|
772
|
-
if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
|
|
773
|
-
return "component";
|
|
824
|
+
function classifyType(absPath, id) {
|
|
825
|
+
const contentType = classifyFile(absPath);
|
|
826
|
+
if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
|
|
827
|
+
return contentType;
|
|
774
828
|
}
|
|
775
829
|
function extractRoute(id) {
|
|
776
830
|
if (!id.endsWith("/page.tsx")) return null;
|
|
@@ -782,7 +836,18 @@ function extractRoute(id) {
|
|
|
782
836
|
return route || "/";
|
|
783
837
|
}
|
|
784
838
|
function nameFromFilename(absPath) {
|
|
785
|
-
return (0,
|
|
839
|
+
return (0, import_node_path5.basename)(absPath, (0, import_node_path5.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
840
|
+
}
|
|
841
|
+
function filePathToApiRoute(apiDir, absPath) {
|
|
842
|
+
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
843
|
+
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
844
|
+
route = route.replace(/\/+/g, "/");
|
|
845
|
+
if (route === "/") return "/api";
|
|
846
|
+
return "/api" + route;
|
|
847
|
+
}
|
|
848
|
+
function camelToPascal(s) {
|
|
849
|
+
if (!s) return s;
|
|
850
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
786
851
|
}
|
|
787
852
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
788
853
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -863,7 +928,7 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
863
928
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
864
929
|
return null;
|
|
865
930
|
}
|
|
866
|
-
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet,
|
|
931
|
+
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
|
|
867
932
|
const edges = [];
|
|
868
933
|
const flagged = [];
|
|
869
934
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -875,13 +940,10 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
875
940
|
if (label) edge.label = label;
|
|
876
941
|
edges.push(edge);
|
|
877
942
|
}
|
|
878
|
-
function edgeTypeFor(
|
|
943
|
+
function edgeTypeFor(isTypeOnlyImport, importedNames) {
|
|
879
944
|
if (isTypeOnlyImport) return "imports";
|
|
880
|
-
const
|
|
881
|
-
if (
|
|
882
|
-
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
883
|
-
if (anyRendered) return "renders";
|
|
884
|
-
}
|
|
945
|
+
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
946
|
+
if (anyRendered) return "renders";
|
|
885
947
|
return "imports";
|
|
886
948
|
}
|
|
887
949
|
for (const imp of parsed.imports) {
|
|
@@ -903,14 +965,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
903
965
|
}
|
|
904
966
|
for (const [targetId, targetNames] of byTarget) {
|
|
905
967
|
const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
|
|
906
|
-
addEdge(targetId, edgeTypeFor(
|
|
968
|
+
addEdge(targetId, edgeTypeFor(allType, targetNames));
|
|
907
969
|
}
|
|
908
970
|
} else {
|
|
909
971
|
const resolved = resolveImport(srcDir, specifier);
|
|
910
972
|
if (resolved) {
|
|
911
973
|
const targetId = toNodeId(srcDir, resolved);
|
|
912
974
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
913
|
-
addEdge(targetId, edgeTypeFor(
|
|
975
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
914
976
|
}
|
|
915
977
|
}
|
|
916
978
|
}
|
|
@@ -919,7 +981,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
919
981
|
if (resolved) {
|
|
920
982
|
const targetId = toNodeId(srcDir, resolved);
|
|
921
983
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
922
|
-
addEdge(targetId, edgeTypeFor(
|
|
984
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
923
985
|
}
|
|
924
986
|
}
|
|
925
987
|
}
|
|
@@ -961,74 +1023,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
961
1023
|
}
|
|
962
1024
|
return { edges, flagged };
|
|
963
1025
|
}
|
|
1026
|
+
function hasNextConfig(rootDir) {
|
|
1027
|
+
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"));
|
|
1028
|
+
}
|
|
964
1029
|
function detect(rootDir) {
|
|
965
|
-
|
|
1030
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
1031
|
+
return paths !== null && hasNextConfig(rootDir);
|
|
966
1032
|
}
|
|
967
1033
|
function generate(rootDir) {
|
|
968
|
-
const
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
);
|
|
976
|
-
const
|
|
977
|
-
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
1034
|
+
const config = loadConfig(rootDir);
|
|
1035
|
+
const paths = resolveProjectPaths(rootDir, config);
|
|
1036
|
+
const srcDir = paths.srcDir;
|
|
1037
|
+
const apiDir = paths.apiDir;
|
|
1038
|
+
const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
|
|
1039
|
+
const clientFiles = walk((0, import_node_path5.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
1040
|
+
const serverFiles = walk((0, import_node_path5.join)(srcDir, "server"), [".ts", ".tsx"]);
|
|
1041
|
+
const libFiles = walk((0, import_node_path5.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
1042
|
+
const configFiles = walk((0, import_node_path5.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
978
1043
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
979
1044
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
980
1045
|
for (const absPath of allDiscovered) {
|
|
981
1046
|
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
982
1047
|
}
|
|
983
1048
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
984
|
-
const
|
|
985
|
-
const
|
|
1049
|
+
const uiNodes = [];
|
|
1050
|
+
const apiNodes = [];
|
|
986
1051
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
987
|
-
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
988
1052
|
const routeToNodeId = /* @__PURE__ */ new Map();
|
|
1053
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path5.basename)(f).startsWith("index."));
|
|
989
1054
|
for (const absPath of fileSet) {
|
|
990
1055
|
const id = toNodeId(srcDir, absPath);
|
|
991
|
-
const type = classifyType(id);
|
|
1056
|
+
const type = classifyType(absPath, id);
|
|
1057
|
+
if (type === "test" || type === "story") continue;
|
|
992
1058
|
const parsed = parsedByPath.get(absPath);
|
|
993
1059
|
const name = parsed.name || nameFromFilename(absPath);
|
|
994
|
-
const
|
|
995
|
-
const deep = extractDeep(absPath);
|
|
996
|
-
nodes.push({
|
|
997
|
-
id,
|
|
998
|
-
type,
|
|
999
|
-
name,
|
|
1000
|
-
route,
|
|
1001
|
-
exports: parsed.exports,
|
|
1002
|
-
elements: deep.elements,
|
|
1003
|
-
stateVars: deep.stateVars,
|
|
1004
|
-
conditions: deep.conditions,
|
|
1005
|
-
variables: deep.variables
|
|
1006
|
-
});
|
|
1060
|
+
const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
|
|
1007
1061
|
nodeIdSet.add(id);
|
|
1008
|
-
|
|
1009
|
-
|
|
1062
|
+
if (layer === "api") {
|
|
1063
|
+
const methods = [];
|
|
1064
|
+
for (const exp of parsed.exports) {
|
|
1065
|
+
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1066
|
+
}
|
|
1067
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1068
|
+
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1069
|
+
const deep = extractDeep(absPath);
|
|
1070
|
+
const routePath = (0, import_node_fs5.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
|
|
1071
|
+
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1072
|
+
const mutates = mutations.length > 0;
|
|
1073
|
+
const authStrategy = [...authWrappers];
|
|
1074
|
+
apiNodes.push({
|
|
1075
|
+
id,
|
|
1076
|
+
type: "endpoint",
|
|
1077
|
+
name: routePath,
|
|
1078
|
+
layer: "api",
|
|
1079
|
+
path: routePath,
|
|
1080
|
+
methods,
|
|
1081
|
+
handler: id,
|
|
1082
|
+
mutates,
|
|
1083
|
+
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1084
|
+
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1085
|
+
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1086
|
+
conditions: deep.conditions,
|
|
1087
|
+
variables: deep.variables,
|
|
1088
|
+
responses: deep.responses,
|
|
1089
|
+
params: deep.params,
|
|
1090
|
+
_dbCalls: dbCalls
|
|
1091
|
+
// temp: used for cross-ref building below
|
|
1092
|
+
});
|
|
1093
|
+
} else {
|
|
1094
|
+
const route = extractRoute(id);
|
|
1095
|
+
const deep = extractDeep(absPath);
|
|
1096
|
+
uiNodes.push({
|
|
1097
|
+
id,
|
|
1098
|
+
type,
|
|
1099
|
+
name,
|
|
1100
|
+
layer: "ui",
|
|
1101
|
+
route,
|
|
1102
|
+
exports: parsed.exports,
|
|
1103
|
+
elements: deep.elements,
|
|
1104
|
+
stateVars: deep.stateVars,
|
|
1105
|
+
conditions: deep.conditions,
|
|
1106
|
+
variables: deep.variables
|
|
1107
|
+
});
|
|
1108
|
+
if (route) routeToNodeId.set(route, id);
|
|
1109
|
+
}
|
|
1010
1110
|
}
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1111
|
+
const uiEdges = [];
|
|
1112
|
+
const uiFlagged = [];
|
|
1013
1113
|
for (const absPath of fileSet) {
|
|
1014
|
-
const
|
|
1114
|
+
const id = toNodeId(srcDir, absPath);
|
|
1115
|
+
if (!nodeIdSet.has(id)) continue;
|
|
1015
1116
|
const parsed = parsedByPath.get(absPath);
|
|
1016
1117
|
const { edges, flagged } = extractEdges(
|
|
1017
1118
|
srcDir,
|
|
1018
1119
|
absPath,
|
|
1019
|
-
|
|
1120
|
+
id,
|
|
1020
1121
|
parsed,
|
|
1021
1122
|
nodeIdSet,
|
|
1022
|
-
nodeTypeMap,
|
|
1023
1123
|
barrelMaps,
|
|
1024
1124
|
routeToNodeId
|
|
1025
1125
|
);
|
|
1026
|
-
|
|
1027
|
-
|
|
1126
|
+
uiEdges.push(...edges);
|
|
1127
|
+
uiFlagged.push(...flagged);
|
|
1028
1128
|
}
|
|
1029
1129
|
const fetchCallEntries = [];
|
|
1030
1130
|
for (const absPath of fileSet) {
|
|
1031
1131
|
const sourceId = toNodeId(srcDir, absPath);
|
|
1132
|
+
if (!nodeIdSet.has(sourceId)) continue;
|
|
1032
1133
|
const parsed = parsedByPath.get(absPath);
|
|
1033
1134
|
if (parsed.fetchCalls.length === 0) continue;
|
|
1034
1135
|
fetchCallEntries.push({
|
|
@@ -1043,7 +1144,7 @@ function generate(rootDir) {
|
|
|
1043
1144
|
});
|
|
1044
1145
|
}
|
|
1045
1146
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1046
|
-
const
|
|
1147
|
+
const IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
1047
1148
|
"node_modules",
|
|
1048
1149
|
".next",
|
|
1049
1150
|
"dist",
|
|
@@ -1056,7 +1157,7 @@ function generate(rootDir) {
|
|
|
1056
1157
|
"out",
|
|
1057
1158
|
".vercel"
|
|
1058
1159
|
]);
|
|
1059
|
-
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"],
|
|
1160
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS2);
|
|
1060
1161
|
for (const absPath of externalCandidates) {
|
|
1061
1162
|
const normalized = absPath.replace(/\\/g, "/");
|
|
1062
1163
|
if (externalScanned.has(normalized)) continue;
|
|
@@ -1066,11 +1167,11 @@ function generate(rootDir) {
|
|
|
1066
1167
|
} catch {
|
|
1067
1168
|
continue;
|
|
1068
1169
|
}
|
|
1069
|
-
const externalId = (0,
|
|
1170
|
+
const externalId = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1070
1171
|
const edgesFromThis = [];
|
|
1071
1172
|
const seen = /* @__PURE__ */ new Set();
|
|
1072
1173
|
for (const imp of parsed.imports) {
|
|
1073
|
-
const { specifier,
|
|
1174
|
+
const { specifier, names } = imp;
|
|
1074
1175
|
let resolved = null;
|
|
1075
1176
|
if (specifier.startsWith("@/")) {
|
|
1076
1177
|
const relToSrc = specifier.slice(2);
|
|
@@ -1096,25 +1197,52 @@ function generate(rootDir) {
|
|
|
1096
1197
|
const targetId = toNodeId(srcDir, resolved);
|
|
1097
1198
|
if (!nodeIdSet.has(targetId)) continue;
|
|
1098
1199
|
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
1099
|
-
const key = `${externalId}\u2192${targetId}
|
|
1200
|
+
const key = `${externalId}\u2192${targetId}`;
|
|
1100
1201
|
if (seen.has(key)) continue;
|
|
1101
1202
|
seen.add(key);
|
|
1102
1203
|
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
1103
1204
|
}
|
|
1104
1205
|
if (edgesFromThis.length === 0) continue;
|
|
1105
|
-
|
|
1206
|
+
uiNodes.push({
|
|
1106
1207
|
id: externalId,
|
|
1107
1208
|
type: "external",
|
|
1108
1209
|
name: parsed.name || nameFromFilename(absPath),
|
|
1210
|
+
layer: "ui",
|
|
1109
1211
|
route: null,
|
|
1110
1212
|
exports: parsed.exports
|
|
1111
1213
|
});
|
|
1112
1214
|
nodeIdSet.add(externalId);
|
|
1113
|
-
|
|
1114
|
-
|
|
1215
|
+
uiEdges.push(...edgesFromThis);
|
|
1216
|
+
}
|
|
1217
|
+
const apiCrossRefs = [];
|
|
1218
|
+
for (const node of apiNodes) {
|
|
1219
|
+
const dbCalls = node._dbCalls;
|
|
1220
|
+
if (!dbCalls) continue;
|
|
1221
|
+
const seenModels = /* @__PURE__ */ new Set();
|
|
1222
|
+
for (const call of dbCalls) {
|
|
1223
|
+
if (seenModels.has(call.model)) continue;
|
|
1224
|
+
seenModels.add(call.model);
|
|
1225
|
+
apiCrossRefs.push({
|
|
1226
|
+
source: node.id,
|
|
1227
|
+
target: camelToPascal(call.model),
|
|
1228
|
+
type: call.isMutation ? "mutates" : "reads",
|
|
1229
|
+
layer: "db"
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
delete node._dbCalls;
|
|
1233
|
+
}
|
|
1234
|
+
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1235
|
+
const apiEdges = [];
|
|
1236
|
+
const uiOnlyEdges = [];
|
|
1237
|
+
for (const edge of uiEdges) {
|
|
1238
|
+
if (apiNodeIds.has(edge.source)) {
|
|
1239
|
+
apiEdges.push(edge);
|
|
1240
|
+
} else {
|
|
1241
|
+
uiOnlyEdges.push(edge);
|
|
1242
|
+
}
|
|
1115
1243
|
}
|
|
1116
1244
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
1117
|
-
const dedupedFlagged =
|
|
1245
|
+
const dedupedFlagged = uiFlagged.filter((f) => {
|
|
1118
1246
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
1119
1247
|
if (flaggedSet.has(key)) return false;
|
|
1120
1248
|
flaggedSet.add(key);
|
|
@@ -1131,10 +1259,12 @@ function generate(rootDir) {
|
|
|
1131
1259
|
hook: 7,
|
|
1132
1260
|
lib: 8
|
|
1133
1261
|
};
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1262
|
+
uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
|
|
1263
|
+
uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1264
|
+
apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
|
|
1265
|
+
apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1266
|
+
const byType = (t) => uiNodes.filter((n) => n.type === t).length;
|
|
1267
|
+
const uiStats = {
|
|
1138
1268
|
total_pages: byType("page"),
|
|
1139
1269
|
total_layouts: byType("layout"),
|
|
1140
1270
|
total_components: byType("component"),
|
|
@@ -1145,197 +1275,122 @@ function generate(rootDir) {
|
|
|
1145
1275
|
total_utils: byType("util"),
|
|
1146
1276
|
total_libs: byType("lib"),
|
|
1147
1277
|
total_external: byType("external"),
|
|
1148
|
-
total_edges:
|
|
1278
|
+
total_edges: uiOnlyEdges.length,
|
|
1149
1279
|
total_flagged: dedupedFlagged.length
|
|
1150
1280
|
};
|
|
1151
|
-
|
|
1281
|
+
const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
|
|
1282
|
+
const result = /* @__PURE__ */ new Map();
|
|
1283
|
+
result.set("ui", {
|
|
1152
1284
|
metadata: {
|
|
1153
1285
|
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1154
1286
|
scope: "main-app-only",
|
|
1155
1287
|
app_root: "src/",
|
|
1156
1288
|
layer: "ui",
|
|
1157
|
-
parser: "
|
|
1158
|
-
...
|
|
1289
|
+
parser: "typescript-project",
|
|
1290
|
+
...uiStats,
|
|
1159
1291
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1160
1292
|
},
|
|
1161
|
-
nodes,
|
|
1162
|
-
edges:
|
|
1293
|
+
nodes: stripLayer(uiNodes),
|
|
1294
|
+
edges: uiOnlyEdges,
|
|
1163
1295
|
cross_refs: [],
|
|
1164
1296
|
contradictions: [],
|
|
1165
1297
|
warnings: [],
|
|
1166
1298
|
flagged_edges: dedupedFlagged,
|
|
1167
1299
|
patterns: {
|
|
1168
|
-
total_nodes:
|
|
1169
|
-
by_type:
|
|
1300
|
+
total_nodes: uiNodes.length,
|
|
1301
|
+
by_type: uiStats,
|
|
1170
1302
|
by_edge_type: {
|
|
1171
|
-
renders:
|
|
1172
|
-
imports:
|
|
1173
|
-
navigates:
|
|
1303
|
+
renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
|
|
1304
|
+
imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
|
|
1305
|
+
navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
|
|
1174
1306
|
},
|
|
1175
1307
|
fetch_calls: fetchCallEntries
|
|
1176
1308
|
}
|
|
1177
|
-
};
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
detect,
|
|
1191
|
-
generate
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
1197
|
-
function walk2(dir) {
|
|
1198
|
-
const results = [];
|
|
1199
|
-
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
1200
|
-
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
1201
|
-
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
1202
|
-
if (entry.isDirectory()) {
|
|
1203
|
-
results.push(...walk2(full));
|
|
1204
|
-
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
1205
|
-
results.push(full);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
return results;
|
|
1209
|
-
}
|
|
1210
|
-
function filePathToRoute(apiDir, absPath) {
|
|
1211
|
-
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
1212
|
-
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
1213
|
-
route = route.replace(/\/+/g, "/");
|
|
1214
|
-
if (route === "/") return "/api";
|
|
1215
|
-
return "/api" + route;
|
|
1216
|
-
}
|
|
1217
|
-
function camelToPascal(s) {
|
|
1218
|
-
if (!s) return s;
|
|
1219
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1220
|
-
}
|
|
1221
|
-
function detect2(rootDir) {
|
|
1222
|
-
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
|
|
1223
|
-
}
|
|
1224
|
-
function generate2(rootDir) {
|
|
1225
|
-
const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
|
|
1226
|
-
const routeFiles = walk2(apiDir);
|
|
1227
|
-
const nodes = [];
|
|
1228
|
-
const edges = [];
|
|
1229
|
-
const crossRefs = [];
|
|
1230
|
-
const mutatorCount = {};
|
|
1231
|
-
const authUsage = {};
|
|
1232
|
-
let endpointsWithAuth = 0;
|
|
1233
|
-
let endpointsWithDbAccess = 0;
|
|
1234
|
-
for (const absPath of routeFiles) {
|
|
1235
|
-
const parsed = parseFileTS(absPath);
|
|
1236
|
-
const dbCalls = extractDbCallsTS(absPath);
|
|
1237
|
-
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1238
|
-
const methods = [];
|
|
1239
|
-
for (const exp of parsed.exports) {
|
|
1240
|
-
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1241
|
-
}
|
|
1242
|
-
const routePath = filePathToRoute(apiDir, absPath);
|
|
1243
|
-
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1244
|
-
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1245
|
-
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1246
|
-
const mutates = mutations.length > 0;
|
|
1247
|
-
if (mutates) {
|
|
1248
|
-
for (const m of mutations) {
|
|
1249
|
-
mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
|
|
1309
|
+
});
|
|
1310
|
+
if (apiNodes.length > 0) {
|
|
1311
|
+
const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
|
|
1312
|
+
const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
|
|
1313
|
+
const authUsage = {};
|
|
1314
|
+
let endpointsWithAuth = 0;
|
|
1315
|
+
for (const n of apiNodes) {
|
|
1316
|
+
const auth = n.auth;
|
|
1317
|
+
if (auth.length > 0 && auth[0] !== "public") {
|
|
1318
|
+
endpointsWithAuth++;
|
|
1319
|
+
for (const w of auth) {
|
|
1320
|
+
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1321
|
+
}
|
|
1250
1322
|
}
|
|
1251
1323
|
}
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1324
|
+
const mutatorCount = {};
|
|
1325
|
+
for (const n of apiNodes) {
|
|
1326
|
+
const ops = n.db_operations;
|
|
1327
|
+
for (const op of ops) {
|
|
1328
|
+
const method = op.split(".")[1];
|
|
1329
|
+
if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
|
|
1330
|
+
}
|
|
1257
1331
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1332
|
+
result.set("api", {
|
|
1333
|
+
metadata: {
|
|
1334
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1335
|
+
scope: "main-app-only",
|
|
1336
|
+
stack: "nextjs-app-router",
|
|
1337
|
+
layer: "api",
|
|
1338
|
+
parser: "typescript-project",
|
|
1339
|
+
total_endpoints: apiNodes.length,
|
|
1340
|
+
total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1341
|
+
endpoints_with_auth: endpointsWithAuth,
|
|
1342
|
+
endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
|
|
1343
|
+
mutator_endpoints: mutatorNodes,
|
|
1344
|
+
read_only_endpoints: readOnlyNodes
|
|
1345
|
+
},
|
|
1346
|
+
nodes: stripLayer(apiNodes),
|
|
1347
|
+
edges: apiEdges,
|
|
1348
|
+
cross_refs: apiCrossRefs,
|
|
1349
|
+
contradictions: [],
|
|
1350
|
+
warnings: [],
|
|
1351
|
+
flagged_edges: [],
|
|
1352
|
+
patterns: {
|
|
1353
|
+
total_endpoints: apiNodes.length,
|
|
1354
|
+
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1355
|
+
acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
|
|
1356
|
+
return acc;
|
|
1357
|
+
}, {}),
|
|
1358
|
+
auth_strategies: authUsage,
|
|
1359
|
+
mutation_operations: mutatorCount,
|
|
1360
|
+
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1361
|
+
}
|
|
1277
1362
|
});
|
|
1278
|
-
const seenModels = /* @__PURE__ */ new Set();
|
|
1279
|
-
for (const call of dbCalls) {
|
|
1280
|
-
if (seenModels.has(call.model)) continue;
|
|
1281
|
-
seenModels.add(call.model);
|
|
1282
|
-
crossRefs.push({
|
|
1283
|
-
source: relPath,
|
|
1284
|
-
target: camelToPascal(call.model),
|
|
1285
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1286
|
-
layer: "db"
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
1363
|
}
|
|
1290
|
-
|
|
1291
|
-
crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1292
|
-
const mutatorNodes = nodes.filter((n) => n.mutates).length;
|
|
1293
|
-
const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
|
|
1294
|
-
return {
|
|
1295
|
-
metadata: {
|
|
1296
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1297
|
-
scope: "main-app-only",
|
|
1298
|
-
stack: "nextjs-app-router",
|
|
1299
|
-
layer: "api",
|
|
1300
|
-
parser: "nextjs-routes-ast",
|
|
1301
|
-
total_endpoints: nodes.length,
|
|
1302
|
-
total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1303
|
-
endpoints_with_auth: endpointsWithAuth,
|
|
1304
|
-
endpoints_with_db_access: endpointsWithDbAccess,
|
|
1305
|
-
mutator_endpoints: mutatorNodes,
|
|
1306
|
-
read_only_endpoints: readOnlyNodes
|
|
1307
|
-
},
|
|
1308
|
-
nodes,
|
|
1309
|
-
edges,
|
|
1310
|
-
cross_refs: crossRefs,
|
|
1311
|
-
contradictions: [],
|
|
1312
|
-
warnings: [],
|
|
1313
|
-
flagged_edges: [],
|
|
1314
|
-
patterns: {
|
|
1315
|
-
total_endpoints: nodes.length,
|
|
1316
|
-
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1317
|
-
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
1318
|
-
return acc;
|
|
1319
|
-
}, {}),
|
|
1320
|
-
auth_strategies: authUsage,
|
|
1321
|
-
mutation_operations: mutatorCount,
|
|
1322
|
-
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1323
|
-
}
|
|
1324
|
-
};
|
|
1364
|
+
return result;
|
|
1325
1365
|
}
|
|
1326
|
-
var import_node_fs5, import_node_path5, HTTP_METHODS,
|
|
1327
|
-
var
|
|
1328
|
-
"src/server/graph/parsers/
|
|
1366
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS, CLASSIFICATION_TO_LAYER, typescriptProjectParser;
|
|
1367
|
+
var init_typescript_project = __esm({
|
|
1368
|
+
"src/server/graph/parsers/ts/typescript-project.ts"() {
|
|
1329
1369
|
"use strict";
|
|
1330
1370
|
import_node_fs5 = require("node:fs");
|
|
1331
1371
|
import_node_path5 = require("node:path");
|
|
1372
|
+
init_config();
|
|
1373
|
+
init_resolve_paths();
|
|
1332
1374
|
init_ts_extractor();
|
|
1333
1375
|
HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1376
|
+
CLASSIFICATION_TO_LAYER = {
|
|
1377
|
+
endpoint: "api",
|
|
1378
|
+
page: "ui",
|
|
1379
|
+
layout: "ui",
|
|
1380
|
+
component: "ui",
|
|
1381
|
+
ui: "ui",
|
|
1382
|
+
hook: "ui",
|
|
1383
|
+
context: "ui",
|
|
1384
|
+
config: "ui",
|
|
1385
|
+
lib: "ui",
|
|
1386
|
+
"mcp-tool": "ui",
|
|
1387
|
+
external: "ui"
|
|
1388
|
+
};
|
|
1389
|
+
typescriptProjectParser = {
|
|
1390
|
+
id: "typescript-project",
|
|
1391
|
+
layers: ["ui", "api"],
|
|
1392
|
+
detect,
|
|
1393
|
+
generate
|
|
1339
1394
|
};
|
|
1340
1395
|
}
|
|
1341
1396
|
});
|
|
@@ -1430,10 +1485,10 @@ function parseEnums(content) {
|
|
|
1430
1485
|
}
|
|
1431
1486
|
return nodes;
|
|
1432
1487
|
}
|
|
1433
|
-
function
|
|
1488
|
+
function detect2(rootDir) {
|
|
1434
1489
|
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
|
|
1435
1490
|
}
|
|
1436
|
-
function
|
|
1491
|
+
function generate2(rootDir) {
|
|
1437
1492
|
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1438
1493
|
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
1439
1494
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
@@ -1493,6 +1548,430 @@ var init_prisma_schema = __esm({
|
|
|
1493
1548
|
prismaSchemaParser = {
|
|
1494
1549
|
id: "prisma-schema",
|
|
1495
1550
|
layer: "db",
|
|
1551
|
+
detect: detect2,
|
|
1552
|
+
generate: generate2
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// src/server/graph/parsers/db/sql-migrations.ts
|
|
1558
|
+
function pgTypeToPrisma(pgType) {
|
|
1559
|
+
const upper = pgType.toUpperCase().trim();
|
|
1560
|
+
return PG_TO_PRISMA[upper] ?? upper;
|
|
1561
|
+
}
|
|
1562
|
+
function parseCreateTable(sql, state) {
|
|
1563
|
+
const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
|
|
1564
|
+
let m;
|
|
1565
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1566
|
+
const tableName = m[1];
|
|
1567
|
+
const body = m[2];
|
|
1568
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1569
|
+
let primaryCol = null;
|
|
1570
|
+
for (const line of body.split("\n")) {
|
|
1571
|
+
const trimmed = line.trim().replace(/,\s*$/, "");
|
|
1572
|
+
if (!trimmed || trimmed.startsWith("--")) continue;
|
|
1573
|
+
const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
|
|
1574
|
+
if (pkMatch) {
|
|
1575
|
+
primaryCol = pkMatch[1];
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
|
|
1579
|
+
const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
|
|
1580
|
+
if (!colMatch) continue;
|
|
1581
|
+
const colName = colMatch[1];
|
|
1582
|
+
let rest = colMatch[2];
|
|
1583
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1584
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
|
|
1585
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1586
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
|
|
1587
|
+
columns.set(colName, {
|
|
1588
|
+
name: colName,
|
|
1589
|
+
type: colType,
|
|
1590
|
+
nullable: !isNotNull,
|
|
1591
|
+
primary: false,
|
|
1592
|
+
unique: false,
|
|
1593
|
+
default: defaultVal
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
if (primaryCol && columns.has(primaryCol)) {
|
|
1597
|
+
columns.get(primaryCol).primary = true;
|
|
1598
|
+
}
|
|
1599
|
+
state.tables.set(tableName, { name: tableName, columns });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function parseCreateEnum(sql, state) {
|
|
1603
|
+
const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
|
|
1604
|
+
let m;
|
|
1605
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1606
|
+
const enumName = m[1];
|
|
1607
|
+
const valuesStr = m[2];
|
|
1608
|
+
const values = new Set(
|
|
1609
|
+
valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
|
|
1610
|
+
);
|
|
1611
|
+
state.enums.set(enumName, { name: enumName, values });
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function parseAlterTable(sql, state) {
|
|
1615
|
+
const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
|
|
1616
|
+
let m;
|
|
1617
|
+
while ((m = addColRe.exec(sql)) !== null) {
|
|
1618
|
+
const tableName = m[1];
|
|
1619
|
+
const colName = m[2];
|
|
1620
|
+
let rest = m[3];
|
|
1621
|
+
const table = state.tables.get(tableName);
|
|
1622
|
+
if (!table) continue;
|
|
1623
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1624
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
|
|
1625
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1626
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
|
|
1627
|
+
table.columns.set(colName, {
|
|
1628
|
+
name: colName,
|
|
1629
|
+
type: colType,
|
|
1630
|
+
nullable: !isNotNull,
|
|
1631
|
+
primary: false,
|
|
1632
|
+
unique: false,
|
|
1633
|
+
default: defaultVal
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
|
|
1637
|
+
while ((m = dropColRe.exec(sql)) !== null) {
|
|
1638
|
+
const table = state.tables.get(m[1]);
|
|
1639
|
+
if (table) table.columns.delete(m[2]);
|
|
1640
|
+
}
|
|
1641
|
+
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;
|
|
1642
|
+
while ((m = fkRe.exec(sql)) !== null) {
|
|
1643
|
+
state.fks.push({
|
|
1644
|
+
constraintName: m[2],
|
|
1645
|
+
sourceTable: m[1],
|
|
1646
|
+
sourceColumn: m[3],
|
|
1647
|
+
targetTable: m[4],
|
|
1648
|
+
targetColumn: m[5],
|
|
1649
|
+
onDelete: m[6] ?? null
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function parseAlterEnum(sql, state) {
|
|
1654
|
+
const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
|
|
1655
|
+
let m;
|
|
1656
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1657
|
+
const en = state.enums.get(m[1]);
|
|
1658
|
+
if (en) en.values.add(m[2]);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
function parseDropTable(sql, state) {
|
|
1662
|
+
const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
|
|
1663
|
+
let m;
|
|
1664
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1665
|
+
state.tables.delete(m[1]);
|
|
1666
|
+
state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function parseUniqueIndex(sql, state) {
|
|
1670
|
+
const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
|
|
1671
|
+
let m;
|
|
1672
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1673
|
+
const table = state.tables.get(m[1]);
|
|
1674
|
+
const col = table?.columns.get(m[2]);
|
|
1675
|
+
if (col) col.unique = true;
|
|
1676
|
+
if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
|
|
1677
|
+
state.uniqueIndexes.get(m[1]).add(m[2]);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function parseMigrations(rootDir) {
|
|
1681
|
+
const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
|
|
1682
|
+
const state = {
|
|
1683
|
+
tables: /* @__PURE__ */ new Map(),
|
|
1684
|
+
enums: /* @__PURE__ */ new Map(),
|
|
1685
|
+
fks: [],
|
|
1686
|
+
uniqueIndexes: /* @__PURE__ */ new Map()
|
|
1687
|
+
};
|
|
1688
|
+
if (!(0, import_node_fs7.existsSync)(migrationsDir)) return state;
|
|
1689
|
+
const dirs = (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1690
|
+
for (const dir of dirs) {
|
|
1691
|
+
const sqlPath = (0, import_node_path7.join)(migrationsDir, dir, "migration.sql");
|
|
1692
|
+
if (!(0, import_node_fs7.existsSync)(sqlPath)) continue;
|
|
1693
|
+
const sql = (0, import_node_fs7.readFileSync)(sqlPath, "utf-8");
|
|
1694
|
+
parseCreateEnum(sql, state);
|
|
1695
|
+
parseCreateTable(sql, state);
|
|
1696
|
+
parseAlterTable(sql, state);
|
|
1697
|
+
parseAlterEnum(sql, state);
|
|
1698
|
+
parseDropTable(sql, state);
|
|
1699
|
+
parseUniqueIndex(sql, state);
|
|
1700
|
+
}
|
|
1701
|
+
return state;
|
|
1702
|
+
}
|
|
1703
|
+
function loadPrismaState(rootDir) {
|
|
1704
|
+
const schemaPath = (0, import_node_path7.join)(rootDir, "prisma", "schema.prisma");
|
|
1705
|
+
if (!(0, import_node_fs7.existsSync)(schemaPath)) return null;
|
|
1706
|
+
const content = (0, import_node_fs7.readFileSync)(schemaPath, "utf-8");
|
|
1707
|
+
const tables = /* @__PURE__ */ new Map();
|
|
1708
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1709
|
+
const relations = [];
|
|
1710
|
+
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1711
|
+
let m;
|
|
1712
|
+
while ((m = modelRe.exec(content)) !== null) {
|
|
1713
|
+
const modelName = m[1];
|
|
1714
|
+
const body = m[2];
|
|
1715
|
+
const cols = [];
|
|
1716
|
+
for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
|
|
1717
|
+
const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
|
|
1718
|
+
if (!fm) continue;
|
|
1719
|
+
const fieldName = fm[1];
|
|
1720
|
+
const fieldType = fm[2];
|
|
1721
|
+
const rest = fm[3] ?? "";
|
|
1722
|
+
const baseType = fieldType.replace(/[?\[\]]/g, "");
|
|
1723
|
+
const isRelation = rest.includes("@relation");
|
|
1724
|
+
if (isRelation) {
|
|
1725
|
+
const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
|
|
1726
|
+
const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
|
|
1727
|
+
if (fieldsMatch) {
|
|
1728
|
+
relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
|
|
1729
|
+
}
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
|
|
1733
|
+
if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
|
|
1734
|
+
}
|
|
1735
|
+
cols.push({
|
|
1736
|
+
name: fieldName,
|
|
1737
|
+
type: fieldType.replace("?", ""),
|
|
1738
|
+
nullable: fieldType.endsWith("?") || fieldType.includes("?"),
|
|
1739
|
+
primary: rest.includes("@id"),
|
|
1740
|
+
unique: rest.includes("@unique")
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
tables.set(modelName, { columns: cols });
|
|
1744
|
+
}
|
|
1745
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1746
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
1747
|
+
const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
1748
|
+
enums.set(m[1], values);
|
|
1749
|
+
}
|
|
1750
|
+
return { tables, enums, relations };
|
|
1751
|
+
}
|
|
1752
|
+
function verify(sqlState, prisma) {
|
|
1753
|
+
const contradictions = [];
|
|
1754
|
+
const flaggedEdges = [];
|
|
1755
|
+
for (const [name] of sqlState.tables) {
|
|
1756
|
+
if (!prisma.tables.has(name)) {
|
|
1757
|
+
contradictions.push({
|
|
1758
|
+
entity: name,
|
|
1759
|
+
source_a: "sql-migrations",
|
|
1760
|
+
source_b: "prisma-schema",
|
|
1761
|
+
detail: `Table "${name}" exists in migrations but not in schema.prisma`
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
for (const [name] of prisma.tables) {
|
|
1766
|
+
if (!sqlState.tables.has(name)) {
|
|
1767
|
+
contradictions.push({
|
|
1768
|
+
entity: name,
|
|
1769
|
+
source_a: "prisma-schema",
|
|
1770
|
+
source_b: "sql-migrations",
|
|
1771
|
+
detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
for (const [tableName, prismaTable] of prisma.tables) {
|
|
1776
|
+
const sqlTable = sqlState.tables.get(tableName);
|
|
1777
|
+
if (!sqlTable) continue;
|
|
1778
|
+
for (const prismaCol of prismaTable.columns) {
|
|
1779
|
+
const sqlCol = sqlTable.columns.get(prismaCol.name);
|
|
1780
|
+
if (!sqlCol) {
|
|
1781
|
+
contradictions.push({
|
|
1782
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1783
|
+
source_a: "prisma-schema",
|
|
1784
|
+
source_b: "sql-migrations",
|
|
1785
|
+
detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
|
|
1786
|
+
});
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
const mappedSqlType = pgTypeToPrisma(sqlCol.type);
|
|
1790
|
+
const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
|
|
1791
|
+
if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
|
|
1792
|
+
if (!sqlState.enums.has(prismaBaseType)) {
|
|
1793
|
+
contradictions.push({
|
|
1794
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1795
|
+
source_a: "sql-migrations",
|
|
1796
|
+
source_b: "prisma-schema",
|
|
1797
|
+
detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (sqlCol.nullable !== prismaCol.nullable) {
|
|
1802
|
+
contradictions.push({
|
|
1803
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1804
|
+
source_a: "sql-migrations",
|
|
1805
|
+
source_b: "prisma-schema",
|
|
1806
|
+
detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
for (const [colName] of sqlTable.columns) {
|
|
1811
|
+
const inPrisma = prismaTable.columns.some((c) => c.name === colName);
|
|
1812
|
+
if (!inPrisma) {
|
|
1813
|
+
contradictions.push({
|
|
1814
|
+
entity: `${tableName}.${colName}`,
|
|
1815
|
+
source_a: "sql-migrations",
|
|
1816
|
+
source_b: "prisma-schema",
|
|
1817
|
+
detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1823
|
+
const prismaValues = prisma.enums.get(name);
|
|
1824
|
+
if (!prismaValues) {
|
|
1825
|
+
contradictions.push({
|
|
1826
|
+
entity: name,
|
|
1827
|
+
source_a: "sql-migrations",
|
|
1828
|
+
source_b: "prisma-schema",
|
|
1829
|
+
detail: `Enum "${name}" exists in migrations but not in schema.prisma`
|
|
1830
|
+
});
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
const prismaSet = new Set(prismaValues);
|
|
1834
|
+
for (const val of sqlEnum.values) {
|
|
1835
|
+
if (!prismaSet.has(val)) {
|
|
1836
|
+
contradictions.push({
|
|
1837
|
+
entity: `${name}.${val}`,
|
|
1838
|
+
source_a: "sql-migrations",
|
|
1839
|
+
source_b: "prisma-schema",
|
|
1840
|
+
detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
for (const val of prismaValues) {
|
|
1845
|
+
if (!sqlEnum.values.has(val)) {
|
|
1846
|
+
contradictions.push({
|
|
1847
|
+
entity: `${name}.${val}`,
|
|
1848
|
+
source_a: "prisma-schema",
|
|
1849
|
+
source_b: "sql-migrations",
|
|
1850
|
+
detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
|
|
1856
|
+
for (const fk of sqlState.fks) {
|
|
1857
|
+
const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
|
|
1858
|
+
if (!prismaFkSet.has(key)) {
|
|
1859
|
+
flaggedEdges.push({
|
|
1860
|
+
source: fk.sourceTable,
|
|
1861
|
+
target: fk.targetTable,
|
|
1862
|
+
type: "belongs_to",
|
|
1863
|
+
label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
|
|
1864
|
+
confidence: "high"
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return { contradictions, flaggedEdges };
|
|
1869
|
+
}
|
|
1870
|
+
function detect3(rootDir) {
|
|
1871
|
+
const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
|
|
1872
|
+
if (!(0, import_node_fs7.existsSync)(migrationsDir)) return false;
|
|
1873
|
+
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")));
|
|
1874
|
+
}
|
|
1875
|
+
function generate3(rootDir) {
|
|
1876
|
+
const sqlState = parseMigrations(rootDir);
|
|
1877
|
+
const prisma = loadPrismaState(rootDir);
|
|
1878
|
+
const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
|
|
1879
|
+
const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
|
|
1880
|
+
const nodes = [];
|
|
1881
|
+
for (const [name, table] of sqlState.tables) {
|
|
1882
|
+
if (prismaTableIds.has(name)) continue;
|
|
1883
|
+
nodes.push({
|
|
1884
|
+
id: name,
|
|
1885
|
+
type: "table",
|
|
1886
|
+
name,
|
|
1887
|
+
source: "sql",
|
|
1888
|
+
columns: [...table.columns.values()].map((c) => ({
|
|
1889
|
+
name: c.name,
|
|
1890
|
+
type: c.type,
|
|
1891
|
+
primary: c.primary,
|
|
1892
|
+
unique: c.unique,
|
|
1893
|
+
nullable: c.nullable,
|
|
1894
|
+
default: c.default,
|
|
1895
|
+
isRelation: false,
|
|
1896
|
+
comment: null
|
|
1897
|
+
}))
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1901
|
+
if (prismaEnumIds.has(name)) continue;
|
|
1902
|
+
nodes.push({
|
|
1903
|
+
id: name,
|
|
1904
|
+
type: "enum",
|
|
1905
|
+
name,
|
|
1906
|
+
source: "sql",
|
|
1907
|
+
values: [...sqlEnum.values]
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
|
|
1911
|
+
const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
|
|
1912
|
+
source: fk.sourceTable,
|
|
1913
|
+
target: fk.targetTable,
|
|
1914
|
+
type: "belongs_to",
|
|
1915
|
+
fk: fk.sourceColumn,
|
|
1916
|
+
onDelete: fk.onDelete
|
|
1917
|
+
}));
|
|
1918
|
+
const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
|
|
1919
|
+
return {
|
|
1920
|
+
metadata: {
|
|
1921
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1922
|
+
scope: "sql-migrations",
|
|
1923
|
+
source: "prisma/migrations/",
|
|
1924
|
+
layer: "db",
|
|
1925
|
+
sql_tables: sqlState.tables.size,
|
|
1926
|
+
sql_enums: sqlState.enums.size,
|
|
1927
|
+
sql_fks: sqlState.fks.length,
|
|
1928
|
+
additive_nodes: nodes.length,
|
|
1929
|
+
contradictions_found: contradictions.length,
|
|
1930
|
+
flagged_edges_found: flaggedEdges.length
|
|
1931
|
+
},
|
|
1932
|
+
nodes,
|
|
1933
|
+
edges,
|
|
1934
|
+
cross_refs: [],
|
|
1935
|
+
contradictions,
|
|
1936
|
+
warnings: [],
|
|
1937
|
+
flagged_edges: flaggedEdges
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
var import_node_fs7, import_node_path7, PG_TO_PRISMA, sqlMigrationsParser;
|
|
1941
|
+
var init_sql_migrations = __esm({
|
|
1942
|
+
"src/server/graph/parsers/db/sql-migrations.ts"() {
|
|
1943
|
+
"use strict";
|
|
1944
|
+
import_node_fs7 = require("node:fs");
|
|
1945
|
+
import_node_path7 = require("node:path");
|
|
1946
|
+
PG_TO_PRISMA = {
|
|
1947
|
+
"TEXT": "String",
|
|
1948
|
+
"VARCHAR": "String",
|
|
1949
|
+
"CHAR": "String",
|
|
1950
|
+
"INTEGER": "Int",
|
|
1951
|
+
"INT": "Int",
|
|
1952
|
+
"SMALLINT": "Int",
|
|
1953
|
+
"BIGINT": "BigInt",
|
|
1954
|
+
"SERIAL": "Int",
|
|
1955
|
+
"BOOLEAN": "Boolean",
|
|
1956
|
+
"BOOL": "Boolean",
|
|
1957
|
+
"TIMESTAMP(3)": "DateTime",
|
|
1958
|
+
"TIMESTAMP": "DateTime",
|
|
1959
|
+
"TIMESTAMPTZ": "DateTime",
|
|
1960
|
+
"DATE": "DateTime",
|
|
1961
|
+
"DOUBLE PRECISION": "Float",
|
|
1962
|
+
"FLOAT": "Float",
|
|
1963
|
+
"REAL": "Float",
|
|
1964
|
+
"DECIMAL": "Decimal",
|
|
1965
|
+
"NUMERIC": "Decimal",
|
|
1966
|
+
"JSONB": "Json",
|
|
1967
|
+
"JSON": "Json",
|
|
1968
|
+
"BYTEA": "Bytes",
|
|
1969
|
+
"UUID": "String",
|
|
1970
|
+
"TEXT[]": "String[]"
|
|
1971
|
+
};
|
|
1972
|
+
sqlMigrationsParser = {
|
|
1973
|
+
id: "sql-migrations",
|
|
1974
|
+
layer: "db",
|
|
1496
1975
|
detect: detect3,
|
|
1497
1976
|
generate: generate3
|
|
1498
1977
|
};
|
|
@@ -1724,36 +2203,36 @@ var init_fetch_resolver = __esm({
|
|
|
1724
2203
|
});
|
|
1725
2204
|
|
|
1726
2205
|
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1727
|
-
function
|
|
1728
|
-
if (!(0,
|
|
2206
|
+
function walk2(dir, exts) {
|
|
2207
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1729
2208
|
const results = [];
|
|
1730
|
-
for (const entry of (0,
|
|
2209
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
1731
2210
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1732
|
-
const full = (0,
|
|
2211
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1733
2212
|
if (entry.isDirectory()) {
|
|
1734
|
-
results.push(...
|
|
1735
|
-
} else if (exts.includes((0,
|
|
2213
|
+
results.push(...walk2(full, exts));
|
|
2214
|
+
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
1736
2215
|
results.push(full);
|
|
1737
2216
|
}
|
|
1738
2217
|
}
|
|
1739
2218
|
return results;
|
|
1740
2219
|
}
|
|
1741
2220
|
function toNodeId2(srcDir, absPath) {
|
|
1742
|
-
return (0,
|
|
2221
|
+
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1743
2222
|
}
|
|
1744
|
-
var
|
|
2223
|
+
var import_node_fs8, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
|
|
1745
2224
|
var init_api_annotations = __esm({
|
|
1746
2225
|
"src/server/graph/parsers/crosslayer/api-annotations.ts"() {
|
|
1747
2226
|
"use strict";
|
|
1748
|
-
|
|
1749
|
-
|
|
2227
|
+
import_node_fs8 = require("node:fs");
|
|
2228
|
+
import_node_path8 = require("node:path");
|
|
1750
2229
|
init_api_route_matching();
|
|
1751
2230
|
API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1752
2231
|
apiAnnotationsParser = {
|
|
1753
2232
|
id: "api-annotations",
|
|
1754
2233
|
layer: "crosslayer",
|
|
1755
2234
|
detect(rootDir) {
|
|
1756
|
-
return (0,
|
|
2235
|
+
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
|
|
1757
2236
|
},
|
|
1758
2237
|
generate(rootDir, layerOutputs) {
|
|
1759
2238
|
const apiOutput = layerOutputs.get("api");
|
|
@@ -1764,13 +2243,13 @@ var init_api_annotations = __esm({
|
|
|
1764
2243
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1765
2244
|
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1766
2245
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1767
|
-
const srcDir = (0,
|
|
1768
|
-
const files =
|
|
2246
|
+
const srcDir = (0, import_node_path8.join)(rootDir, "src");
|
|
2247
|
+
const files = walk2(srcDir, [".ts", ".tsx"]);
|
|
1769
2248
|
const crossRefs = [];
|
|
1770
2249
|
const flaggedEdges = [];
|
|
1771
2250
|
const seen = /* @__PURE__ */ new Set();
|
|
1772
2251
|
for (const absPath of files) {
|
|
1773
|
-
const content = (0,
|
|
2252
|
+
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
1774
2253
|
const sourceId = toNodeId2(srcDir, absPath);
|
|
1775
2254
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
1776
2255
|
let match;
|
|
@@ -1816,73 +2295,741 @@ var init_api_annotations = __esm({
|
|
|
1816
2295
|
});
|
|
1817
2296
|
|
|
1818
2297
|
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1819
|
-
function
|
|
1820
|
-
if (!(0,
|
|
2298
|
+
function walk3(dir, exts) {
|
|
2299
|
+
if (!(0, import_node_fs9.existsSync)(dir)) return [];
|
|
1821
2300
|
const results = [];
|
|
1822
|
-
for (const entry of (0,
|
|
2301
|
+
for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
|
|
1823
2302
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1824
|
-
const full = (0,
|
|
2303
|
+
const full = (0, import_node_path9.join)(dir, entry.name);
|
|
1825
2304
|
if (entry.isDirectory()) {
|
|
1826
|
-
results.push(...
|
|
1827
|
-
} else if (exts.includes((0,
|
|
2305
|
+
results.push(...walk3(full, exts));
|
|
2306
|
+
} else if (exts.includes((0, import_node_path9.extname)(entry.name))) {
|
|
1828
2307
|
results.push(full);
|
|
1829
2308
|
}
|
|
1830
2309
|
}
|
|
1831
|
-
return results;
|
|
2310
|
+
return results;
|
|
2311
|
+
}
|
|
2312
|
+
function toNodeId3(srcDir, absPath) {
|
|
2313
|
+
return (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2314
|
+
}
|
|
2315
|
+
var import_node_fs9, import_node_path9, URL_LITERAL_RE, urlLiteralScannerParser;
|
|
2316
|
+
var init_url_literal_scanner = __esm({
|
|
2317
|
+
"src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
|
|
2318
|
+
"use strict";
|
|
2319
|
+
import_node_fs9 = require("node:fs");
|
|
2320
|
+
import_node_path9 = require("node:path");
|
|
2321
|
+
init_api_route_matching();
|
|
2322
|
+
init_config();
|
|
2323
|
+
init_resolve_paths();
|
|
2324
|
+
URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
2325
|
+
urlLiteralScannerParser = {
|
|
2326
|
+
id: "url-literal-scanner",
|
|
2327
|
+
layer: "crosslayer",
|
|
2328
|
+
detect(rootDir) {
|
|
2329
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2330
|
+
return paths !== null;
|
|
2331
|
+
},
|
|
2332
|
+
generate(rootDir, layerOutputs) {
|
|
2333
|
+
const apiOutput = layerOutputs.get("api");
|
|
2334
|
+
if (!apiOutput) {
|
|
2335
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2336
|
+
}
|
|
2337
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2338
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2339
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2340
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2341
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2342
|
+
const srcDir = paths.srcDir;
|
|
2343
|
+
const clientDir = (0, import_node_path9.join)(srcDir, "client");
|
|
2344
|
+
const files = [
|
|
2345
|
+
...walk3(clientDir, [".ts", ".tsx"]),
|
|
2346
|
+
...walk3(paths.appDir, [".ts", ".tsx"])
|
|
2347
|
+
];
|
|
2348
|
+
const crossRefs = [];
|
|
2349
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2350
|
+
for (const absPath of files) {
|
|
2351
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
2352
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
2353
|
+
const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
|
|
2354
|
+
let match;
|
|
2355
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
2356
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
2357
|
+
const urlPath = match[1];
|
|
2358
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
2359
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
2360
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
2361
|
+
if (seen.has(key)) continue;
|
|
2362
|
+
seen.add(key);
|
|
2363
|
+
crossRefs.push({
|
|
2364
|
+
source: sourceId,
|
|
2365
|
+
target: result.nodeId,
|
|
2366
|
+
type: "references_api",
|
|
2367
|
+
layer: "api"
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return {
|
|
2373
|
+
cross_refs: crossRefs,
|
|
2374
|
+
flagged_edges: [],
|
|
2375
|
+
warnings: [],
|
|
2376
|
+
patterns: {
|
|
2377
|
+
url_literals_resolved: crossRefs.length
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
// src/server/graph/parsers/static/static-values.ts
|
|
2386
|
+
function tryLoadTreeSitter() {
|
|
2387
|
+
if (parseCode) return true;
|
|
2388
|
+
try {
|
|
2389
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2390
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
2391
|
+
parseCode = extractor.parseCodeTS;
|
|
2392
|
+
return true;
|
|
2393
|
+
}
|
|
2394
|
+
} catch {
|
|
2395
|
+
}
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
function classifyScope(source, model) {
|
|
2399
|
+
if (source.includes("prisma/schema.prisma")) return "shared";
|
|
2400
|
+
if (source.includes("prisma/seed") && model) {
|
|
2401
|
+
if (SHARED_MODELS.has(model)) return "shared";
|
|
2402
|
+
if (DB_MODELS.has(model)) return "db";
|
|
2403
|
+
return "shared";
|
|
2404
|
+
}
|
|
2405
|
+
if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
|
|
2406
|
+
if (source.startsWith("src/server/")) return "be";
|
|
2407
|
+
if (source.startsWith("src/config/")) return "be";
|
|
2408
|
+
if (source.startsWith("src/lib/")) return "shared";
|
|
2409
|
+
return "shared";
|
|
2410
|
+
}
|
|
2411
|
+
function extractEnumValues(rootDir) {
|
|
2412
|
+
const nodes = [];
|
|
2413
|
+
const edges = [];
|
|
2414
|
+
const schemaPaths = [
|
|
2415
|
+
(0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"),
|
|
2416
|
+
(0, import_node_path10.join)(rootDir, "prisma", "schema")
|
|
2417
|
+
];
|
|
2418
|
+
let content = "";
|
|
2419
|
+
for (const p of schemaPaths) {
|
|
2420
|
+
if ((0, import_node_fs10.existsSync)(p)) {
|
|
2421
|
+
try {
|
|
2422
|
+
const stat = (0, import_node_fs10.statSync)(p);
|
|
2423
|
+
if (stat.isFile()) {
|
|
2424
|
+
content = (0, import_node_fs10.readFileSync)(p, "utf-8");
|
|
2425
|
+
} else if (stat.isDirectory()) {
|
|
2426
|
+
const files = (0, import_node_fs10.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
|
|
2427
|
+
content = files.map((f) => (0, import_node_fs10.readFileSync)((0, import_node_path10.join)(p, f), "utf-8")).join("\n");
|
|
2428
|
+
}
|
|
2429
|
+
} catch {
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
break;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
if (!content) return { nodes, edges };
|
|
2436
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
2437
|
+
let m;
|
|
2438
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
2439
|
+
const enumName = m[1];
|
|
2440
|
+
const body = m[2];
|
|
2441
|
+
const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
2442
|
+
const scope = classifyScope("prisma/schema.prisma");
|
|
2443
|
+
for (const val of values) {
|
|
2444
|
+
const nodeId = `${enumName}.${val}`;
|
|
2445
|
+
nodes.push({
|
|
2446
|
+
id: nodeId,
|
|
2447
|
+
type: "enum_value",
|
|
2448
|
+
name: val,
|
|
2449
|
+
parentEnum: enumName,
|
|
2450
|
+
value: val,
|
|
2451
|
+
scope,
|
|
2452
|
+
source: "prisma/schema.prisma"
|
|
2453
|
+
});
|
|
2454
|
+
edges.push({
|
|
2455
|
+
source: nodeId,
|
|
2456
|
+
target: `enum:${enumName}`,
|
|
2457
|
+
type: "member_of"
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
nodes.push({
|
|
2461
|
+
id: `enum:${enumName}`,
|
|
2462
|
+
type: "enum_group",
|
|
2463
|
+
name: enumName,
|
|
2464
|
+
valueCount: values.length,
|
|
2465
|
+
scope,
|
|
2466
|
+
source: "prisma/schema.prisma"
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
return { nodes, edges };
|
|
2470
|
+
}
|
|
2471
|
+
function extractPropsFromObjectNode(node) {
|
|
2472
|
+
const props = {};
|
|
2473
|
+
for (const child of node.namedChildren) {
|
|
2474
|
+
if (child.type !== "pair") continue;
|
|
2475
|
+
const keyNode = child.childForFieldName("key");
|
|
2476
|
+
const valNode = child.childForFieldName("value");
|
|
2477
|
+
if (!keyNode || !valNode) continue;
|
|
2478
|
+
const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
|
|
2479
|
+
if (valNode.type === "string" || valNode.type === "template_string") {
|
|
2480
|
+
const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
|
|
2481
|
+
props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
|
|
2482
|
+
} else if (valNode.type === "number") {
|
|
2483
|
+
props[key] = valNode.text;
|
|
2484
|
+
} else if (valNode.type === "true" || valNode.type === "false") {
|
|
2485
|
+
props[key] = valNode.text;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return props;
|
|
2489
|
+
}
|
|
2490
|
+
function extractStringArrayFromNode(node) {
|
|
2491
|
+
const values = [];
|
|
2492
|
+
for (const child of node.namedChildren) {
|
|
2493
|
+
if (child.type === "string") {
|
|
2494
|
+
const frag = child.namedChildren.find((c) => c.type === "string_fragment");
|
|
2495
|
+
if (frag) values.push(frag.text);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
return values;
|
|
2499
|
+
}
|
|
2500
|
+
function findArrayDecl(root, varName) {
|
|
2501
|
+
function walk5(node) {
|
|
2502
|
+
if (node.type === "variable_declarator") {
|
|
2503
|
+
const nameNode = node.childForFieldName("name");
|
|
2504
|
+
const valueNode = node.childForFieldName("value");
|
|
2505
|
+
if (nameNode?.text === varName && valueNode?.type === "array") {
|
|
2506
|
+
return valueNode;
|
|
2507
|
+
}
|
|
2508
|
+
if (nameNode?.text === varName && valueNode?.type === "as_expression") {
|
|
2509
|
+
const inner = valueNode.namedChildren.find((c) => c.type === "array");
|
|
2510
|
+
if (inner) return inner;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
for (const child of node.namedChildren) {
|
|
2514
|
+
const found = walk5(child);
|
|
2515
|
+
if (found) return found;
|
|
2516
|
+
}
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
return walk5(root);
|
|
2520
|
+
}
|
|
2521
|
+
function extractObjectPropsRegex(objStr) {
|
|
2522
|
+
const props = {};
|
|
2523
|
+
const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
|
|
2524
|
+
let m;
|
|
2525
|
+
while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
|
|
2526
|
+
const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
|
|
2527
|
+
while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
|
|
2528
|
+
return props;
|
|
2529
|
+
}
|
|
2530
|
+
function extractStringArrayRegex(arrStr) {
|
|
2531
|
+
return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
|
|
2532
|
+
}
|
|
2533
|
+
function splitArrayObjectsRegex(arrayBody) {
|
|
2534
|
+
const objects = [];
|
|
2535
|
+
let depth = 0;
|
|
2536
|
+
let start = -1;
|
|
2537
|
+
for (let i = 0; i < arrayBody.length; i++) {
|
|
2538
|
+
if (arrayBody[i] === "{") {
|
|
2539
|
+
if (depth === 0) start = i;
|
|
2540
|
+
depth++;
|
|
2541
|
+
} else if (arrayBody[i] === "}") {
|
|
2542
|
+
depth--;
|
|
2543
|
+
if (depth === 0 && start >= 0) {
|
|
2544
|
+
objects.push(arrayBody.slice(start, i + 1));
|
|
2545
|
+
start = -1;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return objects;
|
|
2550
|
+
}
|
|
2551
|
+
function detectSeededArrays(content, sourceFile) {
|
|
2552
|
+
const results = [];
|
|
2553
|
+
const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
|
|
2554
|
+
let fm;
|
|
2555
|
+
while ((fm = forOfRe.exec(content)) !== null) {
|
|
2556
|
+
const arrayName = fm[1];
|
|
2557
|
+
const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
|
|
2558
|
+
const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
|
|
2559
|
+
if (!prismaMatch) continue;
|
|
2560
|
+
results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
|
|
2561
|
+
}
|
|
2562
|
+
return results;
|
|
2563
|
+
}
|
|
2564
|
+
function pickIdField(props) {
|
|
2565
|
+
for (const key of ["key", "slug", "id", "name", "templateId"]) {
|
|
2566
|
+
if (props[key]) return props[key];
|
|
2567
|
+
}
|
|
2568
|
+
return null;
|
|
2569
|
+
}
|
|
2570
|
+
function pickNameField(props) {
|
|
2571
|
+
for (const key of ["name", "slug", "key", "id"]) {
|
|
2572
|
+
if (props[key]) return props[key];
|
|
2573
|
+
}
|
|
2574
|
+
return null;
|
|
2575
|
+
}
|
|
2576
|
+
function modelToNodeType(model) {
|
|
2577
|
+
return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
|
|
2578
|
+
}
|
|
2579
|
+
function extractSeedData(rootDir) {
|
|
2580
|
+
const nodes = [];
|
|
2581
|
+
const edges = [];
|
|
2582
|
+
const seedFiles = [
|
|
2583
|
+
(0, import_node_path10.join)(rootDir, "prisma", "seed.ts"),
|
|
2584
|
+
(0, import_node_path10.join)(rootDir, "prisma", "seed.js"),
|
|
2585
|
+
(0, import_node_path10.join)(rootDir, "src", "server", "lib", "system-tags.ts")
|
|
2586
|
+
].filter(import_node_fs10.existsSync);
|
|
2587
|
+
const useTreeSitter = tryLoadTreeSitter();
|
|
2588
|
+
for (const filePath of seedFiles) {
|
|
2589
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2590
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2591
|
+
const seeded = detectSeededArrays(content, relPath);
|
|
2592
|
+
let astRoot = null;
|
|
2593
|
+
if (useTreeSitter && parseCode) {
|
|
2594
|
+
try {
|
|
2595
|
+
const tree = parseCode(content);
|
|
2596
|
+
astRoot = tree.rootNode;
|
|
2597
|
+
} catch {
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
for (const { arrayName, prismaModel, sourceFile } of seeded) {
|
|
2601
|
+
const nodeType = modelToNodeType(prismaModel);
|
|
2602
|
+
const scope = classifyScope(sourceFile, prismaModel);
|
|
2603
|
+
if (astRoot) {
|
|
2604
|
+
const arrayNode = findArrayDecl(astRoot, arrayName);
|
|
2605
|
+
if (!arrayNode) continue;
|
|
2606
|
+
for (const child of arrayNode.namedChildren) {
|
|
2607
|
+
if (child.type !== "object") continue;
|
|
2608
|
+
const props = extractPropsFromObjectNode(child);
|
|
2609
|
+
const idVal = pickIdField(props);
|
|
2610
|
+
if (!idVal) continue;
|
|
2611
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2612
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2613
|
+
const { scope: _seedScope, ...safeProps } = props;
|
|
2614
|
+
nodes.push({
|
|
2615
|
+
id: nodeId,
|
|
2616
|
+
type: nodeType,
|
|
2617
|
+
name: nameVal,
|
|
2618
|
+
value: idVal,
|
|
2619
|
+
model: prismaModel,
|
|
2620
|
+
...safeProps,
|
|
2621
|
+
seedScope: _seedScope ?? void 0,
|
|
2622
|
+
// preserve as seedScope
|
|
2623
|
+
scope,
|
|
2624
|
+
source: sourceFile
|
|
2625
|
+
});
|
|
2626
|
+
const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
|
|
2627
|
+
if (permsArrayNode) {
|
|
2628
|
+
const valNode = permsArrayNode.childForFieldName("value");
|
|
2629
|
+
if (valNode?.type === "array") {
|
|
2630
|
+
const permKeys = extractStringArrayFromNode(valNode);
|
|
2631
|
+
for (const pk of permKeys) {
|
|
2632
|
+
if (pk === "*") continue;
|
|
2633
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
} else {
|
|
2639
|
+
const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
|
|
2640
|
+
const cm = constRe.exec(content);
|
|
2641
|
+
if (!cm) continue;
|
|
2642
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2643
|
+
let depth = 1, i = bracketStart + 1;
|
|
2644
|
+
while (i < content.length && depth > 0) {
|
|
2645
|
+
if (content[i] === "[") depth++;
|
|
2646
|
+
else if (content[i] === "]") depth--;
|
|
2647
|
+
i++;
|
|
2648
|
+
}
|
|
2649
|
+
if (depth !== 0) continue;
|
|
2650
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2651
|
+
const objects = splitArrayObjectsRegex(body);
|
|
2652
|
+
for (const objStr of objects) {
|
|
2653
|
+
const props = extractObjectPropsRegex(objStr);
|
|
2654
|
+
const idVal = pickIdField(props);
|
|
2655
|
+
if (!idVal) continue;
|
|
2656
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2657
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2658
|
+
const { scope: _rScope, ...rSafeProps } = props;
|
|
2659
|
+
nodes.push({
|
|
2660
|
+
id: nodeId,
|
|
2661
|
+
type: nodeType,
|
|
2662
|
+
name: nameVal,
|
|
2663
|
+
value: idVal,
|
|
2664
|
+
model: prismaModel,
|
|
2665
|
+
...rSafeProps,
|
|
2666
|
+
seedScope: _rScope ?? void 0,
|
|
2667
|
+
scope,
|
|
2668
|
+
source: sourceFile
|
|
2669
|
+
});
|
|
2670
|
+
const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
|
|
2671
|
+
if (permArrayMatch) {
|
|
2672
|
+
for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
|
|
2673
|
+
if (pk === "*") continue;
|
|
2674
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return { nodes, edges };
|
|
2682
|
+
}
|
|
2683
|
+
function walkDir(dir, exts) {
|
|
2684
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
2685
|
+
const results = [];
|
|
2686
|
+
for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
|
|
2687
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2688
|
+
const full = (0, import_node_path10.join)(dir, entry.name);
|
|
2689
|
+
if (entry.isDirectory()) results.push(...walkDir(full, exts));
|
|
2690
|
+
else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
|
|
2691
|
+
}
|
|
2692
|
+
return results;
|
|
2693
|
+
}
|
|
2694
|
+
function extractConstants(rootDir) {
|
|
2695
|
+
const nodes = [];
|
|
2696
|
+
const srcDir = (0, import_node_path10.join)(rootDir, "src");
|
|
2697
|
+
if (!(0, import_node_fs10.existsSync)(srcDir)) return { nodes };
|
|
2698
|
+
for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
|
|
2699
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2700
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2701
|
+
const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
|
|
2702
|
+
let cm;
|
|
2703
|
+
while ((cm = constArrayRe.exec(content)) !== null) {
|
|
2704
|
+
const constName = cm[1];
|
|
2705
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2706
|
+
let depth = 1, i = bracketStart + 1;
|
|
2707
|
+
while (i < content.length && depth > 0) {
|
|
2708
|
+
if (content[i] === "[") depth++;
|
|
2709
|
+
else if (content[i] === "]") depth--;
|
|
2710
|
+
i++;
|
|
2711
|
+
}
|
|
2712
|
+
if (depth !== 0) continue;
|
|
2713
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2714
|
+
const stringValues = extractStringArrayRegex(body);
|
|
2715
|
+
const objectCount = splitArrayObjectsRegex(body).length;
|
|
2716
|
+
const valueCount = Math.max(stringValues.length, objectCount);
|
|
2717
|
+
if (valueCount < 2) continue;
|
|
2718
|
+
const scope = classifyScope(relPath);
|
|
2719
|
+
nodes.push({
|
|
2720
|
+
id: `const:${constName}`,
|
|
2721
|
+
type: "constant",
|
|
2722
|
+
name: constName,
|
|
2723
|
+
valueCount,
|
|
2724
|
+
values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
|
|
2725
|
+
scope,
|
|
2726
|
+
source: relPath
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return { nodes };
|
|
2731
|
+
}
|
|
2732
|
+
function detect4(rootDir) {
|
|
2733
|
+
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"));
|
|
2734
|
+
}
|
|
2735
|
+
function generate4(rootDir) {
|
|
2736
|
+
const enumResult = extractEnumValues(rootDir);
|
|
2737
|
+
const seedResult = extractSeedData(rootDir);
|
|
2738
|
+
const constResult = extractConstants(rootDir);
|
|
2739
|
+
const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
|
|
2740
|
+
const allEdges = [...enumResult.edges, ...seedResult.edges];
|
|
2741
|
+
const typeOrder = {
|
|
2742
|
+
enum_group: 0,
|
|
2743
|
+
enum_value: 1,
|
|
2744
|
+
seed_permission: 2,
|
|
2745
|
+
seed_role: 3,
|
|
2746
|
+
seed_tag: 4,
|
|
2747
|
+
seed_plan: 5,
|
|
2748
|
+
seed_provider: 6,
|
|
2749
|
+
constant: 7
|
|
2750
|
+
};
|
|
2751
|
+
allNodes.sort((a, b) => {
|
|
2752
|
+
const ta = typeOrder[a.type] ?? 8;
|
|
2753
|
+
const tb = typeOrder[b.type] ?? 8;
|
|
2754
|
+
if (ta !== tb) return ta - tb;
|
|
2755
|
+
return a.name.localeCompare(b.name);
|
|
2756
|
+
});
|
|
2757
|
+
const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
|
|
2758
|
+
const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
|
|
2759
|
+
const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
|
|
2760
|
+
const constNodes = allNodes.filter((n) => n.type === "constant").length;
|
|
2761
|
+
const byScope = {};
|
|
2762
|
+
for (const n of allNodes) {
|
|
2763
|
+
const s = n.scope ?? "unknown";
|
|
2764
|
+
byScope[s] = (byScope[s] ?? 0) + 1;
|
|
2765
|
+
}
|
|
2766
|
+
return {
|
|
2767
|
+
metadata: {
|
|
2768
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2769
|
+
scope: "static-values",
|
|
2770
|
+
layer: "static",
|
|
2771
|
+
enum_groups: enumGroups,
|
|
2772
|
+
enum_values: enumValues,
|
|
2773
|
+
seed_records: seedNodes,
|
|
2774
|
+
constants: constNodes,
|
|
2775
|
+
total_nodes: allNodes.length,
|
|
2776
|
+
total_edges: allEdges.length,
|
|
2777
|
+
parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
|
|
2778
|
+
},
|
|
2779
|
+
nodes: allNodes,
|
|
2780
|
+
edges: allEdges,
|
|
2781
|
+
cross_refs: [],
|
|
2782
|
+
contradictions: [],
|
|
2783
|
+
warnings: [],
|
|
2784
|
+
flagged_edges: [],
|
|
2785
|
+
patterns: {
|
|
2786
|
+
by_type: {
|
|
2787
|
+
enum_group: enumGroups,
|
|
2788
|
+
enum_value: enumValues,
|
|
2789
|
+
seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
|
|
2790
|
+
seed_role: allNodes.filter((n) => n.type === "seed_role").length,
|
|
2791
|
+
seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
|
|
2792
|
+
seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
|
|
2793
|
+
seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
|
|
2794
|
+
constant: constNodes
|
|
2795
|
+
},
|
|
2796
|
+
by_scope: byScope
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
var import_node_fs10, import_node_path10, parseCode, SHARED_MODELS, DB_MODELS, staticValuesParser;
|
|
2801
|
+
var init_static_values = __esm({
|
|
2802
|
+
"src/server/graph/parsers/static/static-values.ts"() {
|
|
2803
|
+
"use strict";
|
|
2804
|
+
import_node_fs10 = require("node:fs");
|
|
2805
|
+
import_node_path10 = require("node:path");
|
|
2806
|
+
parseCode = null;
|
|
2807
|
+
SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
|
|
2808
|
+
DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
|
|
2809
|
+
staticValuesParser = {
|
|
2810
|
+
id: "static-values",
|
|
2811
|
+
layer: "static",
|
|
2812
|
+
detect: detect4,
|
|
2813
|
+
generate: generate4
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
// src/server/graph/parsers/crosslayer/static-ref-scanner.ts
|
|
2819
|
+
function walk4(dir, exts) {
|
|
2820
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return [];
|
|
2821
|
+
const results = [];
|
|
2822
|
+
function recurse(d) {
|
|
2823
|
+
for (const entry of (0, import_node_fs11.readdirSync)(d, { withFileTypes: true })) {
|
|
2824
|
+
const full = (0, import_node_path11.join)(d, entry.name);
|
|
2825
|
+
if (entry.isDirectory()) {
|
|
2826
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2827
|
+
recurse(full);
|
|
2828
|
+
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
|
2829
|
+
results.push(full);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
recurse(dir);
|
|
2834
|
+
return results;
|
|
2835
|
+
}
|
|
2836
|
+
function isInCommentOrType(node) {
|
|
2837
|
+
let current = node.parent;
|
|
2838
|
+
while (current) {
|
|
2839
|
+
if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
|
|
2840
|
+
return true;
|
|
2841
|
+
}
|
|
2842
|
+
current = current.parent;
|
|
2843
|
+
}
|
|
2844
|
+
return false;
|
|
2845
|
+
}
|
|
2846
|
+
function collectStaticRefs(root, valueLookup) {
|
|
2847
|
+
const refs = [];
|
|
2848
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2849
|
+
function visit(node) {
|
|
2850
|
+
if (node.type === "string_fragment") {
|
|
2851
|
+
const val = node.text;
|
|
2852
|
+
if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
|
|
2853
|
+
const targets = valueLookup.get(val);
|
|
2854
|
+
if (targets && !isInCommentOrType(node)) {
|
|
2855
|
+
for (const t of targets) {
|
|
2856
|
+
const key = t;
|
|
2857
|
+
if (!seen.has(key)) {
|
|
2858
|
+
seen.add(key);
|
|
2859
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
if (node.type === "member_expression") {
|
|
2866
|
+
const objNode = node.namedChildren.find((c) => c.type === "identifier");
|
|
2867
|
+
const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
|
|
2868
|
+
if (objNode && propNode) {
|
|
2869
|
+
const combined = `${objNode.text}.${propNode.text}`;
|
|
2870
|
+
const targets = valueLookup.get(propNode.text);
|
|
2871
|
+
const directTarget = valueLookup.get(combined);
|
|
2872
|
+
if (directTarget && !isInCommentOrType(node)) {
|
|
2873
|
+
for (const t of directTarget) {
|
|
2874
|
+
if (!seen.has(t)) {
|
|
2875
|
+
seen.add(t);
|
|
2876
|
+
refs.push({ value: combined, targetIds: [t] });
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
} else if (targets && !isInCommentOrType(node)) {
|
|
2880
|
+
for (const t of targets) {
|
|
2881
|
+
if (!seen.has(t)) {
|
|
2882
|
+
seen.add(t);
|
|
2883
|
+
refs.push({ value: propNode.text, targetIds: [t] });
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
for (const child of node.namedChildren) {
|
|
2890
|
+
visit(child);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
visit(root);
|
|
2894
|
+
return refs;
|
|
1832
2895
|
}
|
|
1833
|
-
function
|
|
1834
|
-
|
|
2896
|
+
function collectStaticRefsRegex(content, valueLookup, allValues) {
|
|
2897
|
+
const refs = [];
|
|
2898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2899
|
+
const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2900
|
+
const pattern = new RegExp(
|
|
2901
|
+
`(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
|
|
2902
|
+
"g"
|
|
2903
|
+
);
|
|
2904
|
+
let match;
|
|
2905
|
+
pattern.lastIndex = 0;
|
|
2906
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
2907
|
+
const val = match[1] ?? match[3];
|
|
2908
|
+
if (!val) continue;
|
|
2909
|
+
const targets = valueLookup.get(val);
|
|
2910
|
+
if (!targets) continue;
|
|
2911
|
+
for (const t of targets) {
|
|
2912
|
+
if (!seen.has(t)) {
|
|
2913
|
+
seen.add(t);
|
|
2914
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return refs;
|
|
1835
2919
|
}
|
|
1836
|
-
var
|
|
1837
|
-
var
|
|
1838
|
-
"src/server/graph/parsers/crosslayer/
|
|
2920
|
+
var import_node_fs11, import_node_path11, MIN_VALUE_LENGTH, SKIP_VALUES, staticRefScannerParser;
|
|
2921
|
+
var init_static_ref_scanner = __esm({
|
|
2922
|
+
"src/server/graph/parsers/crosslayer/static-ref-scanner.ts"() {
|
|
1839
2923
|
"use strict";
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2924
|
+
import_node_fs11 = require("node:fs");
|
|
2925
|
+
import_node_path11 = require("node:path");
|
|
2926
|
+
init_config();
|
|
2927
|
+
init_resolve_paths();
|
|
2928
|
+
MIN_VALUE_LENGTH = 4;
|
|
2929
|
+
SKIP_VALUES = /* @__PURE__ */ new Set([
|
|
2930
|
+
"true",
|
|
2931
|
+
"false",
|
|
2932
|
+
"null",
|
|
2933
|
+
"undefined",
|
|
2934
|
+
"none",
|
|
2935
|
+
"default",
|
|
2936
|
+
"name",
|
|
2937
|
+
"type",
|
|
2938
|
+
"data",
|
|
2939
|
+
"text",
|
|
2940
|
+
"info",
|
|
2941
|
+
"error",
|
|
2942
|
+
"open",
|
|
2943
|
+
"read",
|
|
2944
|
+
"user",
|
|
2945
|
+
"test",
|
|
2946
|
+
"json",
|
|
2947
|
+
"form"
|
|
2948
|
+
]);
|
|
2949
|
+
staticRefScannerParser = {
|
|
2950
|
+
id: "static-ref-scanner",
|
|
1846
2951
|
layer: "crosslayer",
|
|
1847
2952
|
detect(rootDir) {
|
|
1848
|
-
|
|
2953
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2954
|
+
return paths !== null;
|
|
1849
2955
|
},
|
|
1850
2956
|
generate(rootDir, layerOutputs) {
|
|
1851
|
-
const
|
|
1852
|
-
if (!
|
|
2957
|
+
const staticOutput = layerOutputs.get("static");
|
|
2958
|
+
if (!staticOutput || staticOutput.nodes.length === 0) {
|
|
1853
2959
|
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1854
2960
|
}
|
|
1855
|
-
const
|
|
1856
|
-
const
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2961
|
+
const valueLookup = /* @__PURE__ */ new Map();
|
|
2962
|
+
for (const node of staticOutput.nodes) {
|
|
2963
|
+
const type = node.type;
|
|
2964
|
+
let valueStr = null;
|
|
2965
|
+
if (type === "enum_value") {
|
|
2966
|
+
valueStr = node.value;
|
|
2967
|
+
const fullId = node.id;
|
|
2968
|
+
if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
|
|
2969
|
+
valueLookup.get(fullId).push(node.id);
|
|
2970
|
+
} else if (type.startsWith("seed_")) {
|
|
2971
|
+
valueStr = node.value;
|
|
2972
|
+
}
|
|
2973
|
+
if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
|
|
2974
|
+
if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
|
|
2975
|
+
valueLookup.get(valueStr).push(node.id);
|
|
2976
|
+
}
|
|
2977
|
+
const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
|
|
2978
|
+
if (allValues.length === 0) {
|
|
2979
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2980
|
+
}
|
|
2981
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2982
|
+
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2983
|
+
const srcDir = paths.srcDir;
|
|
1862
2984
|
const files = [
|
|
1863
|
-
...walk4(
|
|
1864
|
-
...walk4(appDir, [".ts", ".tsx"])
|
|
2985
|
+
...walk4((0, import_node_path11.join)(srcDir, "client"), [".ts", ".tsx"]),
|
|
2986
|
+
...walk4(paths.appDir, [".ts", ".tsx"]),
|
|
2987
|
+
...walk4((0, import_node_path11.join)(srcDir, "server"), [".ts", ".tsx"]),
|
|
2988
|
+
...walk4((0, import_node_path11.join)(srcDir, "lib"), [".ts", ".tsx"]),
|
|
2989
|
+
...walk4((0, import_node_path11.join)(srcDir, "config"), [".ts", ".tsx"])
|
|
1865
2990
|
];
|
|
2991
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2992
|
+
const apiOutput = layerOutputs.get("api");
|
|
2993
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2994
|
+
const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2995
|
+
let parseCode2 = null;
|
|
2996
|
+
try {
|
|
2997
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2998
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
2999
|
+
parseCode2 = extractor.parseCodeTS;
|
|
3000
|
+
}
|
|
3001
|
+
} catch {
|
|
3002
|
+
}
|
|
1866
3003
|
const crossRefs = [];
|
|
1867
3004
|
const seen = /* @__PURE__ */ new Set();
|
|
3005
|
+
let filesScanned = 0;
|
|
1868
3006
|
for (const absPath of files) {
|
|
1869
|
-
const sourceId =
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
3007
|
+
const sourceId = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
3008
|
+
const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
|
|
3009
|
+
if (!sourceLayer) continue;
|
|
3010
|
+
const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
|
|
3011
|
+
filesScanned++;
|
|
3012
|
+
let fileRefs;
|
|
3013
|
+
if (parseCode2) {
|
|
3014
|
+
try {
|
|
3015
|
+
const tree = parseCode2(content);
|
|
3016
|
+
fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
|
|
3017
|
+
} catch {
|
|
3018
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
3019
|
+
}
|
|
3020
|
+
} else {
|
|
3021
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
3022
|
+
}
|
|
3023
|
+
for (const ref of fileRefs) {
|
|
3024
|
+
for (const targetId of ref.targetIds) {
|
|
3025
|
+
const key = `${sourceId}|${targetId}`;
|
|
1879
3026
|
if (seen.has(key)) continue;
|
|
1880
3027
|
seen.add(key);
|
|
1881
3028
|
crossRefs.push({
|
|
1882
3029
|
source: sourceId,
|
|
1883
|
-
target:
|
|
1884
|
-
type: "
|
|
1885
|
-
layer: "
|
|
3030
|
+
target: targetId,
|
|
3031
|
+
type: "references_static",
|
|
3032
|
+
layer: "static"
|
|
1886
3033
|
});
|
|
1887
3034
|
}
|
|
1888
3035
|
}
|
|
@@ -1892,7 +3039,10 @@ var init_url_literal_scanner = __esm({
|
|
|
1892
3039
|
flagged_edges: [],
|
|
1893
3040
|
warnings: [],
|
|
1894
3041
|
patterns: {
|
|
1895
|
-
|
|
3042
|
+
files_scanned: filesScanned,
|
|
3043
|
+
static_values_tracked: valueLookup.size,
|
|
3044
|
+
references_found: crossRefs.length,
|
|
3045
|
+
parser: parseCode2 ? "tree-sitter" : "regex-fallback"
|
|
1896
3046
|
}
|
|
1897
3047
|
};
|
|
1898
3048
|
}
|
|
@@ -1901,14 +3051,19 @@ var init_url_literal_scanner = __esm({
|
|
|
1901
3051
|
});
|
|
1902
3052
|
|
|
1903
3053
|
// src/server/graph/core/parser-registry.ts
|
|
3054
|
+
function isMultiLayerParser(p) {
|
|
3055
|
+
return "layers" in p && Array.isArray(p.layers);
|
|
3056
|
+
}
|
|
1904
3057
|
function registerBuiltins(registry, disabled) {
|
|
1905
3058
|
const builtins = [
|
|
1906
|
-
|
|
1907
|
-
nextjsRoutesParser,
|
|
3059
|
+
typescriptProjectParser,
|
|
1908
3060
|
prismaSchemaParser,
|
|
3061
|
+
sqlMigrationsParser,
|
|
3062
|
+
staticValuesParser,
|
|
1909
3063
|
fetchResolverParser,
|
|
1910
3064
|
apiAnnotationsParser,
|
|
1911
|
-
urlLiteralScannerParser
|
|
3065
|
+
urlLiteralScannerParser,
|
|
3066
|
+
staticRefScannerParser
|
|
1912
3067
|
];
|
|
1913
3068
|
for (const parser of builtins) {
|
|
1914
3069
|
if (disabled.has(parser.id)) continue;
|
|
@@ -1918,11 +3073,11 @@ function registerBuiltins(registry, disabled) {
|
|
|
1918
3073
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1919
3074
|
for (const entry of config.parsers?.custom ?? []) {
|
|
1920
3075
|
try {
|
|
1921
|
-
const absPath = (0,
|
|
3076
|
+
const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
|
|
1922
3077
|
const mod = require(absPath);
|
|
1923
3078
|
const parser = "default" in mod ? mod.default : mod;
|
|
1924
3079
|
if (disabled.has(parser.id)) continue;
|
|
1925
|
-
if (parser.layer !== entry.layer) {
|
|
3080
|
+
if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
|
|
1926
3081
|
process.stderr.write(
|
|
1927
3082
|
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1928
3083
|
`
|
|
@@ -1942,20 +3097,23 @@ function createRegistry(config, rootDir) {
|
|
|
1942
3097
|
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1943
3098
|
return registry;
|
|
1944
3099
|
}
|
|
1945
|
-
var
|
|
3100
|
+
var import_node_path12, ParserRegistry;
|
|
1946
3101
|
var init_parser_registry = __esm({
|
|
1947
3102
|
"src/server/graph/core/parser-registry.ts"() {
|
|
1948
3103
|
"use strict";
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
init_nextjs_routes();
|
|
3104
|
+
import_node_path12 = require("node:path");
|
|
3105
|
+
init_typescript_project();
|
|
1952
3106
|
init_prisma_schema();
|
|
3107
|
+
init_sql_migrations();
|
|
1953
3108
|
init_fetch_resolver();
|
|
1954
3109
|
init_api_annotations();
|
|
1955
3110
|
init_url_literal_scanner();
|
|
3111
|
+
init_static_values();
|
|
3112
|
+
init_static_ref_scanner();
|
|
1956
3113
|
ParserRegistry = class {
|
|
1957
3114
|
constructor() {
|
|
1958
|
-
this.
|
|
3115
|
+
this.singleLayerParsers = /* @__PURE__ */ new Map();
|
|
3116
|
+
this.multiLayerParsers = [];
|
|
1959
3117
|
this.ids = /* @__PURE__ */ new Set();
|
|
1960
3118
|
}
|
|
1961
3119
|
register(parser) {
|
|
@@ -1963,19 +3121,44 @@ var init_parser_registry = __esm({
|
|
|
1963
3121
|
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1964
3122
|
}
|
|
1965
3123
|
this.ids.add(parser.id);
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
3124
|
+
if (isMultiLayerParser(parser)) {
|
|
3125
|
+
this.multiLayerParsers.push(parser);
|
|
3126
|
+
} else {
|
|
3127
|
+
const list = this.singleLayerParsers.get(parser.layer) ?? [];
|
|
3128
|
+
list.push(parser);
|
|
3129
|
+
this.singleLayerParsers.set(parser.layer, list);
|
|
3130
|
+
}
|
|
1969
3131
|
}
|
|
3132
|
+
/** Get single-layer parsers for a specific layer. */
|
|
1970
3133
|
getParsers(layer) {
|
|
1971
|
-
return this.
|
|
3134
|
+
return this.singleLayerParsers.get(layer) ?? [];
|
|
3135
|
+
}
|
|
3136
|
+
/** Get multi-layer parsers that can produce output for the given layer. */
|
|
3137
|
+
getMultiLayerParsersFor(layer) {
|
|
3138
|
+
return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
|
|
3139
|
+
}
|
|
3140
|
+
/** Get all multi-layer parsers. */
|
|
3141
|
+
getMultiLayerParsers() {
|
|
3142
|
+
return [...this.multiLayerParsers];
|
|
1972
3143
|
}
|
|
1973
3144
|
getCrossLayerParsers() {
|
|
1974
|
-
return this.
|
|
3145
|
+
return this.singleLayerParsers.get("crosslayer") ?? [];
|
|
3146
|
+
}
|
|
3147
|
+
/** All layers that registered parsers can produce (single + multi). */
|
|
3148
|
+
getAvailableLayers() {
|
|
3149
|
+
const layers = /* @__PURE__ */ new Set();
|
|
3150
|
+
for (const key of this.singleLayerParsers.keys()) {
|
|
3151
|
+
if (key !== "crosslayer") layers.add(key);
|
|
3152
|
+
}
|
|
3153
|
+
for (const mp of this.multiLayerParsers) {
|
|
3154
|
+
for (const l of mp.layers) layers.add(l);
|
|
3155
|
+
}
|
|
3156
|
+
return [...layers];
|
|
1975
3157
|
}
|
|
1976
3158
|
getAll() {
|
|
1977
3159
|
const all = [];
|
|
1978
|
-
for (const list of this.
|
|
3160
|
+
for (const list of this.singleLayerParsers.values()) all.push(...list);
|
|
3161
|
+
all.push(...this.multiLayerParsers);
|
|
1979
3162
|
return all;
|
|
1980
3163
|
}
|
|
1981
3164
|
};
|
|
@@ -2112,10 +3295,10 @@ var init_merge = __esm({
|
|
|
2112
3295
|
|
|
2113
3296
|
// src/server/graph/core/graph-builder.ts
|
|
2114
3297
|
function readGraphFromDisk(rootDir, layer) {
|
|
2115
|
-
const filePath = (0,
|
|
2116
|
-
if (!(0,
|
|
3298
|
+
const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3299
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) return null;
|
|
2117
3300
|
try {
|
|
2118
|
-
return JSON.parse((0,
|
|
3301
|
+
return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
|
|
2119
3302
|
} catch {
|
|
2120
3303
|
return null;
|
|
2121
3304
|
}
|
|
@@ -2123,18 +3306,24 @@ function readGraphFromDisk(rootDir, layer) {
|
|
|
2123
3306
|
function generateLayer(rootDir, layer) {
|
|
2124
3307
|
const config = loadConfig(rootDir);
|
|
2125
3308
|
const registry = createRegistry(config, rootDir);
|
|
2126
|
-
const parsers = registry.getParsers(layer);
|
|
2127
3309
|
const outputs = [];
|
|
2128
|
-
for (const parser of
|
|
3310
|
+
for (const parser of registry.getParsers(layer)) {
|
|
2129
3311
|
if (!parser.detect(rootDir)) continue;
|
|
2130
3312
|
outputs.push(parser.generate(rootDir));
|
|
2131
3313
|
}
|
|
3314
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3315
|
+
if (!mp.detect(rootDir)) continue;
|
|
3316
|
+
const multiOutput = mp.generate(rootDir);
|
|
3317
|
+
const layerOutput = multiOutput.get(layer);
|
|
3318
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3319
|
+
}
|
|
2132
3320
|
if (outputs.length === 0) return null;
|
|
2133
3321
|
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
2134
3322
|
if (layer === "ui") {
|
|
2135
3323
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
2136
3324
|
layerOutputs.set("ui", merged);
|
|
2137
|
-
for (const otherLayer of
|
|
3325
|
+
for (const otherLayer of registry.getAvailableLayers()) {
|
|
3326
|
+
if (otherLayer === "ui") continue;
|
|
2138
3327
|
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
2139
3328
|
if (existing) layerOutputs.set(otherLayer, existing);
|
|
2140
3329
|
}
|
|
@@ -2155,16 +3344,29 @@ function generateLayer(rootDir, layer) {
|
|
|
2155
3344
|
function generateAll(rootDir) {
|
|
2156
3345
|
const config = loadConfig(rootDir);
|
|
2157
3346
|
const registry = createRegistry(config, rootDir);
|
|
2158
|
-
const
|
|
3347
|
+
const allLayers = registry.getAvailableLayers();
|
|
3348
|
+
const layerOrder = [
|
|
3349
|
+
...allLayers.filter((l) => l !== "ui"),
|
|
3350
|
+
...allLayers.filter((l) => l === "ui")
|
|
3351
|
+
];
|
|
2159
3352
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
2160
3353
|
const results = [];
|
|
3354
|
+
const multiLayerResults = /* @__PURE__ */ new Map();
|
|
2161
3355
|
for (const layer of layerOrder) {
|
|
2162
|
-
const parsers = registry.getParsers(layer);
|
|
2163
3356
|
const outputs = [];
|
|
2164
|
-
for (const parser of
|
|
3357
|
+
for (const parser of registry.getParsers(layer)) {
|
|
2165
3358
|
if (!parser.detect(rootDir)) continue;
|
|
2166
3359
|
outputs.push(parser.generate(rootDir));
|
|
2167
3360
|
}
|
|
3361
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3362
|
+
if (!mp.detect(rootDir)) continue;
|
|
3363
|
+
if (!multiLayerResults.has(mp.id)) {
|
|
3364
|
+
multiLayerResults.set(mp.id, mp.generate(rootDir));
|
|
3365
|
+
}
|
|
3366
|
+
const cached = multiLayerResults.get(mp.id);
|
|
3367
|
+
const layerOutput = cached.get(layer);
|
|
3368
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3369
|
+
}
|
|
2168
3370
|
if (outputs.length === 0) continue;
|
|
2169
3371
|
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
2170
3372
|
layerOutputs.set(layer, merged);
|
|
@@ -2190,14 +3392,16 @@ function generateAll(rootDir) {
|
|
|
2190
3392
|
}
|
|
2191
3393
|
}
|
|
2192
3394
|
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
2193
|
-
|
|
3395
|
+
const wellKnownOrder = ["ui", "api", "db"];
|
|
3396
|
+
const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
|
|
3397
|
+
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
2194
3398
|
}
|
|
2195
|
-
var
|
|
3399
|
+
var import_node_fs12, import_node_path13;
|
|
2196
3400
|
var init_graph_builder = __esm({
|
|
2197
3401
|
"src/server/graph/core/graph-builder.ts"() {
|
|
2198
3402
|
"use strict";
|
|
2199
|
-
|
|
2200
|
-
|
|
3403
|
+
import_node_fs12 = require("node:fs");
|
|
3404
|
+
import_node_path13 = require("node:path");
|
|
2201
3405
|
init_config();
|
|
2202
3406
|
init_parser_registry();
|
|
2203
3407
|
init_merge();
|
|
@@ -2236,18 +3440,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
2236
3440
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
2237
3441
|
const searchDirs = [
|
|
2238
3442
|
rootDir,
|
|
2239
|
-
(0,
|
|
2240
|
-
(0,
|
|
2241
|
-
(0,
|
|
3443
|
+
(0, import_node_path14.join)(rootDir, "src"),
|
|
3444
|
+
(0, import_node_path14.join)(rootDir, "app"),
|
|
3445
|
+
(0, import_node_path14.join)(rootDir, "lib")
|
|
2242
3446
|
];
|
|
2243
3447
|
for (const base of searchDirs) {
|
|
2244
3448
|
for (const convention of conventionDirs) {
|
|
2245
|
-
const dir = (0,
|
|
2246
|
-
if (!(0,
|
|
3449
|
+
const dir = (0, import_node_path14.join)(base, convention);
|
|
3450
|
+
if (!(0, import_node_fs13.existsSync)(dir)) continue;
|
|
2247
3451
|
try {
|
|
2248
|
-
const stat = (0,
|
|
3452
|
+
const stat = (0, import_node_fs13.statSync)(dir);
|
|
2249
3453
|
if (!stat.isDirectory()) continue;
|
|
2250
|
-
const entries = (0,
|
|
3454
|
+
const entries = (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
2251
3455
|
if (entries.length > 0) {
|
|
2252
3456
|
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
2253
3457
|
result.set(relPath, entries);
|
|
@@ -2319,12 +3523,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
|
|
|
2319
3523
|
}
|
|
2320
3524
|
return "root";
|
|
2321
3525
|
}
|
|
2322
|
-
var
|
|
3526
|
+
var import_node_fs13, import_node_path14, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
2323
3527
|
var init_module_tagger = __esm({
|
|
2324
3528
|
"src/server/graph/taggers/module-tagger.ts"() {
|
|
2325
3529
|
"use strict";
|
|
2326
|
-
|
|
2327
|
-
|
|
3530
|
+
import_node_fs13 = require("node:fs");
|
|
3531
|
+
import_node_path14 = require("node:path");
|
|
2328
3532
|
CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
2329
3533
|
GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
2330
3534
|
// JS/TS
|
|
@@ -2536,7 +3740,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
2536
3740
|
for (const entry of config.taggers?.custom ?? []) {
|
|
2537
3741
|
if (disabled.has(entry.id)) continue;
|
|
2538
3742
|
try {
|
|
2539
|
-
const absPath = (0,
|
|
3743
|
+
const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
|
|
2540
3744
|
const mod = require(absPath);
|
|
2541
3745
|
const tagger = "default" in mod ? mod.default : mod;
|
|
2542
3746
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -2557,11 +3761,11 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
2557
3761
|
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
2558
3762
|
return registry;
|
|
2559
3763
|
}
|
|
2560
|
-
var
|
|
3764
|
+
var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
|
|
2561
3765
|
var init_tagger_registry = __esm({
|
|
2562
3766
|
"src/server/graph/core/tagger-registry.ts"() {
|
|
2563
3767
|
"use strict";
|
|
2564
|
-
|
|
3768
|
+
import_node_path15 = require("node:path");
|
|
2565
3769
|
init_module_tagger();
|
|
2566
3770
|
init_screen_tagger();
|
|
2567
3771
|
TaggerRegistry = class {
|
|
@@ -2589,18 +3793,18 @@ var init_tagger_registry = __esm({
|
|
|
2589
3793
|
|
|
2590
3794
|
// src/server/graph/core/tag-store.ts
|
|
2591
3795
|
function tagsFilePath(rootDir) {
|
|
2592
|
-
return (0,
|
|
3796
|
+
return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
2593
3797
|
}
|
|
2594
3798
|
function readTagStore(rootDir) {
|
|
2595
3799
|
const filePath = tagsFilePath(rootDir);
|
|
2596
|
-
if (!(0,
|
|
2597
|
-
const stat = (0,
|
|
3800
|
+
if (!(0, import_node_fs14.existsSync)(filePath)) return {};
|
|
3801
|
+
const stat = (0, import_node_fs14.statSync)(filePath);
|
|
2598
3802
|
const cached = tagCache.get(filePath);
|
|
2599
3803
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2600
3804
|
return cached.store;
|
|
2601
3805
|
}
|
|
2602
3806
|
try {
|
|
2603
|
-
const content = (0,
|
|
3807
|
+
const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
|
|
2604
3808
|
const store = JSON.parse(content);
|
|
2605
3809
|
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
2606
3810
|
return store;
|
|
@@ -2610,15 +3814,15 @@ function readTagStore(rootDir) {
|
|
|
2610
3814
|
}
|
|
2611
3815
|
function writeTagStore(rootDir, store) {
|
|
2612
3816
|
const filePath = tagsFilePath(rootDir);
|
|
2613
|
-
const dir = (0,
|
|
2614
|
-
(0,
|
|
3817
|
+
const dir = (0, import_node_path16.dirname)(filePath);
|
|
3818
|
+
(0, import_node_fs14.mkdirSync)(dir, { recursive: true });
|
|
2615
3819
|
const cleaned = {};
|
|
2616
3820
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
2617
3821
|
if (Object.keys(tags).length > 0) {
|
|
2618
3822
|
cleaned[nodeId] = tags;
|
|
2619
3823
|
}
|
|
2620
3824
|
}
|
|
2621
|
-
(0,
|
|
3825
|
+
(0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
2622
3826
|
tagCache.delete(filePath);
|
|
2623
3827
|
}
|
|
2624
3828
|
function setTag(rootDir, nodeId, key, value) {
|
|
@@ -2636,12 +3840,12 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
2636
3840
|
}
|
|
2637
3841
|
writeTagStore(rootDir, store);
|
|
2638
3842
|
}
|
|
2639
|
-
var
|
|
3843
|
+
var import_node_fs14, import_node_path16, TAGS_FILENAME, GRAPHS_DIR, tagCache;
|
|
2640
3844
|
var init_tag_store = __esm({
|
|
2641
3845
|
"src/server/graph/core/tag-store.ts"() {
|
|
2642
3846
|
"use strict";
|
|
2643
|
-
|
|
2644
|
-
|
|
3847
|
+
import_node_fs14 = require("node:fs");
|
|
3848
|
+
import_node_path16 = require("node:path");
|
|
2645
3849
|
TAGS_FILENAME = "tags.json";
|
|
2646
3850
|
GRAPHS_DIR = ".launchsecure/graphs";
|
|
2647
3851
|
tagCache = /* @__PURE__ */ new Map();
|
|
@@ -2649,18 +3853,23 @@ var init_tag_store = __esm({
|
|
|
2649
3853
|
});
|
|
2650
3854
|
|
|
2651
3855
|
// src/server/graph/index.ts
|
|
3856
|
+
function getAvailableLayers(rootDir) {
|
|
3857
|
+
const dir = (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
|
|
3858
|
+
if (!(0, import_node_fs15.existsSync)(dir)) return [];
|
|
3859
|
+
return (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
|
|
3860
|
+
}
|
|
2652
3861
|
function graphsDir(rootDir) {
|
|
2653
|
-
return (0,
|
|
3862
|
+
return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
|
|
2654
3863
|
}
|
|
2655
3864
|
function graphFilePath(rootDir, layer) {
|
|
2656
|
-
return (0,
|
|
3865
|
+
return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
|
|
2657
3866
|
}
|
|
2658
3867
|
function tagsFilePath2(rootDir) {
|
|
2659
|
-
return (0,
|
|
3868
|
+
return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
|
|
2660
3869
|
}
|
|
2661
3870
|
function getMtimeMs(filePath) {
|
|
2662
|
-
if (!(0,
|
|
2663
|
-
return (0,
|
|
3871
|
+
if (!(0, import_node_fs15.existsSync)(filePath)) return 0;
|
|
3872
|
+
return (0, import_node_fs15.statSync)(filePath).mtimeMs;
|
|
2664
3873
|
}
|
|
2665
3874
|
function invalidateCache(filePath) {
|
|
2666
3875
|
graphCache.delete(filePath);
|
|
@@ -2699,20 +3908,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
2699
3908
|
}
|
|
2700
3909
|
function readGraphRaw(rootDir, layer) {
|
|
2701
3910
|
const filePath = graphFilePath(rootDir, layer);
|
|
2702
|
-
if (!(0,
|
|
2703
|
-
const stat = (0,
|
|
3911
|
+
if (!(0, import_node_fs15.existsSync)(filePath)) return null;
|
|
3912
|
+
const stat = (0, import_node_fs15.statSync)(filePath);
|
|
2704
3913
|
const cached = graphCache.get(filePath);
|
|
2705
3914
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2706
3915
|
return cached.graph;
|
|
2707
3916
|
}
|
|
2708
|
-
const content = (0,
|
|
3917
|
+
const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
|
|
2709
3918
|
const graph = JSON.parse(content);
|
|
2710
3919
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
2711
3920
|
return graph;
|
|
2712
3921
|
}
|
|
2713
3922
|
function readGraph(rootDir, layer) {
|
|
2714
3923
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
2715
|
-
if (!(0,
|
|
3924
|
+
if (!(0, import_node_fs15.existsSync)(rawFilePath)) return null;
|
|
2716
3925
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
2717
3926
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
2718
3927
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -2728,7 +3937,7 @@ function readGraph(rootDir, layer) {
|
|
|
2728
3937
|
}
|
|
2729
3938
|
function readAllGraphs(rootDir) {
|
|
2730
3939
|
const result = {};
|
|
2731
|
-
for (const layer of
|
|
3940
|
+
for (const layer of getAvailableLayers(rootDir)) {
|
|
2732
3941
|
const graph = readGraph(rootDir, layer);
|
|
2733
3942
|
if (graph) result[layer] = graph;
|
|
2734
3943
|
}
|
|
@@ -2742,22 +3951,22 @@ async function generateGraph(rootDir, layer) {
|
|
|
2742
3951
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
2743
3952
|
});
|
|
2744
3953
|
const dir = graphsDir(rootDir);
|
|
2745
|
-
(0,
|
|
3954
|
+
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
2746
3955
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
2747
3956
|
for (const result of results) {
|
|
2748
3957
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
2749
|
-
(0,
|
|
3958
|
+
(0, import_node_fs15.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
2750
3959
|
invalidateCache(filePath);
|
|
2751
3960
|
invalidateTaggedCache(rootDir, result.layer);
|
|
2752
3961
|
}
|
|
2753
3962
|
return results;
|
|
2754
3963
|
}
|
|
2755
|
-
var
|
|
3964
|
+
var import_node_fs15, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
|
|
2756
3965
|
var init_graph = __esm({
|
|
2757
3966
|
"src/server/graph/index.ts"() {
|
|
2758
3967
|
"use strict";
|
|
2759
|
-
|
|
2760
|
-
|
|
3968
|
+
import_node_fs15 = require("node:fs");
|
|
3969
|
+
import_node_path17 = require("node:path");
|
|
2761
3970
|
init_graph_builder();
|
|
2762
3971
|
init_config();
|
|
2763
3972
|
init_tagger_registry();
|
|
@@ -2765,12 +3974,294 @@ var init_graph = __esm({
|
|
|
2765
3974
|
init_ts_extractor();
|
|
2766
3975
|
init_tag_store();
|
|
2767
3976
|
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2768
|
-
LAYERS = ["ui", "api", "db"];
|
|
2769
3977
|
graphCache = /* @__PURE__ */ new Map();
|
|
2770
3978
|
taggedCache = /* @__PURE__ */ new Map();
|
|
2771
3979
|
}
|
|
2772
3980
|
});
|
|
2773
3981
|
|
|
3982
|
+
// src/server/graph/core/audit-core.ts
|
|
3983
|
+
function readGraphFile(rootDir, layer) {
|
|
3984
|
+
const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3985
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return null;
|
|
3986
|
+
try {
|
|
3987
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
|
|
3988
|
+
} catch {
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
function checkSchemaDrift(rootDir) {
|
|
3993
|
+
const findings = [];
|
|
3994
|
+
const db = readGraphFile(rootDir, "db");
|
|
3995
|
+
if (!db) {
|
|
3996
|
+
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." });
|
|
3997
|
+
return buildReport("db", "schema_drift", findings);
|
|
3998
|
+
}
|
|
3999
|
+
for (const c of db.contradictions ?? []) {
|
|
4000
|
+
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
4001
|
+
findings.push({
|
|
4002
|
+
id: `drift:${c.entity}`,
|
|
4003
|
+
severity: isTableLevel ? "error" : "warning",
|
|
4004
|
+
category: "schema_drift",
|
|
4005
|
+
title: c.entity,
|
|
4006
|
+
detail: c.detail
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
return buildReport("db", "schema_drift", findings);
|
|
4010
|
+
}
|
|
4011
|
+
function checkOrphanFks(rootDir) {
|
|
4012
|
+
const findings = [];
|
|
4013
|
+
const db = readGraphFile(rootDir, "db");
|
|
4014
|
+
if (!db) return buildReport("db", "orphan_fks", findings);
|
|
4015
|
+
for (const f of db.flagged_edges ?? []) {
|
|
4016
|
+
findings.push({
|
|
4017
|
+
id: `fk:${f.source}->${f.target}`,
|
|
4018
|
+
severity: "warning",
|
|
4019
|
+
category: "orphan_fks",
|
|
4020
|
+
title: `${f.source} \u2192 ${f.target}`,
|
|
4021
|
+
detail: f.label
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
return buildReport("db", "orphan_fks", findings);
|
|
4025
|
+
}
|
|
4026
|
+
function checkUnprotectedRoutes(rootDir) {
|
|
4027
|
+
const findings = [];
|
|
4028
|
+
const api = readGraphFile(rootDir, "api");
|
|
4029
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4030
|
+
if (!api) return buildReport("api", "unprotected_routes", findings);
|
|
4031
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
4032
|
+
let routePermsContent = "";
|
|
4033
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
4034
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
4035
|
+
}
|
|
4036
|
+
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
4037
|
+
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
4038
|
+
let rm;
|
|
4039
|
+
while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
|
|
4040
|
+
registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
|
|
4041
|
+
}
|
|
4042
|
+
for (const node of api.nodes) {
|
|
4043
|
+
if (node.type !== "endpoint") continue;
|
|
4044
|
+
const route = node.route ?? "";
|
|
4045
|
+
if (!route) continue;
|
|
4046
|
+
const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
|
|
4047
|
+
const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
|
|
4048
|
+
if (!isRegistered) {
|
|
4049
|
+
const methods = node.methods ?? [];
|
|
4050
|
+
findings.push({
|
|
4051
|
+
id: `unprotected:${node.id}`,
|
|
4052
|
+
severity: "warning",
|
|
4053
|
+
category: "unprotected_routes",
|
|
4054
|
+
title: `${methods.join(",")} ${route}`,
|
|
4055
|
+
detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
|
|
4056
|
+
file: node.id
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
return buildReport("api", "unprotected_routes", findings);
|
|
4061
|
+
}
|
|
4062
|
+
function routeMatchesPattern(route, pattern) {
|
|
4063
|
+
const routeParts = route.split("/");
|
|
4064
|
+
const patternParts = pattern.split("/");
|
|
4065
|
+
if (routeParts.length !== patternParts.length) return false;
|
|
4066
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
4067
|
+
const rp = routeParts[i];
|
|
4068
|
+
const pp = patternParts[i];
|
|
4069
|
+
if (rp === pp) continue;
|
|
4070
|
+
if (pp.startsWith("[") || pp.startsWith(":")) continue;
|
|
4071
|
+
if (rp.startsWith("[") || rp.startsWith(":")) continue;
|
|
4072
|
+
return false;
|
|
4073
|
+
}
|
|
4074
|
+
return true;
|
|
4075
|
+
}
|
|
4076
|
+
function checkDeadScreens(rootDir) {
|
|
4077
|
+
const findings = [];
|
|
4078
|
+
const ui = readGraphFile(rootDir, "ui");
|
|
4079
|
+
if (!ui) return buildReport("ui", "dead_screens", findings);
|
|
4080
|
+
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
4081
|
+
const navTargets = /* @__PURE__ */ new Set();
|
|
4082
|
+
for (const e of ui.edges) {
|
|
4083
|
+
if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
|
|
4084
|
+
navTargets.add(e.target);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
for (const cr of ui.cross_refs ?? []) {
|
|
4088
|
+
navTargets.add(cr.target);
|
|
4089
|
+
}
|
|
4090
|
+
for (const page of pages) {
|
|
4091
|
+
if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
|
|
4092
|
+
if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
|
|
4093
|
+
if (!navTargets.has(page.id)) {
|
|
4094
|
+
findings.push({
|
|
4095
|
+
id: `dead:${page.id}`,
|
|
4096
|
+
severity: "info",
|
|
4097
|
+
category: "dead_screens",
|
|
4098
|
+
title: page.name,
|
|
4099
|
+
detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
|
|
4100
|
+
file: page.id
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
return buildReport("ui", "dead_screens", findings);
|
|
4105
|
+
}
|
|
4106
|
+
function checkUnenforcedPermissions(rootDir) {
|
|
4107
|
+
const findings = [];
|
|
4108
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4109
|
+
if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
|
|
4110
|
+
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
4111
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
4112
|
+
let routePermsContent = "";
|
|
4113
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
4114
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
4115
|
+
}
|
|
4116
|
+
for (const perm of permissions) {
|
|
4117
|
+
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
4118
|
+
if (!regex.test(routePermsContent)) {
|
|
4119
|
+
findings.push({
|
|
4120
|
+
id: `unenforced:${perm.key}`,
|
|
4121
|
+
severity: "warning",
|
|
4122
|
+
category: "unenforced_permissions",
|
|
4123
|
+
title: `${perm.name} (${perm.key})`,
|
|
4124
|
+
detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
return buildReport("static", "unenforced_permissions", findings);
|
|
4129
|
+
}
|
|
4130
|
+
function checkHardcodedValues(rootDir) {
|
|
4131
|
+
const findings = [];
|
|
4132
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4133
|
+
if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
|
|
4134
|
+
const knownValues = /* @__PURE__ */ new Set();
|
|
4135
|
+
for (const n of staticGraph.nodes) {
|
|
4136
|
+
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4137
|
+
}
|
|
4138
|
+
const api = readGraphFile(rootDir, "api");
|
|
4139
|
+
if (!api) return buildReport("static", "hardcoded_values", findings);
|
|
4140
|
+
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4141
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4142
|
+
for (const node of api.nodes) {
|
|
4143
|
+
if (node.type !== "endpoint") continue;
|
|
4144
|
+
const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
|
|
4145
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) continue;
|
|
4146
|
+
const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
|
|
4147
|
+
let m;
|
|
4148
|
+
allCapsRe.lastIndex = 0;
|
|
4149
|
+
while ((m = allCapsRe.exec(content)) !== null) {
|
|
4150
|
+
const val = m[1];
|
|
4151
|
+
if (knownValues.has(val)) continue;
|
|
4152
|
+
if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
|
|
4153
|
+
const key = `${node.id}:${val}`;
|
|
4154
|
+
if (seen.has(key)) continue;
|
|
4155
|
+
seen.add(key);
|
|
4156
|
+
findings.push({
|
|
4157
|
+
id: `hardcoded:${key}`,
|
|
4158
|
+
severity: "info",
|
|
4159
|
+
category: "hardcoded_values",
|
|
4160
|
+
title: `"${val}" in ${node.id}`,
|
|
4161
|
+
detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
|
|
4162
|
+
file: node.id
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
return buildReport("static", "hardcoded_values", findings);
|
|
4167
|
+
}
|
|
4168
|
+
function buildReport(layer, check, findings) {
|
|
4169
|
+
return {
|
|
4170
|
+
layer,
|
|
4171
|
+
check,
|
|
4172
|
+
findings,
|
|
4173
|
+
summary: {
|
|
4174
|
+
errors: findings.filter((f) => f.severity === "error").length,
|
|
4175
|
+
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4176
|
+
info: findings.filter((f) => f.severity === "info").length
|
|
4177
|
+
},
|
|
4178
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
function getAvailableChecks() {
|
|
4182
|
+
const result = {};
|
|
4183
|
+
for (const [layer, checks] of Object.entries(CHECKS)) {
|
|
4184
|
+
result[layer] = Object.keys(checks);
|
|
4185
|
+
}
|
|
4186
|
+
return result;
|
|
4187
|
+
}
|
|
4188
|
+
function runAudit(rootDir, layer, check) {
|
|
4189
|
+
if (layer === "all") {
|
|
4190
|
+
const reports = [];
|
|
4191
|
+
for (const [l, checks] of Object.entries(CHECKS)) {
|
|
4192
|
+
for (const fn of Object.values(checks)) {
|
|
4193
|
+
reports.push(fn(rootDir));
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
return reports;
|
|
4197
|
+
}
|
|
4198
|
+
const layerChecks = CHECKS[layer];
|
|
4199
|
+
if (!layerChecks) {
|
|
4200
|
+
return [{
|
|
4201
|
+
layer,
|
|
4202
|
+
check: "unknown",
|
|
4203
|
+
findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
|
|
4204
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4205
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4206
|
+
}];
|
|
4207
|
+
}
|
|
4208
|
+
if (check) {
|
|
4209
|
+
const fn = layerChecks[check];
|
|
4210
|
+
if (!fn) {
|
|
4211
|
+
return [{
|
|
4212
|
+
layer,
|
|
4213
|
+
check,
|
|
4214
|
+
findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
|
|
4215
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4216
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4217
|
+
}];
|
|
4218
|
+
}
|
|
4219
|
+
return [fn(rootDir)];
|
|
4220
|
+
}
|
|
4221
|
+
return Object.values(layerChecks).map((fn) => fn(rootDir));
|
|
4222
|
+
}
|
|
4223
|
+
function formatAsPrompt(reports) {
|
|
4224
|
+
const lines = [];
|
|
4225
|
+
for (const report of reports) {
|
|
4226
|
+
if (report.findings.length === 0) continue;
|
|
4227
|
+
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
4228
|
+
lines.push("");
|
|
4229
|
+
for (const f of report.findings) {
|
|
4230
|
+
const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
|
|
4231
|
+
const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
|
|
4232
|
+
lines.push(`- [${tag}] ${f.title}${filePart}`);
|
|
4233
|
+
lines.push(` ${f.detail}`);
|
|
4234
|
+
}
|
|
4235
|
+
lines.push("");
|
|
4236
|
+
}
|
|
4237
|
+
if (lines.length === 0) return "No audit findings.";
|
|
4238
|
+
return lines.join("\n");
|
|
4239
|
+
}
|
|
4240
|
+
var import_node_fs16, import_node_path18, CHECKS;
|
|
4241
|
+
var init_audit_core = __esm({
|
|
4242
|
+
"src/server/graph/core/audit-core.ts"() {
|
|
4243
|
+
"use strict";
|
|
4244
|
+
import_node_fs16 = require("node:fs");
|
|
4245
|
+
import_node_path18 = require("node:path");
|
|
4246
|
+
CHECKS = {
|
|
4247
|
+
db: {
|
|
4248
|
+
schema_drift: checkSchemaDrift,
|
|
4249
|
+
orphan_fks: checkOrphanFks
|
|
4250
|
+
},
|
|
4251
|
+
api: {
|
|
4252
|
+
unprotected_routes: checkUnprotectedRoutes
|
|
4253
|
+
},
|
|
4254
|
+
ui: {
|
|
4255
|
+
dead_screens: checkDeadScreens
|
|
4256
|
+
},
|
|
4257
|
+
static: {
|
|
4258
|
+
unenforced_permissions: checkUnenforcedPermissions,
|
|
4259
|
+
hardcoded_values: checkHardcodedValues
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
|
|
2774
4265
|
// src/server/chart-serve.ts
|
|
2775
4266
|
var chart_serve_exports = {};
|
|
2776
4267
|
__export(chart_serve_exports, {
|
|
@@ -2783,31 +4274,39 @@ function randomPort() {
|
|
|
2783
4274
|
function findProjectRoot(startDir) {
|
|
2784
4275
|
let dir = startDir;
|
|
2785
4276
|
for (let i = 0; i < 8; i++) {
|
|
2786
|
-
const graphsDir2 =
|
|
2787
|
-
if (
|
|
2788
|
-
const parent =
|
|
4277
|
+
const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
|
|
4278
|
+
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;
|
|
4279
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2789
4280
|
if (parent === dir) break;
|
|
2790
4281
|
dir = parent;
|
|
2791
4282
|
}
|
|
2792
4283
|
dir = startDir;
|
|
2793
4284
|
for (let i = 0; i < 8; i++) {
|
|
2794
|
-
if (
|
|
2795
|
-
const parent =
|
|
4285
|
+
if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
|
|
4286
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2796
4287
|
if (parent === dir) break;
|
|
2797
4288
|
dir = parent;
|
|
2798
4289
|
}
|
|
2799
4290
|
return startDir;
|
|
2800
4291
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
if (!
|
|
2804
|
-
|
|
2805
|
-
|
|
4292
|
+
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
4293
|
+
const projectParam = url.searchParams.get("project");
|
|
4294
|
+
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
4295
|
+
const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
|
|
4296
|
+
if (!resolved.startsWith(monorepoRoot)) {
|
|
4297
|
+
throw new Error("Project path outside monorepo root");
|
|
4298
|
+
}
|
|
4299
|
+
return resolved;
|
|
4300
|
+
}
|
|
4301
|
+
async function buildMergedGraph(root) {
|
|
4302
|
+
let graphs = readAllGraphs(root);
|
|
4303
|
+
if (Object.keys(graphs).length === 0) {
|
|
4304
|
+
await generateGraph(root);
|
|
4305
|
+
graphs = readAllGraphs(root);
|
|
2806
4306
|
}
|
|
2807
4307
|
const nodes = [];
|
|
2808
4308
|
const rawLinks = [];
|
|
2809
|
-
const
|
|
2810
|
-
for (const layer of LAYERS2) {
|
|
4309
|
+
for (const layer of Object.keys(graphs)) {
|
|
2811
4310
|
const g = graphs[layer];
|
|
2812
4311
|
if (!g) continue;
|
|
2813
4312
|
for (const n of g.nodes) {
|
|
@@ -2844,16 +4343,16 @@ async function buildMergedGraph(projectRoot) {
|
|
|
2844
4343
|
};
|
|
2845
4344
|
}
|
|
2846
4345
|
function serveStatic(res, filePath) {
|
|
2847
|
-
if (!
|
|
2848
|
-
const ext =
|
|
4346
|
+
if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
|
|
4347
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2849
4348
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2850
4349
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
2851
|
-
|
|
4350
|
+
import_node_fs17.default.createReadStream(filePath).pipe(res);
|
|
2852
4351
|
return true;
|
|
2853
4352
|
}
|
|
2854
4353
|
function serveIndex(res, clientDir) {
|
|
2855
|
-
const indexPath =
|
|
2856
|
-
if (!
|
|
4354
|
+
const indexPath = import_node_path19.default.join(clientDir, "index.html");
|
|
4355
|
+
if (!import_node_fs17.default.existsSync(indexPath)) {
|
|
2857
4356
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2858
4357
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
2859
4358
|
return;
|
|
@@ -2905,19 +4404,40 @@ async function startChartServer(opts = {}) {
|
|
|
2905
4404
|
}
|
|
2906
4405
|
return { port: existing.port, url: existing.url };
|
|
2907
4406
|
}
|
|
2908
|
-
const clientDir = opts.clientDir ??
|
|
4407
|
+
const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
|
|
4408
|
+
const rootConfig = loadConfig(projectRoot);
|
|
4409
|
+
const projects = rootConfig.projects ?? [];
|
|
2909
4410
|
const server = import_node_http.default.createServer((req, res) => {
|
|
2910
4411
|
try {
|
|
2911
4412
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
4413
|
+
let reqRoot;
|
|
4414
|
+
try {
|
|
4415
|
+
reqRoot = resolveRequestRoot(url2, projectRoot, projects);
|
|
4416
|
+
} catch (err2) {
|
|
4417
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4418
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
4422
|
+
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
4423
|
+
const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
|
|
4424
|
+
const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
|
|
4425
|
+
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"));
|
|
4426
|
+
return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
|
|
4427
|
+
}) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
|
|
4428
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4429
|
+
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
2912
4432
|
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2913
4433
|
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2914
4434
|
(async () => {
|
|
2915
|
-
if (regenerate) await generateGraph(
|
|
2916
|
-
const merged = await buildMergedGraph(
|
|
4435
|
+
if (regenerate) await generateGraph(reqRoot);
|
|
4436
|
+
const merged = await buildMergedGraph(reqRoot);
|
|
2917
4437
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2918
4438
|
res.end(JSON.stringify({
|
|
2919
4439
|
...merged,
|
|
2920
|
-
debug: { cwd, projectRoot }
|
|
4440
|
+
debug: { cwd, projectRoot: reqRoot }
|
|
2921
4441
|
}));
|
|
2922
4442
|
})().catch((e) => {
|
|
2923
4443
|
res.writeHead(500);
|
|
@@ -2926,15 +4446,15 @@ async function startChartServer(opts = {}) {
|
|
|
2926
4446
|
return;
|
|
2927
4447
|
}
|
|
2928
4448
|
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
2929
|
-
const graphs = readAllGraphs(
|
|
4449
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2930
4450
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2931
4451
|
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
2932
4452
|
return;
|
|
2933
4453
|
}
|
|
2934
4454
|
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2935
4455
|
(async () => {
|
|
2936
|
-
await generateGraph(
|
|
2937
|
-
const graphs = readAllGraphs(
|
|
4456
|
+
await generateGraph(reqRoot);
|
|
4457
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2938
4458
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2939
4459
|
res.end(JSON.stringify({
|
|
2940
4460
|
ok: true,
|
|
@@ -2950,41 +4470,43 @@ async function startChartServer(opts = {}) {
|
|
|
2950
4470
|
}
|
|
2951
4471
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
2952
4472
|
const relPath = url2.searchParams.get("path");
|
|
2953
|
-
if (!relPath || relPath.includes("..") ||
|
|
4473
|
+
if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
|
|
2954
4474
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2955
4475
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
2956
4476
|
return;
|
|
2957
4477
|
}
|
|
2958
|
-
const filePath =
|
|
2959
|
-
if (!filePath.startsWith(
|
|
4478
|
+
const filePath = import_node_path19.default.join(reqRoot, relPath);
|
|
4479
|
+
if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
|
|
2960
4480
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2961
4481
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
2962
4482
|
return;
|
|
2963
4483
|
}
|
|
2964
|
-
const ext =
|
|
4484
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2965
4485
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
2966
|
-
const content =
|
|
4486
|
+
const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
2967
4487
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2968
4488
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
2969
4489
|
return;
|
|
2970
4490
|
}
|
|
2971
4491
|
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
2972
4492
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2973
|
-
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
4493
|
+
res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
|
|
2974
4494
|
return;
|
|
2975
4495
|
}
|
|
2976
4496
|
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2977
|
-
const config2 = loadConfig(
|
|
2978
|
-
const
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
4497
|
+
const config2 = loadConfig(reqRoot);
|
|
4498
|
+
const registry = createRegistry(config2, reqRoot);
|
|
4499
|
+
const detection = [];
|
|
4500
|
+
for (const parser of registry.getAll()) {
|
|
4501
|
+
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
4502
|
+
const mp = parser;
|
|
4503
|
+
detection.push({ id: mp.id, layers: mp.layers, detected: mp.detect(reqRoot) });
|
|
4504
|
+
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
4505
|
+
const sp = parser;
|
|
4506
|
+
detection.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(reqRoot) });
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
const crosslayerParsers = registry.getCrossLayerParsers().map((p) => ({ id: p.id }));
|
|
2988
4510
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2989
4511
|
res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
|
|
2990
4512
|
return;
|
|
@@ -2997,8 +4519,8 @@ async function startChartServer(opts = {}) {
|
|
|
2997
4519
|
req.on("end", () => {
|
|
2998
4520
|
try {
|
|
2999
4521
|
const newConfig = JSON.parse(body);
|
|
3000
|
-
const configPath =
|
|
3001
|
-
|
|
4522
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4523
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
3002
4524
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3003
4525
|
res.end(JSON.stringify({ ok: true }));
|
|
3004
4526
|
} catch (err2) {
|
|
@@ -3009,7 +4531,7 @@ async function startChartServer(opts = {}) {
|
|
|
3009
4531
|
return;
|
|
3010
4532
|
}
|
|
3011
4533
|
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
3012
|
-
const config2 = loadConfig(
|
|
4534
|
+
const config2 = loadConfig(reqRoot);
|
|
3013
4535
|
const builtinTaggers = [
|
|
3014
4536
|
{ id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
|
|
3015
4537
|
{ id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
|
|
@@ -3029,10 +4551,10 @@ async function startChartServer(opts = {}) {
|
|
|
3029
4551
|
req.on("end", () => {
|
|
3030
4552
|
try {
|
|
3031
4553
|
const taggerConfig = JSON.parse(body);
|
|
3032
|
-
const config2 = loadConfig(
|
|
4554
|
+
const config2 = loadConfig(reqRoot);
|
|
3033
4555
|
config2.taggers = taggerConfig;
|
|
3034
|
-
const configPath =
|
|
3035
|
-
|
|
4556
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4557
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
3036
4558
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3037
4559
|
res.end(JSON.stringify({ ok: true }));
|
|
3038
4560
|
} catch (err2) {
|
|
@@ -3043,7 +4565,7 @@ async function startChartServer(opts = {}) {
|
|
|
3043
4565
|
return;
|
|
3044
4566
|
}
|
|
3045
4567
|
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
3046
|
-
const store = readTagStore(
|
|
4568
|
+
const store = readTagStore(reqRoot);
|
|
3047
4569
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3048
4570
|
res.end(JSON.stringify(store));
|
|
3049
4571
|
return;
|
|
@@ -3061,7 +4583,7 @@ async function startChartServer(opts = {}) {
|
|
|
3061
4583
|
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
3062
4584
|
return;
|
|
3063
4585
|
}
|
|
3064
|
-
setTag(
|
|
4586
|
+
setTag(reqRoot, nodeId, key, value);
|
|
3065
4587
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3066
4588
|
res.end(JSON.stringify({ ok: true }));
|
|
3067
4589
|
} catch (err2) {
|
|
@@ -3084,7 +4606,76 @@ async function startChartServer(opts = {}) {
|
|
|
3084
4606
|
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
3085
4607
|
return;
|
|
3086
4608
|
}
|
|
3087
|
-
removeTag(
|
|
4609
|
+
removeTag(reqRoot, nodeId, key);
|
|
4610
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4611
|
+
res.end(JSON.stringify({ ok: true }));
|
|
4612
|
+
} catch (err2) {
|
|
4613
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4614
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4615
|
+
}
|
|
4616
|
+
});
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
|
|
4620
|
+
const config2 = loadConfig(reqRoot);
|
|
4621
|
+
const paths = resolveProjectPaths(reqRoot, config2);
|
|
4622
|
+
const isOverride = !!config2.paths?.appDir;
|
|
4623
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4624
|
+
res.end(JSON.stringify({
|
|
4625
|
+
projectRoot: reqRoot,
|
|
4626
|
+
detected: paths ? {
|
|
4627
|
+
srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
|
|
4628
|
+
appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
|
|
4629
|
+
apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir)
|
|
4630
|
+
} : null,
|
|
4631
|
+
isOverride
|
|
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);
|
|
3088
4679
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3089
4680
|
res.end(JSON.stringify({ ok: true }));
|
|
3090
4681
|
} catch (err2) {
|
|
@@ -3095,7 +4686,7 @@ async function startChartServer(opts = {}) {
|
|
|
3095
4686
|
return;
|
|
3096
4687
|
}
|
|
3097
4688
|
if (url2.pathname !== "/") {
|
|
3098
|
-
const staticPath =
|
|
4689
|
+
const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
|
|
3099
4690
|
if (serveStatic(res, staticPath)) return;
|
|
3100
4691
|
}
|
|
3101
4692
|
serveIndex(res, clientDir);
|
|
@@ -3133,6 +4724,10 @@ async function startChartServer(opts = {}) {
|
|
|
3133
4724
|
`);
|
|
3134
4725
|
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
3135
4726
|
`);
|
|
4727
|
+
if (projects.length > 0) {
|
|
4728
|
+
process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
|
|
4729
|
+
`);
|
|
4730
|
+
}
|
|
3136
4731
|
}
|
|
3137
4732
|
return { port, url };
|
|
3138
4733
|
}
|
|
@@ -3151,19 +4746,19 @@ function runServeCli(argv) {
|
|
|
3151
4746
|
process.exit(1);
|
|
3152
4747
|
});
|
|
3153
4748
|
}
|
|
3154
|
-
var import_node_http,
|
|
4749
|
+
var import_node_http, import_node_fs17, import_node_path19, MAX_PORT_SCAN, MIME_TYPES;
|
|
3155
4750
|
var init_chart_serve = __esm({
|
|
3156
4751
|
"src/server/chart-serve.ts"() {
|
|
3157
4752
|
"use strict";
|
|
3158
4753
|
import_node_http = __toESM(require("node:http"));
|
|
3159
|
-
|
|
3160
|
-
|
|
4754
|
+
import_node_fs17 = __toESM(require("node:fs"));
|
|
4755
|
+
import_node_path19 = __toESM(require("node:path"));
|
|
3161
4756
|
init_graph();
|
|
3162
4757
|
init_lockfile();
|
|
3163
4758
|
init_config();
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
4759
|
+
init_parser_registry();
|
|
4760
|
+
init_resolve_paths();
|
|
4761
|
+
init_audit_core();
|
|
3167
4762
|
MAX_PORT_SCAN = 3;
|
|
3168
4763
|
MIME_TYPES = {
|
|
3169
4764
|
".html": "text/html; charset=utf-8",
|
|
@@ -3179,6 +4774,179 @@ var init_chart_serve = __esm({
|
|
|
3179
4774
|
}
|
|
3180
4775
|
});
|
|
3181
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
|
+
|
|
3182
4950
|
// src/server/graph-mcp.ts
|
|
3183
4951
|
var graph_mcp_exports = {};
|
|
3184
4952
|
__export(graph_mcp_exports, {
|
|
@@ -3265,8 +5033,8 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
3265
5033
|
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
3266
5034
|
if (srcIn && dstIn) projectedEdges++;
|
|
3267
5035
|
}
|
|
3268
|
-
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
|
|
3269
|
-
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);
|
|
3270
5038
|
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
3271
5039
|
budgetExceeded = true;
|
|
3272
5040
|
break;
|
|
@@ -3314,9 +5082,6 @@ function err(text) {
|
|
|
3314
5082
|
async function handleGenerateGraph(args) {
|
|
3315
5083
|
const rootDir = process.cwd();
|
|
3316
5084
|
const layer = args.layer;
|
|
3317
|
-
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
3318
|
-
return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
|
|
3319
|
-
}
|
|
3320
5085
|
const results = await generateGraph(rootDir, layer);
|
|
3321
5086
|
if (results.length === 0) {
|
|
3322
5087
|
return err(
|
|
@@ -3353,8 +5118,11 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
3353
5118
|
const offset = args.offset ?? 0;
|
|
3354
5119
|
const limit = args.limit;
|
|
3355
5120
|
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
3356
|
-
if (layer
|
|
3357
|
-
|
|
5121
|
+
if (layer) {
|
|
5122
|
+
const available = getAvailableLayers(rootDir);
|
|
5123
|
+
if (available.length > 0 && !available.includes(layer)) {
|
|
5124
|
+
return { error: `No graph found for layer "${layer}". Available: ${available.join(", ")}. Run generate_graph first.` };
|
|
5125
|
+
}
|
|
3358
5126
|
}
|
|
3359
5127
|
if (!layer) {
|
|
3360
5128
|
const graphs = readAllGraphs(rootDir);
|
|
@@ -3508,9 +5276,12 @@ function handleReadGraph(args) {
|
|
|
3508
5276
|
return okJson(result);
|
|
3509
5277
|
}
|
|
3510
5278
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
3511
|
-
if (layer === "ui") return (0,
|
|
3512
|
-
if (layer === "
|
|
3513
|
-
|
|
5279
|
+
if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
|
|
5280
|
+
if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
|
|
5281
|
+
const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
|
|
5282
|
+
if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
|
|
5283
|
+
const direct = (0, import_node_path21.join)(rootDir, nodeId);
|
|
5284
|
+
if ((0, import_node_fs19.existsSync)(direct)) return direct;
|
|
3514
5285
|
return null;
|
|
3515
5286
|
}
|
|
3516
5287
|
function handleInspectNode(args) {
|
|
@@ -3599,7 +5370,10 @@ function handleGrepNodes(args) {
|
|
|
3599
5370
|
const layer = args.layer;
|
|
3600
5371
|
if (!pattern) return err("pattern is required");
|
|
3601
5372
|
if (!layer) return err("layer is required (ui, api, or db)");
|
|
3602
|
-
|
|
5373
|
+
const grepAvailable = getAvailableLayers(rootDir);
|
|
5374
|
+
if (grepAvailable.length > 0 && !grepAvailable.includes(layer)) {
|
|
5375
|
+
return err(`No graph found for layer "${layer}". Available: ${grepAvailable.join(", ")}. Run generate_graph first.`);
|
|
5376
|
+
}
|
|
3603
5377
|
const hasFilter = !!(args.search || args.type || args.module || args.node_id);
|
|
3604
5378
|
if (!hasFilter) {
|
|
3605
5379
|
return err(
|
|
@@ -3650,11 +5424,11 @@ function handleGrepNodes(args) {
|
|
|
3650
5424
|
let filesSearched = 0;
|
|
3651
5425
|
let truncated = false;
|
|
3652
5426
|
for (const [filePath, nodeId] of filePaths) {
|
|
3653
|
-
if (!(0,
|
|
5427
|
+
if (!(0, import_node_fs19.existsSync)(filePath)) continue;
|
|
3654
5428
|
filesSearched++;
|
|
3655
5429
|
let content;
|
|
3656
5430
|
try {
|
|
3657
|
-
content = (0,
|
|
5431
|
+
content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
|
|
3658
5432
|
} catch {
|
|
3659
5433
|
continue;
|
|
3660
5434
|
}
|
|
@@ -3719,11 +5493,11 @@ function handleStartChartServer(args) {
|
|
|
3719
5493
|
});
|
|
3720
5494
|
}
|
|
3721
5495
|
const entryPath = process.argv[1];
|
|
3722
|
-
const logDir = (0,
|
|
3723
|
-
(0,
|
|
3724
|
-
const logPath = (0,
|
|
3725
|
-
const out = (0,
|
|
3726
|
-
const err2 = (0,
|
|
5496
|
+
const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
5497
|
+
(0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
|
|
5498
|
+
const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
|
|
5499
|
+
const out = (0, import_node_fs19.openSync)(logPath, "a");
|
|
5500
|
+
const err2 = (0, import_node_fs19.openSync)(logPath, "a");
|
|
3727
5501
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
3728
5502
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
3729
5503
|
detached: true,
|
|
@@ -3786,14 +5560,55 @@ function handleRemoveTag(args) {
|
|
|
3786
5560
|
removeTag(rootDir, nodeId, key);
|
|
3787
5561
|
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
3788
5562
|
}
|
|
5563
|
+
function handleAuditLayer(args) {
|
|
5564
|
+
const rootDir = process.cwd();
|
|
5565
|
+
const layer = args.layer;
|
|
5566
|
+
const check = args.check;
|
|
5567
|
+
if (!layer) return err("layer is required");
|
|
5568
|
+
const reports = runAudit(rootDir, layer, check);
|
|
5569
|
+
const totalFindings = reports.reduce((acc, r) => acc + r.findings.length, 0);
|
|
5570
|
+
const totalErrors = reports.reduce((acc, r) => acc + r.summary.errors, 0);
|
|
5571
|
+
const totalWarnings = reports.reduce((acc, r) => acc + r.summary.warnings, 0);
|
|
5572
|
+
const totalInfo = reports.reduce((acc, r) => acc + r.summary.info, 0);
|
|
5573
|
+
const lines = [];
|
|
5574
|
+
lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
|
|
5575
|
+
lines.push("");
|
|
5576
|
+
for (const report of reports) {
|
|
5577
|
+
if (report.findings.length === 0) {
|
|
5578
|
+
lines.push(`\u2713 ${report.check}: no issues found`);
|
|
5579
|
+
continue;
|
|
5580
|
+
}
|
|
5581
|
+
lines.push(`\u2500\u2500 ${report.check} (${report.findings.length}) \u2500\u2500`);
|
|
5582
|
+
for (const f of report.findings) {
|
|
5583
|
+
const tag = f.severity === "error" ? "\u2717" : f.severity === "warning" ? "!" : "\xB7";
|
|
5584
|
+
const filePart = f.file ? ` [${f.file}]` : "";
|
|
5585
|
+
lines.push(` ${tag} ${f.title}${filePart}`);
|
|
5586
|
+
lines.push(` ${f.detail}`);
|
|
5587
|
+
}
|
|
5588
|
+
lines.push("");
|
|
5589
|
+
}
|
|
5590
|
+
return okText(lines.join("\n"));
|
|
5591
|
+
}
|
|
3789
5592
|
function handleDetectProjectStack() {
|
|
3790
5593
|
const rootDir = process.cwd();
|
|
3791
|
-
const parsers = [
|
|
3792
|
-
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
3793
|
-
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
3794
|
-
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
3795
|
-
];
|
|
3796
5594
|
const config = loadConfig(rootDir);
|
|
5595
|
+
const registry = createRegistry(config, rootDir);
|
|
5596
|
+
const parserResults = [];
|
|
5597
|
+
for (const parser of registry.getAll()) {
|
|
5598
|
+
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
5599
|
+
const mp = parser;
|
|
5600
|
+
parserResults.push({ id: mp.id, layers: mp.layers, detected: mp.detect(rootDir) });
|
|
5601
|
+
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
5602
|
+
const sp = parser;
|
|
5603
|
+
parserResults.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(rootDir) });
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
const availableLayers = /* @__PURE__ */ new Set();
|
|
5607
|
+
for (const p of parserResults) {
|
|
5608
|
+
if (p.detected) {
|
|
5609
|
+
for (const l of p.layers) availableLayers.add(l);
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
3797
5612
|
let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
|
|
3798
5613
|
const uiGraph = readGraph(rootDir, "ui");
|
|
3799
5614
|
if (uiGraph) {
|
|
@@ -3805,20 +5620,20 @@ function handleDetectProjectStack() {
|
|
|
3805
5620
|
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
3806
5621
|
}
|
|
3807
5622
|
}
|
|
3808
|
-
const srcDir = (0,
|
|
3809
|
-
if ((0,
|
|
5623
|
+
const srcDir = (0, import_node_path21.join)(rootDir, "src");
|
|
5624
|
+
if ((0, import_node_fs19.existsSync)(srcDir)) {
|
|
3810
5625
|
const scanDir = (dir) => {
|
|
3811
|
-
if (!(0,
|
|
3812
|
-
for (const entry of (0,
|
|
5626
|
+
if (!(0, import_node_fs19.existsSync)(dir)) return;
|
|
5627
|
+
for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
|
|
3813
5628
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3814
|
-
const full = (0,
|
|
5629
|
+
const full = (0, import_node_path21.join)(dir, entry.name);
|
|
3815
5630
|
if (entry.isDirectory()) {
|
|
3816
5631
|
scanDir(full);
|
|
3817
5632
|
continue;
|
|
3818
5633
|
}
|
|
3819
|
-
if (![".ts", ".tsx"].includes((0,
|
|
5634
|
+
if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
|
|
3820
5635
|
try {
|
|
3821
|
-
const content = (0,
|
|
5636
|
+
const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
|
|
3822
5637
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
3823
5638
|
if (matches) stats.annotations += matches.length;
|
|
3824
5639
|
} catch {
|
|
@@ -3833,8 +5648,16 @@ function handleDetectProjectStack() {
|
|
|
3833
5648
|
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
3834
5649
|
recommendedPrimary = "url-literal-scanner";
|
|
3835
5650
|
}
|
|
5651
|
+
const supportedLanguages = /* @__PURE__ */ new Map();
|
|
5652
|
+
supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
|
|
5653
|
+
supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
|
|
5654
|
+
const languages = detectLanguages(rootDir, supportedLanguages);
|
|
5655
|
+
const unsupported = languages.filter((l) => !l.supported);
|
|
5656
|
+
const unsupportedHint = unsupported.length > 0 ? unsupported.map((l) => `${l.id} (${l.fileCount} files)`).join(", ") + " \u2014 detected but not yet supported" : null;
|
|
3836
5657
|
return okJson({
|
|
3837
|
-
|
|
5658
|
+
languages,
|
|
5659
|
+
parsers: parserResults,
|
|
5660
|
+
available_layers: [...availableLayers],
|
|
3838
5661
|
crosslayer_parsers: [
|
|
3839
5662
|
{ id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
|
|
3840
5663
|
{ id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
|
|
@@ -3842,6 +5665,7 @@ function handleDetectProjectStack() {
|
|
|
3842
5665
|
],
|
|
3843
5666
|
stats,
|
|
3844
5667
|
recommended_primary: recommendedPrimary,
|
|
5668
|
+
...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
|
|
3845
5669
|
current_config: Object.keys(config).length > 0 ? config : null,
|
|
3846
5670
|
config_path: ".launchchart.json"
|
|
3847
5671
|
});
|
|
@@ -3917,6 +5741,10 @@ async function handleMessage(msg) {
|
|
|
3917
5741
|
respond(id ?? null, handleRemoveTag(args));
|
|
3918
5742
|
return;
|
|
3919
5743
|
}
|
|
5744
|
+
if (toolName === "audit_layer") {
|
|
5745
|
+
respond(id ?? null, handleAuditLayer(args));
|
|
5746
|
+
return;
|
|
5747
|
+
}
|
|
3920
5748
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
3921
5749
|
return;
|
|
3922
5750
|
}
|
|
@@ -3952,20 +5780,20 @@ function startGraphMcpServer() {
|
|
|
3952
5780
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
3953
5781
|
`);
|
|
3954
5782
|
}
|
|
3955
|
-
var
|
|
5783
|
+
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;
|
|
3956
5784
|
var init_graph_mcp = __esm({
|
|
3957
5785
|
"src/server/graph-mcp.ts"() {
|
|
3958
5786
|
"use strict";
|
|
3959
|
-
|
|
3960
|
-
|
|
5787
|
+
import_node_fs19 = require("node:fs");
|
|
5788
|
+
import_node_path21 = require("node:path");
|
|
3961
5789
|
import_node_child_process2 = require("node:child_process");
|
|
3962
5790
|
import_node_os2 = require("node:os");
|
|
3963
5791
|
init_graph();
|
|
3964
5792
|
init_lockfile();
|
|
3965
5793
|
init_config();
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
5794
|
+
init_parser_registry();
|
|
5795
|
+
init_language_detection();
|
|
5796
|
+
init_audit_core();
|
|
3969
5797
|
SERVER_INFO = {
|
|
3970
5798
|
name: "launchsecure-graph",
|
|
3971
5799
|
version: "0.0.1"
|
|
@@ -3973,14 +5801,13 @@ var init_graph_mcp = __esm({
|
|
|
3973
5801
|
TOOLS = [
|
|
3974
5802
|
{
|
|
3975
5803
|
name: "generate_graph",
|
|
3976
|
-
description: "Regenerate the structural project graph by scanning source code in the current working directory.
|
|
5804
|
+
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.",
|
|
3977
5805
|
inputSchema: {
|
|
3978
5806
|
type: "object",
|
|
3979
5807
|
properties: {
|
|
3980
5808
|
layer: {
|
|
3981
5809
|
type: "string",
|
|
3982
|
-
|
|
3983
|
-
description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
|
|
5810
|
+
description: "Specific layer to regenerate (e.g. 'ui', 'api', 'db'). Omit to regenerate all detectable layers. Run detect_project_stack to see available layers."
|
|
3984
5811
|
}
|
|
3985
5812
|
}
|
|
3986
5813
|
}
|
|
@@ -3993,8 +5820,7 @@ var init_graph_mcp = __esm({
|
|
|
3993
5820
|
properties: {
|
|
3994
5821
|
layer: {
|
|
3995
5822
|
type: "string",
|
|
3996
|
-
|
|
3997
|
-
description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
|
|
5823
|
+
description: "Graph layer to query (e.g. 'ui', 'api', 'db'). Required if any filter is provided. Run detect_project_stack to see available layers."
|
|
3998
5824
|
},
|
|
3999
5825
|
search: {
|
|
4000
5826
|
type: "string",
|
|
@@ -4046,7 +5872,7 @@ var init_graph_mcp = __esm({
|
|
|
4046
5872
|
items: {
|
|
4047
5873
|
type: "object",
|
|
4048
5874
|
properties: {
|
|
4049
|
-
layer: { type: "string"
|
|
5875
|
+
layer: { type: "string" },
|
|
4050
5876
|
search: { type: "string" },
|
|
4051
5877
|
type: { type: "string" },
|
|
4052
5878
|
module: { type: "string" },
|
|
@@ -4088,8 +5914,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
4088
5914
|
properties: {
|
|
4089
5915
|
layer: {
|
|
4090
5916
|
type: "string",
|
|
4091
|
-
|
|
4092
|
-
description: "Graph layer to scope files (required)."
|
|
5917
|
+
description: "Graph layer to scope files (required, e.g. 'ui', 'api', 'db')."
|
|
4093
5918
|
},
|
|
4094
5919
|
pattern: {
|
|
4095
5920
|
type: "string",
|
|
@@ -4122,8 +5947,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
|
|
|
4122
5947
|
properties: {
|
|
4123
5948
|
layer: {
|
|
4124
5949
|
type: "string",
|
|
4125
|
-
|
|
4126
|
-
description: "Graph layer (required)."
|
|
5950
|
+
description: "Graph layer (required, e.g. 'ui', 'api', 'db')."
|
|
4127
5951
|
},
|
|
4128
5952
|
node_id: {
|
|
4129
5953
|
type: "string",
|
|
@@ -4183,7 +6007,7 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4183
6007
|
},
|
|
4184
6008
|
{
|
|
4185
6009
|
name: "detect_project_stack",
|
|
4186
|
-
description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify
|
|
6010
|
+
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.",
|
|
4187
6011
|
inputSchema: {
|
|
4188
6012
|
type: "object",
|
|
4189
6013
|
properties: {}
|
|
@@ -4228,6 +6052,24 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4228
6052
|
},
|
|
4229
6053
|
required: ["node_id", "key"]
|
|
4230
6054
|
}
|
|
6055
|
+
},
|
|
6056
|
+
{
|
|
6057
|
+
name: "audit_layer",
|
|
6058
|
+
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",
|
|
6059
|
+
inputSchema: {
|
|
6060
|
+
type: "object",
|
|
6061
|
+
properties: {
|
|
6062
|
+
layer: {
|
|
6063
|
+
type: "string",
|
|
6064
|
+
description: "Layer to audit: 'db', 'api', 'ui', 'static', or 'all'."
|
|
6065
|
+
},
|
|
6066
|
+
check: {
|
|
6067
|
+
type: "string",
|
|
6068
|
+
description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
|
|
6069
|
+
}
|
|
6070
|
+
},
|
|
6071
|
+
required: ["layer"]
|
|
6072
|
+
}
|
|
4231
6073
|
}
|
|
4232
6074
|
];
|
|
4233
6075
|
COMPACT_SCHEMA = {
|
|
@@ -4284,6 +6126,9 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4284
6126
|
api: 65,
|
|
4285
6127
|
db: 65
|
|
4286
6128
|
};
|
|
6129
|
+
DEFAULT_EST_NODE_FULL = 300;
|
|
6130
|
+
DEFAULT_EST_NODE_MIN = 150;
|
|
6131
|
+
DEFAULT_EST_EDGE = 65;
|
|
4287
6132
|
NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
4288
6133
|
BATCH_BUDGET_CHARS = 6e4;
|
|
4289
6134
|
}
|
|
@@ -4291,10 +6136,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4291
6136
|
|
|
4292
6137
|
// src/server/graph-mcp-entry.ts
|
|
4293
6138
|
var import_node_child_process3 = require("node:child_process");
|
|
4294
|
-
var
|
|
4295
|
-
var
|
|
6139
|
+
var import_node_fs20 = require("node:fs");
|
|
6140
|
+
var import_node_path22 = __toESM(require("node:path"));
|
|
4296
6141
|
var import_node_os3 = require("node:os");
|
|
4297
|
-
var
|
|
6142
|
+
var import_node_fs21 = require("node:fs");
|
|
4298
6143
|
init_lockfile();
|
|
4299
6144
|
function logStderr(msg) {
|
|
4300
6145
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -4310,11 +6155,11 @@ function maybeAutoServe() {
|
|
|
4310
6155
|
return;
|
|
4311
6156
|
}
|
|
4312
6157
|
try {
|
|
4313
|
-
const logDir =
|
|
4314
|
-
(0,
|
|
4315
|
-
const logPath =
|
|
4316
|
-
const out = (0,
|
|
4317
|
-
const err2 = (0,
|
|
6158
|
+
const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
6159
|
+
(0, import_node_fs21.mkdirSync)(logDir, { recursive: true });
|
|
6160
|
+
const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
|
|
6161
|
+
const out = (0, import_node_fs20.openSync)(logPath, "a");
|
|
6162
|
+
const err2 = (0, import_node_fs20.openSync)(logPath, "a");
|
|
4318
6163
|
const entryPath = process.argv[1];
|
|
4319
6164
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
4320
6165
|
detached: true,
|