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