sourcebook 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +17 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +91 -0
- package/dist/generators/claude.d.ts +11 -0
- package/dist/generators/claude.js +191 -0
- package/dist/generators/copilot.d.ts +12 -0
- package/dist/generators/copilot.js +119 -0
- package/dist/generators/cursor.d.ts +17 -0
- package/dist/generators/cursor.js +123 -0
- package/dist/scanner/build.d.ts +2 -0
- package/dist/scanner/build.js +56 -0
- package/dist/scanner/frameworks.d.ts +2 -0
- package/dist/scanner/frameworks.js +230 -0
- package/dist/scanner/git.d.ts +17 -0
- package/dist/scanner/git.js +317 -0
- package/dist/scanner/graph.d.ts +17 -0
- package/dist/scanner/graph.js +251 -0
- package/dist/scanner/index.d.ts +2 -0
- package/dist/scanner/index.js +87 -0
- package/dist/scanner/patterns.d.ts +6 -0
- package/dist/scanner/patterns.js +203 -0
- package/dist/scanner/structure.d.ts +2 -0
- package/dist/scanner/structure.js +148 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +1 -0
- package/dist/utils/output.d.ts +1 -0
- package/dist/utils/output.js +10 -0
- package/package.json +53 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate Cursor rules from scan results.
|
|
3
|
+
*
|
|
4
|
+
* Cursor deprecated `.cursorrules` in favor of modular `.cursor/rules/*.mdc` files.
|
|
5
|
+
* Each .mdc file has YAML frontmatter (description, globs, alwaysApply) + markdown body.
|
|
6
|
+
*
|
|
7
|
+
* We generate a single `sourcebook.mdc` with alwaysApply: true containing
|
|
8
|
+
* the same non-discoverable findings as the Claude generator, formatted for
|
|
9
|
+
* Cursor's conventions (shorter, more directive).
|
|
10
|
+
*/
|
|
11
|
+
export function generateCursor(scan, budget) {
|
|
12
|
+
const critical = scan.findings.filter((f) => f.confidence === "high" && isCritical(f));
|
|
13
|
+
const important = scan.findings.filter((f) => f.confidence === "high" && !isCritical(f));
|
|
14
|
+
const supplementary = scan.findings.filter((f) => f.confidence === "medium");
|
|
15
|
+
const sections = [];
|
|
16
|
+
// MDC frontmatter
|
|
17
|
+
sections.push("---");
|
|
18
|
+
sections.push("description: Project conventions and constraints extracted by sourcebook");
|
|
19
|
+
sections.push("alwaysApply: true");
|
|
20
|
+
sections.push("---");
|
|
21
|
+
sections.push("");
|
|
22
|
+
// Commands
|
|
23
|
+
if (hasCommands(scan.commands)) {
|
|
24
|
+
sections.push("## Commands");
|
|
25
|
+
sections.push("");
|
|
26
|
+
if (scan.commands.dev)
|
|
27
|
+
sections.push(`- Dev: \`${scan.commands.dev}\``);
|
|
28
|
+
if (scan.commands.build)
|
|
29
|
+
sections.push(`- Build: \`${scan.commands.build}\``);
|
|
30
|
+
if (scan.commands.test)
|
|
31
|
+
sections.push(`- Test: \`${scan.commands.test}\``);
|
|
32
|
+
if (scan.commands.lint)
|
|
33
|
+
sections.push(`- Lint: \`${scan.commands.lint}\``);
|
|
34
|
+
sections.push("");
|
|
35
|
+
}
|
|
36
|
+
// Critical constraints at the top
|
|
37
|
+
if (critical.length > 0) {
|
|
38
|
+
sections.push("## Constraints");
|
|
39
|
+
sections.push("");
|
|
40
|
+
for (const finding of critical) {
|
|
41
|
+
sections.push(`- ${finding.description}`);
|
|
42
|
+
}
|
|
43
|
+
sections.push("");
|
|
44
|
+
}
|
|
45
|
+
// Stack (brief)
|
|
46
|
+
if (scan.frameworks.length > 0) {
|
|
47
|
+
sections.push("## Stack");
|
|
48
|
+
sections.push("");
|
|
49
|
+
sections.push(scan.frameworks.join(", "));
|
|
50
|
+
sections.push("");
|
|
51
|
+
}
|
|
52
|
+
// Core modules
|
|
53
|
+
if (scan.rankedFiles && scan.rankedFiles.length > 0) {
|
|
54
|
+
const top5 = scan.rankedFiles.slice(0, 5);
|
|
55
|
+
sections.push("## Core Modules");
|
|
56
|
+
sections.push("");
|
|
57
|
+
for (const { file } of top5) {
|
|
58
|
+
sections.push(`- \`${file}\``);
|
|
59
|
+
}
|
|
60
|
+
sections.push("");
|
|
61
|
+
}
|
|
62
|
+
// Conventions
|
|
63
|
+
if (important.length > 0) {
|
|
64
|
+
sections.push("## Conventions");
|
|
65
|
+
sections.push("");
|
|
66
|
+
for (const finding of important) {
|
|
67
|
+
sections.push(`- ${finding.description}`);
|
|
68
|
+
}
|
|
69
|
+
sections.push("");
|
|
70
|
+
}
|
|
71
|
+
// Additional context
|
|
72
|
+
if (supplementary.length > 0) {
|
|
73
|
+
sections.push("## Additional Context");
|
|
74
|
+
sections.push("");
|
|
75
|
+
for (const finding of supplementary) {
|
|
76
|
+
sections.push(`- ${finding.description}`);
|
|
77
|
+
}
|
|
78
|
+
sections.push("");
|
|
79
|
+
}
|
|
80
|
+
let output = sections.join("\n");
|
|
81
|
+
// Token budget enforcement
|
|
82
|
+
const charBudget = budget * 4;
|
|
83
|
+
if (output.length > charBudget) {
|
|
84
|
+
output = output.slice(0, charBudget);
|
|
85
|
+
const lastNewline = output.lastIndexOf("\n");
|
|
86
|
+
output = output.slice(0, lastNewline) + "\n";
|
|
87
|
+
}
|
|
88
|
+
return output;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Also generate the legacy .cursorrules format for backwards compatibility.
|
|
92
|
+
* Same content as the .mdc but without the frontmatter.
|
|
93
|
+
*/
|
|
94
|
+
export function generateCursorLegacy(scan, budget) {
|
|
95
|
+
const mdc = generateCursor(scan, budget);
|
|
96
|
+
// Strip the YAML frontmatter
|
|
97
|
+
const endOfFrontmatter = mdc.indexOf("---", 4);
|
|
98
|
+
if (endOfFrontmatter !== -1) {
|
|
99
|
+
return mdc.slice(endOfFrontmatter + 4).trimStart();
|
|
100
|
+
}
|
|
101
|
+
return mdc;
|
|
102
|
+
}
|
|
103
|
+
function isCritical(finding) {
|
|
104
|
+
const criticalCategories = new Set([
|
|
105
|
+
"Hidden dependencies",
|
|
106
|
+
"Circular dependencies",
|
|
107
|
+
"Core modules",
|
|
108
|
+
"Fragile code",
|
|
109
|
+
"Git history",
|
|
110
|
+
"Commit conventions",
|
|
111
|
+
]);
|
|
112
|
+
const criticalKeywords = [
|
|
113
|
+
"breaking", "blast radius", "deprecated", "don't", "must",
|
|
114
|
+
"never", "revert", "fragile", "hidden", "invisible", "coupling",
|
|
115
|
+
];
|
|
116
|
+
if (criticalCategories.has(finding.category))
|
|
117
|
+
return true;
|
|
118
|
+
const desc = finding.description.toLowerCase();
|
|
119
|
+
return criticalKeywords.some((kw) => desc.includes(kw));
|
|
120
|
+
}
|
|
121
|
+
function hasCommands(commands) {
|
|
122
|
+
return Object.values(commands).some((v) => v !== undefined);
|
|
123
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function detectBuildCommands(dir) {
|
|
4
|
+
const commands = {};
|
|
5
|
+
// Check package.json scripts
|
|
6
|
+
const pkgPath = path.join(dir, "package.json");
|
|
7
|
+
if (fs.existsSync(pkgPath)) {
|
|
8
|
+
try {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
10
|
+
const scripts = pkg.scripts || {};
|
|
11
|
+
commands.dev = scripts.dev;
|
|
12
|
+
commands.build = scripts.build;
|
|
13
|
+
commands.test = scripts.test;
|
|
14
|
+
commands.lint = scripts.lint;
|
|
15
|
+
commands.start = scripts.start;
|
|
16
|
+
// Detect non-standard but important scripts
|
|
17
|
+
for (const [name, script] of Object.entries(scripts)) {
|
|
18
|
+
if (typeof script === "string" &&
|
|
19
|
+
!commands[name] &&
|
|
20
|
+
(name.includes("migrate") ||
|
|
21
|
+
name.includes("seed") ||
|
|
22
|
+
name.includes("deploy") ||
|
|
23
|
+
name.includes("typecheck") ||
|
|
24
|
+
name.includes("generate"))) {
|
|
25
|
+
commands[name] = script;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// malformed package.json
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Check for Makefile
|
|
34
|
+
const makefilePath = path.join(dir, "Makefile");
|
|
35
|
+
if (fs.existsSync(makefilePath) && !commands.build) {
|
|
36
|
+
commands.build = "make";
|
|
37
|
+
}
|
|
38
|
+
// Check for pyproject.toml
|
|
39
|
+
const pyprojectPath = path.join(dir, "pyproject.toml");
|
|
40
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const content = fs.readFileSync(pyprojectPath, "utf-8");
|
|
43
|
+
if (content.includes("[tool.poetry.scripts]")) {
|
|
44
|
+
// Poetry project
|
|
45
|
+
if (!commands.dev)
|
|
46
|
+
commands.dev = "poetry run dev";
|
|
47
|
+
if (!commands.test)
|
|
48
|
+
commands.test = "poetry run pytest";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// can't read
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return commands;
|
|
56
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function detectFrameworks(dir, files) {
|
|
4
|
+
const detected = [];
|
|
5
|
+
// Read all package.json files (root + workspaces/sub-packages)
|
|
6
|
+
const pkgFiles = files.filter((f) => f.endsWith("package.json") && !f.includes("node_modules"));
|
|
7
|
+
if (pkgFiles.length === 0)
|
|
8
|
+
pkgFiles.push("package.json");
|
|
9
|
+
const allDeps = {};
|
|
10
|
+
for (const pkgFile of pkgFiles) {
|
|
11
|
+
const pkgPath = path.join(dir, pkgFile);
|
|
12
|
+
if (fs.existsSync(pkgPath)) {
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
15
|
+
Object.assign(allDeps, pkg.dependencies || {}, pkg.devDependencies || {});
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// malformed package.json
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// --- Next.js ---
|
|
23
|
+
if (allDeps["next"]) {
|
|
24
|
+
const findings = [];
|
|
25
|
+
const hasAppDir = files.some((f) => f.startsWith("app/") || f.startsWith("src/app/"));
|
|
26
|
+
const hasPagesDir = files.some((f) => f.startsWith("pages/") || f.startsWith("src/pages/"));
|
|
27
|
+
if (hasAppDir && hasPagesDir) {
|
|
28
|
+
findings.push({
|
|
29
|
+
category: "Next.js routing",
|
|
30
|
+
description: "Project uses BOTH App Router and Pages Router. New routes should go in the app/ directory unless there's a specific reason for pages/.",
|
|
31
|
+
rationale: "Mixed routing is a common migration state. Agents default to pages/ because training data has more examples of it.",
|
|
32
|
+
confidence: "high",
|
|
33
|
+
discoverable: false,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else if (hasAppDir) {
|
|
37
|
+
findings.push({
|
|
38
|
+
category: "Next.js routing",
|
|
39
|
+
description: "Project uses App Router (app/ directory). Use server components by default.",
|
|
40
|
+
confidence: "high",
|
|
41
|
+
discoverable: true,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Check for next.config
|
|
45
|
+
const nextConfig = files.find((f) => /^next\.config\.(js|mjs|ts)$/.test(f));
|
|
46
|
+
if (nextConfig) {
|
|
47
|
+
try {
|
|
48
|
+
const configContent = fs.readFileSync(path.join(dir, nextConfig), "utf-8");
|
|
49
|
+
if (configContent.includes("output:") && configContent.includes("standalone")) {
|
|
50
|
+
findings.push({
|
|
51
|
+
category: "Next.js deployment",
|
|
52
|
+
description: "Standalone output mode is enabled. Build produces a self-contained server in .next/standalone.",
|
|
53
|
+
confidence: "high",
|
|
54
|
+
discoverable: false,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (configContent.includes("images") && configContent.includes("remotePatterns")) {
|
|
58
|
+
findings.push({
|
|
59
|
+
category: "Next.js images",
|
|
60
|
+
description: "Remote image patterns are configured. New image domains must be added to next.config before use.",
|
|
61
|
+
rationale: "Agents will try to use next/image with arbitrary URLs and get 400 errors without this config.",
|
|
62
|
+
confidence: "high",
|
|
63
|
+
discoverable: false,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// can't read config
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
detected.push({
|
|
72
|
+
name: "Next.js",
|
|
73
|
+
version: allDeps["next"],
|
|
74
|
+
findings: findings.filter((f) => !f.discoverable),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// --- Expo / React Native ---
|
|
78
|
+
if (allDeps["expo"]) {
|
|
79
|
+
const findings = [];
|
|
80
|
+
const hasExpoRouter = !!allDeps["expo-router"];
|
|
81
|
+
if (hasExpoRouter) {
|
|
82
|
+
findings.push({
|
|
83
|
+
category: "Expo routing",
|
|
84
|
+
description: "Uses Expo Router (file-based routing in app/ directory). Follows Next.js-like conventions.",
|
|
85
|
+
confidence: "high",
|
|
86
|
+
discoverable: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Check for EAS config
|
|
90
|
+
if (files.includes("eas.json")) {
|
|
91
|
+
findings.push({
|
|
92
|
+
category: "Expo builds",
|
|
93
|
+
description: "EAS Build is configured. Use `eas build` for device builds, not `expo build`.",
|
|
94
|
+
rationale: "expo build is deprecated. Agents trained on older docs will suggest it.",
|
|
95
|
+
confidence: "high",
|
|
96
|
+
discoverable: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Check app.json for scheme
|
|
100
|
+
const appJsonPath = path.join(dir, "app.json");
|
|
101
|
+
if (fs.existsSync(appJsonPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const appJson = JSON.parse(fs.readFileSync(appJsonPath, "utf-8"));
|
|
104
|
+
if (appJson?.expo?.scheme) {
|
|
105
|
+
findings.push({
|
|
106
|
+
category: "Expo deep linking",
|
|
107
|
+
description: `Deep link scheme is "${appJson.expo.scheme}://". Use this for universal links and navigation.`,
|
|
108
|
+
confidence: "high",
|
|
109
|
+
discoverable: false,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// malformed app.json
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
detected.push({
|
|
118
|
+
name: "Expo",
|
|
119
|
+
version: allDeps["expo"],
|
|
120
|
+
findings: findings.filter((f) => !f.discoverable),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// --- React (standalone, not Next/Expo) ---
|
|
124
|
+
if (allDeps["react"] && !allDeps["next"] && !allDeps["expo"]) {
|
|
125
|
+
const findings = [];
|
|
126
|
+
const hasVite = !!allDeps["vite"];
|
|
127
|
+
if (hasVite) {
|
|
128
|
+
detected.push({ name: "Vite + React", version: allDeps["vite"], findings });
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
detected.push({ name: "React", version: allDeps["react"], findings });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// --- Supabase ---
|
|
135
|
+
if (allDeps["@supabase/supabase-js"]) {
|
|
136
|
+
const findings = [];
|
|
137
|
+
// Check for RLS awareness
|
|
138
|
+
const hasSupabaseDir = files.some((f) => f.startsWith("supabase/"));
|
|
139
|
+
if (hasSupabaseDir) {
|
|
140
|
+
findings.push({
|
|
141
|
+
category: "Supabase",
|
|
142
|
+
description: "Local Supabase setup detected (supabase/ directory). Use `supabase db push` for migrations, not the dashboard.",
|
|
143
|
+
confidence: "high",
|
|
144
|
+
discoverable: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
detected.push({
|
|
148
|
+
name: "Supabase",
|
|
149
|
+
version: allDeps["@supabase/supabase-js"],
|
|
150
|
+
findings,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// --- Tailwind CSS ---
|
|
154
|
+
if (allDeps["tailwindcss"]) {
|
|
155
|
+
const findings = [];
|
|
156
|
+
const hasTwConfig = files.some((f) => /^tailwind\.config\.(js|ts|mjs|cjs)$/.test(f));
|
|
157
|
+
if (hasTwConfig) {
|
|
158
|
+
try {
|
|
159
|
+
const configPath = files.find((f) => /^tailwind\.config\.(js|ts|mjs|cjs)$/.test(f));
|
|
160
|
+
const content = fs.readFileSync(path.join(dir, configPath), "utf-8");
|
|
161
|
+
if (content.includes("extend") && content.includes("colors")) {
|
|
162
|
+
findings.push({
|
|
163
|
+
category: "Tailwind",
|
|
164
|
+
description: "Custom color tokens are defined in tailwind.config. Use these instead of arbitrary color values.",
|
|
165
|
+
rationale: "Agents default to Tailwind's built-in palette. Using custom tokens keeps the design system consistent.",
|
|
166
|
+
confidence: "medium",
|
|
167
|
+
discoverable: false,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// can't read config
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
detected.push({
|
|
176
|
+
name: "Tailwind CSS",
|
|
177
|
+
version: allDeps["tailwindcss"],
|
|
178
|
+
findings,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// --- Express ---
|
|
182
|
+
if (allDeps["express"]) {
|
|
183
|
+
detected.push({
|
|
184
|
+
name: "Express",
|
|
185
|
+
version: allDeps["express"],
|
|
186
|
+
findings: [],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// --- TypeScript ---
|
|
190
|
+
if (allDeps["typescript"]) {
|
|
191
|
+
const findings = [];
|
|
192
|
+
const tsconfigPath = path.join(dir, "tsconfig.json");
|
|
193
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
194
|
+
try {
|
|
195
|
+
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8"));
|
|
196
|
+
const strict = tsconfig?.compilerOptions?.strict;
|
|
197
|
+
if (strict === false) {
|
|
198
|
+
findings.push({
|
|
199
|
+
category: "TypeScript",
|
|
200
|
+
description: "Strict mode is OFF. Don't add strict type annotations that would break existing patterns.",
|
|
201
|
+
confidence: "high",
|
|
202
|
+
discoverable: false,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const paths = tsconfig?.compilerOptions?.paths;
|
|
206
|
+
if (paths) {
|
|
207
|
+
const aliases = Object.keys(paths)
|
|
208
|
+
.map((k) => k.replace("/*", ""))
|
|
209
|
+
.join(", ");
|
|
210
|
+
findings.push({
|
|
211
|
+
category: "TypeScript imports",
|
|
212
|
+
description: `Path aliases configured: ${aliases}. Use these instead of relative imports.`,
|
|
213
|
+
rationale: "Agents default to relative imports. Path aliases keep imports clean.",
|
|
214
|
+
confidence: "high",
|
|
215
|
+
discoverable: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// malformed tsconfig
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
detected.push({
|
|
224
|
+
name: "TypeScript",
|
|
225
|
+
version: allDeps["typescript"],
|
|
226
|
+
findings,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return detected;
|
|
230
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Finding } from "../types.js";
|
|
2
|
+
interface GitAnalysis {
|
|
3
|
+
findings: Finding[];
|
|
4
|
+
activeAreas: string[];
|
|
5
|
+
revertedPatterns: string[];
|
|
6
|
+
coChangeClusters: [string, string, number][];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Mine git history for non-obvious context:
|
|
10
|
+
* - Reverted commits (literal "don't do this" signals)
|
|
11
|
+
* - Co-change coupling (invisible dependencies)
|
|
12
|
+
* - Recently active areas
|
|
13
|
+
* - Commit message patterns (module structure)
|
|
14
|
+
* - Rapid re-edits (code that was hard to get right)
|
|
15
|
+
*/
|
|
16
|
+
export declare function analyzeGitHistory(dir: string): Promise<GitAnalysis>;
|
|
17
|
+
export {};
|