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,375 @@
1
+ import type {
2
+ ApiEndpoint,
3
+ ExtractedApiCall,
4
+ Pitfall,
5
+ VerificationFinding,
6
+ VerificationResult,
7
+ VerificationStatus,
8
+ } from '../../shared/src/index.ts';
9
+ import type { DocOrbitRepository } from '../../storage/src/index.ts';
10
+ import { matchEndpointPath } from './extractor.ts';
11
+
12
+ export interface VerifierOptions {
13
+ docVersion?: string;
14
+ projectVersion?: string;
15
+ library?: string;
16
+ }
17
+
18
+ export class SchemaVerifier {
19
+ private repo: DocOrbitRepository;
20
+
21
+ constructor(repo: DocOrbitRepository) {
22
+ this.repo = repo;
23
+ }
24
+
25
+ verify(calls: ExtractedApiCall[], options: VerifierOptions = {}): VerificationResult {
26
+ const findings: VerificationFinding[] = [];
27
+ const docVersion = options.docVersion;
28
+ const projectVersion = options.projectVersion;
29
+
30
+ // Load available endpoints and pitfalls for target version
31
+ const allEndpoints = this.repo.searchApiEndpoints('', {
32
+ docVersion,
33
+ limit: 200,
34
+ });
35
+ const allPitfalls = this.repo.searchPitfalls('', {
36
+ docVersion,
37
+ limit: 100,
38
+ });
39
+
40
+ let totalChecks = 0;
41
+
42
+ for (let i = 0; i < calls.length; i++) {
43
+ const call = calls[i];
44
+
45
+ // Handle ambiguous or unsupported calls
46
+ if (call.isDynamicOrAmbiguous) {
47
+ totalChecks++;
48
+ findings.push({
49
+ id: `find_ambiguous_${i}`,
50
+ rule: 'endpoint_validity',
51
+ severity: 'info',
52
+ status: 'insufficient_evidence',
53
+ message: call.reasonIfAmbiguous || 'Unable to statically verify dynamic or unsupported expression.',
54
+ location: {
55
+ line: call.line,
56
+ snippet: call.rawSnippet,
57
+ },
58
+ confidence: 0.5,
59
+ });
60
+ continue;
61
+ }
62
+
63
+ // 1. Next.js Version Syntax Checks
64
+ if (call.symbol?.startsWith('nextjs_route_params')) {
65
+ this.verifyNextJsParams(call, projectVersion || docVersion, findings, i);
66
+ totalChecks++;
67
+ continue;
68
+ }
69
+
70
+ // If no endpoint path was extracted
71
+ if (!call.endpointPath) {
72
+ totalChecks++;
73
+ findings.push({
74
+ id: `find_no_path_${i}`,
75
+ rule: 'endpoint_validity',
76
+ severity: 'info',
77
+ status: 'insufficient_evidence',
78
+ message: 'No API path could be extracted from code call.',
79
+ location: { line: call.line, snippet: call.rawSnippet },
80
+ confidence: 0.5,
81
+ });
82
+ continue;
83
+ }
84
+
85
+ // 2. Endpoint Path Match
86
+ const matchedEndpoints = allEndpoints.filter(ep =>
87
+ matchEndpointPath(call.endpointPath!, ep.path)
88
+ );
89
+
90
+ if (matchedEndpoints.length === 0) {
91
+ totalChecks++;
92
+ // Check if endpoint exists in ANY version (to see if removed)
93
+ const removedCheck = this.repo.searchApiEndpoints(call.endpointPath!, { limit: 5 });
94
+ const removedPitfall = allPitfalls.find(
95
+ p => p.kind === 'removed' && (p.relatedApi?.includes(call.endpointPath!) || p.content.includes(call.endpointPath!))
96
+ );
97
+
98
+ if (removedCheck.length > 0 || removedPitfall) {
99
+ findings.push({
100
+ id: `find_removed_${i}`,
101
+ rule: 'removed_api',
102
+ severity: 'error',
103
+ status: 'mismatch',
104
+ message: `Endpoint "${call.endpointPath}" was removed in API version ${docVersion || 'target'}.`,
105
+ location: { line: call.line, snippet: call.rawSnippet },
106
+ docVersion,
107
+ provenance: removedCheck[0]?.provenance ? {
108
+ sourceUrl: removedCheck[0].provenance.sourceUrl,
109
+ endpointId: removedCheck[0].id,
110
+ } : undefined,
111
+ confidence: 0.95,
112
+ });
113
+ } else {
114
+ findings.push({
115
+ id: `find_endpoint_${i}`,
116
+ rule: 'endpoint_validity',
117
+ severity: 'error',
118
+ status: 'mismatch',
119
+ message: `Endpoint "${call.endpointPath}" does not exist in API documentation (version: ${docVersion || 'latest'}).`,
120
+ location: { line: call.line, snippet: call.rawSnippet },
121
+ docVersion,
122
+ confidence: 0.9,
123
+ });
124
+ }
125
+ continue;
126
+ }
127
+
128
+ // 3. HTTP Method Match
129
+ const methodLower = (call.method || 'get').toLowerCase();
130
+ const exactMethodEp = matchedEndpoints.find(ep => ep.method.toLowerCase() === methodLower);
131
+
132
+ totalChecks++;
133
+ if (!exactMethodEp) {
134
+ const allowed = matchedEndpoints.map(ep => ep.method.toUpperCase()).join(', ');
135
+ findings.push({
136
+ id: `find_method_${i}`,
137
+ rule: 'method_validity',
138
+ severity: 'error',
139
+ status: 'mismatch',
140
+ message: `Endpoint "${call.endpointPath}" does not support HTTP method "${methodLower.toUpperCase()}". Supported methods: [${allowed}].`,
141
+ location: { line: call.line, snippet: call.rawSnippet },
142
+ expected: allowed,
143
+ actual: methodLower.toUpperCase(),
144
+ docVersion,
145
+ provenance: matchedEndpoints[0].provenance ? {
146
+ sourceUrl: matchedEndpoints[0].provenance.sourceUrl,
147
+ endpointId: matchedEndpoints[0].id,
148
+ } : undefined,
149
+ confidence: 0.95,
150
+ });
151
+ continue;
152
+ }
153
+
154
+ // 4. Deprecation Checks
155
+ totalChecks++;
156
+ if (exactMethodEp.deprecated) {
157
+ findings.push({
158
+ id: `find_dep_${i}`,
159
+ rule: 'deprecation',
160
+ severity: 'warning',
161
+ status: 'warning',
162
+ message: `Endpoint "${exactMethodEp.method.toUpperCase()} ${exactMethodEp.path}" is deprecated in documentation (version: ${docVersion || 'current'}).`,
163
+ location: { line: call.line, snippet: call.rawSnippet },
164
+ docVersion,
165
+ provenance: exactMethodEp.provenance ? {
166
+ sourceUrl: exactMethodEp.provenance.sourceUrl,
167
+ endpointId: exactMethodEp.id,
168
+ } : undefined,
169
+ confidence: 1.0,
170
+ });
171
+ }
172
+
173
+ // Check for related deprecation pitfalls
174
+ const depPitfall = allPitfalls.find(
175
+ p => p.kind === 'deprecated' && (p.relatedApi?.includes(exactMethodEp.path) || p.content.includes(exactMethodEp.path))
176
+ );
177
+ if (depPitfall) {
178
+ findings.push({
179
+ id: `find_dep_pit_${i}`,
180
+ rule: 'deprecation',
181
+ severity: 'warning',
182
+ status: 'warning',
183
+ message: `Deprecation notice for ${exactMethodEp.path}: ${depPitfall.title}`,
184
+ location: { line: call.line, snippet: call.rawSnippet },
185
+ docVersion,
186
+ provenance: depPitfall.provenance ? {
187
+ sourceUrl: depPitfall.provenance.sourceUrl,
188
+ pitfallId: depPitfall.id,
189
+ } : undefined,
190
+ confidence: 0.9,
191
+ });
192
+ }
193
+
194
+ // 5. Required Parameters & Body Fields
195
+ totalChecks++;
196
+ this.verifyParameters(call, exactMethodEp, findings, i, docVersion);
197
+
198
+ // 6. Response Assumptions
199
+ if (call.responseFieldsAccessed && call.responseFieldsAccessed.length > 0 && exactMethodEp.responseSchema) {
200
+ totalChecks++;
201
+ this.verifyResponseFields(call, exactMethodEp, findings, i, docVersion);
202
+ }
203
+ }
204
+
205
+ // Determine overall verdict
206
+ let verdict: VerificationStatus = 'verified';
207
+ if (findings.some(f => f.status === 'mismatch')) {
208
+ verdict = 'mismatch';
209
+ } else if (findings.some(f => f.status === 'warning')) {
210
+ verdict = 'warning';
211
+ } else if (findings.length > 0 && findings.every(f => f.status === 'insufficient_evidence')) {
212
+ verdict = 'insufficient_evidence';
213
+ }
214
+
215
+ // Build summary string
216
+ let summary: string;
217
+ const mismatches = findings.filter(f => f.status === 'mismatch').length;
218
+ const warnings = findings.filter(f => f.status === 'warning').length;
219
+
220
+ if (verdict === 'verified') {
221
+ summary = `All ${totalChecks} check(s) verified successfully against API documentation${docVersion ? ` (${docVersion})` : ''}.`;
222
+ } else if (verdict === 'mismatch') {
223
+ summary = `Verification failed with ${mismatches} mismatch(es) and ${warnings} warning(s).`;
224
+ } else if (verdict === 'warning') {
225
+ summary = `Verification passed with ${warnings} warning(s) (e.g. deprecated APIs or unverified fields).`;
226
+ } else {
227
+ summary = 'Verification inconclusive: insufficient evidence or unsupported expressions in code.';
228
+ }
229
+
230
+ return {
231
+ verdict,
232
+ summary,
233
+ targetVersion: docVersion,
234
+ projectVersion,
235
+ matchedLibrary: options.library,
236
+ findings,
237
+ extractedCalls: calls,
238
+ totalChecks,
239
+ untrusted: true,
240
+ };
241
+ }
242
+
243
+ private verifyParameters(
244
+ call: ExtractedApiCall,
245
+ ep: ApiEndpoint,
246
+ findings: VerificationFinding[],
247
+ idx: number,
248
+ docVersion?: string
249
+ ): void {
250
+ const requiredParams = ep.parameters.filter(p => p.required);
251
+ const bodyFields = new Set(call.bodyFields || []);
252
+
253
+ // Check required query/path parameters
254
+ for (const req of requiredParams) {
255
+ if (req.in === 'path') {
256
+ // Path parameters are verified by URL structure match
257
+ continue;
258
+ }
259
+ if (!bodyFields.has(req.name) && !(call.parameters && req.name in call.parameters)) {
260
+ findings.push({
261
+ id: `find_req_param_${idx}_${req.name}`,
262
+ rule: 'required_parameters',
263
+ severity: 'error',
264
+ status: 'mismatch',
265
+ message: `Missing required parameter "${req.name}" (${req.in}) for ${ep.method.toUpperCase()} ${ep.path}.`,
266
+ location: { line: call.line, snippet: call.rawSnippet },
267
+ expected: req.name,
268
+ docVersion,
269
+ provenance: ep.provenance ? {
270
+ sourceUrl: ep.provenance.sourceUrl,
271
+ endpointId: ep.id,
272
+ } : undefined,
273
+ confidence: 0.95,
274
+ });
275
+ }
276
+ }
277
+
278
+ // Check request body schema required fields
279
+ if (ep.requestSchema?.required && Array.isArray(ep.requestSchema.required)) {
280
+ for (const reqField of ep.requestSchema.required) {
281
+ if (!bodyFields.has(reqField)) {
282
+ findings.push({
283
+ id: `find_req_body_${idx}_${reqField}`,
284
+ rule: 'required_parameters',
285
+ severity: 'error',
286
+ status: 'mismatch',
287
+ message: `Missing required body field "${reqField}" for ${ep.method.toUpperCase()} ${ep.path}.`,
288
+ location: { line: call.line, snippet: call.rawSnippet },
289
+ expected: reqField,
290
+ docVersion,
291
+ provenance: ep.provenance ? {
292
+ sourceUrl: ep.provenance.sourceUrl,
293
+ endpointId: ep.id,
294
+ } : undefined,
295
+ confidence: 0.95,
296
+ });
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ private verifyResponseFields(
303
+ call: ExtractedApiCall,
304
+ ep: ApiEndpoint,
305
+ findings: VerificationFinding[],
306
+ idx: number,
307
+ docVersion?: string
308
+ ): void {
309
+ // Collect all documented response property names across 200/201 schemas
310
+ const documentedProps = new Set<string>();
311
+ for (const [status, resp] of Object.entries(ep.responseSchema || {})) {
312
+ if (status.startsWith('2') && resp.schema?.properties) {
313
+ for (const propName of Object.keys(resp.schema.properties)) {
314
+ documentedProps.add(propName);
315
+ }
316
+ }
317
+ }
318
+
319
+ if (documentedProps.size === 0) return;
320
+
321
+ for (const accessedField of call.responseFieldsAccessed || []) {
322
+ if (!documentedProps.has(accessedField)) {
323
+ findings.push({
324
+ id: `find_resp_${idx}_${accessedField}`,
325
+ rule: 'response_assumption',
326
+ severity: 'warning',
327
+ status: 'warning',
328
+ message: `Field "${accessedField}" accessed from response is not in documented 2xx response schema for ${ep.method.toUpperCase()} ${ep.path}.`,
329
+ location: { line: call.line, snippet: call.rawSnippet },
330
+ actual: accessedField,
331
+ docVersion,
332
+ provenance: ep.provenance ? {
333
+ sourceUrl: ep.provenance.sourceUrl,
334
+ endpointId: ep.id,
335
+ } : undefined,
336
+ confidence: 0.75,
337
+ });
338
+ }
339
+ }
340
+ }
341
+
342
+ private verifyNextJsParams(
343
+ call: ExtractedApiCall,
344
+ version: string | undefined,
345
+ findings: VerificationFinding[],
346
+ idx: number
347
+ ): void {
348
+ const isV14 = version?.includes('14') || version?.startsWith('v14');
349
+ const isV15OrAbove = version?.includes('15') || version?.includes('16') || version?.startsWith('v15') || version?.startsWith('v16');
350
+
351
+ if (call.symbol === 'nextjs_route_params_async' && isV14) {
352
+ findings.push({
353
+ id: `find_nextjs_${idx}`,
354
+ rule: 'version_mismatch',
355
+ severity: 'error',
356
+ status: 'mismatch',
357
+ message: 'Next.js 14 requires synchronous route params ("params.slug"). "await params" is only supported in Next.js 15+.',
358
+ location: { line: call.line, snippet: call.rawSnippet },
359
+ docVersion: version,
360
+ confidence: 0.95,
361
+ });
362
+ } else if (call.symbol === 'nextjs_route_params_sync' && isV15OrAbove) {
363
+ findings.push({
364
+ id: `find_nextjs_${idx}`,
365
+ rule: 'deprecation',
366
+ severity: 'warning',
367
+ status: 'warning',
368
+ message: 'Synchronous route params ("params.slug") is deprecated in Next.js 15 and removed in Next.js 16. Use "await params".',
369
+ location: { line: call.line, snippet: call.rawSnippet },
370
+ docVersion: version,
371
+ confidence: 0.95,
372
+ });
373
+ }
374
+ }
375
+ }
@@ -0,0 +1,143 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import type {
4
+ Ecosystem,
5
+ ProjectDependency,
6
+ WorkspaceScanResult,
7
+ } from '../../shared/src/index.ts';
8
+ import {
9
+ type EcosystemStrategy,
10
+ getDefaultStrategies,
11
+ } from './ecosystems/index.ts';
12
+
13
+ const IGNORED_DIRS = new Set([
14
+ 'node_modules',
15
+ '.git',
16
+ '.docorbit',
17
+ 'dist',
18
+ 'build',
19
+ 'target',
20
+ 'vendor',
21
+ '.venv',
22
+ 'venv',
23
+ '.next',
24
+ '.turbo',
25
+ ]);
26
+
27
+ /**
28
+ * Recursively locates files in a directory up to a max depth, skipping ignored folders.
29
+ */
30
+ function findManifestFiles(
31
+ dir: string,
32
+ targetFileNames: Set<string>,
33
+ maxDepth: number = 4,
34
+ currentDepth: number = 0
35
+ ): string[] {
36
+ if (currentDepth > maxDepth) return [];
37
+ const results: string[] = [];
38
+
39
+ try {
40
+ const entries = readdirSync(dir, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ if (IGNORED_DIRS.has(entry.name)) continue;
43
+
44
+ const fullPath = join(dir, entry.name);
45
+ if (entry.isDirectory()) {
46
+ results.push(...findManifestFiles(fullPath, targetFileNames, maxDepth, currentDepth + 1));
47
+ } else if (entry.isFile()) {
48
+ const name = entry.name.toLowerCase();
49
+ if (targetFileNames.has(name)) {
50
+ results.push(fullPath);
51
+ }
52
+ }
53
+ }
54
+ } catch {
55
+ // Directory unreadable, ignore
56
+ }
57
+
58
+ return results;
59
+ }
60
+
61
+ /**
62
+ * Scans a workspace directory, detects package manifests across supported ecosystems,
63
+ * correlates lockfiles with resolved versions, and preserves monorepo boundaries.
64
+ */
65
+ export function detectWorkspaceDependencies(
66
+ workspaceDir: string,
67
+ strategies: EcosystemStrategy[] = getDefaultStrategies()
68
+ ): WorkspaceScanResult {
69
+ // Index strategies by manifest and lockfile names
70
+ const manifestMap = new Map<string, EcosystemStrategy>();
71
+ const lockfileMap = new Map<string, EcosystemStrategy>();
72
+ const allTargetNames = new Set<string>();
73
+
74
+ for (const strat of strategies) {
75
+ for (const mName of strat.manifestNames) {
76
+ const lower = mName.toLowerCase();
77
+ manifestMap.set(lower, strat);
78
+ allTargetNames.add(lower);
79
+ }
80
+ if (strat.lockfileNames) {
81
+ for (const lName of strat.lockfileNames) {
82
+ const lower = lName.toLowerCase();
83
+ lockfileMap.set(lower, strat);
84
+ allTargetNames.add(lower);
85
+ }
86
+ }
87
+ }
88
+
89
+ const manifests = findManifestFiles(workspaceDir, allTargetNames);
90
+ const ecosystems = new Set<Ecosystem>();
91
+ const dependencies: ProjectDependency[] = [];
92
+ const manifestsFound: string[] = [];
93
+
94
+ // 1. Gather all lockfiles first to correlate resolved versions
95
+ const lockfileCache = new Map<string, Map<string, string>>();
96
+
97
+ for (const file of manifests) {
98
+ const rel = relative(workspaceDir, file);
99
+ manifestsFound.push(rel);
100
+ const baseName = file.split(/[/\\]/).pop()?.toLowerCase() || '';
101
+
102
+ const lockStrategy = lockfileMap.get(baseName);
103
+ if (lockStrategy && lockStrategy.parseLockfile) {
104
+ try {
105
+ const content = readFileSync(file, 'utf-8');
106
+ lockfileCache.set(rel, lockStrategy.parseLockfile(baseName, content));
107
+ } catch {
108
+ // Ignore unreadable lockfile
109
+ }
110
+ }
111
+ }
112
+
113
+ // 2. Parse package manifests and resolve versions
114
+ for (const file of manifests) {
115
+ const rel = relative(workspaceDir, file);
116
+ const baseName = file.split(/[/\\]/).pop()?.toLowerCase() || '';
117
+ const pkgDir = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '.';
118
+
119
+ const strat = manifestMap.get(baseName);
120
+ if (strat) {
121
+ try {
122
+ const content = readFileSync(file, 'utf-8');
123
+ ecosystems.add(strat.ecosystem);
124
+ const parsed = strat.parseManifest(baseName, content, rel, pkgDir);
125
+
126
+ if (strat.resolveVersions) {
127
+ strat.resolveVersions(parsed, pkgDir, lockfileCache);
128
+ }
129
+
130
+ dependencies.push(...parsed);
131
+ } catch {
132
+ // Ignore parse failure
133
+ }
134
+ }
135
+ }
136
+
137
+ return {
138
+ workspaceRoot: workspaceDir,
139
+ ecosystems: Array.from(ecosystems),
140
+ manifestsFound,
141
+ dependencies,
142
+ };
143
+ }
@@ -0,0 +1,84 @@
1
+ import { join } from 'node:path';
2
+ import type { ProjectDependency } from '../../../shared/src/index.ts';
3
+ import type { EcosystemStrategy } from './types.ts';
4
+
5
+ export class CargoStrategy implements EcosystemStrategy {
6
+ readonly ecosystem = 'cargo';
7
+ readonly manifestNames = ['cargo.toml'];
8
+ readonly lockfileNames = ['cargo.lock'];
9
+
10
+ parseManifest(_fileName: string, content: string, relPath: string, packageDir: string): ProjectDependency[] {
11
+ const deps: ProjectDependency[] = [];
12
+ let currentSection = '';
13
+ const lines = content.split('\n');
14
+
15
+ for (const line of lines) {
16
+ const trimmed = line.trim();
17
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
18
+ currentSection = trimmed.slice(1, -1).trim();
19
+ continue;
20
+ }
21
+
22
+ const isDep =
23
+ currentSection === 'dependencies' ||
24
+ currentSection === 'dev-dependencies' ||
25
+ currentSection === 'build-dependencies';
26
+
27
+ if (isDep && trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) {
28
+ const eqIdx = trimmed.indexOf('=');
29
+ const name = trimmed.slice(0, eqIdx).trim();
30
+ const val = trimmed.slice(eqIdx + 1).trim();
31
+
32
+ let version = '*';
33
+ if (val.startsWith('"')) {
34
+ version = val.replace(/^"|"$/g, '');
35
+ } else if (val.startsWith('{')) {
36
+ const vMatch = val.match(/version\s*=\s*"([^"]+)"/);
37
+ if (vMatch) version = vMatch[1];
38
+ }
39
+
40
+ deps.push({
41
+ name,
42
+ ecosystem: 'cargo',
43
+ requestedVersion: version,
44
+ sourceFile: relPath,
45
+ packagePath: packageDir,
46
+ isDev: currentSection === 'dev-dependencies',
47
+ });
48
+ }
49
+ }
50
+ return deps;
51
+ }
52
+
53
+ parseLockfile(_fileName: string, content: string): Map<string, string> {
54
+ const resolved = new Map<string, string>();
55
+ const blocks = content.split('[[package]]');
56
+ for (const block of blocks) {
57
+ const nameMatch = block.match(/name\s*=\s*"([^"]+)"/);
58
+ const verMatch = block.match(/version\s*=\s*"([^"]+)"/);
59
+ if (nameMatch && verMatch) {
60
+ resolved.set(nameMatch[1], verMatch[1]);
61
+ }
62
+ }
63
+ return resolved;
64
+ }
65
+
66
+ resolveVersions(
67
+ deps: ProjectDependency[],
68
+ pkgDir: string,
69
+ lockfileCache: Map<string, Map<string, string>>
70
+ ): void {
71
+ const lockMap =
72
+ lockfileCache.get('Cargo.lock') ||
73
+ lockfileCache.get('cargo.lock') ||
74
+ lockfileCache.get(join(pkgDir, 'Cargo.lock')) ||
75
+ lockfileCache.get(join(pkgDir, 'cargo.lock'));
76
+
77
+ if (lockMap) {
78
+ for (const dep of deps) {
79
+ const resolved = lockMap.get(dep.name);
80
+ if (resolved) dep.resolvedVersion = resolved;
81
+ }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,42 @@
1
+ import type { ProjectDependency } from '../../../shared/src/index.ts';
2
+ import type { EcosystemStrategy } from './types.ts';
3
+
4
+ export class ComposerStrategy implements EcosystemStrategy {
5
+ readonly ecosystem = 'composer';
6
+ readonly manifestNames = ['composer.json'];
7
+
8
+ parseManifest(_fileName: string, content: string, relPath: string, packageDir: string): ProjectDependency[] {
9
+ const deps: ProjectDependency[] = [];
10
+ try {
11
+ const composer = JSON.parse(content);
12
+ if (composer.require && typeof composer.require === 'object') {
13
+ for (const [name, ver] of Object.entries(composer.require)) {
14
+ if (name === 'php') continue;
15
+ deps.push({
16
+ name,
17
+ ecosystem: 'composer',
18
+ requestedVersion: String(ver),
19
+ sourceFile: relPath,
20
+ packagePath: packageDir,
21
+ isDev: false,
22
+ });
23
+ }
24
+ }
25
+ if (composer['require-dev'] && typeof composer['require-dev'] === 'object') {
26
+ for (const [name, ver] of Object.entries(composer['require-dev'])) {
27
+ deps.push({
28
+ name,
29
+ ecosystem: 'composer',
30
+ requestedVersion: String(ver),
31
+ sourceFile: relPath,
32
+ packagePath: packageDir,
33
+ isDev: true,
34
+ });
35
+ }
36
+ }
37
+ } catch {
38
+ // Ignore malformed JSON
39
+ }
40
+ return deps;
41
+ }
42
+ }
@@ -0,0 +1,54 @@
1
+ import type { ProjectDependency } from '../../../shared/src/index.ts';
2
+ import type { EcosystemStrategy } from './types.ts';
3
+
4
+ export class GoStrategy implements EcosystemStrategy {
5
+ readonly ecosystem = 'go';
6
+ readonly manifestNames = ['go.mod'];
7
+
8
+ parseManifest(_fileName: string, content: string, relPath: string, packageDir: string): ProjectDependency[] {
9
+ const deps: ProjectDependency[] = [];
10
+ const lines = content.split('\n');
11
+ let inRequireBlock = false;
12
+
13
+ for (const line of lines) {
14
+ const trimmed = line.trim();
15
+ if (trimmed.startsWith('require (')) {
16
+ inRequireBlock = true;
17
+ continue;
18
+ }
19
+ if (inRequireBlock && trimmed === ')') {
20
+ inRequireBlock = false;
21
+ continue;
22
+ }
23
+
24
+ if (inRequireBlock && trimmed && !trimmed.startsWith('//')) {
25
+ const parts = trimmed.split(/\s+/);
26
+ if (parts.length >= 2) {
27
+ deps.push({
28
+ name: parts[0],
29
+ ecosystem: 'go',
30
+ requestedVersion: parts[1],
31
+ resolvedVersion: parts[1].replace(/^\+incompatible$/, ''),
32
+ sourceFile: relPath,
33
+ packagePath: packageDir,
34
+ isDev: false,
35
+ });
36
+ }
37
+ } else if (trimmed.startsWith('require ') && !trimmed.includes('(')) {
38
+ const parts = trimmed.slice('require '.length).trim().split(/\s+/);
39
+ if (parts.length >= 2) {
40
+ deps.push({
41
+ name: parts[0],
42
+ ecosystem: 'go',
43
+ requestedVersion: parts[1],
44
+ resolvedVersion: parts[1].replace(/^\+incompatible$/, ''),
45
+ sourceFile: relPath,
46
+ packagePath: packageDir,
47
+ isDev: false,
48
+ });
49
+ }
50
+ }
51
+ }
52
+ return deps;
53
+ }
54
+ }