sourcebook 0.3.0 → 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.
@@ -39,6 +39,8 @@ export async function detectPatterns(dir, files, frameworks) {
39
39
  findings.push(...detectPythonConventions(files, fileContents));
40
40
  // --- Go conventions ---
41
41
  findings.push(...detectGoConventions(files, fileContents));
42
+ // --- Dominant API/usage patterns ---
43
+ findings.push(...detectDominantPatterns(dir, files, fileContents, frameworks));
42
44
  // Filter out discoverable findings
43
45
  return findings.filter((f) => !f.discoverable);
44
46
  }
@@ -255,6 +257,466 @@ function detectGoConventions(files, contents) {
255
257
  }
256
258
  return findings;
257
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. 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)
675
+ // ========================================
676
+ // Detect directories with clear domain purposes
677
+ const dirPurposes = [];
678
+ // App store / plugin / integration directories
679
+ // Only match top-level integration directories (not deeply nested editor plugins etc.)
680
+ const integrationDirCandidates = ["app-store", "plugins", "integrations", "addons", "extensions"];
681
+ let bestIntegrationDir = "";
682
+ let bestIntegrationCount = 0;
683
+ for (const dirName of integrationDirCandidates) {
684
+ // Find files matching pattern: <prefix>/<dirName>/<integration-name>/<file>
685
+ const matchingFiles = files.filter((f) => new RegExp(`/${dirName}/[^/]+/[^/]+`).test(f) && !f.includes("node_modules"));
686
+ const integrationNames = matchingFiles
687
+ .map((f) => {
688
+ const match = f.match(new RegExp(`(.*?/${dirName})/([^/]+)/`));
689
+ return match ? { dir: match[1], name: match[2] } : null;
690
+ })
691
+ .filter((v) => v !== null && !v.name.startsWith("_"));
692
+ const uniqueNames = [...new Set(integrationNames.map((i) => i.name))];
693
+ if (uniqueNames.length > bestIntegrationCount) {
694
+ bestIntegrationCount = uniqueNames.length;
695
+ bestIntegrationDir = integrationNames[0]?.dir || "";
696
+ }
697
+ }
698
+ if (bestIntegrationCount >= 3 && bestIntegrationDir) {
699
+ const integrations = files
700
+ .filter((f) => f.startsWith(bestIntegrationDir + "/") && !f.includes("node_modules"))
701
+ .map((f) => {
702
+ const suffix = f.slice(bestIntegrationDir.length + 1);
703
+ return suffix.split("/")[0];
704
+ })
705
+ .filter((v) => v && !v.startsWith("_") && v !== "templates" && !v.includes("."))
706
+ .filter((v, i, a) => a.indexOf(v) === i);
707
+ if (integrations.length > 0) {
708
+ const sampleIntegrations = integrations.slice(0, 6).join(", ");
709
+ findings.push({
710
+ category: "Dominant patterns",
711
+ description: `Third-party integrations live under ${bestIntegrationDir}/ (${sampleIntegrations}${integrations.length > 6 ? ", ..." : ""}). Each integration has its own directory with components, lib, and API code.`,
712
+ evidence: `${integrations.length} integrations found`,
713
+ confidence: "high",
714
+ discoverable: false,
715
+ });
716
+ }
717
+ }
718
+ return findings;
719
+ }
258
720
  function detectExportPatterns(contents) {
259
721
  const findings = [];
260
722
  let defaultExports = 0;
package/dist/types.d.ts CHANGED
@@ -48,4 +48,6 @@ export interface ProjectScan {
48
48
  file: string;
49
49
  score: number;
50
50
  }[];
51
+ /** Detected repo mode for context prioritization */
52
+ repoMode?: "app" | "library" | "monorepo";
51
53
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "sourcebook",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
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"