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.
- package/LICENSE +21 -0
- package/README.md +660 -0
- package/apps/cli/bin/docorbit.js +8 -0
- package/apps/cli/src/commands/add.ts +44 -0
- package/apps/cli/src/commands/api.ts +38 -0
- package/apps/cli/src/commands/context.ts +47 -0
- package/apps/cli/src/commands/dashboard.ts +55 -0
- package/apps/cli/src/commands/diff.ts +30 -0
- package/apps/cli/src/commands/evaluate.ts +133 -0
- package/apps/cli/src/commands/examples.ts +39 -0
- package/apps/cli/src/commands/export.ts +89 -0
- package/apps/cli/src/commands/impact.ts +31 -0
- package/apps/cli/src/commands/init.ts +69 -0
- package/apps/cli/src/commands/inspect.ts +30 -0
- package/apps/cli/src/commands/mcp.ts +72 -0
- package/apps/cli/src/commands/pitfalls.ts +38 -0
- package/apps/cli/src/commands/recipes.ts +35 -0
- package/apps/cli/src/commands/search.ts +48 -0
- package/apps/cli/src/commands/update.ts +73 -0
- package/apps/cli/src/commands/verify.ts +48 -0
- package/apps/cli/src/formatters/colors.ts +23 -0
- package/apps/cli/src/formatters/inspection.ts +102 -0
- package/apps/cli/src/formatters/knowledge.ts +272 -0
- package/apps/cli/src/formatters/retrieval.ts +74 -0
- package/apps/cli/src/formatters/terminal.ts +6 -0
- package/apps/cli/src/formatters/verification.ts +126 -0
- package/apps/cli/src/index.ts +409 -0
- package/bin/docorbit.js +8 -0
- package/package.json +46 -0
- package/packages/core/src/dashboard/server.ts +314 -0
- package/packages/core/src/dashboard/ui.ts +586 -0
- package/packages/core/src/implementation-service.ts +451 -0
- package/packages/core/src/index.ts +7 -0
- package/packages/core/src/inspector.ts +71 -0
- package/packages/core/src/pipeline.ts +331 -0
- package/packages/crawler/src/config.ts +12 -0
- package/packages/crawler/src/fetcher.ts +185 -0
- package/packages/crawler/src/index.ts +2 -0
- package/packages/discovery/src/index.ts +31 -0
- package/packages/discovery/src/provider.ts +47 -0
- package/packages/discovery/src/providers/generic.ts +98 -0
- package/packages/discovery/src/providers/github.ts +61 -0
- package/packages/discovery/src/providers/llms-txt.ts +73 -0
- package/packages/discovery/src/providers/markdown.ts +48 -0
- package/packages/discovery/src/providers/openapi.ts +91 -0
- package/packages/discovery/src/providers/sitemap.ts +62 -0
- package/packages/discovery/src/providers/skill.ts +54 -0
- package/packages/discovery/src/ranker.ts +123 -0
- package/packages/evaluation/src/dataset.ts +963 -0
- package/packages/evaluation/src/index.ts +8 -0
- package/packages/evaluation/src/runner.ts +241 -0
- package/packages/evaluation/src/strategies/context7-runner.ts +269 -0
- package/packages/evaluation/src/strategies/docorbit-runner.ts +228 -0
- package/packages/evaluation/src/strategies/firecrawl-runner.ts +172 -0
- package/packages/evaluation/src/strategies/web-search-runner.ts +194 -0
- package/packages/evaluation/src/types.ts +34 -0
- package/packages/evaluation/src/version-matcher.ts +73 -0
- package/packages/export/src/agents-md.ts +200 -0
- package/packages/export/src/claude-md.ts +141 -0
- package/packages/export/src/docs-map.ts +150 -0
- package/packages/export/src/index.ts +6 -0
- package/packages/export/src/llms-txt.ts +96 -0
- package/packages/export/src/service.ts +250 -0
- package/packages/export/src/skill-md.ts +128 -0
- package/packages/mcp/src/index.ts +46 -0
- package/packages/mcp/src/resources/index.ts +189 -0
- package/packages/mcp/src/server.ts +278 -0
- package/packages/mcp/src/tools/analyze-impact.ts +74 -0
- package/packages/mcp/src/tools/check-api.ts +86 -0
- package/packages/mcp/src/tools/diff-docs.ts +68 -0
- package/packages/mcp/src/tools/export-context.ts +73 -0
- package/packages/mcp/src/tools/find-api.ts +99 -0
- package/packages/mcp/src/tools/find-example.ts +100 -0
- package/packages/mcp/src/tools/find-pitfall.ts +94 -0
- package/packages/mcp/src/tools/find-recipe.ts +98 -0
- package/packages/mcp/src/tools/get-doc.ts +130 -0
- package/packages/mcp/src/tools/get-docs-map.ts +64 -0
- package/packages/mcp/src/tools/get-version.ts +118 -0
- package/packages/mcp/src/tools/implementation-context.ts +88 -0
- package/packages/mcp/src/tools/index.ts +59 -0
- package/packages/mcp/src/tools/list-sources.ts +85 -0
- package/packages/mcp/src/tools/search-docs.ts +123 -0
- package/packages/mcp/src/tools/types.ts +28 -0
- package/packages/mcp/src/transports/http.ts +256 -0
- package/packages/mcp/src/transports/stdio.ts +105 -0
- package/packages/mcp/src/transports/types.ts +6 -0
- package/packages/mcp/src/types.ts +102 -0
- package/packages/normalizer/src/example-indexer.ts +240 -0
- package/packages/normalizer/src/html.ts +253 -0
- package/packages/normalizer/src/index.ts +8 -0
- package/packages/normalizer/src/llms.ts +83 -0
- package/packages/normalizer/src/openapi/endpoint-parser.ts +406 -0
- package/packages/normalizer/src/openapi/schema-resolver.ts +111 -0
- package/packages/normalizer/src/openapi.ts +2 -0
- package/packages/normalizer/src/page.ts +184 -0
- package/packages/normalizer/src/pitfall-extractor.ts +190 -0
- package/packages/normalizer/src/slicer.ts +455 -0
- package/packages/retrieval/src/engine.ts +120 -0
- package/packages/retrieval/src/index.ts +7 -0
- package/packages/retrieval/src/intent.ts +43 -0
- package/packages/retrieval/src/packer.ts +145 -0
- package/packages/retrieval/src/recipe-engine.ts +313 -0
- package/packages/retrieval/src/scorer.ts +139 -0
- package/packages/retrieval/src/weights.ts +31 -0
- package/packages/security/src/annotations.ts +112 -0
- package/packages/security/src/index.ts +2 -0
- package/packages/security/src/ssrf.ts +153 -0
- package/packages/shared/src/errors.ts +53 -0
- package/packages/shared/src/hashing.ts +23 -0
- package/packages/shared/src/index.ts +3 -0
- package/packages/shared/src/types.ts +881 -0
- package/packages/storage/src/db.ts +72 -0
- package/packages/storage/src/index.ts +11 -0
- package/packages/storage/src/interfaces.ts +115 -0
- package/packages/storage/src/repositories/api-repository.ts +219 -0
- package/packages/storage/src/repositories/chunk-repository.ts +316 -0
- package/packages/storage/src/repositories/example-repository.ts +206 -0
- package/packages/storage/src/repositories/page-repository.ts +205 -0
- package/packages/storage/src/repositories/pitfall-repository.ts +188 -0
- package/packages/storage/src/repositories/source-repository.ts +205 -0
- package/packages/storage/src/repository.ts +256 -0
- package/packages/storage/src/schema.ts +269 -0
- package/packages/storage/src/search-tokens.ts +28 -0
- package/packages/verification/src/diff-engine.ts +258 -0
- package/packages/verification/src/extractor.ts +339 -0
- package/packages/verification/src/impact-scanner.ts +203 -0
- package/packages/verification/src/index.ts +5 -0
- package/packages/verification/src/services.ts +238 -0
- package/packages/verification/src/verifier.ts +375 -0
- package/packages/workspace/src/detector.ts +143 -0
- package/packages/workspace/src/ecosystems/cargo.ts +84 -0
- package/packages/workspace/src/ecosystems/composer.ts +42 -0
- package/packages/workspace/src/ecosystems/go.ts +54 -0
- package/packages/workspace/src/ecosystems/index.ts +34 -0
- package/packages/workspace/src/ecosystems/maven.ts +34 -0
- package/packages/workspace/src/ecosystems/npm.ts +83 -0
- package/packages/workspace/src/ecosystems/pub.ts +40 -0
- package/packages/workspace/src/ecosystems/pypi.ts +100 -0
- package/packages/workspace/src/ecosystems/rubygems.ts +30 -0
- package/packages/workspace/src/ecosystems/types.ts +18 -0
- package/packages/workspace/src/index.ts +5 -0
- package/packages/workspace/src/lockfile.ts +194 -0
- package/packages/workspace/src/resolver.ts +234 -0
- package/packages/workspace/src/semver.ts +259 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocsLock,
|
|
3
|
+
ProjectDependency,
|
|
4
|
+
WorkspaceScanResult,
|
|
5
|
+
VersionResolutionResult,
|
|
6
|
+
} from '../../shared/src/index.ts';
|
|
7
|
+
import type { DocOrbitRepository } from '../../storage/src/index.ts';
|
|
8
|
+
import { readDocsLock } from './lockfile.ts';
|
|
9
|
+
import { detectWorkspaceDependencies } from './detector.ts';
|
|
10
|
+
import { resolveDocVersion } from './semver.ts';
|
|
11
|
+
|
|
12
|
+
export interface ResolvedProjectContext {
|
|
13
|
+
projectDir: string;
|
|
14
|
+
hasLockfile: boolean;
|
|
15
|
+
matchedDependency?: {
|
|
16
|
+
name: string;
|
|
17
|
+
projectVersion: string;
|
|
18
|
+
targetDocVersion: string;
|
|
19
|
+
targetSnapshotId?: string;
|
|
20
|
+
confidence: number;
|
|
21
|
+
matchType: string;
|
|
22
|
+
};
|
|
23
|
+
allDependencies: ProjectDependency[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalizes query string for token matching against dependency names.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeQueryTokens(query: string): Set<string> {
|
|
30
|
+
const tokens = query
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^\w\s-]/g, ' ')
|
|
33
|
+
.split(/\s+/)
|
|
34
|
+
.filter(t => t.length > 1);
|
|
35
|
+
|
|
36
|
+
const set = new Set(tokens);
|
|
37
|
+
// Add common aliases
|
|
38
|
+
if (set.has('nextjs') || set.has('next.js')) {
|
|
39
|
+
set.add('next');
|
|
40
|
+
}
|
|
41
|
+
if (set.has('reactjs') || set.has('react.js')) {
|
|
42
|
+
set.add('react');
|
|
43
|
+
}
|
|
44
|
+
if (set.has('vuejs') || set.has('vue.js')) {
|
|
45
|
+
set.add('vue');
|
|
46
|
+
}
|
|
47
|
+
return set;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolves project context and detects if a query targets a specific dependency in the project.
|
|
52
|
+
* Uses docs.lock if present for zero-overhead deterministic pinned resolution,
|
|
53
|
+
* or falls back to live manifest scanning and resolution.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveProjectContext(
|
|
56
|
+
projectDir: string,
|
|
57
|
+
query: string,
|
|
58
|
+
repo: DocOrbitRepository
|
|
59
|
+
): ResolvedProjectContext {
|
|
60
|
+
const lock = readDocsLock(projectDir);
|
|
61
|
+
const queryTokens = normalizeQueryTokens(query);
|
|
62
|
+
|
|
63
|
+
if (lock) {
|
|
64
|
+
// 1. Check locked dependencies
|
|
65
|
+
for (const [key, lockedDoc] of Object.entries(lock.dependencies)) {
|
|
66
|
+
const cleanName = lockedDoc.name.toLowerCase().replace(/^@[^/]+\//, '');
|
|
67
|
+
const parts = cleanName.split(/[-_.]/);
|
|
68
|
+
|
|
69
|
+
const matches =
|
|
70
|
+
queryTokens.has(lockedDoc.name.toLowerCase()) ||
|
|
71
|
+
queryTokens.has(cleanName) ||
|
|
72
|
+
parts.some(p => p.length > 2 && queryTokens.has(p));
|
|
73
|
+
|
|
74
|
+
if (matches) {
|
|
75
|
+
return {
|
|
76
|
+
projectDir,
|
|
77
|
+
hasLockfile: true,
|
|
78
|
+
matchedDependency: {
|
|
79
|
+
name: lockedDoc.name,
|
|
80
|
+
projectVersion: lockedDoc.resolvedDependencyVersion,
|
|
81
|
+
targetDocVersion: lockedDoc.docVersion,
|
|
82
|
+
targetSnapshotId: lockedDoc.snapshotId,
|
|
83
|
+
confidence: lockedDoc.confidence,
|
|
84
|
+
matchType: lockedDoc.matchType,
|
|
85
|
+
},
|
|
86
|
+
allDependencies: Object.values(lock.dependencies).map(d => ({
|
|
87
|
+
name: d.name,
|
|
88
|
+
ecosystem: d.ecosystem,
|
|
89
|
+
requestedVersion: d.requestedVersion,
|
|
90
|
+
resolvedVersion: d.resolvedDependencyVersion,
|
|
91
|
+
sourceFile: d.sourceFile,
|
|
92
|
+
})),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
projectDir,
|
|
99
|
+
hasLockfile: true,
|
|
100
|
+
allDependencies: Object.values(lock.dependencies).map(d => ({
|
|
101
|
+
name: d.name,
|
|
102
|
+
ecosystem: d.ecosystem,
|
|
103
|
+
requestedVersion: d.requestedVersion,
|
|
104
|
+
resolvedVersion: d.resolvedDependencyVersion,
|
|
105
|
+
sourceFile: d.sourceFile,
|
|
106
|
+
})),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 2. Fallback: scan workspace manifests directly
|
|
111
|
+
const scan = detectWorkspaceDependencies(projectDir);
|
|
112
|
+
const allSnapshots = repo.listSnapshots();
|
|
113
|
+
const allSources = repo.listSources();
|
|
114
|
+
|
|
115
|
+
for (const dep of scan.dependencies) {
|
|
116
|
+
const cleanName = dep.name.toLowerCase().replace(/^@[^/]+\//, '');
|
|
117
|
+
const parts = cleanName.split(/[-_.]/);
|
|
118
|
+
|
|
119
|
+
const matches =
|
|
120
|
+
queryTokens.has(dep.name.toLowerCase()) ||
|
|
121
|
+
queryTokens.has(cleanName) ||
|
|
122
|
+
parts.some(p => p.length > 2 && queryTokens.has(p));
|
|
123
|
+
|
|
124
|
+
if (matches) {
|
|
125
|
+
// Find matching source
|
|
126
|
+
const src = allSources.find(s => {
|
|
127
|
+
const u = s.url.toLowerCase();
|
|
128
|
+
return u.includes(cleanName) || u.includes(dep.name.toLowerCase());
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (src) {
|
|
132
|
+
const snapshots = allSnapshots.filter(s => s.sourceId === src.id);
|
|
133
|
+
const availableVersions = snapshots.map(s => s.docVersion || 'latest');
|
|
134
|
+
const projectVer = dep.resolvedVersion || dep.requestedVersion;
|
|
135
|
+
const resolution = resolveDocVersion(projectVer, availableVersions, dep.name);
|
|
136
|
+
|
|
137
|
+
const chosenSnapshot = snapshots.find(
|
|
138
|
+
s => (s.docVersion || 'latest') === resolution.selectedDocVersion
|
|
139
|
+
) || snapshots[0];
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
projectDir,
|
|
143
|
+
hasLockfile: false,
|
|
144
|
+
matchedDependency: {
|
|
145
|
+
name: dep.name,
|
|
146
|
+
projectVersion: projectVer,
|
|
147
|
+
targetDocVersion: resolution.selectedDocVersion,
|
|
148
|
+
targetSnapshotId: chosenSnapshot?.id,
|
|
149
|
+
confidence: resolution.confidence,
|
|
150
|
+
matchType: resolution.matchType,
|
|
151
|
+
},
|
|
152
|
+
allDependencies: scan.dependencies,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
projectDir,
|
|
160
|
+
hasLockfile: false,
|
|
161
|
+
allDependencies: scan.dependencies,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface WorkspaceMatch {
|
|
166
|
+
dependency: ProjectDependency;
|
|
167
|
+
targetVersion: string;
|
|
168
|
+
confidence: number;
|
|
169
|
+
snapshotId?: string;
|
|
170
|
+
matchedBy: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface WorkspaceResolutionSummary {
|
|
174
|
+
matches: WorkspaceMatch[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class WorkspaceResolver {
|
|
178
|
+
private repo: DocOrbitRepository;
|
|
179
|
+
|
|
180
|
+
constructor(repo: DocOrbitRepository) {
|
|
181
|
+
this.repo = repo;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
resolveProjectContext(projectDir: string, query: string): ResolvedProjectContext {
|
|
185
|
+
return resolveProjectContext(projectDir, query, this.repo);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
scanWorkspace(projectDir: string): WorkspaceScanResult {
|
|
189
|
+
return detectWorkspaceDependencies(projectDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
resolveWorkspace(scan: WorkspaceScanResult, _existingLock?: DocsLock | null): WorkspaceResolutionSummary {
|
|
193
|
+
const allSnapshots = this.repo.listSnapshots();
|
|
194
|
+
const allSources = this.repo.listSources();
|
|
195
|
+
const matches: WorkspaceMatch[] = [];
|
|
196
|
+
|
|
197
|
+
for (const dep of scan.dependencies) {
|
|
198
|
+
const cleanName = dep.name.toLowerCase().replace(/^@[^/]+\//, '');
|
|
199
|
+
const src = allSources.find(s => {
|
|
200
|
+
const u = s.url.toLowerCase();
|
|
201
|
+
return u.includes(cleanName) || u.includes(dep.name.toLowerCase()) ||
|
|
202
|
+
(s.metadata && String(s.metadata.name || '').toLowerCase() === cleanName);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!src) continue;
|
|
206
|
+
|
|
207
|
+
const snapshots = allSnapshots.filter(s => s.sourceId === src.id);
|
|
208
|
+
if (snapshots.length === 0) continue;
|
|
209
|
+
|
|
210
|
+
const availableVersions = snapshots.map(s => s.docVersion || 'latest');
|
|
211
|
+
const projectVer = dep.resolvedVersion || dep.requestedVersion;
|
|
212
|
+
const resolution = resolveDocVersion(projectVer, availableVersions, dep.name);
|
|
213
|
+
|
|
214
|
+
const chosenSnapshot = snapshots.find(
|
|
215
|
+
s => (s.docVersion || 'latest') === resolution.selectedDocVersion
|
|
216
|
+
) || snapshots[0];
|
|
217
|
+
|
|
218
|
+
matches.push({
|
|
219
|
+
dependency: dep,
|
|
220
|
+
targetVersion: resolution.selectedDocVersion,
|
|
221
|
+
confidence: resolution.confidence,
|
|
222
|
+
snapshotId: chosenSnapshot?.id,
|
|
223
|
+
matchedBy: resolution.matchType,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { matches };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
resolveVersion(projectVersion: string, availableDocVersions: string[], packageName?: string): VersionResolutionResult {
|
|
231
|
+
return resolveDocVersion(projectVersion, availableDocVersions, packageName);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocVersionMatch,
|
|
3
|
+
VersionResolutionResult,
|
|
4
|
+
} from '../../shared/src/index.ts';
|
|
5
|
+
|
|
6
|
+
export interface ParsedSemVer {
|
|
7
|
+
major: number;
|
|
8
|
+
minor: number;
|
|
9
|
+
patch: number;
|
|
10
|
+
prerelease?: string;
|
|
11
|
+
raw: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parses a version string into semantic components.
|
|
16
|
+
* Handles standard SemVer, leading 'v', partial versions ('14', '14.2'),
|
|
17
|
+
* and prereleases ('15.0.0-rc.1', '14.2.0-canary').
|
|
18
|
+
*/
|
|
19
|
+
export function parseSemVer(versionStr: string): ParsedSemVer | null {
|
|
20
|
+
if (!versionStr || typeof versionStr !== 'string') return null;
|
|
21
|
+
|
|
22
|
+
const cleaned = versionStr.trim().replace(/^[v=]/, '');
|
|
23
|
+
// Match major, optional minor, optional patch, optional prerelease
|
|
24
|
+
const match = cleaned.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/);
|
|
25
|
+
if (!match) return null;
|
|
26
|
+
|
|
27
|
+
const major = parseInt(match[1], 10);
|
|
28
|
+
const minor = match[2] !== undefined ? parseInt(match[2], 10) : 0;
|
|
29
|
+
const patch = match[3] !== undefined ? parseInt(match[3], 10) : 0;
|
|
30
|
+
const prerelease = match[4];
|
|
31
|
+
|
|
32
|
+
return { major, minor, patch, prerelease, raw: versionStr.trim() };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Deterministic SemVer comparison.
|
|
37
|
+
* Returns negative if v1 < v2, positive if v1 > v2, 0 if v1 == v2.
|
|
38
|
+
* Prereleases have lower precedence than normal releases (e.g. 15.0.0-rc.1 < 15.0.0).
|
|
39
|
+
*/
|
|
40
|
+
export function compareVersions(v1: string, v2: string): number {
|
|
41
|
+
const p1 = parseSemVer(v1);
|
|
42
|
+
const p2 = parseSemVer(v2);
|
|
43
|
+
|
|
44
|
+
if (!p1 && !p2) return v1.localeCompare(v2);
|
|
45
|
+
if (!p1) return -1;
|
|
46
|
+
if (!p2) return 1;
|
|
47
|
+
|
|
48
|
+
if (p1.major !== p2.major) return p1.major - p2.major;
|
|
49
|
+
if (p1.minor !== p2.minor) return p1.minor - p2.minor;
|
|
50
|
+
if (p1.patch !== p2.patch) return p1.patch - p2.patch;
|
|
51
|
+
|
|
52
|
+
// Prerelease comparison:
|
|
53
|
+
// 1.0.0-alpha < 1.0.0 (version with prerelease is lower than version without)
|
|
54
|
+
if (p1.prerelease && !p2.prerelease) return -1;
|
|
55
|
+
if (!p1.prerelease && p2.prerelease) return 1;
|
|
56
|
+
if (p1.prerelease && p2.prerelease) {
|
|
57
|
+
return p1.prerelease.localeCompare(p2.prerelease);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Evaluates whether a concrete version satisfies a SemVer range specification.
|
|
65
|
+
* Supports '^', '~', '>=', '<=', '<', '>', '*', 'x', space-separated and '||'.
|
|
66
|
+
*/
|
|
67
|
+
export function satisfiesRange(version: string, range: string): boolean {
|
|
68
|
+
if (!range || range === '*' || range === 'latest' || range === 'x') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ver = parseSemVer(version);
|
|
73
|
+
if (!ver) return false;
|
|
74
|
+
|
|
75
|
+
// Handle disjunction (||)
|
|
76
|
+
if (range.includes('||')) {
|
|
77
|
+
return range.split('||').some(r => satisfiesRange(version, r.trim()));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle conjunction (space separated conditions e.g. ">=14.0.0 <15.0.0")
|
|
81
|
+
const clauses = range.trim().split(/\s+/).filter(Boolean);
|
|
82
|
+
if (clauses.length > 1) {
|
|
83
|
+
return clauses.every(c => satisfiesRange(version, c));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const single = clauses[0];
|
|
87
|
+
|
|
88
|
+
// Caret ranges: ^14.2.0 -> >=14.2.0 <15.0.0 (or for 0.x: ^0.2.3 -> >=0.2.3 <0.3.0)
|
|
89
|
+
if (single.startsWith('^')) {
|
|
90
|
+
const base = parseSemVer(single.slice(1));
|
|
91
|
+
if (!base) return false;
|
|
92
|
+
if (ver.major !== base.major) return false;
|
|
93
|
+
if (base.major === 0) {
|
|
94
|
+
if (ver.minor !== base.minor) return false;
|
|
95
|
+
return ver.patch >= base.patch;
|
|
96
|
+
}
|
|
97
|
+
if (ver.minor < base.minor) return false;
|
|
98
|
+
if (ver.minor === base.minor && ver.patch < base.patch) return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Tilde ranges: ~14.2.0 -> >=14.2.0 <14.3.0
|
|
103
|
+
if (single.startsWith('~')) {
|
|
104
|
+
const base = parseSemVer(single.slice(1));
|
|
105
|
+
if (!base) return false;
|
|
106
|
+
if (ver.major !== base.major || ver.minor !== base.minor) return false;
|
|
107
|
+
return ver.patch >= base.patch;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Wildcard ranges: 14.x or 14.*
|
|
111
|
+
const wildcardMatch = single.match(/^(\d+)(?:\.(\d+|\*|x))?(?:\.(\d+|\*|x))?$/);
|
|
112
|
+
if (wildcardMatch) {
|
|
113
|
+
const wMajor = parseInt(wildcardMatch[1], 10);
|
|
114
|
+
const wMinor = wildcardMatch[2];
|
|
115
|
+
if (ver.major !== wMajor) return false;
|
|
116
|
+
if (wMinor && wMinor !== '*' && wMinor !== 'x') {
|
|
117
|
+
if (ver.minor !== parseInt(wMinor, 10)) return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Comparison operators: >=, <=, >, <, =
|
|
123
|
+
const compMatch = single.match(/^(>=|<=|>|<|=)(.+)$/);
|
|
124
|
+
if (compMatch) {
|
|
125
|
+
const op = compMatch[1];
|
|
126
|
+
const target = compMatch[2].trim();
|
|
127
|
+
const cmp = compareVersions(version, target);
|
|
128
|
+
if (op === '>=') return cmp >= 0;
|
|
129
|
+
if (op === '<=') return cmp <= 0;
|
|
130
|
+
if (op === '>') return cmp > 0;
|
|
131
|
+
if (op === '<') return cmp < 0;
|
|
132
|
+
if (op === '=') return cmp === 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Direct equality check
|
|
136
|
+
const targetVer = parseSemVer(single);
|
|
137
|
+
if (targetVer) {
|
|
138
|
+
return (
|
|
139
|
+
ver.major === targetVer.major &&
|
|
140
|
+
ver.minor === targetVer.minor &&
|
|
141
|
+
ver.patch === targetVer.patch
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Deterministically resolves a project's dependency version to the best available
|
|
150
|
+
* documentation version using the hierarchical confidence ladder:
|
|
151
|
+
* exact (1.0) -> major_minor (0.95) -> major (0.90) -> range (0.85) -> latest_fallback (0.50) -> unresolved (0.0)
|
|
152
|
+
*
|
|
153
|
+
* Never claims exact match when documentation is coarse (e.g. 'v14' or 'latest').
|
|
154
|
+
*/
|
|
155
|
+
export function resolveDocVersion(
|
|
156
|
+
projectVersion: string,
|
|
157
|
+
availableDocVersions: string[],
|
|
158
|
+
dependencyName: string = 'package'
|
|
159
|
+
): VersionResolutionResult {
|
|
160
|
+
if (!availableDocVersions || availableDocVersions.length === 0) {
|
|
161
|
+
return {
|
|
162
|
+
dependencyName,
|
|
163
|
+
projectVersion,
|
|
164
|
+
selectedDocVersion: 'unresolved',
|
|
165
|
+
matchType: 'unresolved',
|
|
166
|
+
confidence: 0.0,
|
|
167
|
+
availableVersions: [],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const cleaned = projectVersion.replace(/^[\^~=v><\s]+/, '');
|
|
172
|
+
const pVer = parseSemVer(projectVersion) || parseSemVer(cleaned);
|
|
173
|
+
const cleanTarget = projectVersion.replace(/^[v=]/, '');
|
|
174
|
+
|
|
175
|
+
// 1. Exact match (e.g. project '14.2.3' and doc '14.2.3' or 'v14.2.3')
|
|
176
|
+
const exactMatch = availableDocVersions.find(v => {
|
|
177
|
+
const c = v.replace(/^[v=]/, '');
|
|
178
|
+
return c === cleanTarget;
|
|
179
|
+
});
|
|
180
|
+
if (exactMatch) {
|
|
181
|
+
return {
|
|
182
|
+
dependencyName,
|
|
183
|
+
projectVersion,
|
|
184
|
+
selectedDocVersion: exactMatch,
|
|
185
|
+
matchType: 'exact',
|
|
186
|
+
confidence: 1.0,
|
|
187
|
+
availableVersions: availableDocVersions,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (pVer) {
|
|
192
|
+
// 2. Major.Minor match (e.g. project '14.2.3' and doc 'v14.2' or '14.2')
|
|
193
|
+
const mmTarget = `${pVer.major}.${pVer.minor}`;
|
|
194
|
+
const mmMatch = availableDocVersions.find(v => {
|
|
195
|
+
const c = v.replace(/^[v=]/, '');
|
|
196
|
+
return c === mmTarget || c === `${mmTarget}.x`;
|
|
197
|
+
});
|
|
198
|
+
if (mmMatch) {
|
|
199
|
+
return {
|
|
200
|
+
dependencyName,
|
|
201
|
+
projectVersion,
|
|
202
|
+
selectedDocVersion: mmMatch,
|
|
203
|
+
matchType: 'major_minor',
|
|
204
|
+
confidence: 0.95,
|
|
205
|
+
availableVersions: availableDocVersions,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 3. Major match (e.g. project '14.2.3' and doc 'v14', '14.x', or '14')
|
|
210
|
+
const majorTarget = `${pVer.major}`;
|
|
211
|
+
const majorMatch = availableDocVersions.find(v => {
|
|
212
|
+
const c = v.replace(/^[v=]/, '');
|
|
213
|
+
return c === majorTarget || c === `${majorTarget}.x` || c === `${majorTarget}.*`;
|
|
214
|
+
});
|
|
215
|
+
if (majorMatch) {
|
|
216
|
+
return {
|
|
217
|
+
dependencyName,
|
|
218
|
+
projectVersion,
|
|
219
|
+
selectedDocVersion: majorMatch,
|
|
220
|
+
matchType: 'major',
|
|
221
|
+
confidence: 0.90,
|
|
222
|
+
availableVersions: availableDocVersions,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 4. SemVer range satisfaction (find doc version that satisfies project version range or vice versa)
|
|
227
|
+
const rangeMatch = availableDocVersions
|
|
228
|
+
.filter(v => !['latest', 'stable', 'current', 'main', 'default'].includes(v.toLowerCase()))
|
|
229
|
+
.find(v => {
|
|
230
|
+
return satisfiesRange(projectVersion, v) || satisfiesRange(v, projectVersion);
|
|
231
|
+
});
|
|
232
|
+
if (rangeMatch) {
|
|
233
|
+
return {
|
|
234
|
+
dependencyName,
|
|
235
|
+
projectVersion,
|
|
236
|
+
selectedDocVersion: rangeMatch,
|
|
237
|
+
matchType: 'range',
|
|
238
|
+
confidence: 0.85,
|
|
239
|
+
availableVersions: availableDocVersions,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 5. Fallback: 'latest' / 'stable' / 'current' or highest available version
|
|
245
|
+
const latestCandidate = availableDocVersions.find(v =>
|
|
246
|
+
['latest', 'stable', 'current', 'main', 'default'].includes(v.toLowerCase())
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const fallbackVersion = latestCandidate || availableDocVersions[0];
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
dependencyName,
|
|
253
|
+
projectVersion,
|
|
254
|
+
selectedDocVersion: fallbackVersion,
|
|
255
|
+
matchType: 'latest_fallback',
|
|
256
|
+
confidence: 0.50,
|
|
257
|
+
availableVersions: availableDocVersions,
|
|
258
|
+
};
|
|
259
|
+
}
|