@launchsecure/launch-kit 0.0.25 → 0.0.27

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.
Files changed (132) hide show
  1. package/README.md +50 -0
  2. package/dist/beacon/beacon.mjs +1016 -0
  3. package/dist/beacon/beacon.mjs.map +1 -0
  4. package/dist/beacon/beacon.umd.js +87 -0
  5. package/dist/beacon/beacon.umd.js.map +1 -0
  6. package/dist/beacon/index-DAIDnjfR.mjs +513 -0
  7. package/dist/beacon/index-DAIDnjfR.mjs.map +1 -0
  8. package/dist/beacon/types/capture/element.d.ts +3 -0
  9. package/dist/beacon/types/capture/element.d.ts.map +1 -0
  10. package/dist/beacon/types/capture/framework.d.ts +3 -0
  11. package/dist/beacon/types/capture/framework.d.ts.map +1 -0
  12. package/dist/beacon/types/capture/metadata.d.ts +3 -0
  13. package/dist/beacon/types/capture/metadata.d.ts.map +1 -0
  14. package/dist/beacon/types/capture/overlay.d.ts +7 -0
  15. package/dist/beacon/types/capture/overlay.d.ts.map +1 -0
  16. package/dist/beacon/types/capture/picker.d.ts +12 -0
  17. package/dist/beacon/types/capture/picker.d.ts.map +1 -0
  18. package/dist/beacon/types/capture/screenshot.d.ts +7 -0
  19. package/dist/beacon/types/capture/screenshot.d.ts.map +1 -0
  20. package/dist/beacon/types/capture/selector.d.ts +2 -0
  21. package/dist/beacon/types/capture/selector.d.ts.map +1 -0
  22. package/dist/beacon/types/element.d.ts +50 -0
  23. package/dist/beacon/types/element.d.ts.map +1 -0
  24. package/dist/beacon/types/index.d.ts +4 -0
  25. package/dist/beacon/types/index.d.ts.map +1 -0
  26. package/dist/beacon/types/transport/submit.d.ts +3 -0
  27. package/dist/beacon/types/transport/submit.d.ts.map +1 -0
  28. package/dist/beacon/types/types.d.ts +88 -0
  29. package/dist/beacon/types/types.d.ts.map +1 -0
  30. package/dist/beacon/types/ui/button.d.ts +2 -0
  31. package/dist/beacon/types/ui/button.d.ts.map +1 -0
  32. package/dist/beacon/types/ui/drawer.d.ts +31 -0
  33. package/dist/beacon/types/ui/drawer.d.ts.map +1 -0
  34. package/dist/beacon/types/ui/icons.d.ts +9 -0
  35. package/dist/beacon/types/ui/icons.d.ts.map +1 -0
  36. package/dist/beacon/types/ui/pick-mode-overlay.d.ts +25 -0
  37. package/dist/beacon/types/ui/pick-mode-overlay.d.ts.map +1 -0
  38. package/dist/beacon/types/ui/pin-popover.d.ts +14 -0
  39. package/dist/beacon/types/ui/pin-popover.d.ts.map +1 -0
  40. package/dist/chart-client/assets/index-CJ4mgRRF.css +1 -0
  41. package/dist/chart-client/assets/{index-C8ANseEa.js → index-Ccy-DpI-.js} +82 -73
  42. package/dist/chart-client/index.html +2 -2
  43. package/dist/client/assets/index-DI5qSR_w.css +32 -0
  44. package/dist/client/assets/index-Dp0_okva.js +294 -0
  45. package/dist/client/index.html +2 -2
  46. package/dist/council-client/assets/index-C_-vAM9L.css +1 -0
  47. package/dist/council-client/assets/{index-Dc41S-R2.js → index-Dt4zWKSj.js} +14 -14
  48. package/dist/council-client/index.html +2 -2
  49. package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-W2JQDmje.js} +1 -1
  50. package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-DIBWAId9.js} +1 -1
  51. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-CAIRMvJK.js} +1 -1
  52. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-BeNaNiOi.js} +1 -1
  53. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-B9Ozi62h.js} +1 -1
  54. package/dist/deck-client/assets/channel-CRdozqbp.js +1 -0
  55. package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-D7AZ47dt.js} +1 -1
  56. package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-DnVnNPcI.js} +1 -1
  57. package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-UKYs-YNd.js} +1 -1
  58. package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-D43b-SKn.js} +1 -1
  59. package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-QzBAoyyW.js} +1 -1
  60. package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-Cjif4r6W.js} +1 -1
  61. package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-CqLDirEI.js} +1 -1
  62. package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-_FQvmMs4.js} +1 -1
  63. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-lIZMp57W.js +1 -0
  64. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-lIZMp57W.js +1 -0
  65. package/dist/deck-client/assets/clone-BtWeSTyJ.js +1 -0
  66. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-rfrocesE.js} +1 -1
  67. package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bv_7DJat.js} +1 -1
  68. package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-4F1414G5.js} +1 -1
  69. package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-C4-Pszqm.js} +1 -1
  70. package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-B647TIx9.js} +1 -1
  71. package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-BFAqpezd.js} +1 -1
  72. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-BfBfrJOC.js} +1 -1
  73. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-DX9YAYes.js} +1 -1
  74. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-DCuiy7wF.js} +1 -1
  75. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-CGp1IXUh.js} +1 -1
  76. package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-B7g8aoxv.js} +1 -1
  77. package/dist/deck-client/assets/{index-BfIfJXmS.js → index-Dg1r-WSN.js} +68 -68
  78. package/dist/deck-client/assets/index-DsIZ3LqL.css +1 -0
  79. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-L3fahMkF.js} +1 -1
  80. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-aS_EjWBZ.js} +1 -1
  81. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-djTSQZF9.js} +1 -1
  82. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-CcTHo4CM.js} +1 -1
  83. package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-mEJiadb7.js} +1 -1
  84. package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-XgTKqyRu.js} +1 -1
  85. package/dist/deck-client/assets/{min-CbUksbuI.js → min-Ct9jZdpd.js} +1 -1
  86. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-BaFxCGNU.js} +1 -1
  87. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-CIbYYjtw.js} +1 -1
  88. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-D9EtCOvh.js} +1 -1
  89. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-xeni9eVG.js} +1 -1
  90. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-LYeknz9h.js} +1 -1
  91. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-RDbsKFZf.js} +1 -1
  92. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-BH1Zjglk.js} +1 -1
  93. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BrV78NDR.js +1 -0
  94. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-IFXxKptt.js} +1 -1
  95. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-D-sLkQs9.js} +1 -1
  96. package/dist/deck-client/assets/wardley-RL74JXVD-C010F8l4.js +162 -0
  97. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BTjjuDU3.js} +1 -1
  98. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-AYbv92n-.js} +1 -1
  99. package/dist/deck-client/index.html +2 -2
  100. package/dist/server/chart-serve.js +4524 -3564
  101. package/dist/server/cli.js +27351 -5398
  102. package/dist/server/council-entry.js +17 -5
  103. package/dist/server/council-serve.js +8 -3
  104. package/dist/server/deck-mcp-entry.js +354 -13
  105. package/dist/server/deck-serve.js +298 -7
  106. package/dist/server/graph/queries/classify.scm +8 -0
  107. package/dist/server/graph/queries/exports.scm +7 -0
  108. package/dist/server/graph-mcp-entry.js +5943 -4361
  109. package/dist/server/init-entry.js +609 -0
  110. package/dist/server/orbit-entry.js +2272 -0
  111. package/dist/server/{server/chart-serve.js → parse-worker-entry.js} +1900 -1822
  112. package/dist/server/recall-entry.js +1450 -0
  113. package/package.json +40 -8
  114. package/scaffolds/migrate-safety/.github/workflows/backup-on-migration.yml +72 -0
  115. package/scaffolds/migrate-safety/docs/migrations-runbook.md +172 -0
  116. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +294 -0
  117. package/dist/chart-client/assets/index--120d9P9.css +0 -1
  118. package/dist/client/assets/index-Bf8zdL3x.css +0 -32
  119. package/dist/client/assets/index-Ds9UP_cj.js +0 -291
  120. package/dist/council-client/assets/index-CofZh7pS.css +0 -1
  121. package/dist/deck-client/assets/channel-ERh5jKXV.js +0 -1
  122. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +0 -1
  123. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +0 -1
  124. package/dist/deck-client/assets/clone-DfWhlD4X.js +0 -1
  125. package/dist/deck-client/assets/index-765AIQ9z.css +0 -1
  126. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +0 -1
  127. package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +0 -162
  128. package/dist/server/deck-server/deck-mcp-entry.js +0 -1789
  129. package/dist/server/deck-server/deck-serve.js +0 -1275
  130. package/dist/server/server/cli.js +0 -13360
  131. package/dist/server/server/fb-wizard.js +0 -136
  132. package/dist/server/server/graph-mcp-entry.js +0 -6776
@@ -30,41 +30,171 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
- // src/server/graph/core/config.ts
34
- var config_exports = {};
35
- __export(config_exports, {
36
- loadConfig: () => loadConfig
37
- });
38
- function loadConfig(rootDir) {
39
- const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
40
- if (!(0, import_node_fs.existsSync)(configPath)) return {};
41
- try {
42
- return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
43
- } catch {
44
- return {};
33
+ // src/server/launch-kit-paths.ts
34
+ var LAUNCHSECURE_DIR, LAUNCHCHART_CONFIG_FILE;
35
+ var init_launch_kit_paths = __esm({
36
+ "src/server/launch-kit-paths.ts"() {
37
+ "use strict";
38
+ LAUNCHSECURE_DIR = ".launchsecure";
39
+ LAUNCHCHART_CONFIG_FILE = ".launchchart.json";
45
40
  }
41
+ });
42
+
43
+ // src/server/graph/core/parse-failure-cache.ts
44
+ function hashContent(content) {
45
+ return "sha256:" + (0, import_node_crypto.createHash)("sha256").update(content).digest("hex");
46
+ }
47
+ function setActiveFailureCache(cache) {
48
+ activeCache = cache;
46
49
  }
47
- var import_node_fs, import_node_path, CONFIG_FILENAME;
48
- var init_config = __esm({
49
- "src/server/graph/core/config.ts"() {
50
+ function getActiveFailureCache() {
51
+ return activeCache;
52
+ }
53
+ var import_node_crypto, import_node_fs4, import_node_path4, SCHEMA_VERSION, ParseFailureCache, activeCache;
54
+ var init_parse_failure_cache = __esm({
55
+ "src/server/graph/core/parse-failure-cache.ts"() {
50
56
  "use strict";
51
- import_node_fs = require("node:fs");
52
- import_node_path = require("node:path");
53
- CONFIG_FILENAME = ".launchchart.json";
57
+ import_node_crypto = require("node:crypto");
58
+ import_node_fs4 = require("node:fs");
59
+ import_node_path4 = require("node:path");
60
+ init_launch_kit_paths();
61
+ SCHEMA_VERSION = 1;
62
+ ParseFailureCache = class {
63
+ constructor(rootDir, treeSitterFingerprint) {
64
+ this.failures = /* @__PURE__ */ new Map();
65
+ this.dirty = false;
66
+ this.cacheFilePath = (0, import_node_path4.join)(rootDir, LAUNCHSECURE_DIR, "graphs", ".failures.json");
67
+ this.fingerprint = treeSitterFingerprint;
68
+ this.load();
69
+ }
70
+ /** Path the cache persists to. Exposed so tier 3 can audit it. */
71
+ get filePath() {
72
+ return this.cacheFilePath;
73
+ }
74
+ /**
75
+ * Return the entry for absPath IF the cached entry's contentHash matches the
76
+ * current bytes. Otherwise null (caller should attempt a real parse).
77
+ */
78
+ shouldSkip(absPath, content) {
79
+ const cached = this.failures.get(absPath);
80
+ if (!cached) return null;
81
+ if (cached.contentHash !== hashContent(content)) return null;
82
+ return cached;
83
+ }
84
+ /** Record a parse failure. Increments consecutiveAttempts when hash matches. */
85
+ recordFailure(absPath, content, error) {
86
+ const now = (/* @__PURE__ */ new Date()).toISOString();
87
+ const hash = hashContent(content);
88
+ const prior = this.failures.get(absPath);
89
+ const entry = {
90
+ contentHash: hash,
91
+ failedAt: prior && prior.contentHash === hash ? prior.failedAt : now,
92
+ lastAttemptAt: now,
93
+ lastError: error,
94
+ consecutiveAttempts: prior && prior.contentHash === hash ? prior.consecutiveAttempts + 1 : 1
95
+ };
96
+ this.failures.set(absPath, entry);
97
+ this.dirty = true;
98
+ }
99
+ /** Bump the lastAttemptAt timestamp on a skip without changing the failure record. */
100
+ bumpAttempt(absPath) {
101
+ const cached = this.failures.get(absPath);
102
+ if (!cached) return;
103
+ cached.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
104
+ this.dirty = true;
105
+ }
106
+ /** Clear a single entry (call on successful parse). */
107
+ clear(absPath) {
108
+ if (!this.failures.has(absPath)) return false;
109
+ this.failures.delete(absPath);
110
+ this.dirty = true;
111
+ return true;
112
+ }
113
+ /** Wipe the entire cache (call after a successful tier-3 full regen). */
114
+ clearAll() {
115
+ if (this.failures.size === 0) return;
116
+ this.failures.clear();
117
+ this.dirty = true;
118
+ }
119
+ /** Snapshot for meta/status surfacing. */
120
+ list() {
121
+ const out = [];
122
+ for (const [path, entry] of this.failures) out.push({ path, ...entry });
123
+ return out;
124
+ }
125
+ size() {
126
+ return this.failures.size;
127
+ }
128
+ /**
129
+ * Atomically persist to disk. No-op if nothing has changed since last flush.
130
+ * Uses temp + rename so a torn write never corrupts the cache.
131
+ */
132
+ flush() {
133
+ if (!this.dirty) return;
134
+ const payload = {
135
+ schemaVersion: SCHEMA_VERSION,
136
+ treeSitterFingerprint: this.fingerprint,
137
+ failures: Object.fromEntries(this.failures)
138
+ };
139
+ const dir = (0, import_node_path4.dirname)(this.cacheFilePath);
140
+ const tmp = this.cacheFilePath + ".tmp";
141
+ try {
142
+ require("node:fs").mkdirSync(dir, { recursive: true });
143
+ (0, import_node_fs4.writeFileSync)(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
144
+ (0, import_node_fs4.renameSync)(tmp, this.cacheFilePath);
145
+ this.dirty = false;
146
+ } catch (e) {
147
+ process.stderr.write(`[lc-failure-cache] flush failed for ${this.cacheFilePath}: ${e.message}
148
+ `);
149
+ }
150
+ }
151
+ load() {
152
+ if (!(0, import_node_fs4.existsSync)(this.cacheFilePath)) return;
153
+ let parsed;
154
+ try {
155
+ parsed = JSON.parse((0, import_node_fs4.readFileSync)(this.cacheFilePath, "utf-8"));
156
+ } catch (e) {
157
+ process.stderr.write(`[lc-failure-cache] cache file unreadable, starting fresh: ${e.message}
158
+ `);
159
+ return;
160
+ }
161
+ if (!parsed || parsed.schemaVersion !== SCHEMA_VERSION) {
162
+ process.stderr.write(`[lc-failure-cache] schema version mismatch, discarding cache
163
+ `);
164
+ return;
165
+ }
166
+ if (parsed.treeSitterFingerprint !== this.fingerprint) {
167
+ process.stderr.write(`[lc-failure-cache] tree-sitter fingerprint changed, retrying all cached failures
168
+ `);
169
+ return;
170
+ }
171
+ for (const [path, entry] of Object.entries(parsed.failures)) {
172
+ this.failures.set(path, entry);
173
+ }
174
+ }
175
+ };
176
+ activeCache = null;
54
177
  }
55
178
  });
56
179
 
57
180
  // src/server/graph/core/ts-extractor.ts
58
181
  var ts_extractor_exports = {};
59
182
  __export(ts_extractor_exports, {
183
+ ParseCascadeError: () => ParseCascadeError,
60
184
  classifyFile: () => classifyFile,
185
+ classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
61
186
  createQuery: () => createQuery,
62
187
  extractAuthWrappersTS: () => extractAuthWrappersTS,
63
188
  extractDbCallsTS: () => extractDbCallsTS,
64
189
  extractDeep: () => extractDeep,
190
+ extractEffects: () => extractEffects,
191
+ extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
192
+ getTreeSitterFingerprint: () => getTreeSitterFingerprint,
65
193
  initTreeSitter: () => initTreeSitter,
194
+ middlewarePatternToRegex: () => middlewarePatternToRegex,
66
195
  parseCodeTS: () => parseCodeTS,
67
196
  parseFileTS: () => parseFileTS,
197
+ parseSource: () => parseSource,
68
198
  setExtractorConfig: () => setExtractorConfig
69
199
  });
70
200
  async function initTreeSitter() {
@@ -75,7 +205,8 @@ async function initTreeSitter() {
75
205
  await TreeSitter.init();
76
206
  const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
77
207
  tsxLanguage = await TreeSitter.Language.load(wasmPath);
78
- parserInstance = new TreeSitter();
208
+ TreeSitterCtor = TreeSitter;
209
+ parserInstance = new TreeSitterCtor();
79
210
  parserInstance.setLanguage(tsxLanguage);
80
211
  initialized = true;
81
212
  })();
@@ -86,20 +217,92 @@ function ensureInit() {
86
217
  throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
87
218
  }
88
219
  }
220
+ function getTreeSitterFingerprint() {
221
+ const read = (pkg) => {
222
+ try {
223
+ return String(require(`${pkg}/package.json`).version ?? "unknown");
224
+ } catch {
225
+ return "unknown";
226
+ }
227
+ };
228
+ return `web-tree-sitter@${read("web-tree-sitter")}+tree-sitter-typescript@${read("tree-sitter-typescript")}`;
229
+ }
89
230
  function getQuery(name) {
90
231
  ensureInit();
91
232
  const cached = queryCache.get(name);
92
233
  if (cached) return cached;
93
- const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
94
- const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
234
+ const scmPath = (0, import_node_path5.join)(queriesDir, `${name}.scm`);
235
+ const scm = (0, import_node_fs5.readFileSync)(scmPath, "utf-8");
95
236
  const query = tsxLanguage.query(scm);
96
237
  queryCache.set(name, query);
97
238
  return query;
98
239
  }
240
+ function recreateParser() {
241
+ if (!TreeSitterCtor || !tsxLanguage) {
242
+ throw new Error("recreateParser() called before initTreeSitter()");
243
+ }
244
+ try {
245
+ parserInstance?.delete?.();
246
+ } catch {
247
+ }
248
+ parserInstance = new TreeSitterCtor();
249
+ parserInstance.setLanguage(tsxLanguage);
250
+ }
99
251
  function parseSource(absPath) {
100
252
  ensureInit();
101
- const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
102
- return parserInstance.parse(content);
253
+ let content;
254
+ try {
255
+ const stat = (0, import_node_fs5.statSync)(absPath);
256
+ if (stat.size > MAX_PARSEABLE_BYTES) {
257
+ process.stderr.write(`[lc-extractor] skipping ${absPath}: ${stat.size} bytes exceeds max ${MAX_PARSEABLE_BYTES}
258
+ `);
259
+ return null;
260
+ }
261
+ content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
262
+ } catch (e) {
263
+ process.stderr.write(`[lc-extractor] read failed for ${absPath}: ${e instanceof Error ? e.message : String(e)}
264
+ `);
265
+ return null;
266
+ }
267
+ const cache = getActiveFailureCache();
268
+ if (cache) {
269
+ const skipEntry = cache.shouldSkip(absPath, content);
270
+ if (skipEntry) {
271
+ cache.bumpAttempt(absPath);
272
+ process.stderr.write(`[lc-extractor] skipping ${absPath}: known-bad (${skipEntry.consecutiveAttempts} attempts, last error: ${skipEntry.lastError})
273
+ `);
274
+ return null;
275
+ }
276
+ }
277
+ try {
278
+ const tree = parserInstance.parse(content);
279
+ consecutiveParseFailures = 0;
280
+ cache?.clear(absPath);
281
+ return tree;
282
+ } catch (e) {
283
+ consecutiveParseFailures++;
284
+ const msg = e instanceof Error ? e.message : String(e);
285
+ process.stderr.write(
286
+ `[lc-extractor] parse failed for ${absPath}: ${msg} (consecutive failures: ${consecutiveParseFailures}/${MAX_CONSECUTIVE_PARSE_FAILURES})
287
+ `
288
+ );
289
+ cache?.recordFailure(absPath, content, msg);
290
+ try {
291
+ recreateParser();
292
+ } catch (recreateErr) {
293
+ const rmsg = recreateErr instanceof Error ? recreateErr.message : String(recreateErr);
294
+ process.stderr.write(`[lc-extractor] FATAL: recreateParser failed after ${absPath}: ${rmsg}
295
+ `);
296
+ consecutiveParseFailures = 0;
297
+ throw recreateErr;
298
+ }
299
+ if (consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
300
+ const tripCount = consecutiveParseFailures;
301
+ consecutiveParseFailures = 0;
302
+ throw new ParseCascadeError(absPath, tripCount);
303
+ }
304
+ return null;
305
+ }
103
306
  }
104
307
  function parseCodeTS(code) {
105
308
  ensureInit();
@@ -139,8 +342,20 @@ function childrenOfType(node, type) {
139
342
  function childOfType(node, type) {
140
343
  return node.children.find((n) => n.type === type);
141
344
  }
345
+ function emptyParsedFile(absPath) {
346
+ return {
347
+ name: absPath.split("/").pop() ?? absPath,
348
+ exports: [],
349
+ imports: [],
350
+ reExports: [],
351
+ jsxElements: /* @__PURE__ */ new Set(),
352
+ navigations: [],
353
+ fetchCalls: []
354
+ };
355
+ }
142
356
  function parseFileTS(absPath) {
143
357
  const tree = parseSource(absPath);
358
+ if (!tree) return emptyParsedFile(absPath);
144
359
  const root = tree.rootNode;
145
360
  const imports = [];
146
361
  const importStatements = childrenOfType(root, "import_statement");
@@ -285,6 +500,12 @@ function parseFileTS(absPath) {
285
500
  if (linkTemplate) {
286
501
  navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
287
502
  }
503
+ if (caps["nav.redirect.literal"]) {
504
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.literal"], isTemplate: false });
505
+ }
506
+ if (caps["nav.redirect.template"]) {
507
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.template"], isTemplate: true });
508
+ }
288
509
  if (caps["nav.window.literal"]) {
289
510
  navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
290
511
  }
@@ -329,6 +550,7 @@ function parseFileTS(absPath) {
329
550
  }
330
551
  function extractDbCallsTS(absPath) {
331
552
  const tree = parseSource(absPath);
553
+ if (!tree) return [];
332
554
  const root = tree.rootNode;
333
555
  const dbQuery = getQuery("db-calls");
334
556
  const matches = dbQuery.matches(root);
@@ -363,15 +585,25 @@ function extractDbCallsTS(absPath) {
363
585
  const seen = /* @__PURE__ */ new Set();
364
586
  for (const m of matches) {
365
587
  const caps = captureMap(m);
588
+ const sbTable = caps["sb.table"];
589
+ const sbMethod = caps["sb.method"];
590
+ if (sbTable && sbMethod) {
591
+ const key2 = `sql:${sbTable}.${sbMethod}`;
592
+ if (seen.has(key2)) continue;
593
+ seen.add(key2);
594
+ const isMutation = SUPABASE_MUTATION_METHODS_BUILTIN.has(sbMethod) || extraMutationMethods.includes(sbMethod);
595
+ calls.push({ model: sbTable, method: sbMethod, isMutation, kind: "sql" });
596
+ continue;
597
+ }
366
598
  const identifier = caps["db.identifier"];
367
599
  const model = caps["db.model"];
368
600
  const method = caps["db.method"];
369
601
  if (!identifier || !model || !method) continue;
370
602
  if (!dbIdentifiers.has(identifier)) continue;
371
- const key = `${model}.${method}`;
603
+ const key = `orm:${model}.${method}`;
372
604
  if (seen.has(key)) continue;
373
605
  seen.add(key);
374
- calls.push({ model, method, isMutation: getMutationMethods().has(method) });
606
+ calls.push({ model, method, isMutation: getMutationMethods().has(method), kind: "orm" });
375
607
  }
376
608
  return calls;
377
609
  }
@@ -379,12 +611,17 @@ function classifyFile(absPath) {
379
611
  const fileName = require("path").basename(absPath);
380
612
  if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
381
613
  if (fileName.includes(".stories.")) return "story";
614
+ if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
382
615
  const tree = parseSource(absPath);
616
+ if (!tree) return "util";
383
617
  const root = tree.rootNode;
384
618
  const classifyQuery = getQuery("classify");
385
619
  const captures = classifyQuery.captures(root);
386
620
  const capNames = new Set(captures.map((c) => c.name));
387
621
  if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
622
+ if (capNames.has("use_server_directive") && fileName !== "page.tsx" && fileName !== "layout.tsx") {
623
+ return "server-action";
624
+ }
388
625
  if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
389
626
  if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
390
627
  if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
@@ -395,6 +632,7 @@ function classifyFile(absPath) {
395
632
  }
396
633
  function extractAuthWrappersTS(absPath) {
397
634
  const tree = parseSource(absPath);
635
+ if (!tree) return /* @__PURE__ */ new Set();
398
636
  const root = tree.rootNode;
399
637
  const wrapperQuery = getQuery("wrappers");
400
638
  const matches = wrapperQuery.matches(root);
@@ -405,13 +643,386 @@ function extractAuthWrappersTS(absPath) {
405
643
  wrappers.add(caps["wrapper.fn_name"]);
406
644
  }
407
645
  }
646
+ const inlineHelpers = /* @__PURE__ */ new Set();
647
+ for (const stmt of childrenOfType(root, "import_statement")) {
648
+ const sourceNode = childOfType(stmt, "string");
649
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
650
+ if (!frag) continue;
651
+ const provider = INLINE_AUTH_IMPORTS.find((p) => p.module.test(frag.text));
652
+ if (!provider) continue;
653
+ const clause = childOfType(stmt, "import_clause");
654
+ if (!clause) continue;
655
+ const named = childOfType(clause, "named_imports");
656
+ if (!named) continue;
657
+ for (const specNode of childrenOfType(named, "import_specifier")) {
658
+ const ids = childrenOfType(specNode, "identifier");
659
+ const importedName = ids[0]?.text;
660
+ const localName = ids[ids.length - 1]?.text;
661
+ if (importedName && provider.helpers.includes(importedName) && localName) {
662
+ inlineHelpers.add(localName);
663
+ }
664
+ }
665
+ }
666
+ if (inlineHelpers.size > 0) {
667
+ const text = root.text;
668
+ for (const name of inlineHelpers) {
669
+ const re = new RegExp(`\\b${name}\\s*\\(`);
670
+ if (re.test(text)) {
671
+ wrappers.add("inline");
672
+ break;
673
+ }
674
+ }
675
+ }
408
676
  return wrappers;
409
677
  }
678
+ function inferIntentFromName(name) {
679
+ if (TRUST_AS_PROTECT_KEYS.has(name)) {
680
+ return { intent: "protect", hint: `Next.js config.${name} \u2014 paths matched run through middleware` };
681
+ }
682
+ for (const re of EXEMPT_NAME_PATTERNS) {
683
+ if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
684
+ }
685
+ for (const re of PROTECT_NAME_PATTERNS) {
686
+ if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
687
+ }
688
+ return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
689
+ }
690
+ function looksLikeRoutePattern(s) {
691
+ return s.startsWith("/") && !s.startsWith("//");
692
+ }
693
+ function collectStringsFromArray(arrNode) {
694
+ const out = [];
695
+ for (const child of arrNode.children) {
696
+ if (child.type !== "string") continue;
697
+ const frag = childOfType(child, "string_fragment");
698
+ if (frag) out.push(frag.text);
699
+ }
700
+ return out;
701
+ }
702
+ function findArrayInValue(valueNode) {
703
+ if (!valueNode) return null;
704
+ if (valueNode.type === "array") return valueNode;
705
+ if (valueNode.type === "call_expression") {
706
+ const args = childOfType(valueNode, "arguments");
707
+ if (!args) return null;
708
+ for (const c of args.children) {
709
+ if (c.type === "array") return c;
710
+ }
711
+ }
712
+ return null;
713
+ }
714
+ function extractMatcherFromDeclarator(decl) {
715
+ const nameNode = childOfType(decl, "identifier");
716
+ if (!nameNode) return null;
717
+ let valueNode;
718
+ for (const c of decl.children) {
719
+ if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
720
+ valueNode = c;
721
+ }
722
+ const arr = findArrayInValue(valueNode);
723
+ if (!arr) return null;
724
+ const strings = collectStringsFromArray(arr);
725
+ const routes = strings.filter(looksLikeRoutePattern);
726
+ if (routes.length === 0) return null;
727
+ const { intent, hint } = inferIntentFromName(nameNode.text);
728
+ return { name: nameNode.text, patterns: routes, intent, hint };
729
+ }
730
+ function extractMatchersFromObject(objNode) {
731
+ const out = [];
732
+ for (const pair of childrenOfType(objNode, "pair")) {
733
+ const key = childOfType(pair, "property_identifier");
734
+ if (!key) continue;
735
+ const arr = pair.children.find((c) => c.type === "array");
736
+ if (!arr) continue;
737
+ const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
738
+ if (routes.length === 0) continue;
739
+ const { intent, hint } = inferIntentFromName(key.text);
740
+ out.push({ name: key.text, patterns: routes, intent, hint });
741
+ }
742
+ return out;
743
+ }
744
+ function detectFallthroughProtect(root) {
745
+ const text = root.text;
746
+ const signals = [
747
+ /\bauth\.protect\s*\(/,
748
+ /\bauth\(\)\.protect\s*\(/,
749
+ /\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
750
+ /\bNextResponse\.redirect\s*\(/,
751
+ /\bthrow\s+new\s+\w*Unauthorized/i
752
+ ];
753
+ return signals.some((re) => re.test(text));
754
+ }
755
+ function extractMiddlewareAuthTS(absPath) {
756
+ if (!require("node:fs").existsSync(absPath)) return null;
757
+ const tree = parseSource(absPath);
758
+ if (!tree) return null;
759
+ const root = tree.rootNode;
760
+ const matchers = [];
761
+ for (const stmt of root.children) {
762
+ if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
763
+ for (const decl of childrenOfType(stmt, "variable_declarator")) {
764
+ const m = extractMatcherFromDeclarator(decl);
765
+ if (m) matchers.push(m);
766
+ let valueNode;
767
+ for (const c of decl.children) {
768
+ if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
769
+ valueNode = c;
770
+ }
771
+ if (valueNode?.type === "object") {
772
+ for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
773
+ }
774
+ }
775
+ }
776
+ for (const stmt of root.children) {
777
+ if (stmt.type !== "export_statement") continue;
778
+ const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
779
+ if (!decl) continue;
780
+ for (const d of childrenOfType(decl, "variable_declarator")) {
781
+ let valueNode;
782
+ for (const c of d.children) {
783
+ if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
784
+ valueNode = c;
785
+ }
786
+ if (valueNode?.type === "object") {
787
+ for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
788
+ }
789
+ const m = extractMatcherFromDeclarator(d);
790
+ if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
791
+ matchers.push(m);
792
+ }
793
+ }
794
+ }
795
+ return {
796
+ file: absPath,
797
+ matchers,
798
+ hasFallthroughProtect: detectFallthroughProtect(root)
799
+ };
800
+ }
801
+ function middlewarePatternToRegex(pattern) {
802
+ const negLookaheadMatch = /^(\/?)\(\(\?\!([^)]+)\)(\.\*|\.\+)\)$/.exec(pattern);
803
+ if (negLookaheadMatch) {
804
+ const [, lead, altsRaw, body] = negLookaheadMatch;
805
+ const escapedAlts = altsRaw.split("|").map((alt) => alt.trim().replace(/[.+*?^${}()|[\]\\]/g, "\\$&")).join("|");
806
+ try {
807
+ return new RegExp(`^${lead}(?!${escapedAlts})${body}$`);
808
+ } catch {
809
+ return null;
810
+ }
811
+ }
812
+ if (/\(\?\!/.test(pattern)) return null;
813
+ if (pattern.startsWith("(")) return null;
814
+ let src = "^";
815
+ let i = 0;
816
+ while (i < pattern.length) {
817
+ const ch = pattern[i];
818
+ if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
819
+ src += ".*";
820
+ i += 4;
821
+ continue;
822
+ }
823
+ if (ch === ":") {
824
+ i++;
825
+ while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
826
+ src += "[^/]+";
827
+ continue;
828
+ }
829
+ if (ch === "*") {
830
+ i++;
831
+ while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
832
+ if (pattern[i] === "?") {
833
+ i++;
834
+ src += ".*";
835
+ continue;
836
+ }
837
+ src += ".+";
838
+ continue;
839
+ }
840
+ if (/[.\\+?^${}()|[\]]/.test(ch)) {
841
+ src += "\\" + ch;
842
+ i++;
843
+ continue;
844
+ }
845
+ src += ch;
846
+ i++;
847
+ }
848
+ src += "$";
849
+ try {
850
+ return new RegExp(src);
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ function classifyRouteAgainstMiddleware(routePath, info) {
856
+ for (const m of info.matchers) {
857
+ if (m.intent === "ambiguous") continue;
858
+ for (const pat of m.patterns) {
859
+ const re = middlewarePatternToRegex(pat);
860
+ if (!re) continue;
861
+ if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
862
+ }
863
+ }
864
+ return null;
865
+ }
410
866
  function trunc(s, max = 120) {
411
867
  return s.length > max ? s.slice(0, max) + "..." : s;
412
868
  }
869
+ function dedup(arr) {
870
+ return Array.from(new Set(arr));
871
+ }
872
+ function firstStringArg(callExpr) {
873
+ const args = callExpr.childForFieldName("arguments");
874
+ if (!args) return null;
875
+ for (const c of args.namedChildren) {
876
+ if (c.type === "string") {
877
+ const frag = c.namedChildren.find((cc) => cc.type === "string_fragment");
878
+ return frag ? frag.text : c.text.replace(/^['"`]|['"`]$/g, "");
879
+ }
880
+ if (c.type === "template_string") {
881
+ return c.text.replace(/^`|`$/g, "");
882
+ }
883
+ }
884
+ return null;
885
+ }
886
+ function isFunctionLike(node) {
887
+ return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function_declaration" || node.type === "method_definition" || node.type === "generator_function" || node.type === "generator_function_declaration";
888
+ }
889
+ function stringLiteralValue(n) {
890
+ if (n.type !== "string") return null;
891
+ const frag = n.namedChildren.find((c) => c.type === "string_fragment");
892
+ if (frag) return frag.text;
893
+ return n.text.replace(/^['"`]|['"`]$/g, "");
894
+ }
895
+ function nthStringArg(callExpr, idx) {
896
+ const args = callExpr.childForFieldName("arguments");
897
+ if (!args) return null;
898
+ const child = args.namedChildren[idx];
899
+ return child ? stringLiteralValue(child) : null;
900
+ }
901
+ function objectArgIdValues(callExpr) {
902
+ const args = callExpr.childForFieldName("arguments");
903
+ if (!args) return [];
904
+ const out = [];
905
+ for (const arg of args.namedChildren) {
906
+ if (arg.type !== "object") continue;
907
+ for (const pair of arg.namedChildren) {
908
+ if (pair.type !== "pair") continue;
909
+ const key = pair.childForFieldName("key");
910
+ const val = pair.childForFieldName("value");
911
+ if (!key || !val) continue;
912
+ const keyText = key.type === "property_identifier" ? key.text : stringLiteralValue(key) ?? key.text;
913
+ if (keyText !== "id") continue;
914
+ const strVal = stringLiteralValue(val);
915
+ if (strVal) out.push(strVal);
916
+ }
917
+ }
918
+ return out;
919
+ }
920
+ function extractEffects(node, moduleOnly = false) {
921
+ const calls = [];
922
+ const dom_writes = [];
923
+ const subscribes = [];
924
+ const timers = [];
925
+ const persists = [];
926
+ const fetches = [];
927
+ const globals = [];
928
+ const dom_ids = [];
929
+ const window_events = [];
930
+ const storage_keys = [];
931
+ const fetch_urls = [];
932
+ function visit(n, depth) {
933
+ if (moduleOnly && depth > 0 && isFunctionLike(n)) return;
934
+ if (n.type === "call_expression") {
935
+ const fn = n.childForFieldName("function");
936
+ if (fn) {
937
+ const fnText = fn.text;
938
+ calls.push(fnText);
939
+ for (const id of objectArgIdValues(n)) dom_ids.push(id);
940
+ if (fn.type === "identifier") {
941
+ if (TIMER_FNS.has(fnText)) timers.push(fnText);
942
+ if (fnText === "fetch") {
943
+ const url = firstStringArg(n);
944
+ fetches.push(url ? `fetch("${url}")` : "fetch(...)");
945
+ if (url) fetch_urls.push(url);
946
+ }
947
+ } else if (fn.type === "member_expression") {
948
+ const obj = fn.childForFieldName("object");
949
+ const prop = fn.childForFieldName("property");
950
+ const objText = obj?.text ?? "";
951
+ const propText = prop?.text ?? "";
952
+ if (STORAGE_OBJECTS.has(objText)) {
953
+ const key = firstStringArg(n);
954
+ persists.push(key ? `${objText}.${propText}("${key}")` : `${objText}.${propText}(\u2026)`);
955
+ if (key) storage_keys.push(key);
956
+ } else if (propText === "addEventListener") {
957
+ const event = firstStringArg(n);
958
+ subscribes.push(event ? `${objText}.addEventListener("${event}")` : `${objText}.addEventListener(\u2026)`);
959
+ if (event && (objText === "window" || objText === "document")) {
960
+ window_events.push(`${objText}:${event}`);
961
+ }
962
+ } else if (propText === "removeEventListener") {
963
+ const event = firstStringArg(n);
964
+ subscribes.push(event ? `${objText}.removeEventListener("${event}")` : `${objText}.removeEventListener(\u2026)`);
965
+ } else if (propText === "subscribe" || propText === "on") {
966
+ subscribes.push(`${objText}.${propText}(\u2026)`);
967
+ } else if (DOM_METHOD_NAMES.has(propText)) {
968
+ dom_writes.push(`${objText}.${propText}(\u2026)`);
969
+ if (propText === "setAttribute" && nthStringArg(n, 0) === "id") {
970
+ const idVal = nthStringArg(n, 1);
971
+ if (idVal) dom_ids.push(idVal);
972
+ }
973
+ } else if (objText.endsWith(".classList") && CLASSLIST_METHODS.has(propText)) {
974
+ dom_writes.push(`${objText}.${propText}(\u2026)`);
975
+ } else if (objText === "history" && HISTORY_METHODS.has(propText)) {
976
+ globals.push(`history.${propText}(\u2026)`);
977
+ } else if (objText === "location" && LOCATION_METHODS.has(propText)) {
978
+ globals.push(`location.${propText}(\u2026)`);
979
+ } else if (objText === "window" && propText === "matchMedia") {
980
+ globals.push("window.matchMedia(\u2026)");
981
+ }
982
+ }
983
+ }
984
+ } else if (n.type === "assignment_expression") {
985
+ const lhs = n.children.find((c) => c.type !== "=");
986
+ const rhs = n.childForFieldName("right");
987
+ if (lhs?.type === "member_expression") {
988
+ const prop = lhs.childForFieldName("property");
989
+ const propText = prop?.text ?? "";
990
+ const lhsText = lhs.text;
991
+ if (lhsText.startsWith("document.cookie")) {
992
+ persists.push("document.cookie =");
993
+ } else if (lhsText.startsWith("document.title") || lhsText.startsWith("location.")) {
994
+ globals.push(`${lhsText} =`);
995
+ } else if (ASSIGN_DOM_PROPS.has(propText) || lhsText.includes(".style.") || lhsText.includes(".dataset.")) {
996
+ dom_writes.push(`${lhsText} =`);
997
+ if (propText === "id" && rhs) {
998
+ const idVal = stringLiteralValue(rhs);
999
+ if (idVal) dom_ids.push(idVal);
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+ for (const child of n.children) visit(child, depth + 1);
1005
+ }
1006
+ visit(node, 0);
1007
+ const out = {};
1008
+ if (calls.length) out.calls = dedup(calls);
1009
+ if (dom_writes.length) out.dom_writes = dedup(dom_writes);
1010
+ if (subscribes.length) out.subscribes = dedup(subscribes);
1011
+ if (timers.length) out.timers = dedup(timers);
1012
+ if (persists.length) out.persists = dedup(persists);
1013
+ if (fetches.length) out.fetches = dedup(fetches);
1014
+ if (globals.length) out.globals = dedup(globals);
1015
+ if (dom_ids.length) out.dom_ids = dedup(dom_ids);
1016
+ if (window_events.length) out.window_events = dedup(window_events);
1017
+ if (storage_keys.length) out.storage_keys = dedup(storage_keys);
1018
+ if (fetch_urls.length) out.fetch_urls = dedup(fetch_urls);
1019
+ return out;
1020
+ }
413
1021
  function extractDeep(absPath) {
414
1022
  const tree = parseSource(absPath);
1023
+ if (!tree) {
1024
+ return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
1025
+ }
415
1026
  const root = tree.rootNode;
416
1027
  const elements = [];
417
1028
  const elQuery = getQuery("deep/jsx-semantic");
@@ -504,12 +1115,18 @@ function extractDeep(absPath) {
504
1115
  const caps = captureMap(m);
505
1116
  const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
506
1117
  const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
1118
+ const initNode = m.captures.find((c) => c.name === "var.init")?.node;
507
1119
  if (caps["var.name"] && caps["var.init"]) {
508
- variables.push({
1120
+ const variable = {
509
1121
  name: caps["var.name"],
510
1122
  kind,
511
1123
  init: trunc(caps["var.init"], 100)
512
- });
1124
+ };
1125
+ if (initNode && isFunctionLike(initNode)) {
1126
+ const eff = extractEffects(initNode);
1127
+ if (Object.keys(eff).length > 0) variable.effects = eff;
1128
+ }
1129
+ variables.push(variable);
513
1130
  }
514
1131
  if (caps["var.destructured.obj"]) {
515
1132
  variables.push({
@@ -561,21 +1178,49 @@ function extractDeep(absPath) {
561
1178
  params.push({ name: caps["param.body"], source: "body" });
562
1179
  }
563
1180
  }
564
- return { elements, stateVars, conditions, variables, responses, params };
1181
+ const fileEffects = extractEffects(
1182
+ root,
1183
+ /* moduleOnly */
1184
+ false
1185
+ );
1186
+ const hasEffects = Object.keys(fileEffects).length > 0;
1187
+ return {
1188
+ elements,
1189
+ stateVars,
1190
+ conditions,
1191
+ variables,
1192
+ responses,
1193
+ params,
1194
+ ...hasEffects ? { effects: fileEffects } : {}
1195
+ };
565
1196
  }
566
- var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
1197
+ var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, TreeSitterCtor, initPromise, initialized, queriesDir, queryCache, MAX_PARSEABLE_BYTES, MAX_CONSECUTIVE_PARSE_FAILURES, consecutiveParseFailures, ParseCascadeError, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS, TRUST_AS_PROTECT_KEYS, TIMER_FNS, DOM_METHOD_NAMES, CLASSLIST_METHODS, STORAGE_OBJECTS, HISTORY_METHODS, LOCATION_METHODS, ASSIGN_DOM_PROPS;
567
1198
  var init_ts_extractor = __esm({
568
1199
  "src/server/graph/core/ts-extractor.ts"() {
569
1200
  "use strict";
570
- import_node_fs3 = require("node:fs");
571
- import_node_path3 = require("node:path");
1201
+ import_node_fs5 = require("node:fs");
1202
+ import_node_path5 = require("node:path");
1203
+ init_parse_failure_cache();
572
1204
  initialized = false;
573
1205
  queriesDir = (() => {
574
- const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
1206
+ const srcPath = (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "..", "queries");
575
1207
  if (require("fs").existsSync(srcPath)) return srcPath;
576
- return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
1208
+ return (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "graph", "queries");
577
1209
  })();
578
1210
  queryCache = /* @__PURE__ */ new Map();
1211
+ MAX_PARSEABLE_BYTES = 2 * 1024 * 1024;
1212
+ MAX_CONSECUTIVE_PARSE_FAILURES = 10;
1213
+ consecutiveParseFailures = 0;
1214
+ ParseCascadeError = class extends Error {
1215
+ constructor(lastPath, failureCount) {
1216
+ super(
1217
+ `tree-sitter parse cascade: ${failureCount} consecutive WASM failures (last file: ${lastPath}). The shared Parser's WASM heap is likely corrupted; aborting regen so the graph isn't silently degraded. Restart the chart server to reinitialize, then re-run generate_graph.`
1218
+ );
1219
+ this.lastPath = lastPath;
1220
+ this.failureCount = failureCount;
1221
+ this.name = "ParseCascadeError";
1222
+ }
1223
+ };
579
1224
  PRISMA_MUTATION_METHODS_BUILTIN = [
580
1225
  "create",
581
1226
  "createMany",
@@ -587,148 +1232,411 @@ var init_ts_extractor = __esm({
587
1232
  "delete",
588
1233
  "deleteMany"
589
1234
  ];
1235
+ SUPABASE_MUTATION_METHODS_BUILTIN = /* @__PURE__ */ new Set([
1236
+ "insert",
1237
+ "update",
1238
+ "delete",
1239
+ "upsert"
1240
+ ]);
590
1241
  DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
591
1242
  extraDbIdentifiers = [];
592
1243
  extraMutationMethods = [];
1244
+ INLINE_AUTH_IMPORTS = [
1245
+ { module: /^@clerk\/nextjs(\/server)?$/, helpers: ["auth", "currentUser"] },
1246
+ { module: /^next-auth(\/.+)?$/, helpers: ["auth", "getServerSession"] },
1247
+ { module: /^@auth\//, helpers: ["auth"] },
1248
+ { module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
1249
+ ];
1250
+ EXEMPT_NAME_PATTERNS = [
1251
+ /^is_?public/i,
1252
+ /^public_?routes?/i,
1253
+ /^public_?paths?/i,
1254
+ /^whitelist/i,
1255
+ /^allowlist/i,
1256
+ /^unauthenticated/i,
1257
+ /^anonymous/i,
1258
+ /^guest/i,
1259
+ /^skip_?auth/i,
1260
+ /^bypass/i
1261
+ ];
1262
+ PROTECT_NAME_PATTERNS = [
1263
+ /^is_?protected/i,
1264
+ /^protected_?routes?/i,
1265
+ /^protected_?paths?/i,
1266
+ /^require_?auth/i,
1267
+ /^auth_?required/i,
1268
+ /^private_?routes?/i,
1269
+ /^is_?admin/i,
1270
+ /^admin_?routes?/i,
1271
+ /^secured/i
1272
+ ];
1273
+ TRUST_AS_PROTECT_KEYS = /* @__PURE__ */ new Set(["matcher", "matchers"]);
1274
+ TIMER_FNS = /* @__PURE__ */ new Set([
1275
+ "setInterval",
1276
+ "setTimeout",
1277
+ "clearInterval",
1278
+ "clearTimeout",
1279
+ "requestAnimationFrame",
1280
+ "cancelAnimationFrame",
1281
+ "queueMicrotask"
1282
+ ]);
1283
+ DOM_METHOD_NAMES = /* @__PURE__ */ new Set([
1284
+ "setAttribute",
1285
+ "removeAttribute",
1286
+ "appendChild",
1287
+ "removeChild",
1288
+ "replaceChildren",
1289
+ "replaceWith",
1290
+ "insertBefore",
1291
+ "append",
1292
+ "prepend",
1293
+ "remove",
1294
+ "before",
1295
+ "after",
1296
+ "insertAdjacentHTML",
1297
+ "insertAdjacentElement"
1298
+ ]);
1299
+ CLASSLIST_METHODS = /* @__PURE__ */ new Set(["add", "remove", "toggle", "replace"]);
1300
+ STORAGE_OBJECTS = /* @__PURE__ */ new Set(["localStorage", "sessionStorage"]);
1301
+ HISTORY_METHODS = /* @__PURE__ */ new Set(["pushState", "replaceState", "back", "forward", "go"]);
1302
+ LOCATION_METHODS = /* @__PURE__ */ new Set(["assign", "replace", "reload"]);
1303
+ ASSIGN_DOM_PROPS = /* @__PURE__ */ new Set([
1304
+ "textContent",
1305
+ "innerHTML",
1306
+ "innerText",
1307
+ "value",
1308
+ "src",
1309
+ "href",
1310
+ "className",
1311
+ "id",
1312
+ "checked",
1313
+ "selected",
1314
+ "disabled"
1315
+ ]);
593
1316
  }
594
1317
  });
595
1318
 
596
- // src/server/chart-serve.ts
597
- var chart_serve_exports = {};
598
- __export(chart_serve_exports, {
599
- runServeCli: () => runServeCli,
600
- startChartServer: () => startChartServer
601
- });
602
- module.exports = __toCommonJS(chart_serve_exports);
603
- var import_node_http = __toESM(require("node:http"));
604
- var import_node_fs17 = __toESM(require("node:fs"));
605
- var import_node_path19 = __toESM(require("node:path"));
1319
+ // src/server/parse-worker-entry.ts
1320
+ var parse_worker_entry_exports = {};
1321
+ module.exports = __toCommonJS(parse_worker_entry_exports);
1322
+ var import_node_worker_threads = require("node:worker_threads");
606
1323
 
607
- // src/server/graph/index.ts
608
- var import_node_fs14 = require("node:fs");
609
- var import_node_path16 = require("node:path");
1324
+ // src/server/graph/core/config.ts
1325
+ var import_node_fs = require("node:fs");
1326
+ var import_node_path = require("node:path");
1327
+ init_launch_kit_paths();
1328
+ function loadConfig(rootDir) {
1329
+ const configPath = (0, import_node_path.join)(rootDir, LAUNCHCHART_CONFIG_FILE);
1330
+ if (!(0, import_node_fs.existsSync)(configPath)) return {};
1331
+ try {
1332
+ return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
1333
+ } catch {
1334
+ return {};
1335
+ }
1336
+ }
610
1337
 
611
1338
  // src/server/graph/core/graph-builder.ts
612
- var import_node_fs11 = require("node:fs");
613
- var import_node_path12 = require("node:path");
614
- init_config();
1339
+ var import_node_fs13 = require("node:fs");
1340
+ var import_node_path14 = require("node:path");
1341
+ init_launch_kit_paths();
615
1342
 
616
1343
  // src/server/graph/core/parser-registry.ts
617
- var import_node_path11 = require("node:path");
1344
+ var import_node_path13 = require("node:path");
618
1345
 
619
1346
  // src/server/graph/parsers/ts/typescript-project.ts
620
- var import_node_fs4 = require("node:fs");
621
- var import_node_path4 = require("node:path");
622
- init_config();
1347
+ var import_node_fs6 = require("node:fs");
1348
+ var import_node_path6 = require("node:path");
623
1349
 
624
1350
  // src/server/graph/core/resolve-paths.ts
1351
+ var import_node_fs3 = require("node:fs");
1352
+ var import_node_path3 = require("node:path");
1353
+
1354
+ // src/server/graph/core/walk.ts
625
1355
  var import_node_fs2 = require("node:fs");
626
1356
  var import_node_path2 = require("node:path");
627
- function detectDbDir(rootDir, config) {
628
- if (config.paths?.dbDir) return (0, import_node_path2.join)(rootDir, config.paths.dbDir);
629
- const prismaDir = (0, import_node_path2.join)(rootDir, "prisma");
630
- if ((0, import_node_fs2.existsSync)(prismaDir)) return prismaDir;
631
- return null;
632
- }
633
- function resolveProjectPaths(rootDir, config) {
634
- const dbDir = detectDbDir(rootDir, config);
635
- if (config.paths?.appDir) {
636
- const appDir = (0, import_node_path2.join)(rootDir, config.paths.appDir);
637
- const srcDir = config.paths.srcDir ? (0, import_node_path2.join)(rootDir, config.paths.srcDir) : (0, import_node_path2.dirname)(appDir);
638
- return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api"), dbDir };
639
- }
640
- const srcApp = (0, import_node_path2.join)(rootDir, "src", "app");
641
- if ((0, import_node_fs2.existsSync)(srcApp)) {
642
- return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api"), dbDir };
643
- }
644
- const rootApp = (0, import_node_path2.join)(rootDir, "app");
645
- if ((0, import_node_fs2.existsSync)(rootApp)) {
646
- return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api"), dbDir };
647
- }
648
- return null;
649
- }
650
-
651
- // src/server/graph/parsers/ts/typescript-project.ts
652
- init_ts_extractor();
653
- var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
654
- var CLASSIFICATION_TO_LAYER = {
655
- endpoint: "api",
656
- page: "ui",
657
- layout: "ui",
658
- component: "ui",
659
- ui: "ui",
660
- hook: "ui",
661
- context: "ui",
662
- config: "ui",
663
- lib: "ui",
664
- "mcp-tool": "ui",
665
- external: "ui"
666
- };
1357
+ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
1358
+ "node_modules",
1359
+ "dist",
1360
+ "build",
1361
+ "out",
1362
+ "coverage"
1363
+ ]);
667
1364
  function walk(dir, exts) {
668
1365
  const results = [];
669
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
670
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
671
- const full = (0, import_node_path4.join)(dir, entry.name);
1366
+ if (!(0, import_node_fs2.existsSync)(dir)) return results;
1367
+ for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
1368
+ const full = (0, import_node_path2.join)(dir, entry.name);
672
1369
  if (entry.isDirectory()) {
673
1370
  results.push(...walk(full, exts));
674
- } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
1371
+ } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
675
1372
  results.push(full);
676
1373
  }
677
1374
  }
678
1375
  return results;
679
1376
  }
680
- function walkWithIgnore(dir, exts, ignoreDirs) {
1377
+ function walkWithIgnore(dir, exts, opts = {}) {
681
1378
  const results = [];
682
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
683
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
1379
+ if (!(0, import_node_fs2.existsSync)(dir)) return results;
1380
+ const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
1381
+ for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
684
1382
  if (entry.isDirectory()) {
685
- if (ignoreDirs.has(entry.name)) continue;
686
- results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
687
- } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
688
- results.push((0, import_node_path4.join)(dir, entry.name));
1383
+ if (entry.name.startsWith(".")) continue;
1384
+ if (skip.has(entry.name)) continue;
1385
+ results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, opts));
1386
+ } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
1387
+ results.push((0, import_node_path2.join)(dir, entry.name));
689
1388
  }
690
1389
  }
691
1390
  return results;
692
1391
  }
693
- function toNodeId(srcDir, absPath) {
694
- return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
1392
+
1393
+ // src/server/graph/core/resolve-paths.ts
1394
+ function hasSqlFiles(dir) {
1395
+ if (!(0, import_node_fs3.existsSync)(dir)) return false;
1396
+ try {
1397
+ return (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true }).some(
1398
+ (e) => e.isFile() && e.name.endsWith(".sql")
1399
+ );
1400
+ } catch {
1401
+ return false;
1402
+ }
695
1403
  }
696
- function resolveImport(srcDir, specifier) {
697
- if (!specifier.startsWith("@/")) return null;
698
- const rel = specifier.slice(2);
699
- const base = (0, import_node_path4.join)(srcDir, rel);
700
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
701
- if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
1404
+ function hasNestedMigrationSql(dir) {
1405
+ if (!(0, import_node_fs3.existsSync)(dir)) return false;
1406
+ try {
1407
+ return (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true }).some(
1408
+ (e) => e.isDirectory() && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(dir, e.name, "migration.sql"))
1409
+ );
1410
+ } catch {
1411
+ return false;
702
1412
  }
703
- return null;
704
1413
  }
705
- function resolveRelativeImport(fromFile, specifier) {
706
- const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
707
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
708
- if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
1414
+ function resolveDbFromDir(dir) {
1415
+ if (!(0, import_node_fs3.existsSync)(dir)) return { kind: "none", schemaPath: null, migrationsDir: null };
1416
+ const schemaPath = (0, import_node_path3.join)(dir, "schema.prisma");
1417
+ if ((0, import_node_fs3.existsSync)(schemaPath)) {
1418
+ const migrationsDir2 = (0, import_node_path3.join)(dir, "migrations");
1419
+ return {
1420
+ kind: "prisma",
1421
+ schemaPath,
1422
+ migrationsDir: (0, import_node_fs3.existsSync)(migrationsDir2) ? migrationsDir2 : null
1423
+ };
709
1424
  }
710
- return null;
1425
+ const migrationsDir = (0, import_node_path3.join)(dir, "migrations");
1426
+ if (hasSqlFiles(migrationsDir) || hasNestedMigrationSql(migrationsDir)) {
1427
+ return { kind: "sql-migrations", migrationsDir, schemaPath: null };
1428
+ }
1429
+ if (hasSqlFiles(dir) || hasNestedMigrationSql(dir)) {
1430
+ return { kind: "sql-migrations", migrationsDir: dir, schemaPath: null };
1431
+ }
1432
+ return { kind: "none", schemaPath: null, migrationsDir: null };
711
1433
  }
712
- function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
713
- const cached = memo.get(barrelAbsPath);
714
- if (cached) return cached;
715
- if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
716
- visiting.add(barrelAbsPath);
717
- const parsed = parsedByPath.get(barrelAbsPath);
718
- const map = /* @__PURE__ */ new Map();
719
- if (!parsed) {
720
- visiting.delete(barrelAbsPath);
721
- memo.set(barrelAbsPath, map);
722
- return map;
1434
+ function detectDbConfig(rootDir, config) {
1435
+ if (config.paths?.dbDir) {
1436
+ return resolveDbFromDir((0, import_node_path3.join)(rootDir, config.paths.dbDir));
723
1437
  }
724
- for (const re of parsed.reExports) {
725
- if (!re.from.startsWith(".")) continue;
726
- const resolved = resolveRelativeImport(barrelAbsPath, re.from);
727
- if (!resolved) continue;
728
- if (re.isWildcard) {
729
- const targetBn = (0, import_node_path4.basename)(resolved);
730
- const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
731
- if (targetIsBarrel) {
1438
+ const candidates = ["prisma", "supabase", "drizzle", (0, import_node_path3.join)("db", "migrations"), "migrations"];
1439
+ for (const c of candidates) {
1440
+ const dir = (0, import_node_path3.join)(rootDir, c);
1441
+ const resolved = resolveDbFromDir(dir);
1442
+ if (resolved.kind !== "none") return resolved;
1443
+ }
1444
+ return { kind: "none", schemaPath: null, migrationsDir: null };
1445
+ }
1446
+ function detectDbDir(rootDir, config, dbConfig) {
1447
+ if (config.paths?.dbDir) return (0, import_node_path3.join)(rootDir, config.paths.dbDir);
1448
+ if (dbConfig.kind === "prisma") return (0, import_node_path3.dirname)(dbConfig.schemaPath);
1449
+ if (dbConfig.kind === "sql-migrations") return dbConfig.migrationsDir;
1450
+ return null;
1451
+ }
1452
+ var NON_SOURCE_DIRS = /* @__PURE__ */ new Set([
1453
+ ...DEFAULT_IGNORE_DIRS,
1454
+ // DB conventions (handled by db parsers)
1455
+ "prisma",
1456
+ "supabase",
1457
+ "drizzle",
1458
+ "migrations",
1459
+ // Web assets
1460
+ "public",
1461
+ "static",
1462
+ "assets",
1463
+ // Docs
1464
+ "docs",
1465
+ "documentation",
1466
+ // Test dirs (project tests aren't part of the structural graph)
1467
+ "tests",
1468
+ "__tests__",
1469
+ "e2e",
1470
+ "playwright",
1471
+ "cypress",
1472
+ // Monorepo workspace roots — separate graph projects per .launchchart.json
1473
+ "packages",
1474
+ "apps",
1475
+ "services",
1476
+ "libs"
1477
+ ]);
1478
+ function dirHasTSFiles(dir) {
1479
+ if (!(0, import_node_fs3.existsSync)(dir)) return false;
1480
+ try {
1481
+ const stack = [dir];
1482
+ while (stack.length > 0) {
1483
+ const cur = stack.pop();
1484
+ const entries = (0, import_node_fs3.readdirSync)(cur, { withFileTypes: true });
1485
+ for (const e of entries) {
1486
+ if (e.isFile() && (e.name.endsWith(".ts") || e.name.endsWith(".tsx"))) return true;
1487
+ if (e.isDirectory() && !e.name.startsWith(".") && !DEFAULT_IGNORE_DIRS.has(e.name)) {
1488
+ stack.push((0, import_node_path3.join)(cur, e.name));
1489
+ }
1490
+ }
1491
+ }
1492
+ } catch {
1493
+ }
1494
+ return false;
1495
+ }
1496
+ function collectCodeBearingChildren(dir, extraSkip) {
1497
+ if (!(0, import_node_fs3.existsSync)(dir)) return [];
1498
+ const out = [];
1499
+ try {
1500
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
1501
+ if (!entry.isDirectory()) continue;
1502
+ if (entry.name.startsWith(".")) continue;
1503
+ if (NON_SOURCE_DIRS.has(entry.name)) continue;
1504
+ if (extraSkip?.has(entry.name)) continue;
1505
+ const full = (0, import_node_path3.join)(dir, entry.name);
1506
+ if (dirHasTSFiles(full)) out.push(full);
1507
+ }
1508
+ } catch {
1509
+ }
1510
+ return out;
1511
+ }
1512
+ function detectSrcRoots(rootDir, srcDir, appDir, config) {
1513
+ if (config.paths?.srcRoots && config.paths.srcRoots.length > 0) {
1514
+ const roots2 = /* @__PURE__ */ new Set();
1515
+ roots2.add(appDir);
1516
+ for (const r of config.paths.srcRoots) {
1517
+ const abs = (0, import_node_path3.isAbsolute)(r) ? r : (0, import_node_path3.resolve)(rootDir, r);
1518
+ roots2.add(abs);
1519
+ }
1520
+ return [...roots2];
1521
+ }
1522
+ const roots = /* @__PURE__ */ new Set();
1523
+ roots.add(appDir);
1524
+ for (const c of collectCodeBearingChildren(srcDir)) roots.add(c);
1525
+ if (srcDir !== rootDir) {
1526
+ const skipSrcWrapper = /* @__PURE__ */ new Set([(0, import_node_path3.basename)(srcDir)]);
1527
+ for (const c of collectCodeBearingChildren(rootDir, skipSrcWrapper)) roots.add(c);
1528
+ }
1529
+ return [...roots];
1530
+ }
1531
+ var CONVENTION_NAMES = ["middleware.ts", "middleware.tsx", "instrumentation.ts", "instrumentation.tsx"];
1532
+ function detectConventionFiles(rootDir, srcDir) {
1533
+ const out = [];
1534
+ const seen = /* @__PURE__ */ new Set();
1535
+ const dirs = srcDir === rootDir ? [rootDir] : [srcDir, rootDir];
1536
+ for (const dir of dirs) {
1537
+ for (const name of CONVENTION_NAMES) {
1538
+ const full = (0, import_node_path3.join)(dir, name);
1539
+ if (!seen.has(full) && (0, import_node_fs3.existsSync)(full)) {
1540
+ try {
1541
+ if ((0, import_node_fs3.statSync)(full).isFile()) {
1542
+ seen.add(full);
1543
+ out.push(full);
1544
+ }
1545
+ } catch {
1546
+ }
1547
+ }
1548
+ }
1549
+ }
1550
+ return out;
1551
+ }
1552
+ function resolveProjectPaths(rootDir, config) {
1553
+ let srcDir;
1554
+ let appDir;
1555
+ if (config.paths?.appDir) {
1556
+ appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
1557
+ srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
1558
+ } else {
1559
+ const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
1560
+ const rootApp = (0, import_node_path3.join)(rootDir, "app");
1561
+ if ((0, import_node_fs3.existsSync)(srcApp)) {
1562
+ srcDir = (0, import_node_path3.join)(rootDir, "src");
1563
+ appDir = srcApp;
1564
+ } else if ((0, import_node_fs3.existsSync)(rootApp)) {
1565
+ srcDir = rootDir;
1566
+ appDir = rootApp;
1567
+ } else {
1568
+ return null;
1569
+ }
1570
+ }
1571
+ const apiDir = (0, import_node_path3.join)(appDir, "api");
1572
+ const dbConfig = detectDbConfig(rootDir, config);
1573
+ const dbDir = detectDbDir(rootDir, config, dbConfig);
1574
+ const srcRoots = detectSrcRoots(rootDir, srcDir, appDir, config);
1575
+ const conventionFiles = detectConventionFiles(rootDir, srcDir);
1576
+ return { srcDir, appDir, apiDir, dbDir, srcRoots, conventionFiles, dbConfig };
1577
+ }
1578
+
1579
+ // src/server/graph/parsers/ts/typescript-project.ts
1580
+ init_ts_extractor();
1581
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1582
+ var CLASSIFICATION_TO_LAYER = {
1583
+ endpoint: "api",
1584
+ "server-action": "api",
1585
+ page: "ui",
1586
+ layout: "ui",
1587
+ component: "ui",
1588
+ ui: "ui",
1589
+ hook: "ui",
1590
+ context: "ui",
1591
+ config: "ui",
1592
+ lib: "ui",
1593
+ "mcp-tool": "ui",
1594
+ middleware: "ui",
1595
+ external: "ui"
1596
+ };
1597
+ function toNodeId(srcDir, rootDir, absPath) {
1598
+ const relFromSrc = (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
1599
+ if (relFromSrc.startsWith("..")) {
1600
+ return (0, import_node_path6.relative)(rootDir, absPath).replace(/\\/g, "/");
1601
+ }
1602
+ return relFromSrc;
1603
+ }
1604
+ function resolveImport(srcDir, specifier) {
1605
+ if (!specifier.startsWith("@/")) return null;
1606
+ const rel = specifier.slice(2);
1607
+ const base = (0, import_node_path6.join)(srcDir, rel);
1608
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path6.join)(base, "index.ts"), (0, import_node_path6.join)(base, "index.tsx")]) {
1609
+ if ((0, import_node_fs6.existsSync)(c) && (0, import_node_fs6.statSync)(c).isFile()) return c;
1610
+ }
1611
+ return null;
1612
+ }
1613
+ function resolveRelativeImport(fromFile, specifier) {
1614
+ const base = (0, import_node_path6.join)((0, import_node_path6.dirname)(fromFile), specifier);
1615
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path6.join)(base, "index.ts"), (0, import_node_path6.join)(base, "index.tsx")]) {
1616
+ if ((0, import_node_fs6.existsSync)(c) && (0, import_node_fs6.statSync)(c).isFile()) return c;
1617
+ }
1618
+ return null;
1619
+ }
1620
+ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
1621
+ const cached = memo.get(barrelAbsPath);
1622
+ if (cached) return cached;
1623
+ if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
1624
+ visiting.add(barrelAbsPath);
1625
+ const parsed = parsedByPath.get(barrelAbsPath);
1626
+ const map = /* @__PURE__ */ new Map();
1627
+ if (!parsed) {
1628
+ visiting.delete(barrelAbsPath);
1629
+ memo.set(barrelAbsPath, map);
1630
+ return map;
1631
+ }
1632
+ for (const re of parsed.reExports) {
1633
+ if (!re.from.startsWith(".")) continue;
1634
+ const resolved = resolveRelativeImport(barrelAbsPath, re.from);
1635
+ if (!resolved) continue;
1636
+ if (re.isWildcard) {
1637
+ const targetBn = (0, import_node_path6.basename)(resolved);
1638
+ const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
1639
+ if (targetIsBarrel) {
732
1640
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
733
1641
  for (const [name, target] of nested) {
734
1642
  if (!map.has(name)) map.set(name, target);
@@ -753,12 +1661,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
753
1661
  const barrels = /* @__PURE__ */ new Map();
754
1662
  const memo = /* @__PURE__ */ new Map();
755
1663
  for (const [absPath, parsed] of parsedByPath) {
756
- const bn = (0, import_node_path4.basename)(absPath);
1664
+ const bn = (0, import_node_path6.basename)(absPath);
757
1665
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
758
1666
  if (parsed.reExports.length === 0) continue;
759
1667
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
760
1668
  if (map.size > 0) {
761
- const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
1669
+ const barrelId = (0, import_node_path6.relative)(srcDir, (0, import_node_path6.dirname)(absPath)).replace(/\\/g, "/");
762
1670
  barrels.set(barrelId, map);
763
1671
  }
764
1672
  }
@@ -767,26 +1675,32 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
767
1675
  function classifyType(absPath, id) {
768
1676
  const contentType = classifyFile(absPath);
769
1677
  if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
1678
+ if (/^app\/(.+\/)?page\.tsx$/.test(id)) return "page";
1679
+ if (/^app\/(.+\/)?layout\.tsx$/.test(id)) return "layout";
770
1680
  return contentType;
771
1681
  }
772
1682
  function extractRoute(id) {
773
1683
  if (!id.endsWith("/page.tsx")) return null;
774
1684
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
775
1685
  route = route.replace(/\/\([^)]+\)/g, "");
1686
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
1687
+ route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
776
1688
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
777
1689
  route = route.replace(/\/+/g, "/");
778
1690
  if (!route.startsWith("/")) route = "/" + route;
779
1691
  return route || "/";
780
1692
  }
781
1693
  function nameFromFilename(absPath) {
782
- return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
1694
+ return (0, import_node_path6.basename)(absPath, (0, import_node_path6.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
783
1695
  }
784
- function filePathToApiRoute(apiDir, absPath) {
785
- let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1696
+ function filePathToAppRoute(appDir, absPath) {
1697
+ let route = ("/" + (0, import_node_path6.relative)(appDir, absPath).replace(/\\/g, "/")).replace(/\/route\.tsx?$/, "");
1698
+ route = route.replace(/\/\([^)]+\)/g, "");
1699
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
1700
+ route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
786
1701
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
787
1702
  route = route.replace(/\/+/g, "/");
788
- if (route === "/") return "/api";
789
- return "/api" + route;
1703
+ return route === "" ? "/" : route;
790
1704
  }
791
1705
  function camelToPascal(s) {
792
1706
  if (!s) return s;
@@ -831,25 +1745,52 @@ function resolveTemplateLiteralRoute(template, routeToNodeId) {
831
1745
  function routeMatchScore(candidate, known) {
832
1746
  const segsA = candidate.split("/");
833
1747
  const segsB = known.split("/");
834
- if (segsA.length !== segsB.length) return -1;
835
1748
  let score = 0;
836
- for (let i = 0; i < segsA.length; i++) {
837
- const a = segsA[i], b = segsB[i];
1749
+ let i = 0, j = 0;
1750
+ while (i < segsA.length && j < segsB.length) {
1751
+ const a = segsA[i], b = segsB[j];
1752
+ if (b.startsWith("*") && b.endsWith("?")) {
1753
+ score += 1;
1754
+ return score;
1755
+ }
1756
+ if (b.startsWith("*")) {
1757
+ const remaining = segsA.length - i;
1758
+ if (remaining < 1) return -1;
1759
+ score += 1 + remaining;
1760
+ return score;
1761
+ }
838
1762
  if (a === b) {
839
1763
  score += 3;
1764
+ i++;
1765
+ j++;
840
1766
  continue;
841
1767
  }
842
1768
  if (a.startsWith(":") && b.startsWith(":")) {
843
1769
  score += 2;
1770
+ i++;
1771
+ j++;
844
1772
  continue;
845
1773
  }
846
1774
  if (a.startsWith(":") || b.startsWith(":")) {
847
- score += 0;
1775
+ i++;
1776
+ j++;
848
1777
  continue;
849
1778
  }
850
1779
  return -1;
851
1780
  }
852
- return score;
1781
+ if (i === segsA.length) {
1782
+ while (j < segsB.length) {
1783
+ const b = segsB[j];
1784
+ if (b.startsWith("*") && b.endsWith("?")) {
1785
+ score += 1;
1786
+ j++;
1787
+ continue;
1788
+ }
1789
+ return -1;
1790
+ }
1791
+ return score;
1792
+ }
1793
+ return -1;
853
1794
  }
854
1795
  function templateToRoute(template) {
855
1796
  return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -871,7 +1812,7 @@ function matchRouteToPage(route, routeToNodeId) {
871
1812
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
872
1813
  return null;
873
1814
  }
874
- function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
1815
+ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
875
1816
  const edges = [];
876
1817
  const flagged = [];
877
1818
  const seen = /* @__PURE__ */ new Set();
@@ -884,7 +1825,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
884
1825
  edges.push(edge);
885
1826
  }
886
1827
  function edgeTypeFor(isTypeOnlyImport, importedNames) {
887
- if (isTypeOnlyImport) return "imports";
1828
+ if (isTypeOnlyImport) return "imports_type";
888
1829
  const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
889
1830
  if (anyRendered) return "renders";
890
1831
  return "imports";
@@ -899,7 +1840,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
899
1840
  for (const name of names) {
900
1841
  const targetAbs = barrelMap.get(name);
901
1842
  if (targetAbs) {
902
- const targetId = toNodeId(srcDir, targetAbs);
1843
+ const targetId = toNodeId(srcDir, rootDir, targetAbs);
903
1844
  if (nodeIdSet.has(targetId)) {
904
1845
  if (!byTarget.has(targetId)) byTarget.set(targetId, []);
905
1846
  byTarget.get(targetId).push(name);
@@ -913,18 +1854,20 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
913
1854
  } else {
914
1855
  const resolved = resolveImport(srcDir, specifier);
915
1856
  if (resolved) {
916
- const targetId = toNodeId(srcDir, resolved);
1857
+ const targetId = toNodeId(srcDir, rootDir, resolved);
917
1858
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
918
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
1859
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
1860
+ addEdge(targetId, edgeTypeFor(allType, names));
919
1861
  }
920
1862
  }
921
1863
  }
922
1864
  } else if (specifier.startsWith(".")) {
923
1865
  const resolved = resolveRelativeImport(absPath, specifier);
924
1866
  if (resolved) {
925
- const targetId = toNodeId(srcDir, resolved);
1867
+ const targetId = toNodeId(srcDir, rootDir, resolved);
926
1868
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
927
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
1869
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
1870
+ addEdge(targetId, edgeTypeFor(allType, names));
928
1871
  }
929
1872
  }
930
1873
  }
@@ -966,24 +1909,30 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps,
966
1909
  }
967
1910
  return { edges, flagged };
968
1911
  }
969
- function hasNextConfig(rootDir) {
970
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
971
- }
972
1912
  function detect(rootDir) {
973
1913
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
974
- return paths !== null && hasNextConfig(rootDir);
1914
+ return paths !== null;
975
1915
  }
976
1916
  function generate(rootDir) {
977
1917
  const config = loadConfig(rootDir);
978
1918
  const paths = resolveProjectPaths(rootDir, config);
979
1919
  const srcDir = paths.srcDir;
980
- const apiDir = paths.apiDir;
981
- const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
982
- const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
983
- const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]);
984
- const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
985
- const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
986
- const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
1920
+ const allDiscovered = [];
1921
+ const discoveredSet = /* @__PURE__ */ new Set();
1922
+ for (const root of paths.srcRoots) {
1923
+ for (const f of walk(root, [".tsx", ".ts"])) {
1924
+ if (!discoveredSet.has(f)) {
1925
+ discoveredSet.add(f);
1926
+ allDiscovered.push(f);
1927
+ }
1928
+ }
1929
+ }
1930
+ for (const conv of paths.conventionFiles) {
1931
+ if (!discoveredSet.has(conv)) {
1932
+ discoveredSet.add(conv);
1933
+ allDiscovered.push(conv);
1934
+ }
1935
+ }
987
1936
  const parsedByPath = /* @__PURE__ */ new Map();
988
1937
  for (const absPath of allDiscovered) {
989
1938
  parsedByPath.set(absPath, parseFileTS(absPath));
@@ -993,9 +1942,9 @@ function generate(rootDir) {
993
1942
  const apiNodes = [];
994
1943
  const nodeIdSet = /* @__PURE__ */ new Set();
995
1944
  const routeToNodeId = /* @__PURE__ */ new Map();
996
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
1945
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path6.basename)(f).startsWith("index."));
997
1946
  for (const absPath of fileSet) {
998
- const id = toNodeId(srcDir, absPath);
1947
+ const id = toNodeId(srcDir, rootDir, absPath);
999
1948
  const type = classifyType(absPath, id);
1000
1949
  if (type === "test" || type === "story") continue;
1001
1950
  const parsed = parsedByPath.get(absPath);
@@ -1003,26 +1952,34 @@ function generate(rootDir) {
1003
1952
  const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
1004
1953
  nodeIdSet.add(id);
1005
1954
  if (layer === "api") {
1006
- const methods = [];
1007
- for (const exp of parsed.exports) {
1008
- if (HTTP_METHODS.has(exp)) methods.push(exp);
1009
- }
1010
1955
  const dbCalls = extractDbCallsTS(absPath);
1011
1956
  const authWrappers = extractAuthWrappersTS(absPath);
1012
1957
  const deep = extractDeep(absPath);
1013
- const routePath = (0, import_node_fs4.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
1014
1958
  const mutations = dbCalls.filter((c) => c.isMutation);
1015
1959
  const mutates = mutations.length > 0;
1016
1960
  const authStrategy = [...authWrappers];
1961
+ const isServerAction = type === "server-action";
1962
+ const methods = [];
1963
+ if (!isServerAction) {
1964
+ for (const exp of parsed.exports) {
1965
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
1966
+ }
1967
+ }
1968
+ const routePath = isServerAction ? null : filePathToAppRoute(paths.appDir, absPath);
1969
+ const actions = isServerAction ? parsed.exports.filter((e) => !HTTP_METHODS.has(e)) : [];
1017
1970
  apiNodes.push({
1018
1971
  id,
1019
- type: "endpoint",
1020
- name: routePath,
1972
+ type: isServerAction ? "server-action" : "endpoint",
1973
+ // For HTTP routes: name = URL path. For Server Actions: name = file id +
1974
+ // exported action names — the callable surface, not a URL.
1975
+ name: isServerAction ? actions.length > 0 ? `${id} (${actions.join(", ")})` : id : routePath,
1021
1976
  layer: "api",
1022
1977
  path: routePath,
1978
+ // null for Server Actions
1023
1979
  methods,
1024
1980
  handler: id,
1025
1981
  mutates,
1982
+ ...isServerAction ? { actions } : {},
1026
1983
  auth: authStrategy.length > 0 ? authStrategy : ["public"],
1027
1984
  db_models: [...new Set(dbCalls.map((c) => c.model))],
1028
1985
  db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
@@ -1030,12 +1987,15 @@ function generate(rootDir) {
1030
1987
  variables: deep.variables,
1031
1988
  responses: deep.responses,
1032
1989
  params: deep.params,
1990
+ ...deep.effects ? { effects: deep.effects } : {},
1033
1991
  _dbCalls: dbCalls
1034
1992
  // temp: used for cross-ref building below
1035
1993
  });
1036
1994
  } else {
1037
1995
  const route = extractRoute(id);
1038
1996
  const deep = extractDeep(absPath);
1997
+ const dbCalls = extractDbCallsTS(absPath);
1998
+ const authWrappers = type === "page" || type === "layout" ? [...extractAuthWrappersTS(absPath)] : [];
1039
1999
  uiNodes.push({
1040
2000
  id,
1041
2001
  type,
@@ -1046,19 +2006,29 @@ function generate(rootDir) {
1046
2006
  elements: deep.elements,
1047
2007
  stateVars: deep.stateVars,
1048
2008
  conditions: deep.conditions,
1049
- variables: deep.variables
2009
+ variables: deep.variables,
2010
+ ...deep.effects ? { effects: deep.effects } : {},
2011
+ ...authWrappers.length > 0 ? { auth: authWrappers } : {},
2012
+ ...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
1050
2013
  });
1051
- if (route) routeToNodeId.set(route, id);
2014
+ if (route) {
2015
+ routeToNodeId.set(route, id);
2016
+ const trimmed = route.replace(/\/\*[^/]+\?$/, "");
2017
+ if (trimmed && trimmed !== route && !routeToNodeId.has(trimmed)) {
2018
+ routeToNodeId.set(trimmed, id);
2019
+ }
2020
+ }
1052
2021
  }
1053
2022
  }
1054
2023
  const uiEdges = [];
1055
2024
  const uiFlagged = [];
1056
2025
  for (const absPath of fileSet) {
1057
- const id = toNodeId(srcDir, absPath);
2026
+ const id = toNodeId(srcDir, rootDir, absPath);
1058
2027
  if (!nodeIdSet.has(id)) continue;
1059
2028
  const parsed = parsedByPath.get(absPath);
1060
2029
  const { edges, flagged } = extractEdges(
1061
2030
  srcDir,
2031
+ rootDir,
1062
2032
  absPath,
1063
2033
  id,
1064
2034
  parsed,
@@ -1069,9 +2039,32 @@ function generate(rootDir) {
1069
2039
  uiEdges.push(...edges);
1070
2040
  uiFlagged.push(...flagged);
1071
2041
  }
2042
+ const layoutsById = /* @__PURE__ */ new Set();
2043
+ for (const n of uiNodes) {
2044
+ if (n.type === "layout") layoutsById.add(n.id);
2045
+ }
2046
+ function findClosestLayout(pageId) {
2047
+ let dir = pageId.replace(/\/page\.tsx$/, "");
2048
+ while (dir.length > 0) {
2049
+ const candidate = `${dir}/layout.tsx`;
2050
+ if (layoutsById.has(candidate)) return candidate;
2051
+ const slash = dir.lastIndexOf("/");
2052
+ if (slash < 0) break;
2053
+ dir = dir.slice(0, slash);
2054
+ }
2055
+ if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
2056
+ return null;
2057
+ }
2058
+ for (const n of uiNodes) {
2059
+ if (n.type !== "page") continue;
2060
+ const layoutId = findClosestLayout(n.id);
2061
+ if (layoutId && layoutId !== n.id) {
2062
+ uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
2063
+ }
2064
+ }
1072
2065
  const fetchCallEntries = [];
1073
2066
  for (const absPath of fileSet) {
1074
- const sourceId = toNodeId(srcDir, absPath);
2067
+ const sourceId = toNodeId(srcDir, rootDir, absPath);
1075
2068
  if (!nodeIdSet.has(sourceId)) continue;
1076
2069
  const parsed = parsedByPath.get(absPath);
1077
2070
  if (parsed.fetchCalls.length === 0) continue;
@@ -1087,20 +2080,7 @@ function generate(rootDir) {
1087
2080
  });
1088
2081
  }
1089
2082
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
1090
- const IGNORE_DIRS = /* @__PURE__ */ new Set([
1091
- "node_modules",
1092
- ".next",
1093
- "dist",
1094
- ".launchsecure",
1095
- ".git",
1096
- "src",
1097
- "coverage",
1098
- ".turbo",
1099
- "build",
1100
- "out",
1101
- ".vercel"
1102
- ]);
1103
- const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
2083
+ const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], { extraIgnore: /* @__PURE__ */ new Set(["src"]) });
1104
2084
  for (const absPath of externalCandidates) {
1105
2085
  const normalized = absPath.replace(/\\/g, "/");
1106
2086
  if (externalScanned.has(normalized)) continue;
@@ -1110,7 +2090,7 @@ function generate(rootDir) {
1110
2090
  } catch {
1111
2091
  continue;
1112
2092
  }
1113
- const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
2093
+ const externalId = (0, import_node_path6.relative)(rootDir, absPath).replace(/\\/g, "/");
1114
2094
  const edgesFromThis = [];
1115
2095
  const seen = /* @__PURE__ */ new Set();
1116
2096
  for (const imp of parsed.imports) {
@@ -1123,7 +2103,7 @@ function generate(rootDir) {
1123
2103
  for (const name of names) {
1124
2104
  const targetAbs = barrelMap.get(name);
1125
2105
  if (!targetAbs) continue;
1126
- const targetId2 = toNodeId(srcDir, targetAbs);
2106
+ const targetId2 = toNodeId(srcDir, rootDir, targetAbs);
1127
2107
  if (!nodeIdSet.has(targetId2)) continue;
1128
2108
  const key2 = `${externalId}\u2192${targetId2}`;
1129
2109
  if (seen.has(key2)) continue;
@@ -1137,7 +2117,7 @@ function generate(rootDir) {
1137
2117
  resolved = resolveRelativeImport(absPath, specifier);
1138
2118
  }
1139
2119
  if (!resolved) continue;
1140
- const targetId = toNodeId(srcDir, resolved);
2120
+ const targetId = toNodeId(srcDir, rootDir, resolved);
1141
2121
  if (!nodeIdSet.has(targetId)) continue;
1142
2122
  if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
1143
2123
  const key = `${externalId}\u2192${targetId}`;
@@ -1157,23 +2137,169 @@ function generate(rootDir) {
1157
2137
  nodeIdSet.add(externalId);
1158
2138
  uiEdges.push(...edgesFromThis);
1159
2139
  }
2140
+ const tablesByFile = /* @__PURE__ */ new Map();
2141
+ const allDbNodes = [...apiNodes, ...uiNodes];
2142
+ for (const node of allDbNodes) {
2143
+ const calls = node._dbCalls;
2144
+ if (!calls || calls.length === 0) continue;
2145
+ const map = /* @__PURE__ */ new Map();
2146
+ for (const c of calls) {
2147
+ const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
2148
+ if (!map.has(key)) {
2149
+ map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
2150
+ }
2151
+ }
2152
+ tablesByFile.set(node.id, map);
2153
+ }
2154
+ const reverseRuntimeImports = /* @__PURE__ */ new Map();
2155
+ for (const edge of uiEdges) {
2156
+ if (edge.type !== "imports" && edge.type !== "renders") continue;
2157
+ if (!reverseRuntimeImports.has(edge.target)) {
2158
+ reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
2159
+ }
2160
+ reverseRuntimeImports.get(edge.target).add(edge.source);
2161
+ }
2162
+ let changed = true;
2163
+ let iterations = 0;
2164
+ while (changed && iterations < 50) {
2165
+ changed = false;
2166
+ iterations++;
2167
+ for (const [target, tableMap] of [...tablesByFile]) {
2168
+ const importers = reverseRuntimeImports.get(target);
2169
+ if (!importers) continue;
2170
+ for (const importer of importers) {
2171
+ if (importer === target) continue;
2172
+ let importerMap = tablesByFile.get(importer);
2173
+ if (!importerMap) {
2174
+ importerMap = /* @__PURE__ */ new Map();
2175
+ tablesByFile.set(importer, importerMap);
2176
+ }
2177
+ for (const [key, call] of tableMap) {
2178
+ if (importerMap.has(key)) continue;
2179
+ importerMap.set(key, {
2180
+ model: call.model,
2181
+ method: null,
2182
+ isMutation: call.isMutation,
2183
+ kind: call.kind,
2184
+ via: [...call.via, target]
2185
+ });
2186
+ changed = true;
2187
+ }
2188
+ }
2189
+ }
2190
+ }
2191
+ for (const node of apiNodes) {
2192
+ const map = tablesByFile.get(node.id);
2193
+ if (!map) continue;
2194
+ node.db_models = [...new Set([...map.values()].map((c) => c.model))];
2195
+ node.db_operations = [...new Set(
2196
+ [...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
2197
+ )];
2198
+ node.mutates = [...map.values()].some((c) => c.isMutation);
2199
+ }
1160
2200
  const apiCrossRefs = [];
1161
2201
  for (const node of apiNodes) {
1162
- const dbCalls = node._dbCalls;
1163
- if (!dbCalls) continue;
1164
- const seenModels = /* @__PURE__ */ new Set();
1165
- for (const call of dbCalls) {
1166
- if (seenModels.has(call.model)) continue;
1167
- seenModels.add(call.model);
1168
- apiCrossRefs.push({
2202
+ const map = tablesByFile.get(node.id);
2203
+ if (!map) {
2204
+ delete node._dbCalls;
2205
+ continue;
2206
+ }
2207
+ const seenTargets = /* @__PURE__ */ new Set();
2208
+ for (const call of map.values()) {
2209
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
2210
+ if (seenTargets.has(target)) continue;
2211
+ seenTargets.add(target);
2212
+ const isTransitive = call.via.length > 0;
2213
+ const ref = {
1169
2214
  source: node.id,
1170
- target: camelToPascal(call.model),
1171
- type: call.isMutation ? "mutates" : "reads",
2215
+ target,
2216
+ type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
1172
2217
  layer: "db"
1173
- });
2218
+ };
2219
+ if (isTransitive) ref.via = call.via;
2220
+ apiCrossRefs.push(ref);
1174
2221
  }
1175
2222
  delete node._dbCalls;
1176
2223
  }
2224
+ const uiCrossRefs = [];
2225
+ for (const node of uiNodes) {
2226
+ const map = tablesByFile.get(node.id);
2227
+ if (!map) {
2228
+ if (node._dbCalls) delete node._dbCalls;
2229
+ continue;
2230
+ }
2231
+ const seenTargets = /* @__PURE__ */ new Set();
2232
+ for (const call of map.values()) {
2233
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
2234
+ if (seenTargets.has(target)) continue;
2235
+ seenTargets.add(target);
2236
+ const isTransitive = call.via.length > 0;
2237
+ const ref = {
2238
+ source: node.id,
2239
+ target,
2240
+ type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
2241
+ layer: "db"
2242
+ };
2243
+ if (isTransitive) ref.via = call.via;
2244
+ uiCrossRefs.push(ref);
2245
+ }
2246
+ if (node._dbCalls) delete node._dbCalls;
2247
+ }
2248
+ uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
2249
+ const middlewareInfos = [];
2250
+ for (const conv of paths.conventionFiles) {
2251
+ if (!/middleware\.tsx?$/.test(conv)) continue;
2252
+ try {
2253
+ const info = extractMiddlewareAuthTS(conv);
2254
+ if (info && info.matchers.length > 0) middlewareInfos.push(info);
2255
+ } catch {
2256
+ }
2257
+ }
2258
+ if (middlewareInfos.length > 0) {
2259
+ let setAuth2 = function(node, newTags, replaceAll) {
2260
+ const existing = node.auth ?? [];
2261
+ const meaningful = existing.filter((a) => a !== "public");
2262
+ const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
2263
+ node.auth = merged.length > 0 ? merged : ["public"];
2264
+ }, applyMiddleware2 = function(node, routePath) {
2265
+ let resolved = null;
2266
+ let label = "";
2267
+ let hasAnyExemptMatcher = false;
2268
+ let hasAnyFallthrough = false;
2269
+ for (const info of middlewareInfos) {
2270
+ if (info.hasFallthroughProtect) hasAnyFallthrough = true;
2271
+ if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
2272
+ const c = classifyRouteAgainstMiddleware(routePath, info);
2273
+ if (!c) continue;
2274
+ if (!resolved) {
2275
+ resolved = c.intent;
2276
+ label = c.matcher;
2277
+ }
2278
+ }
2279
+ if (resolved === "exempt") {
2280
+ setAuth2(node, ["public"], true);
2281
+ return;
2282
+ }
2283
+ if (resolved === "protect") {
2284
+ setAuth2(node, [`middleware:${label}`], false);
2285
+ return;
2286
+ }
2287
+ if (hasAnyExemptMatcher && hasAnyFallthrough) {
2288
+ setAuth2(node, ["middleware-protected"], false);
2289
+ }
2290
+ };
2291
+ var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
2292
+ for (const node of apiNodes) {
2293
+ const routePath = node.path;
2294
+ if (!routePath) continue;
2295
+ applyMiddleware2(node, routePath);
2296
+ }
2297
+ for (const node of uiNodes) {
2298
+ const route = node.route;
2299
+ if (!route) continue;
2300
+ applyMiddleware2(node, route);
2301
+ }
2302
+ }
1177
2303
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1178
2304
  const apiEdges = [];
1179
2305
  const uiOnlyEdges = [];
@@ -1235,7 +2361,7 @@ function generate(rootDir) {
1235
2361
  },
1236
2362
  nodes: stripLayer(uiNodes),
1237
2363
  edges: uiOnlyEdges,
1238
- cross_refs: [],
2364
+ cross_refs: uiCrossRefs,
1239
2365
  contradictions: [],
1240
2366
  warnings: [],
1241
2367
  flagged_edges: dedupedFlagged,
@@ -1314,8 +2440,7 @@ var typescriptProjectParser = {
1314
2440
  };
1315
2441
 
1316
2442
  // src/server/graph/parsers/db/prisma-schema.ts
1317
- var import_node_fs5 = require("node:fs");
1318
- var import_node_path5 = require("node:path");
2443
+ var import_node_fs7 = require("node:fs");
1319
2444
  function parseModels(content) {
1320
2445
  const nodes = [];
1321
2446
  const relations = [];
@@ -1406,11 +2531,25 @@ function parseEnums(content) {
1406
2531
  return nodes;
1407
2532
  }
1408
2533
  function detect2(rootDir) {
1409
- return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
2534
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2535
+ return paths?.dbConfig.kind === "prisma" && (0, import_node_fs7.existsSync)(paths.dbConfig.schemaPath);
1410
2536
  }
1411
2537
  function generate2(rootDir) {
1412
- const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
1413
- const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
2538
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2539
+ if (paths.dbConfig.kind !== "prisma") {
2540
+ return {
2541
+ metadata: { generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), layer: "db", source: "none" },
2542
+ nodes: [],
2543
+ edges: [],
2544
+ cross_refs: [],
2545
+ contradictions: [],
2546
+ warnings: [],
2547
+ flagged_edges: [],
2548
+ patterns: { total_tables: 0, total_enums: 0, total_relations: 0 }
2549
+ };
2550
+ }
2551
+ const schemaPath = paths.dbConfig.schemaPath;
2552
+ const content = (0, import_node_fs7.readFileSync)(schemaPath, "utf-8");
1414
2553
  const { nodes: modelNodes, relations } = parseModels(content);
1415
2554
  const enumNodes = parseEnums(content);
1416
2555
  const allNodes = [...modelNodes, ...enumNodes];
@@ -1430,7 +2569,7 @@ function generate2(rootDir) {
1430
2569
  metadata: {
1431
2570
  generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1432
2571
  scope: "prisma-schema",
1433
- source: "prisma/schema.prisma",
2572
+ source: schemaPath,
1434
2573
  provider: "postgresql",
1435
2574
  layer: "db",
1436
2575
  total_models: modelNodes.length,
@@ -1467,8 +2606,8 @@ var prismaSchemaParser = {
1467
2606
  };
1468
2607
 
1469
2608
  // src/server/graph/parsers/db/sql-migrations.ts
1470
- var import_node_fs6 = require("node:fs");
1471
- var import_node_path6 = require("node:path");
2609
+ var import_node_fs8 = require("node:fs");
2610
+ var import_node_path7 = require("node:path");
1472
2611
  var PG_TO_PRISMA = {
1473
2612
  "TEXT": "String",
1474
2613
  "VARCHAR": "String",
@@ -1499,51 +2638,149 @@ function pgTypeToPrisma(pgType) {
1499
2638
  const upper = pgType.toUpperCase().trim();
1500
2639
  return PG_TO_PRISMA[upper] ?? upper;
1501
2640
  }
2641
+ var ID = `(?:"[\\w$]+"|[\\w$]+)`;
2642
+ var QID = `(?:${ID}\\.)?${ID}`;
2643
+ function bareName(captured) {
2644
+ const parts = captured.split(".");
2645
+ const last = parts[parts.length - 1];
2646
+ return last.replace(/^"(.*)"$/, "$1").trim();
2647
+ }
1502
2648
  function parseCreateTable(sql, state) {
1503
- const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
2649
+ const re = new RegExp(
2650
+ `CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QID})\\s*\\(([\\s\\S]*?)\\);`,
2651
+ "gi"
2652
+ );
1504
2653
  let m;
1505
2654
  while ((m = re.exec(sql)) !== null) {
1506
- const tableName = m[1];
2655
+ const tableName = bareName(m[1]);
1507
2656
  const body = m[2];
1508
2657
  const columns = /* @__PURE__ */ new Map();
1509
2658
  let primaryCol = null;
1510
- for (const line of body.split("\n")) {
1511
- const trimmed = line.trim().replace(/,\s*$/, "");
2659
+ const inlineFks = [];
2660
+ const lines = splitTopLevelCommas(body);
2661
+ for (const raw of lines) {
2662
+ const trimmed = raw.trim().replace(/,\s*$/, "");
1512
2663
  if (!trimmed || trimmed.startsWith("--")) continue;
1513
- const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
1514
- if (pkMatch) {
1515
- primaryCol = pkMatch[1];
2664
+ const namedPk = trimmed.match(new RegExp(`^CONSTRAINT\\s+${ID}\\s+PRIMARY\\s+KEY\\s*\\(\\s*(${QID})`, "i"));
2665
+ if (namedPk) {
2666
+ primaryCol = bareName(namedPk[1]);
2667
+ continue;
2668
+ }
2669
+ const tablePk = trimmed.match(new RegExp(`^PRIMARY\\s+KEY\\s*\\(\\s*(${QID})`, "i"));
2670
+ if (tablePk) {
2671
+ primaryCol = bareName(tablePk[1]);
2672
+ continue;
2673
+ }
2674
+ if (/^UNIQUE\s*\(/i.test(trimmed)) continue;
2675
+ const namedFk = trimmed.match(new RegExp(
2676
+ `^CONSTRAINT\\s+(${ID})\\s+FOREIGN\\s+KEY\\s*\\(\\s*(${QID})\\s*\\)\\s+REFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
2677
+ "i"
2678
+ ));
2679
+ if (namedFk) {
2680
+ inlineFks.push({
2681
+ constraintName: bareName(namedFk[1]),
2682
+ sourceTable: tableName,
2683
+ sourceColumn: bareName(namedFk[2]),
2684
+ targetTable: bareName(namedFk[3]),
2685
+ targetColumn: bareName(namedFk[4]),
2686
+ onDelete: namedFk[5] ?? null
2687
+ });
2688
+ continue;
2689
+ }
2690
+ const bareFk = trimmed.match(new RegExp(
2691
+ `^FOREIGN\\s+KEY\\s*\\(\\s*(${QID})\\s*\\)\\s+REFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
2692
+ "i"
2693
+ ));
2694
+ if (bareFk) {
2695
+ inlineFks.push({
2696
+ constraintName: `${tableName}_${bareName(bareFk[1])}_fkey`,
2697
+ sourceTable: tableName,
2698
+ sourceColumn: bareName(bareFk[1]),
2699
+ targetTable: bareName(bareFk[2]),
2700
+ targetColumn: bareName(bareFk[3]),
2701
+ onDelete: bareFk[4] ?? null
2702
+ });
1516
2703
  continue;
1517
2704
  }
1518
- if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
1519
- const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
2705
+ if (/^CONSTRAINT\s/i.test(trimmed)) continue;
2706
+ const colMatch = trimmed.match(new RegExp(`^(${ID})\\s+(.+)`, "i"));
1520
2707
  if (!colMatch) continue;
1521
- const colName = colMatch[1];
2708
+ const colName = bareName(colMatch[1]);
1522
2709
  let rest = colMatch[2];
2710
+ const inlineRefMatch = rest.match(new RegExp(
2711
+ `\\bREFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
2712
+ "i"
2713
+ ));
2714
+ if (inlineRefMatch) {
2715
+ inlineFks.push({
2716
+ constraintName: `${tableName}_${colName}_fkey`,
2717
+ sourceTable: tableName,
2718
+ sourceColumn: colName,
2719
+ targetTable: bareName(inlineRefMatch[1]),
2720
+ targetColumn: bareName(inlineRefMatch[2]),
2721
+ onDelete: inlineRefMatch[3] ?? null
2722
+ });
2723
+ rest = rest.replace(inlineRefMatch[0], "").trim();
2724
+ }
1523
2725
  const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
2726
+ const isPrimaryKey = /\bPRIMARY\s+KEY\b/i.test(rest);
2727
+ const isUnique = /\bUNIQUE\b/i.test(rest);
1524
2728
  const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
1525
2729
  const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
1526
- let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
2730
+ let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bPRIMARY\s+KEY\b/gi, "").replace(/\bUNIQUE\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
1527
2731
  columns.set(colName, {
1528
2732
  name: colName,
1529
2733
  type: colType,
1530
- nullable: !isNotNull,
1531
- primary: false,
1532
- unique: false,
2734
+ nullable: !isNotNull && !isPrimaryKey,
2735
+ primary: isPrimaryKey,
2736
+ unique: isUnique,
1533
2737
  default: defaultVal
1534
2738
  });
2739
+ if (isPrimaryKey) primaryCol = colName;
1535
2740
  }
1536
2741
  if (primaryCol && columns.has(primaryCol)) {
1537
2742
  columns.get(primaryCol).primary = true;
1538
2743
  }
1539
2744
  state.tables.set(tableName, { name: tableName, columns });
2745
+ state.fks.push(...inlineFks);
2746
+ }
2747
+ }
2748
+ function splitTopLevelCommas(body) {
2749
+ const out = [];
2750
+ let depth = 0;
2751
+ let buf = "";
2752
+ let inString = null;
2753
+ for (const ch of body) {
2754
+ if (inString) {
2755
+ buf += ch;
2756
+ if (ch === inString) inString = null;
2757
+ continue;
2758
+ }
2759
+ if (ch === "'" || ch === '"') {
2760
+ inString = ch;
2761
+ buf += ch;
2762
+ continue;
2763
+ }
2764
+ if (ch === "(") depth++;
2765
+ else if (ch === ")") depth--;
2766
+ if (ch === "," && depth === 0) {
2767
+ out.push(buf);
2768
+ buf = "";
2769
+ continue;
2770
+ }
2771
+ buf += ch;
1540
2772
  }
2773
+ if (buf.trim()) out.push(buf);
2774
+ return out;
1541
2775
  }
1542
2776
  function parseCreateEnum(sql, state) {
1543
- const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
2777
+ const re = new RegExp(
2778
+ `CREATE\\s+TYPE\\s+(${QID})\\s+AS\\s+ENUM\\s*\\(([^)]+)\\)`,
2779
+ "gi"
2780
+ );
1544
2781
  let m;
1545
2782
  while ((m = re.exec(sql)) !== null) {
1546
- const enumName = m[1];
2783
+ const enumName = bareName(m[1]);
1547
2784
  const valuesStr = m[2];
1548
2785
  const values = new Set(
1549
2786
  valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
@@ -1552,11 +2789,14 @@ function parseCreateEnum(sql, state) {
1552
2789
  }
1553
2790
  }
1554
2791
  function parseAlterTable(sql, state) {
1555
- const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
2792
+ const addColRe = new RegExp(
2793
+ `ALTER\\s+TABLE\\s+(${QID})\\s+ADD\\s+COLUMN\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(${QID})\\s+(.+?);`,
2794
+ "gi"
2795
+ );
1556
2796
  let m;
1557
2797
  while ((m = addColRe.exec(sql)) !== null) {
1558
- const tableName = m[1];
1559
- const colName = m[2];
2798
+ const tableName = bareName(m[1]);
2799
+ const colName = bareName(m[2]);
1560
2800
  let rest = m[3];
1561
2801
  const table = state.tables.get(tableName);
1562
2802
  if (!table) continue;
@@ -1573,64 +2813,92 @@ function parseAlterTable(sql, state) {
1573
2813
  default: defaultVal
1574
2814
  });
1575
2815
  }
1576
- const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
2816
+ const dropColRe = new RegExp(
2817
+ `ALTER\\s+TABLE\\s+(${QID})\\s+DROP\\s+COLUMN\\s+(?:IF\\s+EXISTS\\s+)?(${QID})`,
2818
+ "gi"
2819
+ );
1577
2820
  while ((m = dropColRe.exec(sql)) !== null) {
1578
- const table = state.tables.get(m[1]);
1579
- if (table) table.columns.delete(m[2]);
2821
+ const table = state.tables.get(bareName(m[1]));
2822
+ if (table) table.columns.delete(bareName(m[2]));
1580
2823
  }
1581
- 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;
2824
+ const fkRe = new RegExp(
2825
+ `ALTER\\s+TABLE\\s+(${QID})\\s+ADD\\s+CONSTRAINT\\s+(${ID})\\s+FOREIGN\\s+KEY\\s*\\(\\s*(${QID})\\s*\\)\\s+REFERENCES\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)(?:\\s+ON\\s+DELETE\\s+(\\w+(?:\\s+\\w+)?))?`,
2826
+ "gi"
2827
+ );
1582
2828
  while ((m = fkRe.exec(sql)) !== null) {
1583
2829
  state.fks.push({
1584
- constraintName: m[2],
1585
- sourceTable: m[1],
1586
- sourceColumn: m[3],
1587
- targetTable: m[4],
1588
- targetColumn: m[5],
2830
+ constraintName: bareName(m[2]),
2831
+ sourceTable: bareName(m[1]),
2832
+ sourceColumn: bareName(m[3]),
2833
+ targetTable: bareName(m[4]),
2834
+ targetColumn: bareName(m[5]),
1589
2835
  onDelete: m[6] ?? null
1590
2836
  });
1591
2837
  }
1592
2838
  }
1593
2839
  function parseAlterEnum(sql, state) {
1594
- const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
2840
+ const re = new RegExp(
2841
+ `ALTER\\s+TYPE\\s+(${QID})\\s+ADD\\s+VALUE\\s+'([^']+)'`,
2842
+ "gi"
2843
+ );
1595
2844
  let m;
1596
2845
  while ((m = re.exec(sql)) !== null) {
1597
- const en = state.enums.get(m[1]);
2846
+ const en = state.enums.get(bareName(m[1]));
1598
2847
  if (en) en.values.add(m[2]);
1599
2848
  }
1600
2849
  }
1601
2850
  function parseDropTable(sql, state) {
1602
- const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
2851
+ const re = new RegExp(
2852
+ `DROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?(${QID})`,
2853
+ "gi"
2854
+ );
1603
2855
  let m;
1604
2856
  while ((m = re.exec(sql)) !== null) {
1605
- state.tables.delete(m[1]);
1606
- state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
2857
+ const dropped = bareName(m[1]);
2858
+ state.tables.delete(dropped);
2859
+ state.fks = state.fks.filter((fk) => fk.sourceTable !== dropped && fk.targetTable !== dropped);
1607
2860
  }
1608
2861
  }
1609
2862
  function parseUniqueIndex(sql, state) {
1610
- const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
2863
+ const re = new RegExp(
2864
+ `CREATE\\s+UNIQUE\\s+INDEX\\s+(?:(?:IF\\s+NOT\\s+EXISTS\\s+)?(?:${ID}\\s+)?)?ON\\s+(${QID})\\s*\\(\\s*(${QID})\\s*\\)`,
2865
+ "gi"
2866
+ );
1611
2867
  let m;
1612
2868
  while ((m = re.exec(sql)) !== null) {
1613
- const table = state.tables.get(m[1]);
1614
- const col = table?.columns.get(m[2]);
2869
+ const tableName = bareName(m[1]);
2870
+ const colName = bareName(m[2]);
2871
+ const table = state.tables.get(tableName);
2872
+ const col = table?.columns.get(colName);
1615
2873
  if (col) col.unique = true;
1616
- if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
1617
- state.uniqueIndexes.get(m[1]).add(m[2]);
2874
+ if (!state.uniqueIndexes.has(tableName)) state.uniqueIndexes.set(tableName, /* @__PURE__ */ new Set());
2875
+ state.uniqueIndexes.get(tableName).add(colName);
2876
+ }
2877
+ }
2878
+ function discoverMigrationFiles(migrationsDir) {
2879
+ if (!(0, import_node_fs8.existsSync)(migrationsDir)) return [];
2880
+ const out = [];
2881
+ const entries = (0, import_node_fs8.readdirSync)(migrationsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
2882
+ for (const entry of entries) {
2883
+ if (entry.isDirectory()) {
2884
+ const sqlPath = (0, import_node_path7.join)(migrationsDir, entry.name, "migration.sql");
2885
+ if ((0, import_node_fs8.existsSync)(sqlPath)) out.push(sqlPath);
2886
+ } else if (entry.isFile() && entry.name.endsWith(".sql")) {
2887
+ out.push((0, import_node_path7.join)(migrationsDir, entry.name));
2888
+ }
1618
2889
  }
2890
+ return out;
1619
2891
  }
1620
- function parseMigrations(rootDir) {
1621
- const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
2892
+ function parseMigrations(migrationsDir) {
1622
2893
  const state = {
1623
2894
  tables: /* @__PURE__ */ new Map(),
1624
2895
  enums: /* @__PURE__ */ new Map(),
1625
2896
  fks: [],
1626
2897
  uniqueIndexes: /* @__PURE__ */ new Map()
1627
2898
  };
1628
- if (!(0, import_node_fs6.existsSync)(migrationsDir)) return state;
1629
- const dirs = (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
1630
- for (const dir of dirs) {
1631
- const sqlPath = (0, import_node_path6.join)(migrationsDir, dir, "migration.sql");
1632
- if (!(0, import_node_fs6.existsSync)(sqlPath)) continue;
1633
- const sql = (0, import_node_fs6.readFileSync)(sqlPath, "utf-8");
2899
+ if (!migrationsDir) return state;
2900
+ for (const sqlPath of discoverMigrationFiles(migrationsDir)) {
2901
+ const sql = (0, import_node_fs8.readFileSync)(sqlPath, "utf-8");
1634
2902
  parseCreateEnum(sql, state);
1635
2903
  parseCreateTable(sql, state);
1636
2904
  parseAlterTable(sql, state);
@@ -1640,10 +2908,9 @@ function parseMigrations(rootDir) {
1640
2908
  }
1641
2909
  return state;
1642
2910
  }
1643
- function loadPrismaState(rootDir) {
1644
- const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1645
- if (!(0, import_node_fs6.existsSync)(schemaPath)) return null;
1646
- const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
2911
+ function loadPrismaState(schemaPath) {
2912
+ if (!schemaPath || !(0, import_node_fs8.existsSync)(schemaPath)) return null;
2913
+ const content = (0, import_node_fs8.readFileSync)(schemaPath, "utf-8");
1647
2914
  const tables = /* @__PURE__ */ new Map();
1648
2915
  const enums = /* @__PURE__ */ new Map();
1649
2916
  const relations = [];
@@ -1807,14 +3074,28 @@ function verify(sqlState, prisma) {
1807
3074
  }
1808
3075
  return { contradictions, flaggedEdges };
1809
3076
  }
3077
+ function migrationsDirFor(rootDir) {
3078
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3079
+ if (!paths) return null;
3080
+ if (paths.dbConfig.kind === "prisma" || paths.dbConfig.kind === "sql-migrations") {
3081
+ return paths.dbConfig.migrationsDir;
3082
+ }
3083
+ return null;
3084
+ }
3085
+ function schemaPathFor(rootDir) {
3086
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3087
+ if (!paths) return null;
3088
+ return paths.dbConfig.kind === "prisma" ? paths.dbConfig.schemaPath : null;
3089
+ }
1810
3090
  function detect3(rootDir) {
1811
- const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
1812
- if (!(0, import_node_fs6.existsSync)(migrationsDir)) return false;
1813
- return (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).some((d) => d.isDirectory() && (0, import_node_fs6.existsSync)((0, import_node_path6.join)(migrationsDir, d.name, "migration.sql")));
3091
+ const dir = migrationsDirFor(rootDir);
3092
+ if (!dir) return false;
3093
+ return discoverMigrationFiles(dir).length > 0;
1814
3094
  }
1815
3095
  function generate3(rootDir) {
1816
- const sqlState = parseMigrations(rootDir);
1817
- const prisma = loadPrismaState(rootDir);
3096
+ const migrationsDir = migrationsDirFor(rootDir);
3097
+ const sqlState = parseMigrations(migrationsDir);
3098
+ const prisma = loadPrismaState(schemaPathFor(rootDir));
1818
3099
  const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
1819
3100
  const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
1820
3101
  const nodes = [];
@@ -1860,7 +3141,7 @@ function generate3(rootDir) {
1860
3141
  metadata: {
1861
3142
  generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1862
3143
  scope: "sql-migrations",
1863
- source: "prisma/migrations/",
3144
+ source: migrationsDir ?? "none",
1864
3145
  layer: "db",
1865
3146
  sql_tables: sqlState.tables.size,
1866
3147
  sql_enums: sqlState.enums.size,
@@ -1885,15 +3166,16 @@ var sqlMigrationsParser = {
1885
3166
  };
1886
3167
 
1887
3168
  // src/server/graph/core/api-route-matching.ts
3169
+ init_launch_kit_paths();
1888
3170
  function loadApiRoutesFromOutput(apiOutput) {
1889
3171
  const routes = [];
1890
3172
  for (const n of apiOutput.nodes) {
1891
- const path2 = n.path;
1892
- if (!path2 || typeof path2 !== "string") continue;
3173
+ const path = n.path;
3174
+ if (!path || typeof path !== "string") continue;
1893
3175
  routes.push({
1894
- path: path2,
3176
+ path,
1895
3177
  nodeId: n.id,
1896
- segments: path2.split("/").filter(Boolean)
3178
+ segments: path.split("/").filter(Boolean)
1897
3179
  });
1898
3180
  }
1899
3181
  return routes;
@@ -1902,6 +3184,10 @@ function buildApiPathMap(routes) {
1902
3184
  const map = /* @__PURE__ */ new Map();
1903
3185
  for (const r of routes) {
1904
3186
  if (!map.has(r.path)) map.set(r.path, r.nodeId);
3187
+ const trimmed = r.path.replace(/\/\*[^/]+\?$/, "");
3188
+ if (trimmed && trimmed !== r.path && !map.has(trimmed)) {
3189
+ map.set(trimmed, r.nodeId);
3190
+ }
1905
3191
  }
1906
3192
  return map;
1907
3193
  }
@@ -1924,26 +3210,54 @@ function normalizeFetchUrl(raw) {
1924
3210
  return { path: s || "/", hadInterpolation };
1925
3211
  }
1926
3212
  function scoreApiRouteMatch(candidate, known) {
1927
- if (candidate.length !== known.length) return -1;
1928
3213
  let score = 0;
1929
- for (let i = 0; i < candidate.length; i++) {
3214
+ let i = 0, j = 0;
3215
+ while (i < candidate.length && j < known.length) {
1930
3216
  const a = candidate[i];
1931
- const b = known[i];
3217
+ const b = known[j];
3218
+ if (b.startsWith("*") && b.endsWith("?")) {
3219
+ score += 1;
3220
+ return score;
3221
+ }
3222
+ if (b.startsWith("*")) {
3223
+ const remaining = candidate.length - i;
3224
+ if (remaining < 1) return -1;
3225
+ score += 1 + remaining;
3226
+ return score;
3227
+ }
1932
3228
  if (a === b) {
1933
3229
  score += 3;
3230
+ i++;
3231
+ j++;
1934
3232
  continue;
1935
3233
  }
1936
3234
  if (a.startsWith(":") && b.startsWith(":")) {
1937
3235
  score += 2;
3236
+ i++;
3237
+ j++;
1938
3238
  continue;
1939
3239
  }
1940
3240
  if (a.startsWith(":") || b.startsWith(":")) {
1941
3241
  score += 1;
3242
+ i++;
3243
+ j++;
1942
3244
  continue;
1943
3245
  }
1944
3246
  return -1;
1945
3247
  }
1946
- return score;
3248
+ if (i === candidate.length) {
3249
+ while (j < known.length) {
3250
+ const b = known[j];
3251
+ if (b.startsWith("*") && b.endsWith("?")) {
3252
+ score += 1;
3253
+ j++;
3254
+ continue;
3255
+ }
3256
+ return -1;
3257
+ }
3258
+ return score;
3259
+ }
3260
+ return -1;
1947
3261
  }
1948
3262
  function resolveFetchCall(call, apiPathMap, apiRoutes) {
1949
3263
  const raw = call.url;
@@ -1953,16 +3267,16 @@ function resolveFetchCall(call, apiPathMap, apiRoutes) {
1953
3267
  if (call.isConcat) {
1954
3268
  return { kind: "dynamic", normalizedUrl: raw };
1955
3269
  }
1956
- const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
1957
- if (!path2.startsWith("/")) {
1958
- return { kind: "unresolved", normalizedUrl: path2 };
3270
+ const { path, hadInterpolation } = normalizeFetchUrl(raw);
3271
+ if (!path.startsWith("/")) {
3272
+ return { kind: "unresolved", normalizedUrl: path };
1959
3273
  }
1960
- const segs = path2.split("/").filter(Boolean);
3274
+ const segs = path.split("/").filter(Boolean);
1961
3275
  if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1962
- return { kind: "dynamic", normalizedUrl: path2 };
3276
+ return { kind: "dynamic", normalizedUrl: path };
1963
3277
  }
1964
- const exact = apiPathMap.get(path2);
1965
- if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
3278
+ const exact = apiPathMap.get(path);
3279
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path };
1966
3280
  let bestScore = -1;
1967
3281
  let bestId = null;
1968
3282
  for (const r of apiRoutes) {
@@ -1973,21 +3287,21 @@ function resolveFetchCall(call, apiPathMap, apiRoutes) {
1973
3287
  }
1974
3288
  }
1975
3289
  if (bestId && bestScore > 0) {
1976
- return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
3290
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path };
1977
3291
  }
1978
- return { kind: "unresolved", normalizedUrl: path2 };
3292
+ return { kind: "unresolved", normalizedUrl: path };
1979
3293
  }
1980
3294
  function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
1981
- const { path: path2, hadInterpolation } = normalizeFetchUrl(urlPath);
1982
- if (!path2.startsWith("/")) {
1983
- return { kind: "unresolved", normalizedUrl: path2 };
3295
+ const { path, hadInterpolation } = normalizeFetchUrl(urlPath);
3296
+ if (!path.startsWith("/")) {
3297
+ return { kind: "unresolved", normalizedUrl: path };
1984
3298
  }
1985
- const segs = path2.split("/").filter(Boolean);
3299
+ const segs = path.split("/").filter(Boolean);
1986
3300
  if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1987
- return { kind: "dynamic", normalizedUrl: path2 };
3301
+ return { kind: "dynamic", normalizedUrl: path };
1988
3302
  }
1989
- const exact = apiPathMap.get(path2);
1990
- if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
3303
+ const exact = apiPathMap.get(path);
3304
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path };
1991
3305
  let bestScore = -1;
1992
3306
  let bestId = null;
1993
3307
  for (const r of apiRoutes) {
@@ -1998,9 +3312,9 @@ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
1998
3312
  }
1999
3313
  }
2000
3314
  if (bestId && bestScore > 0) {
2001
- return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
3315
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path };
2002
3316
  }
2003
- return { kind: "unresolved", normalizedUrl: path2 };
3317
+ return { kind: "unresolved", normalizedUrl: path };
2004
3318
  }
2005
3319
 
2006
3320
  // src/server/graph/parsers/crosslayer/fetch-resolver.ts
@@ -2098,50 +3412,59 @@ var fetchResolverParser = {
2098
3412
  };
2099
3413
 
2100
3414
  // src/server/graph/parsers/crosslayer/api-annotations.ts
2101
- var import_node_fs7 = require("node:fs");
2102
- var import_node_path7 = require("node:path");
3415
+ var import_node_fs9 = require("node:fs");
3416
+ var import_node_path8 = require("node:path");
2103
3417
  var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
2104
- function walk2(dir, exts) {
2105
- if (!(0, import_node_fs7.existsSync)(dir)) return [];
2106
- const results = [];
2107
- for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
2108
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2109
- const full = (0, import_node_path7.join)(dir, entry.name);
2110
- if (entry.isDirectory()) {
2111
- results.push(...walk2(full, exts));
2112
- } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
2113
- results.push(full);
2114
- }
3418
+ function toNodeId2(srcDir, rootDir, absPath) {
3419
+ const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
3420
+ if (relFromSrc.startsWith("..")) {
3421
+ return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
2115
3422
  }
2116
- return results;
2117
- }
2118
- function toNodeId2(srcDir, absPath) {
2119
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
3423
+ return relFromSrc;
2120
3424
  }
2121
3425
  var apiAnnotationsParser = {
2122
3426
  id: "api-annotations",
2123
3427
  layer: "crosslayer",
2124
3428
  concern: "api-binding",
2125
3429
  detect(rootDir) {
2126
- return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
3430
+ return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
2127
3431
  },
2128
3432
  generate(rootDir, layerOutputs) {
2129
3433
  const apiOutput = layerOutputs.get("api");
2130
3434
  if (!apiOutput) {
2131
3435
  return { cross_refs: [], flagged_edges: [], warnings: [] };
2132
3436
  }
3437
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3438
+ if (!paths) {
3439
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
3440
+ }
2133
3441
  const uiOutput = layerOutputs.get("ui");
2134
3442
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2135
3443
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
2136
3444
  const apiPathMap = buildApiPathMap(apiRoutes);
2137
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
2138
- const files = walk2(srcDir, [".ts", ".tsx"]);
3445
+ const srcDir = paths.srcDir;
3446
+ const seen = /* @__PURE__ */ new Set();
3447
+ const files = [];
3448
+ for (const root of paths.srcRoots) {
3449
+ for (const f of walk(root, [".ts", ".tsx"])) {
3450
+ if (!seen.has(f)) {
3451
+ seen.add(f);
3452
+ files.push(f);
3453
+ }
3454
+ }
3455
+ }
3456
+ for (const conv of paths.conventionFiles) {
3457
+ if (!seen.has(conv)) {
3458
+ seen.add(conv);
3459
+ files.push(conv);
3460
+ }
3461
+ }
2139
3462
  const crossRefs = [];
2140
3463
  const flaggedEdges = [];
2141
- const seen = /* @__PURE__ */ new Set();
3464
+ const seenEdge = /* @__PURE__ */ new Set();
2142
3465
  for (const absPath of files) {
2143
- const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
2144
- const sourceId = toNodeId2(srcDir, absPath);
3466
+ const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
3467
+ const sourceId = toNodeId2(srcDir, rootDir, absPath);
2145
3468
  if (!uiNodeIds.has(sourceId)) continue;
2146
3469
  let match;
2147
3470
  API_ANNOTATION_RE.lastIndex = 0;
@@ -2151,8 +3474,8 @@ var apiAnnotationsParser = {
2151
3474
  const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
2152
3475
  if (result.kind === "resolved" && result.nodeId) {
2153
3476
  const key = `${sourceId}|${result.nodeId}|calls_api`;
2154
- if (seen.has(key)) continue;
2155
- seen.add(key);
3477
+ if (seenEdge.has(key)) continue;
3478
+ seenEdge.add(key);
2156
3479
  crossRefs.push({
2157
3480
  source: sourceId,
2158
3481
  target: result.nodeId,
@@ -2184,26 +3507,15 @@ var apiAnnotationsParser = {
2184
3507
  };
2185
3508
 
2186
3509
  // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
2187
- var import_node_fs8 = require("node:fs");
2188
- var import_node_path8 = require("node:path");
2189
- init_config();
2190
- var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
2191
- function walk3(dir, exts) {
2192
- if (!(0, import_node_fs8.existsSync)(dir)) return [];
2193
- const results = [];
2194
- for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
2195
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2196
- const full = (0, import_node_path8.join)(dir, entry.name);
2197
- if (entry.isDirectory()) {
2198
- results.push(...walk3(full, exts));
2199
- } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
2200
- results.push(full);
2201
- }
2202
- }
2203
- return results;
2204
- }
2205
- function toNodeId3(srcDir, absPath) {
2206
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
3510
+ var import_node_fs10 = require("node:fs");
3511
+ var import_node_path9 = require("node:path");
3512
+ var URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
3513
+ function toNodeId3(srcDir, rootDir, absPath) {
3514
+ const relFromSrc = (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
3515
+ if (relFromSrc.startsWith("..")) {
3516
+ return (0, import_node_path9.relative)(rootDir, absPath).replace(/\\/g, "/");
3517
+ }
3518
+ return relFromSrc;
2207
3519
  }
2208
3520
  var urlLiteralScannerParser = {
2209
3521
  id: "url-literal-scanner",
@@ -2224,17 +3536,28 @@ var urlLiteralScannerParser = {
2224
3536
  const apiPathMap = buildApiPathMap(apiRoutes);
2225
3537
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2226
3538
  const srcDir = paths.srcDir;
2227
- const clientDir = (0, import_node_path8.join)(srcDir, "client");
2228
- const files = [
2229
- ...walk3(clientDir, [".ts", ".tsx"]),
2230
- ...walk3(paths.appDir, [".ts", ".tsx"])
2231
- ];
3539
+ const seenFile = /* @__PURE__ */ new Set();
3540
+ const files = [];
3541
+ for (const root of paths.srcRoots) {
3542
+ for (const f of walk(root, [".ts", ".tsx"])) {
3543
+ if (!seenFile.has(f)) {
3544
+ seenFile.add(f);
3545
+ files.push(f);
3546
+ }
3547
+ }
3548
+ }
3549
+ for (const conv of paths.conventionFiles) {
3550
+ if (!seenFile.has(conv)) {
3551
+ seenFile.add(conv);
3552
+ files.push(conv);
3553
+ }
3554
+ }
2232
3555
  const crossRefs = [];
2233
3556
  const seen = /* @__PURE__ */ new Set();
2234
3557
  for (const absPath of files) {
2235
- const sourceId = toNodeId3(srcDir, absPath);
3558
+ const sourceId = toNodeId3(srcDir, rootDir, absPath);
2236
3559
  if (!uiNodeIds.has(sourceId)) continue;
2237
- const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
3560
+ const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
2238
3561
  let match;
2239
3562
  URL_LITERAL_RE.lastIndex = 0;
2240
3563
  while ((match = URL_LITERAL_RE.exec(content)) !== null) {
@@ -2265,8 +3588,8 @@ var urlLiteralScannerParser = {
2265
3588
  };
2266
3589
 
2267
3590
  // src/server/graph/parsers/static/static-values.ts
2268
- var import_node_fs9 = require("node:fs");
2269
- var import_node_path9 = require("node:path");
3591
+ var import_node_fs11 = require("node:fs");
3592
+ var import_node_path10 = require("node:path");
2270
3593
  var parseCode = null;
2271
3594
  function tryLoadTreeSitter() {
2272
3595
  if (parseCode) return true;
@@ -2298,20 +3621,25 @@ function classifyScope(source, model) {
2298
3621
  function extractEnumValues(rootDir) {
2299
3622
  const nodes = [];
2300
3623
  const edges = [];
2301
- const schemaPaths = [
2302
- (0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"),
2303
- (0, import_node_path9.join)(rootDir, "prisma", "schema")
2304
- ];
3624
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3625
+ const schemaPaths = [];
3626
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
3627
+ schemaPaths.push(paths.dbConfig.schemaPath);
3628
+ schemaPaths.push((0, import_node_path10.join)((0, import_node_path10.dirname)(paths.dbConfig.schemaPath), "schema"));
3629
+ } else {
3630
+ schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"));
3631
+ schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema"));
3632
+ }
2305
3633
  let content = "";
2306
3634
  for (const p of schemaPaths) {
2307
- if ((0, import_node_fs9.existsSync)(p)) {
3635
+ if ((0, import_node_fs11.existsSync)(p)) {
2308
3636
  try {
2309
- const stat = (0, import_node_fs9.statSync)(p);
3637
+ const stat = (0, import_node_fs11.statSync)(p);
2310
3638
  if (stat.isFile()) {
2311
- content = (0, import_node_fs9.readFileSync)(p, "utf-8");
3639
+ content = (0, import_node_fs11.readFileSync)(p, "utf-8");
2312
3640
  } else if (stat.isDirectory()) {
2313
- const files = (0, import_node_fs9.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
2314
- content = files.map((f) => (0, import_node_fs9.readFileSync)((0, import_node_path9.join)(p, f), "utf-8")).join("\n");
3641
+ const files = (0, import_node_fs11.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
3642
+ content = files.map((f) => (0, import_node_fs11.readFileSync)((0, import_node_path10.join)(p, f), "utf-8")).join("\n");
2315
3643
  }
2316
3644
  } catch {
2317
3645
  continue;
@@ -2385,7 +3713,7 @@ function extractStringArrayFromNode(node) {
2385
3713
  return values;
2386
3714
  }
2387
3715
  function findArrayDecl(root, varName) {
2388
- function walk5(node) {
3716
+ function walk2(node) {
2389
3717
  if (node.type === "variable_declarator") {
2390
3718
  const nameNode = node.childForFieldName("name");
2391
3719
  const valueNode = node.childForFieldName("value");
@@ -2398,12 +3726,12 @@ function findArrayDecl(root, varName) {
2398
3726
  }
2399
3727
  }
2400
3728
  for (const child of node.namedChildren) {
2401
- const found = walk5(child);
3729
+ const found = walk2(child);
2402
3730
  if (found) return found;
2403
3731
  }
2404
3732
  return null;
2405
3733
  }
2406
- return walk5(root);
3734
+ return walk2(root);
2407
3735
  }
2408
3736
  function extractObjectPropsRegex(objStr) {
2409
3737
  const props = {};
@@ -2466,15 +3794,30 @@ function modelToNodeType(model) {
2466
3794
  function extractSeedData(rootDir) {
2467
3795
  const nodes = [];
2468
3796
  const edges = [];
2469
- const seedFiles = [
2470
- (0, import_node_path9.join)(rootDir, "prisma", "seed.ts"),
2471
- (0, import_node_path9.join)(rootDir, "prisma", "seed.js"),
2472
- (0, import_node_path9.join)(rootDir, "src", "server", "lib", "system-tags.ts")
2473
- ].filter(import_node_fs9.existsSync);
3797
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3798
+ const candidates = [];
3799
+ if (paths?.dbDir) {
3800
+ candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.ts"));
3801
+ candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.js"));
3802
+ } else {
3803
+ candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
3804
+ candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.js"));
3805
+ }
3806
+ const baseRoots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
3807
+ for (const root of baseRoots) {
3808
+ candidates.push((0, import_node_path10.join)(root, "server", "lib", "system-tags.ts"));
3809
+ }
3810
+ const seedFiles = candidates.filter((p) => {
3811
+ try {
3812
+ return (0, import_node_fs11.existsSync)(p) && (0, import_node_fs11.statSync)(p).isFile();
3813
+ } catch {
3814
+ return false;
3815
+ }
3816
+ });
2474
3817
  const useTreeSitter = tryLoadTreeSitter();
2475
3818
  for (const filePath of seedFiles) {
2476
- const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
2477
- const relPath = (0, import_node_path9.relative)(rootDir, filePath);
3819
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3820
+ const relPath = (0, import_node_path10.relative)(rootDir, filePath);
2478
3821
  const seeded = detectSeededArrays(content, relPath);
2479
3822
  let astRoot = null;
2480
3823
  if (useTreeSitter && parseCode) {
@@ -2568,11 +3911,11 @@ function extractSeedData(rootDir) {
2568
3911
  return { nodes, edges };
2569
3912
  }
2570
3913
  function walkDir(dir, exts) {
2571
- if (!(0, import_node_fs9.existsSync)(dir)) return [];
3914
+ if (!(0, import_node_fs11.existsSync)(dir)) return [];
2572
3915
  const results = [];
2573
- for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
3916
+ for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
2574
3917
  if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2575
- const full = (0, import_node_path9.join)(dir, entry.name);
3918
+ const full = (0, import_node_path10.join)(dir, entry.name);
2576
3919
  if (entry.isDirectory()) results.push(...walkDir(full, exts));
2577
3920
  else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
2578
3921
  }
@@ -2580,11 +3923,22 @@ function walkDir(dir, exts) {
2580
3923
  }
2581
3924
  function extractConstants(rootDir) {
2582
3925
  const nodes = [];
2583
- const srcDir = (0, import_node_path9.join)(rootDir, "src");
2584
- if (!(0, import_node_fs9.existsSync)(srcDir)) return { nodes };
2585
- for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
2586
- const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
2587
- const relPath = (0, import_node_path9.relative)(rootDir, filePath);
3926
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3927
+ const roots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
3928
+ const seenFile = /* @__PURE__ */ new Set();
3929
+ const allFiles = [];
3930
+ for (const root of roots) {
3931
+ for (const f of walkDir(root, [".ts", ".tsx"])) {
3932
+ if (!seenFile.has(f)) {
3933
+ seenFile.add(f);
3934
+ allFiles.push(f);
3935
+ }
3936
+ }
3937
+ }
3938
+ if (allFiles.length === 0) return { nodes };
3939
+ for (const filePath of allFiles) {
3940
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3941
+ const relPath = (0, import_node_path10.relative)(rootDir, filePath);
2588
3942
  const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
2589
3943
  let cm;
2590
3944
  while ((cm = constArrayRe.exec(content)) !== null) {
@@ -2617,7 +3971,14 @@ function extractConstants(rootDir) {
2617
3971
  return { nodes };
2618
3972
  }
2619
3973
  function detect4(rootDir) {
2620
- return (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
3974
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3975
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs11.existsSync)(paths.dbConfig.schemaPath)) {
3976
+ return true;
3977
+ }
3978
+ if (paths?.dbDir) {
3979
+ if ((0, import_node_fs11.existsSync)((0, import_node_path10.join)(paths.dbDir, "seed.ts")) || (0, import_node_fs11.existsSync)((0, import_node_path10.join)(paths.dbDir, "seed.js"))) return true;
3980
+ }
3981
+ return (0, import_node_fs11.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs11.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
2621
3982
  }
2622
3983
  function generate4(rootDir) {
2623
3984
  const enumResult = extractEnumValues(rootDir);
@@ -2692,26 +4053,8 @@ var staticValuesParser = {
2692
4053
  };
2693
4054
 
2694
4055
  // src/server/graph/parsers/crosslayer/static-ref-scanner.ts
2695
- var import_node_fs10 = require("node:fs");
2696
- var import_node_path10 = require("node:path");
2697
- init_config();
2698
- function walk4(dir, exts) {
2699
- if (!(0, import_node_fs10.existsSync)(dir)) return [];
2700
- const results = [];
2701
- function recurse(d) {
2702
- for (const entry of (0, import_node_fs10.readdirSync)(d, { withFileTypes: true })) {
2703
- const full = (0, import_node_path10.join)(d, entry.name);
2704
- if (entry.isDirectory()) {
2705
- if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2706
- recurse(full);
2707
- } else if (exts.some((ext) => entry.name.endsWith(ext))) {
2708
- results.push(full);
2709
- }
2710
- }
2711
- }
2712
- recurse(dir);
2713
- return results;
2714
- }
4056
+ var import_node_fs12 = require("node:fs");
4057
+ var import_node_path11 = require("node:path");
2715
4058
  var MIN_VALUE_LENGTH = 4;
2716
4059
  var SKIP_VALUES = /* @__PURE__ */ new Set([
2717
4060
  "true",
@@ -2827,7 +4170,7 @@ var staticRefScannerParser = {
2827
4170
  },
2828
4171
  generate(rootDir, layerOutputs) {
2829
4172
  const staticOutput = layerOutputs.get("static");
2830
- if (!staticOutput || staticOutput.nodes.length === 0) {
4173
+ if (!staticOutput || !Array.isArray(staticOutput.nodes) || staticOutput.nodes.length === 0) {
2831
4174
  return { cross_refs: [], flagged_edges: [], warnings: [] };
2832
4175
  }
2833
4176
  const valueLookup = /* @__PURE__ */ new Map();
@@ -2853,13 +4196,22 @@ var staticRefScannerParser = {
2853
4196
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2854
4197
  if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
2855
4198
  const srcDir = paths.srcDir;
2856
- const files = [
2857
- ...walk4((0, import_node_path10.join)(srcDir, "client"), [".ts", ".tsx"]),
2858
- ...walk4(paths.appDir, [".ts", ".tsx"]),
2859
- ...walk4((0, import_node_path10.join)(srcDir, "server"), [".ts", ".tsx"]),
2860
- ...walk4((0, import_node_path10.join)(srcDir, "lib"), [".ts", ".tsx"]),
2861
- ...walk4((0, import_node_path10.join)(srcDir, "config"), [".ts", ".tsx"])
2862
- ];
4199
+ const seenFile = /* @__PURE__ */ new Set();
4200
+ const files = [];
4201
+ for (const root of paths.srcRoots) {
4202
+ for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
4203
+ if (!seenFile.has(f)) {
4204
+ seenFile.add(f);
4205
+ files.push(f);
4206
+ }
4207
+ }
4208
+ }
4209
+ for (const conv of paths.conventionFiles) {
4210
+ if (!seenFile.has(conv)) {
4211
+ seenFile.add(conv);
4212
+ files.push(conv);
4213
+ }
4214
+ }
2863
4215
  const uiOutput = layerOutputs.get("ui");
2864
4216
  const apiOutput = layerOutputs.get("api");
2865
4217
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
@@ -2876,10 +4228,11 @@ var staticRefScannerParser = {
2876
4228
  const seen = /* @__PURE__ */ new Set();
2877
4229
  let filesScanned = 0;
2878
4230
  for (const absPath of files) {
2879
- const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
4231
+ const relFromSrc = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
4232
+ const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path11.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
2880
4233
  const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
2881
4234
  if (!sourceLayer) continue;
2882
- const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
4235
+ const content = (0, import_node_fs12.readFileSync)(absPath, "utf-8");
2883
4236
  filesScanned++;
2884
4237
  let fileRefs;
2885
4238
  if (parseCode2) {
@@ -2920,6 +4273,111 @@ var staticRefScannerParser = {
2920
4273
  }
2921
4274
  };
2922
4275
 
4276
+ // src/server/graph/parsers/crosslayer/middleware-gates.ts
4277
+ var import_node_path12 = require("node:path");
4278
+ init_ts_extractor();
4279
+ function toNodeId4(srcDir, rootDir, absPath) {
4280
+ const relFromSrc = (0, import_node_path12.relative)(srcDir, absPath).replace(/\\/g, "/");
4281
+ if (relFromSrc.startsWith("..")) {
4282
+ return (0, import_node_path12.relative)(rootDir, absPath).replace(/\\/g, "/");
4283
+ }
4284
+ return relFromSrc;
4285
+ }
4286
+ function collectTargets(apiOutput, uiOutput) {
4287
+ const out = [];
4288
+ for (const n of apiOutput?.nodes ?? []) {
4289
+ if (n.type !== "endpoint") continue;
4290
+ const path = n.path;
4291
+ if (typeof path !== "string" || !path) continue;
4292
+ out.push({ id: n.id, route: path, layer: "api" });
4293
+ }
4294
+ for (const n of uiOutput?.nodes ?? []) {
4295
+ if (n.type !== "page") continue;
4296
+ const route = n.route;
4297
+ if (typeof route !== "string" || !route) continue;
4298
+ out.push({ id: n.id, route, layer: "ui" });
4299
+ }
4300
+ return out;
4301
+ }
4302
+ var middlewareGatesParser = {
4303
+ id: "middleware-gates",
4304
+ layer: "crosslayer",
4305
+ concern: "auth-gate",
4306
+ detect(rootDir) {
4307
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
4308
+ if (!paths) return false;
4309
+ return paths.conventionFiles.some((f) => /middleware\.tsx?$/.test(f));
4310
+ },
4311
+ generate(rootDir, layerOutputs) {
4312
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
4313
+ if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
4314
+ const middlewareFiles = paths.conventionFiles.filter((f) => /middleware\.tsx?$/.test(f));
4315
+ if (middlewareFiles.length === 0) {
4316
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
4317
+ }
4318
+ const apiOutput = layerOutputs.get("api");
4319
+ const uiOutput = layerOutputs.get("ui");
4320
+ const targets = collectTargets(apiOutput, uiOutput);
4321
+ const crossRefs = [];
4322
+ const flagged = [];
4323
+ const seenEdge = /* @__PURE__ */ new Set();
4324
+ const seenUnparseable = /* @__PURE__ */ new Set();
4325
+ let protectMatcherCount = 0;
4326
+ for (const file of middlewareFiles) {
4327
+ let info = null;
4328
+ try {
4329
+ info = extractMiddlewareAuthTS(file);
4330
+ } catch {
4331
+ }
4332
+ if (!info || info.matchers.length === 0) continue;
4333
+ const middlewareId = toNodeId4(paths.srcDir, rootDir, file);
4334
+ for (const matcher of info.matchers) {
4335
+ if (matcher.intent !== "protect") continue;
4336
+ protectMatcherCount += 1;
4337
+ for (const pattern of matcher.patterns) {
4338
+ const re = middlewarePatternToRegex(pattern);
4339
+ if (!re) {
4340
+ const key = `${middlewareId}|${pattern}`;
4341
+ if (seenUnparseable.has(key)) continue;
4342
+ seenUnparseable.add(key);
4343
+ flagged.push({
4344
+ source: middlewareId,
4345
+ target: "UNRESOLVED",
4346
+ type: "middleware_pattern_unparseable",
4347
+ label: `pattern "${pattern}" in matcher "${matcher.name}" \u2014 coverage unknown`,
4348
+ confidence: "high"
4349
+ });
4350
+ continue;
4351
+ }
4352
+ for (const target of targets) {
4353
+ if (!re.test(target.route)) continue;
4354
+ const key = `${middlewareId}|${target.id}|protects`;
4355
+ if (seenEdge.has(key)) continue;
4356
+ seenEdge.add(key);
4357
+ crossRefs.push({
4358
+ source: middlewareId,
4359
+ target: target.id,
4360
+ type: "protects",
4361
+ layer: target.layer
4362
+ });
4363
+ }
4364
+ }
4365
+ }
4366
+ }
4367
+ return {
4368
+ cross_refs: crossRefs,
4369
+ flagged_edges: flagged,
4370
+ warnings: [],
4371
+ patterns: {
4372
+ middleware_files: middlewareFiles.length,
4373
+ protect_matchers: protectMatcherCount,
4374
+ protects_edges: crossRefs.length,
4375
+ unparseable_patterns: flagged.length
4376
+ }
4377
+ };
4378
+ }
4379
+ };
4380
+
2923
4381
  // src/server/graph/core/parser-registry.ts
2924
4382
  function isMultiLayerParser(p) {
2925
4383
  return "layers" in p && Array.isArray(p.layers);
@@ -2985,7 +4443,8 @@ function registerBuiltins(registry, disabled) {
2985
4443
  fetchResolverParser,
2986
4444
  apiAnnotationsParser,
2987
4445
  urlLiteralScannerParser,
2988
- staticRefScannerParser
4446
+ staticRefScannerParser,
4447
+ middlewareGatesParser
2989
4448
  ];
2990
4449
  for (const parser of builtins) {
2991
4450
  if (disabled.has(parser.id)) continue;
@@ -2995,7 +4454,7 @@ function registerBuiltins(registry, disabled) {
2995
4454
  function loadCustomParsers(registry, config, rootDir, disabled) {
2996
4455
  for (const entry of config.parsers?.custom ?? []) {
2997
4456
  try {
2998
- const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
4457
+ const absPath = (0, import_node_path13.resolve)(rootDir, entry.path);
2999
4458
  const mod = require(absPath);
3000
4459
  const parser = "default" in mod ? mod.default : mod;
3001
4460
  if (disabled.has(parser.id)) continue;
@@ -3125,10 +4584,10 @@ function applyCrossLayerResults(uiOutput, results) {
3125
4584
 
3126
4585
  // src/server/graph/core/graph-builder.ts
3127
4586
  function readGraphFromDisk(rootDir, layer) {
3128
- const filePath = (0, import_node_path12.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3129
- if (!(0, import_node_fs11.existsSync)(filePath)) return null;
4587
+ const filePath = (0, import_node_path14.join)(rootDir, LAUNCHSECURE_DIR, "graphs", `${layer}.json`);
4588
+ if (!(0, import_node_fs13.existsSync)(filePath)) return null;
3130
4589
  try {
3131
- return JSON.parse((0, import_node_fs11.readFileSync)(filePath, "utf-8"));
4590
+ return JSON.parse((0, import_node_fs13.readFileSync)(filePath, "utf-8"));
3132
4591
  } catch {
3133
4592
  return null;
3134
4593
  }
@@ -3225,1419 +4684,38 @@ function generateAll(rootDir) {
3225
4684
  return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
3226
4685
  }
3227
4686
 
3228
- // src/server/graph/index.ts
3229
- init_config();
3230
-
3231
- // src/server/graph/core/tagger-registry.ts
3232
- var import_node_path14 = require("node:path");
3233
-
3234
- // src/server/graph/taggers/module-tagger.ts
3235
- var import_node_fs12 = require("node:fs");
3236
- var import_node_path13 = require("node:path");
3237
- function matchGlob(pattern, id) {
3238
- const patParts = pattern.split("/");
3239
- const idParts = id.split("/");
3240
- return matchParts(patParts, 0, idParts, 0);
3241
- }
3242
- function matchParts(pat, pi, id, ii) {
3243
- while (pi < pat.length && ii < id.length) {
3244
- const p = pat[pi];
3245
- if (p === "**") {
3246
- for (let skip = ii; skip <= id.length; skip++) {
3247
- if (matchParts(pat, pi + 1, id, skip)) return true;
3248
- }
3249
- return false;
3250
- }
3251
- if (p === "*") {
3252
- pi++;
3253
- ii++;
3254
- continue;
3255
- }
3256
- if (p !== id[ii]) return false;
3257
- pi++;
3258
- ii++;
3259
- }
3260
- while (pi < pat.length && pat[pi] === "**") pi++;
3261
- return pi === pat.length && ii === id.length;
3262
- }
3263
- var CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
3264
- function detectConventionDirs(rootDir, extraConventionDirs = []) {
3265
- const result = /* @__PURE__ */ new Map();
3266
- const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
3267
- const searchDirs = [
3268
- rootDir,
3269
- (0, import_node_path13.join)(rootDir, "src"),
3270
- (0, import_node_path13.join)(rootDir, "app"),
3271
- (0, import_node_path13.join)(rootDir, "lib")
3272
- ];
3273
- for (const base of searchDirs) {
3274
- for (const convention of conventionDirs) {
3275
- const dir = (0, import_node_path13.join)(base, convention);
3276
- if (!(0, import_node_fs12.existsSync)(dir)) continue;
3277
- try {
3278
- const stat = (0, import_node_fs12.statSync)(dir);
3279
- if (!stat.isDirectory()) continue;
3280
- const entries = (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
3281
- if (entries.length > 0) {
3282
- const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
3283
- result.set(relPath, entries);
3284
- }
3285
- } catch {
3286
- }
3287
- }
3288
- }
3289
- return result;
3290
- }
3291
- function extractRouteGroups(id) {
3292
- const groups = [];
3293
- const re = /\(([^)]+)\)/g;
3294
- let m;
3295
- while ((m = re.exec(id)) !== null) {
3296
- groups.push(m[1]);
3297
- }
3298
- return groups;
3299
- }
3300
- var GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
3301
- // JS/TS
3302
- "components",
3303
- "hooks",
3304
- "pages",
3305
- "views",
3306
- "screens",
3307
- "layouts",
3308
- "utils",
3309
- "helpers",
3310
- "lib",
3311
- "libs",
3312
- "services",
3313
- "api",
3314
- "apis",
3315
- "stores",
3316
- "state",
3317
- "store",
3318
- "context",
3319
- "contexts",
3320
- "providers",
3321
- "types",
3322
- "interfaces",
3323
- "models",
3324
- "schemas",
3325
- "constants",
3326
- "config",
3327
- "configs",
3328
- "assets",
3329
- "styles",
3330
- "public",
3331
- "middleware",
3332
- "middlewares",
3333
- "routes",
3334
- "router",
3335
- "tests",
3336
- "test",
3337
- "__tests__",
3338
- "spec",
3339
- "specs",
3340
- // Go
3341
- "cmd",
3342
- "pkg",
3343
- "internal",
3344
- // Python
3345
- "management",
3346
- "migrations",
3347
- "templatetags",
3348
- "templates",
3349
- // Java
3350
- "controller",
3351
- "controllers",
3352
- "service",
3353
- "repository",
3354
- "repositories",
3355
- "entity",
3356
- "entities",
3357
- "dto",
3358
- "dtos",
3359
- // General
3360
- "shared",
3361
- "common",
3362
- "core",
3363
- "base",
3364
- "app",
3365
- // Next.js specific
3366
- "client",
3367
- "server"
3368
- ]);
3369
- var SKIP_SEGMENTS_BUILTIN = /* @__PURE__ */ new Set([
3370
- "src",
3371
- "app",
3372
- "client",
3373
- "server",
3374
- "lib",
3375
- "config"
3376
- ]);
3377
- function isRouteGroup(segment) {
3378
- return segment.startsWith("(") && segment.endsWith(")");
3379
- }
3380
- function isDynamicSegment(segment) {
3381
- return segment.startsWith("[") || segment.startsWith(":");
3382
- }
3383
- function isDomainDir(segment) {
3384
- return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
3385
- }
3386
- var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
3387
- // Generic app wrappers
3388
- "app",
3389
- "all",
3390
- "ee",
3391
- "home",
3392
- "root",
3393
- "main",
3394
- "site",
3395
- // Auth/access boundary wrappers — protect routes, not feature modules
3396
- "protected",
3397
- "authenticated",
3398
- "authed",
3399
- "private",
3400
- "public",
3401
- "logged-in",
3402
- "logged-out",
3403
- "unprotected",
3404
- "unauthenticated",
3405
- "auth-required",
3406
- "no-auth",
3407
- "guest-only"
3408
- ]);
3409
- function isTrivialGroup(name, extraTrivial) {
3410
- const lower = name.toLowerCase();
3411
- if (TRIVIAL_GROUPS.has(lower)) return true;
3412
- if (extraTrivial && [...extraTrivial].some((t) => t.toLowerCase() === lower)) return true;
3413
- const wrapperPatterns = [
3414
- /^.*-?wrapper$/,
3415
- // "page-wrapper", "use-page-wrapper"
3416
- /^.*-?layout$/,
3417
- // "admin-layout", "settings-layout"
3418
- /^use-/,
3419
- // "use-page-wrapper"
3420
- /^default$/
3421
- ];
3422
- return wrapperPatterns.some((p) => p.test(lower));
3423
- }
3424
- function normalizeGroupName(name) {
3425
- return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
3426
- }
3427
- function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
3428
- const segments = id.split("/");
3429
- const routeGroups = extractRouteGroups(id);
3430
- const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
3431
- if (extraSkipSegments) {
3432
- for (const s of extraSkipSegments) skipSegments.add(s);
3433
- }
3434
- const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
3435
- if (moduleGroups.length > 0) {
3436
- return moduleGroups[moduleGroups.length - 1];
3437
- }
3438
- const meaningful = [];
3439
- for (const seg of segments) {
3440
- if (seg.includes(".")) continue;
3441
- if (isRouteGroup(seg)) continue;
3442
- if (isDynamicSegment(seg)) continue;
3443
- if (isDomainDir(seg)) continue;
3444
- if (skipSegments.has(seg)) continue;
3445
- meaningful.push(seg);
3446
- }
3447
- if (meaningful.length > 0) {
3448
- return meaningful[0];
3449
- }
3450
- return "root";
3451
- }
3452
- var cachedRootDir = null;
3453
- var cachedConventionDirs = /* @__PURE__ */ new Map();
3454
- var moduleTagger = {
3455
- id: "module",
3456
- tagKey: "module",
3457
- trackUntagged: true,
3458
- layers: null,
3459
- // applies to all layers
3460
- tag(nodes, layer, rootDir) {
3461
- let configRules = [];
3462
- let extraTrivial;
3463
- let extraSkipSegments;
3464
- let extraConventionDirs = [];
3465
- try {
3466
- const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
3467
- const config = loadConfig2(rootDir);
3468
- configRules = config.taggers?.module?.rules ?? [];
3469
- const trivialFromConfig = config.taggers?.module?.trivialGroups;
3470
- if (trivialFromConfig?.length) {
3471
- extraTrivial = new Set(trivialFromConfig);
3472
- }
3473
- const skipFromConfig = config.taggers?.module?.skipSegments;
3474
- if (skipFromConfig?.length) {
3475
- extraSkipSegments = new Set(skipFromConfig);
3476
- }
3477
- extraConventionDirs = config.taggers?.module?.conventionDirs ?? [];
3478
- const roleNamesFromConfig = config.taggers?.module?.genericRoleNames;
3479
- if (roleNamesFromConfig?.length) {
3480
- for (const name of roleNamesFromConfig) GENERIC_ROLE_NAMES_BUILTIN.add(name);
3481
- }
3482
- } catch {
3483
- }
3484
- if (cachedRootDir !== rootDir) {
3485
- cachedConventionDirs = detectConventionDirs(rootDir, extraConventionDirs);
3486
- cachedRootDir = rootDir;
3487
- }
3488
- const result = /* @__PURE__ */ new Map();
3489
- for (const node of nodes) {
3490
- const id = node.id;
3491
- let matched = false;
3492
- for (const rule of configRules) {
3493
- if (matchGlob(rule.match, id)) {
3494
- result.set(id, rule.module);
3495
- matched = true;
3496
- break;
3497
- }
3498
- }
3499
- if (matched) continue;
3500
- matched = false;
3501
- for (const [convDir, moduleNames] of cachedConventionDirs) {
3502
- if (id.startsWith(convDir + "/")) {
3503
- const rest = id.slice(convDir.length + 1);
3504
- const firstSeg = rest.split("/")[0];
3505
- if (moduleNames.includes(firstSeg)) {
3506
- result.set(id, firstSeg);
3507
- matched = true;
3508
- break;
3509
- }
3510
- }
3511
- }
3512
- if (matched) continue;
3513
- const module2 = extractModuleFromPath(id, extraTrivial, extraSkipSegments);
3514
- result.set(id, module2);
3515
- }
3516
- return result;
3517
- }
3518
- };
3519
-
3520
- // src/server/graph/taggers/screen-tagger.ts
3521
- var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
3522
- var screenTagger = {
3523
- id: "screen",
3524
- tagKey: "screen",
3525
- trackUntagged: true,
3526
- layers: ["ui"],
3527
- tag(nodes, layer) {
3528
- if (layer !== "ui") return /* @__PURE__ */ new Map();
3529
- const result = /* @__PURE__ */ new Map();
3530
- for (const node of nodes) {
3531
- if (SCREEN_TYPES.has(node.type)) {
3532
- result.set(node.id, "true");
3533
- }
3534
- }
3535
- return result;
3536
- }
3537
- };
3538
-
3539
- // src/server/graph/core/tagger-registry.ts
3540
- var TaggerRegistry = class {
3541
- constructor() {
3542
- this.taggers = [];
3543
- this.ids = /* @__PURE__ */ new Set();
3544
- }
3545
- register(tagger) {
3546
- if (this.ids.has(tagger.id)) {
3547
- throw new Error(`Duplicate tagger id: ${tagger.id}`);
3548
- }
3549
- this.ids.add(tagger.id);
3550
- this.taggers.push(tagger);
3551
- }
3552
- getAll() {
3553
- return this.taggers;
3554
- }
3555
- getForLayer(layer) {
3556
- return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
3557
- }
3558
- };
3559
- var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
3560
- function registerBuiltins2(registry, disabled, config) {
3561
- for (const tagger of BUILTIN_TAGGERS) {
3562
- if (disabled.has(tagger.id)) continue;
3563
- const override = config.taggers?.trackUntagged?.[tagger.id];
3564
- if (override !== void 0) {
3565
- tagger.trackUntagged = override;
3566
- }
3567
- registry.register(tagger);
3568
- }
3569
- }
3570
- function loadCustomTaggers(registry, config, rootDir, disabled) {
3571
- for (const entry of config.taggers?.custom ?? []) {
3572
- if (disabled.has(entry.id)) continue;
3573
- try {
3574
- const absPath = (0, import_node_path14.resolve)(rootDir, entry.path);
3575
- const mod = require(absPath);
3576
- const tagger = "default" in mod ? mod.default : mod;
3577
- const override = config.taggers?.trackUntagged?.[tagger.id];
3578
- if (override !== void 0) {
3579
- tagger.trackUntagged = override;
3580
- }
3581
- registry.register(tagger);
3582
- } catch (err) {
3583
- process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err}
3584
- `);
3585
- }
3586
- }
3587
- }
3588
- function createTaggerRegistry(config, rootDir) {
3589
- const registry = new TaggerRegistry();
3590
- const disabled = new Set(config.taggers?.disabled ?? []);
3591
- registerBuiltins2(registry, disabled, config);
3592
- loadCustomTaggers(registry, config, rootDir, disabled);
3593
- return registry;
3594
- }
3595
-
3596
- // src/server/graph/core/tag-store.ts
3597
- var import_node_fs13 = require("node:fs");
3598
- var import_node_path15 = require("node:path");
3599
- var TAGS_FILENAME = "tags.json";
3600
- var GRAPHS_DIR = ".launchsecure/graphs";
3601
- var tagCache = /* @__PURE__ */ new Map();
3602
- function tagsFilePath(rootDir) {
3603
- return (0, import_node_path15.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
3604
- }
3605
- function readTagStore(rootDir) {
3606
- const filePath = tagsFilePath(rootDir);
3607
- if (!(0, import_node_fs13.existsSync)(filePath)) return {};
3608
- const stat = (0, import_node_fs13.statSync)(filePath);
3609
- const cached = tagCache.get(filePath);
3610
- if (cached && cached.mtimeMs === stat.mtimeMs) {
3611
- return cached.store;
3612
- }
3613
- try {
3614
- const content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
3615
- const store = JSON.parse(content);
3616
- tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
3617
- return store;
3618
- } catch {
3619
- return {};
3620
- }
3621
- }
3622
- function writeTagStore(rootDir, store) {
3623
- const filePath = tagsFilePath(rootDir);
3624
- const dir = (0, import_node_path15.dirname)(filePath);
3625
- (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
3626
- const cleaned = {};
3627
- for (const [nodeId, tags] of Object.entries(store)) {
3628
- if (Object.keys(tags).length > 0) {
3629
- cleaned[nodeId] = tags;
3630
- }
3631
- }
3632
- (0, import_node_fs13.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
3633
- tagCache.delete(filePath);
3634
- }
3635
- function setTag(rootDir, nodeId, key, value) {
3636
- const store = readTagStore(rootDir);
3637
- if (!store[nodeId]) store[nodeId] = {};
3638
- store[nodeId][key] = value;
3639
- writeTagStore(rootDir, store);
3640
- }
3641
- function removeTag(rootDir, nodeId, key) {
3642
- const store = readTagStore(rootDir);
3643
- if (!store[nodeId]) return;
3644
- delete store[nodeId][key];
3645
- if (Object.keys(store[nodeId]).length === 0) {
3646
- delete store[nodeId];
3647
- }
3648
- writeTagStore(rootDir, store);
3649
- }
3650
-
3651
- // src/server/graph/index.ts
4687
+ // src/server/parse-worker-entry.ts
4688
+ init_parse_failure_cache();
3652
4689
  init_ts_extractor();
3653
- var GRAPHS_DIR2 = ".launchsecure/graphs";
3654
- function getAvailableLayers(rootDir) {
3655
- const dir = (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
3656
- if (!(0, import_node_fs14.existsSync)(dir)) return [];
3657
- return (0, import_node_fs14.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
3658
- }
3659
- var graphCache = /* @__PURE__ */ new Map();
3660
- var taggedCache = /* @__PURE__ */ new Map();
3661
- function graphsDir(rootDir) {
3662
- return (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
3663
- }
3664
- function graphFilePath(rootDir, layer) {
3665
- return (0, import_node_path16.join)(graphsDir(rootDir), `${layer}.json`);
3666
- }
3667
- function tagsFilePath2(rootDir) {
3668
- return (0, import_node_path16.join)(graphsDir(rootDir), "tags.json");
3669
- }
3670
- function getMtimeMs(filePath) {
3671
- if (!(0, import_node_fs14.existsSync)(filePath)) return 0;
3672
- return (0, import_node_fs14.statSync)(filePath).mtimeMs;
3673
- }
3674
- function invalidateCache(filePath) {
3675
- graphCache.delete(filePath);
3676
- }
3677
- function invalidateTaggedCache(rootDir, layer) {
3678
- taggedCache.delete(`${rootDir}:${layer}`);
3679
- }
3680
- function applyTags(graph, layer, rootDir) {
3681
- const config = loadConfig(rootDir);
3682
- const registry = createTaggerRegistry(config, rootDir);
3683
- const manualTags = readTagStore(rootDir);
3684
- const taggedNodes = graph.nodes.map((n) => ({ ...n }));
3685
- const taggers = registry.getForLayer(layer);
3686
- for (const tagger of taggers) {
3687
- const assignments = tagger.tag(taggedNodes, layer, rootDir);
3688
- for (const node of taggedNodes) {
3689
- if (!node.tags) node.tags = {};
3690
- const tags = node.tags;
3691
- const value = assignments.get(node.id);
3692
- if (value !== void 0) {
3693
- tags[tagger.tagKey] = value;
3694
- } else if (tagger.trackUntagged) {
3695
- tags[tagger.tagKey] = "untagged";
3696
- }
3697
- }
3698
- }
3699
- for (const node of taggedNodes) {
3700
- const manual = manualTags[node.id];
3701
- if (manual) {
3702
- if (!node.tags) node.tags = {};
3703
- const tags = node.tags;
3704
- Object.assign(tags, manual);
3705
- }
3706
- }
3707
- return { ...graph, nodes: taggedNodes };
3708
- }
3709
- function readGraphRaw(rootDir, layer) {
3710
- const filePath = graphFilePath(rootDir, layer);
3711
- if (!(0, import_node_fs14.existsSync)(filePath)) return null;
3712
- const stat = (0, import_node_fs14.statSync)(filePath);
3713
- const cached = graphCache.get(filePath);
3714
- if (cached && cached.mtimeMs === stat.mtimeMs) {
3715
- return cached.graph;
3716
- }
3717
- const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
3718
- const graph = JSON.parse(content);
3719
- graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
3720
- return graph;
3721
- }
3722
- function readGraph(rootDir, layer) {
3723
- const rawFilePath = graphFilePath(rootDir, layer);
3724
- if (!(0, import_node_fs14.existsSync)(rawFilePath)) return null;
3725
- const rawMtime = getMtimeMs(rawFilePath);
3726
- const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
3727
- const cacheKey = `${rootDir}:${layer}`;
3728
- const cached = taggedCache.get(cacheKey);
3729
- if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
3730
- return cached.graph;
3731
- }
3732
- const raw = readGraphRaw(rootDir, layer);
3733
- if (!raw) return null;
3734
- const tagged = applyTags(raw, layer, rootDir);
3735
- taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
3736
- return tagged;
3737
- }
3738
- function readAllGraphs(rootDir) {
3739
- const result = {};
3740
- for (const layer of getAvailableLayers(rootDir)) {
3741
- const graph = readGraph(rootDir, layer);
3742
- if (graph) result[layer] = graph;
3743
- }
3744
- return result;
3745
- }
3746
- async function generateGraph(rootDir, layer) {
3747
- await initTreeSitter();
3748
- const config = loadConfig(rootDir);
3749
- setExtractorConfig({
3750
- dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
3751
- mutationMethods: config.parsers?.patterns?.mutationMethods
3752
- });
3753
- const dir = graphsDir(rootDir);
3754
- (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
3755
- const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
3756
- for (const result of results) {
3757
- const filePath = graphFilePath(rootDir, result.layer);
3758
- (0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
3759
- invalidateCache(filePath);
3760
- invalidateTaggedCache(rootDir, result.layer);
3761
- }
3762
- return results;
3763
- }
3764
-
3765
- // src/server/lockfile.ts
3766
- var import_node_child_process = require("node:child_process");
3767
- var import_node_fs15 = require("node:fs");
3768
- var import_node_os = require("node:os");
3769
- var import_node_path17 = require("node:path");
3770
- function lockDir(projectRoot) {
3771
- if (projectRoot) {
3772
- return (0, import_node_path17.join)(projectRoot, ".launchsecure");
3773
- }
3774
- return (0, import_node_path17.join)((0, import_node_os.homedir)(), ".launchsecure");
3775
- }
3776
- function lockPath(projectRoot) {
3777
- return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
3778
- }
3779
- var _activeProjectRoot;
3780
- function readLock(projectRoot) {
3781
- const root = projectRoot ?? _activeProjectRoot;
3782
- const p = lockPath(root);
3783
- if (!(0, import_node_fs15.existsSync)(p)) {
3784
- if (root) {
3785
- const globalP = lockPath();
3786
- if ((0, import_node_fs15.existsSync)(globalP)) {
3787
- try {
3788
- const data = JSON.parse((0, import_node_fs15.readFileSync)(globalP, "utf-8"));
3789
- if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
3790
- return data;
3791
- }
3792
- } catch {
3793
- }
3794
- }
3795
- }
3796
- return null;
3797
- }
3798
- try {
3799
- const data = JSON.parse((0, import_node_fs15.readFileSync)(p, "utf-8"));
3800
- if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
3801
- return data;
3802
- } catch {
3803
- return null;
3804
- }
4690
+ if (!import_node_worker_threads.parentPort) {
4691
+ throw new Error("parse-worker-entry must be spawned as a worker thread");
3805
4692
  }
3806
- function isPidAlive(pid) {
4693
+ async function run(req) {
3807
4694
  try {
3808
- process.kill(pid, 0);
3809
- return true;
3810
- } catch {
3811
- return false;
3812
- }
3813
- }
3814
- function getListenerPid(port) {
3815
- try {
3816
- const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
3817
- encoding: "utf-8",
3818
- stdio: ["ignore", "pipe", "ignore"],
3819
- timeout: 500
3820
- }).trim();
3821
- if (!out) return null;
3822
- const pid = parseInt(out.split("\n")[0], 10);
3823
- return Number.isFinite(pid) ? pid : null;
3824
- } catch {
3825
- return null;
3826
- }
3827
- }
3828
- function getLiveLock(projectRoot) {
3829
- const root = projectRoot ?? _activeProjectRoot;
3830
- const lock = readLock(root);
3831
- if (!lock) return null;
3832
- const listenerPid = getListenerPid(lock.port);
3833
- const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
3834
- if (!live) {
3835
- try {
3836
- (0, import_node_fs15.unlinkSync)(lockPath(root));
3837
- } catch {
3838
- }
3839
- return null;
3840
- }
3841
- return lock;
3842
- }
3843
- function writeLock(data, projectRoot) {
3844
- const root = projectRoot ?? _activeProjectRoot;
3845
- (0, import_node_fs15.mkdirSync)(lockDir(root), { recursive: true });
3846
- (0, import_node_fs15.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
3847
- if (root) _activeProjectRoot = root;
3848
- }
3849
- function clearLock(projectRoot) {
3850
- const root = projectRoot ?? _activeProjectRoot;
3851
- try {
3852
- (0, import_node_fs15.unlinkSync)(lockPath(root));
3853
- } catch {
3854
- }
3855
- }
3856
-
3857
- // src/server/chart-serve.ts
3858
- init_config();
3859
-
3860
- // src/server/graph/core/audit-core.ts
3861
- var import_node_fs16 = require("node:fs");
3862
- var import_node_path18 = require("node:path");
3863
- function readGraphFile(rootDir, layer) {
3864
- const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3865
- if (!(0, import_node_fs16.existsSync)(filePath)) return null;
3866
- try {
3867
- return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
3868
- } catch {
3869
- return null;
3870
- }
3871
- }
3872
- function checkSchemaDrift(rootDir) {
3873
- const findings = [];
3874
- const db = readGraphFile(rootDir, "db");
3875
- if (!db) {
3876
- 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." });
3877
- return buildReport("db", "schema_drift", findings);
3878
- }
3879
- for (const c of db.contradictions ?? []) {
3880
- const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
3881
- findings.push({
3882
- id: `drift:${c.entity}`,
3883
- severity: isTableLevel ? "error" : "warning",
3884
- category: "schema_drift",
3885
- title: c.entity,
3886
- detail: c.detail
3887
- });
3888
- }
3889
- return buildReport("db", "schema_drift", findings);
3890
- }
3891
- function checkOrphanFks(rootDir) {
3892
- const findings = [];
3893
- const db = readGraphFile(rootDir, "db");
3894
- if (!db) return buildReport("db", "orphan_fks", findings);
3895
- for (const f of db.flagged_edges ?? []) {
3896
- findings.push({
3897
- id: `fk:${f.source}->${f.target}`,
3898
- severity: "warning",
3899
- category: "orphan_fks",
3900
- title: `${f.source} \u2192 ${f.target}`,
3901
- detail: f.label
4695
+ await initTreeSitter();
4696
+ const config = loadConfig(req.rootDir);
4697
+ setExtractorConfig({
4698
+ dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
4699
+ mutationMethods: config.parsers?.patterns?.mutationMethods
3902
4700
  });
3903
- }
3904
- return buildReport("db", "orphan_fks", findings);
3905
- }
3906
- function checkUnprotectedRoutes(rootDir) {
3907
- const findings = [];
3908
- const api = readGraphFile(rootDir, "api");
3909
- const staticGraph = readGraphFile(rootDir, "static");
3910
- if (!api) return buildReport("api", "unprotected_routes", findings);
3911
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
3912
- let routePermsContent = "";
3913
- if ((0, import_node_fs16.existsSync)(routePermsPath)) {
3914
- routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
3915
- }
3916
- const registeredRoutes = /* @__PURE__ */ new Set();
3917
- const routeEntryRe = /path:\s*'([^']+)'/g;
3918
- let rm;
3919
- while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
3920
- registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
3921
- }
3922
- for (const node of api.nodes) {
3923
- if (node.type !== "endpoint") continue;
3924
- const route = node.route ?? "";
3925
- if (!route) continue;
3926
- const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
3927
- const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
3928
- if (!isRegistered) {
3929
- const methods = node.methods ?? [];
3930
- findings.push({
3931
- id: `unprotected:${node.id}`,
3932
- severity: "warning",
3933
- category: "unprotected_routes",
3934
- title: `${methods.join(",")} ${route}`,
3935
- detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
3936
- file: node.id
3937
- });
3938
- }
3939
- }
3940
- return buildReport("api", "unprotected_routes", findings);
3941
- }
3942
- function routeMatchesPattern(route, pattern) {
3943
- const routeParts = route.split("/");
3944
- const patternParts = pattern.split("/");
3945
- if (routeParts.length !== patternParts.length) return false;
3946
- for (let i = 0; i < routeParts.length; i++) {
3947
- const rp = routeParts[i];
3948
- const pp = patternParts[i];
3949
- if (rp === pp) continue;
3950
- if (pp.startsWith("[") || pp.startsWith(":")) continue;
3951
- if (rp.startsWith("[") || rp.startsWith(":")) continue;
3952
- return false;
3953
- }
3954
- return true;
3955
- }
3956
- function checkDeadScreens(rootDir) {
3957
- const findings = [];
3958
- const ui = readGraphFile(rootDir, "ui");
3959
- if (!ui) return buildReport("ui", "dead_screens", findings);
3960
- const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
3961
- const navTargets = /* @__PURE__ */ new Set();
3962
- for (const e of ui.edges) {
3963
- if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
3964
- navTargets.add(e.target);
3965
- }
3966
- }
3967
- for (const cr of ui.cross_refs ?? []) {
3968
- navTargets.add(cr.target);
3969
- }
3970
- for (const page of pages) {
3971
- if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
3972
- if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
3973
- if (!navTargets.has(page.id)) {
3974
- findings.push({
3975
- id: `dead:${page.id}`,
3976
- severity: "info",
3977
- category: "dead_screens",
3978
- title: page.name,
3979
- detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
3980
- file: page.id
3981
- });
3982
- }
3983
- }
3984
- return buildReport("ui", "dead_screens", findings);
3985
- }
3986
- function checkUnenforcedPermissions(rootDir) {
3987
- const findings = [];
3988
- const staticGraph = readGraphFile(rootDir, "static");
3989
- if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
3990
- const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
3991
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
3992
- let routePermsContent = "";
3993
- if ((0, import_node_fs16.existsSync)(routePermsPath)) {
3994
- routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
3995
- }
3996
- for (const perm of permissions) {
3997
- const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
3998
- if (!regex.test(routePermsContent)) {
3999
- findings.push({
4000
- id: `unenforced:${perm.key}`,
4001
- severity: "warning",
4002
- category: "unenforced_permissions",
4003
- title: `${perm.name} (${perm.key})`,
4004
- detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
4005
- });
4006
- }
4007
- }
4008
- return buildReport("static", "unenforced_permissions", findings);
4009
- }
4010
- function checkHardcodedValues(rootDir) {
4011
- const findings = [];
4012
- const staticGraph = readGraphFile(rootDir, "static");
4013
- if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
4014
- const knownValues = /* @__PURE__ */ new Set();
4015
- for (const n of staticGraph.nodes) {
4016
- if (n.type === "enum_value") knownValues.add(n.value);
4017
- }
4018
- const api = readGraphFile(rootDir, "api");
4019
- if (!api) return buildReport("static", "hardcoded_values", findings);
4020
- const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
4021
- const seen = /* @__PURE__ */ new Set();
4022
- for (const node of api.nodes) {
4023
- if (node.type !== "endpoint") continue;
4024
- const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
4025
- if (!(0, import_node_fs16.existsSync)(filePath)) continue;
4026
- const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
4027
- let m;
4028
- allCapsRe.lastIndex = 0;
4029
- while ((m = allCapsRe.exec(content)) !== null) {
4030
- const val = m[1];
4031
- if (knownValues.has(val)) continue;
4032
- if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
4033
- const key = `${node.id}:${val}`;
4034
- if (seen.has(key)) continue;
4035
- seen.add(key);
4036
- findings.push({
4037
- id: `hardcoded:${key}`,
4038
- severity: "info",
4039
- category: "hardcoded_values",
4040
- title: `"${val}" in ${node.id}`,
4041
- detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
4042
- file: node.id
4043
- });
4044
- }
4045
- }
4046
- return buildReport("static", "hardcoded_values", findings);
4047
- }
4048
- function buildReport(layer, check, findings) {
4049
- return {
4050
- layer,
4051
- check,
4052
- findings,
4053
- summary: {
4054
- errors: findings.filter((f) => f.severity === "error").length,
4055
- warnings: findings.filter((f) => f.severity === "warning").length,
4056
- info: findings.filter((f) => f.severity === "info").length
4057
- },
4058
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4059
- };
4060
- }
4061
- var CHECKS = {
4062
- db: {
4063
- schema_drift: checkSchemaDrift,
4064
- orphan_fks: checkOrphanFks
4065
- },
4066
- api: {
4067
- unprotected_routes: checkUnprotectedRoutes
4068
- },
4069
- ui: {
4070
- dead_screens: checkDeadScreens
4071
- },
4072
- static: {
4073
- unenforced_permissions: checkUnenforcedPermissions,
4074
- hardcoded_values: checkHardcodedValues
4075
- }
4076
- };
4077
- function getAvailableChecks() {
4078
- const result = {};
4079
- for (const [layer, checks] of Object.entries(CHECKS)) {
4080
- result[layer] = Object.keys(checks);
4081
- }
4082
- return result;
4083
- }
4084
- function runAudit(rootDir, layer, check) {
4085
- if (layer === "all") {
4086
- const reports = [];
4087
- for (const [l, checks] of Object.entries(CHECKS)) {
4088
- for (const fn of Object.values(checks)) {
4089
- reports.push(fn(rootDir));
4090
- }
4091
- }
4092
- return reports;
4093
- }
4094
- const layerChecks = CHECKS[layer];
4095
- if (!layerChecks) {
4096
- return [{
4097
- layer,
4098
- check: "unknown",
4099
- findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
4100
- summary: { errors: 1, warnings: 0, info: 0 },
4101
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4102
- }];
4103
- }
4104
- if (check) {
4105
- const fn = layerChecks[check];
4106
- if (!fn) {
4107
- return [{
4108
- layer,
4109
- check,
4110
- findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
4111
- summary: { errors: 1, warnings: 0, info: 0 },
4112
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4113
- }];
4114
- }
4115
- return [fn(rootDir)];
4116
- }
4117
- return Object.values(layerChecks).map((fn) => fn(rootDir));
4118
- }
4119
- function formatAsPrompt(reports) {
4120
- const lines = [];
4121
- for (const report of reports) {
4122
- if (report.findings.length === 0) continue;
4123
- lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
4124
- lines.push("");
4125
- for (const f of report.findings) {
4126
- const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
4127
- const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
4128
- lines.push(`- [${tag}] ${f.title}${filePart}`);
4129
- lines.push(` ${f.detail}`);
4130
- }
4131
- lines.push("");
4132
- }
4133
- if (lines.length === 0) return "No audit findings.";
4134
- return lines.join("\n");
4135
- }
4136
-
4137
- // src/server/chart-serve.ts
4138
- var MAX_PORT_SCAN = 3;
4139
- function randomPort() {
4140
- return 49152 + Math.floor(Math.random() * (65535 - 49152));
4141
- }
4142
- var MIME_TYPES = {
4143
- ".html": "text/html; charset=utf-8",
4144
- ".js": "application/javascript; charset=utf-8",
4145
- ".css": "text/css; charset=utf-8",
4146
- ".json": "application/json; charset=utf-8",
4147
- ".png": "image/png",
4148
- ".svg": "image/svg+xml",
4149
- ".ico": "image/x-icon",
4150
- ".woff": "font/woff",
4151
- ".woff2": "font/woff2"
4152
- };
4153
- function findProjectRoot(startDir) {
4154
- let dir = startDir;
4155
- for (let i = 0; i < 8; i++) {
4156
- const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
4157
- 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;
4158
- const parent = import_node_path19.default.dirname(dir);
4159
- if (parent === dir) break;
4160
- dir = parent;
4161
- }
4162
- dir = startDir;
4163
- for (let i = 0; i < 8; i++) {
4164
- if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
4165
- const parent = import_node_path19.default.dirname(dir);
4166
- if (parent === dir) break;
4167
- dir = parent;
4168
- }
4169
- return startDir;
4170
- }
4171
- function resolveRequestRoot(url, monorepoRoot, projects) {
4172
- const projectParam = url.searchParams.get("project");
4173
- if (!projectParam || projects.length === 0) return monorepoRoot;
4174
- const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
4175
- if (!resolved.startsWith(monorepoRoot)) {
4176
- throw new Error("Project path outside monorepo root");
4177
- }
4178
- return resolved;
4179
- }
4180
- async function buildMergedGraph(root) {
4181
- let graphs = readAllGraphs(root);
4182
- if (Object.keys(graphs).length === 0) {
4183
- await generateGraph(root);
4184
- graphs = readAllGraphs(root);
4185
- }
4186
- const nodes = [];
4187
- const rawLinks = [];
4188
- for (const layer of Object.keys(graphs)) {
4189
- const g = graphs[layer];
4190
- if (!g) continue;
4191
- for (const n of g.nodes) {
4192
- nodes.push({
4193
- id: `${layer}:${n.id}`,
4194
- name: n.name,
4195
- type: n.type,
4196
- layer,
4197
- module: n.module ?? null,
4198
- path: n.path ?? n.id
4199
- });
4200
- }
4201
- for (const e of g.edges) {
4202
- rawLinks.push({ source: `${layer}:${e.source}`, target: `${layer}:${e.target}`, type: e.type, layer, cross: false });
4203
- }
4204
- for (const c of g.cross_refs ?? []) {
4205
- rawLinks.push({ source: `${layer}:${c.source}`, target: `${c.layer}:${c.target}`, type: c.type, layer, cross: true });
4206
- }
4207
- }
4208
- const nodeIds = new Set(nodes.map((n) => n.id));
4209
- const links = rawLinks.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
4210
- return {
4211
- nodes,
4212
- links,
4213
- stats: {
4214
- nodes: nodes.length,
4215
- links: links.length,
4216
- byLayer: {
4217
- ui: graphs.ui ? graphs.ui.nodes.length : 0,
4218
- api: graphs.api ? graphs.api.nodes.length : 0,
4219
- db: graphs.db ? graphs.db.nodes.length : 0
4220
- }
4221
- }
4222
- };
4223
- }
4224
- function serveStatic(res, filePath) {
4225
- if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
4226
- const ext = import_node_path19.default.extname(filePath).toLowerCase();
4227
- const mime = MIME_TYPES[ext] ?? "application/octet-stream";
4228
- res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
4229
- import_node_fs17.default.createReadStream(filePath).pipe(res);
4230
- return true;
4231
- }
4232
- function serveIndex(res, clientDir) {
4233
- const indexPath = import_node_path19.default.join(clientDir, "index.html");
4234
- if (!import_node_fs17.default.existsSync(indexPath)) {
4235
- res.writeHead(500, { "Content-Type": "text/plain" });
4236
- res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
4237
- return;
4238
- }
4239
- serveStatic(res, indexPath);
4240
- }
4241
- function tryListen(server, port) {
4242
- return new Promise((resolve3, reject) => {
4243
- const onError = (err) => {
4244
- server.off("listening", onListening);
4245
- reject(err);
4246
- };
4247
- const onListening = () => {
4248
- server.off("error", onError);
4249
- resolve3(port);
4250
- };
4251
- server.once("error", onError);
4252
- server.once("listening", onListening);
4253
- server.listen(port, "127.0.0.1");
4254
- });
4255
- }
4256
- async function bindWithFallback(server, startPort) {
4257
- let lastErr = null;
4258
- for (let i = 0; i < MAX_PORT_SCAN; i++) {
4259
- const port = startPort + i;
4701
+ const failureCache = new ParseFailureCache(req.rootDir, getTreeSitterFingerprint());
4702
+ if (!req.layer) failureCache.clearAll();
4703
+ setActiveFailureCache(failureCache);
4704
+ let results;
4260
4705
  try {
4261
- return await tryListen(server, port);
4262
- } catch (err) {
4263
- const code = err.code;
4264
- if (code === "EADDRINUSE") {
4265
- lastErr = err;
4266
- continue;
4267
- }
4268
- throw err;
4269
- }
4270
- }
4271
- throw lastErr ?? new Error("Failed to bind any port");
4272
- }
4273
- async function startChartServer(opts = {}) {
4274
- const cwd = opts.cwd ?? process.cwd();
4275
- const projectRoot = findProjectRoot(cwd);
4276
- const existing = getLiveLock(projectRoot);
4277
- if (existing) {
4278
- if (!opts.quiet) {
4279
- process.stderr.write(
4280
- `[launch-chart] already running (pid ${existing.pid}) at ${existing.url}
4281
- `
4282
- );
4283
- }
4284
- return { port: existing.port, url: existing.url };
4285
- }
4286
- const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
4287
- const rootConfig = loadConfig(projectRoot);
4288
- const projects = rootConfig.projects ?? [];
4289
- const server = import_node_http.default.createServer((req, res) => {
4290
- try {
4291
- const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
4292
- let reqRoot;
4293
- try {
4294
- reqRoot = resolveRequestRoot(url2, projectRoot, projects);
4295
- } catch (err) {
4296
- res.writeHead(400, { "Content-Type": "application/json" });
4297
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4298
- return;
4299
- }
4300
- if (req.method === "GET" && url2.pathname === "/api/projects") {
4301
- const projectList = projects.length > 0 ? projects.map((p) => {
4302
- const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
4303
- const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
4304
- 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"));
4305
- return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
4306
- }) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
4307
- res.writeHead(200, { "Content-Type": "application/json" });
4308
- res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
4309
- return;
4310
- }
4311
- if (req.method === "GET" && url2.pathname === "/api/project-graph") {
4312
- const regenerate = url2.searchParams.get("regenerate") === "1";
4313
- (async () => {
4314
- if (regenerate) await generateGraph(reqRoot);
4315
- const merged = await buildMergedGraph(reqRoot);
4316
- res.writeHead(200, { "Content-Type": "application/json" });
4317
- res.end(JSON.stringify({
4318
- ...merged,
4319
- debug: { cwd, projectRoot: reqRoot }
4320
- }));
4321
- })().catch((e) => {
4322
- res.writeHead(500);
4323
- res.end(String(e));
4324
- });
4325
- return;
4326
- }
4327
- if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
4328
- const graphs = readAllGraphs(reqRoot);
4329
- res.writeHead(200, { "Content-Type": "application/json" });
4330
- res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
4331
- return;
4332
- }
4333
- if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
4334
- (async () => {
4335
- await generateGraph(reqRoot);
4336
- const graphs = readAllGraphs(reqRoot);
4337
- res.writeHead(200, { "Content-Type": "application/json" });
4338
- res.end(JSON.stringify({
4339
- ok: true,
4340
- ui: graphs.ui ?? null,
4341
- api: graphs.api ?? null,
4342
- db: graphs.db ?? null
4343
- }));
4344
- })().catch((err) => {
4345
- res.writeHead(500, { "Content-Type": "application/json" });
4346
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4347
- });
4348
- return;
4349
- }
4350
- if (req.method === "GET" && url2.pathname === "/api/file-content") {
4351
- const relPath = url2.searchParams.get("path");
4352
- if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
4353
- res.writeHead(400, { "Content-Type": "application/json" });
4354
- res.end(JSON.stringify({ error: "Invalid path" }));
4355
- return;
4356
- }
4357
- const filePath = import_node_path19.default.join(reqRoot, relPath);
4358
- if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
4359
- res.writeHead(404, { "Content-Type": "application/json" });
4360
- res.end(JSON.stringify({ error: "File not found" }));
4361
- return;
4362
- }
4363
- const ext = import_node_path19.default.extname(filePath).toLowerCase();
4364
- const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
4365
- const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
4366
- res.writeHead(200, { "Content-Type": "application/json" });
4367
- res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
4368
- return;
4369
- }
4370
- if (req.method === "GET" && url2.pathname === "/api/health") {
4371
- res.writeHead(200, { "Content-Type": "application/json" });
4372
- res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
4373
- return;
4374
- }
4375
- if (req.method === "GET" && url2.pathname === "/api/parser-config") {
4376
- const config2 = loadConfig(reqRoot);
4377
- const registry = createRegistry(config2, reqRoot);
4378
- const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
4379
- const detection = [];
4380
- for (const parser of registry.getAll()) {
4381
- if ("layers" in parser && Array.isArray(parser.layers)) {
4382
- const mp = parser;
4383
- detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
4384
- } else if ("layer" in parser && parser.layer !== "crosslayer") {
4385
- const sp = parser;
4386
- detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
4387
- }
4388
- }
4389
- const crosslayerParsers = {};
4390
- for (const p of registry.getCrossLayerParsers()) {
4391
- const concern = p.concern ?? "api-binding";
4392
- if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
4393
- crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
4394
- }
4395
- res.writeHead(200, { "Content-Type": "application/json" });
4396
- res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
4397
- return;
4398
- }
4399
- if (req.method === "POST" && url2.pathname === "/api/parser-config") {
4400
- let body = "";
4401
- req.on("data", (chunk) => {
4402
- body += chunk.toString();
4403
- });
4404
- req.on("end", () => {
4405
- try {
4406
- const newConfig = JSON.parse(body);
4407
- const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4408
- import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
4409
- res.writeHead(200, { "Content-Type": "application/json" });
4410
- res.end(JSON.stringify({ ok: true }));
4411
- } catch (err) {
4412
- res.writeHead(400, { "Content-Type": "application/json" });
4413
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4414
- }
4415
- });
4416
- return;
4417
- }
4418
- if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
4419
- const config2 = loadConfig(reqRoot);
4420
- const builtinTaggers = [
4421
- { id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
4422
- { id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
4423
- ];
4424
- const disabled = config2.taggers?.disabled ?? [];
4425
- const customTaggers = config2.taggers?.custom ?? [];
4426
- const moduleRules = config2.taggers?.module?.rules ?? [];
4427
- res.writeHead(200, { "Content-Type": "application/json" });
4428
- res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
4429
- return;
4430
- }
4431
- if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
4432
- let body = "";
4433
- req.on("data", (chunk) => {
4434
- body += chunk.toString();
4435
- });
4436
- req.on("end", () => {
4437
- try {
4438
- const taggerConfig = JSON.parse(body);
4439
- const config2 = loadConfig(reqRoot);
4440
- config2.taggers = taggerConfig;
4441
- const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4442
- import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
4443
- res.writeHead(200, { "Content-Type": "application/json" });
4444
- res.end(JSON.stringify({ ok: true }));
4445
- } catch (err) {
4446
- res.writeHead(400, { "Content-Type": "application/json" });
4447
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4448
- }
4449
- });
4450
- return;
4451
- }
4452
- if (req.method === "GET" && url2.pathname === "/api/tags") {
4453
- const store = readTagStore(reqRoot);
4454
- res.writeHead(200, { "Content-Type": "application/json" });
4455
- res.end(JSON.stringify(store));
4456
- return;
4457
- }
4458
- if (req.method === "POST" && url2.pathname === "/api/tags") {
4459
- let body = "";
4460
- req.on("data", (chunk) => {
4461
- body += chunk.toString();
4462
- });
4463
- req.on("end", () => {
4464
- try {
4465
- const { nodeId, key, value } = JSON.parse(body);
4466
- if (!nodeId || !key || !value) {
4467
- res.writeHead(400, { "Content-Type": "application/json" });
4468
- res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
4469
- return;
4470
- }
4471
- setTag(reqRoot, nodeId, key, value);
4472
- res.writeHead(200, { "Content-Type": "application/json" });
4473
- res.end(JSON.stringify({ ok: true }));
4474
- } catch (err) {
4475
- res.writeHead(400, { "Content-Type": "application/json" });
4476
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4477
- }
4478
- });
4479
- return;
4480
- }
4481
- if (req.method === "DELETE" && url2.pathname === "/api/tags") {
4482
- let body = "";
4483
- req.on("data", (chunk) => {
4484
- body += chunk.toString();
4485
- });
4486
- req.on("end", () => {
4487
- try {
4488
- const { nodeId, key } = JSON.parse(body);
4489
- if (!nodeId || !key) {
4490
- res.writeHead(400, { "Content-Type": "application/json" });
4491
- res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
4492
- return;
4493
- }
4494
- removeTag(reqRoot, nodeId, key);
4495
- res.writeHead(200, { "Content-Type": "application/json" });
4496
- res.end(JSON.stringify({ ok: true }));
4497
- } catch (err) {
4498
- res.writeHead(400, { "Content-Type": "application/json" });
4499
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4500
- }
4501
- });
4502
- return;
4503
- }
4504
- if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
4505
- const config2 = loadConfig(reqRoot);
4506
- const paths = resolveProjectPaths(reqRoot, config2);
4507
- const overrides = {
4508
- appDir: !!config2.paths?.appDir,
4509
- dbDir: !!config2.paths?.dbDir
4510
- };
4511
- res.writeHead(200, { "Content-Type": "application/json" });
4512
- res.end(JSON.stringify({
4513
- projectRoot: reqRoot,
4514
- detected: paths ? {
4515
- srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
4516
- appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
4517
- apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
4518
- dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
4519
- } : null,
4520
- overrides,
4521
- isOverride: overrides.appDir
4522
- }));
4523
- return;
4524
- }
4525
- if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
4526
- const browsePath = url2.searchParams.get("path") || projectRoot;
4527
- const abs = import_node_path19.default.resolve(browsePath);
4528
- const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
4529
- if (!abs.startsWith(twoUp)) {
4530
- res.writeHead(403, { "Content-Type": "application/json" });
4531
- res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
4532
- return;
4533
- }
4534
- try {
4535
- const entries = import_node_fs17.default.readdirSync(abs, { withFileTypes: true });
4536
- 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();
4537
- const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
4538
- res.writeHead(200, { "Content-Type": "application/json" });
4539
- res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
4540
- } catch (err) {
4541
- res.writeHead(400, { "Content-Type": "application/json" });
4542
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4543
- }
4544
- return;
4545
- }
4546
- if (req.method === "GET" && url2.pathname === "/api/audit") {
4547
- const layer = url2.searchParams.get("layer") ?? "all";
4548
- const check = url2.searchParams.get("check") ?? void 0;
4549
- const reports = runAudit(reqRoot, layer, check);
4550
- const prompt = formatAsPrompt(reports);
4551
- res.writeHead(200, { "Content-Type": "application/json" });
4552
- res.end(JSON.stringify({ reports, prompt, checks: getAvailableChecks() }));
4553
- return;
4554
- }
4555
- if (req.method === "POST" && url2.pathname === "/api/projects") {
4556
- let body = "";
4557
- req.on("data", (chunk) => {
4558
- body += chunk.toString();
4559
- });
4560
- req.on("end", () => {
4561
- try {
4562
- const { projects: newProjects } = JSON.parse(body);
4563
- const config2 = loadConfig(projectRoot);
4564
- config2.projects = newProjects.length > 0 ? newProjects : void 0;
4565
- const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
4566
- import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
4567
- projects.length = 0;
4568
- if (config2.projects) projects.push(...config2.projects);
4569
- res.writeHead(200, { "Content-Type": "application/json" });
4570
- res.end(JSON.stringify({ ok: true }));
4571
- } catch (err) {
4572
- res.writeHead(400, { "Content-Type": "application/json" });
4573
- res.end(JSON.stringify({ ok: false, error: String(err) }));
4574
- }
4575
- });
4576
- return;
4577
- }
4578
- if (url2.pathname !== "/") {
4579
- const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
4580
- if (serveStatic(res, staticPath)) return;
4581
- }
4582
- serveIndex(res, clientDir);
4583
- } catch (err) {
4584
- res.writeHead(500, { "Content-Type": "application/json" });
4585
- res.end(JSON.stringify({ error: String(err) }));
4586
- }
4587
- });
4588
- const config = loadConfig(projectRoot);
4589
- const startPort = opts.port ?? config.port ?? randomPort();
4590
- const port = await bindWithFallback(server, startPort);
4591
- const url = `http://localhost:${port}`;
4592
- writeLock({
4593
- pid: process.pid,
4594
- port,
4595
- cwd,
4596
- url,
4597
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
4598
- }, projectRoot);
4599
- const cleanup = () => {
4600
- clearLock(projectRoot);
4601
- server.close();
4602
- };
4603
- process.once("SIGINT", () => {
4604
- cleanup();
4605
- process.exit(0);
4606
- });
4607
- process.once("SIGTERM", () => {
4608
- cleanup();
4609
- process.exit(0);
4610
- });
4611
- process.once("exit", cleanup);
4612
- if (!opts.quiet) {
4613
- process.stderr.write(`[launch-chart] serving ${url}
4614
- `);
4615
- process.stderr.write(`[launch-chart] project root: ${projectRoot}
4616
- `);
4617
- if (projects.length > 0) {
4618
- process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
4619
- `);
4620
- }
4621
- }
4622
- return { port, url };
4623
- }
4624
- function runServeCli(argv) {
4625
- let port;
4626
- for (let i = 0; i < argv.length; i++) {
4627
- if (argv[i] === "--port" && argv[i + 1]) {
4628
- port = parseInt(argv[++i], 10);
4629
- } else if (argv[i].startsWith("--port=")) {
4630
- port = parseInt(argv[i].slice("--port=".length), 10);
4706
+ results = req.layer ? [generateLayer(req.rootDir, req.layer)].filter((r) => r !== null) : generateAll(req.rootDir);
4707
+ } finally {
4708
+ failureCache.flush();
4709
+ setActiveFailureCache(null);
4631
4710
  }
4711
+ const failedFiles = failureCache.list().map((f) => f.path);
4712
+ return { ok: true, results, failedFiles };
4713
+ } catch (e) {
4714
+ const err = e instanceof Error ? e : new Error(String(e));
4715
+ return { ok: false, error: { name: err.name, message: err.message, stack: err.stack } };
4632
4716
  }
4633
- startChartServer({ port }).catch((err) => {
4634
- process.stderr.write(`[launch-chart] failed to start: ${err}
4635
- `);
4636
- process.exit(1);
4637
- });
4638
4717
  }
4639
- // Annotate the CommonJS export names for ESM import in node:
4640
- 0 && (module.exports = {
4641
- runServeCli,
4642
- startChartServer
4718
+ import_node_worker_threads.parentPort.once("message", async (msg) => {
4719
+ const reply = await run(msg);
4720
+ import_node_worker_threads.parentPort.postMessage(reply);
4643
4721
  });