@devflow-tools/knowledge-engine 0.17.8 → 0.18.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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/dist/dependency-source-compiler.d.ts +1 -0
  3. package/dist/dependency-source-compiler.d.ts.map +1 -1
  4. package/dist/dependency-source-compiler.js +37 -87
  5. package/dist/dependency-source-compiler.js.map +1 -1
  6. package/dist/embedding-provider.d.ts +1 -1
  7. package/dist/embedding-provider.d.ts.map +1 -1
  8. package/dist/embedding-provider.js +54 -0
  9. package/dist/embedding-provider.js.map +1 -1
  10. package/dist/hybrid-search.d.ts +3 -1
  11. package/dist/hybrid-search.d.ts.map +1 -1
  12. package/dist/hybrid-search.js +26 -10
  13. package/dist/hybrid-search.js.map +1 -1
  14. package/dist/index.d.ts +8 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/knowledge-compiler.d.ts +1 -4
  19. package/dist/knowledge-compiler.d.ts.map +1 -1
  20. package/dist/knowledge-compiler.js +3 -13
  21. package/dist/knowledge-compiler.js.map +1 -1
  22. package/dist/knowledge-engine.d.ts +26 -58
  23. package/dist/knowledge-engine.d.ts.map +1 -1
  24. package/dist/knowledge-engine.js +159 -412
  25. package/dist/knowledge-engine.js.map +1 -1
  26. package/dist/knowledge-ingest-runtime.d.ts +4 -1
  27. package/dist/knowledge-ingest-runtime.d.ts.map +1 -1
  28. package/dist/knowledge-ingest-runtime.js +47 -33
  29. package/dist/knowledge-ingest-runtime.js.map +1 -1
  30. package/dist/knowledge-maintenance.d.ts.map +1 -1
  31. package/dist/knowledge-maintenance.js +38 -54
  32. package/dist/knowledge-maintenance.js.map +1 -1
  33. package/dist/knowledge-quality.js +1 -1
  34. package/dist/knowledge-quality.js.map +1 -1
  35. package/dist/knowledge-query-planner.d.ts +1 -1
  36. package/dist/knowledge-query-planner.d.ts.map +1 -1
  37. package/dist/knowledge-query-planner.js +4 -7
  38. package/dist/knowledge-query-planner.js.map +1 -1
  39. package/dist/knowledge-query-service.d.ts +62 -0
  40. package/dist/knowledge-query-service.d.ts.map +1 -0
  41. package/dist/knowledge-query-service.js +258 -0
  42. package/dist/knowledge-query-service.js.map +1 -0
  43. package/dist/knowledge-source-lifecycle.d.ts +91 -0
  44. package/dist/knowledge-source-lifecycle.d.ts.map +1 -0
  45. package/dist/knowledge-source-lifecycle.js +152 -0
  46. package/dist/knowledge-source-lifecycle.js.map +1 -0
  47. package/dist/knowledge-source-service.d.ts +66 -0
  48. package/dist/knowledge-source-service.d.ts.map +1 -0
  49. package/dist/knowledge-source-service.js +444 -0
  50. package/dist/knowledge-source-service.js.map +1 -0
  51. package/dist/knowledge-store.d.ts +59 -16
  52. package/dist/knowledge-store.d.ts.map +1 -1
  53. package/dist/knowledge-store.js +345 -133
  54. package/dist/knowledge-store.js.map +1 -1
  55. package/dist/markdown-converter.d.ts +0 -3
  56. package/dist/markdown-converter.d.ts.map +1 -1
  57. package/dist/markdown-converter.js +5 -5
  58. package/dist/markdown-converter.js.map +1 -1
  59. package/dist/semantic-output-contract.d.ts +3 -2
  60. package/dist/semantic-output-contract.d.ts.map +1 -1
  61. package/dist/semantic-output-contract.js +13 -0
  62. package/dist/semantic-output-contract.js.map +1 -1
  63. package/dist/source-catalog.d.ts +8 -6
  64. package/dist/source-catalog.d.ts.map +1 -1
  65. package/dist/source-catalog.js +26 -22
  66. package/dist/source-catalog.js.map +1 -1
  67. package/dist/source-readiness.d.ts +3 -2
  68. package/dist/source-readiness.d.ts.map +1 -1
  69. package/dist/source-readiness.js +10 -11
  70. package/dist/source-readiness.js.map +1 -1
  71. package/dist/source-selector.d.ts +1 -1
  72. package/dist/source-selector.d.ts.map +1 -1
  73. package/dist/source-selector.js +8 -3
  74. package/dist/source-selector.js.map +1 -1
  75. package/package.json +12 -4
@@ -14,13 +14,16 @@ import { estimateKnowledgeTokens as estimateTokens, lexicalRelevance, resolveKno
14
14
  import { detectProjectKnowledgeVersions, expectedVersionForSource, isKnowledgeVersionCompatible, } from './source-readiness.js';
15
15
  import { evaluateKnowledgeQuality } from './knowledge-quality.js';
16
16
  import { KnowledgeCompiler } from './knowledge-compiler.js';
17
- import { SourceCatalog } from './source-catalog.js';
17
+ import { buildProjectKnowledgeProfile, SourceCatalog } from './source-catalog.js';
18
18
  import { compileDependencySource } from './dependency-source-compiler.js';
19
19
  import { expandKnowledgeEvidenceGraph } from './evidence-graph.js';
20
20
  import { KNOWLEDGE_RETRIEVAL_SCORING_PROFILE } from './retrieval-scoring-profile.js';
21
21
  import { planKnowledgeQuery } from './knowledge-query-planner.js';
22
22
  import { emitSemanticControlLog } from '@devflow-tools/telemetry';
23
23
  import { createLexicalOnlyEmbeddingProvider } from './embedding-provider.js';
24
+ import { requestValidatedEmbeddingBatch } from '@devflow-tools/provider-runtime';
25
+ import { KnowledgeSourceService } from './knowledge-source-service.js';
26
+ import { KnowledgeQueryService, } from './knowledge-query-service.js';
24
27
  export const DEFAULT_KNOWLEDGE_TOKEN_BUDGET = 3000;
25
28
  export const estimateKnowledgeTokens = estimateTokens;
26
29
  function normalizeActiveSkill(value) {
@@ -45,13 +48,16 @@ function dependencyPackagesForMissingNeeds(needs, dependencies) {
45
48
  return packageName ? [packageName] : [];
46
49
  }))];
47
50
  }
48
- function buildSafeFtsQuery(value) {
51
+ function compileKnowledgeFtsQuery(value) {
49
52
  const terms = value.normalize('NFKC')
50
53
  .match(/[\p{L}\p{N}_@.-]{2,}/gu)
51
54
  ?.map(term => term.replace(/^\.+|\.+$/gu, '').replace(/"/gu, ''))
52
55
  .filter(Boolean)
53
56
  .slice(0, 24) ?? [];
54
- return [...new Set(terms)].map(term => `"${term}"`).join(' OR ');
57
+ const uniqueTerms = [...new Set(terms)];
58
+ return uniqueTerms.length > 0
59
+ ? uniqueTerms.map(term => `"${term}"`).join(' OR ')
60
+ : undefined;
55
61
  }
56
62
  function dedupeKnowledgeEntries(entries) {
57
63
  const seen = new Set();
@@ -171,27 +177,36 @@ function normalizeLookup(value) {
171
177
  }
172
178
  export class KnowledgeEngine {
173
179
  projectRoot;
180
+ projectId;
181
+ knowledgeAssetRoot;
174
182
  hybridSearch;
175
183
  store;
176
184
  embedder;
177
185
  compiler;
178
186
  catalog;
187
+ sources;
188
+ query;
179
189
  compatibilityRejectionCount = 0;
180
190
  lowScoreRejectionCount = 0;
181
191
  constructor(opts) {
182
192
  this.projectRoot = opts.projectRoot;
193
+ this.projectId = opts.projectId;
194
+ this.knowledgeAssetRoot = opts.dataSource?.assetRoot ?? join(opts.projectRoot, '.devflow', 'knowledge');
183
195
  this.embedder = opts.embedder ?? createLexicalOnlyEmbeddingProvider(opts.projectRoot);
184
- this.store = new KnowledgeStore(join(this.knowledgeDir, 'knowledge.db'));
185
- this.compiler = new KnowledgeCompiler(this.store);
186
- this.catalog = new SourceCatalog(this.store, this.projectRoot);
187
- this.catalog.refreshProjectProfile();
196
+ this.store = new KnowledgeStore(opts.dataSource?.databasePath ?? join(this.knowledgeDir, 'knowledge.db'), this.embedder.getCapability('knowledge_retrieval').profile);
197
+ this.compiler = new KnowledgeCompiler();
198
+ this.catalog = new SourceCatalog(this.store, this.projectRoot, this.projectId);
199
+ this.sources = new KnowledgeSourceService(this.store, this.catalog, this.projectRoot);
200
+ this.query = new KnowledgeQueryService({
201
+ searchReadOnly: (query, options) => this.searchReadOnlyInternal(query, options),
202
+ });
188
203
  this.hybridSearch = new KnowledgeHybridSearch(this.store, this.embedder);
189
204
  for (const suffix of ['', '-wal', '-shm']) {
190
205
  rmSync(join(this.knowledgeDir, `vectors.db${suffix}`), { force: true });
191
206
  }
192
207
  }
193
208
  get knowledgeDir() {
194
- return join(this.projectRoot, ".devflow", "knowledge");
209
+ return this.knowledgeAssetRoot;
195
210
  }
196
211
  pluginDir(pluginName) {
197
212
  return join(this.knowledgeDir, pluginName);
@@ -199,7 +214,7 @@ export class KnowledgeEngine {
199
214
  ensureManifestSourceSchema(name, manifest, version) {
200
215
  const sourceSchemaId = manifest.sourceSchemaId ?? `schema:${name}:v1`;
201
216
  const sourceKind = manifest.sourceKind ?? (manifest.pluginName === 'builtin' ? 'official' : 'plugin');
202
- this.catalog.registerSourceSchema({
217
+ this.sources.registerSourceSchema({
203
218
  id: sourceSchemaId,
204
219
  sourceId: name,
205
220
  sourceKind,
@@ -214,17 +229,18 @@ export class KnowledgeEngine {
214
229
  resolvePersistedSourceVersion(name, manifest, existingVersion) {
215
230
  if (manifest.version && manifest.version !== 'current')
216
231
  return manifest.version;
217
- const expectedVersion = expectedVersionForSource(name, detectProjectKnowledgeVersions(this.projectRoot));
232
+ const sourceKind = manifest.sourceKind ?? (manifest.pluginName === 'builtin' ? 'official' : 'plugin');
233
+ const expectedVersion = expectedVersionForSource(name, detectProjectKnowledgeVersions(this.projectRoot), sourceKind);
218
234
  const persistedVersion = existingVersion && existingVersion !== 'current' ? existingVersion : undefined;
219
235
  return expectedVersion ?? persistedVersion;
220
236
  }
221
- async registerSource(source) {
237
+ async registerSource(source, operationTimestamp = Date.now()) {
222
238
  const dir = this.pluginDir(source.name);
223
239
  mkdirSync(dir, { recursive: true });
224
240
  const manifestPath = join(dir, "manifest.json");
225
241
  const sourceKind = source.pluginName === 'builtin' ? 'official' : 'plugin';
226
242
  const sourceSchemaId = `schema:${source.name}:v1`;
227
- this.catalog.registerSourceSchema({
243
+ this.sources.registerSourceSchema({
228
244
  id: sourceSchemaId,
229
245
  sourceId: source.name,
230
246
  sourceKind,
@@ -235,7 +251,7 @@ export class KnowledgeEngine {
235
251
  allowedEntities: [source.name],
236
252
  supportedTaskKinds: ['overview', 'create', 'modify', 'debug', 'test', 'build', 'migrate', 'explain'],
237
253
  refreshStrategy: 'on_demand',
238
- });
254
+ }, operationTimestamp);
239
255
  if (existsSync(manifestPath)) {
240
256
  const existing = JSON.parse(readFileSync(manifestPath, "utf-8"));
241
257
  // Merge URLs — don't overwrite existing URLs
@@ -327,7 +343,7 @@ export class KnowledgeEngine {
327
343
  })),
328
344
  ...gitResult,
329
345
  ].flatMap(c => {
330
- const chunks = sliceByHeadings(c.content, { minTokens: 1, targetTokens: 1200, maxTokens: 3000 });
346
+ const chunks = sliceByHeadings(c.content, { targetTokens: 1200, maxTokens: 3000 });
331
347
  return chunks.length > 0
332
348
  ? chunks.map(chunk => ({ ...c, title: resolveKnowledgeTitle({ headingPath: chunk.headingPath, documentTitle: c.title, content: chunk.content, source: name, url: c.path ?? c.sourceUrl }), content: chunk.content, headingPath: chunk.headingPath, codeBlocks: chunk.codeBlocks }))
333
349
  : [{ ...c, title: resolveKnowledgeTitle({ headingPath: c.headingPath, documentTitle: c.title, content: c.content, source: name, url: c.path ?? c.sourceUrl }) }];
@@ -357,19 +373,6 @@ export class KnowledgeEngine {
357
373
  errors: [error.message],
358
374
  }];
359
375
  }
360
- // Also write JSON files for backward compat
361
- this.clearChunkFiles(dir);
362
- for (const [i, c] of totalResult.entries()) {
363
- const cf = {
364
- id: `chunk-${String(i + 1).padStart(3, "0")}`,
365
- title: c.title ?? name,
366
- section: c.path,
367
- content: c.content,
368
- sourceUrl: `https://github.com/${sourceConfig.githubRepo}`,
369
- fetchedAt: Date.now(),
370
- };
371
- writeFileSync(join(dir, `${cf.id}.json`), JSON.stringify(cf, null, 2), "utf-8");
372
- }
373
376
  writeFileSync(manifestPath, JSON.stringify({
374
377
  ...manifest,
375
378
  urls: manifest.urls,
@@ -406,18 +409,6 @@ export class KnowledgeEngine {
406
409
  version: manifest.version,
407
410
  deferVectorBackfill: options.deferVectorBackfill,
408
411
  });
409
- this.clearChunkFiles(dir);
410
- for (const [index, chunk] of compiled.chunks.entries()) {
411
- const file = {
412
- id: `chunk-${String(index + 1).padStart(3, '0')}`,
413
- title: chunk.headingBreadcrumbs.at(-1) ?? compiled.page.title,
414
- section: chunk.headingBreadcrumbs.join(' > '),
415
- content: chunk.content,
416
- sourceUrl: fetchedDocuments[0].url,
417
- fetchedAt: Date.now(),
418
- };
419
- writeFileSync(join(dir, `${file.id}.json`), JSON.stringify(file, null, 2), 'utf8');
420
- }
421
412
  const successful = results.find(result => result.errors.length === 0);
422
413
  if (successful)
423
414
  successful.added = compiled.chunks.length;
@@ -449,139 +440,33 @@ export class KnowledgeEngine {
449
440
  await closeBrowser();
450
441
  return results;
451
442
  }
452
- reingestLocalSources(source) {
453
- if (!existsSync(this.knowledgeDir))
454
- return [];
455
- const sources = source
456
- ? [source]
457
- : readdirSync(this.knowledgeDir, { withFileTypes: true })
458
- .filter(entry => entry.isDirectory())
459
- .map(entry => entry.name)
460
- .sort();
461
- const results = [];
462
- for (const name of sources) {
463
- const dir = this.pluginDir(name);
464
- const manifestPath = join(dir, 'manifest.json');
465
- if (!existsSync(manifestPath))
466
- continue;
467
- try {
468
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
469
- let chunkFiles = readdirSync(dir)
470
- .filter(file => /^chunk-\d+\.json$/u.test(file))
471
- .sort()
472
- .flatMap(file => {
473
- try {
474
- const chunk = JSON.parse(readFileSync(join(dir, file), 'utf8'));
475
- return typeof chunk.content === 'string' && chunk.content.trim()
476
- ? [{
477
- title: chunk.title?.trim() || name,
478
- content: chunk.content,
479
- sourceUrl: chunk.sourceUrl || manifest.sourceUrl || manifest.urls?.[0] || '',
480
- }]
481
- : [];
482
- }
483
- catch {
484
- return [];
485
- }
486
- });
487
- if (chunkFiles.length === 0) {
488
- chunkFiles = this.store.getDocChunks(name, 100_000).flatMap(chunk => typeof chunk.content === 'string' && chunk.content.trim()
489
- ? [{
490
- title: String(chunk.heading_path || name),
491
- content: chunk.content,
492
- sourceUrl: manifest.sourceUrl || manifest.urls?.[0] || '',
493
- }]
494
- : []);
495
- }
496
- if (chunkFiles.length === 0) {
497
- results.push({ source: name, documentsRetired: 0, chunks: 0, errors: ['no local chunks'] });
498
- continue;
499
- }
500
- const combined = chunkFiles.map(chunk => `# ${chunk.title}\n\n${chunk.content}`).join('\n\n');
501
- const existingVersion = this.store.listChunks({ sources: [name], limit: 1 })[0]?.version;
502
- const version = this.resolvePersistedSourceVersion(name, manifest, existingVersion);
503
- const sourceSchemaId = this.ensureManifestSourceSchema(name, manifest, version);
504
- const url = chunkFiles.find(chunk => chunk.sourceUrl)?.sourceUrl
505
- || manifest.urls?.[0]
506
- || `devflow://knowledge/${name}`;
507
- const compiled = this.compiler.compileAndStore({
508
- projectRoot: this.projectRoot,
509
- sourceId: name,
510
- sourceSchemaId,
511
- sourceKind: manifest.sourceKind ?? (manifest.pluginName === 'builtin' ? 'official' : 'plugin'),
512
- sourceUri: url,
513
- markdown: combined,
514
- title: `${name} local knowledge`,
515
- version,
516
- });
517
- const documentsRetired = this.store.deactivateSourceDocuments(name, compiled.page.id);
518
- const storedChunks = this.store.listChunks({ sources: [name] });
519
- const vectors = this.store.countEmbeddings(name);
520
- const coverage = storedChunks.length > 0 ? vectors / storedChunks.length : 0;
521
- const entities = [...new Set([
522
- name,
523
- ...storedChunks.flatMap(chunk => parseJsonStrings(chunk.entities)),
524
- ])];
525
- const symbols = [...new Set(storedChunks.flatMap(chunk => [
526
- ...parseJsonStrings(chunk.symbols),
527
- ...parseJsonStrings(chunk.api_identifiers),
528
- ]))];
529
- this.catalog.activateSource({
530
- schemaId: sourceSchemaId,
531
- version,
532
- contentHash: compiled.page.contentHash,
533
- readiness: coverage >= 0.95
534
- ? {
535
- status: 'ready', activeContentHash: compiled.page.contentHash,
536
- documents: 1, chunks: storedChunks.length, vectors, coverage,
537
- lastSuccessfulRefresh: Date.now(), lastAttempt: Date.now(),
538
- }
539
- : {
540
- status: 'degraded', activeContentHash: compiled.page.contentHash,
541
- documents: 1, chunks: storedChunks.length, vectors, coverage,
542
- lastAttempt: Date.now(),
543
- degradation: {
544
- reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
545
- previousHealthyRevisionRetained: false,
546
- },
547
- },
548
- entities,
549
- symbols,
550
- });
551
- this.clearChunkFiles(dir);
552
- for (const [index, chunk] of compiled.chunks.entries()) {
553
- const file = {
554
- id: `chunk-${String(index + 1).padStart(3, '0')}`,
555
- title: chunk.headingBreadcrumbs.at(-1) ?? compiled.page.title,
556
- section: chunk.headingBreadcrumbs.join(' > '),
557
- content: chunk.content,
558
- sourceUrl: url,
559
- fetchedAt: Date.now(),
560
- };
561
- writeFileSync(join(dir, `${file.id}.json`), JSON.stringify(file, null, 2), 'utf8');
562
- }
563
- writeFileSync(manifestPath, JSON.stringify({
564
- ...manifest,
565
- version: version ?? manifest.version,
566
- fetchedAt: Date.now(),
567
- chunkCount: compiled.chunks.length,
568
- chunkPolicyVersion: KNOWLEDGE_CHUNK_POLICY_VERSION,
569
- }, null, 2), 'utf8');
570
- results.push({ source: name, documentsRetired, chunks: compiled.chunks.length, errors: [] });
571
- }
572
- catch (error) {
573
- results.push({
574
- source: name,
575
- documentsRetired: 0,
576
- chunks: 0,
577
- errors: [error instanceof Error ? error.message : String(error)],
578
- });
579
- }
580
- }
581
- this.store.refreshSourceReadiness();
582
- return results;
443
+ async ingestSourceMarkdown(input) {
444
+ const operationTimestamp = input.operationTimestamp ?? Date.now();
445
+ const manifestPath = join(this.pluginDir(input.source), 'manifest.json');
446
+ if (!existsSync(manifestPath))
447
+ throw new Error(`KNOWLEDGE_SOURCE_NOT_REGISTERED:${input.source}`);
448
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
449
+ const compiled = await this.replaceCompiledSource({
450
+ source: input.source,
451
+ manifest,
452
+ sourceUri: input.sourceUri,
453
+ title: input.title,
454
+ markdown: input.markdown,
455
+ version: this.resolvePersistedSourceVersion(input.source, manifest, input.version ?? manifest.version),
456
+ deferVectorBackfill: input.deferVectorBackfill,
457
+ operationTimestamp,
458
+ });
459
+ writeFileSync(manifestPath, JSON.stringify({
460
+ ...manifest,
461
+ version: this.resolvePersistedSourceVersion(input.source, manifest, input.version ?? manifest.version),
462
+ fetchedAt: operationTimestamp,
463
+ chunkCount: compiled.chunks.length,
464
+ chunkPolicyVersion: KNOWLEDGE_CHUNK_POLICY_VERSION,
465
+ }, null, 2), 'utf8');
466
+ return compiled;
583
467
  }
584
468
  activatePersistedLocalSources(source) {
469
+ const operationTimestamp = Date.now();
585
470
  if (!existsSync(this.knowledgeDir))
586
471
  return [];
587
472
  const activeCatalogSources = new Map(this.catalog.getCatalog().sources.map(entry => [entry.sourceId, entry]));
@@ -613,27 +498,20 @@ export class KnowledgeEngine {
613
498
  if (currentPolicy && currentSemantic && currentCatalog)
614
499
  continue;
615
500
  if (!currentPolicy || !currentSemantic) {
616
- const migrated = this.reingestLocalSources(name)[0];
617
- const migratedChunks = this.store.countChunks(name);
618
- const migratedVectors = this.store.countEmbeddings(name);
619
- const errors = migrated?.errors ?? ['local policy migration produced no result'];
620
- if (errors.length === 0) {
621
- const activated = this.catalog.getCatalog().sources.find(entry => entry.sourceId === name);
622
- if (activated)
623
- activeCatalogSources.set(name, activated);
624
- }
625
501
  results.push({
626
502
  source: name,
627
- chunks: migratedChunks,
628
- vectors: migratedVectors,
629
- activated: errors.length === 0 && migratedChunks > 0,
630
- errors,
503
+ chunks: this.store.countChunks(name),
504
+ vectors: this.store.countEmbeddings(name),
505
+ activated: false,
506
+ errors: [`KNOWLEDGE_SOURCE_UNSUPPORTED:${name}:chunk_policy_or_provenance`],
631
507
  });
632
508
  continue;
633
509
  }
634
510
  const sourceSchemaId = this.ensureManifestSourceSchema(name, manifest, version);
511
+ const schema = this.store.getSourceSchema(sourceSchemaId);
512
+ if (!schema)
513
+ throw new Error(`Knowledge source schema ${sourceSchemaId} is not registered`);
635
514
  const vectors = this.store.countEmbeddings(name);
636
- const coverage = vectors / chunks.length;
637
515
  const contentHash = createHash('sha256')
638
516
  .update(chunks.map(chunk => `${chunk.id}\0${chunk.content_hash || chunk.content}`).sort().join('\n'))
639
517
  .digest('hex');
@@ -645,30 +523,17 @@ export class KnowledgeEngine {
645
523
  ...parseJsonStrings(chunk.symbols),
646
524
  ...parseJsonStrings(chunk.api_identifiers),
647
525
  ]))];
648
- const now = Date.now();
649
- this.catalog.activateSource({
650
- schemaId: sourceSchemaId,
526
+ this.sources.activateExistingRevision({
527
+ schema,
528
+ sourceLocator: chunks.find(chunk => chunk.source_url)?.source_url
529
+ || chunks.find(chunk => chunk.url)?.url
530
+ || `devflow://knowledge/${name}`,
651
531
  version,
652
532
  contentHash,
653
- readiness: coverage >= 0.95
654
- ? {
655
- status: 'ready', activeContentHash: contentHash,
656
- documents: new Set(chunks.map(chunk => chunk.doc_id)).size,
657
- chunks: chunks.length, vectors, coverage,
658
- lastSuccessfulRefresh: now, lastAttempt: now,
659
- }
660
- : {
661
- status: 'degraded', activeContentHash: contentHash,
662
- documents: new Set(chunks.map(chunk => chunk.doc_id)).size,
663
- chunks: chunks.length, vectors, coverage,
664
- lastAttempt: now,
665
- degradation: {
666
- reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
667
- previousHealthyRevisionRetained: vectors > 0,
668
- },
669
- },
533
+ documents: new Set(chunks.map(chunk => chunk.doc_id)).size,
670
534
  entities,
671
535
  symbols,
536
+ operationTimestamp,
672
537
  });
673
538
  const activated = this.catalog.getCatalog().sources.find(entry => entry.sourceId === name);
674
539
  if (activated)
@@ -687,12 +552,6 @@ export class KnowledgeEngine {
687
552
  }
688
553
  return results;
689
554
  }
690
- clearChunkFiles(dir) {
691
- for (const file of readdirSync(dir)) {
692
- if (/^chunk-\d+\.json$/.test(file))
693
- rmSync(join(dir, file), { force: true });
694
- }
695
- }
696
555
  async replaceDocument(doc, chunks, deferVectorBackfill = false) {
697
556
  if (deferVectorBackfill) {
698
557
  this.store.replaceDocumentChunks({ doc, chunks });
@@ -704,14 +563,19 @@ export class KnowledgeEngine {
704
563
  ?? new Error(this.embedder.getLastError() ?? 'Knowledge embedding warmup failed');
705
564
  }
706
565
  const texts = chunks.map(chunk => `${chunk.headingPath}\n${chunk.content}`);
707
- const embeddings = await this.embedder.embedBatch(texts);
566
+ const embeddings = await requestValidatedEmbeddingBatch({
567
+ provider: this.embedder,
568
+ purpose: 'knowledge_retrieval',
569
+ texts,
570
+ });
708
571
  this.store.replaceDocument({ doc, chunks, embeddings });
709
572
  }
710
573
  async replaceCompiledSource(input) {
574
+ const operationTimestamp = input.operationTimestamp ?? Date.now();
711
575
  const sourceSchemaId = input.manifest.sourceSchemaId ?? `schema:${input.source}:v1`;
712
576
  const sourceKind = input.manifest.sourceKind ?? 'plugin';
713
577
  if (!this.store.getSourceSchema(sourceSchemaId)) {
714
- this.catalog.registerSourceSchema({
578
+ this.sources.registerSourceSchema({
715
579
  id: sourceSchemaId,
716
580
  sourceId: input.source,
717
581
  sourceKind,
@@ -724,88 +588,55 @@ export class KnowledgeEngine {
724
588
  refreshStrategy: 'on_demand',
725
589
  });
726
590
  }
727
- const inputHash = createHash('sha256').update(input.markdown).digest('hex');
728
- const refresh = this.catalog.beginRefresh(input.source, inputHash);
729
- try {
730
- const compiled = this.compiler.compile({
731
- projectRoot: this.projectRoot,
732
- sourceId: input.source,
733
- sourceSchemaId,
734
- sourceKind,
735
- sourceUri: input.sourceUri,
736
- markdown: input.markdown,
737
- title: input.title,
738
- version: input.version,
739
- });
740
- let embeddings = [];
741
- if (!input.deferVectorBackfill) {
742
- const warmed = await this.embedder.warmup();
743
- if (!warmed) {
744
- throw this.embedder.getLastFailure?.()
745
- ?? new Error(this.embedder.getLastError() ?? 'Knowledge embedding warmup failed');
746
- }
747
- embeddings = await this.embedder.embedBatch(compiled.chunks.map(chunk => chunk.embeddingText));
748
- if (embeddings.length !== compiled.chunks.length) {
749
- throw new Error(`Knowledge embedding count mismatch: ${embeddings.length}/${compiled.chunks.length}`);
750
- }
751
- }
752
- this.store.replaceCompiledPage(compiled.page, compiled.chunks);
753
- this.store.replaceKnowledgeAnalysisManifest(compiled.analysis.manifest, compiled.analysis.quarantined);
754
- if (embeddings.length > 0) {
755
- this.store.insertEmbeddingBatch(compiled.chunks.map((chunk, index) => ({
756
- id: chunk.id,
757
- embedding: embeddings[index],
758
- })));
591
+ const schema = this.store.getSourceSchema(sourceSchemaId);
592
+ if (!schema)
593
+ throw new Error(`Knowledge source schema ${sourceSchemaId} is not registered`);
594
+ const compiled = this.compiler.compile({
595
+ projectRoot: this.projectRoot,
596
+ projectId: this.catalog.projectId,
597
+ sourceId: input.source,
598
+ sourceSchemaId,
599
+ sourceKind,
600
+ sourceUri: input.sourceUri,
601
+ markdown: input.markdown,
602
+ title: input.title,
603
+ version: input.version === 'current' ? undefined : input.version,
604
+ compiledAt: operationTimestamp,
605
+ retrievedAt: operationTimestamp,
606
+ });
607
+ let embeddings = [];
608
+ if (!input.deferVectorBackfill) {
609
+ const warmed = await this.embedder.warmup();
610
+ if (!warmed) {
611
+ throw this.embedder.getLastFailure?.()
612
+ ?? new Error(this.embedder.getLastError() ?? 'Knowledge embedding warmup failed');
759
613
  }
760
- this.store.deactivateSourceDocuments(input.source, compiled.page.id);
761
- const vectors = embeddings.length;
762
- const coverage = compiled.chunks.length > 0 ? vectors / compiled.chunks.length : 0;
763
- this.catalog.activateSource({
764
- schemaId: sourceSchemaId,
765
- version: input.version === 'current' ? undefined : input.version,
766
- contentHash: compiled.page.contentHash,
767
- readiness: coverage >= 0.95
768
- ? {
769
- status: 'ready', activeContentHash: compiled.page.contentHash,
770
- documents: 1, chunks: compiled.chunks.length, vectors, coverage,
771
- lastSuccessfulRefresh: Date.now(), lastAttempt: Date.now(),
772
- }
773
- : {
774
- status: 'degraded', activeContentHash: compiled.page.contentHash,
775
- documents: 1, chunks: compiled.chunks.length, vectors, coverage,
776
- lastAttempt: Date.now(),
777
- degradation: {
778
- reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
779
- previousHealthyRevisionRetained: false,
780
- },
781
- },
782
- entities: [input.source, ...compiled.page.entities],
783
- symbols: compiled.page.symbols,
784
- refreshJobId: refresh.id,
614
+ embeddings = await requestValidatedEmbeddingBatch({
615
+ provider: this.embedder,
616
+ purpose: 'knowledge_retrieval',
617
+ texts: compiled.chunks.map(chunk => chunk.embeddingText),
785
618
  });
786
- return compiled;
787
- }
788
- catch (error) {
789
- this.catalog.failRefresh(refresh.id, error);
790
- throw error;
791
619
  }
620
+ this.sources.publishCompiledSource({ compiled, schema, embeddings, operationTimestamp });
621
+ return compiled;
792
622
  }
793
623
  compileDependency(packageName) {
794
624
  return compileDependencySource({
795
625
  projectRoot: this.projectRoot,
626
+ projectId: this.projectId,
796
627
  packageName,
797
628
  store: this.store,
798
629
  deferVectorBackfill: true,
799
630
  });
800
631
  }
801
632
  async getProjectProfile() {
802
- return this.store.getProjectProfile(this.projectRoot) ?? this.catalog.refreshProjectProfile();
633
+ return this.store.getProjectProfile(this.catalog.projectId) ?? this.catalog.refreshProjectProfile();
803
634
  }
804
635
  async getSourceCatalog() {
805
636
  return this.catalog.getCatalog();
806
637
  }
807
638
  async getCoverageGaps() {
808
- return this.store.listCoverageGaps(this.projectRoot);
639
+ return this.store.listCoverageGaps(this.catalog.projectId);
809
640
  }
810
641
  async search(query, options) {
811
642
  return (await this.searchWithStatus(query, options)).entries;
@@ -822,7 +653,7 @@ export class KnowledgeEngine {
822
653
  queryPlanHash: options?.queryPlanHash,
823
654
  };
824
655
  try {
825
- const result = await this.searchWithStatusInternal(query, options);
656
+ const result = await this.query.search(query, options);
826
657
  const abstained = result.retrievalDiagnostics?.abstentionReason;
827
658
  emitSemanticControlLog({
828
659
  event: 'knowledge.query.executed',
@@ -866,44 +697,18 @@ export class KnowledgeEngine {
866
697
  throw error;
867
698
  }
868
699
  }
869
- async searchWithStatusInternal(query, options) {
700
+ async searchReadOnlyInternal(query, options) {
870
701
  const limit = options?.limit ?? 10;
871
702
  const queryDecision = planKnowledgeQuery({
872
703
  action: options?.action,
873
704
  knowledgeNeeds: options?.knowledgeNeeds,
874
705
  retrievalIntent: options?.retrievalIntent ?? 'explicit',
875
706
  });
876
- if (queryDecision.requirement === 'knowledge_not_required') {
877
- return {
878
- entries: [],
879
- warnings: ['knowledge_not_required'],
880
- budgetTokens: Math.max(0, options?.budgetTokens ?? DEFAULT_KNOWLEDGE_TOKEN_BUDGET),
881
- tokenCount: 0,
882
- truncatedCount: 0,
883
- rejectedLowScore: 0,
884
- rejectedIncompatibleSources: 0,
885
- retrievalDiagnostics: {
886
- selectedSources: [],
887
- sourceSignals: [],
888
- sourceRejections: [],
889
- coverageGaps: [],
890
- compatibleChunks: 0,
891
- hybridCandidates: 0,
892
- qualityRejected: {},
893
- abstentionReason: queryDecision.reason,
894
- },
895
- };
896
- }
897
707
  const activeSkill = options?.activeSkill ?? options?.projectSignals?.activeSkill;
898
708
  const taskAware = Boolean(options?.action || queryDecision.needs.length);
899
- let availableSources = this.store.getDistinctSources();
900
- if (taskAware) {
901
- const catalogSources = new Set(this.catalog.getCatalog().sources.map(entry => entry.sourceId));
902
- if (availableSources.some(source => !catalogSources.has(source))) {
903
- this.activatePersistedLocalSources();
904
- }
905
- }
906
- const projectProfile = this.catalog.refreshProjectProfile({
709
+ const availableSources = this.store.getDistinctSources();
710
+ const persistedProfile = this.store.getProjectProfile(this.catalog.projectId);
711
+ const projectProfile = persistedProfile ?? buildProjectKnowledgeProfile(this.projectRoot, this.catalog.projectId, {
907
712
  activeSkills: activeSkill ? [normalizeActiveSkill(activeSkill)] : [],
908
713
  });
909
714
  const selectionInput = () => ({
@@ -921,65 +726,36 @@ export class KnowledgeEngine {
921
726
  action: options?.action,
922
727
  knowledgeNeeds: queryDecision.needs,
923
728
  });
924
- let selection = selectKnowledgeSources(selectionInput());
925
- const compileFailures = [];
926
- for (const packageName of taskAware
927
- ? dependencyPackagesForMissingNeeds(selection.missingNeeds, projectProfile.dependencies)
928
- : []) {
929
- try {
930
- this.compileDependency(packageName);
931
- }
932
- catch (error) {
933
- const reason = error instanceof Error ? error.message : String(error);
934
- this.catalog.recordCoverageGap({
729
+ const selection = selectKnowledgeSources(selectionInput());
730
+ const compileFailures = selection.missingNeeds.flatMap(need => {
731
+ const packageName = dependencyPackagesForMissingNeeds([need], projectProfile.dependencies)[0];
732
+ return packageName ? [{
935
733
  sourceId: `dependency:${packageName}`,
936
- kind: 'compile_failure',
734
+ kind: 'missing_source',
937
735
  entity: packageName,
938
- reason,
939
- });
940
- compileFailures.push({ sourceId: `dependency:${packageName}`, kind: 'compile_failure', entity: packageName, reason });
941
- }
942
- }
943
- if (selection.missingNeeds.length > 0) {
944
- availableSources = this.store.getDistinctSources();
945
- selection = selectKnowledgeSources(selectionInput());
946
- }
947
- for (const gap of selection.coverageGaps)
948
- this.catalog.recordCoverageGap(gap);
736
+ reason: `Dependency knowledge source ${packageName} requires an explicit ingest command`,
737
+ }] : [];
738
+ });
949
739
  this.compatibilityRejectionCount += selection.rejectedSources.length;
950
- const sources = selection.sources;
951
- const semanticMigrationWarnings = [];
952
- for (const source of sources) {
953
- const legacy = this.store.listChunks({ sources: [source], limit: 200_000 })
954
- .some(chunk => !provenanceComplete(chunk.provenance));
955
- if (!legacy)
956
- continue;
957
- const migrated = this.reingestLocalSources(source)[0];
958
- if (!migrated || migrated.errors.length > 0 || migrated.chunks === 0) {
959
- semanticMigrationWarnings.push(`knowledge-semantic-migration-failed:${source}:${migrated?.errors[0] ?? 'no_result'}`);
960
- }
961
- else {
962
- semanticMigrationWarnings.push(`knowledge-semantic-migrated:${source}:${migrated.chunks}`);
963
- try {
964
- const backfill = await this.backfillVectors({ source, batchSize: 32 });
965
- semanticMigrationWarnings.push(`knowledge-semantic-vectors:${source}:${backfill.processed}:${backfill.remaining}`);
966
- }
967
- catch (error) {
968
- semanticMigrationWarnings.push(`knowledge-semantic-vector-degraded:${source}:${error instanceof Error ? error.message : String(error)}`);
969
- }
970
- }
971
- }
740
+ const unsupportedSources = new Set(selection.sources.filter(source => this.store.listChunks({ sources: [source], limit: 200_000 })
741
+ .some(chunk => !provenanceComplete(chunk.provenance))));
742
+ const sources = selection.sources.filter(source => !unsupportedSources.has(source));
743
+ const unsupportedSourceWarnings = [...unsupportedSources]
744
+ .map(source => `knowledge-source-unsupported:${source}`);
972
745
  const budgetTokens = Math.max(0, options?.budgetTokens ?? DEFAULT_KNOWLEDGE_TOKEN_BUDGET);
973
746
  const projectVersions = detectProjectKnowledgeVersions(this.projectRoot);
974
- this.store.refreshSourceReadiness(Object.fromEntries(sources.map(source => [source, expectedVersionForSource(source, projectVersions)])));
975
- const readinessWarnings = this.store.getSourceReadiness()
976
- .filter(row => sources.includes(row.source) && row.status !== 'ready')
977
- .map(row => `knowledge-source-degraded:${row.source}:${row.failure_category ?? 'unknown'}`);
747
+ const catalogBySource = new Map(this.catalog.getCatalog().sources.map(entry => [entry.sourceId, entry]));
748
+ const readinessWarnings = sources.flatMap(source => {
749
+ const readiness = catalogBySource.get(source)?.readiness;
750
+ return readiness && readiness.status !== 'ready'
751
+ ? [`knowledge-source-degraded:${source}:${readiness.degradation?.category ?? readiness.status}`]
752
+ : [];
753
+ });
978
754
  const coverageWarnings = selection.coverageGaps.length > 0 ? ['knowledge_source_missing'] : [];
979
755
  if (sources.length === 0) {
980
756
  return {
981
757
  entries: [],
982
- warnings: [...semanticMigrationWarnings, ...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'],
758
+ warnings: [...unsupportedSourceWarnings, ...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'],
983
759
  budgetTokens,
984
760
  tokenCount: 0,
985
761
  truncatedCount: 0,
@@ -997,11 +773,11 @@ export class KnowledgeEngine {
997
773
  }
998
774
  const allChunks = this.store.listChunks({ sources });
999
775
  const chunks = allChunks.filter(chunk => chunk.chunk_policy_version === KNOWLEDGE_CHUNK_POLICY_VERSION
1000
- && isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions));
776
+ && isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions, catalogBySource.get(chunk.source)?.sourceKind ?? 'plugin'));
1001
777
  const rejectedByQualityGate = allChunks.length - chunks.length;
1002
778
  if (chunks.length === 0) {
1003
779
  return {
1004
- entries: [], warnings: [...semanticMigrationWarnings, ...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'], budgetTokens, tokenCount: 0, truncatedCount: 0,
780
+ entries: [], warnings: [...unsupportedSourceWarnings, ...coverageWarnings, ...readinessWarnings, 'knowledge_insufficient'], budgetTokens, tokenCount: 0, truncatedCount: 0,
1005
781
  rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
1006
782
  retrievalDiagnostics: {
1007
783
  selectedSources: sources,
@@ -1033,7 +809,7 @@ export class KnowledgeEngine {
1033
809
  const packed = this.packEntries(candidates, chunks, budgetTokens);
1034
810
  return {
1035
811
  ...packed,
1036
- warnings: [...semanticMigrationWarnings, ...coverageWarnings, ...readinessWarnings],
812
+ warnings: [...unsupportedSourceWarnings, ...coverageWarnings, ...readinessWarnings],
1037
813
  budgetTokens,
1038
814
  rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
1039
815
  retrievalDiagnostics: {
@@ -1047,25 +823,11 @@ export class KnowledgeEngine {
1047
823
  },
1048
824
  };
1049
825
  }
1050
- let bm25Results = [];
1051
- try {
1052
- bm25Results = this.store.searchBM25(query, limit * 3, sources);
1053
- }
1054
- catch {
1055
- // Retry below with a bounded literal-term query.
1056
- }
1057
- if (bm25Results.length === 0) {
1058
- const safeQuery = buildSafeFtsQuery(query);
1059
- if (safeQuery) {
1060
- try {
1061
- bm25Results = this.store.searchBM25(safeQuery, limit * 3, sources);
1062
- }
1063
- catch {
1064
- // Vector retrieval can still answer when FTS cannot parse any bounded fallback.
1065
- }
1066
- }
1067
- }
1068
- const hybrid = await this.hybridSearch.searchWithStatus(query, chunks, bm25Results, limit, sources, options?.minScore, options?.knowledgeNeeds);
826
+ const lexicalQuery = compileKnowledgeFtsQuery(query);
827
+ const bm25Results = lexicalQuery
828
+ ? this.store.searchBM25(lexicalQuery, limit * 3, sources)
829
+ : [];
830
+ const hybrid = await this.hybridSearch.searchWithStatus(query, chunks, bm25Results, limit, sources, options?.minScore, options?.knowledgeNeeds, options?.embeddingProfileHash);
1069
831
  this.lowScoreRejectionCount += hybrid.rejectedLowScore;
1070
832
  const chunkMap = new Map(chunks.map(chunk => [chunk.id, chunk]));
1071
833
  const qualityRejected = {};
@@ -1075,7 +837,7 @@ export class KnowledgeEngine {
1075
837
  query,
1076
838
  chunk,
1077
839
  retrievalScore: result.score,
1078
- versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions),
840
+ versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions, catalogBySource.get(chunk.source)?.sourceKind ?? 'plugin'),
1079
841
  action: options?.action,
1080
842
  knowledgeNeeds: options?.knowledgeNeeds,
1081
843
  });
@@ -1120,7 +882,7 @@ export class KnowledgeEngine {
1120
882
  query,
1121
883
  chunk,
1122
884
  retrievalScore: expansion.score,
1123
- versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions),
885
+ versionCompatible: isKnowledgeVersionCompatible(chunk.source, chunk.version, projectVersions, catalogBySource.get(chunk.source)?.sourceKind ?? 'plugin'),
1124
886
  action: options?.action,
1125
887
  knowledgeNeeds: options?.knowledgeNeeds,
1126
888
  });
@@ -1163,7 +925,7 @@ export class KnowledgeEngine {
1163
925
  const packed = this.packEntries(candidates, chunks, budgetTokens);
1164
926
  return {
1165
927
  ...packed,
1166
- warnings: [...semanticMigrationWarnings, ...hybrid.warnings, ...coverageWarnings, ...readinessWarnings, ...(candidates.length === 0 ? ['knowledge_insufficient'] : [])],
928
+ warnings: [...unsupportedSourceWarnings, ...hybrid.warnings, ...coverageWarnings, ...readinessWarnings, ...(candidates.length === 0 ? ['knowledge_insufficient'] : [])],
1167
929
  budgetTokens,
1168
930
  rejectedLowScore: hybrid.rejectedLowScore,
1169
931
  rejectedIncompatibleSources: selection.rejectedSources.length + rejectedByQualityGate,
@@ -1176,6 +938,9 @@ export class KnowledgeEngine {
1176
938
  hybridCandidates: hybrid.results.length,
1177
939
  qualityRejected,
1178
940
  channelCandidateCounts: hybrid.channelCandidateCounts,
941
+ vectorStatus: hybrid.vectorStatus,
942
+ embeddingProfileHash: hybrid.embeddingCapability.profile.profileHash,
943
+ embeddingGeneration: hybrid.embeddingCapability.profile.generation,
1179
944
  graphExpansions: graphCandidates.length,
1180
945
  },
1181
946
  };
@@ -1185,6 +950,11 @@ export class KnowledgeEngine {
1185
950
  const entries = [];
1186
951
  const sourceTokens = new Map();
1187
952
  const pageTokens = new Map();
953
+ const structuredPages = new Set(candidates.flatMap(entry => {
954
+ const pageId = chunkMap.get(entry.id)?.page_id;
955
+ return pageId ? [pageId] : [];
956
+ }));
957
+ const enforcePageDiversity = structuredPages.size > 1;
1188
958
  let tokenCount = 0;
1189
959
  let truncatedCount = 0;
1190
960
  for (const entry of candidates) {
@@ -1199,7 +969,7 @@ export class KnowledgeEngine {
1199
969
  const pageLimit = Math.max(entryTokens, Math.floor(budgetTokens * 0.40));
1200
970
  const overSourceBudget = Boolean(structuredPage)
1201
971
  && (sourceTokens.get(source) ?? 0) + entryTokens > sourceLimit;
1202
- const overPageBudget = Boolean(structuredPage)
972
+ const overPageBudget = enforcePageDiversity && Boolean(structuredPage)
1203
973
  && (pageTokens.get(structuredPage) ?? 0) + entryTokens > pageLimit;
1204
974
  if (tokenCount + entryTokens > budgetTokens || overSourceBudget || overPageBudget) {
1205
975
  truncatedCount++;
@@ -1230,7 +1000,11 @@ export class KnowledgeEngine {
1230
1000
  });
1231
1001
  if (chunks.length === 0)
1232
1002
  break;
1233
- const embeddings = await this.embedder.embedBatch(chunks.map(chunk => `${chunk.heading_path}\n${chunk.content}`));
1003
+ const embeddings = await requestValidatedEmbeddingBatch({
1004
+ provider: this.embedder,
1005
+ purpose: 'knowledge_retrieval',
1006
+ texts: chunks.map(chunk => `${chunk.heading_path}\n${chunk.content}`),
1007
+ });
1234
1008
  this.store.insertEmbeddingBatch(chunks.map((chunk, index) => ({
1235
1009
  id: chunk.id,
1236
1010
  embedding: embeddings[index],
@@ -1241,37 +1015,10 @@ export class KnowledgeEngine {
1241
1015
  - this.store.countEmbeddings(options.source);
1242
1016
  options.onProgress?.({ processed, remaining, lastId });
1243
1017
  }
1244
- const versions = detectProjectKnowledgeVersions(this.projectRoot);
1245
- this.store.refreshSourceReadiness(Object.fromEntries(this.store.getDistinctSources().map(source => [source, expectedVersionForSource(source, versions)])));
1246
1018
  for (const entry of this.catalog.getCatalog().sources) {
1247
1019
  if (options.source && entry.sourceId !== options.source)
1248
1020
  continue;
1249
- const chunks = this.store.countChunks(entry.sourceId);
1250
- const vectors = this.store.countEmbeddings(entry.sourceId);
1251
- const coverage = chunks > 0 ? vectors / chunks : 0;
1252
- this.catalog.activateSource({
1253
- schemaId: entry.schemaId,
1254
- version: entry.version,
1255
- contentHash: entry.contentHash,
1256
- readiness: coverage >= 0.95
1257
- ? {
1258
- status: 'ready', activeContentHash: entry.contentHash,
1259
- documents: entry.readiness.documents, chunks, vectors, coverage,
1260
- lastSuccessfulRefresh: Date.now(), lastAttempt: Date.now(),
1261
- }
1262
- : {
1263
- status: 'degraded', activeContentHash: entry.contentHash,
1264
- documents: entry.readiness.documents, chunks, vectors, coverage,
1265
- lastAttempt: Date.now(),
1266
- degradation: {
1267
- reason: 'vector_backfill_pending', category: 'vector_degraded', recoverable: true,
1268
- previousHealthyRevisionRetained: true,
1269
- },
1270
- },
1271
- entities: entry.entities,
1272
- symbols: entry.symbols,
1273
- taskKinds: entry.taskKinds,
1274
- });
1021
+ this.sources.refreshIndexReadiness(entry, options.operationTimestamp ?? Date.now());
1275
1022
  }
1276
1023
  return {
1277
1024
  processed,