@devflow-tools/knowledge-engine 0.16.27 → 0.16.29
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 +499 -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
|
}
|
|
@@ -331,6 +467,29 @@ export class KnowledgeEngine {
|
|
|
331
467
|
}));
|
|
332
468
|
this.store.replaceDocumentChunks({ doc, chunks });
|
|
333
469
|
const documentsRetired = this.store.deactivateSourceDocuments(name, documentId);
|
|
470
|
+
const vectors = this.store.countEmbeddings(name);
|
|
471
|
+
const coverage = chunks.length > 0 ? vectors / chunks.length : 0;
|
|
472
|
+
this.catalog.activateSource({
|
|
473
|
+
schemaId: manifest.sourceSchemaId ?? `schema:${name}:v1`,
|
|
474
|
+
version,
|
|
475
|
+
contentHash: doc.contentHash,
|
|
476
|
+
readiness: coverage >= 0.95
|
|
477
|
+
? {
|
|
478
|
+
status: 'ready', activeContentHash: doc.contentHash,
|
|
479
|
+
documents: 1, chunks: chunks.length, vectors, coverage,
|
|
480
|
+
lastSuccessfulRefresh: Date.now(), lastAttempt: Date.now(),
|
|
481
|
+
}
|
|
482
|
+
: {
|
|
483
|
+
status: 'degraded', activeContentHash: doc.contentHash,
|
|
484
|
+
documents: 1, chunks: chunks.length, vectors, coverage,
|
|
485
|
+
lastAttempt: Date.now(),
|
|
486
|
+
degradation: {
|
|
487
|
+
reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
|
|
488
|
+
previousHealthyRevisionRetained: false,
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
entities: [name],
|
|
492
|
+
});
|
|
334
493
|
this.clearChunkFiles(dir);
|
|
335
494
|
for (const [index, chunk] of chunks.entries()) {
|
|
336
495
|
const file = {
|
|
@@ -384,23 +543,153 @@ export class KnowledgeEngine {
|
|
|
384
543
|
const embeddings = await this.embedder.embedBatch(texts);
|
|
385
544
|
this.store.replaceDocument({ doc, chunks, embeddings });
|
|
386
545
|
}
|
|
546
|
+
async replaceCompiledSource(input) {
|
|
547
|
+
const sourceSchemaId = input.manifest.sourceSchemaId ?? `schema:${input.source}:v1`;
|
|
548
|
+
const sourceKind = input.manifest.sourceKind ?? 'plugin';
|
|
549
|
+
if (!this.store.getSourceSchema(sourceSchemaId)) {
|
|
550
|
+
this.catalog.registerSourceSchema({
|
|
551
|
+
id: sourceSchemaId,
|
|
552
|
+
sourceId: input.source,
|
|
553
|
+
sourceKind,
|
|
554
|
+
authority: sourceKind === 'official' ? 'official' : 'maintainer',
|
|
555
|
+
versionPolicy: input.version && input.version !== 'current'
|
|
556
|
+
? { mode: 'exact_major', expectedVersion: input.version }
|
|
557
|
+
: { mode: 'unversioned' },
|
|
558
|
+
allowedEntities: [input.source],
|
|
559
|
+
supportedTaskKinds: ['overview', 'create', 'modify', 'debug', 'test', 'build', 'migrate', 'explain'],
|
|
560
|
+
refreshStrategy: 'on_demand',
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const inputHash = createHash('sha256').update(input.markdown).digest('hex');
|
|
564
|
+
const refresh = this.catalog.beginRefresh(input.source, inputHash);
|
|
565
|
+
try {
|
|
566
|
+
const compiled = this.compiler.compile({
|
|
567
|
+
projectRoot: this.projectRoot,
|
|
568
|
+
sourceId: input.source,
|
|
569
|
+
sourceSchemaId,
|
|
570
|
+
sourceKind,
|
|
571
|
+
sourceUri: input.sourceUri,
|
|
572
|
+
markdown: input.markdown,
|
|
573
|
+
title: input.title,
|
|
574
|
+
version: input.version,
|
|
575
|
+
});
|
|
576
|
+
let embeddings = [];
|
|
577
|
+
if (!input.deferVectorBackfill) {
|
|
578
|
+
const warmed = await this.embedder.warmup();
|
|
579
|
+
if (!warmed) {
|
|
580
|
+
throw this.embedder.getLastFailure?.()
|
|
581
|
+
?? new Error(this.embedder.getLastError() ?? 'Knowledge embedding warmup failed');
|
|
582
|
+
}
|
|
583
|
+
embeddings = await this.embedder.embedBatch(compiled.chunks.map(chunk => chunk.embeddingText));
|
|
584
|
+
if (embeddings.length !== compiled.chunks.length) {
|
|
585
|
+
throw new Error(`Knowledge embedding count mismatch: ${embeddings.length}/${compiled.chunks.length}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
this.store.replaceCompiledPage(compiled.page, compiled.chunks);
|
|
589
|
+
if (embeddings.length > 0) {
|
|
590
|
+
this.store.insertEmbeddingBatch(compiled.chunks.map((chunk, index) => ({
|
|
591
|
+
id: chunk.id,
|
|
592
|
+
embedding: embeddings[index],
|
|
593
|
+
})));
|
|
594
|
+
}
|
|
595
|
+
this.store.deactivateSourceDocuments(input.source, compiled.page.id);
|
|
596
|
+
const vectors = embeddings.length;
|
|
597
|
+
const coverage = compiled.chunks.length > 0 ? vectors / compiled.chunks.length : 0;
|
|
598
|
+
this.catalog.activateSource({
|
|
599
|
+
schemaId: sourceSchemaId,
|
|
600
|
+
version: input.version === 'current' ? undefined : input.version,
|
|
601
|
+
contentHash: compiled.page.contentHash,
|
|
602
|
+
readiness: coverage >= 0.95
|
|
603
|
+
? {
|
|
604
|
+
status: 'ready', activeContentHash: compiled.page.contentHash,
|
|
605
|
+
documents: 1, chunks: compiled.chunks.length, vectors, coverage,
|
|
606
|
+
lastSuccessfulRefresh: Date.now(), lastAttempt: Date.now(),
|
|
607
|
+
}
|
|
608
|
+
: {
|
|
609
|
+
status: 'degraded', activeContentHash: compiled.page.contentHash,
|
|
610
|
+
documents: 1, chunks: compiled.chunks.length, vectors, coverage,
|
|
611
|
+
lastAttempt: Date.now(),
|
|
612
|
+
degradation: {
|
|
613
|
+
reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
|
|
614
|
+
previousHealthyRevisionRetained: false,
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
entities: [input.source, ...compiled.page.entities],
|
|
618
|
+
symbols: compiled.page.symbols,
|
|
619
|
+
refreshJobId: refresh.id,
|
|
620
|
+
});
|
|
621
|
+
return compiled;
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
this.catalog.failRefresh(refresh.id, error);
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
compileDependency(packageName) {
|
|
629
|
+
return compileDependencySource({
|
|
630
|
+
projectRoot: this.projectRoot,
|
|
631
|
+
packageName,
|
|
632
|
+
store: this.store,
|
|
633
|
+
deferVectorBackfill: true,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
async getProjectProfile() {
|
|
637
|
+
return this.store.getProjectProfile(this.projectRoot) ?? this.catalog.refreshProjectProfile();
|
|
638
|
+
}
|
|
639
|
+
async getSourceCatalog() {
|
|
640
|
+
return this.catalog.getCatalog();
|
|
641
|
+
}
|
|
642
|
+
async getCoverageGaps() {
|
|
643
|
+
return this.store.listCoverageGaps(this.projectRoot);
|
|
644
|
+
}
|
|
387
645
|
async search(query, options) {
|
|
388
646
|
return (await this.searchWithStatus(query, options)).entries;
|
|
389
647
|
}
|
|
390
648
|
async searchWithStatus(query, options) {
|
|
391
649
|
const limit = options?.limit ?? 10;
|
|
392
|
-
const
|
|
393
|
-
const
|
|
650
|
+
const activeSkill = options?.activeSkill ?? options?.projectSignals?.activeSkill;
|
|
651
|
+
const taskAware = Boolean(options?.action || options?.knowledgeNeeds?.length);
|
|
652
|
+
const projectProfile = this.catalog.refreshProjectProfile({
|
|
653
|
+
activeSkills: activeSkill ? [normalizeActiveSkill(activeSkill)] : [],
|
|
654
|
+
});
|
|
655
|
+
const selectionInput = () => ({
|
|
394
656
|
query,
|
|
395
|
-
availableSources,
|
|
657
|
+
availableSources: this.store.getDistinctSources(),
|
|
396
658
|
explicitSources: options?.sources,
|
|
397
659
|
projectRoot: this.projectRoot,
|
|
398
660
|
projectSignals: {
|
|
399
661
|
...options?.projectSignals,
|
|
400
|
-
activeSkill
|
|
662
|
+
activeSkill,
|
|
401
663
|
relevantFiles: options?.relevantFiles ?? options?.projectSignals?.relevantFiles,
|
|
402
664
|
},
|
|
665
|
+
projectProfile: taskAware ? projectProfile : null,
|
|
666
|
+
catalog: taskAware ? this.catalog.getCatalog() : null,
|
|
667
|
+
action: options?.action,
|
|
668
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
403
669
|
});
|
|
670
|
+
let selection = selectKnowledgeSources(selectionInput());
|
|
671
|
+
const compileFailures = [];
|
|
672
|
+
for (const packageName of taskAware
|
|
673
|
+
? dependencyPackagesForMissingNeeds(selection.missingNeeds, projectProfile.dependencies)
|
|
674
|
+
: []) {
|
|
675
|
+
try {
|
|
676
|
+
this.compileDependency(packageName);
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
680
|
+
this.catalog.recordCoverageGap({
|
|
681
|
+
sourceId: `dependency:${packageName}`,
|
|
682
|
+
kind: 'compile_failure',
|
|
683
|
+
entity: packageName,
|
|
684
|
+
reason,
|
|
685
|
+
});
|
|
686
|
+
compileFailures.push({ sourceId: `dependency:${packageName}`, kind: 'compile_failure', entity: packageName, reason });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (selection.missingNeeds.length > 0)
|
|
690
|
+
selection = selectKnowledgeSources(selectionInput());
|
|
691
|
+
for (const gap of selection.coverageGaps)
|
|
692
|
+
this.catalog.recordCoverageGap(gap);
|
|
404
693
|
this.compatibilityRejectionCount += selection.rejectedSources.length;
|
|
405
694
|
const sources = selection.sources;
|
|
406
695
|
const budgetTokens = Math.max(0, options?.budgetTokens ?? DEFAULT_KNOWLEDGE_TOKEN_BUDGET);
|
|
@@ -409,17 +698,39 @@ export class KnowledgeEngine {
|
|
|
409
698
|
const readinessWarnings = this.store.getSourceReadiness()
|
|
410
699
|
.filter(row => sources.includes(row.source) && row.status !== 'ready')
|
|
411
700
|
.map(row => `knowledge-source-degraded:${row.source}:${row.failure_category ?? 'unknown'}`);
|
|
701
|
+
const coverageWarnings = selection.coverageGaps.length > 0 ? ['knowledge_source_missing'] : [];
|
|
702
|
+
if (sources.length === 0) {
|
|
703
|
+
return {
|
|
704
|
+
entries: [],
|
|
705
|
+
warnings: [...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'],
|
|
706
|
+
budgetTokens,
|
|
707
|
+
tokenCount: 0,
|
|
708
|
+
truncatedCount: 0,
|
|
709
|
+
rejectedIncompatibleSources: selection.rejectedSources.length,
|
|
710
|
+
retrievalDiagnostics: {
|
|
711
|
+
selectedSources: [],
|
|
712
|
+
sourceSignals: selection.signals,
|
|
713
|
+
sourceRejections: selection.rejections,
|
|
714
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
715
|
+
compatibleChunks: 0,
|
|
716
|
+
hybridCandidates: 0,
|
|
717
|
+
qualityRejected: {},
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
}
|
|
412
721
|
const allChunks = this.store.listChunks({ sources });
|
|
413
722
|
const chunks = allChunks.filter(chunk => chunk.chunk_policy_version === KNOWLEDGE_CHUNK_POLICY_VERSION
|
|
414
723
|
&& isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions));
|
|
415
724
|
const rejectedByQualityGate = allChunks.length - chunks.length;
|
|
416
725
|
if (chunks.length === 0) {
|
|
417
726
|
return {
|
|
418
|
-
entries: [], warnings: [...readinessWarnings, 'knowledge_insufficient'], budgetTokens, tokenCount: 0, truncatedCount: 0,
|
|
727
|
+
entries: [], warnings: [...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'], budgetTokens, tokenCount: 0, truncatedCount: 0,
|
|
419
728
|
rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
|
|
420
729
|
retrievalDiagnostics: {
|
|
421
730
|
selectedSources: sources,
|
|
422
731
|
sourceSignals: selection.signals,
|
|
732
|
+
sourceRejections: selection.rejections,
|
|
733
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
423
734
|
compatibleChunks: 0,
|
|
424
735
|
hybridCandidates: 0,
|
|
425
736
|
qualityRejected: {},
|
|
@@ -445,12 +756,14 @@ export class KnowledgeEngine {
|
|
|
445
756
|
const packed = this.packEntries(candidates, chunks, budgetTokens);
|
|
446
757
|
return {
|
|
447
758
|
...packed,
|
|
448
|
-
warnings: readinessWarnings,
|
|
759
|
+
warnings: [...coverageWarnings, ...readinessWarnings],
|
|
449
760
|
budgetTokens,
|
|
450
761
|
rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
|
|
451
762
|
retrievalDiagnostics: {
|
|
452
763
|
selectedSources: sources,
|
|
453
764
|
sourceSignals: selection.signals,
|
|
765
|
+
sourceRejections: selection.rejections,
|
|
766
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
454
767
|
compatibleChunks: chunks.length,
|
|
455
768
|
hybridCandidates: candidates.length,
|
|
456
769
|
qualityRejected: {},
|
|
@@ -462,19 +775,32 @@ export class KnowledgeEngine {
|
|
|
462
775
|
bm25Results = this.store.searchBM25(query, limit * 3, sources);
|
|
463
776
|
}
|
|
464
777
|
catch {
|
|
465
|
-
//
|
|
778
|
+
// Retry below with a bounded literal-term query.
|
|
779
|
+
}
|
|
780
|
+
if (bm25Results.length === 0) {
|
|
781
|
+
const safeQuery = buildSafeFtsQuery(query);
|
|
782
|
+
if (safeQuery) {
|
|
783
|
+
try {
|
|
784
|
+
bm25Results = this.store.searchBM25(safeQuery, limit * 3, sources);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
// Vector retrieval can still answer when FTS cannot parse any bounded fallback.
|
|
788
|
+
}
|
|
789
|
+
}
|
|
466
790
|
}
|
|
467
|
-
const hybrid = await this.hybridSearch.searchWithStatus(query, chunks, bm25Results, limit, sources, options?.minScore);
|
|
791
|
+
const hybrid = await this.hybridSearch.searchWithStatus(query, chunks, bm25Results, limit, sources, options?.minScore, options?.knowledgeNeeds);
|
|
468
792
|
this.lowScoreRejectionCount += hybrid.rejectedLowScore;
|
|
469
793
|
const chunkMap = new Map(chunks.map(chunk => [chunk.id, chunk]));
|
|
470
794
|
const qualityRejected = {};
|
|
471
|
-
const
|
|
795
|
+
const seedResults = hybrid.results.flatMap(result => {
|
|
472
796
|
const chunk = chunkMap.get(result.id);
|
|
473
797
|
const quality = evaluateKnowledgeQuality({
|
|
474
798
|
query,
|
|
475
799
|
chunk,
|
|
476
800
|
retrievalScore: result.score,
|
|
477
801
|
versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions),
|
|
802
|
+
action: options?.action,
|
|
803
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
478
804
|
});
|
|
479
805
|
if (!quality.eligible) {
|
|
480
806
|
const reason = quality.reason ?? 'unknown';
|
|
@@ -493,40 +819,119 @@ export class KnowledgeEngine {
|
|
|
493
819
|
version: chunk.version ?? undefined,
|
|
494
820
|
embedding: [],
|
|
495
821
|
score: Math.round(quality.score * 10000) / 10000,
|
|
822
|
+
scoreDetails: {
|
|
823
|
+
...result.scoreDetails,
|
|
824
|
+
qualityScore: Math.round(quality.score * 10000) / 10000,
|
|
825
|
+
finalScore: Math.round((0.65 * result.scoreDetails.finalScore + 0.35 * quality.score) * 10000) / 10000,
|
|
826
|
+
versionCompatible: true,
|
|
827
|
+
},
|
|
496
828
|
}];
|
|
497
829
|
});
|
|
830
|
+
const graphExpansions = expandKnowledgeEvidenceGraph({
|
|
831
|
+
query,
|
|
832
|
+
seeds: seedResults.map(result => ({ id: result.id, score: result.scoreDetails?.finalScore ?? result.score ?? 0 })),
|
|
833
|
+
chunks,
|
|
834
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
835
|
+
limit: Math.max(0, limit - Math.min(limit, seedResults.length)),
|
|
836
|
+
});
|
|
837
|
+
const graphCandidates = graphExpansions.flatMap(expansion => {
|
|
838
|
+
const chunk = chunkMap.get(expansion.id);
|
|
839
|
+
if (!chunk)
|
|
840
|
+
return [];
|
|
841
|
+
const quality = evaluateKnowledgeQuality({
|
|
842
|
+
query,
|
|
843
|
+
chunk,
|
|
844
|
+
retrievalScore: expansion.score,
|
|
845
|
+
versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions),
|
|
846
|
+
action: options?.action,
|
|
847
|
+
knowledgeNeeds: options?.knowledgeNeeds,
|
|
848
|
+
});
|
|
849
|
+
if (!quality.eligible)
|
|
850
|
+
return [];
|
|
851
|
+
return [{
|
|
852
|
+
id: chunk.id,
|
|
853
|
+
source: chunk.source,
|
|
854
|
+
title: resolveKnowledgeTitle({
|
|
855
|
+
headingPath: chunk.heading_path, documentTitle: chunk.document_title, content: chunk.content,
|
|
856
|
+
source: chunk.source, url: chunk.url,
|
|
857
|
+
}) ?? chunk.source,
|
|
858
|
+
content: quality.excerpt,
|
|
859
|
+
excerpt: quality.excerpt,
|
|
860
|
+
matchingTerms: quality.matchingTerms,
|
|
861
|
+
applicability: [...quality.applicability, `graph:${expansion.relation}`],
|
|
862
|
+
url: chunk.url,
|
|
863
|
+
version: chunk.version ?? undefined,
|
|
864
|
+
embedding: [],
|
|
865
|
+
score: expansion.score,
|
|
866
|
+
scoreDetails: {
|
|
867
|
+
profileVersion: KNOWLEDGE_RETRIEVAL_SCORING_PROFILE.version,
|
|
868
|
+
channelRanks: {},
|
|
869
|
+
rawScore: expansion.score, normalizedScore: expansion.score, pageScore: expansion.score,
|
|
870
|
+
qualityScore: quality.score, finalScore: expansion.score,
|
|
871
|
+
authority: authorityFromProvenance(chunk.provenance, chunk.source),
|
|
872
|
+
versionCompatible: true, provenanceComplete: provenanceComplete(chunk.provenance),
|
|
873
|
+
graphRelation: {
|
|
874
|
+
seedId: expansion.seedId, type: expansion.relation, evidence: expansion.relationEvidence,
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
}];
|
|
878
|
+
});
|
|
879
|
+
const rankedCandidates = dedupeKnowledgeEntries([...seedResults, ...graphCandidates])
|
|
880
|
+
.sort((left, right) => (right.scoreDetails?.finalScore ?? right.score ?? 0)
|
|
881
|
+
- (left.scoreDetails?.finalScore ?? left.score ?? 0));
|
|
882
|
+
const candidates = addNecessaryAdjacentEntries({
|
|
883
|
+
candidates: rankedCandidates, chunks, query, knowledgeNeeds: options?.knowledgeNeeds ?? [], limit,
|
|
884
|
+
});
|
|
498
885
|
const packed = this.packEntries(candidates, chunks, budgetTokens);
|
|
499
886
|
return {
|
|
500
887
|
...packed,
|
|
501
|
-
warnings: [...hybrid.warnings, ...readinessWarnings, ...(candidates.length === 0 ? ['knowledge_insufficient'] : [])],
|
|
888
|
+
warnings: [...hybrid.warnings, ...coverageWarnings, ...readinessWarnings, ...(candidates.length === 0 ? ['knowledge_insufficient'] : [])],
|
|
502
889
|
budgetTokens,
|
|
503
890
|
rejectedLowScore: hybrid.rejectedLowScore,
|
|
504
891
|
rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
|
|
505
892
|
retrievalDiagnostics: {
|
|
506
893
|
selectedSources: sources,
|
|
507
894
|
sourceSignals: selection.signals,
|
|
895
|
+
sourceRejections: selection.rejections,
|
|
896
|
+
coverageGaps: [...selection.coverageGaps, ...compileFailures],
|
|
508
897
|
compatibleChunks: chunks.length,
|
|
509
898
|
hybridCandidates: hybrid.results.length,
|
|
510
899
|
qualityRejected,
|
|
900
|
+
channelCandidateCounts: hybrid.channelCandidateCounts,
|
|
901
|
+
graphExpansions: graphCandidates.length,
|
|
511
902
|
},
|
|
512
903
|
};
|
|
513
904
|
}
|
|
514
905
|
packEntries(candidates, chunks, budgetTokens) {
|
|
515
906
|
const chunkMap = new Map(chunks.map(chunk => [chunk.id, chunk]));
|
|
516
907
|
const entries = [];
|
|
908
|
+
const sourceTokens = new Map();
|
|
909
|
+
const pageTokens = new Map();
|
|
517
910
|
let tokenCount = 0;
|
|
518
911
|
let truncatedCount = 0;
|
|
519
912
|
for (const entry of candidates) {
|
|
913
|
+
const chunk = chunkMap.get(entry.id);
|
|
520
914
|
const entryTokens = (entry.excerpt
|
|
521
915
|
? estimateKnowledgeTokens(entry.content)
|
|
522
|
-
:
|
|
916
|
+
: chunk?.token_count ?? estimateKnowledgeTokens(entry.content))
|
|
523
917
|
+ estimateKnowledgeTokens(entry.title);
|
|
524
|
-
|
|
918
|
+
const structuredPage = chunk?.page_id ?? null;
|
|
919
|
+
const source = chunk?.source ?? entry.source;
|
|
920
|
+
const sourceLimit = Math.max(entryTokens, Math.floor(budgetTokens * 0.60));
|
|
921
|
+
const pageLimit = Math.max(entryTokens, Math.floor(budgetTokens * 0.40));
|
|
922
|
+
const overSourceBudget = Boolean(structuredPage)
|
|
923
|
+
&& (sourceTokens.get(source) ?? 0) + entryTokens > sourceLimit;
|
|
924
|
+
const overPageBudget = Boolean(structuredPage)
|
|
925
|
+
&& (pageTokens.get(structuredPage) ?? 0) + entryTokens > pageLimit;
|
|
926
|
+
if (tokenCount + entryTokens > budgetTokens || overSourceBudget || overPageBudget) {
|
|
525
927
|
truncatedCount++;
|
|
526
928
|
continue;
|
|
527
929
|
}
|
|
528
930
|
entries.push(entry);
|
|
529
931
|
tokenCount += entryTokens;
|
|
932
|
+
sourceTokens.set(source, (sourceTokens.get(source) ?? 0) + entryTokens);
|
|
933
|
+
if (structuredPage)
|
|
934
|
+
pageTokens.set(structuredPage, (pageTokens.get(structuredPage) ?? 0) + entryTokens);
|
|
530
935
|
}
|
|
531
936
|
return { entries, tokenCount, truncatedCount };
|
|
532
937
|
}
|
|
@@ -560,6 +965,36 @@ export class KnowledgeEngine {
|
|
|
560
965
|
}
|
|
561
966
|
const versions = detectProjectKnowledgeVersions(this.projectRoot);
|
|
562
967
|
this.store.refreshSourceReadiness(Object.fromEntries(this.store.getDistinctSources().map(source => [source, expectedVersionForSource(source, versions)])));
|
|
968
|
+
for (const entry of this.catalog.getCatalog().sources) {
|
|
969
|
+
if (options.source && entry.sourceId !== options.source)
|
|
970
|
+
continue;
|
|
971
|
+
const chunks = this.store.countChunks(entry.sourceId);
|
|
972
|
+
const vectors = this.store.countEmbeddings(entry.sourceId);
|
|
973
|
+
const coverage = chunks > 0 ? vectors / chunks : 0;
|
|
974
|
+
this.catalog.activateSource({
|
|
975
|
+
schemaId: entry.schemaId,
|
|
976
|
+
version: entry.version,
|
|
977
|
+
contentHash: entry.contentHash,
|
|
978
|
+
readiness: coverage >= 0.95
|
|
979
|
+
? {
|
|
980
|
+
status: 'ready', activeContentHash: entry.contentHash,
|
|
981
|
+
documents: entry.readiness.documents, chunks, vectors, coverage,
|
|
982
|
+
lastSuccessfulRefresh: Date.now(), lastAttempt: Date.now(),
|
|
983
|
+
}
|
|
984
|
+
: {
|
|
985
|
+
status: 'degraded', activeContentHash: entry.contentHash,
|
|
986
|
+
documents: entry.readiness.documents, chunks, vectors, coverage,
|
|
987
|
+
lastAttempt: Date.now(),
|
|
988
|
+
degradation: {
|
|
989
|
+
reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
|
|
990
|
+
previousHealthyRevisionRetained: true,
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
entities: entry.entities,
|
|
994
|
+
symbols: entry.symbols,
|
|
995
|
+
taskKinds: entry.taskKinds,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
563
998
|
return {
|
|
564
999
|
processed,
|
|
565
1000
|
remaining: this.store.countChunks(options.source) - this.store.countEmbeddings(options.source),
|