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,316 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentChunk,
|
|
3
|
+
ChunkRelationship,
|
|
4
|
+
ChunkCode,
|
|
5
|
+
SymbolReference,
|
|
6
|
+
ChunkType,
|
|
7
|
+
Provenance,
|
|
8
|
+
} from '../../../shared/src/index.ts';
|
|
9
|
+
import type { DocOrbitDb } from '../db.ts';
|
|
10
|
+
import type { IChunkRepository } from '../interfaces.ts';
|
|
11
|
+
|
|
12
|
+
export class ChunkRepository implements IChunkRepository {
|
|
13
|
+
private db: DocOrbitDb;
|
|
14
|
+
|
|
15
|
+
constructor(db: DocOrbitDb) {
|
|
16
|
+
this.db = db;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Saves chunks, code snippets, symbols, and relationships in a single atomic transaction.
|
|
21
|
+
* Transactionally synchronizes the FTS5 chunks_fts table with chunks.
|
|
22
|
+
*/
|
|
23
|
+
saveChunks(
|
|
24
|
+
chunks: DocumentChunk[],
|
|
25
|
+
relationships: ChunkRelationship[] = [],
|
|
26
|
+
codeSnippets: ChunkCode[] = [],
|
|
27
|
+
symbols: SymbolReference[] = []
|
|
28
|
+
): void {
|
|
29
|
+
if (chunks.length === 0) return;
|
|
30
|
+
|
|
31
|
+
const raw = this.db.getRawDb();
|
|
32
|
+
const createdAt = new Date().toISOString();
|
|
33
|
+
|
|
34
|
+
// Group symbols by chunkId for fast indexing into chunks_fts
|
|
35
|
+
const symbolsByChunk = new Map<string, string[]>();
|
|
36
|
+
for (const s of symbols) {
|
|
37
|
+
const list = symbolsByChunk.get(s.chunkId) || [];
|
|
38
|
+
list.push(s.name);
|
|
39
|
+
symbolsByChunk.set(s.chunkId, list);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
raw.exec('BEGIN IMMEDIATE TRANSACTION;');
|
|
43
|
+
try {
|
|
44
|
+
const chunkStmt = raw.prepare(`
|
|
45
|
+
INSERT INTO chunks (
|
|
46
|
+
id, page_id, snapshot_id, title, section_path_json, content,
|
|
47
|
+
chunk_type, language, token_estimate, ordinal, content_hash,
|
|
48
|
+
doc_version, provenance_json, created_at
|
|
49
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
50
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
51
|
+
title = excluded.title,
|
|
52
|
+
section_path_json = excluded.section_path_json,
|
|
53
|
+
content = excluded.content,
|
|
54
|
+
chunk_type = excluded.chunk_type,
|
|
55
|
+
language = excluded.language,
|
|
56
|
+
token_estimate = excluded.token_estimate,
|
|
57
|
+
ordinal = excluded.ordinal,
|
|
58
|
+
content_hash = excluded.content_hash,
|
|
59
|
+
doc_version = excluded.doc_version,
|
|
60
|
+
provenance_json = excluded.provenance_json
|
|
61
|
+
`);
|
|
62
|
+
|
|
63
|
+
const ftsDelStmt = this.db.isFtsAvailable()
|
|
64
|
+
? raw.prepare('DELETE FROM chunks_fts WHERE chunk_id = ?')
|
|
65
|
+
: null;
|
|
66
|
+
|
|
67
|
+
const ftsInsStmt = this.db.isFtsAvailable()
|
|
68
|
+
? raw.prepare(`
|
|
69
|
+
INSERT INTO chunks_fts (chunk_id, title, section_path, content, symbols)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?)
|
|
71
|
+
`)
|
|
72
|
+
: null;
|
|
73
|
+
|
|
74
|
+
for (const chunk of chunks) {
|
|
75
|
+
const sectionPath = Array.isArray(chunk.sectionPath)
|
|
76
|
+
? chunk.sectionPath
|
|
77
|
+
: (Array.isArray((chunk as any).headingPath) ? (chunk as any).headingPath : []);
|
|
78
|
+
const chunkType = chunk.chunkType || (chunk as any).type || 'prose';
|
|
79
|
+
const tokenEstimate = chunk.tokenEstimate ?? (chunk as any).tokenCount ?? 0;
|
|
80
|
+
const ordinal = chunk.ordinal ?? (chunk as any).orderIndex ?? 0;
|
|
81
|
+
const provenance = chunk.provenance ? JSON.stringify(chunk.provenance) : null;
|
|
82
|
+
|
|
83
|
+
chunkStmt.run(
|
|
84
|
+
chunk.id,
|
|
85
|
+
chunk.pageId || null,
|
|
86
|
+
chunk.snapshotId || 'default',
|
|
87
|
+
chunk.title || null,
|
|
88
|
+
JSON.stringify(sectionPath),
|
|
89
|
+
chunk.content,
|
|
90
|
+
chunkType,
|
|
91
|
+
chunk.language || null,
|
|
92
|
+
tokenEstimate,
|
|
93
|
+
ordinal,
|
|
94
|
+
chunk.contentHash,
|
|
95
|
+
chunk.docVersion || null,
|
|
96
|
+
provenance,
|
|
97
|
+
createdAt
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (ftsDelStmt && ftsInsStmt) {
|
|
101
|
+
ftsDelStmt.run(chunk.id);
|
|
102
|
+
const chunkSymbols = symbolsByChunk.get(chunk.id)?.join(' ') || '';
|
|
103
|
+
const sectionPathStr = sectionPath.join(' > ');
|
|
104
|
+
ftsInsStmt.run(chunk.id, chunk.title || '', sectionPathStr, chunk.content, chunkSymbols);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Save code snippets
|
|
109
|
+
if (codeSnippets.length > 0) {
|
|
110
|
+
const codeStmt = raw.prepare(`
|
|
111
|
+
INSERT OR REPLACE INTO chunk_code (id, chunk_id, language, code)
|
|
112
|
+
VALUES (?, ?, ?, ?)
|
|
113
|
+
`);
|
|
114
|
+
for (const cs of codeSnippets) {
|
|
115
|
+
codeStmt.run(cs.id, cs.chunkId, cs.language || null, cs.code);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Save symbols
|
|
120
|
+
if (symbols.length > 0) {
|
|
121
|
+
const symStmt = raw.prepare(`
|
|
122
|
+
INSERT OR REPLACE INTO symbol_references (id, chunk_id, name, kind)
|
|
123
|
+
VALUES (?, ?, ?, ?)
|
|
124
|
+
`);
|
|
125
|
+
for (const s of symbols) {
|
|
126
|
+
symStmt.run(s.id, s.chunkId, s.name, s.kind);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Save relationships
|
|
131
|
+
if (relationships.length > 0) {
|
|
132
|
+
const relStmt = raw.prepare(`
|
|
133
|
+
INSERT OR REPLACE INTO chunk_relationships (source_chunk_id, target_chunk_id, type)
|
|
134
|
+
VALUES (?, ?, ?)
|
|
135
|
+
`);
|
|
136
|
+
for (const r of relationships) {
|
|
137
|
+
relStmt.run(r.sourceChunkId, r.targetChunkId, r.type);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
raw.exec('COMMIT;');
|
|
142
|
+
} catch (err) {
|
|
143
|
+
raw.exec('ROLLBACK;');
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getChunk(id: string): DocumentChunk | null {
|
|
149
|
+
const raw = this.db.getRawDb();
|
|
150
|
+
const row = raw.prepare('SELECT * FROM chunks WHERE id = ?').get(id) as Record<string, unknown> | undefined;
|
|
151
|
+
if (!row) return null;
|
|
152
|
+
return this.hydrateChunk(row);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getChunksByPage(pageId: string): DocumentChunk[] {
|
|
156
|
+
const raw = this.db.getRawDb();
|
|
157
|
+
const rows = raw.prepare('SELECT * FROM chunks WHERE page_id = ? ORDER BY ordinal ASC').all(pageId) as Array<Record<string, unknown>>;
|
|
158
|
+
return rows.map(r => this.hydrateChunk(r));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getChunksBySnapshot(snapshotId: string): DocumentChunk[] {
|
|
162
|
+
const raw = this.db.getRawDb();
|
|
163
|
+
const rows = raw.prepare('SELECT * FROM chunks WHERE snapshot_id = ? ORDER BY page_id ASC, ordinal ASC').all(snapshotId) as Array<Record<string, unknown>>;
|
|
164
|
+
return rows.map(r => this.hydrateChunk(r));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getChunksByVersion(docVersion: string): DocumentChunk[] {
|
|
168
|
+
const raw = this.db.getRawDb();
|
|
169
|
+
const rows = raw.prepare('SELECT * FROM chunks WHERE doc_version = ? ORDER BY page_id ASC, ordinal ASC').all(docVersion) as Array<Record<string, unknown>>;
|
|
170
|
+
return rows.map(r => this.hydrateChunk(r));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getChunkCode(chunkId: string): ChunkCode[] {
|
|
174
|
+
const raw = this.db.getRawDb();
|
|
175
|
+
const rows = raw.prepare('SELECT id, chunk_id, language, code FROM chunk_code WHERE chunk_id = ?').all(chunkId) as Array<Record<string, unknown>>;
|
|
176
|
+
return rows.map(r => ({
|
|
177
|
+
id: String(r.id),
|
|
178
|
+
chunkId: String(r.chunk_id),
|
|
179
|
+
language: r.language ? String(r.language) : undefined,
|
|
180
|
+
code: String(r.code),
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getChunkSymbols(chunkId: string): SymbolReference[] {
|
|
185
|
+
const raw = this.db.getRawDb();
|
|
186
|
+
const rows = raw.prepare('SELECT id, chunk_id, name, kind FROM symbol_references WHERE chunk_id = ?').all(chunkId) as Array<Record<string, unknown>>;
|
|
187
|
+
return rows.map(r => ({
|
|
188
|
+
id: String(r.id),
|
|
189
|
+
chunkId: String(r.chunk_id),
|
|
190
|
+
name: String(r.name),
|
|
191
|
+
kind: r.kind as any,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
getChunkRelationships(chunkId: string): ChunkRelationship[] {
|
|
196
|
+
const raw = this.db.getRawDb();
|
|
197
|
+
const rows = raw.prepare(`
|
|
198
|
+
SELECT source_chunk_id, target_chunk_id, type
|
|
199
|
+
FROM chunk_relationships
|
|
200
|
+
WHERE source_chunk_id = ? OR target_chunk_id = ?
|
|
201
|
+
`).all(chunkId, chunkId) as Array<Record<string, unknown>>;
|
|
202
|
+
return rows.map(r => ({
|
|
203
|
+
sourceChunkId: String(r.source_chunk_id),
|
|
204
|
+
targetChunkId: String(r.target_chunk_id),
|
|
205
|
+
type: r.type as any,
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
countChunks(snapshotId?: string): number {
|
|
210
|
+
const raw = this.db.getRawDb();
|
|
211
|
+
if (snapshotId) {
|
|
212
|
+
const row = raw.prepare('SELECT COUNT(*) as count FROM chunks WHERE snapshot_id = ?').get(snapshotId) as { count: number };
|
|
213
|
+
return row?.count || 0;
|
|
214
|
+
}
|
|
215
|
+
const row = raw.prepare('SELECT COUNT(*) as count FROM chunks').get() as { count: number };
|
|
216
|
+
return row?.count || 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
searchChunksFts(
|
|
220
|
+
query: string,
|
|
221
|
+
options: { limit?: number; snapshotId?: string; chunkType?: ChunkType; docVersion?: string } = {}
|
|
222
|
+
): Array<{ chunk: DocumentChunk; ftsRank: number; symbols: SymbolReference[]; codeSnippets: ChunkCode[] }> {
|
|
223
|
+
if (!this.db.isFtsAvailable()) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const raw = this.db.getRawDb();
|
|
228
|
+
const limit = options.limit || 20;
|
|
229
|
+
|
|
230
|
+
// Sanitize query for FTS5: strip dangerous punctuation, quote bare words or phrases
|
|
231
|
+
const cleanTokens = query
|
|
232
|
+
.replace(/[^\w\s-]/g, ' ')
|
|
233
|
+
.trim()
|
|
234
|
+
.split(/\s+/)
|
|
235
|
+
.filter(t => t.length > 0);
|
|
236
|
+
|
|
237
|
+
if (cleanTokens.length === 0) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Use OR-joined quoted tokens or phrase search
|
|
242
|
+
const sanitizedFtsQuery = cleanTokens.map(t => `"${t.replace(/"/g, '""')}"`).join(' OR ');
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
let sql = `
|
|
246
|
+
SELECT c.*, fts.rank as fts_rank
|
|
247
|
+
FROM chunks c
|
|
248
|
+
JOIN chunks_fts fts ON c.id = fts.chunk_id
|
|
249
|
+
WHERE chunks_fts MATCH ?
|
|
250
|
+
`;
|
|
251
|
+
const params: unknown[] = [sanitizedFtsQuery];
|
|
252
|
+
|
|
253
|
+
if (options.snapshotId) {
|
|
254
|
+
sql += ' AND c.snapshot_id = ?';
|
|
255
|
+
params.push(options.snapshotId);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options.chunkType) {
|
|
259
|
+
sql += ' AND c.chunk_type = ?';
|
|
260
|
+
params.push(options.chunkType);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.docVersion) {
|
|
264
|
+
sql += ' AND c.doc_version = ?';
|
|
265
|
+
params.push(options.docVersion);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
sql += ' ORDER BY fts.rank LIMIT ?';
|
|
269
|
+
params.push(limit);
|
|
270
|
+
|
|
271
|
+
const rows = raw.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
272
|
+
|
|
273
|
+
return rows.map(r => {
|
|
274
|
+
const chunk = this.hydrateChunk(r);
|
|
275
|
+
return {
|
|
276
|
+
chunk,
|
|
277
|
+
ftsRank: Number(r.fts_rank),
|
|
278
|
+
symbols: this.getChunkSymbols(chunk.id),
|
|
279
|
+
codeSnippets: this.getChunkCode(chunk.id),
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private hydrateChunk(row: Record<string, unknown>): DocumentChunk {
|
|
288
|
+
const sectionPath: string[] = row.section_path_json ? JSON.parse(String(row.section_path_json)) : [];
|
|
289
|
+
const provenance: Provenance = row.provenance_json
|
|
290
|
+
? JSON.parse(String(row.provenance_json))
|
|
291
|
+
: {
|
|
292
|
+
sourceUrl: '',
|
|
293
|
+
targetUrl: '',
|
|
294
|
+
fetchedAt: String(row.created_at || ''),
|
|
295
|
+
discoveredBy: 'direct',
|
|
296
|
+
contentHash: String(row.content_hash || ''),
|
|
297
|
+
snapshotId: String(row.snapshot_id || ''),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
id: String(row.id),
|
|
302
|
+
pageId: String(row.page_id),
|
|
303
|
+
snapshotId: String(row.snapshot_id),
|
|
304
|
+
title: row.title ? String(row.title) : undefined,
|
|
305
|
+
sectionPath,
|
|
306
|
+
content: String(row.content),
|
|
307
|
+
chunkType: row.chunk_type as ChunkType,
|
|
308
|
+
language: row.language ? String(row.language) : undefined,
|
|
309
|
+
tokenEstimate: Number(row.token_estimate),
|
|
310
|
+
ordinal: Number(row.ordinal),
|
|
311
|
+
contentHash: String(row.content_hash),
|
|
312
|
+
docVersion: row.doc_version ? String(row.doc_version) : undefined,
|
|
313
|
+
provenance,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { IndexedExample } from '../../../shared/src/index.ts';
|
|
2
|
+
import type { DocOrbitDb } from '../db.ts';
|
|
3
|
+
import type { IExampleRepository } from '../interfaces.ts';
|
|
4
|
+
import { extractSearchTokens } from '../search-tokens.ts';
|
|
5
|
+
|
|
6
|
+
export class ExampleRepository implements IExampleRepository {
|
|
7
|
+
private db: DocOrbitDb;
|
|
8
|
+
|
|
9
|
+
constructor(db: DocOrbitDb) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
saveIndexedExamples(examples: IndexedExample[]): void {
|
|
14
|
+
if (examples.length === 0) return;
|
|
15
|
+
const raw = this.db.getRawDb();
|
|
16
|
+
|
|
17
|
+
const insertEx = raw.prepare(`
|
|
18
|
+
INSERT INTO indexed_examples (
|
|
19
|
+
id, chunk_id, page_id, snapshot_id, language, framework,
|
|
20
|
+
task, code, source_url, source_authority, related_api,
|
|
21
|
+
related_symbol, doc_version, provenance_json, created_at
|
|
22
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
23
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
24
|
+
chunk_id = excluded.chunk_id,
|
|
25
|
+
language = excluded.language,
|
|
26
|
+
framework = excluded.framework,
|
|
27
|
+
task = excluded.task,
|
|
28
|
+
code = excluded.code,
|
|
29
|
+
source_url = excluded.source_url,
|
|
30
|
+
source_authority = excluded.source_authority,
|
|
31
|
+
related_api = excluded.related_api,
|
|
32
|
+
related_symbol = excluded.related_symbol,
|
|
33
|
+
doc_version = excluded.doc_version,
|
|
34
|
+
provenance_json = excluded.provenance_json
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
const insertFts = this.db.isFtsAvailable()
|
|
38
|
+
? raw.prepare(`
|
|
39
|
+
INSERT INTO indexed_examples_fts (example_id, task, language, framework, code, related_api, related_symbol)
|
|
40
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
41
|
+
`)
|
|
42
|
+
: null;
|
|
43
|
+
|
|
44
|
+
const deleteFts = this.db.isFtsAvailable()
|
|
45
|
+
? raw.prepare('DELETE FROM indexed_examples_fts WHERE example_id = ?')
|
|
46
|
+
: null;
|
|
47
|
+
|
|
48
|
+
raw.exec('BEGIN TRANSACTION;');
|
|
49
|
+
try {
|
|
50
|
+
for (const ex of examples) {
|
|
51
|
+
insertEx.run(
|
|
52
|
+
ex.id,
|
|
53
|
+
ex.chunkId || null,
|
|
54
|
+
ex.pageId,
|
|
55
|
+
ex.snapshotId || 'snap_default',
|
|
56
|
+
ex.language,
|
|
57
|
+
ex.framework || null,
|
|
58
|
+
ex.task,
|
|
59
|
+
ex.code,
|
|
60
|
+
ex.sourceUrl,
|
|
61
|
+
ex.sourceAuthority,
|
|
62
|
+
ex.relatedApi || null,
|
|
63
|
+
ex.relatedSymbol || null,
|
|
64
|
+
ex.docVersion || null,
|
|
65
|
+
ex.provenance ? JSON.stringify(ex.provenance) : null,
|
|
66
|
+
ex.createdAt || new Date().toISOString()
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (insertFts && deleteFts) {
|
|
70
|
+
deleteFts.run(ex.id);
|
|
71
|
+
insertFts.run(
|
|
72
|
+
ex.id,
|
|
73
|
+
ex.task,
|
|
74
|
+
ex.language,
|
|
75
|
+
ex.framework || '',
|
|
76
|
+
ex.code,
|
|
77
|
+
ex.relatedApi || '',
|
|
78
|
+
ex.relatedSymbol || ''
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
raw.exec('COMMIT;');
|
|
83
|
+
} catch (err) {
|
|
84
|
+
raw.exec('ROLLBACK;');
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getIndexedExamplesBySnapshot(snapshotId: string): IndexedExample[] {
|
|
90
|
+
const raw = this.db.getRawDb();
|
|
91
|
+
const rows = raw.prepare('SELECT * FROM indexed_examples WHERE snapshot_id = ?').all(snapshotId) as Array<Record<string, unknown>>;
|
|
92
|
+
return rows.map(r => this.hydrateIndexedExample(r));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
searchIndexedExamples(
|
|
96
|
+
query: string,
|
|
97
|
+
options: { language?: string; framework?: string; docVersion?: string; limit?: number } = {}
|
|
98
|
+
): IndexedExample[] {
|
|
99
|
+
const raw = this.db.getRawDb();
|
|
100
|
+
const limit = options.limit || 10;
|
|
101
|
+
const term = query.trim();
|
|
102
|
+
|
|
103
|
+
let results: IndexedExample[] = [];
|
|
104
|
+
|
|
105
|
+
// FTS attempt
|
|
106
|
+
if (this.db.isFtsAvailable() && term.length > 0) {
|
|
107
|
+
const cleanTokens = extractSearchTokens(term);
|
|
108
|
+
|
|
109
|
+
if (cleanTokens.length > 0) {
|
|
110
|
+
const sanitizedFts = cleanTokens
|
|
111
|
+
.map(t => t.length >= 4 ? `"${t.replace(/"/g, '""')}"*` : `"${t.replace(/"/g, '""')}"`)
|
|
112
|
+
.join(' OR ');
|
|
113
|
+
try {
|
|
114
|
+
let sql = `
|
|
115
|
+
SELECT ex.*, fts.rank as fts_rank
|
|
116
|
+
FROM indexed_examples ex
|
|
117
|
+
JOIN indexed_examples_fts fts ON ex.id = fts.example_id
|
|
118
|
+
WHERE indexed_examples_fts MATCH ?
|
|
119
|
+
`;
|
|
120
|
+
const params: unknown[] = [sanitizedFts];
|
|
121
|
+
|
|
122
|
+
if (options.language) {
|
|
123
|
+
sql += ' AND lower(ex.language) = ?';
|
|
124
|
+
params.push(options.language.toLowerCase());
|
|
125
|
+
}
|
|
126
|
+
if (options.framework) {
|
|
127
|
+
sql += ' AND lower(ex.framework) = ?';
|
|
128
|
+
params.push(options.framework.toLowerCase());
|
|
129
|
+
}
|
|
130
|
+
if (options.docVersion) {
|
|
131
|
+
sql += ' AND (ex.doc_version = ? OR ex.doc_version IS NULL)';
|
|
132
|
+
params.push(options.docVersion);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
sql += " ORDER BY CASE WHEN ex.source_authority = 'official' THEN 0 ELSE 1 END, fts.rank LIMIT ?";
|
|
136
|
+
params.push(limit);
|
|
137
|
+
|
|
138
|
+
const rows = raw.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
139
|
+
results = rows.map(r => this.hydrateIndexedExample(r));
|
|
140
|
+
} catch {
|
|
141
|
+
// Fall through to LIKE
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (results.length === 0) {
|
|
147
|
+
// LIKE fallback
|
|
148
|
+
let sql = 'SELECT * FROM indexed_examples WHERE 1=1';
|
|
149
|
+
const params: unknown[] = [];
|
|
150
|
+
|
|
151
|
+
if (options.language) {
|
|
152
|
+
sql += ' AND lower(language) = ?';
|
|
153
|
+
params.push(options.language.toLowerCase());
|
|
154
|
+
}
|
|
155
|
+
if (options.framework) {
|
|
156
|
+
sql += ' AND lower(framework) = ?';
|
|
157
|
+
params.push(options.framework.toLowerCase());
|
|
158
|
+
}
|
|
159
|
+
if (options.docVersion) {
|
|
160
|
+
sql += ' AND (doc_version = ? OR doc_version IS NULL)';
|
|
161
|
+
params.push(options.docVersion);
|
|
162
|
+
}
|
|
163
|
+
if (term.length > 0) {
|
|
164
|
+
const tokens = extractSearchTokens(term);
|
|
165
|
+
if (tokens.length > 0) {
|
|
166
|
+
const tokenClauses = tokens.map(() => '(task LIKE ? OR code LIKE ? OR related_api LIKE ? OR related_symbol LIKE ?)');
|
|
167
|
+
sql += ` AND (${tokenClauses.join(' OR ')})`;
|
|
168
|
+
for (const t of tokens) {
|
|
169
|
+
const lp = `%${t}%`;
|
|
170
|
+
params.push(lp, lp, lp, lp);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
sql += " ORDER BY CASE WHEN source_authority = 'official' THEN 0 ELSE 1 END LIMIT ?";
|
|
178
|
+
params.push(limit);
|
|
179
|
+
|
|
180
|
+
const rows = raw.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
181
|
+
results = rows.map(r => this.hydrateIndexedExample(r));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private hydrateIndexedExample(row: Record<string, unknown>): IndexedExample {
|
|
188
|
+
return {
|
|
189
|
+
id: String(row.id),
|
|
190
|
+
chunkId: row.chunk_id ? String(row.chunk_id) : undefined,
|
|
191
|
+
pageId: String(row.page_id),
|
|
192
|
+
snapshotId: String(row.snapshot_id),
|
|
193
|
+
language: String(row.language),
|
|
194
|
+
framework: row.framework ? String(row.framework) : undefined,
|
|
195
|
+
task: String(row.task),
|
|
196
|
+
code: String(row.code),
|
|
197
|
+
sourceUrl: String(row.source_url),
|
|
198
|
+
sourceAuthority: String(row.source_authority) as IndexedExample['sourceAuthority'],
|
|
199
|
+
relatedApi: row.related_api ? String(row.related_api) : undefined,
|
|
200
|
+
relatedSymbol: row.related_symbol ? String(row.related_symbol) : undefined,
|
|
201
|
+
docVersion: row.doc_version ? String(row.doc_version) : undefined,
|
|
202
|
+
provenance: row.provenance_json ? JSON.parse(String(row.provenance_json)) : undefined,
|
|
203
|
+
createdAt: String(row.created_at),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|