project-context-ai 2.2.6 → 2.3.0

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 (39) hide show
  1. package/dist/generators/template-sections.d.ts +8 -0
  2. package/dist/generators/template-sections.d.ts.map +1 -0
  3. package/dist/generators/template-sections.js +261 -0
  4. package/dist/generators/template-sections.js.map +1 -0
  5. package/dist/generators/template.d.ts +1 -6
  6. package/dist/generators/template.d.ts.map +1 -1
  7. package/dist/generators/template.js +2 -280
  8. package/dist/generators/template.js.map +1 -1
  9. package/dist/scanner/business.d.ts.map +1 -1
  10. package/dist/scanner/business.js +7 -752
  11. package/dist/scanner/business.js.map +1 -1
  12. package/dist/scanner/conventions.js +25 -6
  13. package/dist/scanner/conventions.js.map +1 -1
  14. package/dist/scanner/dependencies.d.ts.map +1 -1
  15. package/dist/scanner/dependencies.js +30 -0
  16. package/dist/scanner/dependencies.js.map +1 -1
  17. package/dist/scanner/description.d.ts +7 -0
  18. package/dist/scanner/description.d.ts.map +1 -0
  19. package/dist/scanner/description.js +172 -0
  20. package/dist/scanner/description.js.map +1 -0
  21. package/dist/scanner/domain.d.ts +10 -0
  22. package/dist/scanner/domain.d.ts.map +1 -0
  23. package/dist/scanner/domain.js +323 -0
  24. package/dist/scanner/domain.js.map +1 -0
  25. package/dist/scanner/frameworks.d.ts.map +1 -1
  26. package/dist/scanner/frameworks.js +13 -0
  27. package/dist/scanner/frameworks.js.map +1 -1
  28. package/dist/scanner/patterns-code.d.ts +9 -0
  29. package/dist/scanner/patterns-code.d.ts.map +1 -0
  30. package/dist/scanner/patterns-code.js +283 -0
  31. package/dist/scanner/patterns-code.js.map +1 -0
  32. package/dist/scanner/patterns.d.ts.map +1 -1
  33. package/dist/scanner/patterns.js +2 -288
  34. package/dist/scanner/patterns.js.map +1 -1
  35. package/dist/scanner/skills.d.ts +5 -0
  36. package/dist/scanner/skills.d.ts.map +1 -0
  37. package/dist/scanner/skills.js +242 -0
  38. package/dist/scanner/skills.js.map +1 -0
  39. package/package.json +1 -1
@@ -1,6 +1,10 @@
1
+ // ─── Business Context Scanner — Orchestrator ────────────
1
2
  import { join } from "node:path";
2
- import { readTextFile, readFilesMatching } from "../utils/files.js";
3
+ import { readTextFile } from "../utils/files.js";
3
4
  import { getScanConfig } from "./config.js";
5
+ import { humanizeName, findFile } from "./description.js";
6
+ import { detectSkills, scanHandlers, scanFrontend } from "./skills.js";
7
+ import { scanValidation, detectAuthPatterns, extractDomainEntities, extractEntityGroups, extractKeyModules, extractDirectoryMap, detectEntryPointDescriptions, inferDataFlow, } from "./domain.js";
4
8
  // Doc files to look for (in priority order)
5
9
  const DOC_FILES = [
6
10
  "README.md",
@@ -46,14 +50,12 @@ export async function scanBusiness(rootPath, allFiles, deps, frameworks, routes,
46
50
  }
47
51
  // ─── Description ──────────────────────────────────────────
48
52
  async function extractDescription(rootPath, allFiles) {
49
- // Try README.md first
50
53
  const readmeFile = findFile(allFiles, "readme.md");
51
54
  if (readmeFile) {
52
55
  const desc = await extractFirstParagraph(rootPath, readmeFile);
53
56
  if (desc && desc.length > 20)
54
57
  return desc;
55
58
  }
56
- // Try PRODUCT_VISION.md or similar
57
59
  for (const docName of ["PRODUCT_VISION.md", "ABOUT.md", "OVERVIEW.md"]) {
58
60
  const docFile = findFile(allFiles, docName.toLowerCase());
59
61
  if (docFile) {
@@ -62,7 +64,6 @@ async function extractDescription(rootPath, allFiles) {
62
64
  return desc;
63
65
  }
64
66
  }
65
- // Fallback: package.json description
66
67
  const pkgFile = allFiles.find((f) => f === "package.json");
67
68
  if (pkgFile) {
68
69
  const content = await readTextFile(join(rootPath, pkgFile));
@@ -201,7 +202,6 @@ function extractAllBullets(content, max) {
201
202
  let pendingHeading = null;
202
203
  for (const line of lines) {
203
204
  const trimmed = line.trim();
204
- // Capture ## headings — but only add if followed by actual content
205
205
  if (trimmed.startsWith("## ")) {
206
206
  const heading = trimmed.replace(/^##\s+/, "").replace(/\*\*/g, "").trim();
207
207
  if (heading && !heading.match(/^(Table of Contents|TOC|Index|Sumário)/i)) {
@@ -209,12 +209,10 @@ function extractAllBullets(content, max) {
209
209
  }
210
210
  continue;
211
211
  }
212
- // Capture bullet points
213
212
  const bulletMatch = trimmed.match(/^[-*+]\s+(.+)/);
214
213
  if (bulletMatch) {
215
214
  const text = bulletMatch[1].replace(/\*\*/g, "").replace(/`/g, "").trim();
216
215
  if (text.length > 3 && text.length < 200) {
217
- // Emit pending heading only if there's content after it
218
216
  if (pendingHeading) {
219
217
  bullets.push(`[${pendingHeading}]`);
220
218
  pendingHeading = null;
@@ -227,171 +225,12 @@ function extractAllBullets(content, max) {
227
225
  }
228
226
  return bullets;
229
227
  }
230
- // ─── Skills / Plugins Detection ───────────────────────────
231
- async function detectSkills(rootPath, allFiles) {
232
- const skills = [];
233
- // Only files named *-skill.ts or in skills/plugins directories (NOT handlers — those are separate)
234
- const skillFiles = allFiles.filter((f) => (f.match(/[-_](skill|plugin)\.(ts|js)$/i) ||
235
- f.match(/(Skill|Plugin)\.(ts|js)$/i) ||
236
- f.match(/(skills|plugins)\//)) &&
237
- f.match(/\.(ts|js)$/) &&
238
- !f.includes("index.") && !f.includes(".test.") && !f.includes(".spec.") &&
239
- !f.includes("handlers/"));
240
- if (skillFiles.length === 0)
241
- return [];
242
- const cfg = getScanConfig();
243
- const files = await readFilesMatching(rootPath, skillFiles, { maxFiles: cfg.maxFiles, maxFileSize: cfg.maxFileSize });
244
- // Build content map for companion file lookups
245
- const contentMap = new Map();
246
- for (const { file, content } of files)
247
- contentMap.set(file, content);
248
- for (const { file, content } of files) {
249
- const skill = extractSkillInfo(file, content);
250
- if (!skill)
251
- continue;
252
- // If no tools found, look for companion tools file (e.g. browser-skill.ts → browser-tools.ts)
253
- if (!skill.tools || skill.tools.length === 0) {
254
- const dir = file.split("/").slice(0, -1).join("/");
255
- const baseName = file.split("/").pop()?.replace(/[-_]skill\.(ts|js)$/i, "") || "";
256
- const companionPatterns = [
257
- `${dir}/${baseName}-tools.ts`,
258
- `${dir}/${baseName}Tools.ts`,
259
- `${dir}/${baseName}-tool.ts`,
260
- `${dir}/${baseName}.tools.ts`,
261
- `${dir}/tools.ts`,
262
- `${dir}/${baseName}-actions.ts`,
263
- ];
264
- for (const companion of companionPatterns) {
265
- const companionContent = contentMap.get(companion);
266
- if (companionContent) {
267
- const companionTools = [];
268
- // Broad tool name extraction from companion files
269
- const patterns = [
270
- /name\s*:\s*['"`]([a-z][a-z0-9_-]+)['"`]/g,
271
- /registerTool\s*\(\s*['"`]([^'"`]+)['"`]/g,
272
- /\.tool\s*\(\s*['"`]([a-z][a-z0-9_-]+)['"`]/g,
273
- /(?:create|define|add)Tool\s*\(\s*['"`]([^'"`]+)['"`]/g,
274
- ];
275
- for (const pattern of patterns) {
276
- let m;
277
- while ((m = pattern.exec(companionContent)) !== null) {
278
- if (!companionTools.includes(m[1])) {
279
- companionTools.push(m[1]);
280
- }
281
- }
282
- }
283
- if (companionTools.length > 0) {
284
- skill.tools = companionTools.slice(0, 20);
285
- skill.toolCount = companionTools.length;
286
- }
287
- break;
288
- }
289
- }
290
- }
291
- skills.push(skill);
292
- }
293
- return skills.slice(0, 50);
294
- }
295
- function extractSkillInfo(file, content) {
296
- // ONLY extract classes that implement an interface (actual skills, not random exports)
297
- const classMatch = content.match(/export\s+class\s+(\w+)\s+(?:extends\s+\w+\s+)?implements\s+(\w+)/);
298
- if (!classMatch) {
299
- // Fallback: only accept classes with "Skill" or "Plugin" in the name
300
- const namedMatch = content.match(/export\s+class\s+(\w+(?:Skill|Plugin))\b/);
301
- if (!namedMatch)
302
- return null;
303
- const name = namedMatch[1];
304
- return extractSkillData(name, undefined, file, content);
305
- }
306
- return extractSkillData(classMatch[1], classMatch[2], file, content);
307
- }
308
- function extractSkillData(name, iface, file, content) {
309
- // Extract JSDoc description (first comment block before class)
310
- let description;
311
- const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\/\s*(?:export\s+)?class/);
312
- if (jsdocMatch) {
313
- description = jsdocMatch[1].replace(/\s*\*\s*/g, " ").replace(/@\w+.*/g, "").trim();
314
- }
315
- // Fallback: infer description from class name (fix spacing for acronyms like SEO, API)
316
- if (!description || description.length < 5) {
317
- const cleanName = name.replace(/Skill$|Plugin$|Handler$|Command$/, "");
318
- description = humanizeName(cleanName);
319
- }
320
- // Truncate JSDoc descriptions cleanly (don't cut mid-word)
321
- if (description && description.length > 350) {
322
- const cut = description.lastIndexOf(" ", 350);
323
- description = description.slice(0, cut > 150 ? cut : 350);
324
- }
325
- // Extract tool names — multiple strategies combined
326
- const tools = [];
327
- const seenTools = new Set();
328
- function addTool(t) {
329
- if (t.length > 2 && !seenTools.has(t) && tools.length < 20) {
330
- seenTools.add(t);
331
- tools.push(t);
332
- }
333
- }
334
- // Strategy 1: registerTool("name", ...)
335
- const toolRegex = /registerTool\s*\(\s*['"`]([^'"`]+)['"`]/g;
336
- let toolMatch;
337
- while ((toolMatch = toolRegex.exec(content)) !== null)
338
- addTool(toolMatch[1]);
339
- // Strategy 2: name: "tool_name" inside any object (tool definitions)
340
- const nameRegex = /name\s*:\s*['"`]([a-z][a-z0-9_]+)['"`]/g;
341
- let nameMatch;
342
- while ((nameMatch = nameRegex.exec(content)) !== null) {
343
- const tn = nameMatch[1];
344
- if (tn.includes("_"))
345
- addTool(tn);
346
- }
347
- // Strategy 3: .tool("name", ...) — MCP/SDK-style chaining
348
- const chainToolRegex = /\.tool\s*\(\s*['"`]([a-z][a-z0-9_-]+)['"`]/g;
349
- let chainMatch;
350
- while ((chainMatch = chainToolRegex.exec(content)) !== null)
351
- addTool(chainMatch[1]);
352
- // Strategy 4: createTool/defineTool/addTool("name", ...)
353
- const factoryRegex = /(?:create|define|add|register)(?:Tool|Action)\s*\(\s*['"`]([^'"`]+)['"`]/g;
354
- let factoryMatch;
355
- while ((factoryMatch = factoryRegex.exec(content)) !== null)
356
- addTool(factoryMatch[1]);
357
- // Strategy 5: @Tool("name") or @tool("name") — decorator pattern
358
- const decoratorRegex = /@(?:Tool|tool|Action|action)\s*\(\s*['"`]([^'"`]+)['"`]/g;
359
- let decoratorMatch;
360
- while ((decoratorMatch = decoratorRegex.exec(content)) !== null)
361
- addTool(decoratorMatch[1]);
362
- // Strategy 6: name: "tool-name" with hyphens (relaxed) — only if near description/inputSchema
363
- const relaxedNameRegex = /name\s*:\s*['"`]([a-z][a-z0-9_-]+)['"`]/g;
364
- let relaxedMatch;
365
- const contentLines = content.split("\n");
366
- while ((relaxedMatch = relaxedNameRegex.exec(content)) !== null) {
367
- const tn = relaxedMatch[1];
368
- if (seenTools.has(tn))
369
- continue;
370
- // Check surrounding context (5 lines) for tool-definition keywords
371
- const lineIdx = content.slice(0, relaxedMatch.index).split("\n").length - 1;
372
- const nearby = contentLines.slice(Math.max(0, lineIdx - 3), lineIdx + 5).join("\n");
373
- if (/description\s*:|inputSchema|handler\s*:|execute\s*\(|parameters\s*:/.test(nearby)) {
374
- addTool(tn);
375
- }
376
- }
377
- return {
378
- name,
379
- file,
380
- interface: iface,
381
- description: description || undefined,
382
- tools: tools.length > 0 ? tools : undefined,
383
- toolCount: tools.length > 0 ? tools.length : undefined,
384
- };
385
- }
386
228
  // ─── Documentation Summaries ──────────────────────────────
387
229
  async function extractDocumentation(rootPath, allFiles) {
388
230
  const docs = [];
389
- const allFilesLower = allFiles.map((f) => f.toLowerCase());
390
- // Find all .md files at root level and in docs/ folder
391
231
  const mdFiles = allFiles.filter((f) => f.match(/\.md$/i) &&
392
- (!f.includes("/") || // root level
393
- f.startsWith("docs/") // docs folder
394
- ) &&
232
+ (!f.includes("/") ||
233
+ f.startsWith("docs/")) &&
395
234
  !f.match(/^(CLAUDE|AGENTS|LICENSE|CHANGELOG|HISTORY)\.md$/i) &&
396
235
  !f.includes("node_modules") &&
397
236
  !f.includes(".context-ai"));
@@ -400,10 +239,8 @@ async function extractDocumentation(rootPath, allFiles) {
400
239
  const content = await readTextFile(join(rootPath, file));
401
240
  if (!content || content.length < 50)
402
241
  continue;
403
- // Extract title (first # heading)
404
242
  const titleMatch = content.match(/^#\s+(.+)$/m);
405
243
  const title = titleMatch ? titleMatch[1].trim() : file;
406
- // Extract summary: headings list gives a good overview
407
244
  const headings = [];
408
245
  for (const line of content.split("\n")) {
409
246
  if (line.startsWith("## ")) {
@@ -444,586 +281,4 @@ async function extractDocumentation(rootPath, allFiles) {
444
281
  }
445
282
  return docs;
446
283
  }
447
- // ─── Validation Rules ─────────────────────────────────────
448
- async function scanValidation(rootPath, allFiles, deps) {
449
- const depNames = deps.map((d) => d.name);
450
- const rules = [];
451
- const hasZod = depNames.includes("zod");
452
- const hasYup = depNames.includes("yup");
453
- const hasJoi = depNames.includes("joi") || depNames.includes("@hapi/joi");
454
- if (!hasZod && !hasYup && !hasJoi)
455
- return [];
456
- const cfg = getScanConfig();
457
- const codeFiles = await readFilesMatching(rootPath, allFiles, {
458
- extensions: [".ts", ".tsx", ".js", ".jsx"],
459
- maxFiles: cfg.maxFiles,
460
- maxFileSize: cfg.maxFileSize,
461
- pathPatterns: [/(schema|validation|validator|types|dto|input|form)/i],
462
- });
463
- for (const { file, content } of codeFiles) {
464
- if (hasZod) {
465
- const zodRegex = /(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*z\.object\s*\(\s*\{([^}]*)\}/g;
466
- let match;
467
- while ((match = zodRegex.exec(content)) !== null) {
468
- const fields = extractObjectKeys(match[2]);
469
- rules.push({ schema: match[1], file, library: "zod", fields: fields.slice(0, 10) });
470
- }
471
- }
472
- if (hasYup) {
473
- const yupRegex = /(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:yup|Yup)\.object\s*\(\s*\{([^}]*)\}/g;
474
- let match;
475
- while ((match = yupRegex.exec(content)) !== null) {
476
- const fields = extractObjectKeys(match[2]);
477
- rules.push({ schema: match[1], file, library: "yup", fields: fields.slice(0, 10) });
478
- }
479
- }
480
- if (hasJoi) {
481
- const joiRegex = /(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*Joi\.object\s*\(\s*\{([^}]*)\}/g;
482
- let match;
483
- while ((match = joiRegex.exec(content)) !== null) {
484
- const fields = extractObjectKeys(match[2]);
485
- rules.push({ schema: match[1], file, library: "joi", fields: fields.slice(0, 10) });
486
- }
487
- }
488
- if (rules.length >= 30)
489
- break;
490
- }
491
- return rules;
492
- }
493
- function extractObjectKeys(objectBody) {
494
- const keys = [];
495
- const keyRegex = /(\w+)\s*:/g;
496
- let match;
497
- while ((match = keyRegex.exec(objectBody)) !== null) {
498
- keys.push(match[1]);
499
- }
500
- return keys;
501
- }
502
- // ─── Auth Patterns ────────────────────────────────────────
503
- function detectAuthPatterns(allFiles, deps, frameworks, routes) {
504
- const patterns = [];
505
- const depNames = deps.map((d) => d.name);
506
- if (depNames.includes("next-auth") || depNames.includes("@auth/core")) {
507
- patterns.push("NextAuth.js / Auth.js session-based authentication");
508
- }
509
- if (depNames.some((d) => d.startsWith("@clerk/"))) {
510
- patterns.push("Clerk authentication and user management");
511
- }
512
- if (depNames.some((d) => d.startsWith("@auth0/"))) {
513
- patterns.push("Auth0 authentication");
514
- }
515
- if (depNames.includes("passport")) {
516
- patterns.push("Passport.js authentication strategies");
517
- }
518
- if (depNames.includes("jsonwebtoken") || depNames.includes("jose")) {
519
- patterns.push("JWT token-based authentication");
520
- }
521
- if (depNames.includes("bcrypt") || depNames.includes("bcryptjs") || depNames.includes("argon2")) {
522
- patterns.push("Password hashing (bcrypt/argon2)");
523
- }
524
- const hasAuthMiddleware = allFiles.some((f) => f.match(/(middleware|guard|auth)\.(ts|js)$/) && f.includes("auth"));
525
- if (hasAuthMiddleware)
526
- patterns.push("Custom auth middleware");
527
- const hasRBAC = allFiles.some((f) => f.match(/(role|permission|rbac|authorize)\.(ts|js)$/));
528
- if (hasRBAC)
529
- patterns.push("Role-based access control (RBAC)");
530
- if (routes && routes.some((r) => r.auth)) {
531
- const authRouteCount = routes.filter((r) => r.auth).length;
532
- patterns.push(`${authRouteCount}/${routes.length} routes require authentication`);
533
- }
534
- return patterns;
535
- }
536
- // ─── Domain Entities ──────────────────────────────────────
537
- async function extractDomainEntities(rootPath, allFiles, database) {
538
- const entities = new Set();
539
- if (database) {
540
- for (const model of database)
541
- entities.add(model.name);
542
- }
543
- const domainFiles = allFiles.filter((f) => f.match(/(domain|entities|models)\//i) &&
544
- f.match(/\.(ts|js)$/) &&
545
- !f.includes("index.") &&
546
- !f.includes(".test.") &&
547
- !f.includes(".spec."));
548
- const cfg = getScanConfig();
549
- const files = await readFilesMatching(rootPath, domainFiles, { maxFiles: cfg.maxFiles, maxFileSize: cfg.maxFileSize });
550
- for (const { content } of files) {
551
- const classRegex = /(?:export\s+)?(?:class|interface|type)\s+(\w+)/g;
552
- let match;
553
- while ((match = classRegex.exec(content)) !== null) {
554
- const name = match[1];
555
- if (!name.match(/^(I[A-Z]|Props|State|Config|Options|Params|Args|Result|Response|Request|Error|Exception|Helper|Util|Service|Repository|Controller|Module|Guard|Pipe|Filter|Interceptor|Decorator|Interface|Type|Enum|Dto|Input|Output)/)) {
556
- entities.add(name);
557
- }
558
- }
559
- }
560
- // Limit to 15 most important cross-boundary types
561
- return Array.from(entities).slice(0, 15);
562
- }
563
- // ─── Entity Groups (by file) ──────────────────────────────
564
- async function extractEntityGroups(rootPath, allFiles, database) {
565
- const groups = [];
566
- // From database models — group by file
567
- if (database) {
568
- const byFile = new Map();
569
- for (const m of database) {
570
- if (!byFile.has(m.file))
571
- byFile.set(m.file, []);
572
- byFile.get(m.file).push(m.name);
573
- }
574
- for (const [file, entities] of byFile) {
575
- groups.push({ file, entities });
576
- }
577
- }
578
- // From domain/entity/model dirs (NOT core — too noisy)
579
- const domainFiles = allFiles.filter((f) => f.match(/(domain|entities|models)\//i) &&
580
- f.match(/\.(ts|js)$/) &&
581
- !f.includes("index.") && !f.includes(".test.") && !f.includes(".spec."));
582
- const cfg = getScanConfig();
583
- const files = await readFilesMatching(rootPath, domainFiles, { maxFiles: cfg.maxFiles, maxFileSize: cfg.maxFileSize });
584
- for (const { file, content } of files) {
585
- const entities = [];
586
- const regex = /(?:export\s+)?(?:class|interface|type)\s+(\w+)/g;
587
- let match;
588
- while ((match = regex.exec(content)) !== null) {
589
- const name = match[1];
590
- if (!name.match(/^(I[A-Z]|Props|State|Config|Options|Params|Args|Result|Response|Request|Error|Helper|Util|Service|Repository|Controller|Module)/)) {
591
- entities.push(name);
592
- }
593
- }
594
- if (entities.length > 0) {
595
- groups.push({ file, entities });
596
- }
597
- }
598
- return groups.slice(0, 30);
599
- }
600
- // ─── Key Modules (important files with descriptions) ──────
601
- async function extractKeyModules(rootPath, allFiles) {
602
- const modules = [];
603
- // Find important files in core/key directories — exclude skill internals and handler files (already in Handlers section)
604
- const EXCLUDE_FROM_KEY_MODULES = /[-_](tools|publishers|constants|types|helpers|utils)\.(ts|js)$|command[-_]handler/i;
605
- const keyFiles = allFiles.filter((f) => f.match(/\.(ts|js)$/) &&
606
- !f.includes(".test.") && !f.includes(".spec.") && !f.includes("index.") &&
607
- !EXCLUDE_FROM_KEY_MODULES.test(f) &&
608
- !f.includes("handlers/") && // already covered by Handlers section
609
- (f.match(/(core|domain)\//i) ||
610
- f.match(/^(src|backend|lib)\/(server|app|main|agent|run)/i) ||
611
- f.match(/(infra\/(?!skills))/i) // infra but not skill internals
612
- ));
613
- const cfg = getScanConfig();
614
- const files = await readFilesMatching(rootPath, keyFiles, {
615
- maxFiles: Math.min(cfg.maxFiles, 200),
616
- maxFileSize: cfg.maxFileSize, // use full config limit to not skip large critical files
617
- });
618
- for (const { file, content } of files) {
619
- const description = extractModuleDescription(content, file);
620
- if (description && description.length > 5) {
621
- const name = file.split("/").pop()?.replace(/\.(ts|js)$/, "") || file;
622
- modules.push({ file, name, description });
623
- }
624
- }
625
- // Prioritize critical files (agent, run-loop, pipeline, etc.) at the top
626
- const CRITICAL = /\b(agent|run[-_]?loop|pipeline|prompt[-_]?router|system[-_]?prompt|session|conversation|handler)\b/i;
627
- modules.sort((a, b) => {
628
- const aScore = CRITICAL.test(a.name) ? 0 : 1;
629
- const bScore = CRITICAL.test(b.name) ? 0 : 1;
630
- return aScore - bScore;
631
- });
632
- return modules.slice(0, 25);
633
- }
634
- // ─── Directory Map ────────────────────────────────────────
635
- async function extractDirectoryMap(rootPath, allFiles) {
636
- const dirs = new Map();
637
- // Count files per 2nd-level directory
638
- for (const file of allFiles) {
639
- const parts = file.split("/");
640
- if (parts.length >= 2) {
641
- const dir = parts.slice(0, 2).join("/");
642
- dirs.set(dir, (dirs.get(dir) || 0) + 1);
643
- }
644
- if (parts.length >= 3 && (parts[0] === "src" || parts[0] === "backend" || parts[0] === "frontend" || parts[0] === "lib")) {
645
- const dir = parts.slice(0, 3).join("/");
646
- dirs.set(dir, (dirs.get(dir) || 0) + 1);
647
- }
648
- }
649
- // Known descriptions for common directories
650
- const KNOWN = {
651
- core: "Engine/motor (agent, run-loop, pipeline, tools, prompts)",
652
- domain: "Pure types, interfaces, and ports",
653
- infra: "Implementations (providers, stores, skills, adapters)",
654
- server: "HTTP + WebSocket + request handlers",
655
- services: "Service layer / registry",
656
- handlers: "Request/event handlers",
657
- controllers: "Request controllers",
658
- middleware: "Middleware pipeline",
659
- middlewares: "Middleware pipeline",
660
- components: "UI components",
661
- hooks: "Custom React/Vue hooks",
662
- stores: "State management stores",
663
- utils: "Utility functions",
664
- helpers: "Helper functions",
665
- lib: "Shared library code",
666
- shared: "Shared code across modules",
667
- api: "API routes / endpoints",
668
- apis: "Multiple API definitions",
669
- routes: "Route definitions",
670
- models: "Data models / schemas",
671
- entities: "Database entities",
672
- repositories: "Data access layer",
673
- config: "Configuration",
674
- scripts: "Build/automation scripts",
675
- skills: "Agent skills / plugins",
676
- plugins: "Plugin modules",
677
- tools: "Tool definitions",
678
- providers: "Service providers (LLM, auth, etc.)",
679
- electron: "Electron main process + desktop shell",
680
- migrations: "Database migrations",
681
- tests: "Test files",
682
- __tests__: "Test files",
683
- };
684
- const results = [];
685
- const SKIP_DIRS = /^(dist|build|out|\.next|\.nuxt|\.output|coverage|dist-\w+|__pycache__|node_modules)/;
686
- for (const [dir, fileCount] of [...dirs.entries()].sort((a, b) => b[1] - a[1])) {
687
- if (fileCount < 1)
688
- continue;
689
- // Skip build artifacts
690
- if (dir.split("/").some((p) => SKIP_DIRS.test(p)))
691
- continue;
692
- const lastPart = dir.split("/").pop() || "";
693
- const description = KNOWN[lastPart] || `${lastPart} directory`;
694
- if (KNOWN[lastPart] || fileCount >= 3) {
695
- results.push({ path: dir, description: `${description} (${fileCount} files)` });
696
- }
697
- }
698
- return results.slice(0, 20);
699
- }
700
- // ─── Handlers (Fix #4) ────────────────────────────────────
701
- async function scanHandlers(rootPath, allFiles) {
702
- const handlerFiles = allFiles.filter((f) => (f.match(/[-_]handler\.(ts|js)$/i) ||
703
- (f.includes("handlers/") && f.match(/\.(ts|js)$/) && !f.includes("index."))) &&
704
- !f.match(/types?\.(ts|js)$/) // exclude type definition files
705
- );
706
- const cfg = getScanConfig();
707
- const files = await readFilesMatching(rootPath, handlerFiles, { maxFiles: cfg.maxFiles, maxFileSize: cfg.maxFileSize });
708
- const handlers = [];
709
- for (const { file, content } of files) {
710
- const baseName = file.split("/").pop()?.replace(/\.(ts|js)$/, "").replace(/-handler$/, "") || "";
711
- // Use shared description extractor (avoids decorative headers)
712
- let description = extractModuleDescription(content, file);
713
- // If generic, enhance with export names
714
- if (description.startsWith("Exports:") || description.length < 10) {
715
- const exportNames = [];
716
- const exportRegex = /export\s+(?:const|function|async\s+function)\s+(\w+)/g;
717
- let m;
718
- while ((m = exportRegex.exec(content)) !== null) {
719
- exportNames.push(m[1]);
720
- if (exportNames.length >= 4)
721
- break;
722
- }
723
- if (exportNames.length > 0) {
724
- description = `Handles ${baseName} — exports: ${exportNames.join(", ")}`;
725
- }
726
- else {
727
- description = `Handles ${baseName} requests`;
728
- }
729
- }
730
- handlers.push({ name: baseName, file, description });
731
- }
732
- return handlers;
733
- }
734
- // ─── Frontend (Fix #6) ────────────────────────────────────
735
- async function scanFrontend(rootPath, allFiles) {
736
- // Detect stores (Zustand, Redux, Pinia, etc.)
737
- const storeFiles = allFiles.filter((f) => f.match(/(stores?|state)\//i) && f.match(/\.(ts|tsx|js|jsx)$/) && !f.includes(".test."));
738
- const stores = storeFiles.map((f) => {
739
- const name = f.split("/").pop()?.replace(/\.(ts|tsx|js|jsx)$/, "") || "";
740
- return name;
741
- }).filter((n) => n && n !== "index");
742
- // Detect hooks
743
- const hookFiles = allFiles.filter((f) => (f.match(/hooks?\//i) || f.match(/use[A-Z]\w+\.(ts|tsx)$/)) &&
744
- f.match(/\.(ts|tsx)$/) && !f.includes(".test."));
745
- const hooks = hookFiles.map((f) => {
746
- const name = f.split("/").pop()?.replace(/\.(ts|tsx)$/, "") || "";
747
- return name;
748
- }).filter((n) => n && n !== "index");
749
- // Detect component directories
750
- const componentDirs = new Set();
751
- for (const f of allFiles) {
752
- if (f.match(/components?\//i) && f.match(/\.(tsx|jsx)$/)) {
753
- const parts = f.split("/");
754
- const compIdx = parts.findIndex((p) => p.match(/components?/i));
755
- if (compIdx >= 0 && parts[compIdx + 1]) {
756
- componentDirs.add(parts[compIdx + 1]);
757
- }
758
- }
759
- }
760
- // Detect UI features
761
- const features = [];
762
- const featurePatterns = [
763
- [/voice|speech|whisper/i, "Voice input"],
764
- [/vim[/-]?mode|vim\./i, "Vim mode"],
765
- [/keybind|shortcut/i, "Keyboard shortcuts"],
766
- [/theme|dark-?mode/i, "Theming / dark mode"],
767
- [/i18n|locale|translate/i, "Internationalization"],
768
- [/extension/i, "Extensions system"],
769
- [/buddy|assistant/i, "AI buddy/assistant UI"],
770
- ];
771
- for (const [pattern, label] of featurePatterns) {
772
- if (allFiles.some((f) => pattern.test(f)))
773
- features.push(label);
774
- }
775
- if (stores.length === 0 && hooks.length === 0 && componentDirs.size === 0)
776
- return null;
777
- return {
778
- stores: stores.slice(0, 15),
779
- hooks: hooks.slice(0, 15),
780
- componentDirs: Array.from(componentDirs).slice(0, 15),
781
- features,
782
- };
783
- }
784
- // ─── Entry Points (Fix #2) ───────────────────────────────
785
- async function detectEntryPointDescriptions(rootPath, allFiles) {
786
- const entries = [];
787
- const entryPatterns = [
788
- { pattern: /^(src\/)?index\.(ts|js)$/, defaultDesc: "Main entry point" },
789
- { pattern: /^(backend\/)?src\/index\.(ts|js)$/, defaultDesc: "Backend entry (CLI/REPL)" },
790
- { pattern: /^(backend\/)?src\/server\.(ts|js)$/, defaultDesc: "HTTP/WS server" },
791
- { pattern: /web-?server\.(ts|js)$/, defaultDesc: "Web server" },
792
- { pattern: /electron\/main\.(cjs|js|ts)$/, defaultDesc: "Electron desktop main process" },
793
- { pattern: /^src\/main\.(ts|tsx|js)$/, defaultDesc: "App main entry" },
794
- { pattern: /^src\/app\.(ts|tsx|js)$/, defaultDesc: "App root" },
795
- { pattern: /^app\/layout\.(tsx|ts)$/, defaultDesc: "Next.js App Router layout" },
796
- { pattern: /^pages\/_app\.(tsx|ts)$/, defaultDesc: "Next.js Pages Router entry" },
797
- { pattern: /^main\.(py|go)$/, defaultDesc: "Main entry" },
798
- { pattern: /manage\.py$/, defaultDesc: "Django management" },
799
- ];
800
- for (const { pattern, defaultDesc } of entryPatterns) {
801
- const match = allFiles.find((f) => pattern.test(f));
802
- if (!match)
803
- continue;
804
- // Use defaultDesc as base, enhance with port detection only
805
- let description = defaultDesc;
806
- const content = await readTextFile(join(rootPath, match));
807
- if (content) {
808
- // Only extract REAL JSDoc descriptions (not decorative headers)
809
- const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//);
810
- if (jsdocMatch) {
811
- const desc = jsdocMatch[1].replace(/\s*\*\s*/g, " ").replace(/@\w+.*/g, "").trim();
812
- // Only use if it's a real sentence, not a header like "--- X ---"
813
- if (desc.length > 15 && !desc.match(/^-{2,}/) && !desc.match(/imports/) && desc.includes(" ")) {
814
- description = desc.slice(0, 100);
815
- }
816
- }
817
- // Detect port
818
- const portMatch = content.match(/(?:port|PORT)\s*[=:]\s*(\d{2,5})/);
819
- if (portMatch && !description.includes("port"))
820
- description += ` (port ${portMatch[1]})`;
821
- }
822
- entries.push({ file: match, description });
823
- }
824
- return entries;
825
- }
826
- // ─── Data Flow (Fix #1) ──────────────────────────────────
827
- function inferDataFlow(allFiles, entryPoints, frameworks) {
828
- const flow = [];
829
- const fwNames = frameworks.map((f) => f.name);
830
- // Detect if there's a WebSocket layer
831
- const hasWS = allFiles.some((f) => f.match(/ws[-_]?handler|websocket|socket/i));
832
- const hasElectron = allFiles.some((f) => f.startsWith("electron/"));
833
- const hasFrontend = allFiles.some((f) => f.match(/(frontend|ui)\//i) && f.match(/\.(tsx|jsx)$/));
834
- const hasAgent = allFiles.some((f) => f.match(/agent\.(ts|js)$/));
835
- const hasRunLoop = allFiles.some((f) => f.match(/run[-_]?loop/i));
836
- const hasHandlers = allFiles.some((f) => f.includes("handlers/"));
837
- // Build flow based on what exists (entry points already shown in their own section)
838
- if (hasFrontend && hasWS) {
839
- flow.push("Frontend → WebSocket → Handler → Agent/Service → Response → Stream → Frontend");
840
- }
841
- else if (hasFrontend && hasHandlers) {
842
- flow.push("Frontend → HTTP API → Handler → Service → Response → Frontend");
843
- }
844
- else if (fwNames.includes("Next.js")) {
845
- flow.push("Browser → Next.js Route → API Handler → Service → Response");
846
- }
847
- else if (fwNames.includes("Express") || fwNames.includes("Fastify")) {
848
- flow.push("Client → HTTP → Middleware → Controller → Service → Repository → Response");
849
- }
850
- if (hasAgent && hasRunLoop) {
851
- flow.push("Agent.run() → RunLoop → LLM Provider → Tool Execution → Stream Events");
852
- }
853
- if (hasElectron && hasFrontend) {
854
- flow.push("Electron main → spawns Backend → opens BrowserWindow → Frontend connects via WebSocket");
855
- }
856
- return flow;
857
- }
858
- // ─── Module Description ───────────────────────────────────
859
- function extractModuleDescription(content, file) {
860
- const fileName = file.split("/").pop()?.replace(/\.(ts|js)$/, "") || "";
861
- // 1a. BEST: JSDoc on exported class (the main abstraction — prioritized over functions)
862
- const classJSDoc = content.match(/\/\*\*\s*([\s\S]*?)\*\/\s*\n\s*export\s+(?:class|abstract\s+class)\s+(\w+)/);
863
- if (classJSDoc) {
864
- const desc = cleanJSDoc(classJSDoc[1]);
865
- if (desc && isGoodDescription(desc))
866
- return sanitizeDescription(desc);
867
- }
868
- // Check if file has an exported class (even without JSDoc) — if so, skip function JSDoc
869
- // because the module's purpose IS the class, not helper functions
870
- const hasExportedClass = /export\s+(?:class|abstract\s+class)\s+\w+/.test(content);
871
- // 1b. Fallback: JSDoc on exported function — ONLY if no class exists in file
872
- if (!hasExportedClass) {
873
- const funcJSDoc = content.match(/\/\*\*\s*([\s\S]*?)\*\/\s*\n\s*export\s+(?:function|async\s+function)\s+(\w+)/);
874
- if (funcJSDoc) {
875
- const desc = cleanJSDoc(funcJSDoc[1]);
876
- if (desc && isGoodDescription(desc))
877
- return sanitizeDescription(desc);
878
- }
879
- }
880
- // 2. File-level JSDoc at the very top (before any import/code)
881
- const topJSDoc = content.match(/^\s*\/\*\*\s*([\s\S]*?)\*\//);
882
- if (topJSDoc) {
883
- const desc = cleanJSDoc(topJSDoc[1]);
884
- if (desc && isGoodDescription(desc))
885
- return sanitizeDescription(desc);
886
- }
887
- // 3. Meaningful comment block at the top of file (before first export)
888
- const lines = content.split("\n");
889
- for (const line of lines.slice(0, 50)) {
890
- const trimmed = line.trim();
891
- if (!trimmed)
892
- continue;
893
- if (trimmed.startsWith("import ") || trimmed.startsWith("import{"))
894
- continue;
895
- // Stop at first export — comments between functions are NOT module-level
896
- if (trimmed.startsWith("export ") || trimmed.startsWith("export{"))
897
- break;
898
- // Only look at pure comment lines (not inline code comments)
899
- const commentMatch = trimmed.match(/^\/\/\s*(.+)/);
900
- if (commentMatch) {
901
- const text = commentMatch[1].trim();
902
- if (isGoodDescription(text))
903
- return sanitizeDescription(text);
904
- continue;
905
- }
906
- // If we hit actual code, stop looking for comments
907
- if (!trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*")) {
908
- break;
909
- }
910
- }
911
- // 4a. Try first meaningful comment inside the main class body
912
- const classBody = content.match(/export\s+(?:class|abstract\s+class)\s+\w+[^{]*\{([\s\S]*)/);
913
- if (classBody) {
914
- const bodyLines = classBody[1].split("\n").slice(0, 30);
915
- for (const bline of bodyLines) {
916
- const cm = bline.trim().match(/^\/\/\s+(.+)/);
917
- if (cm && isGoodDescription(cm[1].trim())) {
918
- return sanitizeDescription(cm[1].trim());
919
- }
920
- // Also check single-line JSDoc inside class (on first method or property)
921
- const methodDoc = bline.trim().match(/\/\*\*\s*(.+)\s*\*\//);
922
- if (methodDoc && isGoodDescription(methodDoc[1].trim()) && methodDoc[1].trim().length > 20) {
923
- return sanitizeDescription(methodDoc[1].trim());
924
- }
925
- }
926
- }
927
- // 4b. Infer from filename + exports
928
- const exports = [];
929
- const exportRegex = /export\s+(?:class|function|const|interface|type|async\s+function)\s+(\w+)/g;
930
- let match;
931
- while ((match = exportRegex.exec(content)) !== null) {
932
- exports.push(match[1]);
933
- if (exports.length >= 5)
934
- break;
935
- }
936
- if (exports.length > 0) {
937
- const humanName = humanizeName(fileName);
938
- // Avoid redundant "Agent — exports: Agent"
939
- if (exports.length === 1 && humanizeName(exports[0]).toLowerCase() === humanName.toLowerCase()) {
940
- return humanName;
941
- }
942
- return sanitizeDescription(`${humanName} — exports: ${exports.join(", ")}`);
943
- }
944
- // 5. Filename-based fallback
945
- return humanizeName(fileName);
946
- }
947
- function cleanJSDoc(raw) {
948
- let text = raw
949
- .replace(/\s*\*\s*/g, " ")
950
- .replace(/@\w+.*/g, "")
951
- .replace(/\{[^}]*\}/g, "") // remove type annotations
952
- .trim();
953
- // Stop at first code-like boundary (semicolon, opening brace, pipe type, typed field)
954
- const codeBoundary = text.search(/[;{|}]|:\s*[A-Z]\w*[<\[]/);
955
- if (codeBoundary > 10)
956
- text = text.slice(0, codeBoundary).trim();
957
- return text;
958
- }
959
- function sanitizeDescription(desc) {
960
- return desc
961
- .replace(/\n/g, " ")
962
- .replace(/\s{2,}/g, " ")
963
- .replace(/[;{}]/g, "")
964
- .trim()
965
- .slice(0, 200);
966
- }
967
- function isGoodDescription(text) {
968
- if (text.length < 10)
969
- return false;
970
- // Skip decorative box-drawing lines (─, ═, ━, ──── patterns)
971
- if (text.includes("────") || text.includes("════") || text.includes("━━━━"))
972
- return false;
973
- if (/^[─═━\s]/.test(text))
974
- return false;
975
- // Skip decorative headers (--- X ---, === X ===, ### X)
976
- if (text.match(/^[-=]{2,}/) || text.match(/^#{1,3}\s/))
977
- return false;
978
- // Skip implementation details / TODO comments
979
- if (/^(Allow|Ensure|Make sure|TODO|FIXME|HACK|XXX)\b/i.test(text))
980
- return false;
981
- // Skip just a name/title (single word or ALL_CAPS)
982
- if (text.match(/^[A-Z_]+$/) || !text.includes(" "))
983
- return false;
984
- // Skip lines that are just file references
985
- if (text.match(/^\w+\.(ts|js|tsx|jsx)$/))
986
- return false;
987
- // Skip code fragments that leaked into descriptions
988
- if (text.includes("export const") || text.includes("export function"))
989
- return false;
990
- if (text.includes(" = {") || text.includes(" = [") || text.match(/;\s*$/))
991
- return false;
992
- // Skip lines with TypeScript type syntax
993
- if (/:\s*(Map|Set|Array|Record|Promise|string|number|boolean)</.test(text))
994
- return false;
995
- // Skip lines that are mostly special characters
996
- if (text.replace(/[^a-zA-Z\s]/g, "").length < text.length * 0.4)
997
- return false;
998
- return true;
999
- }
1000
- // ─── Helpers ──────────────────────────────────────────────
1001
- function humanizeName(name) {
1002
- // Handle kebab-case and snake_case
1003
- let clean = name.replace(/[-_]/g, " ");
1004
- // Handle PascalCase/camelCase — but NOT UPPER_CASE (keep ANALYSIS_PROMPT as-is)
1005
- if (!name.match(/^[A-Z_0-9]+$/)) {
1006
- // Preserve known compound words before splitting
1007
- clean = clean
1008
- .replace(/YouTube/g, "Youtube")
1009
- .replace(/GitHub/g, "Github")
1010
- .replace(/TypeScript/g, "Typescript")
1011
- .replace(/JavaScript/g, "Javascript")
1012
- .replace(/WebSocket/g, "Websocket");
1013
- clean = clean
1014
- .replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase
1015
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2"); // ACRONYMWord
1016
- // Restore proper names
1017
- clean = clean
1018
- .replace(/Youtube/g, "YouTube")
1019
- .replace(/Github/g, "GitHub")
1020
- .replace(/Typescript/g, "TypeScript")
1021
- .replace(/Javascript/g, "JavaScript")
1022
- .replace(/Websocket/g, "WebSocket");
1023
- }
1024
- return clean.trim().charAt(0).toUpperCase() + clean.trim().slice(1);
1025
- }
1026
- function findFile(allFiles, nameLower) {
1027
- return allFiles.find((f) => f.toLowerCase() === nameLower || f.toLowerCase().endsWith(`/${nameLower}`));
1028
- }
1029
284
  //# sourceMappingURL=business.js.map