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,205 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NormalizedPage,
|
|
3
|
+
Link,
|
|
4
|
+
CodeExample,
|
|
5
|
+
Heading,
|
|
6
|
+
SecurityAnnotation,
|
|
7
|
+
Provenance,
|
|
8
|
+
} from '../../../shared/src/index.ts';
|
|
9
|
+
import type { DocOrbitDb } from '../db.ts';
|
|
10
|
+
|
|
11
|
+
export class PageRepository {
|
|
12
|
+
private db: DocOrbitDb;
|
|
13
|
+
|
|
14
|
+
constructor(db: DocOrbitDb) {
|
|
15
|
+
this.db = db;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
savePage(page: NormalizedPage): void {
|
|
19
|
+
const raw = this.db.getRawDb();
|
|
20
|
+
|
|
21
|
+
const pageStmt = raw.prepare(`
|
|
22
|
+
INSERT INTO pages (
|
|
23
|
+
id, source_id, title, url, content, content_hash,
|
|
24
|
+
fetched_at, raw_bytes, estimated_tokens,
|
|
25
|
+
headings_json, security_annotations_json, provenance_json
|
|
26
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
27
|
+
ON CONFLICT(url) DO UPDATE SET
|
|
28
|
+
title = excluded.title,
|
|
29
|
+
content = excluded.content,
|
|
30
|
+
content_hash = excluded.content_hash,
|
|
31
|
+
fetched_at = excluded.fetched_at,
|
|
32
|
+
raw_bytes = excluded.raw_bytes,
|
|
33
|
+
estimated_tokens = excluded.estimated_tokens,
|
|
34
|
+
headings_json = excluded.headings_json,
|
|
35
|
+
security_annotations_json = excluded.security_annotations_json,
|
|
36
|
+
provenance_json = excluded.provenance_json
|
|
37
|
+
`);
|
|
38
|
+
|
|
39
|
+
pageStmt.run(
|
|
40
|
+
page.id,
|
|
41
|
+
page.sourceId || null,
|
|
42
|
+
page.title,
|
|
43
|
+
page.url,
|
|
44
|
+
page.content,
|
|
45
|
+
page.contentHash,
|
|
46
|
+
page.fetchedAt,
|
|
47
|
+
page.rawBytes,
|
|
48
|
+
page.estimatedTokens,
|
|
49
|
+
JSON.stringify(page.headings),
|
|
50
|
+
JSON.stringify(page.securityAnnotations),
|
|
51
|
+
page.provenance ? JSON.stringify(page.provenance) : null
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
raw.prepare('DELETE FROM links WHERE page_id = ?').run(page.id);
|
|
55
|
+
const linkStmt = raw.prepare(`
|
|
56
|
+
INSERT INTO links (id, page_id, text, url, is_external)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?)
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < page.links.length; i++) {
|
|
61
|
+
const link = page.links[i];
|
|
62
|
+
const linkId = `${page.id}_link_${i}`;
|
|
63
|
+
linkStmt.run(linkId, page.id, link.text, link.url, link.isExternal ? 1 : 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
raw.prepare('DELETE FROM code_examples WHERE page_id = ?').run(page.id);
|
|
67
|
+
const codeStmt = raw.prepare(`
|
|
68
|
+
INSERT INTO code_examples (id, page_id, language, code, caption)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?)
|
|
70
|
+
`);
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < page.codeExamples.length; i++) {
|
|
73
|
+
const example = page.codeExamples[i];
|
|
74
|
+
const exampleId = `${page.id}_code_${i}`;
|
|
75
|
+
codeStmt.run(exampleId, page.id, example.language, example.code, example.caption || null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (this.db.isFtsAvailable()) {
|
|
79
|
+
try {
|
|
80
|
+
raw.prepare('DELETE FROM pages_fts WHERE page_id = ?').run(page.id);
|
|
81
|
+
raw.prepare(`
|
|
82
|
+
INSERT INTO pages_fts (page_id, title, content)
|
|
83
|
+
VALUES (?, ?, ?)
|
|
84
|
+
`).run(page.id, page.title, page.content);
|
|
85
|
+
} catch {
|
|
86
|
+
// Continue
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getPage(id: string): NormalizedPage | null {
|
|
92
|
+
const raw = this.db.getRawDb();
|
|
93
|
+
const row = raw.prepare('SELECT * FROM pages WHERE id = ?').get(id) as Record<string, unknown> | undefined;
|
|
94
|
+
if (!row) return null;
|
|
95
|
+
return this.hydratePage(row);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
getPageByUrl(url: string): NormalizedPage | null {
|
|
99
|
+
const raw = this.db.getRawDb();
|
|
100
|
+
const row = raw.prepare('SELECT * FROM pages WHERE url = ?').get(url) as Record<string, unknown> | undefined;
|
|
101
|
+
if (!row) return null;
|
|
102
|
+
return this.hydratePage(row);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getPagesBySource(sourceId: string): NormalizedPage[] {
|
|
106
|
+
const raw = this.db.getRawDb();
|
|
107
|
+
const rows = raw.prepare('SELECT * FROM pages WHERE source_id = ? ORDER BY fetched_at ASC').all(sourceId) as Array<Record<string, unknown>>;
|
|
108
|
+
return rows.map(r => this.hydratePage(r));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
listPages(limit: number = 50, offset: number = 0): NormalizedPage[] {
|
|
112
|
+
const raw = this.db.getRawDb();
|
|
113
|
+
const rows = raw.prepare('SELECT * FROM pages ORDER BY fetched_at DESC LIMIT ? OFFSET ?').all(limit, offset) as Array<Record<string, unknown>>;
|
|
114
|
+
return rows.map(r => this.hydratePage(r));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
countPages(sourceId?: string): number {
|
|
118
|
+
const raw = this.db.getRawDb();
|
|
119
|
+
if (sourceId) {
|
|
120
|
+
const row = raw.prepare('SELECT COUNT(*) as count FROM pages WHERE source_id = ?').get(sourceId) as { count: number };
|
|
121
|
+
return row?.count || 0;
|
|
122
|
+
}
|
|
123
|
+
const row = raw.prepare('SELECT COUNT(*) as count FROM pages').get() as { count: number };
|
|
124
|
+
return row?.count || 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
searchPagesFts(query: string, limit: number = 10): NormalizedPage[] {
|
|
128
|
+
if (!this.db.isFtsAvailable()) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const raw = this.db.getRawDb();
|
|
133
|
+
const sanitizedQuery = `"${query.replace(/"/g, '""')}"`;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const rows = raw.prepare(`
|
|
137
|
+
SELECT p.* FROM pages p
|
|
138
|
+
JOIN pages_fts fts ON p.id = fts.page_id
|
|
139
|
+
WHERE pages_fts MATCH ?
|
|
140
|
+
ORDER BY rank
|
|
141
|
+
LIMIT ?
|
|
142
|
+
`).all(sanitizedQuery, limit) as Array<Record<string, unknown>>;
|
|
143
|
+
|
|
144
|
+
return rows.map(r => this.hydratePage(r));
|
|
145
|
+
} catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getPageLinks(pageId: string): string[] {
|
|
151
|
+
const raw = this.db.getRawDb();
|
|
152
|
+
const rows = raw.prepare('SELECT url FROM links WHERE page_id = ?').all(pageId) as Array<Record<string, unknown>>;
|
|
153
|
+
return rows.map(r => String(r.url));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
savePageLinks(pageId: string, targetUrls: string[]): void {
|
|
157
|
+
const raw = this.db.getRawDb();
|
|
158
|
+
raw.prepare('DELETE FROM links WHERE page_id = ?').run(pageId);
|
|
159
|
+
const stmt = raw.prepare('INSERT OR IGNORE INTO links (id, page_id, text, url, is_external) VALUES (?, ?, ?, ?, ?)');
|
|
160
|
+
for (let i = 0; i < targetUrls.length; i++) {
|
|
161
|
+
stmt.run(`${pageId}_link_${i}`, pageId, '', targetUrls[i], 0);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
hydratePage(row: Record<string, unknown>): NormalizedPage {
|
|
166
|
+
const raw = this.db.getRawDb();
|
|
167
|
+
const pageId = String(row.id);
|
|
168
|
+
|
|
169
|
+
const linkRows = raw.prepare('SELECT text, url, is_external FROM links WHERE page_id = ?').all(pageId) as Array<Record<string, unknown>>;
|
|
170
|
+
const links: Link[] = linkRows.map(lr => ({
|
|
171
|
+
text: String(lr.text),
|
|
172
|
+
url: String(lr.url),
|
|
173
|
+
isExternal: Number(lr.is_external) === 1,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
const codeRows = raw.prepare('SELECT id, language, code, caption FROM code_examples WHERE page_id = ?').all(pageId) as Array<Record<string, unknown>>;
|
|
177
|
+
const codeExamples: CodeExample[] = codeRows.map(cr => ({
|
|
178
|
+
id: String(cr.id),
|
|
179
|
+
language: String(cr.language),
|
|
180
|
+
code: String(cr.code),
|
|
181
|
+
caption: cr.caption ? String(cr.caption) : undefined,
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const headings: Heading[] = row.headings_json ? JSON.parse(String(row.headings_json)) : [];
|
|
185
|
+
const securityAnnotations: SecurityAnnotation[] = row.security_annotations_json ? JSON.parse(String(row.security_annotations_json)) : [];
|
|
186
|
+
const provenance: Provenance | undefined = row.provenance_json ? JSON.parse(String(row.provenance_json)) : undefined;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
id: pageId,
|
|
190
|
+
sourceId: String(row.source_id),
|
|
191
|
+
title: String(row.title),
|
|
192
|
+
url: String(row.url),
|
|
193
|
+
content: String(row.content),
|
|
194
|
+
headings,
|
|
195
|
+
links,
|
|
196
|
+
codeExamples,
|
|
197
|
+
contentHash: String(row.content_hash),
|
|
198
|
+
fetchedAt: String(row.fetched_at),
|
|
199
|
+
rawBytes: Number(row.raw_bytes),
|
|
200
|
+
estimatedTokens: Number(row.estimated_tokens),
|
|
201
|
+
securityAnnotations,
|
|
202
|
+
provenance,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { Pitfall, PitfallKind } from '../../../shared/src/index.ts';
|
|
2
|
+
import type { DocOrbitDb } from '../db.ts';
|
|
3
|
+
import type { IPitfallRepository } from '../interfaces.ts';
|
|
4
|
+
import { extractSearchTokens } from '../search-tokens.ts';
|
|
5
|
+
|
|
6
|
+
export class PitfallRepository implements IPitfallRepository {
|
|
7
|
+
private db: DocOrbitDb;
|
|
8
|
+
|
|
9
|
+
constructor(db: DocOrbitDb) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
savePitfalls(pitfalls: Pitfall[]): void {
|
|
14
|
+
if (pitfalls.length === 0) return;
|
|
15
|
+
const raw = this.db.getRawDb();
|
|
16
|
+
|
|
17
|
+
const insertPf = raw.prepare(`
|
|
18
|
+
INSERT INTO pitfalls (
|
|
19
|
+
id, chunk_id, page_id, snapshot_id, kind, title,
|
|
20
|
+
content, related_api, related_symbol, doc_version,
|
|
21
|
+
provenance_json, created_at
|
|
22
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
23
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
24
|
+
chunk_id = excluded.chunk_id,
|
|
25
|
+
kind = excluded.kind,
|
|
26
|
+
title = excluded.title,
|
|
27
|
+
content = excluded.content,
|
|
28
|
+
related_api = excluded.related_api,
|
|
29
|
+
related_symbol = excluded.related_symbol,
|
|
30
|
+
doc_version = excluded.doc_version,
|
|
31
|
+
provenance_json = excluded.provenance_json
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
const insertFts = this.db.isFtsAvailable()
|
|
35
|
+
? raw.prepare(`
|
|
36
|
+
INSERT INTO pitfalls_fts (pitfall_id, kind, title, content, related_api, related_symbol)
|
|
37
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
38
|
+
`)
|
|
39
|
+
: null;
|
|
40
|
+
|
|
41
|
+
const deleteFts = this.db.isFtsAvailable()
|
|
42
|
+
? raw.prepare('DELETE FROM pitfalls_fts WHERE pitfall_id = ?')
|
|
43
|
+
: null;
|
|
44
|
+
|
|
45
|
+
raw.exec('BEGIN TRANSACTION;');
|
|
46
|
+
try {
|
|
47
|
+
for (const pf of pitfalls) {
|
|
48
|
+
insertPf.run(
|
|
49
|
+
pf.id,
|
|
50
|
+
pf.chunkId || null,
|
|
51
|
+
pf.pageId,
|
|
52
|
+
pf.snapshotId || 'snap_default',
|
|
53
|
+
pf.kind,
|
|
54
|
+
pf.title,
|
|
55
|
+
pf.content || pf.message || '',
|
|
56
|
+
pf.relatedApi || null,
|
|
57
|
+
pf.relatedSymbol || null,
|
|
58
|
+
pf.docVersion || null,
|
|
59
|
+
pf.provenance ? JSON.stringify(pf.provenance) : null,
|
|
60
|
+
pf.createdAt || new Date().toISOString()
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (insertFts && deleteFts) {
|
|
64
|
+
deleteFts.run(pf.id);
|
|
65
|
+
insertFts.run(
|
|
66
|
+
pf.id,
|
|
67
|
+
pf.kind,
|
|
68
|
+
pf.title,
|
|
69
|
+
pf.content || pf.message || '',
|
|
70
|
+
pf.relatedApi || '',
|
|
71
|
+
pf.relatedSymbol || ''
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
raw.exec('COMMIT;');
|
|
76
|
+
} catch (err) {
|
|
77
|
+
raw.exec('ROLLBACK;');
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getPitfallsBySnapshot(snapshotId: string): Pitfall[] {
|
|
83
|
+
const raw = this.db.getRawDb();
|
|
84
|
+
const rows = raw.prepare('SELECT * FROM pitfalls WHERE snapshot_id = ?').all(snapshotId) as Array<Record<string, unknown>>;
|
|
85
|
+
return rows.map(r => this.hydratePitfall(r));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
searchPitfalls(
|
|
89
|
+
query: string,
|
|
90
|
+
options: { kind?: PitfallKind; docVersion?: string; limit?: number } = {}
|
|
91
|
+
): Pitfall[] {
|
|
92
|
+
const raw = this.db.getRawDb();
|
|
93
|
+
const limit = options.limit || 10;
|
|
94
|
+
const term = query.trim();
|
|
95
|
+
|
|
96
|
+
let results: Pitfall[] = [];
|
|
97
|
+
|
|
98
|
+
// FTS attempt
|
|
99
|
+
if (this.db.isFtsAvailable() && term.length > 0) {
|
|
100
|
+
const cleanTokens = extractSearchTokens(term);
|
|
101
|
+
|
|
102
|
+
if (cleanTokens.length > 0) {
|
|
103
|
+
const sanitizedFts = cleanTokens
|
|
104
|
+
.map(t => t.length >= 4 ? `"${t.replace(/"/g, '""')}"*` : `"${t.replace(/"/g, '""')}"`)
|
|
105
|
+
.join(' OR ');
|
|
106
|
+
try {
|
|
107
|
+
let sql = `
|
|
108
|
+
SELECT pf.*, fts.rank as fts_rank
|
|
109
|
+
FROM pitfalls pf
|
|
110
|
+
JOIN pitfalls_fts fts ON pf.id = fts.pitfall_id
|
|
111
|
+
WHERE pitfalls_fts MATCH ?
|
|
112
|
+
`;
|
|
113
|
+
const params: unknown[] = [sanitizedFts];
|
|
114
|
+
|
|
115
|
+
if (options.kind) {
|
|
116
|
+
sql += ' AND pf.kind = ?';
|
|
117
|
+
params.push(options.kind);
|
|
118
|
+
}
|
|
119
|
+
if (options.docVersion) {
|
|
120
|
+
sql += ' AND (pf.doc_version = ? OR pf.doc_version IS NULL)';
|
|
121
|
+
params.push(options.docVersion);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
sql += ' ORDER BY fts.rank LIMIT ?';
|
|
125
|
+
params.push(limit);
|
|
126
|
+
|
|
127
|
+
const rows = raw.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
128
|
+
results = rows.map(r => this.hydratePitfall(r));
|
|
129
|
+
} catch {
|
|
130
|
+
// Fall through to LIKE
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (results.length === 0) {
|
|
136
|
+
// LIKE fallback
|
|
137
|
+
let sql = 'SELECT * FROM pitfalls WHERE 1=1';
|
|
138
|
+
const params: unknown[] = [];
|
|
139
|
+
|
|
140
|
+
if (options.kind) {
|
|
141
|
+
sql += ' AND kind = ?';
|
|
142
|
+
params.push(options.kind);
|
|
143
|
+
}
|
|
144
|
+
if (options.docVersion) {
|
|
145
|
+
sql += ' AND (doc_version = ? OR doc_version IS NULL)';
|
|
146
|
+
params.push(options.docVersion);
|
|
147
|
+
}
|
|
148
|
+
if (term.length > 0) {
|
|
149
|
+
const tokens = extractSearchTokens(term);
|
|
150
|
+
if (tokens.length > 0) {
|
|
151
|
+
const tokenClauses = tokens.map(() => '(title LIKE ? OR content LIKE ? OR related_api LIKE ? OR related_symbol LIKE ?)');
|
|
152
|
+
sql += ` AND (${tokenClauses.join(' OR ')})`;
|
|
153
|
+
for (const t of tokens) {
|
|
154
|
+
const lp = `%${t}%`;
|
|
155
|
+
params.push(lp, lp, lp, lp);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
sql += ' ORDER BY kind LIMIT ?';
|
|
163
|
+
params.push(limit);
|
|
164
|
+
|
|
165
|
+
const rows = raw.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
166
|
+
results = rows.map(r => this.hydratePitfall(r));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return results;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private hydratePitfall(row: Record<string, unknown>): Pitfall {
|
|
173
|
+
return {
|
|
174
|
+
id: String(row.id),
|
|
175
|
+
chunkId: row.chunk_id ? String(row.chunk_id) : undefined,
|
|
176
|
+
pageId: String(row.page_id),
|
|
177
|
+
snapshotId: String(row.snapshot_id),
|
|
178
|
+
kind: String(row.kind) as PitfallKind,
|
|
179
|
+
title: String(row.title),
|
|
180
|
+
content: String(row.content),
|
|
181
|
+
relatedApi: row.related_api ? String(row.related_api) : undefined,
|
|
182
|
+
relatedSymbol: row.related_symbol ? String(row.related_symbol) : undefined,
|
|
183
|
+
docVersion: row.doc_version ? String(row.doc_version) : undefined,
|
|
184
|
+
provenance: row.provenance_json ? JSON.parse(String(row.provenance_json)) : undefined,
|
|
185
|
+
createdAt: String(row.created_at),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { computeSha256 } from '../../../shared/src/index.ts';
|
|
2
|
+
import type { DiscoveredSource, NormalizedPage } from '../../../shared/src/index.ts';
|
|
3
|
+
import type { DocOrbitDb } from '../db.ts';
|
|
4
|
+
|
|
5
|
+
export interface SnapshotRecord {
|
|
6
|
+
id: string;
|
|
7
|
+
sourceId: string;
|
|
8
|
+
snapshotHash: string;
|
|
9
|
+
pageCount: number;
|
|
10
|
+
capturedAt: string;
|
|
11
|
+
docVersion?: string;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SourceRepository {
|
|
16
|
+
private db: DocOrbitDb;
|
|
17
|
+
private hydratePageFn?: (row: Record<string, unknown>) => NormalizedPage;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
db: DocOrbitDb,
|
|
21
|
+
hydratePageFn?: (row: Record<string, unknown>) => NormalizedPage
|
|
22
|
+
) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
this.hydratePageFn = hydratePageFn;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
saveSource(source: DiscoveredSource): string {
|
|
28
|
+
const raw = this.db.getRawDb();
|
|
29
|
+
const id = `src_${computeSha256(source.url).slice(0, 16)}`;
|
|
30
|
+
const createdAt = new Date().toISOString();
|
|
31
|
+
|
|
32
|
+
const stmt = raw.prepare(`
|
|
33
|
+
INSERT INTO sources (
|
|
34
|
+
id, url, type, discovered_by, status, content_type,
|
|
35
|
+
confidence, authority, machine_readable, metadata, created_at
|
|
36
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
37
|
+
ON CONFLICT(url) DO UPDATE SET
|
|
38
|
+
type = excluded.type,
|
|
39
|
+
discovered_by = excluded.discovered_by,
|
|
40
|
+
status = excluded.status,
|
|
41
|
+
content_type = excluded.content_type,
|
|
42
|
+
confidence = excluded.confidence,
|
|
43
|
+
authority = excluded.authority,
|
|
44
|
+
machine_readable = excluded.machine_readable,
|
|
45
|
+
metadata = excluded.metadata
|
|
46
|
+
`);
|
|
47
|
+
|
|
48
|
+
stmt.run(
|
|
49
|
+
id,
|
|
50
|
+
source.url,
|
|
51
|
+
source.type,
|
|
52
|
+
source.discoveredBy || 'manual',
|
|
53
|
+
source.status,
|
|
54
|
+
source.contentType || null,
|
|
55
|
+
source.confidence,
|
|
56
|
+
source.authority,
|
|
57
|
+
source.machineReadable ? 1 : 0,
|
|
58
|
+
source.metadata ? JSON.stringify(source.metadata) : null,
|
|
59
|
+
createdAt
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const row = raw.prepare('SELECT id FROM sources WHERE url = ?').get(source.url) as { id: string } | undefined;
|
|
63
|
+
return row?.id || id;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getSource(id: string): (DiscoveredSource & { id: string }) | null {
|
|
67
|
+
const raw = this.db.getRawDb();
|
|
68
|
+
const row = raw.prepare('SELECT * FROM sources WHERE id = ?').get(id) as Record<string, unknown> | undefined;
|
|
69
|
+
if (!row) return null;
|
|
70
|
+
return this.mapSourceRow(row);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getSourceByUrl(url: string): (DiscoveredSource & { id: string }) | null {
|
|
74
|
+
const raw = this.db.getRawDb();
|
|
75
|
+
const row = raw.prepare('SELECT * FROM sources WHERE url = ?').get(url) as Record<string, unknown> | undefined;
|
|
76
|
+
if (!row) return null;
|
|
77
|
+
return this.mapSourceRow(row);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
listSources(): Array<DiscoveredSource & { id: string; createdAt: string }> {
|
|
81
|
+
const raw = this.db.getRawDb();
|
|
82
|
+
const rows = raw.prepare('SELECT * FROM sources ORDER BY created_at DESC').all() as Array<Record<string, unknown>>;
|
|
83
|
+
return rows.map(r => ({
|
|
84
|
+
...this.mapSourceRow(r),
|
|
85
|
+
createdAt: String(r.created_at),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
createSnapshot(sourceId: string, metadata?: Record<string, unknown>, docVersion?: string): string {
|
|
90
|
+
const raw = this.db.getRawDb();
|
|
91
|
+
const pages = raw.prepare('SELECT id, content_hash FROM pages WHERE source_id = ? ORDER BY id ASC').all(sourceId) as Array<{ id: string; content_hash: string }>;
|
|
92
|
+
|
|
93
|
+
const combinedHash = computeSha256(pages.map(p => p.content_hash).join(':'));
|
|
94
|
+
const snapshotId = `snap_${combinedHash.slice(0, 16)}`;
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
const version = docVersion || (metadata?.docVersion as string) || null;
|
|
97
|
+
|
|
98
|
+
const stmt = raw.prepare(`
|
|
99
|
+
INSERT INTO snapshots (id, source_id, snapshot_hash, page_count, captured_at, doc_version, metadata)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
101
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
102
|
+
captured_at = excluded.captured_at,
|
|
103
|
+
page_count = excluded.page_count,
|
|
104
|
+
doc_version = COALESCE(excluded.doc_version, snapshots.doc_version),
|
|
105
|
+
metadata = excluded.metadata
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
stmt.run(
|
|
109
|
+
snapshotId,
|
|
110
|
+
sourceId,
|
|
111
|
+
combinedHash,
|
|
112
|
+
pages.length,
|
|
113
|
+
now,
|
|
114
|
+
version,
|
|
115
|
+
metadata ? JSON.stringify(metadata) : null
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Link snapshot to page membership in snapshot_pages
|
|
119
|
+
const linkStmt = raw.prepare(`
|
|
120
|
+
INSERT OR REPLACE INTO snapshot_pages (snapshot_id, page_id, content_hash)
|
|
121
|
+
VALUES (?, ?, ?)
|
|
122
|
+
`);
|
|
123
|
+
for (const p of pages) {
|
|
124
|
+
linkStmt.run(snapshotId, p.id, p.content_hash);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return snapshotId;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
linkSnapshotPages(snapshotId: string, pageIds: string[]): void {
|
|
131
|
+
const raw = this.db.getRawDb();
|
|
132
|
+
const linkStmt = raw.prepare(`
|
|
133
|
+
INSERT OR REPLACE INTO snapshot_pages (snapshot_id, page_id, content_hash)
|
|
134
|
+
VALUES (?, ?, COALESCE((SELECT content_hash FROM pages WHERE id = ?), 'hash_default'))
|
|
135
|
+
`);
|
|
136
|
+
for (const pageId of pageIds) {
|
|
137
|
+
linkStmt.run(snapshotId, pageId, pageId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getSnapshot(snapshotId: string): SnapshotRecord | null {
|
|
142
|
+
const raw = this.db.getRawDb();
|
|
143
|
+
const row = raw.prepare('SELECT * FROM snapshots WHERE id = ?').get(snapshotId) as Record<string, unknown> | undefined;
|
|
144
|
+
if (!row) return null;
|
|
145
|
+
return {
|
|
146
|
+
id: String(row.id),
|
|
147
|
+
sourceId: String(row.source_id),
|
|
148
|
+
snapshotHash: String(row.snapshot_hash),
|
|
149
|
+
pageCount: Number(row.page_count),
|
|
150
|
+
capturedAt: String(row.captured_at),
|
|
151
|
+
docVersion: row.doc_version ? String(row.doc_version) : undefined,
|
|
152
|
+
metadata: row.metadata ? JSON.parse(String(row.metadata)) : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
listSnapshots(sourceId?: string): SnapshotRecord[] {
|
|
157
|
+
const raw = this.db.getRawDb();
|
|
158
|
+
let sql = 'SELECT * FROM snapshots';
|
|
159
|
+
const params: unknown[] = [];
|
|
160
|
+
if (sourceId) {
|
|
161
|
+
sql += ' WHERE source_id = ?';
|
|
162
|
+
params.push(sourceId);
|
|
163
|
+
}
|
|
164
|
+
sql += ' ORDER BY captured_at DESC';
|
|
165
|
+
const rows = raw.prepare(sql).all(...params) as Array<Record<string, unknown>>;
|
|
166
|
+
return rows.map(row => ({
|
|
167
|
+
id: String(row.id),
|
|
168
|
+
sourceId: String(row.source_id),
|
|
169
|
+
snapshotHash: String(row.snapshot_hash),
|
|
170
|
+
pageCount: Number(row.page_count),
|
|
171
|
+
capturedAt: String(row.captured_at),
|
|
172
|
+
docVersion: row.doc_version ? String(row.doc_version) : undefined,
|
|
173
|
+
metadata: row.metadata ? JSON.parse(String(row.metadata)) : undefined,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getSnapshotPages(snapshotId: string): NormalizedPage[] {
|
|
178
|
+
const raw = this.db.getRawDb();
|
|
179
|
+
const rows = raw.prepare(`
|
|
180
|
+
SELECT p.* FROM pages p
|
|
181
|
+
JOIN snapshot_pages sp ON p.id = sp.page_id
|
|
182
|
+
WHERE sp.snapshot_id = ?
|
|
183
|
+
ORDER BY p.id ASC
|
|
184
|
+
`).all(snapshotId) as Array<Record<string, unknown>>;
|
|
185
|
+
if (this.hydratePageFn) {
|
|
186
|
+
return rows.map(r => this.hydratePageFn!(r));
|
|
187
|
+
}
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private mapSourceRow(row: Record<string, unknown>): DiscoveredSource & { id: string } {
|
|
192
|
+
return {
|
|
193
|
+
id: String(row.id),
|
|
194
|
+
url: String(row.url),
|
|
195
|
+
type: row.type as any,
|
|
196
|
+
discoveredBy: String(row.discovered_by),
|
|
197
|
+
status: row.status as any,
|
|
198
|
+
contentType: row.content_type ? String(row.content_type) : undefined,
|
|
199
|
+
confidence: Number(row.confidence),
|
|
200
|
+
authority: row.authority as any,
|
|
201
|
+
machineReadable: Number(row.machine_readable) === 1,
|
|
202
|
+
metadata: row.metadata ? JSON.parse(String(row.metadata)) : undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|