sourcebook 0.4.1 → 0.5.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/claude.js +14 -8
- package/dist/generators/shared.d.ts +12 -0
- package/dist/generators/shared.js +108 -0
- package/dist/scanner/index.js +23 -0
- package/dist/scanner/patterns.js +205 -1
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { groupByCategory, hasCommands, categorizeFindings, enforceTokenBudget, } from "./shared.js";
|
|
1
|
+
import { groupByCategory, hasCommands, categorizeFindings, enforceTokenBudget, buildQuickReference, getModePriorities, } from "./shared.js";
|
|
2
2
|
/**
|
|
3
3
|
* Generate a CLAUDE.md file from scan results.
|
|
4
4
|
*
|
|
@@ -10,6 +10,7 @@ import { groupByCategory, hasCommands, categorizeFindings, enforceTokenBudget, }
|
|
|
10
10
|
*/
|
|
11
11
|
export function generateClaude(scan, budget) {
|
|
12
12
|
const { critical, important, supplementary } = categorizeFindings(scan.findings);
|
|
13
|
+
const priorities = getModePriorities(scan.repoMode);
|
|
13
14
|
const sections = [];
|
|
14
15
|
// ============================================
|
|
15
16
|
// BEGINNING: Most critical info goes here
|
|
@@ -23,7 +24,12 @@ export function generateClaude(scan, budget) {
|
|
|
23
24
|
"",
|
|
24
25
|
].join("\n");
|
|
25
26
|
sections.push({ key: "header", content: header, priority: 100 });
|
|
26
|
-
//
|
|
27
|
+
// Quick Reference — the "30-second handoff" (highest priority in app mode)
|
|
28
|
+
const quickRef = buildQuickReference(scan.findings);
|
|
29
|
+
if (quickRef) {
|
|
30
|
+
sections.push({ key: "quick_reference", content: quickRef, priority: priorities.quick_reference });
|
|
31
|
+
}
|
|
32
|
+
// Commands — most immediately actionable
|
|
27
33
|
if (hasCommands(scan.commands)) {
|
|
28
34
|
const lines = ["## Commands", ""];
|
|
29
35
|
if (scan.commands.dev)
|
|
@@ -49,7 +55,7 @@ export function generateClaude(scan, budget) {
|
|
|
49
55
|
lines.push(`- **${finding.category}:** ${finding.description}`);
|
|
50
56
|
}
|
|
51
57
|
lines.push("");
|
|
52
|
-
sections.push({ key: "critical", content: lines.join("\n"), priority:
|
|
58
|
+
sections.push({ key: "critical", content: lines.join("\n"), priority: priorities.critical });
|
|
53
59
|
}
|
|
54
60
|
// ============================================
|
|
55
61
|
// MIDDLE: Less critical but useful info
|
|
@@ -58,7 +64,7 @@ export function generateClaude(scan, budget) {
|
|
|
58
64
|
// Stack (brief)
|
|
59
65
|
if (scan.frameworks.length > 0) {
|
|
60
66
|
const content = ["## Stack", "", scan.frameworks.join(", "), ""].join("\n");
|
|
61
|
-
sections.push({ key: "stack", content, priority:
|
|
67
|
+
sections.push({ key: "stack", content, priority: priorities.stack });
|
|
62
68
|
}
|
|
63
69
|
// Key directories (only non-obvious ones)
|
|
64
70
|
if (Object.keys(scan.structure.directories).length > 0) {
|
|
@@ -69,7 +75,7 @@ export function generateClaude(scan, budget) {
|
|
|
69
75
|
lines.push(`- \`${dir}/\` — ${purpose}`);
|
|
70
76
|
}
|
|
71
77
|
lines.push("");
|
|
72
|
-
sections.push({ key: "structure", content: lines.join("\n"), priority:
|
|
78
|
+
sections.push({ key: "structure", content: lines.join("\n"), priority: priorities.structure });
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
// Core modules (from PageRank)
|
|
@@ -80,7 +86,7 @@ export function generateClaude(scan, budget) {
|
|
|
80
86
|
lines.push(`- \`${file}\``);
|
|
81
87
|
}
|
|
82
88
|
lines.push("");
|
|
83
|
-
sections.push({ key: "core_modules", content: lines.join("\n"), priority:
|
|
89
|
+
sections.push({ key: "core_modules", content: lines.join("\n"), priority: priorities.core_modules });
|
|
84
90
|
}
|
|
85
91
|
// Important findings (high confidence, non-critical)
|
|
86
92
|
if (important.length > 0) {
|
|
@@ -98,7 +104,7 @@ export function generateClaude(scan, budget) {
|
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
lines.push("");
|
|
101
|
-
sections.push({ key: "conventions", content: lines.join("\n"), priority:
|
|
107
|
+
sections.push({ key: "conventions", content: lines.join("\n"), priority: priorities.conventions });
|
|
102
108
|
}
|
|
103
109
|
// Supplementary findings (medium confidence)
|
|
104
110
|
if (supplementary.length > 0) {
|
|
@@ -116,7 +122,7 @@ export function generateClaude(scan, budget) {
|
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
lines.push("");
|
|
119
|
-
sections.push({ key: "supplementary", content: lines.join("\n"), priority:
|
|
125
|
+
sections.push({ key: "supplementary", content: lines.join("\n"), priority: priorities.supplementary });
|
|
120
126
|
}
|
|
121
127
|
// ============================================
|
|
122
128
|
// END: Important reminders go here
|
|
@@ -27,6 +27,18 @@ export declare function categorizeFindings(findings: Finding[]): {
|
|
|
27
27
|
* 6. Supplementary findings (drop first)
|
|
28
28
|
* 7. Footer/manual section (always keep — end of context = high retention)
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* Build a Quick Reference section from dominant pattern findings.
|
|
32
|
+
* This is the "30-second senior engineer handoff" — the single most
|
|
33
|
+
* actionable section in the output.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildQuickReference(findings: Finding[]): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Get priority adjustments based on repo mode.
|
|
38
|
+
* App repos: boost dominant patterns, demote structural.
|
|
39
|
+
* Library repos: boost structural, demote patterns.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getModePriorities(repoMode?: "app" | "library" | "monorepo"): Record<string, number>;
|
|
30
42
|
export declare function enforceTokenBudget(sections: {
|
|
31
43
|
key: string;
|
|
32
44
|
content: string;
|
|
@@ -10,6 +10,7 @@ const CRITICAL_CATEGORIES = new Set([
|
|
|
10
10
|
"Git history",
|
|
11
11
|
"Commit conventions",
|
|
12
12
|
"Anti-patterns",
|
|
13
|
+
"Critical constraints",
|
|
13
14
|
]);
|
|
14
15
|
const CRITICAL_KEYWORDS = [
|
|
15
16
|
"breaking", "blast radius", "deprecated", "don't", "must",
|
|
@@ -62,6 +63,113 @@ export function categorizeFindings(findings) {
|
|
|
62
63
|
* 6. Supplementary findings (drop first)
|
|
63
64
|
* 7. Footer/manual section (always keep — end of context = high retention)
|
|
64
65
|
*/
|
|
66
|
+
/**
|
|
67
|
+
* Build a Quick Reference section from dominant pattern findings.
|
|
68
|
+
* This is the "30-second senior engineer handoff" — the single most
|
|
69
|
+
* actionable section in the output.
|
|
70
|
+
*/
|
|
71
|
+
export function buildQuickReference(findings) {
|
|
72
|
+
const patterns = findings.filter((f) => f.category === "Dominant patterns");
|
|
73
|
+
if (patterns.length < 2)
|
|
74
|
+
return null;
|
|
75
|
+
const lines = ["## Quick Reference", ""];
|
|
76
|
+
for (const p of patterns) {
|
|
77
|
+
// Extract a short label from the description
|
|
78
|
+
const desc = p.description;
|
|
79
|
+
let label = "";
|
|
80
|
+
let value = desc;
|
|
81
|
+
if (desc.includes("internationalization") || desc.includes("i18n") || desc.includes("translation")) {
|
|
82
|
+
label = "i18n";
|
|
83
|
+
}
|
|
84
|
+
else if (desc.includes("route") || desc.includes("endpoint") || desc.includes("API")) {
|
|
85
|
+
label = "routing";
|
|
86
|
+
}
|
|
87
|
+
else if (desc.includes("validation") || desc.includes("schema") || desc.includes("Zod") || desc.includes("Pydantic")) {
|
|
88
|
+
label = "validation";
|
|
89
|
+
}
|
|
90
|
+
else if ((desc.includes("auth") || desc.includes("Auth") || desc.includes("session")) && !desc.includes("integration")) {
|
|
91
|
+
label = "auth";
|
|
92
|
+
}
|
|
93
|
+
else if (desc.includes("Test") || desc.includes("test")) {
|
|
94
|
+
label = "testing";
|
|
95
|
+
}
|
|
96
|
+
else if (desc.includes("Tailwind") || desc.includes("styled") || desc.includes("CSS")) {
|
|
97
|
+
label = "styling";
|
|
98
|
+
}
|
|
99
|
+
else if (desc.includes("database") || desc.includes("Database") || desc.includes("Prisma") || desc.includes("ORM")) {
|
|
100
|
+
label = "database";
|
|
101
|
+
}
|
|
102
|
+
else if (desc.includes("fetching") || desc.includes("Query") || desc.includes("SWR")) {
|
|
103
|
+
label = "data fetching";
|
|
104
|
+
}
|
|
105
|
+
else if (desc.includes("Route definitions") || desc.includes("Add new endpoints")) {
|
|
106
|
+
label = "routes";
|
|
107
|
+
}
|
|
108
|
+
else if (desc.includes("integration") || desc.includes("Third-party")) {
|
|
109
|
+
label = "integrations";
|
|
110
|
+
}
|
|
111
|
+
else if (desc.includes("components") || desc.includes("UI")) {
|
|
112
|
+
label = "components";
|
|
113
|
+
}
|
|
114
|
+
else if (desc.includes("Generated") || desc.includes("generated") || desc.includes("DO NOT")) {
|
|
115
|
+
label = "generated";
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
continue; // Skip findings we can't label cleanly
|
|
119
|
+
}
|
|
120
|
+
// Compress the description to a short actionable line
|
|
121
|
+
const short = desc
|
|
122
|
+
.replace(/\. Follow this pattern.*$/, "")
|
|
123
|
+
.replace(/\. This is the project's standard.*$/, "")
|
|
124
|
+
.replace(/\. Each integration has.*$/, "");
|
|
125
|
+
lines.push(`- **${label}:** ${short}`);
|
|
126
|
+
}
|
|
127
|
+
if (lines.length <= 2)
|
|
128
|
+
return null; // Nothing useful
|
|
129
|
+
// Deduplicate by label — keep only first occurrence of each
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const deduped = [lines[0], lines[1]]; // Keep header
|
|
132
|
+
for (let i = 2; i < lines.length; i++) {
|
|
133
|
+
const labelMatch = lines[i].match(/^\- \*\*(\w[\w\s]*)\:\*\*/);
|
|
134
|
+
if (labelMatch) {
|
|
135
|
+
const lbl = labelMatch[1];
|
|
136
|
+
if (seen.has(lbl))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(lbl);
|
|
139
|
+
}
|
|
140
|
+
deduped.push(lines[i]);
|
|
141
|
+
}
|
|
142
|
+
deduped.push("");
|
|
143
|
+
return deduped.join("\n");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get priority adjustments based on repo mode.
|
|
147
|
+
* App repos: boost dominant patterns, demote structural.
|
|
148
|
+
* Library repos: boost structural, demote patterns.
|
|
149
|
+
*/
|
|
150
|
+
export function getModePriorities(repoMode) {
|
|
151
|
+
if (repoMode === "library") {
|
|
152
|
+
return {
|
|
153
|
+
quick_reference: 85,
|
|
154
|
+
critical: 92,
|
|
155
|
+
core_modules: 90,
|
|
156
|
+
conventions: 80,
|
|
157
|
+
stack: 50,
|
|
158
|
+
structure: 40,
|
|
159
|
+
supplementary: 25,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Default: app mode (boost patterns, Quick Reference highest)
|
|
163
|
+
return {
|
|
164
|
+
quick_reference: 96,
|
|
165
|
+
critical: 92,
|
|
166
|
+
core_modules: 50,
|
|
167
|
+
conventions: 85,
|
|
168
|
+
stack: 45,
|
|
169
|
+
structure: 60,
|
|
170
|
+
supplementary: 20,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
65
173
|
export function enforceTokenBudget(sections, budget) {
|
|
66
174
|
// Sort by priority descending (highest priority = keep)
|
|
67
175
|
const sorted = [...sections].sort((a, b) => b.priority - a.priority);
|
package/dist/scanner/index.js
CHANGED
|
@@ -51,6 +51,8 @@ export async function scanProject(dir) {
|
|
|
51
51
|
...gitAnalysis.findings,
|
|
52
52
|
...graphAnalysis.findings,
|
|
53
53
|
];
|
|
54
|
+
// Detect repo mode for prioritization
|
|
55
|
+
const repoMode = detectRepoMode(dir, files, frameworks.map((f) => f.name));
|
|
54
56
|
return {
|
|
55
57
|
dir,
|
|
56
58
|
files,
|
|
@@ -60,8 +62,29 @@ export async function scanProject(dir) {
|
|
|
60
62
|
structure,
|
|
61
63
|
findings,
|
|
62
64
|
rankedFiles: graphAnalysis.rankedFiles,
|
|
65
|
+
repoMode,
|
|
63
66
|
};
|
|
64
67
|
}
|
|
68
|
+
function detectRepoMode(dir, files, frameworks) {
|
|
69
|
+
// Monorepo detection
|
|
70
|
+
const hasWorkspaces = files.some((f) => f === "pnpm-workspace.yaml" || f === "lerna.json" || f === "nx.json");
|
|
71
|
+
if (hasWorkspaces)
|
|
72
|
+
return "monorepo";
|
|
73
|
+
// Library detection
|
|
74
|
+
const hasPublishConfig = files.some((f) => f === "setup.py" || f === "pyproject.toml" || f === "setup.cfg");
|
|
75
|
+
const hasSrcLib = files.some((f) => f.startsWith("src/lib/") || f.startsWith("lib/"));
|
|
76
|
+
const hasExportsField = false; // would need to read package.json, but frameworks already detected
|
|
77
|
+
const isLibraryFramework = frameworks.some((f) => ["FastAPI", "Flask", "Django", "Hono", "Express"].includes(f));
|
|
78
|
+
// If it has a setup.py/pyproject.toml AND no app/ or pages/ → library
|
|
79
|
+
const hasAppDirs = files.some((f) => f.startsWith("app/") || f.startsWith("pages/") || f.startsWith("src/app/") || f.startsWith("src/pages/"));
|
|
80
|
+
const hasComponents = files.some((f) => f.includes("/components/"));
|
|
81
|
+
if (hasPublishConfig && !hasAppDirs && !hasComponents)
|
|
82
|
+
return "library";
|
|
83
|
+
if (hasSrcLib && !hasAppDirs && !hasComponents && !isLibraryFramework)
|
|
84
|
+
return "library";
|
|
85
|
+
// Default: app
|
|
86
|
+
return "app";
|
|
87
|
+
}
|
|
65
88
|
function detectLanguages(files) {
|
|
66
89
|
const extMap = {
|
|
67
90
|
".ts": "TypeScript",
|
package/dist/scanner/patterns.js
CHANGED
|
@@ -467,7 +467,211 @@ function detectDominantPatterns(dir, files, contents, frameworks) {
|
|
|
467
467
|
});
|
|
468
468
|
}
|
|
469
469
|
// ========================================
|
|
470
|
-
// 6.
|
|
470
|
+
// 6. AUTH PATTERNS
|
|
471
|
+
// ========================================
|
|
472
|
+
const authPatterns = [
|
|
473
|
+
{ pattern: "useAuth|useSession|useUser", name: "auth hooks (useAuth/useSession/useUser)", count: 0 },
|
|
474
|
+
{ pattern: "withAuth|authMiddleware|requireAuth", name: "auth middleware", count: 0 },
|
|
475
|
+
{ pattern: "passport\\.authenticate", name: "Passport.js", count: 0 },
|
|
476
|
+
{ pattern: "jwt\\.verify|jwt\\.sign|jsonwebtoken", name: "JWT (jsonwebtoken)", count: 0 },
|
|
477
|
+
{ pattern: "@login_required|LoginRequiredMixin", name: "Django login_required", count: 0 },
|
|
478
|
+
{ pattern: "IsAuthenticated|AllowAny|BasePermission", name: "DRF permissions", count: 0 },
|
|
479
|
+
{ pattern: "NextAuth|getServerSession", name: "NextAuth.js", count: 0 },
|
|
480
|
+
{ pattern: "supabase\\.auth|useSupabaseClient", name: "Supabase Auth", count: 0 },
|
|
481
|
+
{ pattern: "clerk|useClerk|ClerkProvider", name: "Clerk", count: 0 },
|
|
482
|
+
];
|
|
483
|
+
for (const [, content] of allContents) {
|
|
484
|
+
for (const p of authPatterns) {
|
|
485
|
+
if (new RegExp(p.pattern).test(content)) {
|
|
486
|
+
p.count++;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const dominantAuth = authPatterns.filter((p) => p.count >= 2).sort((a, b) => b.count - a.count);
|
|
491
|
+
if (dominantAuth.length > 0) {
|
|
492
|
+
const primary = dominantAuth[0];
|
|
493
|
+
// Find auth middleware/guard files
|
|
494
|
+
const authFiles = files.filter((f) => (f.includes("auth") || f.includes("middleware") || f.includes("guard") || f.includes("session")) &&
|
|
495
|
+
!f.includes("node_modules") && !f.includes(".test.") &&
|
|
496
|
+
(f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".py")));
|
|
497
|
+
const authEntrypoint = authFiles.find((f) => f.includes("middleware") || f.includes("guard") || f.includes("auth/index"));
|
|
498
|
+
let desc = `Auth uses ${primary.name}.`;
|
|
499
|
+
if (authEntrypoint) {
|
|
500
|
+
desc += ` Auth logic lives in ${authEntrypoint}.`;
|
|
501
|
+
}
|
|
502
|
+
findings.push({
|
|
503
|
+
category: "Dominant patterns",
|
|
504
|
+
description: desc,
|
|
505
|
+
evidence: `${primary.count} files`,
|
|
506
|
+
confidence: "high",
|
|
507
|
+
discoverable: false,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
// ========================================
|
|
511
|
+
// 7. STYLING CONVENTIONS
|
|
512
|
+
// ========================================
|
|
513
|
+
const stylePatterns = [
|
|
514
|
+
{ pattern: "className=|class=.*tw-", name: "Tailwind CSS", desc: "Styling uses Tailwind CSS utility classes", count: 0 },
|
|
515
|
+
{ pattern: "styled\\.|styled\\(|css`", name: "styled-components/Emotion", desc: "Styling uses CSS-in-JS (styled-components or Emotion)", count: 0 },
|
|
516
|
+
{ pattern: "styles\\.\\w+|from.*\\.module\\.(css|scss)", name: "CSS Modules", desc: "Styling uses CSS Modules (*.module.css)", count: 0 },
|
|
517
|
+
];
|
|
518
|
+
for (const [f, content] of allContents) {
|
|
519
|
+
for (const p of stylePatterns) {
|
|
520
|
+
if (new RegExp(p.pattern).test(content)) {
|
|
521
|
+
p.count++;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const dominantStyle = stylePatterns.filter((p) => p.count >= 3).sort((a, b) => b.count - a.count);
|
|
526
|
+
if (dominantStyle.length > 0) {
|
|
527
|
+
const primary = dominantStyle[0];
|
|
528
|
+
let desc = `${primary.desc}.`;
|
|
529
|
+
// For Tailwind, check for custom tokens
|
|
530
|
+
if (primary.name === "Tailwind CSS") {
|
|
531
|
+
const twConfig = files.find((f) => f.includes("tailwind.config"));
|
|
532
|
+
if (twConfig) {
|
|
533
|
+
try {
|
|
534
|
+
const configContent = fs.readFileSync(path.join(dir, twConfig), "utf-8");
|
|
535
|
+
if (configContent.includes("colors") || configContent.includes("extend")) {
|
|
536
|
+
desc += ` Custom design tokens defined in ${twConfig} — use these instead of arbitrary values.`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch { /* skip */ }
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
findings.push({
|
|
543
|
+
category: "Dominant patterns",
|
|
544
|
+
description: desc,
|
|
545
|
+
evidence: `${primary.count} files`,
|
|
546
|
+
confidence: "high",
|
|
547
|
+
discoverable: false,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// ========================================
|
|
551
|
+
// 8. DATABASE / ORM PATTERNS
|
|
552
|
+
// ========================================
|
|
553
|
+
const dbPatterns = [
|
|
554
|
+
{ pattern: "prisma\\.|PrismaClient|\\$queryRaw", name: "Prisma", entryHint: "prisma/schema.prisma", count: 0 },
|
|
555
|
+
{ pattern: "drizzle\\(|pgTable|sqliteTable", name: "Drizzle ORM", entryHint: "drizzle.config.ts", count: 0 },
|
|
556
|
+
{ pattern: "knex\\(|knex\\.schema", name: "Knex.js", entryHint: "knexfile", count: 0 },
|
|
557
|
+
{ pattern: "sequelize\\.define|Model\\.init", name: "Sequelize", entryHint: "models/", count: 0 },
|
|
558
|
+
{ pattern: "TypeORM|@Entity|getRepository", name: "TypeORM", entryHint: "entities/", count: 0 },
|
|
559
|
+
{ pattern: "mongoose\\.model|Schema\\(\\{", name: "Mongoose", entryHint: "models/", count: 0 },
|
|
560
|
+
{ pattern: "from django\\.db|models\\.Model", name: "Django ORM", entryHint: "models.py", count: 0 },
|
|
561
|
+
{ pattern: "SQLAlchemy|declarative_base|sessionmaker", name: "SQLAlchemy", entryHint: "models/", count: 0 },
|
|
562
|
+
{ pattern: "from tortoise|tortoise\\.models", name: "Tortoise ORM", entryHint: "models/", count: 0 },
|
|
563
|
+
];
|
|
564
|
+
for (const [, content] of allContents) {
|
|
565
|
+
for (const p of dbPatterns) {
|
|
566
|
+
if (new RegExp(p.pattern).test(content)) {
|
|
567
|
+
p.count++;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const dominantDB = dbPatterns.filter((p) => p.count >= 2).sort((a, b) => b.count - a.count);
|
|
572
|
+
if (dominantDB.length > 0) {
|
|
573
|
+
const primary = dominantDB[0];
|
|
574
|
+
// Try to find the actual entrypoint file
|
|
575
|
+
const dbEntryFile = files.find((f) => f.includes(primary.entryHint) && !f.includes("node_modules"));
|
|
576
|
+
let desc = `Database access uses ${primary.name}.`;
|
|
577
|
+
if (dbEntryFile) {
|
|
578
|
+
desc += ` Schema/models defined in ${dbEntryFile}.`;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
desc += ` Look for schemas in ${primary.entryHint}.`;
|
|
582
|
+
}
|
|
583
|
+
findings.push({
|
|
584
|
+
category: "Dominant patterns",
|
|
585
|
+
description: desc,
|
|
586
|
+
evidence: `${primary.count} files`,
|
|
587
|
+
confidence: "high",
|
|
588
|
+
discoverable: false,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
// ========================================
|
|
592
|
+
// 9. GENERATED / DO-NOT-EDIT FILES
|
|
593
|
+
// ========================================
|
|
594
|
+
const generatedFiles = [];
|
|
595
|
+
for (const [file, content] of allContents) {
|
|
596
|
+
const firstLines = content.slice(0, 500);
|
|
597
|
+
if (/@generated/.test(firstLines) ||
|
|
598
|
+
/DO NOT EDIT/i.test(firstLines) ||
|
|
599
|
+
/auto-generated/i.test(firstLines) ||
|
|
600
|
+
/this file is generated/i.test(firstLines) ||
|
|
601
|
+
/generated by/i.test(firstLines)) {
|
|
602
|
+
generatedFiles.push(file);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
// Also check for common generated file patterns in the full file list
|
|
606
|
+
const knownGenerated = files.filter((f) => !f.includes("node_modules") &&
|
|
607
|
+
(f.includes(".generated.") ||
|
|
608
|
+
f.includes(".gen.") ||
|
|
609
|
+
f.endsWith(".d.ts") && f.includes("generated") ||
|
|
610
|
+
f.includes("__generated__") ||
|
|
611
|
+
f.includes("codegen")));
|
|
612
|
+
const allGenerated = [...new Set([...generatedFiles, ...knownGenerated])];
|
|
613
|
+
if (allGenerated.length >= 2) {
|
|
614
|
+
const samples = allGenerated.slice(0, 5).join(", ");
|
|
615
|
+
findings.push({
|
|
616
|
+
category: "Critical constraints",
|
|
617
|
+
description: `Generated files detected (${samples}${allGenerated.length > 5 ? ", ..." : ""}). Do NOT edit these directly — modify the source/schema they are generated from.`,
|
|
618
|
+
evidence: `${allGenerated.length} generated files`,
|
|
619
|
+
confidence: "high",
|
|
620
|
+
discoverable: false,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
// ========================================
|
|
624
|
+
// 10. EDIT ENTRYPOINTS (where changes usually land)
|
|
625
|
+
// ========================================
|
|
626
|
+
// For routing — find where route definitions live
|
|
627
|
+
if (dominantRouter.length > 0) {
|
|
628
|
+
const routeDirs = files
|
|
629
|
+
.filter((f) => (f.includes("routes") || f.includes("routers") || f.includes("api/") || f.includes("app/api/")) &&
|
|
630
|
+
!f.includes("node_modules") && !f.includes(".test.") &&
|
|
631
|
+
(f.endsWith(".ts") || f.endsWith(".js") || f.endsWith(".py") || f.endsWith(".go")))
|
|
632
|
+
.map((f) => {
|
|
633
|
+
const parts = f.split("/");
|
|
634
|
+
// Get the directory containing route files
|
|
635
|
+
return parts.slice(0, -1).join("/");
|
|
636
|
+
})
|
|
637
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
638
|
+
.slice(0, 3);
|
|
639
|
+
if (routeDirs.length > 0) {
|
|
640
|
+
findings.push({
|
|
641
|
+
category: "Dominant patterns",
|
|
642
|
+
description: `Route definitions live in: ${routeDirs.join(", ")}. Add new endpoints here.`,
|
|
643
|
+
evidence: `${routeDirs.length} route directories`,
|
|
644
|
+
confidence: "high",
|
|
645
|
+
discoverable: false,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// For components — find where UI components live
|
|
650
|
+
const componentDirs = files
|
|
651
|
+
.filter((f) => (f.includes("/components/") || f.includes("/ui/")) &&
|
|
652
|
+
!f.includes("node_modules") && !f.includes(".test.") &&
|
|
653
|
+
(f.endsWith(".tsx") || f.endsWith(".jsx") || f.endsWith(".vue") || f.endsWith(".svelte")))
|
|
654
|
+
.map((f) => {
|
|
655
|
+
const match = f.match(/(.*\/(?:components|ui))\//);
|
|
656
|
+
return match ? match[1] : null;
|
|
657
|
+
})
|
|
658
|
+
.filter((v) => v !== null)
|
|
659
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
660
|
+
.slice(0, 3);
|
|
661
|
+
if (componentDirs.length > 0 && componentDirs.some((d) => !d.includes("node_modules"))) {
|
|
662
|
+
const filtered = componentDirs.filter((d) => !d.includes("node_modules"));
|
|
663
|
+
if (filtered.length > 0) {
|
|
664
|
+
findings.push({
|
|
665
|
+
category: "Dominant patterns",
|
|
666
|
+
description: `UI components live in: ${filtered.join(", ")}. Add new components here.`,
|
|
667
|
+
evidence: `${filtered.length} component directories`,
|
|
668
|
+
confidence: "medium",
|
|
669
|
+
discoverable: false,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// ========================================
|
|
674
|
+
// 11. KEY DIRECTORY PURPOSES (app-specific)
|
|
471
675
|
// ========================================
|
|
472
676
|
// Detect directories with clear domain purposes
|
|
473
677
|
const dirPurposes = [];
|
package/dist/types.d.ts
CHANGED