sourcebook 0.1.0 → 0.4.1

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.
@@ -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
  }
@@ -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
  */
@@ -18,6 +18,8 @@ const IGNORE_PATTERNS = [
18
18
  "**/ios/**",
19
19
  "**/*.lock",
20
20
  "**/package-lock.json",
21
+ "**/.claude/worktrees/**",
22
+ "**/.claude/**",
21
23
  ];
22
24
  export async function scanProject(dir) {
23
25
  // Collect all files
@@ -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
- // Only analyze source files
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,12 @@ 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));
42
+ // --- Dominant API/usage patterns ---
43
+ findings.push(...detectDominantPatterns(dir, files, fileContents, frameworks));
36
44
  // Filter out discoverable findings
37
45
  return findings.filter((f) => !f.discoverable);
38
46
  }
@@ -170,6 +178,341 @@ function detectErrorHandling(contents) {
170
178
  }
171
179
  return findings;
172
180
  }
181
+ function detectPythonConventions(files, contents) {
182
+ const findings = [];
183
+ const pyFiles = [...contents.entries()].filter(([f]) => f.endsWith(".py"));
184
+ if (pyFiles.length < 3)
185
+ return findings;
186
+ // Detect __init__.py barrel pattern
187
+ const initFiles = files.filter((f) => f.endsWith("__init__.py"));
188
+ const nonEmptyInits = initFiles.filter((f) => {
189
+ const content = contents.get(f);
190
+ return content && content.trim().length > 10;
191
+ });
192
+ if (nonEmptyInits.length >= 3) {
193
+ findings.push({
194
+ category: "Python conventions",
195
+ description: "Uses __init__.py as barrel exports. Import from the package, not from internal modules.",
196
+ evidence: `${nonEmptyInits.length} non-empty __init__.py files`,
197
+ confidence: "high",
198
+ discoverable: false,
199
+ });
200
+ }
201
+ // Detect type hint usage
202
+ let typeHintCount = 0;
203
+ let noHintCount = 0;
204
+ for (const [, content] of pyFiles) {
205
+ const funcDefs = content.match(/def\s+\w+\s*\([^)]*\)/g) || [];
206
+ for (const def of funcDefs) {
207
+ if (def.includes(":") && def.includes("->"))
208
+ typeHintCount++;
209
+ else
210
+ noHintCount++;
211
+ }
212
+ }
213
+ if (typeHintCount + noHintCount > 10 && typeHintCount > noHintCount * 2) {
214
+ findings.push({
215
+ category: "Python conventions",
216
+ description: "Project uses type hints extensively. Add type annotations to all new functions.",
217
+ evidence: `${typeHintCount} typed vs ${noHintCount} untyped function signatures`,
218
+ confidence: "high",
219
+ discoverable: false,
220
+ });
221
+ }
222
+ return findings;
223
+ }
224
+ function detectGoConventions(files, contents) {
225
+ const findings = [];
226
+ const goFiles = [...contents.entries()].filter(([f]) => f.endsWith(".go"));
227
+ if (goFiles.length < 3)
228
+ return findings;
229
+ // Detect error handling style
230
+ let errNilCount = 0;
231
+ let errWrapCount = 0;
232
+ for (const [, content] of goFiles) {
233
+ errNilCount += (content.match(/if\s+err\s*!=\s*nil/g) || []).length;
234
+ errWrapCount += (content.match(/fmt\.Errorf\(.*%w/g) || []).length;
235
+ }
236
+ if (errWrapCount > 5 && errWrapCount > errNilCount * 0.3) {
237
+ findings.push({
238
+ category: "Go conventions",
239
+ description: "Project wraps errors with fmt.Errorf(%w). Use error wrapping, not bare returns.",
240
+ evidence: `${errWrapCount} wrapped errors found`,
241
+ confidence: "high",
242
+ discoverable: false,
243
+ });
244
+ }
245
+ // Detect interface-first design
246
+ let interfaceCount = 0;
247
+ for (const [, content] of goFiles) {
248
+ interfaceCount += (content.match(/type\s+\w+\s+interface\s*\{/g) || []).length;
249
+ }
250
+ if (interfaceCount >= 5) {
251
+ findings.push({
252
+ category: "Go conventions",
253
+ description: `Project uses interface-first design (${interfaceCount} interfaces). Define interfaces at the consumer, not the producer.`,
254
+ confidence: "medium",
255
+ discoverable: false,
256
+ });
257
+ }
258
+ return findings;
259
+ }
260
+ /**
261
+ * Detect dominant API/usage patterns — the conventions humans naturally
262
+ * put in handwritten briefs but agents can't infer from structure alone.
263
+ *
264
+ * This closes the gap between sourcebook and handwritten context.
265
+ */
266
+ function detectDominantPatterns(dir, files, contents, frameworks) {
267
+ const findings = [];
268
+ // Read MORE files for pattern detection — we need a wider sample
269
+ // to detect dominant patterns reliably
270
+ const allSource = files.filter((f) => (f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".js") || f.endsWith(".jsx") ||
271
+ f.endsWith(".py") || f.endsWith(".go")) &&
272
+ !f.includes("node_modules") && !f.includes(".test.") && !f.includes(".spec."));
273
+ // Read up to 100 additional files for pattern counts
274
+ const extraSample = allSource.sort(() => Math.random() - 0.5).slice(0, 100);
275
+ const allContents = new Map(contents);
276
+ for (const file of extraSample) {
277
+ if (!allContents.has(file)) {
278
+ try {
279
+ const content = fs.readFileSync(path.join(dir, file), "utf-8");
280
+ allContents.set(file, content);
281
+ }
282
+ catch { /* skip */ }
283
+ }
284
+ }
285
+ // ========================================
286
+ // 1. I18N / LOCALIZATION PATTERNS
287
+ // ========================================
288
+ const i18nPatterns = [
289
+ { pattern: "useLocale", hook: "useLocale()", count: 0, files: [] },
290
+ { pattern: "useTranslation", hook: "useTranslation()", count: 0, files: [] },
291
+ { pattern: "useTranslations", hook: "useTranslations()", count: 0, files: [] },
292
+ { pattern: "useIntl", hook: "useIntl()", count: 0, files: [] },
293
+ { pattern: "intl\\.formatMessage", hook: "intl.formatMessage()", count: 0, files: [] },
294
+ { pattern: "\\bt\\(['\"]", hook: "t(\"key\")", count: 0, files: [] },
295
+ { pattern: "i18next", hook: "i18next", count: 0, files: [] },
296
+ { pattern: "gettext", hook: "gettext()", count: 0, files: [] },
297
+ { pattern: "_\\(['\"]", hook: "_(\"string\")", count: 0, files: [] },
298
+ ];
299
+ for (const [file, content] of allContents) {
300
+ for (const p of i18nPatterns) {
301
+ if (new RegExp(p.pattern).test(content)) {
302
+ p.count++;
303
+ if (p.files.length < 3)
304
+ p.files.push(file);
305
+ }
306
+ }
307
+ }
308
+ const dominantI18n = i18nPatterns.filter((p) => p.count >= 3).sort((a, b) => b.count - a.count);
309
+ if (dominantI18n.length > 0) {
310
+ const primary = dominantI18n[0];
311
+ let desc = `User-facing strings use ${primary.hook} for internationalization.`;
312
+ // Find where translation keys live
313
+ const localeFiles = files.filter((f) => (f.includes("locale") || f.includes("i18n") || f.includes("translations") || f.includes("messages")) &&
314
+ (f.endsWith(".json") || f.endsWith(".ts") || f.endsWith(".js")) &&
315
+ !f.includes("node_modules"));
316
+ const commonLocale = localeFiles.find((f) => f.includes("en/") || f.includes("en."));
317
+ if (commonLocale) {
318
+ desc += ` Add new translation keys in ${commonLocale}.`;
319
+ }
320
+ else if (localeFiles.length > 0) {
321
+ desc += ` Translation files are in: ${localeFiles[0]}.`;
322
+ }
323
+ findings.push({
324
+ category: "Dominant patterns",
325
+ description: desc,
326
+ evidence: `${primary.count} files use ${primary.hook}`,
327
+ confidence: "high",
328
+ discoverable: false,
329
+ });
330
+ }
331
+ // ========================================
332
+ // 2. ROUTING / API PATTERNS
333
+ // ========================================
334
+ const routerPatterns = [
335
+ { pattern: "trpc\\.router|createTRPCRouter|t\\.router", name: "tRPC routers", count: 0 },
336
+ { pattern: "express\\.Router|router\\.get|router\\.post", name: "Express routers", count: 0 },
337
+ { pattern: "app\\.get\\(|app\\.post\\(|app\\.put\\(", name: "Express app routes", count: 0 },
338
+ { pattern: "Hono|app\\.route\\(|c\\.json\\(", name: "Hono routes", count: 0 },
339
+ { pattern: "FastAPI|@app\\.(get|post|put|delete)", name: "FastAPI endpoints", count: 0 },
340
+ { pattern: "flask\\.route|@app\\.route", name: "Flask routes", count: 0 },
341
+ { pattern: "gin\\.Engine|r\\.GET|r\\.POST", name: "Gin routes", count: 0 },
342
+ { pattern: "fiber\\.App|app\\.Get|app\\.Post", name: "Fiber routes", count: 0 },
343
+ ];
344
+ for (const [, content] of allContents) {
345
+ for (const p of routerPatterns) {
346
+ if (new RegExp(p.pattern).test(content)) {
347
+ p.count++;
348
+ }
349
+ }
350
+ }
351
+ const dominantRouter = routerPatterns.filter((p) => p.count >= 2).sort((a, b) => b.count - a.count);
352
+ if (dominantRouter.length > 0) {
353
+ const primary = dominantRouter[0];
354
+ findings.push({
355
+ category: "Dominant patterns",
356
+ description: `API endpoints use ${primary.name}. Follow this pattern for new routes.`,
357
+ evidence: `${primary.count} files use ${primary.name}`,
358
+ confidence: "high",
359
+ discoverable: false,
360
+ });
361
+ }
362
+ // ========================================
363
+ // 3. VALIDATION / SCHEMA PATTERNS
364
+ // ========================================
365
+ const schemaPatterns = [
366
+ { pattern: "z\\.object|z\\.string|z\\.number", name: "Zod", usage: "Use Zod schemas for validation", count: 0 },
367
+ { pattern: "BaseModel|Field\\(", name: "Pydantic", usage: "Use Pydantic BaseModel for data classes", count: 0 },
368
+ { pattern: "Joi\\.object|Joi\\.string", name: "Joi", usage: "Use Joi schemas for validation", count: 0 },
369
+ { pattern: "yup\\.object|yup\\.string", name: "Yup", usage: "Use Yup schemas for validation", count: 0 },
370
+ { pattern: "class.*Serializer.*:|serializers\\.Serializer", name: "Django serializers", usage: "Use Django REST serializers for API data", count: 0 },
371
+ { pattern: "@dataclass", name: "dataclasses", usage: "Use @dataclass for data structures", count: 0 },
372
+ ];
373
+ for (const [, content] of allContents) {
374
+ for (const p of schemaPatterns) {
375
+ if (new RegExp(p.pattern).test(content)) {
376
+ p.count++;
377
+ }
378
+ }
379
+ }
380
+ const dominantSchema = schemaPatterns.filter((p) => p.count >= 3).sort((a, b) => b.count - a.count);
381
+ if (dominantSchema.length > 0) {
382
+ const primary = dominantSchema[0];
383
+ findings.push({
384
+ category: "Dominant patterns",
385
+ description: `${primary.usage}. This is the project's standard validation approach.`,
386
+ evidence: `${primary.count} files use ${primary.name}`,
387
+ confidence: "high",
388
+ discoverable: false,
389
+ });
390
+ }
391
+ // ========================================
392
+ // 4. STATE MANAGEMENT / DATA FETCHING
393
+ // ========================================
394
+ const statePatterns = [
395
+ { pattern: "useQuery|useMutation|QueryClient", name: "React Query/TanStack Query", desc: "Data fetching uses React Query (useQuery/useMutation)", count: 0 },
396
+ { pattern: "useSWR|mutate\\(", name: "SWR", desc: "Data fetching uses SWR (useSWR)", count: 0 },
397
+ { pattern: "createSlice|configureStore", name: "Redux Toolkit", desc: "State management uses Redux Toolkit (createSlice)", count: 0 },
398
+ { pattern: "create\\(.*set.*get|useStore", name: "Zustand", desc: "State management uses Zustand", count: 0 },
399
+ { pattern: "atom\\(|useAtom", name: "Jotai", desc: "State management uses Jotai atoms", count: 0 },
400
+ ];
401
+ for (const [, content] of allContents) {
402
+ for (const p of statePatterns) {
403
+ if (new RegExp(p.pattern).test(content)) {
404
+ p.count++;
405
+ }
406
+ }
407
+ }
408
+ const dominantState = statePatterns.filter((p) => p.count >= 3).sort((a, b) => b.count - a.count);
409
+ if (dominantState.length > 0) {
410
+ const primary = dominantState[0];
411
+ findings.push({
412
+ category: "Dominant patterns",
413
+ description: `${primary.desc}. Follow this pattern for new data operations.`,
414
+ evidence: `${primary.count} files`,
415
+ confidence: "high",
416
+ discoverable: false,
417
+ });
418
+ }
419
+ // ========================================
420
+ // 5. TESTING PATTERNS
421
+ // ========================================
422
+ const testPatterns = [
423
+ { pattern: "describe\\(|it\\(|test\\(", name: "Jest/Vitest", count: 0 },
424
+ { pattern: "def test_|class Test|pytest", name: "pytest", count: 0 },
425
+ { pattern: "func Test.*\\(t \\*testing\\.T\\)", name: "Go testing", count: 0 },
426
+ { pattern: "expect\\(.*\\)\\.to", name: "Chai/expect", count: 0 },
427
+ ];
428
+ const testFiles = [...allContents.entries()].filter(([f]) => f.includes(".test.") || f.includes(".spec.") || f.includes("_test.") || f.startsWith("test_"));
429
+ // Read a few test files specifically
430
+ const testSampled = files
431
+ .filter((f) => f.includes(".test.") || f.includes(".spec.") || f.includes("_test.go") || f.includes("test_"))
432
+ .slice(0, 10);
433
+ for (const file of testSampled) {
434
+ if (!allContents.has(file)) {
435
+ try {
436
+ const content = fs.readFileSync(path.join(dir, file), "utf-8");
437
+ allContents.set(file, content);
438
+ }
439
+ catch { /* skip */ }
440
+ }
441
+ }
442
+ for (const [f, content] of allContents) {
443
+ if (f.includes("test") || f.includes("spec")) {
444
+ for (const p of testPatterns) {
445
+ if (new RegExp(p.pattern).test(content)) {
446
+ p.count++;
447
+ }
448
+ }
449
+ }
450
+ }
451
+ const dominantTest = testPatterns.filter((p) => p.count >= 2).sort((a, b) => b.count - a.count);
452
+ if (dominantTest.length > 0) {
453
+ const primary = dominantTest[0];
454
+ // Also detect common test utilities/helpers
455
+ const testHelperFiles = files.filter((f) => (f.includes("test-utils") || f.includes("testUtils") || f.includes("fixtures") || f.includes("helpers")) &&
456
+ (f.includes("test") || f.includes("spec")));
457
+ let desc = `Tests use ${primary.name}.`;
458
+ if (testHelperFiles.length > 0) {
459
+ desc += ` Test utilities in: ${testHelperFiles[0]}.`;
460
+ }
461
+ findings.push({
462
+ category: "Dominant patterns",
463
+ description: desc,
464
+ evidence: `${primary.count} test files`,
465
+ confidence: "high",
466
+ discoverable: false,
467
+ });
468
+ }
469
+ // ========================================
470
+ // 6. KEY DIRECTORY PURPOSES (app-specific)
471
+ // ========================================
472
+ // Detect directories with clear domain purposes
473
+ const dirPurposes = [];
474
+ // App store / plugin / integration directories
475
+ // Only match top-level integration directories (not deeply nested editor plugins etc.)
476
+ const integrationDirCandidates = ["app-store", "plugins", "integrations", "addons", "extensions"];
477
+ let bestIntegrationDir = "";
478
+ let bestIntegrationCount = 0;
479
+ for (const dirName of integrationDirCandidates) {
480
+ // Find files matching pattern: <prefix>/<dirName>/<integration-name>/<file>
481
+ const matchingFiles = files.filter((f) => new RegExp(`/${dirName}/[^/]+/[^/]+`).test(f) && !f.includes("node_modules"));
482
+ const integrationNames = matchingFiles
483
+ .map((f) => {
484
+ const match = f.match(new RegExp(`(.*?/${dirName})/([^/]+)/`));
485
+ return match ? { dir: match[1], name: match[2] } : null;
486
+ })
487
+ .filter((v) => v !== null && !v.name.startsWith("_"));
488
+ const uniqueNames = [...new Set(integrationNames.map((i) => i.name))];
489
+ if (uniqueNames.length > bestIntegrationCount) {
490
+ bestIntegrationCount = uniqueNames.length;
491
+ bestIntegrationDir = integrationNames[0]?.dir || "";
492
+ }
493
+ }
494
+ if (bestIntegrationCount >= 3 && bestIntegrationDir) {
495
+ const integrations = files
496
+ .filter((f) => f.startsWith(bestIntegrationDir + "/") && !f.includes("node_modules"))
497
+ .map((f) => {
498
+ const suffix = f.slice(bestIntegrationDir.length + 1);
499
+ return suffix.split("/")[0];
500
+ })
501
+ .filter((v) => v && !v.startsWith("_") && v !== "templates" && !v.includes("."))
502
+ .filter((v, i, a) => a.indexOf(v) === i);
503
+ if (integrations.length > 0) {
504
+ const sampleIntegrations = integrations.slice(0, 6).join(", ");
505
+ findings.push({
506
+ category: "Dominant patterns",
507
+ description: `Third-party integrations live under ${bestIntegrationDir}/ (${sampleIntegrations}${integrations.length > 6 ? ", ..." : ""}). Each integration has its own directory with components, lib, and API code.`,
508
+ evidence: `${integrations.length} integrations found`,
509
+ confidence: "high",
510
+ discoverable: false,
511
+ });
512
+ }
513
+ }
514
+ return findings;
515
+ }
173
516
  function detectExportPatterns(contents) {
174
517
  const findings = [];
175
518
  let defaultExports = 0;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "sourcebook",
3
- "version": "0.1.0",
3
+ "version": "0.4.1",
4
4
  "description": "Extract the conventions, constraints, and architectural truths your AI coding agents keep missing.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "sourcebook": "./dist/cli.js"
7
+ "sourcebook": "dist/cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",
@@ -22,14 +22,15 @@
22
22
  "cli",
23
23
  "code-analysis",
24
24
  "llm",
25
- "agents"
25
+ "agents",
26
+ "mcp"
26
27
  ],
27
- "author": "maroond",
28
- "license": "MIT",
28
+ "author": "maroond labs <roy@maroond.ai>",
29
+ "license": "BSL-1.1",
29
30
  "homepage": "https://sourcebook.run",
30
31
  "repository": {
31
32
  "type": "git",
32
- "url": "https://github.com/maroondlabs/sourcebook.git"
33
+ "url": "git+https://github.com/maroondlabs/sourcebook.git"
33
34
  },
34
35
  "bugs": {
35
36
  "url": "https://github.com/maroondlabs/sourcebook/issues"