@launchsecure/launch-kit 0.0.9 → 0.0.10

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.
@@ -155,37 +155,134 @@ var init_config = __esm({
155
155
  }
156
156
  });
157
157
 
158
- // src/server/graph/core/ast-helpers.ts
159
- function getTs() {
160
- if (!tsModule) {
161
- tsModule = require("typescript");
158
+ // src/server/graph/core/ts-extractor.ts
159
+ async function initTreeSitter() {
160
+ if (initialized) return;
161
+ if (initPromise) return initPromise;
162
+ initPromise = (async () => {
163
+ const TreeSitter = require("web-tree-sitter");
164
+ await TreeSitter.init();
165
+ const wasmPath = require.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
166
+ tsxLanguage = await TreeSitter.Language.load(wasmPath);
167
+ parserInstance = new TreeSitter();
168
+ parserInstance.setLanguage(tsxLanguage);
169
+ initialized = true;
170
+ })();
171
+ return initPromise;
172
+ }
173
+ function ensureInit() {
174
+ if (!initialized || !tsxLanguage || !parserInstance) {
175
+ throw new Error("Tree-sitter not initialized. Call initTreeSitter() first.");
162
176
  }
163
- return tsModule;
164
177
  }
165
- function parseFile(absPath) {
166
- const ts = getTs();
178
+ function getQuery(name) {
179
+ ensureInit();
180
+ const cached = queryCache.get(name);
181
+ if (cached) return cached;
182
+ const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
183
+ const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
184
+ const query = tsxLanguage.query(scm);
185
+ queryCache.set(name, query);
186
+ return query;
187
+ }
188
+ function parseSource(absPath) {
189
+ ensureInit();
167
190
  const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
168
- const ext = (0, import_node_path3.extname)(absPath);
169
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
170
- const sourceFile = ts.createSourceFile(
171
- absPath,
172
- content,
173
- ts.ScriptTarget.Latest,
174
- /* setParentNodes */
175
- true,
176
- scriptKind
177
- );
191
+ return parserInstance.parse(content);
192
+ }
193
+ function setExtractorConfig(config) {
194
+ extraDbIdentifiers = config.dbIdentifiers ?? [];
195
+ extraMutationMethods = config.mutationMethods ?? [];
196
+ }
197
+ function getMutationMethods() {
198
+ return /* @__PURE__ */ new Set([...PRISMA_MUTATION_METHODS_BUILTIN, ...extraMutationMethods]);
199
+ }
200
+ function getFallbackDbIdentifiers() {
201
+ return /* @__PURE__ */ new Set([...DB_IDENTIFIERS_FALLBACK, ...extraDbIdentifiers]);
202
+ }
203
+ function looksLikeUrl(s) {
204
+ return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
205
+ }
206
+ function templateStartsWithSlash(text) {
207
+ const content = text.slice(1);
208
+ return content.startsWith("/");
209
+ }
210
+ function captureMap(match) {
211
+ const map = {};
212
+ for (const c of match.captures) {
213
+ map[c.name] = c.node.text;
214
+ }
215
+ return map;
216
+ }
217
+ function childrenOfType(node, type) {
218
+ return node.children.filter((n) => n.type === type);
219
+ }
220
+ function childOfType(node, type) {
221
+ return node.children.find((n) => n.type === type);
222
+ }
223
+ function parseFileTS(absPath) {
224
+ const tree = parseSource(absPath);
225
+ const root = tree.rootNode;
226
+ const imports = [];
227
+ const importStatements = childrenOfType(root, "import_statement");
228
+ for (const stmt of importStatements) {
229
+ const sourceNode = childOfType(stmt, "string");
230
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
231
+ if (!frag) continue;
232
+ const specifier = frag.text;
233
+ const hasTypeKeyword = stmt.children.some(
234
+ (n) => n.type === "type" && n.text === "type"
235
+ );
236
+ const clause = childOfType(stmt, "import_clause");
237
+ if (!clause) {
238
+ imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
239
+ continue;
240
+ }
241
+ const names = [];
242
+ const typeNames = /* @__PURE__ */ new Set();
243
+ const defaultId = childOfType(clause, "identifier");
244
+ if (defaultId) names.push(defaultId.text);
245
+ const nsImport = childOfType(clause, "namespace_import");
246
+ if (nsImport) {
247
+ const nsId = childOfType(nsImport, "identifier");
248
+ if (nsId) names.push(nsId.text);
249
+ }
250
+ const namedImports = childOfType(clause, "named_imports");
251
+ if (namedImports) {
252
+ for (const spec of childrenOfType(namedImports, "import_specifier")) {
253
+ const identifiers = childrenOfType(spec, "identifier");
254
+ const importedName = identifiers.length > 1 ? identifiers[identifiers.length - 1].text : identifiers[0]?.text;
255
+ if (importedName) {
256
+ names.push(importedName);
257
+ const specIsType = spec.children.some(
258
+ (n) => n.type === "type" && n.text === "type"
259
+ );
260
+ if (specIsType) typeNames.add(importedName);
261
+ }
262
+ }
263
+ }
264
+ if (names.length > 0 || hasTypeKeyword) {
265
+ imports.push({ names, specifier, isTypeOnly: hasTypeKeyword, typeNames });
266
+ }
267
+ }
268
+ const importQuery = getQuery("imports");
269
+ const importMatches = importQuery.matches(root);
270
+ for (const m of importMatches) {
271
+ const caps = captureMap(m);
272
+ if (caps["import.dynamic"]) {
273
+ imports.push({
274
+ names: [],
275
+ specifier: caps["import.dynamic"],
276
+ isTypeOnly: false,
277
+ typeNames: /* @__PURE__ */ new Set()
278
+ });
279
+ }
280
+ }
178
281
  const exportsSet = /* @__PURE__ */ new Set();
179
282
  const exportsOrdered = [];
180
283
  let defaultName = null;
181
284
  let firstValueExport = null;
182
285
  let firstTypeExport = null;
183
- const imports = [];
184
- const reExports = [];
185
- const jsxElements = /* @__PURE__ */ new Set();
186
- const navigations = [];
187
- const fetchCalls = [];
188
- const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
189
286
  function addExport(name2, kind) {
190
287
  if (!exportsSet.has(name2)) {
191
288
  exportsSet.add(name2);
@@ -195,298 +292,354 @@ function parseFile(absPath) {
195
292
  else if (kind === "value" && !firstValueExport) firstValueExport = name2;
196
293
  else if (kind === "type" && !firstTypeExport) firstTypeExport = name2;
197
294
  }
198
- function hasModifier(node, kind) {
199
- const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
200
- return modifiers?.some((m) => m.kind === kind) ?? false;
295
+ const exportQuery = getQuery("exports");
296
+ const exportMatches = exportQuery.matches(root);
297
+ for (const m of exportMatches) {
298
+ const caps = captureMap(m);
299
+ if (caps["export.default.func"]) addExport(caps["export.default.func"], "default");
300
+ else if (caps["export.default.class"]) addExport(caps["export.default.class"], "default");
301
+ else if (caps["export.default.value"]) addExport(caps["export.default.value"], "default");
302
+ else if (caps["export.named.func"]) addExport(caps["export.named.func"], "value");
303
+ else if (caps["export.named.class"]) addExport(caps["export.named.class"], "value");
304
+ else if (caps["export.named.const"]) addExport(caps["export.named.const"], "value");
305
+ else if (caps["export.named.enum"]) addExport(caps["export.named.enum"], "value");
306
+ else if (caps["export.named.type"]) addExport(caps["export.named.type"], "type");
307
+ else if (caps["export.named.interface"]) addExport(caps["export.named.interface"], "type");
308
+ else if (caps["export.bare.name"]) addExport(caps["export.bare.name"], "value");
309
+ if (caps["reexport.name"]) addExport(caps["reexport.name"], "value");
310
+ }
311
+ for (const stmt of childrenOfType(root, "export_statement")) {
312
+ const hasDefault = stmt.children.some((n) => n.text === "default");
313
+ if (hasDefault && !defaultName) {
314
+ defaultName = "default";
315
+ }
201
316
  }
202
- function extractTargetFromExpression(expr) {
203
- if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
204
- return { target: expr.text, isTemplate: false };
317
+ const reExports = [];
318
+ for (const m of exportMatches) {
319
+ const caps = captureMap(m);
320
+ if (caps["reexport.name"] && caps["reexport.source"]) {
321
+ reExports.push({ name: caps["reexport.name"], from: caps["reexport.source"] });
205
322
  }
206
- if (ts.isTemplateExpression(expr)) {
207
- return { target: expr.getText(sourceFile), isTemplate: true };
323
+ if (caps["reexport.wildcard.source"]) {
324
+ reExports.push({ name: "*", from: caps["reexport.wildcard.source"], isWildcard: true });
208
325
  }
209
- return null;
210
- }
211
- function looksLikeUrl(s) {
212
- return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
213
326
  }
214
- function templateStartsWithSlash(expr) {
215
- const head = expr.head.text;
216
- return head.startsWith("/");
327
+ const jsxElements = /* @__PURE__ */ new Set();
328
+ const jsxQuery = getQuery("jsx-elements");
329
+ const jsxCaptures = jsxQuery.captures(root);
330
+ for (const c of jsxCaptures) {
331
+ if (c.name === "jsx.tag") {
332
+ if (/^[A-Z]/.test(c.node.text)) {
333
+ jsxElements.add(c.node.text);
334
+ }
335
+ } else if (c.name === "jsx.member_tag") {
336
+ const rootName = c.node.text.split(".")[0];
337
+ if (rootName && /^[A-Z]/.test(rootName)) {
338
+ jsxElements.add(rootName);
339
+ }
340
+ }
217
341
  }
218
- function extractUrlFromFetchArg(arg) {
219
- if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
220
- if (!looksLikeUrl(arg.text)) return null;
221
- return { url: arg.text, isTemplate: false };
342
+ const navigations = [];
343
+ const navQuery = getQuery("navigations");
344
+ const navMatches = navQuery.matches(root);
345
+ for (const m of navMatches) {
346
+ const caps = captureMap(m);
347
+ if (caps["nav.target.literal"] && caps["nav.method"]) {
348
+ navigations.push({
349
+ kind: caps["nav.method"] === "push" ? "router-push" : "router-replace",
350
+ target: caps["nav.target.literal"],
351
+ isTemplate: false
352
+ });
222
353
  }
223
- if (ts.isTemplateExpression(arg)) {
224
- if (!templateStartsWithSlash(arg)) return null;
225
- return { url: arg.getText(sourceFile), isTemplate: true };
354
+ if (caps["nav.target.template"] && caps["nav.method.template"]) {
355
+ navigations.push({
356
+ kind: caps["nav.method.template"] === "push" ? "router-push" : "router-replace",
357
+ target: caps["nav.target.template"],
358
+ isTemplate: true
359
+ });
226
360
  }
227
- if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
228
- let leftmost = arg;
229
- while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
230
- leftmost = leftmost.left;
231
- }
232
- if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
233
- return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
234
- }
361
+ const linkLiteral = caps["nav.link.literal"] || caps["nav.link.literal.self"];
362
+ if (linkLiteral) {
363
+ navigations.push({ kind: "link-href", target: linkLiteral, isTemplate: false });
364
+ }
365
+ const linkTemplate = caps["nav.link.template"] || caps["nav.link.template.self"];
366
+ if (linkTemplate) {
367
+ navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
368
+ }
369
+ if (caps["nav.window.literal"]) {
370
+ navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
371
+ }
372
+ if (caps["nav.window.assign.target"]) {
373
+ navigations.push({ kind: "window-location", target: caps["nav.window.assign.target"], isTemplate: false });
235
374
  }
236
- return null;
237
375
  }
238
- function visit(node) {
239
- if (ts.isImportDeclaration(node)) {
240
- const moduleSpec = node.moduleSpecifier;
241
- if (ts.isStringLiteral(moduleSpec)) {
242
- const specifier = moduleSpec.text;
243
- const clause = node.importClause;
244
- const isTypeOnly = !!clause?.isTypeOnly;
245
- const names = [];
246
- const typeNames = /* @__PURE__ */ new Set();
247
- if (clause) {
248
- if (clause.name) names.push(clause.name.text);
249
- const nb = clause.namedBindings;
250
- if (nb && ts.isNamedImports(nb)) {
251
- for (const el of nb.elements) {
252
- names.push(el.name.text);
253
- if (el.isTypeOnly) typeNames.add(el.name.text);
254
- }
255
- } else if (nb && ts.isNamespaceImport(nb)) {
256
- names.push(nb.name.text);
257
- }
258
- }
259
- if (names.length > 0 || isTypeOnly) {
260
- imports.push({ names, specifier, isTypeOnly, typeNames });
261
- } else if (!clause) {
262
- imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
263
- }
264
- }
376
+ const fetchCalls = [];
377
+ const fetchQuery = getQuery("fetch-calls");
378
+ const fetchMatches = fetchQuery.matches(root);
379
+ for (const m of fetchMatches) {
380
+ const caps = captureMap(m);
381
+ if (caps["fetch.url.literal"] && looksLikeUrl(caps["fetch.url.literal"])) {
382
+ fetchCalls.push({ url: caps["fetch.url.literal"], isTemplate: false, kind: "fetch" });
383
+ }
384
+ if (caps["fetch.url.template"] && templateStartsWithSlash(caps["fetch.url.template"])) {
385
+ fetchCalls.push({ url: caps["fetch.url.template"], isTemplate: true, kind: "fetch" });
386
+ }
387
+ const clientUrl = caps["fetch.client.url.literal"] || caps["fetch.await.url.literal"];
388
+ const clientMethod = caps["fetch.method"] || caps["fetch.await.method"];
389
+ if (clientUrl && clientMethod && looksLikeUrl(clientUrl)) {
390
+ fetchCalls.push({
391
+ method: clientMethod.toUpperCase(),
392
+ url: clientUrl,
393
+ isTemplate: false,
394
+ kind: "client-method"
395
+ });
396
+ }
397
+ const clientUrlTpl = caps["fetch.client.url.template"] || caps["fetch.await.url.template"];
398
+ const clientMethodTpl = caps["fetch.method.template"] || caps["fetch.await.method.template"];
399
+ if (clientUrlTpl && clientMethodTpl && templateStartsWithSlash(clientUrlTpl)) {
400
+ fetchCalls.push({
401
+ method: clientMethodTpl.toUpperCase(),
402
+ url: clientUrlTpl,
403
+ isTemplate: true,
404
+ kind: "client-method"
405
+ });
265
406
  }
266
- if (ts.isExportDeclaration(node)) {
267
- const fromSpec = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
268
- if (node.exportClause && ts.isNamedExports(node.exportClause)) {
269
- for (const el of node.exportClause.elements) {
270
- const exportedName = el.name.text;
271
- addExport(exportedName, "value");
272
- if (fromSpec) {
273
- reExports.push({ name: exportedName, from: fromSpec });
407
+ }
408
+ const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
409
+ return { name, exports: exportsOrdered, imports, reExports, jsxElements, navigations, fetchCalls };
410
+ }
411
+ function extractDbCallsTS(absPath) {
412
+ const tree = parseSource(absPath);
413
+ const root = tree.rootNode;
414
+ const dbQuery = getQuery("db-calls");
415
+ const matches = dbQuery.matches(root);
416
+ const dbIdentifiers = /* @__PURE__ */ new Set();
417
+ for (const stmt of childrenOfType(root, "import_statement")) {
418
+ const sourceNode = childOfType(stmt, "string");
419
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
420
+ if (!frag) continue;
421
+ const spec = frag.text;
422
+ if (spec.includes("prisma") || spec.includes("/db") || spec === "@prisma/client") {
423
+ const clause = childOfType(stmt, "import_clause");
424
+ if (clause) {
425
+ const defaultId = childOfType(clause, "identifier");
426
+ if (defaultId) dbIdentifiers.add(defaultId.text);
427
+ const named = childOfType(clause, "named_imports");
428
+ if (named) {
429
+ for (const specNode of childrenOfType(named, "import_specifier")) {
430
+ const ids = childrenOfType(specNode, "identifier");
431
+ const importedName = ids[ids.length - 1];
432
+ if (importedName) dbIdentifiers.add(importedName.text);
274
433
  }
275
434
  }
276
- } else if (!node.exportClause && fromSpec) {
277
- reExports.push({ name: "*", from: fromSpec, isWildcard: true });
278
435
  }
279
436
  }
280
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
281
- const arg = node.arguments[0];
282
- if (arg && ts.isStringLiteral(arg)) {
283
- imports.push({
284
- names: [],
285
- specifier: arg.text,
286
- isTypeOnly: false,
287
- typeNames: /* @__PURE__ */ new Set()
288
- });
289
- }
437
+ }
438
+ if (dbIdentifiers.size === 0) {
439
+ for (const id of getFallbackDbIdentifiers()) dbIdentifiers.add(id);
440
+ } else {
441
+ for (const id of extraDbIdentifiers) dbIdentifiers.add(id);
442
+ }
443
+ const calls = [];
444
+ const seen = /* @__PURE__ */ new Set();
445
+ for (const m of matches) {
446
+ const caps = captureMap(m);
447
+ const identifier = caps["db.identifier"];
448
+ const model = caps["db.model"];
449
+ const method = caps["db.method"];
450
+ if (!identifier || !model || !method) continue;
451
+ if (!dbIdentifiers.has(identifier)) continue;
452
+ const key = `${model}.${method}`;
453
+ if (seen.has(key)) continue;
454
+ seen.add(key);
455
+ calls.push({ model, method, isMutation: getMutationMethods().has(method) });
456
+ }
457
+ return calls;
458
+ }
459
+ function extractAuthWrappersTS(absPath) {
460
+ const tree = parseSource(absPath);
461
+ const root = tree.rootNode;
462
+ const wrapperQuery = getQuery("wrappers");
463
+ const matches = wrapperQuery.matches(root);
464
+ const wrappers = /* @__PURE__ */ new Set();
465
+ for (const m of matches) {
466
+ const caps = captureMap(m);
467
+ if (caps["wrapper.fn_name"]) {
468
+ wrappers.add(caps["wrapper.fn_name"]);
290
469
  }
291
- if (ts.isExportAssignment(node) && !node.isExportEquals) {
292
- if (ts.isIdentifier(node.expression)) {
293
- addExport(node.expression.text, "default");
294
- } else {
295
- if (!defaultName) defaultName = "default";
296
- }
470
+ }
471
+ return wrappers;
472
+ }
473
+ function trunc(s, max = 120) {
474
+ return s.length > max ? s.slice(0, max) + "..." : s;
475
+ }
476
+ function extractDeep(absPath) {
477
+ const tree = parseSource(absPath);
478
+ const root = tree.rootNode;
479
+ const elements = [];
480
+ const elQuery = getQuery("deep/jsx-semantic");
481
+ const elMatches = elQuery.matches(root);
482
+ const elementMap = /* @__PURE__ */ new Map();
483
+ for (const m of elMatches) {
484
+ const tag = m.captures.find((c) => c.name === "el.tag")?.node;
485
+ if (!tag || !/^[A-Z]/.test(tag.text)) continue;
486
+ const elNode = m.captures.find((c) => c.name === "el.self" || c.name === "el.open")?.node;
487
+ const key = elNode ? `${elNode.startPosition.row}:${elNode.startPosition.column}` : `${tag.startPosition.row}:${tag.startPosition.column}`;
488
+ if (!elementMap.has(key)) {
489
+ elementMap.set(key, { tag: tag.text, props: {}, nodeKey: key });
297
490
  }
298
- if (ts.isFunctionDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
299
- const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
300
- if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
491
+ const entry = elementMap.get(key);
492
+ const propName = m.captures.find((c) => c.name === "el.prop.name")?.node.text;
493
+ const propVal = m.captures.find((c) => c.name === "el.prop.value.str")?.node.text;
494
+ const propExpr = m.captures.find((c) => c.name === "el.prop.value.expr")?.node.text;
495
+ if (propName) {
496
+ entry.props[propName] = propVal ?? (propExpr ? trunc(propExpr, 60) : "true");
301
497
  }
302
- if (ts.isVariableStatement(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
303
- for (const decl of node.declarationList.declarations) {
304
- if (ts.isIdentifier(decl.name)) {
305
- addExport(decl.name.text, "value");
306
- }
498
+ }
499
+ for (const m of elMatches) {
500
+ const textTag = m.captures.find((c) => c.name === "el.text.tag")?.node;
501
+ const textContent = m.captures.find((c) => c.name === "el.text.content")?.node;
502
+ if (textTag && textContent && /^[A-Z]/.test(textTag.text)) {
503
+ const key = `${textTag.startPosition.row}:${textTag.startPosition.column}`;
504
+ if (!elementMap.has(key)) {
505
+ elementMap.set(key, { tag: textTag.text, props: {}, nodeKey: key });
307
506
  }
507
+ const entry = elementMap.get(key);
508
+ const text = textContent.text.trim();
509
+ if (text) entry.props["_text"] = text;
308
510
  }
309
- if (ts.isClassDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
310
- const isDefault = hasModifier(node, ts.SyntaxKind.DefaultKeyword);
311
- if (node.name) addExport(node.name.text, isDefault ? "default" : "value");
312
- }
313
- if (ts.isTypeAliasDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
314
- addExport(node.name.text, "type");
511
+ }
512
+ for (const entry of elementMap.values()) {
513
+ const el = { tag: entry.tag, props: entry.props };
514
+ const hasExpr = Object.values(entry.props).some((v) => v.includes("{") || v.includes("("));
515
+ if (hasExpr) el.dynamic = true;
516
+ if (entry.props["_text"]) {
517
+ el.text = entry.props["_text"];
518
+ delete el.props["_text"];
315
519
  }
316
- if (ts.isInterfaceDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
317
- addExport(node.name.text, "type");
520
+ elements.push(el);
521
+ }
522
+ const stateVars = [];
523
+ const hookQuery = getQuery("deep/state-hooks");
524
+ const hookMatches = hookQuery.matches(root);
525
+ for (const m of hookMatches) {
526
+ const caps = captureMap(m);
527
+ if (caps["hook.var"] && caps["hook.setter"] && caps["hook.fn"]) {
528
+ stateVars.push({
529
+ name: caps["hook.var"],
530
+ setter: caps["hook.setter"],
531
+ hook: caps["hook.fn"],
532
+ init: trunc(caps["hook.init"] ?? "", 80)
533
+ });
318
534
  }
319
- if (ts.isEnumDeclaration(node) && hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
320
- addExport(node.name.text, "value");
535
+ if (caps["reducer.var"] && caps["reducer.dispatch"]) {
536
+ stateVars.push({
537
+ name: caps["reducer.var"],
538
+ setter: caps["reducer.dispatch"],
539
+ hook: "useReducer",
540
+ init: trunc(caps["reducer.init"] ?? "", 80)
541
+ });
321
542
  }
322
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
323
- const tagName = node.tagName;
324
- if (ts.isIdentifier(tagName) && /^[A-Z]/.test(tagName.text)) {
325
- jsxElements.add(tagName.text);
326
- } else if (ts.isPropertyAccessExpression(tagName)) {
327
- let root = tagName;
328
- while (ts.isPropertyAccessExpression(root)) root = root.expression;
329
- if (ts.isIdentifier(root) && /^[A-Z]/.test(root.text)) {
330
- jsxElements.add(root.text);
331
- }
332
- }
543
+ }
544
+ const conditions = [];
545
+ const condQuery = getQuery("deep/conditions");
546
+ const condMatches = condQuery.matches(root);
547
+ for (const m of condMatches) {
548
+ const caps = captureMap(m);
549
+ if (caps["cond.test"]) {
550
+ conditions.push({
551
+ kind: "if",
552
+ test: trunc(caps["cond.test"], 100),
553
+ consequence: caps["cond.consequence"] ? trunc(caps["cond.consequence"], 80) : void 0
554
+ });
333
555
  }
334
- if (ts.isCallExpression(node)) {
335
- const expr = node.expression;
336
- if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === "router" && (expr.name.text === "push" || expr.name.text === "replace")) {
337
- const arg = node.arguments[0];
338
- if (arg) {
339
- const parsed = extractTargetFromExpression(arg);
340
- if (parsed) {
341
- navigations.push({
342
- kind: expr.name.text === "push" ? "router-push" : "router-replace",
343
- target: parsed.target,
344
- isTemplate: parsed.isTemplate
345
- });
346
- }
347
- }
348
- }
556
+ if (caps["ternary.test"]) {
557
+ conditions.push({
558
+ kind: "ternary",
559
+ test: trunc(caps["ternary.test"], 100)
560
+ });
349
561
  }
350
- if (ts.isCallExpression(node) && node.arguments.length > 0) {
351
- const expr = node.expression;
352
- const firstArg = node.arguments[0];
353
- if (ts.isIdentifier(expr) && expr.text === "fetch") {
354
- const extracted = extractUrlFromFetchArg(firstArg);
355
- if (extracted) {
356
- fetchCalls.push({
357
- url: extracted.url,
358
- isTemplate: extracted.isTemplate,
359
- ...extracted.isConcat ? { isConcat: true } : {},
360
- kind: "fetch"
361
- });
362
- }
363
- }
364
- if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
365
- const methodName = expr.name.text;
366
- if (HTTP_METHODS.has(methodName)) {
367
- const extracted = extractUrlFromFetchArg(firstArg);
368
- if (extracted) {
369
- fetchCalls.push({
370
- method: methodName.toUpperCase(),
371
- url: extracted.url,
372
- isTemplate: extracted.isTemplate,
373
- ...extracted.isConcat ? { isConcat: true } : {},
374
- kind: "client-method"
375
- });
376
- }
377
- }
378
- }
562
+ }
563
+ const variables = [];
564
+ const varQuery = getQuery("deep/variables");
565
+ const varMatches = varQuery.matches(root);
566
+ for (const m of varMatches) {
567
+ const caps = captureMap(m);
568
+ const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
569
+ const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
570
+ if (caps["var.name"] && caps["var.init"]) {
571
+ variables.push({
572
+ name: caps["var.name"],
573
+ kind,
574
+ init: trunc(caps["var.init"], 100)
575
+ });
379
576
  }
380
- if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
381
- const tagName = node.tagName;
382
- if (ts.isIdentifier(tagName) && tagName.text === "Link") {
383
- for (const attr of node.attributes.properties) {
384
- if (ts.isJsxAttribute(attr) && attr.name.getText(sourceFile) === "href" && attr.initializer) {
385
- const init = attr.initializer;
386
- if (ts.isStringLiteral(init)) {
387
- navigations.push({ kind: "link-href", target: init.text, isTemplate: false });
388
- } else if (ts.isJsxExpression(init) && init.expression) {
389
- const parsed = extractTargetFromExpression(init.expression);
390
- if (parsed) {
391
- navigations.push({ kind: "link-href", target: parsed.target, isTemplate: parsed.isTemplate });
392
- }
393
- }
394
- }
395
- }
396
- }
577
+ if (caps["var.destructured.obj"]) {
578
+ variables.push({
579
+ name: trunc(caps["var.destructured.obj"], 60),
580
+ kind,
581
+ init: trunc(caps["var.destructured.init"], 100)
582
+ });
397
583
  }
398
- if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
399
- const left = node.left;
400
- if (ts.isPropertyAccessExpression(left) && ts.isPropertyAccessExpression(left.expression) && ts.isIdentifier(left.expression.expression) && left.expression.expression.text === "window" && left.expression.name.text === "location" && left.name.text === "href") {
401
- const parsed = extractTargetFromExpression(node.right);
402
- if (parsed) {
403
- navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
404
- }
405
- }
584
+ if (caps["var.array.pattern"]) {
585
+ variables.push({
586
+ name: trunc(caps["var.array.pattern"], 60),
587
+ kind,
588
+ init: trunc(caps["var.array.init"], 100)
589
+ });
406
590
  }
407
- if (ts.isCallExpression(node)) {
408
- const expr = node.expression;
409
- if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "window" && expr.expression.name.text === "location" && (expr.name.text === "assign" || expr.name.text === "replace")) {
410
- const arg = node.arguments[0];
411
- if (arg) {
412
- const parsed = extractTargetFromExpression(arg);
413
- if (parsed) {
414
- navigations.push({ kind: "window-location", target: parsed.target, isTemplate: parsed.isTemplate });
415
- }
416
- }
417
- }
591
+ }
592
+ const responses = [];
593
+ const respQuery = getQuery("deep/responses");
594
+ const respMatches = respQuery.matches(root);
595
+ const explicitBodies = /* @__PURE__ */ new Set();
596
+ for (const m of respMatches) {
597
+ const caps = captureMap(m);
598
+ if (caps["resp.status"] && caps["resp.body"]) {
599
+ explicitBodies.add(trunc(caps["resp.body"], 80));
600
+ responses.push({
601
+ status: caps["resp.status"],
602
+ body: trunc(caps["resp.body"], 80)
603
+ });
418
604
  }
419
- ts.forEachChild(node, visit);
420
605
  }
421
- visit(sourceFile);
422
- const name = defaultName ?? firstValueExport ?? firstTypeExport ?? "";
423
- return {
424
- name,
425
- exports: exportsOrdered,
426
- imports,
427
- reExports,
428
- jsxElements,
429
- navigations,
430
- fetchCalls
431
- };
432
- }
433
- function extractDbCalls(absPath) {
434
- const ts = getTs();
435
- const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
436
- const ext = (0, import_node_path3.extname)(absPath);
437
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
438
- const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
439
- const calls = [];
440
- const seen = /* @__PURE__ */ new Set();
441
- function visit(node) {
442
- if (ts.isCallExpression(node)) {
443
- const expr = node.expression;
444
- if (ts.isPropertyAccessExpression(expr) && ts.isPropertyAccessExpression(expr.expression) && ts.isIdentifier(expr.expression.expression) && expr.expression.expression.text === "db") {
445
- const model = expr.expression.name.text;
446
- const method = expr.name.text;
447
- const key = `${model}.${method}`;
448
- if (!seen.has(key)) {
449
- seen.add(key);
450
- calls.push({
451
- model,
452
- method,
453
- isMutation: MUTATION_METHODS.has(method)
454
- });
455
- }
606
+ for (const m of respMatches) {
607
+ const caps = captureMap(m);
608
+ if (caps["resp.body.default"] && !caps["resp.status"]) {
609
+ const bodyText = trunc(caps["resp.body.default"], 80);
610
+ if (!explicitBodies.has(bodyText)) {
611
+ responses.push({ status: "200", body: bodyText });
456
612
  }
457
613
  }
458
- ts.forEachChild(node, visit);
459
614
  }
460
- visit(sourceFile);
461
- return calls;
462
- }
463
- function extractAuthWrappers(absPath) {
464
- const ts = getTs();
465
- const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
466
- const ext = (0, import_node_path3.extname)(absPath);
467
- const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
468
- const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
469
- const wrappers = /* @__PURE__ */ new Set();
470
- const AUTH_WRAPPERS = /* @__PURE__ */ new Set(["withAuth", "withPermission", "withRole", "requireAuth"]);
471
- function visit(node) {
472
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
473
- if (AUTH_WRAPPERS.has(node.expression.text)) {
474
- wrappers.add(node.expression.text);
475
- }
615
+ const params = [];
616
+ const paramQuery = getQuery("deep/request-params");
617
+ const paramMatches = paramQuery.matches(root);
618
+ for (const m of paramMatches) {
619
+ const caps = captureMap(m);
620
+ if (caps["param.name"]) {
621
+ params.push({ name: caps["param.name"], source: "body-field" });
622
+ }
623
+ if (caps["param.body"]) {
624
+ params.push({ name: caps["param.body"], source: "body" });
476
625
  }
477
- ts.forEachChild(node, visit);
478
626
  }
479
- visit(sourceFile);
480
- return wrappers;
627
+ return { elements, stateVars, conditions, variables, responses, params };
481
628
  }
482
- var import_node_fs3, import_node_path3, tsModule, HTTP_METHODS, MUTATION_METHODS;
483
- var init_ast_helpers = __esm({
484
- "src/server/graph/core/ast-helpers.ts"() {
629
+ var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
630
+ var init_ts_extractor = __esm({
631
+ "src/server/graph/core/ts-extractor.ts"() {
485
632
  "use strict";
486
633
  import_node_fs3 = require("node:fs");
487
634
  import_node_path3 = require("node:path");
488
- HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
489
- MUTATION_METHODS = /* @__PURE__ */ new Set([
635
+ initialized = false;
636
+ queriesDir = (() => {
637
+ const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
638
+ if (require("fs").existsSync(srcPath)) return srcPath;
639
+ return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
640
+ })();
641
+ queryCache = /* @__PURE__ */ new Map();
642
+ PRISMA_MUTATION_METHODS_BUILTIN = [
490
643
  "create",
491
644
  "createMany",
492
645
  "createManyAndReturn",
@@ -496,7 +649,10 @@ var init_ast_helpers = __esm({
496
649
  "upsert",
497
650
  "delete",
498
651
  "deleteMany"
499
- ]);
652
+ ];
653
+ DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
654
+ extraDbIdentifiers = [];
655
+ extraMutationMethods = [];
500
656
  }
501
657
  });
502
658
 
@@ -822,7 +978,7 @@ function generate(rootDir) {
822
978
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
823
979
  const parsedByPath = /* @__PURE__ */ new Map();
824
980
  for (const absPath of allDiscovered) {
825
- parsedByPath.set(absPath, parseFile(absPath));
981
+ parsedByPath.set(absPath, parseFileTS(absPath));
826
982
  }
827
983
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
828
984
  const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
@@ -836,7 +992,18 @@ function generate(rootDir) {
836
992
  const parsed = parsedByPath.get(absPath);
837
993
  const name = parsed.name || nameFromFilename(absPath);
838
994
  const route = extractRoute(id);
839
- nodes.push({ id, type, name, route, exports: parsed.exports });
995
+ const deep = extractDeep(absPath);
996
+ nodes.push({
997
+ id,
998
+ type,
999
+ name,
1000
+ route,
1001
+ exports: parsed.exports,
1002
+ elements: deep.elements,
1003
+ stateVars: deep.stateVars,
1004
+ conditions: deep.conditions,
1005
+ variables: deep.variables
1006
+ });
840
1007
  nodeIdSet.add(id);
841
1008
  nodeTypeMap.set(id, type);
842
1009
  if (route) routeToNodeId.set(route, id);
@@ -895,7 +1062,7 @@ function generate(rootDir) {
895
1062
  if (externalScanned.has(normalized)) continue;
896
1063
  let parsed;
897
1064
  try {
898
- parsed = parseFile(absPath);
1065
+ parsed = parseFileTS(absPath);
899
1066
  } catch {
900
1067
  continue;
901
1068
  }
@@ -1015,7 +1182,7 @@ var init_react_nextjs = __esm({
1015
1182
  "use strict";
1016
1183
  import_node_fs4 = require("node:fs");
1017
1184
  import_node_path4 = require("node:path");
1018
- init_ast_helpers();
1185
+ init_ts_extractor();
1019
1186
  RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
1020
1187
  reactNextjsParser = {
1021
1188
  id: "react-nextjs",
@@ -1065,12 +1232,12 @@ function generate2(rootDir) {
1065
1232
  let endpointsWithAuth = 0;
1066
1233
  let endpointsWithDbAccess = 0;
1067
1234
  for (const absPath of routeFiles) {
1068
- const parsed = parseFile(absPath);
1069
- const dbCalls = extractDbCalls(absPath);
1070
- const authWrappers = extractAuthWrappers(absPath);
1235
+ const parsed = parseFileTS(absPath);
1236
+ const dbCalls = extractDbCallsTS(absPath);
1237
+ const authWrappers = extractAuthWrappersTS(absPath);
1071
1238
  const methods = [];
1072
1239
  for (const exp of parsed.exports) {
1073
- if (HTTP_METHODS2.has(exp)) methods.push(exp);
1240
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
1074
1241
  }
1075
1242
  const routePath = filePathToRoute(apiDir, absPath);
1076
1243
  const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
@@ -1089,6 +1256,7 @@ function generate2(rootDir) {
1089
1256
  authUsage[w] = (authUsage[w] ?? 0) + 1;
1090
1257
  }
1091
1258
  if (authStrategy.length > 0) endpointsWithAuth++;
1259
+ const deep = extractDeep(absPath);
1092
1260
  nodes.push({
1093
1261
  id: relPath,
1094
1262
  type: "endpoint",
@@ -1100,7 +1268,12 @@ function generate2(rootDir) {
1100
1268
  mutates,
1101
1269
  auth: authStrategy.length > 0 ? authStrategy : ["public"],
1102
1270
  db_models: [...new Set(dbCalls.map((c) => c.model))],
1103
- db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))]
1271
+ db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1272
+ // Deep extraction fields
1273
+ conditions: deep.conditions,
1274
+ variables: deep.variables,
1275
+ responses: deep.responses,
1276
+ params: deep.params
1104
1277
  });
1105
1278
  const seenModels = /* @__PURE__ */ new Set();
1106
1279
  for (const call of dbCalls) {
@@ -1140,7 +1313,7 @@ function generate2(rootDir) {
1140
1313
  flagged_edges: [],
1141
1314
  patterns: {
1142
1315
  total_endpoints: nodes.length,
1143
- methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
1316
+ methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1144
1317
  acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
1145
1318
  return acc;
1146
1319
  }, {}),
@@ -1150,14 +1323,14 @@ function generate2(rootDir) {
1150
1323
  }
1151
1324
  };
1152
1325
  }
1153
- var import_node_fs5, import_node_path5, HTTP_METHODS2, nextjsRoutesParser;
1326
+ var import_node_fs5, import_node_path5, HTTP_METHODS, nextjsRoutesParser;
1154
1327
  var init_nextjs_routes = __esm({
1155
1328
  "src/server/graph/parsers/api/nextjs-routes.ts"() {
1156
1329
  "use strict";
1157
1330
  import_node_fs5 = require("node:fs");
1158
1331
  import_node_path5 = require("node:path");
1159
- init_ast_helpers();
1160
- HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1332
+ init_ts_extractor();
1333
+ HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1161
1334
  nextjsRoutesParser = {
1162
1335
  id: "nextjs-routes",
1163
1336
  layer: "api",
@@ -2058,8 +2231,9 @@ function matchParts(pat, pi, id, ii) {
2058
2231
  while (pi < pat.length && pat[pi] === "**") pi++;
2059
2232
  return pi === pat.length && ii === id.length;
2060
2233
  }
2061
- function detectConventionDirs(rootDir) {
2234
+ function detectConventionDirs(rootDir, extraConventionDirs = []) {
2062
2235
  const result = /* @__PURE__ */ new Map();
2236
+ const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
2063
2237
  const searchDirs = [
2064
2238
  rootDir,
2065
2239
  (0, import_node_path11.join)(rootDir, "src"),
@@ -2067,7 +2241,7 @@ function detectConventionDirs(rootDir) {
2067
2241
  (0, import_node_path11.join)(rootDir, "lib")
2068
2242
  ];
2069
2243
  for (const base of searchDirs) {
2070
- for (const convention of CONVENTION_DIRS) {
2244
+ for (const convention of conventionDirs) {
2071
2245
  const dir = (0, import_node_path11.join)(base, convention);
2072
2246
  if (!(0, import_node_fs10.existsSync)(dir)) continue;
2073
2247
  try {
@@ -2120,9 +2294,13 @@ function isTrivialGroup(name, extraTrivial) {
2120
2294
  function normalizeGroupName(name) {
2121
2295
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
2122
2296
  }
2123
- function extractModuleFromPath(id, extraTrivial) {
2297
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
2124
2298
  const segments = id.split("/");
2125
2299
  const routeGroups = extractRouteGroups(id);
2300
+ const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
2301
+ if (extraSkipSegments) {
2302
+ for (const s of extraSkipSegments) skipSegments.add(s);
2303
+ }
2126
2304
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
2127
2305
  if (moduleGroups.length > 0) {
2128
2306
  return moduleGroups[moduleGroups.length - 1];
@@ -2133,7 +2311,7 @@ function extractModuleFromPath(id, extraTrivial) {
2133
2311
  if (isRouteGroup(seg)) continue;
2134
2312
  if (isDynamicSegment(seg)) continue;
2135
2313
  if (isDomainDir(seg)) continue;
2136
- if (SKIP_SEGMENTS.has(seg)) continue;
2314
+ if (skipSegments.has(seg)) continue;
2137
2315
  meaningful.push(seg);
2138
2316
  }
2139
2317
  if (meaningful.length > 0) {
@@ -2141,14 +2319,83 @@ function extractModuleFromPath(id, extraTrivial) {
2141
2319
  }
2142
2320
  return "root";
2143
2321
  }
2144
- var import_node_fs10, import_node_path11, CONVENTION_DIRS, SKIP_SEGMENTS, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
2322
+ var import_node_fs10, import_node_path11, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
2145
2323
  var init_module_tagger = __esm({
2146
2324
  "src/server/graph/taggers/module-tagger.ts"() {
2147
2325
  "use strict";
2148
2326
  import_node_fs10 = require("node:fs");
2149
2327
  import_node_path11 = require("node:path");
2150
- CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
2151
- SKIP_SEGMENTS = /* @__PURE__ */ new Set([
2328
+ CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
2329
+ GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
2330
+ // JS/TS
2331
+ "components",
2332
+ "hooks",
2333
+ "pages",
2334
+ "views",
2335
+ "screens",
2336
+ "layouts",
2337
+ "utils",
2338
+ "helpers",
2339
+ "lib",
2340
+ "libs",
2341
+ "services",
2342
+ "api",
2343
+ "apis",
2344
+ "stores",
2345
+ "state",
2346
+ "store",
2347
+ "context",
2348
+ "contexts",
2349
+ "providers",
2350
+ "types",
2351
+ "interfaces",
2352
+ "models",
2353
+ "schemas",
2354
+ "constants",
2355
+ "config",
2356
+ "configs",
2357
+ "assets",
2358
+ "styles",
2359
+ "public",
2360
+ "middleware",
2361
+ "middlewares",
2362
+ "routes",
2363
+ "router",
2364
+ "tests",
2365
+ "test",
2366
+ "__tests__",
2367
+ "spec",
2368
+ "specs",
2369
+ // Go
2370
+ "cmd",
2371
+ "pkg",
2372
+ "internal",
2373
+ // Python
2374
+ "management",
2375
+ "migrations",
2376
+ "templatetags",
2377
+ "templates",
2378
+ // Java
2379
+ "controller",
2380
+ "controllers",
2381
+ "service",
2382
+ "repository",
2383
+ "repositories",
2384
+ "entity",
2385
+ "entities",
2386
+ "dto",
2387
+ "dtos",
2388
+ // General
2389
+ "shared",
2390
+ "common",
2391
+ "core",
2392
+ "base",
2393
+ "app",
2394
+ // Next.js specific
2395
+ "client",
2396
+ "server"
2397
+ ]);
2398
+ SKIP_SEGMENTS_BUILTIN = /* @__PURE__ */ new Set([
2152
2399
  "src",
2153
2400
  "app",
2154
2401
  "client",
@@ -2188,12 +2435,10 @@ var init_module_tagger = __esm({
2188
2435
  layers: null,
2189
2436
  // applies to all layers
2190
2437
  tag(nodes, layer, rootDir) {
2191
- if (cachedRootDir !== rootDir) {
2192
- cachedConventionDirs = detectConventionDirs(rootDir);
2193
- cachedRootDir = rootDir;
2194
- }
2195
2438
  let configRules = [];
2196
2439
  let extraTrivial;
2440
+ let extraSkipSegments;
2441
+ let extraConventionDirs = [];
2197
2442
  try {
2198
2443
  const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
2199
2444
  const config = loadConfig2(rootDir);
@@ -2202,8 +2447,21 @@ var init_module_tagger = __esm({
2202
2447
  if (trivialFromConfig?.length) {
2203
2448
  extraTrivial = new Set(trivialFromConfig);
2204
2449
  }
2450
+ const skipFromConfig = config.taggers?.module?.skipSegments;
2451
+ if (skipFromConfig?.length) {
2452
+ extraSkipSegments = new Set(skipFromConfig);
2453
+ }
2454
+ extraConventionDirs = config.taggers?.module?.conventionDirs ?? [];
2455
+ const roleNamesFromConfig = config.taggers?.module?.genericRoleNames;
2456
+ if (roleNamesFromConfig?.length) {
2457
+ for (const name of roleNamesFromConfig) GENERIC_ROLE_NAMES_BUILTIN.add(name);
2458
+ }
2205
2459
  } catch {
2206
2460
  }
2461
+ if (cachedRootDir !== rootDir) {
2462
+ cachedConventionDirs = detectConventionDirs(rootDir, extraConventionDirs);
2463
+ cachedRootDir = rootDir;
2464
+ }
2207
2465
  const result = /* @__PURE__ */ new Map();
2208
2466
  for (const node of nodes) {
2209
2467
  const id = node.id;
@@ -2229,7 +2487,7 @@ var init_module_tagger = __esm({
2229
2487
  }
2230
2488
  }
2231
2489
  if (matched) continue;
2232
- const module2 = extractModuleFromPath(id, extraTrivial);
2490
+ const module2 = extractModuleFromPath(id, extraTrivial, extraSkipSegments);
2233
2491
  result.set(id, module2);
2234
2492
  }
2235
2493
  return result;
@@ -2476,7 +2734,13 @@ function readAllGraphs(rootDir) {
2476
2734
  }
2477
2735
  return result;
2478
2736
  }
2479
- function generateGraph(rootDir, layer) {
2737
+ async function generateGraph(rootDir, layer) {
2738
+ await initTreeSitter();
2739
+ const config = loadConfig(rootDir);
2740
+ setExtractorConfig({
2741
+ dbIdentifiers: config.parsers?.patterns?.dbIdentifiers,
2742
+ mutationMethods: config.parsers?.patterns?.mutationMethods
2743
+ });
2480
2744
  const dir = graphsDir(rootDir);
2481
2745
  (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
2482
2746
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
@@ -2498,6 +2762,7 @@ var init_graph = __esm({
2498
2762
  init_config();
2499
2763
  init_tagger_registry();
2500
2764
  init_tag_store();
2765
+ init_ts_extractor();
2501
2766
  init_tag_store();
2502
2767
  GRAPHS_DIR2 = ".launchsecure/graphs";
2503
2768
  LAYERS = ["ui", "api", "db"];
@@ -2533,10 +2798,10 @@ function findProjectRoot(startDir) {
2533
2798
  }
2534
2799
  return startDir;
2535
2800
  }
2536
- function buildMergedGraph(projectRoot) {
2801
+ async function buildMergedGraph(projectRoot) {
2537
2802
  let graphs = readAllGraphs(projectRoot);
2538
2803
  if (!graphs.ui && !graphs.api && !graphs.db) {
2539
- generateGraph(projectRoot);
2804
+ await generateGraph(projectRoot);
2540
2805
  graphs = readAllGraphs(projectRoot);
2541
2806
  }
2542
2807
  const nodes = [];
@@ -2646,13 +2911,18 @@ async function startChartServer(opts = {}) {
2646
2911
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
2647
2912
  if (req.method === "GET" && url2.pathname === "/api/project-graph") {
2648
2913
  const regenerate = url2.searchParams.get("regenerate") === "1";
2649
- if (regenerate) generateGraph(projectRoot);
2650
- const merged = buildMergedGraph(projectRoot);
2651
- res.writeHead(200, { "Content-Type": "application/json" });
2652
- res.end(JSON.stringify({
2653
- ...merged,
2654
- debug: { cwd, projectRoot }
2655
- }));
2914
+ (async () => {
2915
+ if (regenerate) await generateGraph(projectRoot);
2916
+ const merged = await buildMergedGraph(projectRoot);
2917
+ res.writeHead(200, { "Content-Type": "application/json" });
2918
+ res.end(JSON.stringify({
2919
+ ...merged,
2920
+ debug: { cwd, projectRoot }
2921
+ }));
2922
+ })().catch((e) => {
2923
+ res.writeHead(500);
2924
+ res.end(String(e));
2925
+ });
2656
2926
  return;
2657
2927
  }
2658
2928
  if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
@@ -2662,8 +2932,8 @@ async function startChartServer(opts = {}) {
2662
2932
  return;
2663
2933
  }
2664
2934
  if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
2665
- try {
2666
- generateGraph(projectRoot);
2935
+ (async () => {
2936
+ await generateGraph(projectRoot);
2667
2937
  const graphs = readAllGraphs(projectRoot);
2668
2938
  res.writeHead(200, { "Content-Type": "application/json" });
2669
2939
  res.end(JSON.stringify({
@@ -2672,10 +2942,10 @@ async function startChartServer(opts = {}) {
2672
2942
  api: graphs.api ?? null,
2673
2943
  db: graphs.db ?? null
2674
2944
  }));
2675
- } catch (err2) {
2945
+ })().catch((err2) => {
2676
2946
  res.writeHead(500, { "Content-Type": "application/json" });
2677
2947
  res.end(JSON.stringify({ ok: false, error: String(err2) }));
2678
- }
2948
+ });
2679
2949
  return;
2680
2950
  }
2681
2951
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
@@ -2941,7 +3211,7 @@ function toCompactNode(n) {
2941
3211
  if (n.columns != null) out.c = n.columns;
2942
3212
  if (tags != null) out.tg = tags;
2943
3213
  for (const k of Object.keys(n)) {
2944
- if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
3214
+ if (!COMPACT_NODE_KNOWN_KEYS.has(k) && !DEEP_FIELDS.has(k) && n[k] != null) out[k] = n[k];
2945
3215
  }
2946
3216
  return out;
2947
3217
  }
@@ -3041,13 +3311,13 @@ function okJson(data) {
3041
3311
  function err(text) {
3042
3312
  return { content: [{ type: "text", text }], isError: true };
3043
3313
  }
3044
- function handleGenerateGraph(args) {
3314
+ async function handleGenerateGraph(args) {
3045
3315
  const rootDir = process.cwd();
3046
3316
  const layer = args.layer;
3047
3317
  if (layer && !["ui", "api", "db"].includes(layer)) {
3048
3318
  return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
3049
3319
  }
3050
- const results = generateGraph(rootDir, layer);
3320
+ const results = await generateGraph(rootDir, layer);
3051
3321
  if (results.length === 0) {
3052
3322
  return err(
3053
3323
  layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
@@ -3080,6 +3350,8 @@ function runReadGraphQueryRaw(rootDir, args) {
3080
3350
  const layerIsDb = args.layer === "db";
3081
3351
  const minimal = args.minimal ?? layerIsDb;
3082
3352
  const includeEdges = args.include_edges;
3353
+ const offset = args.offset ?? 0;
3354
+ const limit = args.limit;
3083
3355
  const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
3084
3356
  if (layer && !["ui", "api", "db"].includes(layer)) {
3085
3357
  return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
@@ -3151,18 +3423,29 @@ function runReadGraphQueryRaw(rootDir, args) {
3151
3423
  hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
3152
3424
  };
3153
3425
  }
3426
+ const totalMatched = matched.length;
3427
+ const paginatedNodes = limit != null ? matched.slice(offset, offset + limit) : matched.slice(offset);
3428
+ const hasMore = offset + paginatedNodes.length < totalMatched;
3154
3429
  const wantEdges = includeEdges ?? false;
3430
+ const returnedIds = new Set(paginatedNodes.map((n) => n.id));
3431
+ const returnedEdges = graph.edges.filter((e) => returnedIds.has(e.source) && returnedIds.has(e.target));
3155
3432
  const result = {
3156
3433
  layer,
3157
3434
  filter: { search, type, module: module_ },
3158
- matched: matched.length,
3159
- edge_count: matchedEdges.length,
3160
- nodes: minimal ? toMinimal(matched) : matched
3435
+ total: totalMatched,
3436
+ returned: paginatedNodes.length,
3437
+ offset,
3438
+ has_more: hasMore,
3439
+ edge_count: returnedEdges.length,
3440
+ nodes: minimal ? toMinimal(paginatedNodes) : paginatedNodes
3161
3441
  };
3442
+ if (hasMore) {
3443
+ result.next_offset = offset + paginatedNodes.length;
3444
+ }
3162
3445
  if (wantEdges) {
3163
- result.edges = matchedEdges;
3164
- } else {
3165
- result.edges_hint = `${matchedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
3446
+ result.edges = returnedEdges;
3447
+ } else if (returnedEdges.length > 0) {
3448
+ result.edges_hint = `${returnedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
3166
3449
  }
3167
3450
  return result;
3168
3451
  }
@@ -3230,6 +3513,48 @@ function nodeToFilePath(rootDir, layer, nodeId) {
3230
3513
  if (layer === "db") return (0, import_node_path16.join)(rootDir, "prisma", "schema.prisma");
3231
3514
  return null;
3232
3515
  }
3516
+ function handleInspectNode(args) {
3517
+ const rootDir = process.cwd();
3518
+ const layer = args.layer;
3519
+ const nodeId = args.node_id;
3520
+ const search = args.search;
3521
+ const fields = args.fields;
3522
+ if (!layer) return err("layer is required.");
3523
+ if (!nodeId && !search) return err("Either node_id or search is required.");
3524
+ const graph = readGraph(rootDir, layer);
3525
+ if (!graph) return err(`No graph found for layer "${layer}". Run generate_graph first.`);
3526
+ let matched;
3527
+ if (nodeId) {
3528
+ const node = graph.nodes.find((n) => n.id === nodeId);
3529
+ if (!node) return err(`Node "${nodeId}" not found in ${layer} layer.`);
3530
+ matched = [node];
3531
+ } else {
3532
+ const searchLower = search.toLowerCase();
3533
+ matched = graph.nodes.filter(
3534
+ (n) => n.id.toLowerCase().includes(searchLower) || n.name.toLowerCase().includes(searchLower) || n.route?.toLowerCase().includes(searchLower)
3535
+ );
3536
+ }
3537
+ if (matched.length === 0) return err(`No nodes matching "${search}" in ${layer} layer.`);
3538
+ if (matched.length > 5) {
3539
+ return err(`${matched.length} nodes match "${search}". Narrow your search (max 5 for inspect_node).`);
3540
+ }
3541
+ const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
3542
+ const requestedFields = fields ?? allDeepFields;
3543
+ const results = matched.map((node) => {
3544
+ const deep = { id: node.id, name: node.name, type: node.type };
3545
+ for (const field of requestedFields) {
3546
+ if (allDeepFields.includes(field) && node[field] != null) {
3547
+ deep[field] = node[field];
3548
+ }
3549
+ }
3550
+ return deep;
3551
+ });
3552
+ return okJson({
3553
+ layer,
3554
+ matched: results.length,
3555
+ nodes: results
3556
+ });
3557
+ }
3233
3558
  function handleGrepNodes(args) {
3234
3559
  const rootDir = process.cwd();
3235
3560
  const pattern = args.pattern;
@@ -3492,7 +3817,7 @@ function respond(id, result) {
3492
3817
  function respondError(id, code, message) {
3493
3818
  send({ jsonrpc: "2.0", id, error: { code, message } });
3494
3819
  }
3495
- function handleMessage(msg) {
3820
+ async function handleMessage(msg) {
3496
3821
  const method = msg.method;
3497
3822
  const id = msg.id;
3498
3823
  if (method === "initialize") {
@@ -3515,7 +3840,7 @@ function handleMessage(msg) {
3515
3840
  const toolName = params.name;
3516
3841
  const args = params.arguments ?? {};
3517
3842
  if (toolName === "generate_graph") {
3518
- respond(id ?? null, handleGenerateGraph(args));
3843
+ respond(id ?? null, await handleGenerateGraph(args));
3519
3844
  return;
3520
3845
  }
3521
3846
  if (toolName === "read_graph") {
@@ -3526,6 +3851,10 @@ function handleMessage(msg) {
3526
3851
  respond(id ?? null, handleGrepNodes(args));
3527
3852
  return;
3528
3853
  }
3854
+ if (toolName === "inspect_node") {
3855
+ respond(id ?? null, handleInspectNode(args));
3856
+ return;
3857
+ }
3529
3858
  if (toolName === "chart_server_status") {
3530
3859
  respond(id ?? null, handleChartServerStatus());
3531
3860
  return;
@@ -3585,7 +3914,7 @@ function startGraphMcpServer() {
3585
3914
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
3586
3915
  `);
3587
3916
  }
3588
- var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
3917
+ var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
3589
3918
  var init_graph_mcp = __esm({
3590
3919
  "src/server/graph-mcp.ts"() {
3591
3920
  "use strict";
@@ -3620,7 +3949,7 @@ var init_graph_mcp = __esm({
3620
3949
  },
3621
3950
  {
3622
3951
  name: "read_graph",
3623
- description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
3952
+ description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
3624
3953
  inputSchema: {
3625
3954
  type: "object",
3626
3955
  properties: {
@@ -3665,6 +3994,14 @@ var init_graph_mcp = __esm({
3665
3994
  type: "boolean",
3666
3995
  description: "Include the edge list in the response. Default TRUE for neighborhood queries (node_id), FALSE for filter queries. Filter responses always include edge_count. Only set true on filter queries when you actually need edge data."
3667
3996
  },
3997
+ offset: {
3998
+ type: "number",
3999
+ description: "Skip first N matched nodes (pagination). Default 0. Use next_offset from a previous response to get the next page."
4000
+ },
4001
+ limit: {
4002
+ type: "number",
4003
+ description: "Max nodes to return. Default: all matched nodes. Use with offset for pagination."
4004
+ },
3668
4005
  queries: {
3669
4006
  type: "array",
3670
4007
  description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
@@ -3733,6 +4070,40 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
3733
4070
  required: ["layer", "pattern"]
3734
4071
  }
3735
4072
  },
4073
+ {
4074
+ name: "inspect_node",
4075
+ description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, and request params.
4076
+
4077
+ USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?"
4078
+
4079
+ DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
4080
+
4081
+ Returns deep fields only \u2014 not structural metadata (use read_graph for that).`,
4082
+ inputSchema: {
4083
+ type: "object",
4084
+ properties: {
4085
+ layer: {
4086
+ type: "string",
4087
+ enum: ["ui", "api", "db"],
4088
+ description: "Graph layer (required)."
4089
+ },
4090
+ node_id: {
4091
+ type: "string",
4092
+ description: "Node ID to inspect. Use read_graph to find node IDs first."
4093
+ },
4094
+ search: {
4095
+ type: "string",
4096
+ description: "Substring match on node id/name/route. Returns deep data for all matching nodes (max 5)."
4097
+ },
4098
+ fields: {
4099
+ type: "array",
4100
+ items: { type: "string" },
4101
+ description: "Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params. Omit for all."
4102
+ }
4103
+ },
4104
+ required: ["layer"]
4105
+ }
4106
+ },
3736
4107
  {
3737
4108
  name: "chart_server_status",
3738
4109
  description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
@@ -3844,6 +4215,14 @@ Use this when the user asks "is the chart running", "show me the project graph U
3844
4215
  "columns",
3845
4216
  "tags"
3846
4217
  ]);
4218
+ DEEP_FIELDS = /* @__PURE__ */ new Set([
4219
+ "elements",
4220
+ "stateVars",
4221
+ "conditions",
4222
+ "variables",
4223
+ "responses",
4224
+ "params"
4225
+ ]);
3847
4226
  EST_CHARS_PER_NODE_FULL = {
3848
4227
  ui: 300,
3849
4228
  api: 300,