byterover-cli 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +4 -0
- package/README.md +17 -0
- package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
- package/dist/agent/infra/agent/agent-schemas.js +1 -0
- package/dist/agent/infra/sandbox/curate-service.js +14 -0
- package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +10 -0
- package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +214 -101
- package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
- package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
- package/dist/agent/infra/tools/tool-registry.js +1 -1
- package/dist/agent/infra/tools/write-guard.d.ts +11 -0
- package/dist/agent/infra/tools/write-guard.js +48 -0
- package/dist/agent/resources/prompts/system-prompt.yml +9 -0
- package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
- package/dist/agent/resources/tools/search_knowledge.txt +11 -1
- package/dist/oclif/commands/curate/index.js +4 -3
- package/dist/oclif/commands/curate/view.js +2 -2
- package/dist/oclif/commands/main.js +13 -0
- package/dist/oclif/commands/query.js +4 -3
- package/dist/oclif/commands/source/add.d.ts +12 -0
- package/dist/oclif/commands/source/add.js +42 -0
- package/dist/oclif/commands/source/index.d.ts +6 -0
- package/dist/oclif/commands/source/index.js +8 -0
- package/dist/oclif/commands/source/list.d.ts +6 -0
- package/dist/oclif/commands/source/list.js +32 -0
- package/dist/oclif/commands/source/remove.d.ts +9 -0
- package/dist/oclif/commands/source/remove.js +33 -0
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +41 -6
- package/dist/oclif/commands/worktree/add.d.ts +12 -0
- package/dist/oclif/commands/worktree/add.js +44 -0
- package/dist/oclif/commands/worktree/index.d.ts +6 -0
- package/dist/oclif/commands/worktree/index.js +8 -0
- package/dist/oclif/commands/worktree/list.d.ts +6 -0
- package/dist/oclif/commands/worktree/list.js +28 -0
- package/dist/oclif/commands/worktree/remove.d.ts +9 -0
- package/dist/oclif/commands/worktree/remove.js +35 -0
- package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
- package/dist/oclif/lib/daemon-client.d.ts +4 -2
- package/dist/oclif/lib/daemon-client.js +19 -3
- package/dist/server/constants.d.ts +6 -0
- package/dist/server/constants.js +8 -0
- package/dist/server/core/domain/client/client-info.d.ts +7 -0
- package/dist/server/core/domain/client/client-info.js +11 -0
- package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
- package/dist/server/core/domain/project/worktrees-schema.js +17 -0
- package/dist/server/core/domain/source/source-operations.d.ts +31 -0
- package/dist/server/core/domain/source/source-operations.js +201 -0
- package/dist/server/core/domain/source/source-schema.d.ts +94 -0
- package/dist/server/core/domain/source/source-schema.js +121 -0
- package/dist/server/core/domain/transport/schemas.d.ts +8 -0
- package/dist/server/core/domain/transport/schemas.js +4 -0
- package/dist/server/core/domain/transport/task-info.d.ts +2 -0
- package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
- package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
- package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
- package/dist/server/infra/client/client-manager.d.ts +1 -0
- package/dist/server/infra/client/client-manager.js +16 -0
- package/dist/server/infra/daemon/agent-process.js +15 -5
- package/dist/server/infra/executor/curate-executor.js +4 -2
- package/dist/server/infra/executor/direct-search-responder.js +5 -1
- package/dist/server/infra/executor/folder-pack-executor.js +23 -12
- package/dist/server/infra/executor/query-executor.d.ts +23 -0
- package/dist/server/infra/executor/query-executor.js +115 -21
- package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
- package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
- package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
- package/dist/server/infra/mcp/mcp-server.js +11 -6
- package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
- package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
- package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
- package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
- package/dist/server/infra/process/connection-coordinator.js +11 -0
- package/dist/server/infra/process/feature-handlers.js +4 -1
- package/dist/server/infra/process/task-router.d.ts +1 -0
- package/dist/server/infra/process/task-router.js +60 -5
- package/dist/server/infra/project/resolve-project.d.ts +106 -0
- package/dist/server/infra/project/resolve-project.js +473 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/source-handler.js +37 -0
- package/dist/server/infra/transport/handlers/status-handler.js +55 -13
- package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
- package/dist/server/infra/transport/transport-connector.d.ts +10 -4
- package/dist/server/infra/transport/transport-connector.js +2 -2
- package/dist/server/utils/path-utils.d.ts +5 -0
- package/dist/server/utils/path-utils.js +11 -1
- package/dist/shared/transport/events/client-events.d.ts +3 -0
- package/dist/shared/transport/events/client-events.js +3 -0
- package/dist/shared/transport/events/index.d.ts +13 -0
- package/dist/shared/transport/events/index.js +9 -0
- package/dist/shared/transport/events/source-events.d.ts +30 -0
- package/dist/shared/transport/events/source-events.js +5 -0
- package/dist/shared/transport/events/status-events.d.ts +5 -0
- package/dist/shared/transport/events/task-events.d.ts +4 -1
- package/dist/shared/transport/events/worktree-events.d.ts +31 -0
- package/dist/shared/transport/events/worktree-events.js +5 -0
- package/dist/shared/transport/types/dto.d.ts +19 -0
- package/dist/tui/features/commands/definitions/index.js +6 -0
- package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-add.js +48 -0
- package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-list.js +47 -0
- package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-remove.js +38 -0
- package/dist/tui/features/commands/definitions/source.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source.js +8 -0
- package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
- package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
- package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
- package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree.js +8 -0
- package/dist/tui/features/curate/api/create-curate-task.js +3 -1
- package/dist/tui/features/query/api/create-query-task.js +3 -1
- package/dist/tui/features/source/api/source-api.d.ts +4 -0
- package/dist/tui/features/source/api/source-api.js +22 -0
- package/dist/tui/features/status/api/get-status.js +2 -1
- package/dist/tui/features/status/utils/format-status.js +23 -1
- package/dist/tui/features/transport/components/transport-initializer.js +36 -1
- package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
- package/dist/tui/features/worktree/api/worktree-api.js +22 -0
- package/dist/tui/repl-startup.d.ts +2 -0
- package/dist/tui/repl-startup.js +5 -3
- package/dist/tui/stores/transport-store.d.ts +6 -0
- package/dist/tui/stores/transport-store.js +6 -0
- package/oclif.manifest.json +418 -158
- package/package.json +1 -1
|
@@ -2,17 +2,18 @@ import MiniSearch from 'minisearch';
|
|
|
2
2
|
import { realpath } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { removeStopwords } from 'stopword';
|
|
5
|
-
import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, OVERVIEW_EXTENSION, SUMMARY_INDEX_FILE } from '../../../../server/constants.js';
|
|
5
|
+
import { BRV_DIR, CONTEXT_FILE_EXTENSION, CONTEXT_TREE_DIR, OVERVIEW_EXTENSION, SHARED_SOURCE_LOCAL_SCORE_BOOST, SUMMARY_INDEX_FILE, } from '../../../../server/constants.js';
|
|
6
6
|
import { parseFrontmatterScoring, updateScoringInContent, } from '../../../../server/core/domain/knowledge/markdown-writer.js';
|
|
7
7
|
import { applyDecay, applyDefaultScoring, compoundScore, determineTier, recordAccessHits, } from '../../../../server/core/domain/knowledge/memory-scoring.js';
|
|
8
|
+
import { loadSources } from '../../../../server/core/domain/source/source-schema.js';
|
|
8
9
|
import { isArchiveStub, isDerivedArtifact } from '../../../../server/infra/context-tree/derived-artifact.js';
|
|
9
|
-
import { parseArchiveStubFrontmatter, parseSummaryFrontmatter } from '../../../../server/infra/context-tree/summary-frontmatter.js';
|
|
10
|
+
import { parseArchiveStubFrontmatter, parseSummaryFrontmatter, } from '../../../../server/infra/context-tree/summary-frontmatter.js';
|
|
10
11
|
import { isPathLikeQuery, matchMemoryPath, parseSymbolicQuery } from './memory-path-matcher.js';
|
|
11
12
|
import { buildReferenceIndex, buildSymbolTree, getSubtreeDocumentIds, getSymbolKindLabel, getSymbolOverview, MemorySymbolKind, } from './memory-symbol-tree.js';
|
|
12
13
|
const MAX_CONTEXT_TREE_FILES = 10_000;
|
|
13
14
|
const DEFAULT_CACHE_TTL_MS = 5000;
|
|
14
15
|
/** Bump when MINISEARCH_OPTIONS fields/boost change to invalidate cached indexes */
|
|
15
|
-
const INDEX_SCHEMA_VERSION =
|
|
16
|
+
const INDEX_SCHEMA_VERSION = 5;
|
|
16
17
|
/** Only include results whose normalized score is at least this fraction of the top result's score */
|
|
17
18
|
const SCORE_GAP_RATIO = 0.7;
|
|
18
19
|
/** Minimum normalized score for the top result. Below this, the query is considered out-of-domain */
|
|
@@ -34,6 +35,12 @@ const CHUNK_OVERLAP_CHARS = 120;
|
|
|
34
35
|
function normalizeScore(rawScore) {
|
|
35
36
|
return rawScore / (1 + rawScore);
|
|
36
37
|
}
|
|
38
|
+
function getSymbolPath(origin, relativePath) {
|
|
39
|
+
if (origin.origin === 'local') {
|
|
40
|
+
return relativePath;
|
|
41
|
+
}
|
|
42
|
+
return `[${origin.alias ?? origin.originKey}]:${relativePath}`;
|
|
43
|
+
}
|
|
37
44
|
/**
|
|
38
45
|
* Propagate BM25 scores upward to parent domain/topic nodes.
|
|
39
46
|
*
|
|
@@ -44,10 +51,11 @@ function normalizeScore(rawScore) {
|
|
|
44
51
|
* @param results - Already-enriched search results (gap-ratio filtered)
|
|
45
52
|
* @param symbolTree - Symbol tree for parent-chain traversal
|
|
46
53
|
* @param summaryMap - Map of _index.md file paths → SummaryDocLike (for excerpt/metadata)
|
|
54
|
+
* @param symbolPathDocMap - symbolPath → IndexedDocument lookup for context.md fallback
|
|
47
55
|
* @param propagationFactor - Score multiplier per level up (default 0.55)
|
|
48
56
|
* @returns New parent entries only — caller merges and re-sorts
|
|
49
57
|
*/
|
|
50
|
-
function propagateScoresToParents(results, symbolTree, summaryMap,
|
|
58
|
+
function propagateScoresToParents(results, symbolTree, summaryMap, symbolPathDocMap, propagationFactor = 0.55) {
|
|
51
59
|
const boosts = new Map();
|
|
52
60
|
for (const r of results) {
|
|
53
61
|
const symbol = symbolTree.symbolMap.get(r.path);
|
|
@@ -65,7 +73,7 @@ function propagateScoresToParents(results, symbolTree, summaryMap, documentMap,
|
|
|
65
73
|
for (const [parentPath, score] of boosts.entries()) {
|
|
66
74
|
if (existingPaths.has(parentPath))
|
|
67
75
|
continue;
|
|
68
|
-
const doc = getSummarySource(parentPath, summaryMap,
|
|
76
|
+
const doc = getSummarySource(parentPath, summaryMap, symbolPathDocMap);
|
|
69
77
|
if (!doc)
|
|
70
78
|
continue;
|
|
71
79
|
// Propagate the strongest child BM25 signal upward, then apply the parent
|
|
@@ -97,10 +105,10 @@ const MINISEARCH_OPTIONS = {
|
|
|
97
105
|
},
|
|
98
106
|
storeFields: ['title', 'path'],
|
|
99
107
|
};
|
|
100
|
-
function getSummaryAccessPath(path, summaryMap,
|
|
101
|
-
return getSummarySource(path, summaryMap,
|
|
108
|
+
function getSummaryAccessPath(path, summaryMap, symbolPathDocMap) {
|
|
109
|
+
return getSummarySource(path, summaryMap, symbolPathDocMap)?.path ?? `${path}/${SUMMARY_INDEX_FILE}`;
|
|
102
110
|
}
|
|
103
|
-
function getSummarySource(path, summaryMap,
|
|
111
|
+
function getSummarySource(path, summaryMap, symbolPathDocMap) {
|
|
104
112
|
const summaryDoc = summaryMap.get(`${path}/${SUMMARY_INDEX_FILE}`);
|
|
105
113
|
if (summaryDoc) {
|
|
106
114
|
return {
|
|
@@ -109,7 +117,9 @@ function getSummarySource(path, summaryMap, documentMap) {
|
|
|
109
117
|
scoring: summaryDoc.scoring,
|
|
110
118
|
};
|
|
111
119
|
}
|
|
112
|
-
|
|
120
|
+
// Look up context.md via symbolPath-keyed map since documentMap keys are
|
|
121
|
+
// origin-qualified (e.g. 'local::path') but callers use symbol tree paths.
|
|
122
|
+
const contextDoc = symbolPathDocMap.get(`${path}/context.md`);
|
|
113
123
|
if (contextDoc) {
|
|
114
124
|
return {
|
|
115
125
|
excerpt: extractExcerpt(contextDoc.content, contextDoc.title),
|
|
@@ -276,22 +286,12 @@ function isCacheValid(cache, currentFiles) {
|
|
|
276
286
|
}
|
|
277
287
|
return true;
|
|
278
288
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
documentMap: new Map(),
|
|
286
|
-
fileMtimes: new Map(),
|
|
287
|
-
index,
|
|
288
|
-
lastValidatedAt: now,
|
|
289
|
-
referenceIndex: { backlinks: new Map(), forwardLinks: new Map() },
|
|
290
|
-
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
291
|
-
summaryMap: new Map(),
|
|
292
|
-
symbolTree: { root: [], symbolMap: new Map() },
|
|
293
|
-
};
|
|
294
|
-
}
|
|
289
|
+
/**
|
|
290
|
+
* Read and index documents from a single context tree origin.
|
|
291
|
+
* Returns documents with origin-qualified IDs (<originKey>::<path>)
|
|
292
|
+
* and summary docs keyed by origin-qualified paths.
|
|
293
|
+
*/
|
|
294
|
+
async function indexOriginDocuments(fileSystem, origin, filesWithMtime) {
|
|
295
295
|
// Partition files: _index.md → summaryFiles, .overview.md → overviewFiles (for cache
|
|
296
296
|
// invalidation + sibling detection), other derived artifacts → skip, rest → indexable
|
|
297
297
|
const summaryFiles = [];
|
|
@@ -309,39 +309,45 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
309
309
|
overviewFiles.push(file);
|
|
310
310
|
}
|
|
311
311
|
else if (!isDerivedArtifact(file.path)) {
|
|
312
|
-
// Includes regular .md files AND .stub.md files (stubs are searchable)
|
|
313
312
|
indexableFiles.push(file);
|
|
314
313
|
}
|
|
315
314
|
// .full.md, .abstract.md, and _manifest.json are skipped (isDerivedArtifact returns true)
|
|
316
315
|
}
|
|
317
|
-
// Read indexable documents for BM25 index
|
|
318
316
|
const documentPromises = indexableFiles.map(async ({ mtime, path: filePath }) => {
|
|
319
317
|
try {
|
|
320
|
-
const fullPath = join(
|
|
318
|
+
const fullPath = join(origin.contextTreeRoot, filePath);
|
|
321
319
|
const { content } = await fileSystem.readFile(fullPath);
|
|
322
320
|
const title = extractTitle(content, filePath.replace(/\.md$/, '').split('/').pop() || filePath);
|
|
323
321
|
const scoring = parseFrontmatterScoring(content) ?? applyDefaultScoring();
|
|
322
|
+
const qualifiedId = `${origin.originKey}::${filePath}`;
|
|
323
|
+
const symbolPath = getSymbolPath(origin, filePath);
|
|
324
324
|
// Check if a .overview.md sibling exists (written by abstract generation queue)
|
|
325
325
|
const overviewRelPath = filePath.replace(/\.md$/, OVERVIEW_EXTENSION);
|
|
326
326
|
const overviewPath = knownPaths.has(overviewRelPath) ? overviewRelPath : undefined;
|
|
327
|
-
|
|
327
|
+
const doc = {
|
|
328
328
|
content,
|
|
329
|
-
id:
|
|
329
|
+
id: qualifiedId,
|
|
330
330
|
mtime,
|
|
331
|
+
origin: origin.origin,
|
|
332
|
+
originContextTreeRoot: origin.contextTreeRoot,
|
|
333
|
+
originKey: origin.originKey,
|
|
331
334
|
...(overviewPath !== undefined && { overviewPath }),
|
|
332
335
|
path: filePath,
|
|
333
336
|
scoring,
|
|
337
|
+
symbolPath,
|
|
334
338
|
title,
|
|
335
339
|
};
|
|
340
|
+
if (origin.alias)
|
|
341
|
+
doc.originAlias = origin.alias;
|
|
342
|
+
return doc;
|
|
336
343
|
}
|
|
337
344
|
catch {
|
|
338
345
|
return null;
|
|
339
346
|
}
|
|
340
347
|
});
|
|
341
|
-
// Read _index.md files separately for summaryMap (not indexed in BM25)
|
|
342
348
|
const summaryPromises = summaryFiles.map(async ({ path: filePath }) => {
|
|
343
349
|
try {
|
|
344
|
-
const fullPath = join(
|
|
350
|
+
const fullPath = join(origin.contextTreeRoot, filePath);
|
|
345
351
|
const { content } = await fileSystem.readFile(fullPath);
|
|
346
352
|
const fm = parseSummaryFrontmatter(content);
|
|
347
353
|
if (!fm)
|
|
@@ -354,7 +360,7 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
354
360
|
return {
|
|
355
361
|
condensationOrder: fm.condensation_order,
|
|
356
362
|
excerpt: stripMarkdownFrontmatter(content).slice(0, 400),
|
|
357
|
-
path: filePath,
|
|
363
|
+
path: getSymbolPath(origin, filePath),
|
|
358
364
|
scoring,
|
|
359
365
|
tokenCount: fm.token_count,
|
|
360
366
|
};
|
|
@@ -363,24 +369,18 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
363
369
|
return null;
|
|
364
370
|
}
|
|
365
371
|
});
|
|
366
|
-
const [docResults, summaryResults] = await Promise.all([
|
|
367
|
-
Promise.all(documentPromises),
|
|
368
|
-
Promise.all(summaryPromises),
|
|
369
|
-
]);
|
|
372
|
+
const [docResults, summaryResults] = await Promise.all([Promise.all(documentPromises), Promise.all(summaryPromises)]);
|
|
370
373
|
const documents = docResults.filter((doc) => doc !== null);
|
|
371
|
-
const documentMap = new Map();
|
|
372
374
|
const fileMtimes = new Map();
|
|
373
375
|
for (const doc of documents) {
|
|
374
|
-
|
|
375
|
-
fileMtimes.set(doc.path, doc.mtime);
|
|
376
|
+
fileMtimes.set(doc.id, doc.mtime);
|
|
376
377
|
}
|
|
377
|
-
// Also track summary file mtimes for cache invalidation
|
|
378
378
|
for (const sf of summaryFiles) {
|
|
379
|
-
fileMtimes.set(sf.path
|
|
379
|
+
fileMtimes.set(`${origin.originKey}::${sf.path}`, sf.mtime);
|
|
380
380
|
}
|
|
381
381
|
// Track .overview.md mtimes so the cache invalidates when a new overview is written
|
|
382
382
|
for (const ov of overviewFiles) {
|
|
383
|
-
fileMtimes.set(ov.path
|
|
383
|
+
fileMtimes.set(`${origin.originKey}::${ov.path}`, ov.mtime);
|
|
384
384
|
}
|
|
385
385
|
const summaryMap = new Map();
|
|
386
386
|
for (const summary of summaryResults) {
|
|
@@ -388,28 +388,87 @@ async function buildFreshIndex(fileSystem, contextTreePath, filesWithMtime) {
|
|
|
388
388
|
summaryMap.set(summary.path, summary);
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
|
+
return { documents, fileMtimes, summaryMap };
|
|
392
|
+
}
|
|
393
|
+
async function buildFreshIndex(fileSystem, contextTreePath, localFiles, sharedOrigins, sourcesFileMtime) {
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
// Build the local origin descriptor.
|
|
396
|
+
// Note: `originKey: 'local'` is a string sentinel — shared origins use a 12-char
|
|
397
|
+
// SHA-256 hex hash from `deriveOriginKey()`. Consumers comparing originKey should
|
|
398
|
+
// treat 'local' as a reserved literal, not a hash.
|
|
399
|
+
const localOrigin = {
|
|
400
|
+
contextTreeRoot: contextTreePath,
|
|
401
|
+
origin: 'local',
|
|
402
|
+
originKey: 'local',
|
|
403
|
+
};
|
|
404
|
+
// Index local documents
|
|
405
|
+
const localResult = await indexOriginDocuments(fileSystem, localOrigin, localFiles);
|
|
406
|
+
// Index shared origin documents in parallel
|
|
407
|
+
const sharedResults = await Promise.all(sharedOrigins.map(async (origin) => {
|
|
408
|
+
try {
|
|
409
|
+
const files = await findMarkdownFilesWithMtime(fileSystem, origin.contextTreeRoot);
|
|
410
|
+
const filtered = files.filter((f) => !isDerivedArtifact(f.path) || f.path.split('/').at(-1) === SUMMARY_INDEX_FILE);
|
|
411
|
+
return indexOriginDocuments(fileSystem, origin, filtered);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return { documents: [], fileMtimes: new Map(), summaryMap: new Map() };
|
|
415
|
+
}
|
|
416
|
+
}));
|
|
417
|
+
// Merge all documents, fileMtimes, and summaryMaps
|
|
418
|
+
const allDocuments = [...localResult.documents];
|
|
419
|
+
const fileMtimes = new Map(localResult.fileMtimes);
|
|
420
|
+
const summaryMap = new Map(localResult.summaryMap);
|
|
421
|
+
for (const result of sharedResults) {
|
|
422
|
+
allDocuments.push(...result.documents);
|
|
423
|
+
for (const [key, mtime] of result.fileMtimes) {
|
|
424
|
+
fileMtimes.set(key, mtime);
|
|
425
|
+
}
|
|
426
|
+
for (const [key, summary] of result.summaryMap) {
|
|
427
|
+
summaryMap.set(key, summary);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const documentMap = new Map();
|
|
431
|
+
const pathToDocumentId = new Map();
|
|
432
|
+
// Reverse lookup: symbolPath → document (for getSummarySource context.md fallback)
|
|
433
|
+
const symbolPathDocMap = new Map();
|
|
434
|
+
for (const doc of allDocuments) {
|
|
435
|
+
documentMap.set(doc.id, doc);
|
|
436
|
+
pathToDocumentId.set(doc.symbolPath, doc.id);
|
|
437
|
+
symbolPathDocMap.set(doc.symbolPath, doc);
|
|
438
|
+
}
|
|
391
439
|
const index = new MiniSearch(MINISEARCH_OPTIONS);
|
|
392
|
-
index.addAll(
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
440
|
+
index.addAll(allDocuments);
|
|
441
|
+
const symbolDocumentMap = new Map();
|
|
442
|
+
for (const doc of allDocuments) {
|
|
443
|
+
symbolDocumentMap.set(doc.id, { ...doc, path: doc.symbolPath });
|
|
444
|
+
}
|
|
445
|
+
const symbolTree = buildSymbolTree(symbolDocumentMap, summaryMap);
|
|
446
|
+
// Reference index only uses local docs — cross-project references are not tracked
|
|
447
|
+
const referenceIndex = buildReferenceIndex(new Map(localResult.documents.map((doc) => [doc.id, doc])));
|
|
396
448
|
return {
|
|
397
449
|
contextTreePath,
|
|
398
450
|
documentMap,
|
|
399
451
|
fileMtimes,
|
|
400
452
|
index,
|
|
401
453
|
lastValidatedAt: now,
|
|
454
|
+
pathToDocumentId,
|
|
402
455
|
referenceIndex,
|
|
403
456
|
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
457
|
+
sharedOrigins,
|
|
458
|
+
sourcesFileMtime,
|
|
404
459
|
summaryMap,
|
|
460
|
+
symbolPathDocMap,
|
|
405
461
|
symbolTree,
|
|
406
462
|
};
|
|
407
463
|
}
|
|
408
464
|
/**
|
|
409
465
|
* Acquires the search index, using cached data when valid or building a fresh index.
|
|
410
466
|
* Uses promise-based locking to prevent duplicate builds during parallel execution.
|
|
467
|
+
*
|
|
468
|
+
* Self-loads knowledge sources from `.brv/sources.json` during each validation
|
|
469
|
+
* cycle, with mtime-based invalidation to detect source additions/removals.
|
|
411
470
|
*/
|
|
412
|
-
async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeBuild) {
|
|
471
|
+
async function acquireIndex(state, fileSystem, contextTreePath, baseDirectory, ttlMs, onBeforeBuild) {
|
|
413
472
|
const now = Date.now();
|
|
414
473
|
// Fast path: TTL-based cache hit (no I/O needed)
|
|
415
474
|
if (state.cachedIndex &&
|
|
@@ -425,6 +484,23 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
|
|
|
425
484
|
}
|
|
426
485
|
// Create and store the build promise SYNCHRONOUSLY before any await
|
|
427
486
|
// This prevents race conditions where multiple parallel calls all start building
|
|
487
|
+
const emptyResult = () => {
|
|
488
|
+
const emptyIndex = new MiniSearch(MINISEARCH_OPTIONS);
|
|
489
|
+
return {
|
|
490
|
+
contextTreePath: '',
|
|
491
|
+
documentMap: new Map(),
|
|
492
|
+
fileMtimes: new Map(),
|
|
493
|
+
index: emptyIndex,
|
|
494
|
+
lastValidatedAt: 0,
|
|
495
|
+
pathToDocumentId: new Map(),
|
|
496
|
+
referenceIndex: { backlinks: new Map(), forwardLinks: new Map() },
|
|
497
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
498
|
+
sharedOrigins: [],
|
|
499
|
+
summaryMap: new Map(),
|
|
500
|
+
symbolPathDocMap: new Map(),
|
|
501
|
+
symbolTree: { root: [], symbolMap: new Map() },
|
|
502
|
+
};
|
|
503
|
+
};
|
|
428
504
|
const buildPromise = (async () => {
|
|
429
505
|
// Check if context tree exists (only if no cache or different path)
|
|
430
506
|
if (!state.cachedIndex || state.cachedIndex.contextTreePath !== contextTreePath) {
|
|
@@ -432,21 +508,13 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
|
|
|
432
508
|
await fileSystem.listDirectory(contextTreePath);
|
|
433
509
|
}
|
|
434
510
|
catch {
|
|
435
|
-
|
|
436
|
-
const emptyIndex = new MiniSearch(MINISEARCH_OPTIONS);
|
|
437
|
-
return {
|
|
438
|
-
contextTreePath: '',
|
|
439
|
-
documentMap: new Map(),
|
|
440
|
-
fileMtimes: new Map(),
|
|
441
|
-
index: emptyIndex,
|
|
442
|
-
lastValidatedAt: 0,
|
|
443
|
-
referenceIndex: { backlinks: new Map(), forwardLinks: new Map() },
|
|
444
|
-
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
445
|
-
summaryMap: new Map(),
|
|
446
|
-
symbolTree: { root: [], symbolMap: new Map() },
|
|
447
|
-
};
|
|
511
|
+
return emptyResult();
|
|
448
512
|
}
|
|
449
513
|
}
|
|
514
|
+
// Self-load knowledge sources — mtime-based invalidation
|
|
515
|
+
const loadedSources = loadSources(baseDirectory);
|
|
516
|
+
const sourcesFileMtime = loadedSources?.mtime;
|
|
517
|
+
const sharedOrigins = loadedSources?.origins ?? [];
|
|
450
518
|
let allFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
|
|
451
519
|
// Exclude non-indexable derived artifacts (.full.md) so that currentFiles
|
|
452
520
|
// matches what buildFreshIndex tracks in fileMtimes. Without this filter,
|
|
@@ -454,7 +522,7 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
|
|
|
454
522
|
// _index.md is kept (tracked for summary staleness), .stub.md is kept (BM25 indexed).
|
|
455
523
|
// Keep _index.md (summary tracking) and .overview.md (sibling detection for overviewPath).
|
|
456
524
|
// .full.md, .abstract.md, and _manifest.json remain excluded.
|
|
457
|
-
let
|
|
525
|
+
let localFiles = allFiles.filter((f) => !isDerivedArtifact(f.path) ||
|
|
458
526
|
f.path.split('/').at(-1) === SUMMARY_INDEX_FILE ||
|
|
459
527
|
f.path.endsWith(OVERVIEW_EXTENSION));
|
|
460
528
|
// Flush pending access hits before reusing a stale-enough cache entry.
|
|
@@ -463,16 +531,32 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
|
|
|
463
531
|
const wroteScoringUpdates = await onBeforeBuild(contextTreePath);
|
|
464
532
|
if (wroteScoringUpdates) {
|
|
465
533
|
allFiles = await findMarkdownFilesWithMtime(fileSystem, contextTreePath);
|
|
466
|
-
|
|
534
|
+
localFiles = allFiles.filter((f) => !isDerivedArtifact(f.path) ||
|
|
467
535
|
f.path.split('/').at(-1) === SUMMARY_INDEX_FILE ||
|
|
468
536
|
f.path.endsWith(OVERVIEW_EXTENSION));
|
|
469
537
|
}
|
|
470
538
|
}
|
|
471
|
-
//
|
|
472
|
-
|
|
539
|
+
// Qualify local file mtime keys with 'local::' prefix to match buildFreshIndex
|
|
540
|
+
const qualifiedLocalFiles = localFiles.map((f) => ({ mtime: f.mtime, path: `local::${f.path}` }));
|
|
541
|
+
// Glob shared origin files for cache validation (detect edits in shared projects)
|
|
542
|
+
const sharedFileArrays = await Promise.all(sharedOrigins.map(async (origin) => {
|
|
543
|
+
try {
|
|
544
|
+
const files = await findMarkdownFilesWithMtime(fileSystem, origin.contextTreeRoot);
|
|
545
|
+
const filtered = files.filter((f) => !isDerivedArtifact(f.path) || f.path.split('/').at(-1) === SUMMARY_INDEX_FILE);
|
|
546
|
+
return filtered.map((f) => ({ mtime: f.mtime, path: `${origin.originKey}::${f.path}` }));
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
return [];
|
|
550
|
+
}
|
|
551
|
+
}));
|
|
552
|
+
const allQualifiedFiles = [...qualifiedLocalFiles, ...sharedFileArrays.flat()];
|
|
553
|
+
// Re-check cache validity: local files + shared files + sources-file mtime must match
|
|
554
|
+
const sourcesFileChanged = state.cachedIndex?.sourcesFileMtime !== sourcesFileMtime;
|
|
555
|
+
if (!sourcesFileChanged &&
|
|
556
|
+
state.cachedIndex &&
|
|
473
557
|
state.cachedIndex.contextTreePath === contextTreePath &&
|
|
474
558
|
state.cachedIndex.schemaVersion === INDEX_SCHEMA_VERSION &&
|
|
475
|
-
isCacheValid(state.cachedIndex,
|
|
559
|
+
isCacheValid(state.cachedIndex, allQualifiedFiles)) {
|
|
476
560
|
// Update timestamp atomically by creating a new object
|
|
477
561
|
const updatedCache = {
|
|
478
562
|
...state.cachedIndex,
|
|
@@ -481,8 +565,8 @@ async function acquireIndex(state, fileSystem, contextTreePath, ttlMs, onBeforeB
|
|
|
481
565
|
state.cachedIndex = updatedCache;
|
|
482
566
|
return updatedCache;
|
|
483
567
|
}
|
|
484
|
-
// Build fresh index
|
|
485
|
-
const freshIndex = await buildFreshIndex(fileSystem, contextTreePath,
|
|
568
|
+
// Build fresh index with local + shared origins
|
|
569
|
+
const freshIndex = await buildFreshIndex(fileSystem, contextTreePath, localFiles, sharedOrigins, sourcesFileMtime);
|
|
486
570
|
state.cachedIndex = freshIndex;
|
|
487
571
|
return freshIndex;
|
|
488
572
|
})();
|
|
@@ -568,12 +652,12 @@ export class SearchKnowledgeService {
|
|
|
568
652
|
const resolvedBaseDirectory = await realpath(this.baseDirectory).catch(() => this.baseDirectory);
|
|
569
653
|
const contextTreePath = join(resolvedBaseDirectory, BRV_DIR, CONTEXT_TREE_DIR);
|
|
570
654
|
// Acquire index with parallel-safe locking; flush pending access hits before any rebuild
|
|
571
|
-
const indexResult = await acquireIndex(this.state, this.fileSystem, contextTreePath, this.cacheTtlMs, (ctxPath) => this.flushAccessHits(ctxPath));
|
|
655
|
+
const indexResult = await acquireIndex(this.state, this.fileSystem, contextTreePath, this.baseDirectory, this.cacheTtlMs, (ctxPath) => this.flushAccessHits(ctxPath));
|
|
572
656
|
// Handle error case (context tree not initialized)
|
|
573
657
|
if ('error' in indexResult) {
|
|
574
658
|
return indexResult.result;
|
|
575
659
|
}
|
|
576
|
-
const { documentMap, index, referenceIndex, summaryMap, symbolTree } = indexResult;
|
|
660
|
+
const { documentMap, index, pathToDocumentId, referenceIndex, summaryMap, symbolPathDocMap, symbolTree } = indexResult;
|
|
577
661
|
if (documentMap.size === 0) {
|
|
578
662
|
return {
|
|
579
663
|
message: 'Context tree is empty. Use /curate to add knowledge.',
|
|
@@ -587,7 +671,7 @@ export class SearchKnowledgeService {
|
|
|
587
671
|
}
|
|
588
672
|
// Symbolic path resolution: try path-based query first
|
|
589
673
|
if (isPathLikeQuery(query, symbolTree)) {
|
|
590
|
-
const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, summaryMap, options);
|
|
674
|
+
const symbolicResult = this.trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, pathToDocumentId, summaryMap, symbolPathDocMap, options);
|
|
591
675
|
if (symbolicResult) {
|
|
592
676
|
return symbolicResult;
|
|
593
677
|
}
|
|
@@ -597,10 +681,10 @@ export class SearchKnowledgeService {
|
|
|
597
681
|
const effectiveScope = options?.scope ?? parsed.scopePath;
|
|
598
682
|
const effectiveQuery = parsed.scopePath ? parsed.textQuery : query;
|
|
599
683
|
// Run text-based MiniSearch (existing pipeline), optionally scoped to a subtree
|
|
600
|
-
const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, symbolTree, referenceIndex, summaryMap, options);
|
|
684
|
+
const textResult = this.runTextSearch(effectiveQuery || query, documentMap, index, limit, effectiveScope, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options);
|
|
601
685
|
// If scoped search returned nothing and we had a scope, fall back to global search
|
|
602
686
|
if (textResult.results.length === 0 && effectiveScope && effectiveQuery) {
|
|
603
|
-
return this.runTextSearch(query, documentMap, index, limit, undefined, symbolTree, referenceIndex, summaryMap, options);
|
|
687
|
+
return this.runTextSearch(query, documentMap, index, limit, undefined, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options);
|
|
604
688
|
}
|
|
605
689
|
return textResult;
|
|
606
690
|
}
|
|
@@ -636,14 +720,15 @@ export class SearchKnowledgeService {
|
|
|
636
720
|
* For archive stubs, extracts points_to path into archiveFullPath.
|
|
637
721
|
*/
|
|
638
722
|
enrichResult(result, symbolTree, referenceIndex, documentMap) {
|
|
639
|
-
const
|
|
723
|
+
const doc = documentMap.get(result.id);
|
|
724
|
+
const symbolPath = doc?.symbolPath ?? result.path;
|
|
725
|
+
const symbol = symbolTree.symbolMap.get(symbolPath);
|
|
640
726
|
const backlinks = referenceIndex.backlinks.get(result.path);
|
|
641
727
|
// Detect archive stubs and extract points_to for drill-down
|
|
642
728
|
let archiveFullPath;
|
|
643
729
|
let symbolKind = symbol ? getSymbolKindLabel(symbol.kind) : undefined;
|
|
644
730
|
if (isArchiveStub(result.path)) {
|
|
645
731
|
symbolKind = 'archive_stub';
|
|
646
|
-
const doc = documentMap.get(result.path);
|
|
647
732
|
if (doc) {
|
|
648
733
|
const stubFm = parseArchiveStubFrontmatter(doc.content);
|
|
649
734
|
if (stubFm) {
|
|
@@ -651,17 +736,26 @@ export class SearchKnowledgeService {
|
|
|
651
736
|
}
|
|
652
737
|
}
|
|
653
738
|
}
|
|
654
|
-
|
|
739
|
+
// Origin metadata for shared-source results
|
|
740
|
+
const origin = doc?.origin;
|
|
741
|
+
const originAlias = doc?.originAlias;
|
|
742
|
+
const originContextTreeRoot = doc?.origin === 'shared' ? doc.originContextTreeRoot : undefined;
|
|
655
743
|
const overviewPath = doc?.overviewPath;
|
|
656
744
|
const isContextSummary = doc?.path.endsWith('/context.md') || doc?.path === 'context.md';
|
|
657
745
|
const summaryPath = isContextSummary
|
|
658
746
|
? doc?.path.slice(0, -'/context.md'.length) || doc?.path || result.path
|
|
659
747
|
: result.path;
|
|
748
|
+
// Destructure to strip `id` from output — not part of SearchKnowledgeResult
|
|
749
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
750
|
+
const { id: _id, ...rest } = result;
|
|
660
751
|
return {
|
|
661
|
-
...
|
|
752
|
+
...rest,
|
|
662
753
|
...(archiveFullPath && { archiveFullPath }),
|
|
663
754
|
...(overviewPath && { overviewPath }),
|
|
664
755
|
backlinkCount: backlinks?.length ?? 0,
|
|
756
|
+
...(origin && { origin }),
|
|
757
|
+
...(originAlias && { originAlias }),
|
|
758
|
+
...(originContextTreeRoot && { originContextTreeRoot }),
|
|
665
759
|
...(isContextSummary && { path: summaryPath }),
|
|
666
760
|
relatedPaths: backlinks?.slice(0, 3),
|
|
667
761
|
symbolKind: isContextSummary ? 'summary' : symbolKind,
|
|
@@ -671,15 +765,22 @@ export class SearchKnowledgeService {
|
|
|
671
765
|
/**
|
|
672
766
|
* Run the standard text-based MiniSearch pipeline, optionally scoped to a subtree.
|
|
673
767
|
*/
|
|
674
|
-
runTextSearch(query, documentMap, index, limit, scopePath, symbolTree, referenceIndex, summaryMap, options) {
|
|
768
|
+
runTextSearch(query, documentMap, index, limit, scopePath, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options) {
|
|
675
769
|
const filteredQuery = filterStopWords(query);
|
|
676
770
|
const filteredWords = filteredQuery.split(/\s+/).filter((w) => w.length >= 2);
|
|
677
771
|
// Build scope filter if a subtree is specified
|
|
678
772
|
let scopeFilter;
|
|
679
773
|
if (scopePath) {
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
774
|
+
const subtreePaths = getSubtreeDocumentIds(symbolTree, scopePath);
|
|
775
|
+
const subtreeQualifiedIds = new Set();
|
|
776
|
+
for (const symbolPath of subtreePaths) {
|
|
777
|
+
const docId = pathToDocumentId.get(symbolPath);
|
|
778
|
+
if (docId) {
|
|
779
|
+
subtreeQualifiedIds.add(docId);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (subtreeQualifiedIds.size > 0) {
|
|
783
|
+
scopeFilter = (result) => subtreeQualifiedIds.has(result.id);
|
|
683
784
|
}
|
|
684
785
|
}
|
|
685
786
|
// AND-first strategy: for multi-word queries, try AND for concentrated scores.
|
|
@@ -699,6 +800,7 @@ export class SearchKnowledgeService {
|
|
|
699
800
|
}
|
|
700
801
|
// Normalize BM25 scores to [0, 1) then blend with importance + recency via compound scoring.
|
|
701
802
|
// Decay is computed lazily from file mtime — no disk writes during search.
|
|
803
|
+
// Local results get a configurable score boost to prefer local knowledge over shared.
|
|
702
804
|
const now = Date.now();
|
|
703
805
|
const searchResults = rawResults.map((r) => {
|
|
704
806
|
const doc = documentMap.get(r.id);
|
|
@@ -706,10 +808,15 @@ export class SearchKnowledgeService {
|
|
|
706
808
|
const daysSince = doc ? Math.max(0, (now - doc.mtime) / 86_400_000) : 0;
|
|
707
809
|
const decayed = applyDecay(scoring, daysSince);
|
|
708
810
|
const bm25 = normalizeScore(r.score);
|
|
811
|
+
let finalScore = compoundScore(bm25, decayed.importance ?? 50, decayed.recency ?? 1, decayed.maturity ?? 'draft');
|
|
812
|
+
// Local score boost: prefer local results over shared when scores are close
|
|
813
|
+
if (doc?.origin === 'local') {
|
|
814
|
+
finalScore = Math.min(finalScore + SHARED_SOURCE_LOCAL_SCORE_BOOST, 1);
|
|
815
|
+
}
|
|
709
816
|
return {
|
|
710
817
|
...r,
|
|
711
818
|
bm25Score: bm25,
|
|
712
|
-
score:
|
|
819
|
+
score: finalScore,
|
|
713
820
|
};
|
|
714
821
|
});
|
|
715
822
|
searchResults.sort((a, b) => b.score - a.score);
|
|
@@ -752,6 +859,7 @@ export class SearchKnowledgeService {
|
|
|
752
859
|
if (document) {
|
|
753
860
|
const enriched = this.enrichResult({
|
|
754
861
|
excerpt: extractExcerpt(document.content, query),
|
|
862
|
+
id: result.id,
|
|
755
863
|
path: document.path,
|
|
756
864
|
score: Math.round(result.score * 100) / 100,
|
|
757
865
|
title: document.title,
|
|
@@ -765,10 +873,10 @@ export class SearchKnowledgeService {
|
|
|
765
873
|
}
|
|
766
874
|
if (options?.minMaturity && enriched.symbolKind) {
|
|
767
875
|
const docMaturity = enriched.symbolKind === 'summary'
|
|
768
|
-
? getSummarySource(enriched.path, summaryMap,
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
: symbolTree.symbolMap.get(document.
|
|
876
|
+
? (getSummarySource(enriched.path, summaryMap, symbolPathDocMap)?.scoring?.maturity ??
|
|
877
|
+
symbolTree.symbolMap.get(enriched.path)?.metadata.maturity ??
|
|
878
|
+
'draft')
|
|
879
|
+
: (symbolTree.symbolMap.get(document.symbolPath)?.metadata.maturity ?? 'draft');
|
|
772
880
|
if ((MATURITY_TIER_RANK[docMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1)) {
|
|
773
881
|
continue;
|
|
774
882
|
}
|
|
@@ -776,14 +884,17 @@ export class SearchKnowledgeService {
|
|
|
776
884
|
results.push(enriched);
|
|
777
885
|
propagationInputs.push({
|
|
778
886
|
bm25Score: result.bm25Score,
|
|
779
|
-
path: document.
|
|
887
|
+
path: document.symbolPath,
|
|
780
888
|
});
|
|
781
889
|
}
|
|
782
890
|
}
|
|
783
891
|
}
|
|
784
892
|
// Propagate scores upward to parent domain/topic nodes (hierarchical retrieval)
|
|
785
|
-
const propagated = propagateScoresToParents(propagationInputs, symbolTree, summaryMap,
|
|
893
|
+
const propagated = propagateScoresToParents(propagationInputs, symbolTree, summaryMap, symbolPathDocMap);
|
|
786
894
|
for (const p of propagated) {
|
|
895
|
+
// Apply local score boost to propagated summaries so they stay competitive
|
|
896
|
+
// with boosted direct BM25 hits (the boost was already applied to direct hits above)
|
|
897
|
+
p.score = Math.min(p.score + SHARED_SOURCE_LOCAL_SCORE_BOOST, 1);
|
|
787
898
|
if (scoreFloor !== undefined && p.score < scoreFloor)
|
|
788
899
|
continue;
|
|
789
900
|
if (options?.includeKinds && p.symbolKind && !options.includeKinds.includes(p.symbolKind))
|
|
@@ -791,7 +902,7 @@ export class SearchKnowledgeService {
|
|
|
791
902
|
if (options?.excludeKinds && p.symbolKind && options.excludeKinds.includes(p.symbolKind))
|
|
792
903
|
continue;
|
|
793
904
|
if (options?.minMaturity && p.symbolKind === 'summary') {
|
|
794
|
-
const summaryDoc = getSummarySource(p.path, summaryMap,
|
|
905
|
+
const summaryDoc = getSummarySource(p.path, summaryMap, symbolPathDocMap);
|
|
795
906
|
const summaryMaturity = summaryDoc?.scoring?.maturity ?? 'draft';
|
|
796
907
|
if ((MATURITY_TIER_RANK[summaryMaturity] ?? 1) < (MATURITY_TIER_RANK[options.minMaturity] ?? 1))
|
|
797
908
|
continue;
|
|
@@ -808,9 +919,7 @@ export class SearchKnowledgeService {
|
|
|
808
919
|
// Synthetic 'summary' results carry folder-style paths (e.g. 'auth') that are not
|
|
809
920
|
// real files; map them to their _index.md so flushAccessHits can read and update them.
|
|
810
921
|
if (results.length > 0) {
|
|
811
|
-
this.accumulateAccessHits(results.map((r) =>
|
|
812
|
-
? getSummaryAccessPath(r.path, summaryMap, documentMap)
|
|
813
|
-
: r.path)));
|
|
922
|
+
this.accumulateAccessHits(results.map((r) => r.symbolKind === 'summary' ? getSummaryAccessPath(r.path, summaryMap, symbolPathDocMap) : r.path));
|
|
814
923
|
}
|
|
815
924
|
return {
|
|
816
925
|
message: results.length > 0
|
|
@@ -823,7 +932,7 @@ export class SearchKnowledgeService {
|
|
|
823
932
|
/**
|
|
824
933
|
* Try to resolve the query as a symbolic path. Returns null if no path match found.
|
|
825
934
|
*/
|
|
826
|
-
trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, summaryMap, options) {
|
|
935
|
+
trySymbolicSearch(query, symbolTree, referenceIndex, documentMap, index, limit, pathToDocumentId, summaryMap, symbolPathDocMap, options) {
|
|
827
936
|
const pathMatches = matchMemoryPath(symbolTree, query.split(/\s+/)[0].includes('/') ? query.split(/\s+/)[0] : query);
|
|
828
937
|
if (pathMatches.length === 0) {
|
|
829
938
|
return null;
|
|
@@ -831,12 +940,15 @@ export class SearchKnowledgeService {
|
|
|
831
940
|
const topMatch = pathMatches[0].matchedSymbol;
|
|
832
941
|
// If the matched symbol is a leaf Context, return it directly
|
|
833
942
|
if (topMatch.kind === MemorySymbolKind.Context) {
|
|
834
|
-
const
|
|
943
|
+
const docId = pathToDocumentId.get(topMatch.path);
|
|
944
|
+
const doc = docId ? documentMap.get(docId) : undefined;
|
|
835
945
|
if (!doc) {
|
|
836
946
|
return null;
|
|
837
947
|
}
|
|
838
|
-
const result = this.enrichResult({ excerpt: extractExcerpt(doc.content, query), path: doc.path, score: 1, title: doc.title }, symbolTree, referenceIndex, documentMap);
|
|
839
|
-
|
|
948
|
+
const result = this.enrichResult({ excerpt: extractExcerpt(doc.content, query), id: doc.id, path: doc.path, score: 1, title: doc.title }, symbolTree, referenceIndex, documentMap);
|
|
949
|
+
if (doc.origin === 'local') {
|
|
950
|
+
this.accumulateAccessHits([doc.path]);
|
|
951
|
+
}
|
|
840
952
|
return {
|
|
841
953
|
message: `Found exact match: ${topMatch.path}`,
|
|
842
954
|
results: [result],
|
|
@@ -849,13 +961,13 @@ export class SearchKnowledgeService {
|
|
|
849
961
|
const textPart = query.slice(query.indexOf(pathPart) + pathPart.length).trim();
|
|
850
962
|
if (textPart) {
|
|
851
963
|
// Scoped search: search text within the matched subtree
|
|
852
|
-
return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, symbolTree, referenceIndex, summaryMap, options);
|
|
964
|
+
return this.runTextSearch(textPart, documentMap, index, limit, topMatch.path, pathToDocumentId, symbolTree, referenceIndex, summaryMap, symbolPathDocMap, options);
|
|
853
965
|
}
|
|
854
966
|
// No text part — return all children of the matched node
|
|
855
967
|
const subtreeIds = getSubtreeDocumentIds(symbolTree, topMatch.path);
|
|
856
968
|
const results = [];
|
|
857
969
|
const accessHitPaths = [];
|
|
858
|
-
const summaryDoc = getSummarySource(topMatch.path, summaryMap,
|
|
970
|
+
const summaryDoc = getSummarySource(topMatch.path, summaryMap, symbolPathDocMap);
|
|
859
971
|
if (summaryDoc) {
|
|
860
972
|
results.push({
|
|
861
973
|
backlinkCount: 0,
|
|
@@ -868,13 +980,14 @@ export class SearchKnowledgeService {
|
|
|
868
980
|
});
|
|
869
981
|
accessHitPaths.push(summaryDoc.path);
|
|
870
982
|
}
|
|
871
|
-
for (const
|
|
983
|
+
for (const symbolPath of subtreeIds) {
|
|
872
984
|
if (results.length >= limit)
|
|
873
985
|
break;
|
|
874
|
-
const
|
|
986
|
+
const docId = pathToDocumentId.get(symbolPath);
|
|
987
|
+
const doc = docId ? documentMap.get(docId) : undefined;
|
|
875
988
|
if (!doc)
|
|
876
989
|
continue;
|
|
877
|
-
results.push(this.enrichResult({ excerpt: extractExcerpt(doc.content, query), path: doc.path, score: 0.9, title: doc.title }, symbolTree, referenceIndex, documentMap));
|
|
990
|
+
results.push(this.enrichResult({ excerpt: extractExcerpt(doc.content, query), id: doc.id, path: doc.path, score: 0.9, title: doc.title }, symbolTree, referenceIndex, documentMap));
|
|
878
991
|
accessHitPaths.push(doc.path);
|
|
879
992
|
}
|
|
880
993
|
if (accessHitPaths.length > 0) {
|