@devflow-tools/knowledge-engine 0.16.27 → 0.16.28
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/dist/context7-fetcher.d.ts +2 -0
- package/dist/context7-fetcher.d.ts.map +1 -1
- package/dist/context7-fetcher.js +35 -2
- package/dist/context7-fetcher.js.map +1 -1
- package/dist/dependency-source-compiler.d.ts +24 -0
- package/dist/dependency-source-compiler.d.ts.map +1 -0
- package/dist/dependency-source-compiler.js +265 -0
- package/dist/dependency-source-compiler.js.map +1 -0
- package/dist/evidence-graph.d.ts +24 -0
- package/dist/evidence-graph.d.ts.map +1 -0
- package/dist/evidence-graph.js +82 -0
- package/dist/evidence-graph.js.map +1 -0
- package/dist/fetcher.d.ts +11 -0
- package/dist/fetcher.d.ts.map +1 -1
- package/dist/fetcher.js +12 -0
- package/dist/fetcher.js.map +1 -1
- package/dist/git-fetcher.d.ts +2 -0
- package/dist/git-fetcher.d.ts.map +1 -1
- package/dist/git-fetcher.js +47 -0
- package/dist/git-fetcher.js.map +1 -1
- package/dist/hybrid-search.d.ts +6 -2
- package/dist/hybrid-search.d.ts.map +1 -1
- package/dist/hybrid-search.js +192 -56
- package/dist/hybrid-search.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge-compiler.d.ts +48 -0
- package/dist/knowledge-compiler.d.ts.map +1 -0
- package/dist/knowledge-compiler.js +121 -0
- package/dist/knowledge-compiler.js.map +1 -0
- package/dist/knowledge-engine.d.ts +25 -1
- package/dist/knowledge-engine.d.ts.map +1 -1
- package/dist/knowledge-engine.js +446 -64
- package/dist/knowledge-engine.js.map +1 -1
- package/dist/knowledge-quality.d.ts +3 -0
- package/dist/knowledge-quality.d.ts.map +1 -1
- package/dist/knowledge-quality.js +14 -5
- package/dist/knowledge-quality.js.map +1 -1
- package/dist/knowledge-store.d.ts +61 -0
- package/dist/knowledge-store.d.ts.map +1 -1
- package/dist/knowledge-store.js +448 -1
- package/dist/knowledge-store.js.map +1 -1
- package/dist/markdown-converter.d.ts +1 -1
- package/dist/markdown-converter.js +1 -1
- package/dist/retrieval-scoring-profile.d.ts +20 -0
- package/dist/retrieval-scoring-profile.d.ts.map +1 -0
- package/dist/retrieval-scoring-profile.js +35 -0
- package/dist/retrieval-scoring-profile.js.map +1 -0
- package/dist/source-catalog.d.ts +57 -0
- package/dist/source-catalog.d.ts.map +1 -0
- package/dist/source-catalog.js +351 -0
- package/dist/source-catalog.js.map +1 -0
- package/dist/source-readiness.d.ts +7 -0
- package/dist/source-readiness.d.ts.map +1 -1
- package/dist/source-readiness.js +31 -2
- package/dist/source-readiness.js.map +1 -1
- package/dist/source-selector.d.ts +11 -0
- package/dist/source-selector.d.ts.map +1 -1
- package/dist/source-selector.js +160 -15
- package/dist/source-selector.js.map +1 -1
- package/dist/structural-markdown-chunker.d.ts +33 -0
- package/dist/structural-markdown-chunker.d.ts.map +1 -0
- package/dist/structural-markdown-chunker.js +328 -0
- package/dist/structural-markdown-chunker.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -5
package/dist/knowledge-engine.js
CHANGED
|
@@ -2,8 +2,8 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { fetchHtmlKnowledgeSource, closeBrowser } from "./fetcher.js";
|
|
6
|
+
import { KNOWLEDGE_CHUNK_POLICY_VERSION, sliceByHeadings } from "./markdown-converter.js";
|
|
7
7
|
import { KnowledgeHybridSearch } from "./hybrid-search.js";
|
|
8
8
|
import { KnowledgeStore } from "./knowledge-store.js";
|
|
9
9
|
import { EmbeddingProvider } from "@devflow-tools/memory-engine";
|
|
@@ -11,22 +11,165 @@ import { BUILTIN_SOURCES } from "./types.js";
|
|
|
11
11
|
import { fetchFromGitHub } from "./git-fetcher.js";
|
|
12
12
|
import { fetchFromContext7 } from "./context7-fetcher.js";
|
|
13
13
|
import { selectKnowledgeSources } from "./source-selector.js";
|
|
14
|
-
import { estimateKnowledgeTokens as estimateTokens, resolveKnowledgeTitle } from "./result-quality.js";
|
|
14
|
+
import { estimateKnowledgeTokens as estimateTokens, lexicalRelevance, resolveKnowledgeTitle, } from "./result-quality.js";
|
|
15
15
|
import { detectProjectKnowledgeVersions, expectedVersionForSource, isKnowledgeVersionCompatible, } from './source-readiness.js';
|
|
16
16
|
import { evaluateKnowledgeQuality } from './knowledge-quality.js';
|
|
17
|
+
import { KnowledgeCompiler } from './knowledge-compiler.js';
|
|
18
|
+
import { SourceCatalog } from './source-catalog.js';
|
|
19
|
+
import { compileDependencySource } from './dependency-source-compiler.js';
|
|
20
|
+
import { expandKnowledgeEvidenceGraph } from './evidence-graph.js';
|
|
21
|
+
import { KNOWLEDGE_RETRIEVAL_SCORING_PROFILE } from './retrieval-scoring-profile.js';
|
|
17
22
|
export const DEFAULT_KNOWLEDGE_TOKEN_BUDGET = 3000;
|
|
18
23
|
export const estimateKnowledgeTokens = estimateTokens;
|
|
24
|
+
function normalizeActiveSkill(value) {
|
|
25
|
+
return value.normalize('NFKC').trim().replace(/^\/?devflow:/iu, '').toLocaleLowerCase('en-US');
|
|
26
|
+
}
|
|
27
|
+
function dependencyPackagesForMissingNeeds(needs, dependencies) {
|
|
28
|
+
const normalizedDependencies = new Map(Object.keys(dependencies).map(name => [name.toLocaleLowerCase('en-US'), name]));
|
|
29
|
+
const aliases = {
|
|
30
|
+
angular: '@angular/core',
|
|
31
|
+
nest: '@nestjs/core',
|
|
32
|
+
nextjs: 'next',
|
|
33
|
+
node: '@types/node',
|
|
34
|
+
'node.js': '@types/node',
|
|
35
|
+
tailwind: 'tailwindcss',
|
|
36
|
+
};
|
|
37
|
+
return [...new Set(needs.flatMap(need => {
|
|
38
|
+
if (!need.entity)
|
|
39
|
+
return [];
|
|
40
|
+
const key = need.entity.normalize('NFKC').trim().toLocaleLowerCase('en-US');
|
|
41
|
+
const packageName = normalizedDependencies.get(key)
|
|
42
|
+
?? normalizedDependencies.get(aliases[key] ?? '');
|
|
43
|
+
return packageName ? [packageName] : [];
|
|
44
|
+
}))];
|
|
45
|
+
}
|
|
46
|
+
function buildSafeFtsQuery(value) {
|
|
47
|
+
const terms = value.normalize('NFKC')
|
|
48
|
+
.match(/[\p{L}\p{N}_@.-]{2,}/gu)
|
|
49
|
+
?.map(term => term.replace(/^\.+|\.+$/gu, '').replace(/"/gu, ''))
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.slice(0, 24) ?? [];
|
|
52
|
+
return [...new Set(terms)].map(term => `"${term}"`).join(' OR ');
|
|
53
|
+
}
|
|
54
|
+
function dedupeKnowledgeEntries(entries) {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
return entries.filter(entry => {
|
|
57
|
+
if (seen.has(entry.id))
|
|
58
|
+
return false;
|
|
59
|
+
seen.add(entry.id);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function provenanceComplete(value) {
|
|
64
|
+
if (!value)
|
|
65
|
+
return false;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(value);
|
|
68
|
+
return parsed.immutable === true
|
|
69
|
+
&& typeof parsed.sourceId === 'string'
|
|
70
|
+
&& typeof parsed.sourceUri === 'string'
|
|
71
|
+
&& typeof parsed.contentHash === 'string';
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function authorityFromProvenance(value, source) {
|
|
78
|
+
if (value) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(value);
|
|
81
|
+
if (parsed.sourceKind === 'official')
|
|
82
|
+
return 'official';
|
|
83
|
+
if (parsed.sourceKind === 'project')
|
|
84
|
+
return 'project';
|
|
85
|
+
if (parsed.sourceKind === 'plugin' || parsed.sourceKind === 'dependency')
|
|
86
|
+
return 'maintainer';
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Legacy chunks use source-name authority.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return /react|typescript|node|vue|angular|mdn|official/iu.test(source) ? 'official' : 'community';
|
|
93
|
+
}
|
|
94
|
+
function addNecessaryAdjacentEntries(input) {
|
|
95
|
+
const chunks = new Map(input.chunks.map(chunk => [chunk.id, chunk]));
|
|
96
|
+
const selected = new Set(input.candidates.map(candidate => candidate.id));
|
|
97
|
+
const additions = [];
|
|
98
|
+
const requiredSymbols = input.knowledgeNeeds.flatMap(need => need.symbol ? [normalizeLookup(need.symbol)] : []);
|
|
99
|
+
const requiredEntities = input.knowledgeNeeds.flatMap(need => need.entity ? [normalizeLookup(need.entity)] : []);
|
|
100
|
+
for (const candidate of input.candidates) {
|
|
101
|
+
if (input.candidates.length + additions.length >= input.limit)
|
|
102
|
+
break;
|
|
103
|
+
const chunk = chunks.get(candidate.id);
|
|
104
|
+
if (!chunk?.page_id)
|
|
105
|
+
continue;
|
|
106
|
+
const neighbors = input.chunks.filter(neighbor => neighbor.page_id === chunk.page_id
|
|
107
|
+
&& Math.abs(neighbor.chunk_index - chunk.chunk_index) === 1
|
|
108
|
+
&& !selected.has(neighbor.id));
|
|
109
|
+
for (const neighbor of neighbors) {
|
|
110
|
+
const symbols = parseJsonStrings(neighbor.symbols).map(normalizeLookup);
|
|
111
|
+
const entities = parseJsonStrings(neighbor.entities).map(normalizeLookup);
|
|
112
|
+
const preservesNeed = requiredSymbols.some(symbol => symbols.includes(symbol))
|
|
113
|
+
|| requiredEntities.some(entity => entities.includes(entity) || normalizeLookup(neighbor.source).includes(entity));
|
|
114
|
+
if (!preservesNeed || lexicalRelevance(input.query, neighbor) < 0.12)
|
|
115
|
+
continue;
|
|
116
|
+
const excerpt = neighbor.content.trim();
|
|
117
|
+
if (!excerpt)
|
|
118
|
+
continue;
|
|
119
|
+
selected.add(neighbor.id);
|
|
120
|
+
additions.push({
|
|
121
|
+
id: neighbor.id,
|
|
122
|
+
source: neighbor.source,
|
|
123
|
+
title: resolveKnowledgeTitle({
|
|
124
|
+
headingPath: neighbor.heading_path, documentTitle: neighbor.document_title, content: neighbor.content,
|
|
125
|
+
source: neighbor.source, url: neighbor.url,
|
|
126
|
+
}) ?? neighbor.source,
|
|
127
|
+
content: excerpt,
|
|
128
|
+
excerpt,
|
|
129
|
+
applicability: ['adjacent:necessary', `page:${neighbor.page_id}`],
|
|
130
|
+
url: neighbor.url,
|
|
131
|
+
version: neighbor.version ?? undefined,
|
|
132
|
+
embedding: [],
|
|
133
|
+
score: Math.max(0.1, (candidate.score ?? 0.4) * 0.7),
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return [...input.candidates, ...additions]
|
|
139
|
+
.sort((left, right) => (right.scoreDetails?.finalScore ?? right.score ?? 0)
|
|
140
|
+
- (left.scoreDetails?.finalScore ?? left.score ?? 0))
|
|
141
|
+
.slice(0, input.limit);
|
|
142
|
+
}
|
|
143
|
+
function parseJsonStrings(value) {
|
|
144
|
+
if (!value)
|
|
145
|
+
return [];
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(value);
|
|
148
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : [];
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function normalizeLookup(value) {
|
|
155
|
+
return value.normalize('NFKC').trim().toLocaleLowerCase('en-US');
|
|
156
|
+
}
|
|
19
157
|
export class KnowledgeEngine {
|
|
20
158
|
projectRoot;
|
|
21
159
|
hybridSearch;
|
|
22
160
|
store;
|
|
23
161
|
embedder;
|
|
162
|
+
compiler;
|
|
163
|
+
catalog;
|
|
24
164
|
compatibilityRejectionCount = 0;
|
|
25
165
|
lowScoreRejectionCount = 0;
|
|
26
166
|
constructor(opts) {
|
|
27
167
|
this.projectRoot = opts.projectRoot;
|
|
28
168
|
this.embedder = opts.embedder ?? new EmbeddingProvider();
|
|
29
169
|
this.store = new KnowledgeStore(join(this.knowledgeDir, 'knowledge.db'));
|
|
170
|
+
this.compiler = new KnowledgeCompiler(this.store);
|
|
171
|
+
this.catalog = new SourceCatalog(this.store, this.projectRoot);
|
|
172
|
+
this.catalog.refreshProjectProfile();
|
|
30
173
|
this.hybridSearch = new KnowledgeHybridSearch(this.store, this.embedder);
|
|
31
174
|
for (const suffix of ['', '-wal', '-shm']) {
|
|
32
175
|
rmSync(join(this.knowledgeDir, `vectors.db${suffix}`), { force: true });
|
|
@@ -42,6 +185,20 @@ export class KnowledgeEngine {
|
|
|
42
185
|
const dir = this.pluginDir(source.name);
|
|
43
186
|
mkdirSync(dir, { recursive: true });
|
|
44
187
|
const manifestPath = join(dir, "manifest.json");
|
|
188
|
+
const sourceKind = source.pluginName === 'builtin' ? 'official' : 'plugin';
|
|
189
|
+
const sourceSchemaId = `schema:${source.name}:v1`;
|
|
190
|
+
this.catalog.registerSourceSchema({
|
|
191
|
+
id: sourceSchemaId,
|
|
192
|
+
sourceId: source.name,
|
|
193
|
+
sourceKind,
|
|
194
|
+
authority: sourceKind === 'official' ? 'official' : 'maintainer',
|
|
195
|
+
versionPolicy: source.version && source.version !== 'current'
|
|
196
|
+
? { mode: 'exact_major', expectedVersion: source.version }
|
|
197
|
+
: { mode: 'unversioned' },
|
|
198
|
+
allowedEntities: [source.name],
|
|
199
|
+
supportedTaskKinds: ['overview', 'create', 'modify', 'debug', 'test', 'build', 'migrate', 'explain'],
|
|
200
|
+
refreshStrategy: 'on_demand',
|
|
201
|
+
});
|
|
45
202
|
if (existsSync(manifestPath)) {
|
|
46
203
|
const existing = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
47
204
|
// Merge URLs — don't overwrite existing URLs
|
|
@@ -55,6 +212,8 @@ export class KnowledgeEngine {
|
|
|
55
212
|
sourceUrl: existing.sourceUrl,
|
|
56
213
|
renderJs: source.renderJs ?? existing.renderJs ?? false,
|
|
57
214
|
chunkPolicyVersion: existing.chunkPolicyVersion ?? 0,
|
|
215
|
+
sourceSchemaId,
|
|
216
|
+
sourceKind,
|
|
58
217
|
}, null, 2), "utf-8");
|
|
59
218
|
}
|
|
60
219
|
else {
|
|
@@ -66,6 +225,8 @@ export class KnowledgeEngine {
|
|
|
66
225
|
version: source.version,
|
|
67
226
|
renderJs: source.renderJs ?? false,
|
|
68
227
|
chunkPolicyVersion: KNOWLEDGE_CHUNK_POLICY_VERSION,
|
|
228
|
+
sourceSchemaId,
|
|
229
|
+
sourceKind,
|
|
69
230
|
}, null, 2), "utf-8");
|
|
70
231
|
}
|
|
71
232
|
}
|
|
@@ -129,29 +290,19 @@ export class KnowledgeEngine {
|
|
|
129
290
|
: [{ ...c, title: resolveKnowledgeTitle({ headingPath: c.headingPath, documentTitle: c.title, content: c.content, source: name, url: c.path ?? c.sourceUrl }) }];
|
|
130
291
|
}).filter(c => Boolean(c.title));
|
|
131
292
|
if (totalResult.length > 0) {
|
|
132
|
-
|
|
133
|
-
const docId = `${name}:github-docs`;
|
|
134
|
-
const fullContent = totalResult.map(c => c.content).join('\n\n');
|
|
135
|
-
const contentHash = createHash('sha256').update(fullContent).digest('hex');
|
|
293
|
+
const fullContent = totalResult.map(c => `# ${c.title ?? name}\n\n${c.content}`).join('\n\n');
|
|
136
294
|
const ghPath = Array.isArray(sourceConfig.githubDocsPath) ? sourceConfig.githubDocsPath[0] : sourceConfig.githubDocsPath;
|
|
137
|
-
|
|
138
|
-
id: docId, source: name, url: `https://github.com/${sourceConfig.githubRepo}/tree/main/${ghPath}`,
|
|
139
|
-
title: `${name} docs (GitHub)`,
|
|
140
|
-
version: sourceConfig.version,
|
|
141
|
-
contentHash,
|
|
142
|
-
};
|
|
143
|
-
const sqliteChunks = totalResult.map((c, i) => {
|
|
144
|
-
const chunkId = `${docId}:chunk-${String(i + 1).padStart(3, '0')}`;
|
|
145
|
-
return {
|
|
146
|
-
id: chunkId, docId, index: i,
|
|
147
|
-
headingPath: c.headingPath ?? c.title ?? name,
|
|
148
|
-
content: c.content,
|
|
149
|
-
codeBlocks: c.codeBlocks,
|
|
150
|
-
tokenCount: estimateKnowledgeTokens(c.content),
|
|
151
|
-
};
|
|
152
|
-
});
|
|
295
|
+
let compiled;
|
|
153
296
|
try {
|
|
154
|
-
await this.
|
|
297
|
+
compiled = await this.replaceCompiledSource({
|
|
298
|
+
source: name,
|
|
299
|
+
manifest,
|
|
300
|
+
sourceUri: `https://github.com/${sourceConfig.githubRepo}/tree/main/${ghPath}`,
|
|
301
|
+
title: `${name} docs (GitHub)`,
|
|
302
|
+
markdown: fullContent,
|
|
303
|
+
version: sourceConfig.version,
|
|
304
|
+
deferVectorBackfill: options.deferVectorBackfill,
|
|
305
|
+
});
|
|
155
306
|
this.store.deactivateDocument(`${name}:local-policy-v${KNOWLEDGE_CHUNK_POLICY_VERSION}`);
|
|
156
307
|
}
|
|
157
308
|
catch (error) {
|
|
@@ -179,10 +330,10 @@ export class KnowledgeEngine {
|
|
|
179
330
|
...manifest,
|
|
180
331
|
urls: manifest.urls,
|
|
181
332
|
fetchedAt: Date.now(),
|
|
182
|
-
chunkCount:
|
|
333
|
+
chunkCount: compiled.chunks.length,
|
|
183
334
|
chunkPolicyVersion: KNOWLEDGE_CHUNK_POLICY_VERSION,
|
|
184
335
|
}, null, 2), "utf-8");
|
|
185
|
-
return [{ source: name, added:
|
|
336
|
+
return [{ source: name, added: compiled.chunks.length, updated: 0, errors: [] }];
|
|
186
337
|
}
|
|
187
338
|
// L1+L2 empty — fall through to L3 HTML
|
|
188
339
|
}
|
|
@@ -194,14 +345,19 @@ export class KnowledgeEngine {
|
|
|
194
345
|
const errors = [];
|
|
195
346
|
let added = 0;
|
|
196
347
|
try {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
:
|
|
200
|
-
|
|
201
|
-
|
|
348
|
+
const raw = await fetchHtmlKnowledgeSource(url, { renderJs: manifest.renderJs });
|
|
349
|
+
const compiled = await this.replaceCompiledSource({
|
|
350
|
+
source: name,
|
|
351
|
+
manifest,
|
|
352
|
+
sourceUri: raw.sourceUri,
|
|
353
|
+
markdown: raw.markdown,
|
|
354
|
+
version: manifest.version,
|
|
355
|
+
deferVectorBackfill: options.deferVectorBackfill,
|
|
356
|
+
});
|
|
357
|
+
const chunks = compiled.chunks;
|
|
202
358
|
const chunkFiles = chunks.map((chunk) => ({
|
|
203
359
|
id: `chunk-${String(nextChunkIndex++).padStart(3, "0")}`,
|
|
204
|
-
title: chunk.title,
|
|
360
|
+
title: chunk.headingBreadcrumbs.at(-1) ?? compiled.page.title,
|
|
205
361
|
section: new URL(url).pathname,
|
|
206
362
|
content: chunk.content,
|
|
207
363
|
sourceUrl: url,
|
|
@@ -210,26 +366,6 @@ export class KnowledgeEngine {
|
|
|
210
366
|
for (const cf of chunkFiles) {
|
|
211
367
|
writeFileSync(join(dir, `${cf.id}.json`), JSON.stringify(cf, null, 2), "utf-8");
|
|
212
368
|
}
|
|
213
|
-
// Also populate SQLite store
|
|
214
|
-
const docId = `${name}:${new URL(url).pathname}`;
|
|
215
|
-
const contentHash = createHash('sha256').update(markdown).digest('hex');
|
|
216
|
-
const doc = {
|
|
217
|
-
id: docId, source: name, url,
|
|
218
|
-
title: chunks[0]?.title ?? name,
|
|
219
|
-
version: manifest.version,
|
|
220
|
-
contentHash,
|
|
221
|
-
};
|
|
222
|
-
const sqliteChunks = chunks.map((c, i) => {
|
|
223
|
-
const chunkId = `${docId}:chunk-${String(i + 1).padStart(3, '0')}`;
|
|
224
|
-
return {
|
|
225
|
-
id: chunkId, docId, index: i,
|
|
226
|
-
headingPath: c.headingPath ?? c.title,
|
|
227
|
-
content: c.content,
|
|
228
|
-
codeBlocks: c.codeBlocks,
|
|
229
|
-
tokenCount: estimateKnowledgeTokens(c.content),
|
|
230
|
-
};
|
|
231
|
-
});
|
|
232
|
-
await this.replaceDocument(doc, sqliteChunks, options.deferVectorBackfill);
|
|
233
369
|
added = chunkFiles.length;
|
|
234
370
|
totalChunkCount += added;
|
|
235
371
|
}
|
|
@@ -384,23 +520,153 @@ export class KnowledgeEngine {
|
|
|
384
520
|
const embeddings = await this.embedder.embedBatch(texts);
|
|
385
521
|
this.store.replaceDocument({ doc, chunks, embeddings });
|
|
386
522
|
}
|
|
523
|
+
async replaceCompiledSource(input) {
|
|
524
|
+
const sourceSchemaId = input.manifest.sourceSchemaId ?? `schema:${input.source}:v1`;
|
|
525
|
+
const sourceKind = input.manifest.sourceKind ?? 'plugin';
|
|
526
|
+
if (!this.store.getSourceSchema(sourceSchemaId)) {
|
|
527
|
+
this.catalog.registerSourceSchema({
|
|
528
|
+
id: sourceSchemaId,
|
|
529
|
+
sourceId: input.source,
|
|
530
|
+
sourceKind,
|
|
531
|
+
authority: sourceKind === 'official' ? 'official' : 'maintainer',
|
|
532
|
+
versionPolicy: input.version && input.version !== 'current'
|
|
533
|
+
? { mode: 'exact_major', expectedVersion: input.version }
|
|
534
|
+
: { mode: 'unversioned' },
|
|
535
|
+
allowedEntities: [input.source],
|
|
536
|
+
supportedTaskKinds: ['overview', 'create', 'modify', 'debug', 'test', 'build', 'migrate', 'explain'],
|
|
537
|
+
refreshStrategy: 'on_demand',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const inputHash = createHash('sha256').update(input.markdown).digest('hex');
|
|
541
|
+
const refresh = this.catalog.beginRefresh(input.source, inputHash);
|
|
542
|
+
try {
|
|
543
|
+
const compiled = this.compiler.compile({
|
|
544
|
+
projectRoot: this.projectRoot,
|
|
545
|
+
sourceId: input.source,
|
|
546
|
+
sourceSchemaId,
|
|
547
|
+
sourceKind,
|
|
548
|
+
sourceUri: input.sourceUri,
|
|
549
|
+
markdown: input.markdown,
|
|
550
|
+
title: input.title,
|
|
551
|
+
version: input.version,
|
|
552
|
+
});
|
|
553
|
+
let embeddings = [];
|
|
554
|
+
if (!input.deferVectorBackfill) {
|
|
555
|
+
const warmed = await this.embedder.warmup();
|
|
556
|
+
if (!warmed) {
|
|
557
|
+
throw this.embedder.getLastFailure?.()
|
|
558
|
+
?? new Error(this.embedder.getLastError() ?? 'Knowledge embedding warmup failed');
|
|
559
|
+
}
|
|
560
|
+
embeddings = await this.embedder.embedBatch(compiled.chunks.map(chunk => chunk.embeddingText));
|
|
561
|
+
if (embeddings.length !== compiled.chunks.length) {
|
|
562
|
+
throw new Error(`Knowledge embedding count mismatch: ${embeddings.length}/${compiled.chunks.length}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
this.store.replaceCompiledPage(compiled.page, compiled.chunks);
|
|
566
|
+
if (embeddings.length > 0) {
|
|
567
|
+
this.store.insertEmbeddingBatch(compiled.chunks.map((chunk, index) => ({
|
|
568
|
+
id: chunk.id,
|
|
569
|
+
embedding: embeddings[index],
|
|
570
|
+
})));
|
|
571
|
+
}
|
|
572
|
+
this.store.deactivateSourceDocuments(input.source, compiled.page.id);
|
|
573
|
+
const vectors = embeddings.length;
|
|
574
|
+
const coverage = compiled.chunks.length > 0 ? vectors / compiled.chunks.length : 0;
|
|
575
|
+
this.catalog.activateSource({
|
|
576
|
+
schemaId: sourceSchemaId,
|
|
577
|
+
version: input.version === 'current' ? undefined : input.version,
|
|
578
|
+
contentHash: compiled.page.contentHash,
|
|
579
|
+
readiness: coverage >= 0.95
|
|
580
|
+
? {
|
|
581
|
+
status: 'ready', activeContentHash: compiled.page.contentHash,
|
|
582
|
+
documents: 1, chunks: compiled.chunks.length, vectors, coverage,
|
|
583
|
+
lastSuccessfulRefresh: Date.now(), lastAttempt: Date.now(),
|
|
584
|
+
}
|
|
585
|
+
: {
|
|
586
|
+
status: 'degraded', activeContentHash: compiled.page.contentHash,
|
|
587
|
+
documents: 1, chunks: compiled.chunks.length, vectors, coverage,
|
|
588
|
+
lastAttempt: Date.now(),
|
|
589
|
+
degradation: {
|
|
590
|
+
reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
|
|
591
|
+
previousHealthyRevisionRetained: false,
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
entities: [input.source, ...compiled.page.entities],
|
|
595
|
+
symbols: compiled.page.symbols,
|
|
596
|
+
refreshJobId: refresh.id,
|
|
597
|
+
});
|
|
598
|
+
return compiled;
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
this.catalog.failRefresh(refresh.id, error);
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
compileDependency(packageName) {
|
|
606
|
+
return compileDependencySource({
|
|
607
|
+
projectRoot: this.projectRoot,
|
|
608
|
+
packageName,
|
|
609
|
+
store: this.store,
|
|
610
|
+
deferVectorBackfill: true,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async getProjectProfile() {
|
|
614
|
+
return this.store.getProjectProfile(this.projectRoot) ?? this.catalog.refreshProjectProfile();
|
|
615
|
+
}
|
|
616
|
+
async getSourceCatalog() {
|
|
617
|
+
return this.catalog.getCatalog();
|
|
618
|
+
}
|
|
619
|
+
async getCoverageGaps() {
|
|
620
|
+
return this.store.listCoverageGaps(this.projectRoot);
|
|
621
|
+
}
|
|
387
622
|
async search(query, options) {
|
|
388
623
|
return (await this.searchWithStatus(query, options)).entries;
|
|
389
624
|
}
|
|
390
625
|
async searchWithStatus(query, options) {
|
|
391
626
|
const limit = options?.limit ?? 10;
|
|
392
|
-
const
|
|
393
|
-
const
|
|
627
|
+
const activeSkill = options?.activeSkill ?? options?.projectSignals?.activeSkill;
|
|
628
|
+
const taskAware = Boolean(options?.action || options?.knowledgeNeeds?.length);
|
|
629
|
+
const projectProfile = this.catalog.refreshProjectProfile({
|
|
630
|
+
activeSkills: activeSkill ? [normalizeActiveSkill(activeSkill)] : [],
|
|
631
|
+
});
|
|
632
|
+
const selectionInput = () => ({
|
|
394
633
|
query,
|
|
395
|
-
availableSources,
|
|
634
|
+
availableSources: this.store.getDistinctSources(),
|
|
396
635
|
explicitSources: options?.sources,
|
|
397
636
|
projectRoot: this.projectRoot,
|
|
398
637
|
projectSignals: {
|
|
399
638
|
...options?.projectSignals,
|
|
400
|
-
activeSkill
|
|
639
|
+
activeSkill,
|
|
401
640
|
relevantFiles: options?.relevantFiles ?? options?.projectSignals?.relevantFiles,
|
|
402
641
|
},
|
|
642
|
+
projectProfile: taskAware ? projectProfile : null,
|
|
643
|
+
catalog: taskAware ? this.catalog.getCatalog() : null,
|
|
644
|
+
action: options?.action,
|
|
645
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
403
646
|
});
|
|
647
|
+
let selection = selectKnowledgeSources(selectionInput());
|
|
648
|
+
const compileFailures = [];
|
|
649
|
+
for (const packageName of taskAware
|
|
650
|
+
? dependencyPackagesForMissingNeeds(selection.missingNeeds, projectProfile.dependencies)
|
|
651
|
+
: []) {
|
|
652
|
+
try {
|
|
653
|
+
this.compileDependency(packageName);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
657
|
+
this.catalog.recordCoverageGap({
|
|
658
|
+
sourceId: `dependency:${packageName}`,
|
|
659
|
+
kind: 'compile_failure',
|
|
660
|
+
entity: packageName,
|
|
661
|
+
reason,
|
|
662
|
+
});
|
|
663
|
+
compileFailures.push({ sourceId: `dependency:${packageName}`, kind: 'compile_failure', entity: packageName, reason });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (selection.missingNeeds.length > 0)
|
|
667
|
+
selection = selectKnowledgeSources(selectionInput());
|
|
668
|
+
for (const gap of selection.coverageGaps)
|
|
669
|
+
this.catalog.recordCoverageGap(gap);
|
|
404
670
|
this.compatibilityRejectionCount += selection.rejectedSources.length;
|
|
405
671
|
const sources = selection.sources;
|
|
406
672
|
const budgetTokens = Math.max(0, options?.budgetTokens ?? DEFAULT_KNOWLEDGE_TOKEN_BUDGET);
|
|
@@ -409,17 +675,39 @@ export class KnowledgeEngine {
|
|
|
409
675
|
const readinessWarnings = this.store.getSourceReadiness()
|
|
410
676
|
.filter(row => sources.includes(row.source) && row.status !== 'ready')
|
|
411
677
|
.map(row => `knowledge-source-degraded:${row.source}:${row.failure_category ?? 'unknown'}`);
|
|
678
|
+
const coverageWarnings = selection.coverageGaps.length > 0 ? ['knowledge_source_missing'] : [];
|
|
679
|
+
if (sources.length === 0) {
|
|
680
|
+
return {
|
|
681
|
+
entries: [],
|
|
682
|
+
warnings: [...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'],
|
|
683
|
+
budgetTokens,
|
|
684
|
+
tokenCount: 0,
|
|
685
|
+
truncatedCount: 0,
|
|
686
|
+
rejectedIncompatibleSources: selection.rejectedSources.length,
|
|
687
|
+
retrievalDiagnostics: {
|
|
688
|
+
selectedSources: [],
|
|
689
|
+
sourceSignals: selection.signals,
|
|
690
|
+
sourceRejections: selection.rejections,
|
|
691
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
692
|
+
compatibleChunks: 0,
|
|
693
|
+
hybridCandidates: 0,
|
|
694
|
+
qualityRejected: {},
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
412
698
|
const allChunks = this.store.listChunks({ sources });
|
|
413
699
|
const chunks = allChunks.filter(chunk => chunk.chunk_policy_version === KNOWLEDGE_CHUNK_POLICY_VERSION
|
|
414
700
|
&& isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions));
|
|
415
701
|
const rejectedByQualityGate = allChunks.length - chunks.length;
|
|
416
702
|
if (chunks.length === 0) {
|
|
417
703
|
return {
|
|
418
|
-
entries: [], warnings: [...readinessWarnings, 'knowledge_insufficient'], budgetTokens, tokenCount: 0, truncatedCount: 0,
|
|
704
|
+
entries: [], warnings: [...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'], budgetTokens, tokenCount: 0, truncatedCount: 0,
|
|
419
705
|
rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
|
|
420
706
|
retrievalDiagnostics: {
|
|
421
707
|
selectedSources: sources,
|
|
422
708
|
sourceSignals: selection.signals,
|
|
709
|
+
sourceRejections: selection.rejections,
|
|
710
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
423
711
|
compatibleChunks: 0,
|
|
424
712
|
hybridCandidates: 0,
|
|
425
713
|
qualityRejected: {},
|
|
@@ -445,12 +733,14 @@ export class KnowledgeEngine {
|
|
|
445
733
|
const packed = this.packEntries(candidates, chunks, budgetTokens);
|
|
446
734
|
return {
|
|
447
735
|
...packed,
|
|
448
|
-
warnings: readinessWarnings,
|
|
736
|
+
warnings: [...coverageWarnings, ...readinessWarnings],
|
|
449
737
|
budgetTokens,
|
|
450
738
|
rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
|
|
451
739
|
retrievalDiagnostics: {
|
|
452
740
|
selectedSources: sources,
|
|
453
741
|
sourceSignals: selection.signals,
|
|
742
|
+
sourceRejections: selection.rejections,
|
|
743
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
454
744
|
compatibleChunks: chunks.length,
|
|
455
745
|
hybridCandidates: candidates.length,
|
|
456
746
|
qualityRejected: {},
|
|
@@ -462,19 +752,32 @@ export class KnowledgeEngine {
|
|
|
462
752
|
bm25Results = this.store.searchBM25(query, limit * 3, sources);
|
|
463
753
|
}
|
|
464
754
|
catch {
|
|
465
|
-
//
|
|
755
|
+
// Retry below with a bounded literal-term query.
|
|
466
756
|
}
|
|
467
|
-
|
|
757
|
+
if (bm25Results.length === 0) {
|
|
758
|
+
const safeQuery = buildSafeFtsQuery(query);
|
|
759
|
+
if (safeQuery) {
|
|
760
|
+
try {
|
|
761
|
+
bm25Results = this.store.searchBM25(safeQuery, limit * 3, sources);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
// Vector retrieval can still answer when FTS cannot parse any bounded fallback.
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const hybrid = await this.hybridSearch.searchWithStatus(query, chunks, bm25Results, limit, sources, options?.minScore, options?.knowledgeNeeds);
|
|
468
769
|
this.lowScoreRejectionCount += hybrid.rejectedLowScore;
|
|
469
770
|
const chunkMap = new Map(chunks.map(chunk => [chunk.id, chunk]));
|
|
470
771
|
const qualityRejected = {};
|
|
471
|
-
const
|
|
772
|
+
const seedResults = hybrid.results.flatMap(result => {
|
|
472
773
|
const chunk = chunkMap.get(result.id);
|
|
473
774
|
const quality = evaluateKnowledgeQuality({
|
|
474
775
|
query,
|
|
475
776
|
chunk,
|
|
476
777
|
retrievalScore: result.score,
|
|
477
778
|
versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions),
|
|
779
|
+
action: options?.action,
|
|
780
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
478
781
|
});
|
|
479
782
|
if (!quality.eligible) {
|
|
480
783
|
const reason = quality.reason ?? 'unknown';
|
|
@@ -493,40 +796,119 @@ export class KnowledgeEngine {
|
|
|
493
796
|
version: chunk.version ?? undefined,
|
|
494
797
|
embedding: [],
|
|
495
798
|
score: Math.round(quality.score * 10000) / 10000,
|
|
799
|
+
scoreDetails: {
|
|
800
|
+
...result.scoreDetails,
|
|
801
|
+
qualityScore: Math.round(quality.score * 10000) / 10000,
|
|
802
|
+
finalScore: Math.round((0.65 * result.scoreDetails.finalScore + 0.35 * quality.score) * 10000) / 10000,
|
|
803
|
+
versionCompatible: true,
|
|
804
|
+
},
|
|
496
805
|
}];
|
|
497
806
|
});
|
|
807
|
+
const graphExpansions = expandKnowledgeEvidenceGraph({
|
|
808
|
+
query,
|
|
809
|
+
seeds: seedResults.map(result => ({ id: result.id, score: result.scoreDetails?.finalScore ?? result.score ?? 0 })),
|
|
810
|
+
chunks,
|
|
811
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
812
|
+
limit: Math.max(0, limit - Math.min(limit, seedResults.length)),
|
|
813
|
+
});
|
|
814
|
+
const graphCandidates = graphExpansions.flatMap(expansion => {
|
|
815
|
+
const chunk = chunkMap.get(expansion.id);
|
|
816
|
+
if (!chunk)
|
|
817
|
+
return [];
|
|
818
|
+
const quality = evaluateKnowledgeQuality({
|
|
819
|
+
query,
|
|
820
|
+
chunk,
|
|
821
|
+
retrievalScore: expansion.score,
|
|
822
|
+
versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions),
|
|
823
|
+
action: options?.action,
|
|
824
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
825
|
+
});
|
|
826
|
+
if (!quality.eligible)
|
|
827
|
+
return [];
|
|
828
|
+
return [{
|
|
829
|
+
id: chunk.id,
|
|
830
|
+
source: chunk.source,
|
|
831
|
+
title: resolveKnowledgeTitle({
|
|
832
|
+
headingPath: chunk.heading_path, documentTitle: chunk.document_title, content: chunk.content,
|
|
833
|
+
source: chunk.source, url: chunk.url,
|
|
834
|
+
}) ?? chunk.source,
|
|
835
|
+
content: quality.excerpt,
|
|
836
|
+
excerpt: quality.excerpt,
|
|
837
|
+
matchingTerms: quality.matchingTerms,
|
|
838
|
+
applicability: [...quality.applicability, `graph:${expansion.relation}`],
|
|
839
|
+
url: chunk.url,
|
|
840
|
+
version: chunk.version ?? undefined,
|
|
841
|
+
embedding: [],
|
|
842
|
+
score: expansion.score,
|
|
843
|
+
scoreDetails: {
|
|
844
|
+
profileVersion: KNOWLEDGE_RETRIEVAL_SCORING_PROFILE.version,
|
|
845
|
+
channelRanks: {},
|
|
846
|
+
rawScore: expansion.score, normalizedScore: expansion.score, pageScore: expansion.score,
|
|
847
|
+
qualityScore: quality.score, finalScore: expansion.score,
|
|
848
|
+
authority: authorityFromProvenance(chunk.provenance, chunk.source),
|
|
849
|
+
versionCompatible: true, provenanceComplete: provenanceComplete(chunk.provenance),
|
|
850
|
+
graphRelation: {
|
|
851
|
+
seedId: expansion.seedId, type: expansion.relation, evidence: expansion.relationEvidence,
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
}];
|
|
855
|
+
});
|
|
856
|
+
const rankedCandidates = dedupeKnowledgeEntries([...seedResults, ...graphCandidates])
|
|
857
|
+
.sort((left, right) => (right.scoreDetails?.finalScore ?? right.score ?? 0)
|
|
858
|
+
- (left.scoreDetails?.finalScore ?? left.score ?? 0));
|
|
859
|
+
const candidates = addNecessaryAdjacentEntries({
|
|
860
|
+
candidates: rankedCandidates, chunks, query, knowledgeNeeds: options?.knowledgeNeeds ?? [], limit,
|
|
861
|
+
});
|
|
498
862
|
const packed = this.packEntries(candidates, chunks, budgetTokens);
|
|
499
863
|
return {
|
|
500
864
|
...packed,
|
|
501
|
-
warnings: [...hybrid.warnings, ...readinessWarnings, ...(candidates.length === 0 ? ['knowledge_insufficient'] : [])],
|
|
865
|
+
warnings: [...hybrid.warnings, ...coverageWarnings, ...readinessWarnings, ...(candidates.length === 0 ? ['knowledge_insufficient'] : [])],
|
|
502
866
|
budgetTokens,
|
|
503
867
|
rejectedLowScore: hybrid.rejectedLowScore,
|
|
504
868
|
rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
|
|
505
869
|
retrievalDiagnostics: {
|
|
506
870
|
selectedSources: sources,
|
|
507
871
|
sourceSignals: selection.signals,
|
|
872
|
+
sourceRejections: selection.rejections,
|
|
873
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
508
874
|
compatibleChunks: chunks.length,
|
|
509
875
|
hybridCandidates: hybrid.results.length,
|
|
510
876
|
qualityRejected,
|
|
877
|
+
channelCandidateCounts: hybrid.channelCandidateCounts,
|
|
878
|
+
graphExpansions: graphCandidates.length,
|
|
511
879
|
},
|
|
512
880
|
};
|
|
513
881
|
}
|
|
514
882
|
packEntries(candidates, chunks, budgetTokens) {
|
|
515
883
|
const chunkMap = new Map(chunks.map(chunk => [chunk.id, chunk]));
|
|
516
884
|
const entries = [];
|
|
885
|
+
const sourceTokens = new Map();
|
|
886
|
+
const pageTokens = new Map();
|
|
517
887
|
let tokenCount = 0;
|
|
518
888
|
let truncatedCount = 0;
|
|
519
889
|
for (const entry of candidates) {
|
|
890
|
+
const chunk = chunkMap.get(entry.id);
|
|
520
891
|
const entryTokens = (entry.excerpt
|
|
521
892
|
? estimateKnowledgeTokens(entry.content)
|
|
522
|
-
:
|
|
893
|
+
: chunk?.token_count ?? estimateKnowledgeTokens(entry.content))
|
|
523
894
|
+ estimateKnowledgeTokens(entry.title);
|
|
524
|
-
|
|
895
|
+
const structuredPage = chunk?.page_id ?? null;
|
|
896
|
+
const source = chunk?.source ?? entry.source;
|
|
897
|
+
const sourceLimit = Math.max(entryTokens, Math.floor(budgetTokens * 0.60));
|
|
898
|
+
const pageLimit = Math.max(entryTokens, Math.floor(budgetTokens * 0.40));
|
|
899
|
+
const overSourceBudget = Boolean(structuredPage)
|
|
900
|
+
&& (sourceTokens.get(source) ?? 0) + entryTokens > sourceLimit;
|
|
901
|
+
const overPageBudget = Boolean(structuredPage)
|
|
902
|
+
&& (pageTokens.get(structuredPage) ?? 0) + entryTokens > pageLimit;
|
|
903
|
+
if (tokenCount + entryTokens > budgetTokens || overSourceBudget || overPageBudget) {
|
|
525
904
|
truncatedCount++;
|
|
526
905
|
continue;
|
|
527
906
|
}
|
|
528
907
|
entries.push(entry);
|
|
529
908
|
tokenCount += entryTokens;
|
|
909
|
+
sourceTokens.set(source, (sourceTokens.get(source) ?? 0) + entryTokens);
|
|
910
|
+
if (structuredPage)
|
|
911
|
+
pageTokens.set(structuredPage, (pageTokens.get(structuredPage) ?? 0) + entryTokens);
|
|
530
912
|
}
|
|
531
913
|
return { entries, tokenCount, truncatedCount };
|
|
532
914
|
}
|