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,102 @@
|
|
|
1
|
+
export interface JsonRpcRequest {
|
|
2
|
+
jsonrpc: '2.0';
|
|
3
|
+
id?: string | number | null;
|
|
4
|
+
method: string;
|
|
5
|
+
params?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface JsonRpcResponse {
|
|
9
|
+
jsonrpc: '2.0';
|
|
10
|
+
id: string | number | null;
|
|
11
|
+
result?: unknown;
|
|
12
|
+
error?: JsonRpcErrorObject;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface JsonRpcNotification {
|
|
16
|
+
jsonrpc: '2.0';
|
|
17
|
+
method: string;
|
|
18
|
+
params?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JsonRpcErrorObject {
|
|
22
|
+
code: number;
|
|
23
|
+
message: string;
|
|
24
|
+
data?: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const JSONRPC_ERRORS = {
|
|
28
|
+
PARSE_ERROR: -32700,
|
|
29
|
+
INVALID_REQUEST: -32600,
|
|
30
|
+
METHOD_NOT_FOUND: -32601,
|
|
31
|
+
INVALID_PARAMS: -32602,
|
|
32
|
+
INTERNAL_ERROR: -32603,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
export interface ToolInputSchema {
|
|
36
|
+
type: 'object';
|
|
37
|
+
properties: Record<string, {
|
|
38
|
+
type: string;
|
|
39
|
+
description: string;
|
|
40
|
+
items?: Record<string, unknown>;
|
|
41
|
+
enum?: string[];
|
|
42
|
+
default?: unknown;
|
|
43
|
+
}>;
|
|
44
|
+
required?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface McpTool {
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
inputSchema: ToolInputSchema;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ToolContentItem {
|
|
54
|
+
type: 'text';
|
|
55
|
+
text: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CallToolResult {
|
|
59
|
+
content: ToolContentItem[];
|
|
60
|
+
isError?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface McpResource {
|
|
64
|
+
uri: string;
|
|
65
|
+
name: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
mimeType?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ResourceContent {
|
|
71
|
+
uri: string;
|
|
72
|
+
mimeType?: string;
|
|
73
|
+
text?: string;
|
|
74
|
+
blob?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ReadResourceResult {
|
|
78
|
+
contents: ResourceContent[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ServerCapabilities {
|
|
82
|
+
tools?: {
|
|
83
|
+
listChanged?: boolean;
|
|
84
|
+
};
|
|
85
|
+
resources?: {
|
|
86
|
+
subscribe?: boolean;
|
|
87
|
+
listChanged?: boolean;
|
|
88
|
+
};
|
|
89
|
+
prompts?: {
|
|
90
|
+
listChanged?: boolean;
|
|
91
|
+
};
|
|
92
|
+
logging?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface InitializeResult {
|
|
96
|
+
protocolVersion: string;
|
|
97
|
+
serverInfo: {
|
|
98
|
+
name: string;
|
|
99
|
+
version: string;
|
|
100
|
+
};
|
|
101
|
+
capabilities: ServerCapabilities;
|
|
102
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
NormalizedPage,
|
|
4
|
+
DocumentChunk,
|
|
5
|
+
IndexedExample,
|
|
6
|
+
SourceAuthority,
|
|
7
|
+
Provenance,
|
|
8
|
+
} from '../../shared/src/index.ts';
|
|
9
|
+
|
|
10
|
+
const FRAMEWORK_PATTERNS: Array<{ framework: string; pattern: RegExp }> = [
|
|
11
|
+
{ framework: 'next', pattern: /(?:from\s+['"]next(?:\/.*)?['"]|require\(['"]next(?:\/.*)?['"]\)|'use client'|'use server')/i },
|
|
12
|
+
{ framework: 'react', pattern: /(?:from\s+['"]react['"]|require\(['"]react['"]\)|useState|useEffect|useMemo|useCallback)/ },
|
|
13
|
+
{ framework: 'express', pattern: /(?:from\s+['"]express['"]|require\(['"]express['"]\)|express\(\))/i },
|
|
14
|
+
{ framework: 'fastify', pattern: /(?:from\s+['"]fastify['"]|require\(['"]fastify['"]\))/i },
|
|
15
|
+
{ framework: 'hono', pattern: /(?:from\s+['"]hono['"]|require\(['"]hono['"]\))/i },
|
|
16
|
+
{ framework: 'vue', pattern: /(?:from\s+['"]vue['"]|createApp\(|defineComponent)/i },
|
|
17
|
+
{ framework: 'svelte', pattern: /(?:from\s+['"]svelte['"]|<script.*lang=["']ts["']>)/i },
|
|
18
|
+
{ framework: 'fastapi', pattern: /(?:from\s+fastapi\s+import|FastAPI\()/i },
|
|
19
|
+
{ framework: 'flask', pattern: /(?:from\s+flask\s+import|Flask\(__name__\))/i },
|
|
20
|
+
{ framework: 'django', pattern: /(?:from\s+django|django\.)/i },
|
|
21
|
+
{ framework: 'spring', pattern: /(?:@SpringBootApplication|@RestController|@GetMapping|@PostMapping)/i },
|
|
22
|
+
{ framework: 'gin', pattern: /(?:github\.com\/gin-gonic\/gin|gin\.Default\(\))/i },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const API_METHOD_PATH_REGEX = /\b(GET|POST|PUT|DELETE|PATCH)\s+([/\w\-_{}]+)/i;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detects framework from code snippet content.
|
|
29
|
+
*/
|
|
30
|
+
function detectFramework(code: string): string | undefined {
|
|
31
|
+
for (const { framework, pattern } of FRAMEWORK_PATTERNS) {
|
|
32
|
+
if (pattern.test(code)) {
|
|
33
|
+
return framework;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detects an API endpoint reference from code or task context.
|
|
41
|
+
*/
|
|
42
|
+
function detectRelatedApi(code: string, context: string): string | undefined {
|
|
43
|
+
// Check context first (e.g. "POST /v1/webhook_endpoints")
|
|
44
|
+
const contextMatch = context.match(API_METHOD_PATH_REGEX);
|
|
45
|
+
if (contextMatch) {
|
|
46
|
+
return `${contextMatch[1].toUpperCase()} ${contextMatch[2]}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check code
|
|
50
|
+
const codeMatch = code.match(API_METHOD_PATH_REGEX);
|
|
51
|
+
if (codeMatch) {
|
|
52
|
+
return `${codeMatch[1].toUpperCase()} ${codeMatch[2]}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Common client library calls (e.g. fetch('/api/v1/foo', { method: 'POST' }))
|
|
56
|
+
const fetchMatch = code.match(/fetch\(\s*['"`]([/\w\-_{}]+)['"`](?:,\s*\{[^}]*method:\s*['"](\w+)['"])?/i);
|
|
57
|
+
if (fetchMatch) {
|
|
58
|
+
const method = (fetchMatch[2] || 'GET').toUpperCase();
|
|
59
|
+
return `${method} ${fetchMatch[1]}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Framework router calls: app.post('/api/webhook', ...) or router.get('/users', ...) or @app.post('/webhook')
|
|
63
|
+
const routeMatch = code.match(/(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([/\w\-_{}]+)['"`]/i);
|
|
64
|
+
if (routeMatch) {
|
|
65
|
+
return `${routeMatch[1].toUpperCase()} ${routeMatch[2]}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Python decorators: @app.post("/webhook")
|
|
69
|
+
const pyRouteMatch = code.match(/@(?:app|router)\.(get|post|put|delete|patch)\(\s*['"`]([/\w\-_{}]+)['"`]/i);
|
|
70
|
+
if (pyRouteMatch) {
|
|
71
|
+
return `${pyRouteMatch[1].toUpperCase()} ${pyRouteMatch[2]}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detects a primary symbol or function call from code.
|
|
79
|
+
*/
|
|
80
|
+
function detectRelatedSymbol(code: string): string | undefined {
|
|
81
|
+
// Check function declarations
|
|
82
|
+
const funcMatch = code.match(/(?:function\s+([a-zA-Z0-9_$]+)|const\s+([a-zA-Z0-9_$]+)\s*=\s*(?:async\s*)?\()/);
|
|
83
|
+
if (funcMatch) {
|
|
84
|
+
return funcMatch[1] || funcMatch[2];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check class declarations
|
|
88
|
+
const classMatch = code.match(/class\s+([a-zA-Z0-9_$]+)/);
|
|
89
|
+
if (classMatch) {
|
|
90
|
+
return classMatch[1];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check prominent method invocations, ignoring routing boilerplate like app.post or res.json
|
|
94
|
+
const boilerplateCalls = new Set([
|
|
95
|
+
'app.get', 'app.post', 'app.put', 'app.delete', 'app.use',
|
|
96
|
+
'router.get', 'router.post', 'router.use',
|
|
97
|
+
'express.raw', 'express.json', 'express.urlencoded',
|
|
98
|
+
'res.json', 'res.send', 'res.status', 'console.log', 'console.error',
|
|
99
|
+
]);
|
|
100
|
+
const allCalls = Array.from(code.matchAll(/([a-zA-Z0-9_$]+(?:\.[a-zA-Z0-9_$]+)+)\s*\(/g));
|
|
101
|
+
for (const match of allCalls) {
|
|
102
|
+
if (!boilerplateCalls.has(match[1])) {
|
|
103
|
+
return match[1];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (allCalls.length > 0) {
|
|
107
|
+
return allCalls[0][1];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extracts and indexes first-class code examples from normalized pages and chunks.
|
|
115
|
+
*/
|
|
116
|
+
export function extractIndexedExamples(
|
|
117
|
+
page: NormalizedPage,
|
|
118
|
+
chunks: DocumentChunk[],
|
|
119
|
+
authority: SourceAuthority,
|
|
120
|
+
docVersion?: string,
|
|
121
|
+
snapshotId?: string
|
|
122
|
+
): IndexedExample[] {
|
|
123
|
+
const examples: IndexedExample[] = [];
|
|
124
|
+
const seenCodeHashes = new Set<string>();
|
|
125
|
+
const nowIso = new Date().toISOString();
|
|
126
|
+
|
|
127
|
+
// Map each code example in the page to its enclosing chunk
|
|
128
|
+
for (const codeExample of page.codeExamples) {
|
|
129
|
+
const rawCode = codeExample.code.trim();
|
|
130
|
+
if (!rawCode || rawCode.length < 15) continue; // Skip trivial snippets
|
|
131
|
+
|
|
132
|
+
const codeHash = createHash('sha256').update(rawCode).digest('hex');
|
|
133
|
+
if (seenCodeHashes.has(codeHash)) continue;
|
|
134
|
+
seenCodeHashes.add(codeHash);
|
|
135
|
+
|
|
136
|
+
// Find the chunk that contains this code snippet
|
|
137
|
+
const matchingChunk = chunks.find(c =>
|
|
138
|
+
c.chunkType === 'code' && c.content.includes(rawCode)
|
|
139
|
+
) || chunks.find(c => c.content.includes(rawCode));
|
|
140
|
+
|
|
141
|
+
// Determine task context from breadcrumbs or headings
|
|
142
|
+
let task = '';
|
|
143
|
+
if (matchingChunk && matchingChunk.sectionPath.length > 0) {
|
|
144
|
+
task = matchingChunk.sectionPath.join(' > ');
|
|
145
|
+
} else if (matchingChunk && matchingChunk.title) {
|
|
146
|
+
task = matchingChunk.title;
|
|
147
|
+
} else {
|
|
148
|
+
task = page.title;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const language = (codeExample.language || (matchingChunk?.language) || 'typescript').toLowerCase();
|
|
152
|
+
const framework = detectFramework(rawCode);
|
|
153
|
+
const relatedApi = detectRelatedApi(rawCode, task);
|
|
154
|
+
const relatedSymbol = detectRelatedSymbol(rawCode);
|
|
155
|
+
|
|
156
|
+
const exampleId = createHash('sha256')
|
|
157
|
+
.update(`${page.id}:${codeHash}`)
|
|
158
|
+
.digest('hex')
|
|
159
|
+
.substring(0, 16);
|
|
160
|
+
|
|
161
|
+
const provenance: Provenance = {
|
|
162
|
+
sourceUrl: page.url,
|
|
163
|
+
retrievedAt: page.metadata?.extractedAt || nowIso,
|
|
164
|
+
sourceAuthority: authority,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const effectiveSnapshotId = snapshotId || matchingChunk?.snapshotId || (page as any).snapshotId || 'snap_default';
|
|
168
|
+
|
|
169
|
+
examples.push({
|
|
170
|
+
id: exampleId,
|
|
171
|
+
chunkId: matchingChunk?.id,
|
|
172
|
+
pageId: page.id,
|
|
173
|
+
snapshotId: effectiveSnapshotId,
|
|
174
|
+
language,
|
|
175
|
+
framework,
|
|
176
|
+
task,
|
|
177
|
+
code: rawCode,
|
|
178
|
+
sourceUrl: page.url,
|
|
179
|
+
sourceAuthority: authority,
|
|
180
|
+
relatedApi,
|
|
181
|
+
relatedSymbol,
|
|
182
|
+
docVersion: docVersion || page.docVersion,
|
|
183
|
+
provenance,
|
|
184
|
+
createdAt: nowIso,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Also check chunks of type 'code' that might not have been caught in page.codeExamples
|
|
189
|
+
for (const chunk of chunks) {
|
|
190
|
+
if (chunk.chunkType !== 'code') continue;
|
|
191
|
+
const rawCode = chunk.content.trim();
|
|
192
|
+
if (!rawCode || rawCode.length < 15) continue;
|
|
193
|
+
|
|
194
|
+
const codeHash = createHash('sha256').update(rawCode).digest('hex');
|
|
195
|
+
if (seenCodeHashes.has(codeHash)) continue;
|
|
196
|
+
seenCodeHashes.add(codeHash);
|
|
197
|
+
|
|
198
|
+
const task = chunk.sectionPath.length > 0
|
|
199
|
+
? chunk.sectionPath.join(' > ')
|
|
200
|
+
: (chunk.title || page.title);
|
|
201
|
+
|
|
202
|
+
const language = (chunk.language || 'typescript').toLowerCase();
|
|
203
|
+
const framework = detectFramework(rawCode);
|
|
204
|
+
const relatedApi = detectRelatedApi(rawCode, task);
|
|
205
|
+
const relatedSymbol = detectRelatedSymbol(rawCode);
|
|
206
|
+
|
|
207
|
+
const exampleId = createHash('sha256')
|
|
208
|
+
.update(`${page.id}:${codeHash}`)
|
|
209
|
+
.digest('hex')
|
|
210
|
+
.substring(0, 16);
|
|
211
|
+
|
|
212
|
+
const provenance: Provenance = {
|
|
213
|
+
sourceUrl: page.url,
|
|
214
|
+
retrievedAt: page.metadata?.extractedAt || nowIso,
|
|
215
|
+
sourceAuthority: authority,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const effectiveSnapshotId = snapshotId || chunk.snapshotId || (page as any).snapshotId || 'snap_default';
|
|
219
|
+
|
|
220
|
+
examples.push({
|
|
221
|
+
id: exampleId,
|
|
222
|
+
chunkId: chunk.id,
|
|
223
|
+
pageId: page.id,
|
|
224
|
+
snapshotId: effectiveSnapshotId,
|
|
225
|
+
language,
|
|
226
|
+
framework,
|
|
227
|
+
task,
|
|
228
|
+
code: rawCode,
|
|
229
|
+
sourceUrl: page.url,
|
|
230
|
+
sourceAuthority: authority,
|
|
231
|
+
relatedApi,
|
|
232
|
+
relatedSymbol,
|
|
233
|
+
docVersion: docVersion || chunk.docVersion || page.docVersion,
|
|
234
|
+
provenance,
|
|
235
|
+
createdAt: nowIso,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return examples;
|
|
240
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { Heading, Link, CodeExample } from '../../shared/src/index.ts';
|
|
2
|
+
|
|
3
|
+
export interface ExtractedHtmlDoc {
|
|
4
|
+
title: string;
|
|
5
|
+
markdown: string;
|
|
6
|
+
headings: Heading[];
|
|
7
|
+
links: Link[];
|
|
8
|
+
codeExamples: CodeExample[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function unescapeHtml(text: string): string {
|
|
12
|
+
return text
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, "'")
|
|
18
|
+
.replace(/'/g, "'")
|
|
19
|
+
.replace(/ /g, ' ')
|
|
20
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
|
|
21
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function slugify(text: string): string {
|
|
25
|
+
return text
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/[^\w\s-]/g, '')
|
|
29
|
+
.replace(/[\s_-]+/g, '-')
|
|
30
|
+
.replace(/^-+|-+$/g, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deterministically normalizes HTML documentation into clean Markdown,
|
|
35
|
+
* preserving code fences, language tags, headings, tables, callouts, and links.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeHtmlToMarkdown(html: string, baseUrl: string): ExtractedHtmlDoc {
|
|
38
|
+
const headings: Heading[] = [];
|
|
39
|
+
const links: Link[] = [];
|
|
40
|
+
const codeExamples: CodeExample[] = [];
|
|
41
|
+
|
|
42
|
+
let title = '';
|
|
43
|
+
|
|
44
|
+
// 1. Extract title from <title> tag if present
|
|
45
|
+
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
46
|
+
if (titleMatch) {
|
|
47
|
+
title = unescapeHtml(titleMatch[1]).replace(/\s+/g, ' ').trim();
|
|
48
|
+
title = title.split(/\s+[|–—-]\s+/)[0].trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Remove non-content / boilerplate tags
|
|
52
|
+
let cleaned = html
|
|
53
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
54
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
55
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
56
|
+
.replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, '')
|
|
57
|
+
.replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, '')
|
|
58
|
+
.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, '')
|
|
59
|
+
.replace(/<nav\b[^>]*>[\s\S]*?<\/nav>/gi, '')
|
|
60
|
+
.replace(/<header\b[^>]*>[\s\S]*?<\/header>/gi, '')
|
|
61
|
+
.replace(/<footer\b[^>]*>[\s\S]*?<\/footer>/gi, '')
|
|
62
|
+
.replace(/<div\b[^>]*(?:class|id)=["'][^"']*\b(?:sidebar|nav|menu|navbar|footer|cookie-banner|breadcrumbs)\b[^"']*["'][^>]*>[\s\S]*?<\/div>/gi, '');
|
|
63
|
+
|
|
64
|
+
// 3. Extract and preserve code blocks
|
|
65
|
+
const codeBlockPlaceholders: string[] = [];
|
|
66
|
+
cleaned = cleaned.replace(/<pre\b[^>]*>(?:<code\b([^>]*)>)?([\s\S]*?)(?:<\/code>)?<\/pre>/gi, (_, codeAttrs, codeBody) => {
|
|
67
|
+
let lang = '';
|
|
68
|
+
if (codeAttrs) {
|
|
69
|
+
const langMatch = codeAttrs.match(/\b(?:class|data-language)=["'][^"']*\b(?:language-|lang-)?([a-zA-Z0-9_-]+)\b[^"']*["']/i);
|
|
70
|
+
if (langMatch && !['hljs', 'code'].includes(langMatch[1].toLowerCase())) {
|
|
71
|
+
lang = langMatch[1].toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rawCode = unescapeHtml(codeBody.replace(/<[^>]+>/g, '')).trim();
|
|
76
|
+
if (!rawCode) return '';
|
|
77
|
+
|
|
78
|
+
const exampleId = `code_${codeExamples.length + 1}`;
|
|
79
|
+
codeExamples.push({
|
|
80
|
+
id: exampleId,
|
|
81
|
+
language: lang || 'text',
|
|
82
|
+
code: rawCode,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const placeholder = `__DOCORBIT_CODE_BLOCK_${codeBlockPlaceholders.length}__`;
|
|
86
|
+
codeBlockPlaceholders.push(`\n\`\`\`${lang}\n${rawCode}\n\`\`\`\n`);
|
|
87
|
+
return placeholder;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 4. Extract Headings and build anchors
|
|
91
|
+
cleaned = cleaned.replace(/<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi, (_, levelStr, attrs, innerText) => {
|
|
92
|
+
const level = parseInt(levelStr, 10);
|
|
93
|
+
const plainText = unescapeHtml(innerText.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
|
|
94
|
+
if (!plainText) return '';
|
|
95
|
+
|
|
96
|
+
let anchor = '';
|
|
97
|
+
const idMatch = attrs.match(/\bid=["']([^"']+)["']/i);
|
|
98
|
+
if (idMatch) {
|
|
99
|
+
anchor = idMatch[1];
|
|
100
|
+
} else {
|
|
101
|
+
anchor = slugify(plainText);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
headings.push({ level, text: plainText, anchor });
|
|
105
|
+
const hashes = '#'.repeat(level);
|
|
106
|
+
return `\n\n${hashes} ${plainText}\n\n`;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Prefer first H1 as title, otherwise fallback to HTML title, then first heading
|
|
110
|
+
const h1 = headings.find(h => h.level === 1);
|
|
111
|
+
if (h1) {
|
|
112
|
+
title = h1.text;
|
|
113
|
+
} else if (!title && headings.length > 0) {
|
|
114
|
+
title = headings[0].text;
|
|
115
|
+
}
|
|
116
|
+
if (!title) {
|
|
117
|
+
title = 'Documentation Page';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 5. Extract and normalize Links
|
|
121
|
+
cleaned = cleaned.replace(/<a\b[^>]*\bhref=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, linkText) => {
|
|
122
|
+
const text = unescapeHtml(linkText.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
|
|
123
|
+
if (!text || href.startsWith('#') || href.startsWith('javascript:')) return text;
|
|
124
|
+
|
|
125
|
+
let absoluteUrl = href;
|
|
126
|
+
let isExternal = false;
|
|
127
|
+
try {
|
|
128
|
+
const resolved = new URL(href, baseUrl);
|
|
129
|
+
absoluteUrl = resolved.href;
|
|
130
|
+
const baseHost = new URL(baseUrl).hostname;
|
|
131
|
+
isExternal = resolved.hostname !== baseHost;
|
|
132
|
+
} catch {
|
|
133
|
+
// Keep raw href
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
links.push({
|
|
137
|
+
text,
|
|
138
|
+
url: absoluteUrl,
|
|
139
|
+
isExternal,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return `[${text}](${absoluteUrl})`;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 6. Handle Callouts & Blockquotes
|
|
146
|
+
cleaned = cleaned.replace(/<blockquote\b[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, inner) => {
|
|
147
|
+
const blockText = inner
|
|
148
|
+
.replace(/<p\b[^>]*>/gi, '')
|
|
149
|
+
.replace(/<\/p>/gi, '\n')
|
|
150
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
151
|
+
.replace(/<[^>]+>/g, '');
|
|
152
|
+
const cleanLines = unescapeHtml(blockText)
|
|
153
|
+
.split('\n')
|
|
154
|
+
.map(l => l.trim())
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.map(l => `> ${l}`)
|
|
157
|
+
.join('\n');
|
|
158
|
+
return `\n\n${cleanLines}\n\n`;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
cleaned = cleaned.replace(/<div\b[^>]*\bclass=["'][^"']*\b(?:alert|callout|admonition|tip|warning|note|info)\b[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, inner) => {
|
|
162
|
+
const alertText = unescapeHtml(inner.replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim();
|
|
163
|
+
if (!alertText) return '';
|
|
164
|
+
return `\n\n> [!NOTE]\n> ${alertText}\n\n`;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// 7. Handle Tables
|
|
168
|
+
cleaned = cleaned.replace(/<table\b[^>]*>([\s\S]*?)<\/table>/gi, (_, tableContent) => {
|
|
169
|
+
const rows: string[][] = [];
|
|
170
|
+
const rowMatches = tableContent.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi);
|
|
171
|
+
|
|
172
|
+
for (const rMatch of rowMatches) {
|
|
173
|
+
const rowContent = rMatch[1];
|
|
174
|
+
const cells: string[] = [];
|
|
175
|
+
const cellMatches = rowContent.matchAll(/<t[hd]\b[^>]*>([\s\S]*?)<\/t[hd]>/gi);
|
|
176
|
+
for (const cMatch of cellMatches) {
|
|
177
|
+
const cellText = unescapeHtml(cMatch[1].replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
|
|
178
|
+
cells.push(cellText);
|
|
179
|
+
}
|
|
180
|
+
if (cells.length > 0) {
|
|
181
|
+
rows.push(cells);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (rows.length === 0) return '';
|
|
186
|
+
|
|
187
|
+
const colCount = Math.max(...rows.map(r => r.length));
|
|
188
|
+
const headerRow = rows[0].map(c => c || '-');
|
|
189
|
+
while (headerRow.length < colCount) headerRow.push('-');
|
|
190
|
+
|
|
191
|
+
const delimiterRow = Array(colCount).fill('---');
|
|
192
|
+
const tableLines = [
|
|
193
|
+
`| ${headerRow.join(' | ')} |`,
|
|
194
|
+
`| ${delimiterRow.join(' | ')} |`,
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
for (let i = 1; i < rows.length; i++) {
|
|
198
|
+
const dataRow = rows[i].map(c => c || '');
|
|
199
|
+
while (dataRow.length < colCount) dataRow.push('');
|
|
200
|
+
tableLines.push(`| ${dataRow.join(' | ')} |`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return `\n\n${tableLines.join('\n')}\n\n`;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 8. Handle Lists
|
|
207
|
+
cleaned = cleaned
|
|
208
|
+
.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_, inner) => {
|
|
209
|
+
const text = unescapeHtml(inner.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
|
|
210
|
+
return `\n- ${text}`;
|
|
211
|
+
})
|
|
212
|
+
.replace(/<\/?(?:ul|ol)\b[^>]*>/gi, '\n');
|
|
213
|
+
|
|
214
|
+
// 9. Handle Inline elements
|
|
215
|
+
cleaned = cleaned
|
|
216
|
+
.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_, inner) => {
|
|
217
|
+
const text = unescapeHtml(inner.replace(/<[^>]+>/g, '')).trim();
|
|
218
|
+
return text ? `\`${text}\`` : '';
|
|
219
|
+
})
|
|
220
|
+
.replace(/<(?:strong|b)\b[^>]*>([\s\S]*?)<\/(?:strong|b)>/gi, (_, inner) => {
|
|
221
|
+
const text = inner.trim();
|
|
222
|
+
return text ? `**${text}**` : '';
|
|
223
|
+
})
|
|
224
|
+
.replace(/<(?:em|i)\b[^>]*>([\s\S]*?)<\/(?:em|i)>/gi, (_, inner) => {
|
|
225
|
+
const text = inner.trim();
|
|
226
|
+
return text ? `*${text}*` : '';
|
|
227
|
+
})
|
|
228
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
229
|
+
.replace(/<\/?(?:p|div|section|article|main)\b[^>]*>/gi, '\n\n');
|
|
230
|
+
|
|
231
|
+
// 10. Strip all remaining HTML tags
|
|
232
|
+
cleaned = cleaned.replace(/<[^>]+>/g, '');
|
|
233
|
+
|
|
234
|
+
// 11. Restore preserved code blocks
|
|
235
|
+
for (let i = 0; i < codeBlockPlaceholders.length; i++) {
|
|
236
|
+
cleaned = cleaned.replace(`__DOCORBIT_CODE_BLOCK_${i}__`, codeBlockPlaceholders[i]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 12. Normalize whitespace
|
|
240
|
+
cleaned = unescapeHtml(cleaned)
|
|
241
|
+
.replace(/[ \t]+/g, ' ')
|
|
242
|
+
.replace(/\n\s+\n/g, '\n\n')
|
|
243
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
244
|
+
.trim();
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
title,
|
|
248
|
+
markdown: cleaned,
|
|
249
|
+
headings,
|
|
250
|
+
links,
|
|
251
|
+
codeExamples,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { LlmsDocument, LlmsSection, LlmsLink } from '../../shared/src/index.ts';
|
|
2
|
+
|
|
3
|
+
export function parseLlmsTxt(content: string, baseUrl: string): LlmsDocument {
|
|
4
|
+
const lines = content.split('\n');
|
|
5
|
+
let title: string | undefined;
|
|
6
|
+
let summary: string | undefined;
|
|
7
|
+
const sections: LlmsSection[] = [];
|
|
8
|
+
let currentSection: LlmsSection = { name: 'Overview', links: [] };
|
|
9
|
+
|
|
10
|
+
const summaryLines: string[] = [];
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
const rawLine = lines[i];
|
|
14
|
+
const line = rawLine.trim();
|
|
15
|
+
|
|
16
|
+
if (!line) continue;
|
|
17
|
+
|
|
18
|
+
if (line.startsWith('# ') && !title) {
|
|
19
|
+
title = line.slice(2).trim();
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (line.startsWith('>')) {
|
|
24
|
+
const text = line.replace(/^>\s*/, '').trim();
|
|
25
|
+
if (text) summaryLines.push(text);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (line.startsWith('## ')) {
|
|
30
|
+
if (currentSection.links.length > 0 || currentSection.name !== 'Overview') {
|
|
31
|
+
sections.push(currentSection);
|
|
32
|
+
}
|
|
33
|
+
currentSection = {
|
|
34
|
+
name: line.slice(3).trim(),
|
|
35
|
+
links: [],
|
|
36
|
+
};
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const linkMatch = line.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)(?::\s*(.*))?$/);
|
|
41
|
+
if (linkMatch) {
|
|
42
|
+
const linkTitle = linkMatch[1].trim();
|
|
43
|
+
let rawHref = linkMatch[2].trim();
|
|
44
|
+
const description = linkMatch[3] ? linkMatch[3].trim() : undefined;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
rawHref = new URL(rawHref, baseUrl).href;
|
|
48
|
+
} catch {
|
|
49
|
+
// Keep raw
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const linkItem: LlmsLink = {
|
|
53
|
+
title: linkTitle,
|
|
54
|
+
url: rawHref,
|
|
55
|
+
...(description ? { description } : {}),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
currentSection.links.push(linkItem);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (currentSection.links.length > 0 || sections.length === 0) {
|
|
63
|
+
sections.push(currentSection);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (summaryLines.length > 0) {
|
|
67
|
+
summary = summaryLines.join(' ');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
title,
|
|
72
|
+
summary,
|
|
73
|
+
sections,
|
|
74
|
+
rawContent: content,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isValidLlmsTxt(content: string): boolean {
|
|
79
|
+
if (!content || typeof content !== 'string') return false;
|
|
80
|
+
const hasH1 = /^#\s+.+/m.test(content);
|
|
81
|
+
const hasLink = /^[-*]\s+\[.+\]\(.+\)/m.test(content);
|
|
82
|
+
return hasH1 && hasLink;
|
|
83
|
+
}
|