sourcebook 0.1.0 → 0.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/README.md +4 -0
- package/dist/cli.js +18 -2
- package/dist/commands/diff.d.ts +12 -0
- package/dist/commands/diff.js +97 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +177 -0
- package/dist/generators/claude.js +61 -111
- package/dist/generators/copilot.d.ts +1 -7
- package/dist/generators/copilot.js +65 -80
- package/dist/generators/cursor.d.ts +3 -9
- package/dist/generators/cursor.js +49 -79
- package/dist/generators/shared.d.ts +34 -0
- package/dist/generators/shared.js +87 -0
- package/dist/scanner/build.js +28 -0
- package/dist/scanner/frameworks.js +141 -0
- package/dist/scanner/git.js +69 -0
- package/dist/scanner/index.js +2 -0
- package/dist/scanner/patterns.js +87 -2
- package/package.json +1 -1
|
@@ -226,5 +226,146 @@ export async function detectFrameworks(dir, files) {
|
|
|
226
226
|
findings,
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
|
+
// --- Python ---
|
|
230
|
+
const hasPyproject = fs.existsSync(path.join(dir, "pyproject.toml"));
|
|
231
|
+
const hasRequirements = fs.existsSync(path.join(dir, "requirements.txt"));
|
|
232
|
+
const hasSetupPy = fs.existsSync(path.join(dir, "setup.py"));
|
|
233
|
+
if (hasPyproject || hasRequirements || hasSetupPy) {
|
|
234
|
+
const findings = [];
|
|
235
|
+
let pyDeps = "";
|
|
236
|
+
if (hasPyproject) {
|
|
237
|
+
try {
|
|
238
|
+
pyDeps = fs.readFileSync(path.join(dir, "pyproject.toml"), "utf-8");
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
}
|
|
242
|
+
else if (hasRequirements) {
|
|
243
|
+
try {
|
|
244
|
+
pyDeps = fs.readFileSync(path.join(dir, "requirements.txt"), "utf-8");
|
|
245
|
+
}
|
|
246
|
+
catch { }
|
|
247
|
+
}
|
|
248
|
+
const pyDepsLower = pyDeps.toLowerCase();
|
|
249
|
+
// Django
|
|
250
|
+
if (pyDepsLower.includes("django")) {
|
|
251
|
+
const hasManagePy = files.includes("manage.py");
|
|
252
|
+
const settingsFile = files.find((f) => f.endsWith("settings.py") || f.includes("settings/"));
|
|
253
|
+
findings.push({
|
|
254
|
+
category: "Django",
|
|
255
|
+
description: `Django project${settingsFile ? ` (settings: ${settingsFile})` : ""}. Use \`python manage.py\` for management commands.`,
|
|
256
|
+
confidence: "high",
|
|
257
|
+
discoverable: false,
|
|
258
|
+
});
|
|
259
|
+
if (files.some((f) => f.endsWith("models.py"))) {
|
|
260
|
+
findings.push({
|
|
261
|
+
category: "Django",
|
|
262
|
+
description: "After modifying models, run `python manage.py makemigrations && python manage.py migrate`.",
|
|
263
|
+
rationale: "Agents forget to create migrations after model changes, causing runtime errors.",
|
|
264
|
+
confidence: "high",
|
|
265
|
+
discoverable: false,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
detected.push({ name: "Django", findings });
|
|
269
|
+
}
|
|
270
|
+
// FastAPI
|
|
271
|
+
else if (pyDepsLower.includes("fastapi")) {
|
|
272
|
+
findings.push({
|
|
273
|
+
category: "FastAPI",
|
|
274
|
+
description: "FastAPI project. Use Pydantic models for request/response schemas, not raw dicts.",
|
|
275
|
+
confidence: "high",
|
|
276
|
+
discoverable: false,
|
|
277
|
+
});
|
|
278
|
+
if (pyDepsLower.includes("sqlalchemy") || pyDepsLower.includes("sqlmodel")) {
|
|
279
|
+
findings.push({
|
|
280
|
+
category: "FastAPI",
|
|
281
|
+
description: "Uses SQLAlchemy/SQLModel for ORM. Database sessions must be properly closed (use dependency injection).",
|
|
282
|
+
confidence: "high",
|
|
283
|
+
discoverable: false,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
detected.push({ name: "FastAPI", findings });
|
|
287
|
+
}
|
|
288
|
+
// Flask
|
|
289
|
+
else if (pyDepsLower.includes("flask")) {
|
|
290
|
+
detected.push({ name: "Flask", findings: [] });
|
|
291
|
+
}
|
|
292
|
+
// Generic Python
|
|
293
|
+
else {
|
|
294
|
+
detected.push({ name: "Python", findings: [] });
|
|
295
|
+
}
|
|
296
|
+
// pytest detection
|
|
297
|
+
if (pyDepsLower.includes("pytest")) {
|
|
298
|
+
findings.push({
|
|
299
|
+
category: "Testing",
|
|
300
|
+
description: "Uses pytest. Test files should be named `test_*.py` or `*_test.py`.",
|
|
301
|
+
confidence: "high",
|
|
302
|
+
discoverable: false,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
// Virtual environment detection
|
|
306
|
+
const hasVenv = files.some((f) => f.startsWith(".venv/") || f.startsWith("venv/"));
|
|
307
|
+
if (hasVenv) {
|
|
308
|
+
findings.push({
|
|
309
|
+
category: "Python environment",
|
|
310
|
+
description: "Virtual environment detected. Activate with `source .venv/bin/activate` before running commands.",
|
|
311
|
+
confidence: "medium",
|
|
312
|
+
discoverable: false,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// --- Go ---
|
|
317
|
+
const hasGoMod = fs.existsSync(path.join(dir, "go.mod"));
|
|
318
|
+
if (hasGoMod) {
|
|
319
|
+
const findings = [];
|
|
320
|
+
try {
|
|
321
|
+
const goMod = fs.readFileSync(path.join(dir, "go.mod"), "utf-8");
|
|
322
|
+
const moduleMatch = goMod.match(/^module\s+(.+)$/m);
|
|
323
|
+
if (moduleMatch) {
|
|
324
|
+
findings.push({
|
|
325
|
+
category: "Go module",
|
|
326
|
+
description: `Module path: ${moduleMatch[1]}. Use this as the import prefix for all internal packages.`,
|
|
327
|
+
confidence: "high",
|
|
328
|
+
discoverable: false,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// Detect web frameworks
|
|
332
|
+
if (goMod.includes("github.com/gin-gonic/gin")) {
|
|
333
|
+
detected.push({ name: "Go + Gin", findings });
|
|
334
|
+
}
|
|
335
|
+
else if (goMod.includes("github.com/labstack/echo")) {
|
|
336
|
+
detected.push({ name: "Go + Echo", findings });
|
|
337
|
+
}
|
|
338
|
+
else if (goMod.includes("github.com/gofiber/fiber")) {
|
|
339
|
+
detected.push({ name: "Go + Fiber", findings });
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
detected.push({ name: "Go", findings });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
detected.push({ name: "Go", findings: [] });
|
|
347
|
+
}
|
|
348
|
+
// cmd/ vs pkg/ layout
|
|
349
|
+
const hasCmdDir = files.some((f) => f.startsWith("cmd/"));
|
|
350
|
+
const hasPkgDir = files.some((f) => f.startsWith("pkg/"));
|
|
351
|
+
const hasInternalDir = files.some((f) => f.startsWith("internal/"));
|
|
352
|
+
if (hasCmdDir) {
|
|
353
|
+
findings.push({
|
|
354
|
+
category: "Go layout",
|
|
355
|
+
description: `Standard Go project layout: ${hasCmdDir ? "cmd/" : ""}${hasPkgDir ? " pkg/" : ""}${hasInternalDir ? " internal/" : ""}. Entry points are in cmd/ subdirectories.`,
|
|
356
|
+
confidence: "high",
|
|
357
|
+
discoverable: false,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (hasInternalDir) {
|
|
361
|
+
findings.push({
|
|
362
|
+
category: "Go visibility",
|
|
363
|
+
description: "internal/ packages cannot be imported by external modules. Keep private code here.",
|
|
364
|
+
rationale: "Go enforces this at the compiler level. Agents may try to import internal packages from external code.",
|
|
365
|
+
confidence: "high",
|
|
366
|
+
discoverable: false,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
229
370
|
return detected;
|
|
230
371
|
}
|
package/dist/scanner/git.js
CHANGED
|
@@ -19,6 +19,8 @@ export async function analyzeGitHistory(dir) {
|
|
|
19
19
|
}
|
|
20
20
|
// 1. Reverted commits -- "don't do this" signals
|
|
21
21
|
findings.push(...detectRevertedPatterns(dir, revertedPatterns));
|
|
22
|
+
// 1b. Anti-patterns from reverts and deleted approaches
|
|
23
|
+
findings.push(...detectAntiPatterns(dir));
|
|
22
24
|
// 2. Recently active areas
|
|
23
25
|
findings.push(...detectActiveAreas(dir, activeAreas));
|
|
24
26
|
// 3. Co-change coupling -- invisible dependencies
|
|
@@ -84,6 +86,73 @@ function detectRevertedPatterns(dir, revertedPatterns) {
|
|
|
84
86
|
}
|
|
85
87
|
return findings;
|
|
86
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Detect anti-patterns from reverted commits and deleted files.
|
|
91
|
+
* Reverts contain the original commit message — extract the approach that failed.
|
|
92
|
+
* Deleted files may indicate abandoned approaches.
|
|
93
|
+
*/
|
|
94
|
+
function detectAntiPatterns(dir) {
|
|
95
|
+
const findings = [];
|
|
96
|
+
// Extract detailed info from reverted commits
|
|
97
|
+
const revertLog = git(dir, 'log --grep="^Revert" --format="%s" --since="1 year ago" -20');
|
|
98
|
+
if (revertLog.trim()) {
|
|
99
|
+
const antiPatterns = [];
|
|
100
|
+
for (const line of revertLog.trim().split("\n").filter(Boolean)) {
|
|
101
|
+
const match = line.match(/^Revert "(.+)"/);
|
|
102
|
+
if (match) {
|
|
103
|
+
antiPatterns.push(match[1]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (antiPatterns.length > 0) {
|
|
107
|
+
for (const pattern of antiPatterns.slice(0, 5)) {
|
|
108
|
+
findings.push({
|
|
109
|
+
category: "Anti-patterns",
|
|
110
|
+
description: `Tried and reverted: "${pattern}". This approach was explicitly rejected.`,
|
|
111
|
+
rationale: "This commit was made and then reverted. The approach failed for a reason. Do not re-attempt without understanding why it was rolled back.",
|
|
112
|
+
confidence: "high",
|
|
113
|
+
discoverable: false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Detect files deleted in bulk (abandoned features/approaches)
|
|
119
|
+
const deletedLog = git(dir, 'log --diff-filter=D --name-only --pretty=format:"COMMIT %s" --since="6 months ago" -50');
|
|
120
|
+
if (deletedLog.trim()) {
|
|
121
|
+
const deletionBatches = [];
|
|
122
|
+
let currentMessage = "";
|
|
123
|
+
let currentFiles = [];
|
|
124
|
+
for (const line of deletedLog.split("\n")) {
|
|
125
|
+
const commitMatch = line.match(/^"?COMMIT (.+)"?$/);
|
|
126
|
+
if (commitMatch) {
|
|
127
|
+
if (currentFiles.length >= 3) {
|
|
128
|
+
deletionBatches.push({ message: currentMessage, files: currentFiles });
|
|
129
|
+
}
|
|
130
|
+
currentMessage = commitMatch[1];
|
|
131
|
+
currentFiles = [];
|
|
132
|
+
}
|
|
133
|
+
else if (line.trim() && !line.includes("node_modules")) {
|
|
134
|
+
currentFiles.push(line.trim());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (currentFiles.length >= 3) {
|
|
138
|
+
deletionBatches.push({ message: currentMessage, files: currentFiles });
|
|
139
|
+
}
|
|
140
|
+
// Only report significant deletions (3+ files in one commit = abandoned feature)
|
|
141
|
+
for (const batch of deletionBatches.slice(0, 3)) {
|
|
142
|
+
if (batch.files.length >= 3) {
|
|
143
|
+
const fileList = batch.files.slice(0, 3).map((f) => path.basename(f)).join(", ");
|
|
144
|
+
findings.push({
|
|
145
|
+
category: "Anti-patterns",
|
|
146
|
+
description: `Abandoned: "${batch.message}" (${batch.files.length} files deleted including ${fileList})`,
|
|
147
|
+
rationale: "A batch of files was deleted in a single commit, suggesting an abandoned approach or feature removal.",
|
|
148
|
+
confidence: "medium",
|
|
149
|
+
discoverable: false,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return findings;
|
|
155
|
+
}
|
|
87
156
|
/**
|
|
88
157
|
* Find recently active areas -- where development is concentrated.
|
|
89
158
|
*/
|
package/dist/scanner/index.js
CHANGED
package/dist/scanner/patterns.js
CHANGED
|
@@ -6,11 +6,13 @@ import path from "node:path";
|
|
|
6
6
|
*/
|
|
7
7
|
export async function detectPatterns(dir, files, frameworks) {
|
|
8
8
|
const findings = [];
|
|
9
|
-
//
|
|
9
|
+
// Analyze source files (JS/TS + Python + Go)
|
|
10
10
|
const sourceFiles = files.filter((f) => f.endsWith(".ts") ||
|
|
11
11
|
f.endsWith(".tsx") ||
|
|
12
12
|
f.endsWith(".js") ||
|
|
13
|
-
f.endsWith(".jsx")
|
|
13
|
+
f.endsWith(".jsx") ||
|
|
14
|
+
f.endsWith(".py") ||
|
|
15
|
+
f.endsWith(".go"));
|
|
14
16
|
// Sample files for pattern detection (don't read everything)
|
|
15
17
|
const sampled = sampleFiles(sourceFiles, 50);
|
|
16
18
|
const fileContents = new Map();
|
|
@@ -33,6 +35,10 @@ export async function detectPatterns(dir, files, frameworks) {
|
|
|
33
35
|
findings.push(...detectErrorHandling(fileContents));
|
|
34
36
|
// --- Export patterns ---
|
|
35
37
|
findings.push(...detectExportPatterns(fileContents));
|
|
38
|
+
// --- Python conventions ---
|
|
39
|
+
findings.push(...detectPythonConventions(files, fileContents));
|
|
40
|
+
// --- Go conventions ---
|
|
41
|
+
findings.push(...detectGoConventions(files, fileContents));
|
|
36
42
|
// Filter out discoverable findings
|
|
37
43
|
return findings.filter((f) => !f.discoverable);
|
|
38
44
|
}
|
|
@@ -170,6 +176,85 @@ function detectErrorHandling(contents) {
|
|
|
170
176
|
}
|
|
171
177
|
return findings;
|
|
172
178
|
}
|
|
179
|
+
function detectPythonConventions(files, contents) {
|
|
180
|
+
const findings = [];
|
|
181
|
+
const pyFiles = [...contents.entries()].filter(([f]) => f.endsWith(".py"));
|
|
182
|
+
if (pyFiles.length < 3)
|
|
183
|
+
return findings;
|
|
184
|
+
// Detect __init__.py barrel pattern
|
|
185
|
+
const initFiles = files.filter((f) => f.endsWith("__init__.py"));
|
|
186
|
+
const nonEmptyInits = initFiles.filter((f) => {
|
|
187
|
+
const content = contents.get(f);
|
|
188
|
+
return content && content.trim().length > 10;
|
|
189
|
+
});
|
|
190
|
+
if (nonEmptyInits.length >= 3) {
|
|
191
|
+
findings.push({
|
|
192
|
+
category: "Python conventions",
|
|
193
|
+
description: "Uses __init__.py as barrel exports. Import from the package, not from internal modules.",
|
|
194
|
+
evidence: `${nonEmptyInits.length} non-empty __init__.py files`,
|
|
195
|
+
confidence: "high",
|
|
196
|
+
discoverable: false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Detect type hint usage
|
|
200
|
+
let typeHintCount = 0;
|
|
201
|
+
let noHintCount = 0;
|
|
202
|
+
for (const [, content] of pyFiles) {
|
|
203
|
+
const funcDefs = content.match(/def\s+\w+\s*\([^)]*\)/g) || [];
|
|
204
|
+
for (const def of funcDefs) {
|
|
205
|
+
if (def.includes(":") && def.includes("->"))
|
|
206
|
+
typeHintCount++;
|
|
207
|
+
else
|
|
208
|
+
noHintCount++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (typeHintCount + noHintCount > 10 && typeHintCount > noHintCount * 2) {
|
|
212
|
+
findings.push({
|
|
213
|
+
category: "Python conventions",
|
|
214
|
+
description: "Project uses type hints extensively. Add type annotations to all new functions.",
|
|
215
|
+
evidence: `${typeHintCount} typed vs ${noHintCount} untyped function signatures`,
|
|
216
|
+
confidence: "high",
|
|
217
|
+
discoverable: false,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return findings;
|
|
221
|
+
}
|
|
222
|
+
function detectGoConventions(files, contents) {
|
|
223
|
+
const findings = [];
|
|
224
|
+
const goFiles = [...contents.entries()].filter(([f]) => f.endsWith(".go"));
|
|
225
|
+
if (goFiles.length < 3)
|
|
226
|
+
return findings;
|
|
227
|
+
// Detect error handling style
|
|
228
|
+
let errNilCount = 0;
|
|
229
|
+
let errWrapCount = 0;
|
|
230
|
+
for (const [, content] of goFiles) {
|
|
231
|
+
errNilCount += (content.match(/if\s+err\s*!=\s*nil/g) || []).length;
|
|
232
|
+
errWrapCount += (content.match(/fmt\.Errorf\(.*%w/g) || []).length;
|
|
233
|
+
}
|
|
234
|
+
if (errWrapCount > 5 && errWrapCount > errNilCount * 0.3) {
|
|
235
|
+
findings.push({
|
|
236
|
+
category: "Go conventions",
|
|
237
|
+
description: "Project wraps errors with fmt.Errorf(%w). Use error wrapping, not bare returns.",
|
|
238
|
+
evidence: `${errWrapCount} wrapped errors found`,
|
|
239
|
+
confidence: "high",
|
|
240
|
+
discoverable: false,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Detect interface-first design
|
|
244
|
+
let interfaceCount = 0;
|
|
245
|
+
for (const [, content] of goFiles) {
|
|
246
|
+
interfaceCount += (content.match(/type\s+\w+\s+interface\s*\{/g) || []).length;
|
|
247
|
+
}
|
|
248
|
+
if (interfaceCount >= 5) {
|
|
249
|
+
findings.push({
|
|
250
|
+
category: "Go conventions",
|
|
251
|
+
description: `Project uses interface-first design (${interfaceCount} interfaces). Define interfaces at the consumer, not the producer.`,
|
|
252
|
+
confidence: "medium",
|
|
253
|
+
discoverable: false,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return findings;
|
|
257
|
+
}
|
|
173
258
|
function detectExportPatterns(contents) {
|
|
174
259
|
const findings = [];
|
|
175
260
|
let defaultExports = 0;
|