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,258 @@
1
+ import type {
2
+ ApiEndpoint,
3
+ DocDiffResult,
4
+ EndpointDiffItem,
5
+ Pitfall,
6
+ } from '../../shared/src/index.ts';
7
+ import type { DocOrbitRepository } from '../../storage/src/index.ts';
8
+
9
+ export interface DiffOptions {
10
+ fromVersion?: string;
11
+ toVersion?: string;
12
+ fromSnapshotId?: string;
13
+ toSnapshotId?: string;
14
+ sourceId?: string;
15
+ }
16
+
17
+ export class DocDiffEngine {
18
+ private repo: DocOrbitRepository;
19
+
20
+ constructor(repo: DocOrbitRepository) {
21
+ this.repo = repo;
22
+ }
23
+
24
+ diff(options: DiffOptions): DocDiffResult {
25
+ const fromEndpoints = this.getEndpoints(options.fromSnapshotId, options.fromVersion);
26
+ const toEndpoints = this.getEndpoints(options.toSnapshotId, options.toVersion);
27
+
28
+ const fromPitfalls = this.getPitfalls(options.fromSnapshotId, options.fromVersion);
29
+ const toPitfalls = this.getPitfalls(options.toSnapshotId, options.toVersion);
30
+
31
+ // 1. Compute Endpoint Diffs
32
+ const apiChanges: EndpointDiffItem[] = [];
33
+ const fromMap = new Map<string, ApiEndpoint>();
34
+ const toMap = new Map<string, ApiEndpoint>();
35
+
36
+ for (const ep of fromEndpoints) {
37
+ fromMap.set(`${ep.method.toUpperCase()} ${ep.path}`, ep);
38
+ }
39
+ for (const ep of toEndpoints) {
40
+ toMap.set(`${ep.method.toUpperCase()} ${ep.path}`, ep);
41
+ }
42
+
43
+ let addedCount = 0;
44
+ let removedCount = 0;
45
+ let modifiedCount = 0;
46
+ let deprecatedCount = 0;
47
+
48
+ // Check additions, deprecations, and modifications in 'to'
49
+ for (const [key, toEp] of toMap.entries()) {
50
+ const fromEp = fromMap.get(key);
51
+
52
+ if (!fromEp) {
53
+ // Newly added endpoint
54
+ apiChanges.push({
55
+ path: toEp.path,
56
+ method: toEp.method,
57
+ changeType: 'added',
58
+ endpoint: toEp,
59
+ });
60
+ addedCount++;
61
+ } else {
62
+ // Existed previously: check deprecation status
63
+ if (toEp.deprecated && !fromEp.deprecated) {
64
+ apiChanges.push({
65
+ path: toEp.path,
66
+ method: toEp.method,
67
+ changeType: 'deprecated',
68
+ endpoint: toEp,
69
+ previousEndpoint: fromEp,
70
+ });
71
+ deprecatedCount++;
72
+ }
73
+
74
+ // Check semantic parameter and schema modifications
75
+ const changes = this.detectEndpointModifications(fromEp, toEp);
76
+ if (changes.length > 0) {
77
+ apiChanges.push({
78
+ path: toEp.path,
79
+ method: toEp.method,
80
+ changeType: 'modified',
81
+ changes,
82
+ endpoint: toEp,
83
+ previousEndpoint: fromEp,
84
+ });
85
+ modifiedCount++;
86
+ }
87
+ }
88
+ }
89
+
90
+ // Check removals from 'from'
91
+ for (const [key, fromEp] of fromMap.entries()) {
92
+ if (!toMap.has(key)) {
93
+ apiChanges.push({
94
+ path: fromEp.path,
95
+ method: fromEp.method,
96
+ changeType: 'removed',
97
+ endpoint: fromEp,
98
+ });
99
+ removedCount++;
100
+ }
101
+ }
102
+
103
+ // 2. Compute Pitfall Diffs
104
+ const pitfallChanges: Array<{ changeType: 'added' | 'removed'; pitfall: Pitfall }> = [];
105
+ const fromPitfallTitles = new Set(fromPitfalls.map(p => `${p.kind}:${p.title}`));
106
+ const toPitfallTitles = new Set(toPitfalls.map(p => `${p.kind}:${p.title}`));
107
+
108
+ for (const toP of toPitfalls) {
109
+ if (!fromPitfallTitles.has(`${toP.kind}:${toP.title}`)) {
110
+ pitfallChanges.push({ changeType: 'added', pitfall: toP });
111
+ }
112
+ }
113
+ for (const fromP of fromPitfalls) {
114
+ if (!toPitfallTitles.has(`${fromP.kind}:${fromP.title}`)) {
115
+ pitfallChanges.push({ changeType: 'removed', pitfall: fromP });
116
+ }
117
+ }
118
+
119
+ // 3. Compute Content/Chunk Diffs (Semantic only, ignoring formatting-only whitespace)
120
+ const contentChanges = this.computeContentDiffs(options);
121
+
122
+ return {
123
+ fromVersion: options.fromVersion,
124
+ toVersion: options.toVersion,
125
+ fromSnapshotId: options.fromSnapshotId,
126
+ toSnapshotId: options.toSnapshotId,
127
+ sourceId: options.sourceId,
128
+ summary: {
129
+ endpointsAdded: addedCount,
130
+ endpointsRemoved: removedCount,
131
+ endpointsModified: modifiedCount,
132
+ endpointsDeprecated: deprecatedCount,
133
+ pitfallsAdded: pitfallChanges.filter(p => p.changeType === 'added').length,
134
+ pitfallsRemoved: pitfallChanges.filter(p => p.changeType === 'removed').length,
135
+ contentChanged: contentChanges.length,
136
+ },
137
+ apiChanges,
138
+ pitfallChanges,
139
+ contentChanges,
140
+ retrievedAt: new Date().toISOString(),
141
+ };
142
+ }
143
+
144
+ private getEndpoints(snapshotId?: string, docVersion?: string): ApiEndpoint[] {
145
+ if (snapshotId) {
146
+ return this.repo.getApiEndpointsBySnapshot(snapshotId);
147
+ }
148
+ return this.repo.searchApiEndpoints('', {
149
+ docVersion,
150
+ limit: 500,
151
+ });
152
+ }
153
+
154
+ private getPitfalls(snapshotId?: string, docVersion?: string): Pitfall[] {
155
+ if (snapshotId) {
156
+ return this.repo.getPitfallsBySnapshot(snapshotId);
157
+ }
158
+ return this.repo.searchPitfalls('', {
159
+ docVersion,
160
+ limit: 200,
161
+ });
162
+ }
163
+
164
+ private detectEndpointModifications(fromEp: ApiEndpoint, toEp: ApiEndpoint): string[] {
165
+ const changes: string[] = [];
166
+
167
+ // Compare parameters
168
+ const fromParams = new Map(fromEp.parameters.map(p => [p.name, p]));
169
+ const toParams = new Map(toEp.parameters.map(p => [p.name, p]));
170
+
171
+ for (const [name, toP] of toParams.entries()) {
172
+ const fromP = fromParams.get(name);
173
+ if (!fromP) {
174
+ changes.push(`Added parameter "${name}"${toP.required ? ' (required)' : ''}`);
175
+ } else if (!fromP.required && toP.required) {
176
+ changes.push(`Parameter "${name}" is now required (was optional)`);
177
+ } else if (fromP.type !== toP.type && toP.type) {
178
+ changes.push(`Parameter "${name}" type changed from ${fromP.type || 'any'} to ${toP.type}`);
179
+ }
180
+ }
181
+
182
+ for (const name of fromParams.keys()) {
183
+ if (!toParams.has(name)) {
184
+ changes.push(`Removed parameter "${name}"`);
185
+ }
186
+ }
187
+
188
+ // Compare Request Body required fields
189
+ const fromReq = new Set(fromEp.requestSchema?.required || []);
190
+ const toReq = new Set(toEp.requestSchema?.required || []);
191
+
192
+ for (const field of toReq) {
193
+ if (!fromReq.has(field)) {
194
+ changes.push(`Request body field "${field}" is now required`);
195
+ }
196
+ }
197
+ for (const field of fromReq) {
198
+ if (!toReq.has(field)) {
199
+ changes.push(`Request body field "${field}" is no longer required`);
200
+ }
201
+ }
202
+
203
+ return changes;
204
+ }
205
+
206
+ private computeContentDiffs(options: DiffOptions): Array<{ title: string; changeType: 'added' | 'removed' | 'modified'; summary: string }> {
207
+ const fromChunks = options.fromSnapshotId
208
+ ? this.repo.getChunksBySnapshot(options.fromSnapshotId)
209
+ : options.fromVersion
210
+ ? this.repo.getChunksByVersion(options.fromVersion)
211
+ : [];
212
+ const toChunks = options.toSnapshotId
213
+ ? this.repo.getChunksBySnapshot(options.toSnapshotId)
214
+ : options.toVersion
215
+ ? this.repo.getChunksByVersion(options.toVersion)
216
+ : [];
217
+
218
+ if (fromChunks.length === 0 && toChunks.length === 0) return [];
219
+
220
+ const changes: Array<{ title: string; changeType: 'added' | 'removed' | 'modified'; summary: string }> = [];
221
+ const fromMap = new Map(fromChunks.map(c => [c.title, c]));
222
+ const toMap = new Map(toChunks.map(c => [c.title, c]));
223
+
224
+ for (const [title, toC] of toMap.entries()) {
225
+ const fromC = fromMap.get(title);
226
+ if (!fromC) {
227
+ changes.push({
228
+ title,
229
+ changeType: 'added',
230
+ summary: `New documentation section: "${title}"`,
231
+ });
232
+ } else if (fromC.contentHash !== toC.contentHash) {
233
+ // Compare normalized text to ignore formatting-only whitespace
234
+ const normFrom = fromC.content.replace(/\s+/g, ' ').trim();
235
+ const normTo = toC.content.replace(/\s+/g, ' ').trim();
236
+ if (normFrom !== normTo) {
237
+ changes.push({
238
+ title,
239
+ changeType: 'modified',
240
+ summary: `Content updated for section "${title}"`,
241
+ });
242
+ }
243
+ }
244
+ }
245
+
246
+ for (const title of fromMap.keys()) {
247
+ if (!toMap.has(title)) {
248
+ changes.push({
249
+ title,
250
+ changeType: 'removed',
251
+ summary: `Documentation section removed: "${title}"`,
252
+ });
253
+ }
254
+ }
255
+
256
+ return changes;
257
+ }
258
+ }
@@ -0,0 +1,339 @@
1
+ import type { ExtractedApiCall } from '../../shared/src/index.ts';
2
+
3
+ /**
4
+ * Normalizes an API path for comparison against schema paths.
5
+ * E.g. extracts pathname from full URLs ("https://api.stripe.com/v1/webhook_endpoints" -> "/v1/webhook_endpoints")
6
+ */
7
+ export function normalizeApiPath(urlOrPath: string): string {
8
+ try {
9
+ if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
10
+ const parsed = new URL(urlOrPath);
11
+ return parsed.pathname;
12
+ }
13
+ } catch {
14
+ // Fall back to regex
15
+ }
16
+ const match = urlOrPath.match(/^https?:\/\/[^/]+(\/.*)$/);
17
+ if (match) return match[1].split('?')[0];
18
+ return urlOrPath.split('?')[0].trim();
19
+ }
20
+
21
+ /**
22
+ * Checks if an extracted path matches an OpenAPI schema path template.
23
+ * E.g. candidate "/v1/webhook_endpoints/we_123" matches schema "/v1/webhook_endpoints/{id}"
24
+ */
25
+ export function matchEndpointPath(candidatePath: string, schemaPath: string): boolean {
26
+ const normCandidate = normalizeApiPath(candidatePath).replace(/\/+$/, '');
27
+ const normSchema = normalizeApiPath(schemaPath).replace(/\/+$/, '');
28
+
29
+ if (normCandidate.toLowerCase() === normSchema.toLowerCase()) {
30
+ return true;
31
+ }
32
+
33
+ // Convert schema {param} or :param to regex wildcard
34
+ const regexPattern = normSchema
35
+ .replace(/{[^}]+}/g, '([^/]+)')
36
+ .replace(/:[a-zA-Z0-9_]+/g, '([^/]+)')
37
+ .replace(/\//g, '\\/');
38
+
39
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
40
+ return regex.test(normCandidate);
41
+ }
42
+
43
+ /**
44
+ * Extracts top-level key names from a JS/Python object or JSON snippet string.
45
+ */
46
+ function extractObjectKeys(objSnippet: string): string[] {
47
+ const keys: Set<string> = new Set();
48
+ // Try standard JSON parse first
49
+ try {
50
+ const parsed = JSON.parse(objSnippet);
51
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
52
+ return Object.keys(parsed);
53
+ }
54
+ } catch {
55
+ // Fall back to lexical key extraction
56
+ }
57
+
58
+ // Match identifier keys: { foo: ..., "bar": ..., 'baz': ... }
59
+ const keyRegex = /(?:[{,]\s*)(?:["']?([a-zA-Z0-9_-]+)["']?)\s*:/g;
60
+ let match: RegExpExecArray | null;
61
+ while ((match = keyRegex.exec(objSnippet)) !== null) {
62
+ if (match[1] && !['method', 'headers', 'body'].includes(match[1])) {
63
+ keys.add(match[1]);
64
+ }
65
+ }
66
+
67
+ return Array.from(keys);
68
+ }
69
+
70
+ /**
71
+ * Deterministically extracts API calls, parameters, methods, and version syntax from code.
72
+ */
73
+ export class CodeApiExtractor {
74
+ /**
75
+ * Extracts API calls from code.
76
+ */
77
+ extract(code: string, language?: string): ExtractedApiCall[] {
78
+ const results: ExtractedApiCall[] = [];
79
+ const lines = code.split('\n');
80
+
81
+ // 1. Check for Next.js Route Params patterns (sync vs async)
82
+ this.extractNextJsParams(code, lines, results);
83
+
84
+ // 2. Extract Fetch calls: fetch('url', { method: 'POST', body: ... })
85
+ this.extractFetchCalls(code, lines, results);
86
+
87
+ // 3. Extract Axios calls: axios.post('url', {...}) / axios.get('url')
88
+ this.extractAxiosCalls(code, lines, results);
89
+
90
+ // 4. Extract Python Requests calls: requests.post('url', json={...})
91
+ this.extractPythonRequestsCalls(code, lines, results);
92
+
93
+ // 5. Extract cURL commands: curl -X POST ...
94
+ this.extractCurlCalls(code, lines, results);
95
+
96
+ // 6. Extract common SDK calls: stripe.webhookEndpoints.create({...}), etc.
97
+ this.extractSdkCalls(code, lines, results);
98
+
99
+ // 7. Check for dynamic / unresolvable URLs: e.g. fetch(apiUrl) or axios.get(endpoint)
100
+ this.extractDynamicCalls(code, lines, results);
101
+
102
+ // If nothing was extracted at all, create an ambiguous marker
103
+ if (results.length === 0) {
104
+ results.push({
105
+ rawSnippet: code.slice(0, 150),
106
+ isDynamicOrAmbiguous: true,
107
+ reasonIfAmbiguous: language && !['javascript', 'typescript', 'python', 'curl', 'bash'].includes(language.toLowerCase())
108
+ ? `Language "${language}" is not currently supported for deep AST schema verification.`
109
+ : 'No recognizable HTTP/API client calls, SDK invocations, or endpoint paths found in code.',
110
+ language,
111
+ });
112
+ }
113
+
114
+ return results;
115
+ }
116
+
117
+ private findLineNumber(lines: string[], snippetPattern: string): number {
118
+ const clean = snippetPattern.trim().split('\n')[0].slice(0, 40);
119
+ for (let i = 0; i < lines.length; i++) {
120
+ if (lines[i].includes(clean)) return i + 1;
121
+ }
122
+ return 1;
123
+ }
124
+
125
+ private extractNextJsParams(code: string, lines: string[], results: ExtractedApiCall[]): void {
126
+ // Next.js 14 sync: params.slug, const { slug } = params
127
+ const syncParamRegex = /(?:const\s+{\s*([^}]+)\s*}\s*=\s*params\b|\bparams\.([a-zA-Z0-9_]+))/;
128
+ // Next.js 15+ async: await params, const { slug } = await params
129
+ const asyncParamRegex = /(?:await\s+params\b|const\s+{\s*([^}]+)\s*}\s*=\s*await\s+params\b)/;
130
+
131
+ const hasAsync = asyncParamRegex.test(code);
132
+ const hasSync = syncParamRegex.test(code) && !hasAsync;
133
+
134
+ if (hasAsync) {
135
+ const match = code.match(asyncParamRegex);
136
+ results.push({
137
+ symbol: 'nextjs_route_params_async',
138
+ endpointPath: 'nextjs/app_router/params',
139
+ method: 'get',
140
+ rawSnippet: match ? match[0] : 'await params',
141
+ line: match ? this.findLineNumber(lines, match[0]) : 1,
142
+ });
143
+ } else if (hasSync) {
144
+ const match = code.match(syncParamRegex);
145
+ results.push({
146
+ symbol: 'nextjs_route_params_sync',
147
+ endpointPath: 'nextjs/app_router/params',
148
+ method: 'get',
149
+ rawSnippet: match ? match[0] : 'params.slug',
150
+ line: match ? this.findLineNumber(lines, match[0]) : 1,
151
+ });
152
+ }
153
+ }
154
+
155
+ private extractFetchCalls(code: string, lines: string[], results: ExtractedApiCall[]): void {
156
+ // Matches: fetch(['"`]([^'"`]+)['"`](?:\s*,\s*({[\s\S]*?}))?\s*\)
157
+ const fetchRegex = /fetch\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*({[\s\S]*?}))?\s*\)/g;
158
+ let match: RegExpExecArray | null;
159
+
160
+ while ((match = fetchRegex.exec(code)) !== null) {
161
+ const url = match[1];
162
+ const optionsSnippet = match[2] || '';
163
+ const endpointPath = normalizeApiPath(url);
164
+
165
+ let method = 'get';
166
+ const methodMatch = optionsSnippet.match(/method\s*:\s*['"]([a-zA-Z]+)['"]/i);
167
+ if (methodMatch) {
168
+ method = methodMatch[1].toLowerCase();
169
+ }
170
+
171
+ // Extract body fields if body: JSON.stringify({...}) or body: {...}
172
+ let bodyFields: string[] = [];
173
+ const bodyMatch = optionsSnippet.match(/body\s*:\s*(?:JSON\.stringify\s*\(\s*)?({[\s\S]*?})(?:\s*\))?/);
174
+ if (bodyMatch) {
175
+ bodyFields = extractObjectKeys(bodyMatch[1]);
176
+ }
177
+
178
+ // Extract response fields accessed: e.g. data.foo, res.json()
179
+ const responseFields = this.extractResponseFields(code, endpointPath);
180
+
181
+ results.push({
182
+ endpointPath,
183
+ method,
184
+ bodyFields,
185
+ responseFieldsAccessed: responseFields,
186
+ rawSnippet: match[0],
187
+ line: this.findLineNumber(lines, match[0]),
188
+ language: 'javascript',
189
+ });
190
+ }
191
+ }
192
+
193
+ private extractAxiosCalls(code: string, lines: string[], results: ExtractedApiCall[]): void {
194
+ // Matches: axios.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*({[\s\S]*?}))?/g
195
+ const axiosRegex = /axios\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*({[\s\S]*?}))?/g;
196
+ let match: RegExpExecArray | null;
197
+
198
+ while ((match = axiosRegex.exec(code)) !== null) {
199
+ const method = match[1].toLowerCase();
200
+ const url = match[2];
201
+ const payloadSnippet = match[3] || '';
202
+ const endpointPath = normalizeApiPath(url);
203
+ const bodyFields = payloadSnippet ? extractObjectKeys(payloadSnippet) : [];
204
+ const responseFields = this.extractResponseFields(code, endpointPath);
205
+
206
+ results.push({
207
+ endpointPath,
208
+ method,
209
+ bodyFields,
210
+ responseFieldsAccessed: responseFields,
211
+ rawSnippet: match[0],
212
+ line: this.findLineNumber(lines, match[0]),
213
+ language: 'javascript',
214
+ });
215
+ }
216
+ }
217
+
218
+ private extractPythonRequestsCalls(code: string, lines: string[], results: ExtractedApiCall[]): void {
219
+ // Matches: requests.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*(?:json|data)\s*=\s*({[\s\S]*?}))?
220
+ const requestsRegex = /requests\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*(?:json|data|params)\s*=\s*({[\s\S]*?}))?/g;
221
+ let match: RegExpExecArray | null;
222
+
223
+ while ((match = requestsRegex.exec(code)) !== null) {
224
+ const method = match[1].toLowerCase();
225
+ const url = match[2];
226
+ const payloadSnippet = match[3] || '';
227
+ const endpointPath = normalizeApiPath(url);
228
+ const bodyFields = payloadSnippet ? extractObjectKeys(payloadSnippet) : [];
229
+
230
+ results.push({
231
+ endpointPath,
232
+ method,
233
+ bodyFields,
234
+ rawSnippet: match[0],
235
+ line: this.findLineNumber(lines, match[0]),
236
+ language: 'python',
237
+ });
238
+ }
239
+ }
240
+
241
+ private extractCurlCalls(code: string, lines: string[], results: ExtractedApiCall[]): void {
242
+ // Matches: curl ... https://... -X POST -d '{...}'
243
+ const curlRegex = /curl\s+([^\n]+)/g;
244
+ let match: RegExpExecArray | null;
245
+
246
+ while ((match = curlRegex.exec(code)) !== null) {
247
+ const fullCmd = match[1];
248
+ const urlMatch = fullCmd.match(/https?:\/\/[^\s'"]+/);
249
+ if (!urlMatch) continue;
250
+
251
+ const endpointPath = normalizeApiPath(urlMatch[0]);
252
+ let method = 'get';
253
+ const methodMatch = fullCmd.match(/-X\s+([A-Z]+)/i);
254
+ if (methodMatch) {
255
+ method = methodMatch[1].toLowerCase();
256
+ } else if (fullCmd.includes('-d ') || fullCmd.includes('--data')) {
257
+ method = 'post';
258
+ }
259
+
260
+ let bodyFields: string[] = [];
261
+ const dataMatch = fullCmd.match(/(?:-d|--data(?:-raw)?)\s+['"]({[\s\S]*?})['"]/);
262
+ if (dataMatch) {
263
+ bodyFields = extractObjectKeys(dataMatch[1]);
264
+ }
265
+
266
+ results.push({
267
+ endpointPath,
268
+ method,
269
+ bodyFields,
270
+ rawSnippet: match[0],
271
+ line: this.findLineNumber(lines, match[0]),
272
+ language: 'curl',
273
+ });
274
+ }
275
+ }
276
+
277
+ private extractSdkCalls(code: string, lines: string[], results: ExtractedApiCall[]): void {
278
+ // Stripe SDK: stripe.webhookEndpoints.create({...})
279
+ const stripeWebhookRegex = /stripe\.webhookEndpoints\.create\s*\(\s*({[\s\S]*?})\s*\)/g;
280
+ let match: RegExpExecArray | null;
281
+ while ((match = stripeWebhookRegex.exec(code)) !== null) {
282
+ const bodyFields = extractObjectKeys(match[1]);
283
+ results.push({
284
+ endpointPath: '/v1/webhook_endpoints',
285
+ method: 'post',
286
+ symbol: 'stripe.webhookEndpoints.create',
287
+ bodyFields,
288
+ rawSnippet: match[0],
289
+ line: this.findLineNumber(lines, match[0]),
290
+ });
291
+ }
292
+
293
+ // Stripe SDK: stripe.charges.create({...})
294
+ const stripeChargesRegex = /stripe\.charges\.create\s*\(\s*({[\s\S]*?})\s*\)/g;
295
+ while ((match = stripeChargesRegex.exec(code)) !== null) {
296
+ const bodyFields = extractObjectKeys(match[1]);
297
+ results.push({
298
+ endpointPath: '/v1/charges',
299
+ method: 'post',
300
+ symbol: 'stripe.charges.create',
301
+ bodyFields,
302
+ rawSnippet: match[0],
303
+ line: this.findLineNumber(lines, match[0]),
304
+ });
305
+ }
306
+ }
307
+
308
+ private extractDynamicCalls(code: string, lines: string[], results: ExtractedApiCall[]): void {
309
+ // Matches fetch(dynamicVar) or axios.get(dynamicVar) where argument does not start with string quote
310
+ const dynamicFetch = /fetch\s*\(\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:,|\))/g;
311
+ let match: RegExpExecArray | null;
312
+ while ((match = dynamicFetch.exec(code)) !== null) {
313
+ const varName = match[1];
314
+ // Check if variable is defined as a string literal above
315
+ const defRegex = new RegExp(`(?:const|let|var)\\s+${varName}\\s*=\\s*['"\`]([^'"\`]+)['"\`]`);
316
+ if (!defRegex.test(code)) {
317
+ results.push({
318
+ rawSnippet: match[0],
319
+ isDynamicOrAmbiguous: true,
320
+ reasonIfAmbiguous: `Dynamic variable "${varName}" passed to fetch() cannot be statically resolved to an API path.`,
321
+ line: this.findLineNumber(lines, match[0]),
322
+ });
323
+ }
324
+ }
325
+ }
326
+
327
+ private extractResponseFields(code: string, _endpoint: string): string[] {
328
+ const fields: Set<string> = new Set();
329
+ // Matches res.data.fieldName or data.fieldName or json.fieldName or event.fieldName
330
+ const fieldRegex = /(?:res(?:ponse)?(?:\.data)?|data|json|body|payload)\.([a-zA-Z0-9_]+)/g;
331
+ let m: RegExpExecArray | null;
332
+ while ((m = fieldRegex.exec(code)) !== null) {
333
+ if (m[1] && !['json', 'status', 'headers', 'data', 'text'].includes(m[1])) {
334
+ fields.add(m[1]);
335
+ }
336
+ }
337
+ return Array.from(fields);
338
+ }
339
+ }