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.
- package/dist/generators/template-sections.d.ts +8 -0
- package/dist/generators/template-sections.d.ts.map +1 -0
- package/dist/generators/template-sections.js +261 -0
- package/dist/generators/template-sections.js.map +1 -0
- package/dist/generators/template.d.ts +1 -6
- package/dist/generators/template.d.ts.map +1 -1
- package/dist/generators/template.js +2 -280
- package/dist/generators/template.js.map +1 -1
- package/dist/scanner/business.d.ts.map +1 -1
- package/dist/scanner/business.js +7 -752
- package/dist/scanner/business.js.map +1 -1
- package/dist/scanner/conventions.js +25 -6
- package/dist/scanner/conventions.js.map +1 -1
- package/dist/scanner/dependencies.d.ts.map +1 -1
- package/dist/scanner/dependencies.js +30 -0
- package/dist/scanner/dependencies.js.map +1 -1
- package/dist/scanner/description.d.ts +7 -0
- package/dist/scanner/description.d.ts.map +1 -0
- package/dist/scanner/description.js +172 -0
- package/dist/scanner/description.js.map +1 -0
- package/dist/scanner/domain.d.ts +10 -0
- package/dist/scanner/domain.d.ts.map +1 -0
- package/dist/scanner/domain.js +323 -0
- package/dist/scanner/domain.js.map +1 -0
- package/dist/scanner/frameworks.d.ts.map +1 -1
- package/dist/scanner/frameworks.js +13 -0
- package/dist/scanner/frameworks.js.map +1 -1
- package/dist/scanner/patterns-code.d.ts +9 -0
- package/dist/scanner/patterns-code.d.ts.map +1 -0
- package/dist/scanner/patterns-code.js +283 -0
- package/dist/scanner/patterns-code.js.map +1 -0
- package/dist/scanner/patterns.d.ts.map +1 -1
- package/dist/scanner/patterns.js +2 -288
- package/dist/scanner/patterns.js.map +1 -1
- package/dist/scanner/skills.d.ts +5 -0
- package/dist/scanner/skills.d.ts.map +1 -0
- package/dist/scanner/skills.js +242 -0
- package/dist/scanner/skills.js.map +1 -0
- package/package.json +1 -1
package/dist/scanner/business.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
// ─── Business Context Scanner — Orchestrator ────────────
|
|
1
2
|
import { join } from "node:path";
|
|
2
|
-
import { readTextFile
|
|
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("/") ||
|
|
393
|
-
f.startsWith("docs/")
|
|
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
|