docorbit 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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +660 -0
  3. package/apps/cli/bin/docorbit.js +8 -0
  4. package/apps/cli/src/commands/add.ts +44 -0
  5. package/apps/cli/src/commands/api.ts +38 -0
  6. package/apps/cli/src/commands/context.ts +47 -0
  7. package/apps/cli/src/commands/dashboard.ts +55 -0
  8. package/apps/cli/src/commands/diff.ts +30 -0
  9. package/apps/cli/src/commands/evaluate.ts +133 -0
  10. package/apps/cli/src/commands/examples.ts +39 -0
  11. package/apps/cli/src/commands/export.ts +89 -0
  12. package/apps/cli/src/commands/impact.ts +31 -0
  13. package/apps/cli/src/commands/init.ts +69 -0
  14. package/apps/cli/src/commands/inspect.ts +30 -0
  15. package/apps/cli/src/commands/mcp.ts +72 -0
  16. package/apps/cli/src/commands/pitfalls.ts +38 -0
  17. package/apps/cli/src/commands/recipes.ts +35 -0
  18. package/apps/cli/src/commands/search.ts +48 -0
  19. package/apps/cli/src/commands/update.ts +73 -0
  20. package/apps/cli/src/commands/verify.ts +48 -0
  21. package/apps/cli/src/formatters/colors.ts +23 -0
  22. package/apps/cli/src/formatters/inspection.ts +102 -0
  23. package/apps/cli/src/formatters/knowledge.ts +272 -0
  24. package/apps/cli/src/formatters/retrieval.ts +74 -0
  25. package/apps/cli/src/formatters/terminal.ts +6 -0
  26. package/apps/cli/src/formatters/verification.ts +126 -0
  27. package/apps/cli/src/index.ts +409 -0
  28. package/bin/docorbit.js +8 -0
  29. package/package.json +46 -0
  30. package/packages/core/src/dashboard/server.ts +314 -0
  31. package/packages/core/src/dashboard/ui.ts +586 -0
  32. package/packages/core/src/implementation-service.ts +451 -0
  33. package/packages/core/src/index.ts +7 -0
  34. package/packages/core/src/inspector.ts +71 -0
  35. package/packages/core/src/pipeline.ts +331 -0
  36. package/packages/crawler/src/config.ts +12 -0
  37. package/packages/crawler/src/fetcher.ts +185 -0
  38. package/packages/crawler/src/index.ts +2 -0
  39. package/packages/discovery/src/index.ts +31 -0
  40. package/packages/discovery/src/provider.ts +47 -0
  41. package/packages/discovery/src/providers/generic.ts +98 -0
  42. package/packages/discovery/src/providers/github.ts +61 -0
  43. package/packages/discovery/src/providers/llms-txt.ts +73 -0
  44. package/packages/discovery/src/providers/markdown.ts +48 -0
  45. package/packages/discovery/src/providers/openapi.ts +91 -0
  46. package/packages/discovery/src/providers/sitemap.ts +62 -0
  47. package/packages/discovery/src/providers/skill.ts +54 -0
  48. package/packages/discovery/src/ranker.ts +123 -0
  49. package/packages/evaluation/src/dataset.ts +963 -0
  50. package/packages/evaluation/src/index.ts +8 -0
  51. package/packages/evaluation/src/runner.ts +241 -0
  52. package/packages/evaluation/src/strategies/context7-runner.ts +269 -0
  53. package/packages/evaluation/src/strategies/docorbit-runner.ts +228 -0
  54. package/packages/evaluation/src/strategies/firecrawl-runner.ts +172 -0
  55. package/packages/evaluation/src/strategies/web-search-runner.ts +194 -0
  56. package/packages/evaluation/src/types.ts +34 -0
  57. package/packages/evaluation/src/version-matcher.ts +73 -0
  58. package/packages/export/src/agents-md.ts +200 -0
  59. package/packages/export/src/claude-md.ts +141 -0
  60. package/packages/export/src/docs-map.ts +150 -0
  61. package/packages/export/src/index.ts +6 -0
  62. package/packages/export/src/llms-txt.ts +96 -0
  63. package/packages/export/src/service.ts +250 -0
  64. package/packages/export/src/skill-md.ts +128 -0
  65. package/packages/mcp/src/index.ts +46 -0
  66. package/packages/mcp/src/resources/index.ts +189 -0
  67. package/packages/mcp/src/server.ts +278 -0
  68. package/packages/mcp/src/tools/analyze-impact.ts +74 -0
  69. package/packages/mcp/src/tools/check-api.ts +86 -0
  70. package/packages/mcp/src/tools/diff-docs.ts +68 -0
  71. package/packages/mcp/src/tools/export-context.ts +73 -0
  72. package/packages/mcp/src/tools/find-api.ts +99 -0
  73. package/packages/mcp/src/tools/find-example.ts +100 -0
  74. package/packages/mcp/src/tools/find-pitfall.ts +94 -0
  75. package/packages/mcp/src/tools/find-recipe.ts +98 -0
  76. package/packages/mcp/src/tools/get-doc.ts +130 -0
  77. package/packages/mcp/src/tools/get-docs-map.ts +64 -0
  78. package/packages/mcp/src/tools/get-version.ts +118 -0
  79. package/packages/mcp/src/tools/implementation-context.ts +88 -0
  80. package/packages/mcp/src/tools/index.ts +59 -0
  81. package/packages/mcp/src/tools/list-sources.ts +85 -0
  82. package/packages/mcp/src/tools/search-docs.ts +123 -0
  83. package/packages/mcp/src/tools/types.ts +28 -0
  84. package/packages/mcp/src/transports/http.ts +256 -0
  85. package/packages/mcp/src/transports/stdio.ts +105 -0
  86. package/packages/mcp/src/transports/types.ts +6 -0
  87. package/packages/mcp/src/types.ts +102 -0
  88. package/packages/normalizer/src/example-indexer.ts +240 -0
  89. package/packages/normalizer/src/html.ts +253 -0
  90. package/packages/normalizer/src/index.ts +8 -0
  91. package/packages/normalizer/src/llms.ts +83 -0
  92. package/packages/normalizer/src/openapi/endpoint-parser.ts +406 -0
  93. package/packages/normalizer/src/openapi/schema-resolver.ts +111 -0
  94. package/packages/normalizer/src/openapi.ts +2 -0
  95. package/packages/normalizer/src/page.ts +184 -0
  96. package/packages/normalizer/src/pitfall-extractor.ts +190 -0
  97. package/packages/normalizer/src/slicer.ts +455 -0
  98. package/packages/retrieval/src/engine.ts +120 -0
  99. package/packages/retrieval/src/index.ts +7 -0
  100. package/packages/retrieval/src/intent.ts +43 -0
  101. package/packages/retrieval/src/packer.ts +145 -0
  102. package/packages/retrieval/src/recipe-engine.ts +313 -0
  103. package/packages/retrieval/src/scorer.ts +139 -0
  104. package/packages/retrieval/src/weights.ts +31 -0
  105. package/packages/security/src/annotations.ts +112 -0
  106. package/packages/security/src/index.ts +2 -0
  107. package/packages/security/src/ssrf.ts +153 -0
  108. package/packages/shared/src/errors.ts +53 -0
  109. package/packages/shared/src/hashing.ts +23 -0
  110. package/packages/shared/src/index.ts +3 -0
  111. package/packages/shared/src/types.ts +881 -0
  112. package/packages/storage/src/db.ts +72 -0
  113. package/packages/storage/src/index.ts +11 -0
  114. package/packages/storage/src/interfaces.ts +115 -0
  115. package/packages/storage/src/repositories/api-repository.ts +219 -0
  116. package/packages/storage/src/repositories/chunk-repository.ts +316 -0
  117. package/packages/storage/src/repositories/example-repository.ts +206 -0
  118. package/packages/storage/src/repositories/page-repository.ts +205 -0
  119. package/packages/storage/src/repositories/pitfall-repository.ts +188 -0
  120. package/packages/storage/src/repositories/source-repository.ts +205 -0
  121. package/packages/storage/src/repository.ts +256 -0
  122. package/packages/storage/src/schema.ts +269 -0
  123. package/packages/storage/src/search-tokens.ts +28 -0
  124. package/packages/verification/src/diff-engine.ts +258 -0
  125. package/packages/verification/src/extractor.ts +339 -0
  126. package/packages/verification/src/impact-scanner.ts +203 -0
  127. package/packages/verification/src/index.ts +5 -0
  128. package/packages/verification/src/services.ts +238 -0
  129. package/packages/verification/src/verifier.ts +375 -0
  130. package/packages/workspace/src/detector.ts +143 -0
  131. package/packages/workspace/src/ecosystems/cargo.ts +84 -0
  132. package/packages/workspace/src/ecosystems/composer.ts +42 -0
  133. package/packages/workspace/src/ecosystems/go.ts +54 -0
  134. package/packages/workspace/src/ecosystems/index.ts +34 -0
  135. package/packages/workspace/src/ecosystems/maven.ts +34 -0
  136. package/packages/workspace/src/ecosystems/npm.ts +83 -0
  137. package/packages/workspace/src/ecosystems/pub.ts +40 -0
  138. package/packages/workspace/src/ecosystems/pypi.ts +100 -0
  139. package/packages/workspace/src/ecosystems/rubygems.ts +30 -0
  140. package/packages/workspace/src/ecosystems/types.ts +18 -0
  141. package/packages/workspace/src/index.ts +5 -0
  142. package/packages/workspace/src/lockfile.ts +194 -0
  143. package/packages/workspace/src/resolver.ts +234 -0
  144. package/packages/workspace/src/semver.ts +259 -0
@@ -0,0 +1,184 @@
1
+ import {
2
+ computeContentHash,
3
+ estimateTokenCount,
4
+ } from '../../shared/src/index.ts';
5
+ import type {
6
+ NormalizedPage,
7
+ Heading,
8
+ Link,
9
+ CodeExample,
10
+ Provenance,
11
+ } from '../../shared/src/index.ts';
12
+ import { detectSecurityAnnotations } from '../../security/src/index.ts';
13
+ import { normalizeHtmlToMarkdown } from './html.ts';
14
+ import { parseLlmsTxt, isValidLlmsTxt } from './llms.ts';
15
+ import { detectOpenApiSpec } from './openapi.ts';
16
+
17
+ function extractMarkdownHeadings(content: string): Heading[] {
18
+ const headings: Heading[] = [];
19
+ const lines = content.split('\n');
20
+ for (const line of lines) {
21
+ const match = line.match(/^(#{1,6})\s+(.+)$/);
22
+ if (match) {
23
+ const level = match[1].length;
24
+ const text = match[2].trim();
25
+ const anchor = text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-');
26
+ headings.push({ level, text, anchor });
27
+ }
28
+ }
29
+ return headings;
30
+ }
31
+
32
+ function extractMarkdownCodeBlocks(content: string): CodeExample[] {
33
+ const examples: CodeExample[] = [];
34
+ const regex = /```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g;
35
+ let match: RegExpExecArray | null;
36
+ let idx = 1;
37
+ while ((match = regex.exec(content)) !== null) {
38
+ const lang = match[1].trim() || 'text';
39
+ const code = match[2].trim();
40
+ if (code) {
41
+ examples.push({
42
+ id: `code_${idx++}`,
43
+ language: lang,
44
+ code,
45
+ });
46
+ }
47
+ }
48
+ return examples;
49
+ }
50
+
51
+ function extractMarkdownLinks(content: string, baseUrl: string): Link[] {
52
+ const links: Link[] = [];
53
+ const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
54
+ let match: RegExpExecArray | null;
55
+ while ((match = regex.exec(content)) !== null) {
56
+ const text = match[1].trim();
57
+ const href = match[2].trim();
58
+ if (href.startsWith('#') || href.startsWith('javascript:')) continue;
59
+
60
+ let absoluteUrl = href;
61
+ let isExternal = false;
62
+ try {
63
+ const resolved = new URL(href, baseUrl);
64
+ absoluteUrl = resolved.href;
65
+ const baseHost = new URL(baseUrl).hostname;
66
+ isExternal = resolved.hostname !== baseHost;
67
+ } catch {
68
+ // Keep raw href
69
+ }
70
+
71
+ links.push({ text, url: absoluteUrl, isExternal });
72
+ }
73
+ return links;
74
+ }
75
+
76
+ export interface BuildNormalizedPageInput {
77
+ sourceId: string;
78
+ url: string;
79
+ rawContent: string;
80
+ contentType?: string;
81
+ sourceUrl?: string;
82
+ targetUrl?: string;
83
+ discoveredBy?: string;
84
+ fetchedAt?: string;
85
+ snapshotId?: string;
86
+ }
87
+
88
+ export function buildNormalizedPage(input: BuildNormalizedPageInput): NormalizedPage {
89
+ const { sourceId, url, rawContent, contentType = '' } = input;
90
+ const rawBytes = Buffer.byteLength(rawContent, 'utf-8');
91
+
92
+ let title = 'Documentation';
93
+ let cleanMarkdown = '';
94
+ let headings: Heading[] = [];
95
+ let links: Link[] = [];
96
+ let codeExamples: CodeExample[] = [];
97
+
98
+ const isHtml = contentType.includes('text/html') || /^\s*<(!DOCTYPE|html)/i.test(rawContent);
99
+ const isOpenApi = contentType.includes('application/json') || url.endsWith('.json') || url.endsWith('.yaml');
100
+
101
+ if (isOpenApi) {
102
+ const apiSummary = detectOpenApiSpec(rawContent, url);
103
+ if (apiSummary) {
104
+ title = `${apiSummary.title} (OpenAPI ${apiSummary.specVersion})`;
105
+ cleanMarkdown = `# ${title}\n\n${apiSummary.description || ''}\n\n` +
106
+ `**Servers**: ${apiSummary.servers.join(', ') || 'None specified'}\n\n` +
107
+ `**Endpoints Count**: ${apiSummary.pathCount}\n\n` +
108
+ `\`\`\`json\n${JSON.stringify(apiSummary.rawJson, null, 2)}\n\`\`\``;
109
+ headings = [
110
+ { level: 1, text: title, anchor: 'title' },
111
+ { level: 2, text: 'API Overview', anchor: 'api-overview' },
112
+ ];
113
+ codeExamples = [
114
+ {
115
+ id: 'spec_1',
116
+ language: 'json',
117
+ code: JSON.stringify(apiSummary.rawJson, null, 2),
118
+ caption: 'OpenAPI Specification',
119
+ },
120
+ ];
121
+ }
122
+ }
123
+
124
+ if (!cleanMarkdown && isHtml) {
125
+ const extracted = normalizeHtmlToMarkdown(rawContent, url);
126
+ title = extracted.title;
127
+ cleanMarkdown = extracted.markdown;
128
+ headings = extracted.headings;
129
+ links = extracted.links;
130
+ codeExamples = extracted.codeExamples;
131
+ }
132
+
133
+ if (!cleanMarkdown) {
134
+ if (isValidLlmsTxt(rawContent)) {
135
+ const llmsDoc = parseLlmsTxt(rawContent, url);
136
+ title = llmsDoc.title || 'llms.txt Index';
137
+ cleanMarkdown = rawContent;
138
+ headings = extractMarkdownHeadings(rawContent);
139
+ links = extractMarkdownLinks(rawContent, url);
140
+ codeExamples = extractMarkdownCodeBlocks(rawContent);
141
+ } else {
142
+ cleanMarkdown = rawContent.trim();
143
+ headings = extractMarkdownHeadings(cleanMarkdown);
144
+ links = extractMarkdownLinks(cleanMarkdown, url);
145
+ codeExamples = extractMarkdownCodeBlocks(cleanMarkdown);
146
+ if (headings.length > 0 && headings[0].level === 1) {
147
+ title = headings[0].text;
148
+ }
149
+ }
150
+ }
151
+
152
+ const contentHash = computeContentHash(cleanMarkdown);
153
+ const estimatedTokens = estimateTokenCount(cleanMarkdown);
154
+ const securityAnnotations = detectSecurityAnnotations(cleanMarkdown);
155
+
156
+ const id = `page_${computeContentHash(`${sourceId}:${url}`).slice(0, 16)}`;
157
+ const fetchedAt = input.fetchedAt || new Date().toISOString();
158
+
159
+ const provenance: Provenance = {
160
+ sourceUrl: input.sourceUrl || url,
161
+ targetUrl: input.targetUrl || url,
162
+ fetchedAt,
163
+ discoveredBy: input.discoveredBy || 'direct',
164
+ contentHash,
165
+ snapshotId: input.snapshotId,
166
+ };
167
+
168
+ return {
169
+ id,
170
+ sourceId,
171
+ title,
172
+ url,
173
+ content: cleanMarkdown,
174
+ headings,
175
+ links,
176
+ codeExamples,
177
+ contentHash,
178
+ fetchedAt,
179
+ rawBytes,
180
+ estimatedTokens,
181
+ securityAnnotations,
182
+ provenance,
183
+ };
184
+ }
@@ -0,0 +1,190 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type {
3
+ NormalizedPage,
4
+ DocumentChunk,
5
+ Pitfall,
6
+ PitfallKind,
7
+ Provenance,
8
+ } from '../../shared/src/index.ts';
9
+
10
+ interface PitfallPattern {
11
+ kind: PitfallKind;
12
+ pattern: RegExp;
13
+ titlePrefix: string;
14
+ }
15
+
16
+ const PITFALL_PATTERNS: PitfallPattern[] = [
17
+ {
18
+ kind: 'removed',
19
+ pattern: /(?:has been removed in|was removed in|removed in version|no longer supported in)\s+([^\n.]+)/i,
20
+ titlePrefix: 'Removed in',
21
+ },
22
+ {
23
+ kind: 'deprecated',
24
+ pattern: /(?:@deprecated|deprecated since|deprecated in|this (?:method|function|endpoint|parameter|feature) is deprecated|is deprecated; use|deprecated:\s+use)\s+([^\n.]+)/i,
25
+ titlePrefix: 'Deprecated',
26
+ },
27
+ {
28
+ kind: 'breaking_change',
29
+ pattern: /(?:breaking change:?|breaking in v?\d+|migrating to v?\d+ requires|incompatible with previous)\s*([^\n.]*)/i,
30
+ titlePrefix: 'Breaking Change',
31
+ },
32
+ {
33
+ kind: 'server_only',
34
+ pattern: /(?:can only be called (?:on|from) the server|server-only|server components? only|cannot be imported (?:on|into) the client|never expose.*in client(?:-side)? code)/i,
35
+ titlePrefix: 'Server-Only Restriction',
36
+ },
37
+ {
38
+ kind: 'client_only',
39
+ pattern: /(?:client-only|can only be called in the browser|client components? only|requires a browser environment|window is not defined)/i,
40
+ titlePrefix: 'Client-Only Restriction',
41
+ },
42
+ {
43
+ kind: 'rate_limit',
44
+ pattern: /(?:rate limit(?:ed)?|429 too many requests|requests per (?:minute|second|hour)|burst limit|quota exceeded)/i,
45
+ titlePrefix: 'Rate Limit Warning',
46
+ },
47
+ {
48
+ kind: 'security',
49
+ pattern: /(?:security warning|vulnerability|signature verification failure|never commit (?:your|the) secret|timing attacks?|replay attacks?)/i,
50
+ titlePrefix: 'Security Requirement',
51
+ },
52
+ {
53
+ kind: 'permission',
54
+ pattern: /(?:requires permission|requires scope|insufficient privileges|403 forbidden|unauthorized access|must have admin)/i,
55
+ titlePrefix: 'Permission Requirement',
56
+ },
57
+ {
58
+ kind: 'required_config',
59
+ pattern: /(?:must be configured before|environment variable.*is required|missing required config|prerequisite:\s+set)\s+([^\n.]+)/i,
60
+ titlePrefix: 'Required Configuration',
61
+ },
62
+ {
63
+ kind: 'runtime_restriction',
64
+ pattern: /(?:not supported in (?:the )?edge runtime|node(?:\.js)? runtime only|requires webcrypto|unsupported platform)/i,
65
+ titlePrefix: 'Runtime Restriction',
66
+ },
67
+ ];
68
+
69
+ const ADMONITION_REGEX = /(?:^|\n)(?:>|:::\s*)\[!(WARNING|CAUTION|DANGER|IMPORTANT|NOTE)\](?:\s+([^\n]+))?\n([\s\S]*?)(?=\n(?:>|:::|\s*$|$))/gi;
70
+
71
+ /**
72
+ * Extracts explicit, non-inferential pitfalls, deprecations, breaking changes,
73
+ * and security requirements from documentation pages and chunks.
74
+ */
75
+ export function extractPitfalls(
76
+ page: NormalizedPage,
77
+ chunks: DocumentChunk[],
78
+ docVersion?: string,
79
+ snapshotId?: string
80
+ ): Pitfall[] {
81
+ const pitfalls: Pitfall[] = [];
82
+ const seenKeys = new Set<string>();
83
+ const nowIso = new Date().toISOString();
84
+
85
+ // Helper to add pitfall with deduplication
86
+ const addPitfall = (
87
+ kind: PitfallKind,
88
+ title: string,
89
+ content: string,
90
+ chunk?: DocumentChunk
91
+ ) => {
92
+ const trimmedContent = content.trim();
93
+ if (!trimmedContent || trimmedContent.length < 15) return;
94
+
95
+ const dedupeKey = `${kind}:${title.toLowerCase()}:${trimmedContent.substring(0, 80).toLowerCase()}`;
96
+ if (seenKeys.has(dedupeKey)) return;
97
+ seenKeys.add(dedupeKey);
98
+
99
+ const id = createHash('sha256')
100
+ .update(`${page.id}:${kind}:${title}:${trimmedContent.substring(0, 100)}`)
101
+ .digest('hex')
102
+ .substring(0, 16);
103
+
104
+ // Extract related API or symbol from chunk if available
105
+ let relatedApi: string | undefined = undefined;
106
+ let relatedSymbol: string | undefined = undefined;
107
+
108
+ if (chunk) {
109
+ // Check section path or title for API
110
+ const apiMatch = (chunk.title || chunk.sectionPath.join(' ')).match(/\b(GET|POST|PUT|DELETE|PATCH)\s+([/\w\-_{}]+)/i);
111
+ if (apiMatch) {
112
+ relatedApi = `${apiMatch[1].toUpperCase()} ${apiMatch[2]}`;
113
+ }
114
+ }
115
+
116
+ const provenance: Provenance = {
117
+ sourceUrl: page.url,
118
+ retrievedAt: page.metadata?.extractedAt || nowIso,
119
+ sourceAuthority: 'official',
120
+ };
121
+
122
+ const effectiveSnapshotId = snapshotId || chunk?.snapshotId || (page as any).snapshotId || 'snap_default';
123
+
124
+ pitfalls.push({
125
+ id,
126
+ chunkId: chunk?.id,
127
+ pageId: page.id,
128
+ snapshotId: effectiveSnapshotId,
129
+ kind,
130
+ title: title.trim(),
131
+ content: trimmedContent,
132
+ relatedApi,
133
+ relatedSymbol,
134
+ docVersion: docVersion || chunk?.docVersion || page.docVersion,
135
+ provenance,
136
+ createdAt: nowIso,
137
+ });
138
+ };
139
+
140
+ // 1. Process chunks for admonitions (> [!WARNING], > [!CAUTION], etc.)
141
+ for (const chunk of chunks) {
142
+ const content = chunk.content;
143
+
144
+ // Match admonitions
145
+ let admMatch: RegExpExecArray | null;
146
+ const admRegex = new RegExp(ADMONITION_REGEX.source, 'gi');
147
+ while ((admMatch = admRegex.exec(content)) !== null) {
148
+ const calloutType = admMatch[1].toUpperCase();
149
+ const customTitle = admMatch[2]?.trim();
150
+ const calloutBody = admMatch[3].replace(/^>\s?/gm, '').trim();
151
+
152
+ let kind: PitfallKind = 'security';
153
+ if (calloutType === 'WARNING' || calloutType === 'DANGER') {
154
+ kind = 'breaking_change';
155
+ } else if (calloutType === 'CAUTION') {
156
+ kind = 'security';
157
+ } else if (calloutType === 'IMPORTANT') {
158
+ kind = 'required_config';
159
+ }
160
+
161
+ // Check if body specifically matches any pitfall pattern
162
+ for (const p of PITFALL_PATTERNS) {
163
+ if (p.pattern.test(calloutBody)) {
164
+ kind = p.kind;
165
+ break;
166
+ }
167
+ }
168
+
169
+ const defaultTitle = customTitle || `${kind.replace(/_/g, ' ').toUpperCase()} Warning: ${chunk.title || page.title}`;
170
+ addPitfall(kind, defaultTitle, calloutBody, chunk);
171
+ }
172
+
173
+ // 2. Scan chunk text for explicit pattern matches
174
+ for (const { kind, pattern, titlePrefix } of PITFALL_PATTERNS) {
175
+ const match = content.match(pattern);
176
+ if (match) {
177
+ // Extract paragraph containing the match
178
+ const paragraphs = content.split(/\n\s*\n/);
179
+ const matchingParagraph = paragraphs.find(p => pattern.test(p));
180
+ if (matchingParagraph) {
181
+ const detail = match[1]?.trim() ? `: ${match[1].trim()}` : '';
182
+ const title = `${titlePrefix}${detail}`;
183
+ addPitfall(kind, title, matchingParagraph, chunk);
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ return pitfalls;
190
+ }