vemora 0.1.0-alpha.8
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/README.md +716 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +589 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/ask.d.ts +14 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +136 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/audit.d.ts +17 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +408 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/brief.d.ts +14 -0
- package/dist/commands/brief.d.ts.map +1 -0
- package/dist/commands/brief.js +73 -0
- package/dist/commands/brief.js.map +1 -0
- package/dist/commands/chat.d.ts +7 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +161 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/context.d.ts +61 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +778 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/deps.d.ts +20 -0
- package/dist/commands/deps.d.ts.map +1 -0
- package/dist/commands/deps.js +138 -0
- package/dist/commands/deps.js.map +1 -0
- package/dist/commands/focus.d.ts +6 -0
- package/dist/commands/focus.d.ts.map +1 -0
- package/dist/commands/focus.js +302 -0
- package/dist/commands/focus.js.map +1 -0
- package/dist/commands/index.d.ts +10 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +366 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init-agent.d.ts +23 -0
- package/dist/commands/init-agent.d.ts.map +1 -0
- package/dist/commands/init-agent.js +447 -0
- package/dist/commands/init-agent.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +122 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/knowledge.d.ts +8 -0
- package/dist/commands/knowledge.d.ts.map +1 -0
- package/dist/commands/knowledge.js +98 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/plan.d.ts +16 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +535 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/query.d.ts +39 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +389 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/remember.d.ts +11 -0
- package/dist/commands/remember.d.ts.map +1 -0
- package/dist/commands/remember.js +174 -0
- package/dist/commands/remember.js.map +1 -0
- package/dist/commands/report.d.ts +10 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +180 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +127 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/summarize.d.ts +14 -0
- package/dist/commands/summarize.d.ts.map +1 -0
- package/dist/commands/summarize.js +205 -0
- package/dist/commands/summarize.js.map +1 -0
- package/dist/commands/triage.d.ts +33 -0
- package/dist/commands/triage.d.ts.map +1 -0
- package/dist/commands/triage.js +419 -0
- package/dist/commands/triage.js.map +1 -0
- package/dist/commands/usages.d.ts +14 -0
- package/dist/commands/usages.d.ts.map +1 -0
- package/dist/commands/usages.js +236 -0
- package/dist/commands/usages.js.map +1 -0
- package/dist/core/config.d.ts +35 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +140 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/types.d.ts +251 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +4 -0
- package/dist/core/types.js.map +1 -0
- package/dist/embeddings/factory.d.ts +9 -0
- package/dist/embeddings/factory.d.ts.map +1 -0
- package/dist/embeddings/factory.js +26 -0
- package/dist/embeddings/factory.js.map +1 -0
- package/dist/embeddings/noop.d.ts +17 -0
- package/dist/embeddings/noop.d.ts.map +1 -0
- package/dist/embeddings/noop.js +22 -0
- package/dist/embeddings/noop.js.map +1 -0
- package/dist/embeddings/ollama.d.ts +16 -0
- package/dist/embeddings/ollama.d.ts.map +1 -0
- package/dist/embeddings/ollama.js +41 -0
- package/dist/embeddings/ollama.js.map +1 -0
- package/dist/embeddings/openai.d.ts +10 -0
- package/dist/embeddings/openai.d.ts.map +1 -0
- package/dist/embeddings/openai.js +67 -0
- package/dist/embeddings/openai.js.map +1 -0
- package/dist/embeddings/provider.d.ts +19 -0
- package/dist/embeddings/provider.d.ts.map +1 -0
- package/dist/embeddings/provider.js +3 -0
- package/dist/embeddings/provider.js.map +1 -0
- package/dist/indexer/callgraph.d.ts +16 -0
- package/dist/indexer/callgraph.d.ts.map +1 -0
- package/dist/indexer/callgraph.js +154 -0
- package/dist/indexer/callgraph.js.map +1 -0
- package/dist/indexer/chunkBySlidingWindow.d.ts +6 -0
- package/dist/indexer/chunkBySlidingWindow.d.ts.map +1 -0
- package/dist/indexer/chunkBySlidingWindow.js +30 -0
- package/dist/indexer/chunkBySlidingWindow.js.map +1 -0
- package/dist/indexer/chunkBySymbols.d.ts +7 -0
- package/dist/indexer/chunkBySymbols.d.ts.map +1 -0
- package/dist/indexer/chunkBySymbols.js +57 -0
- package/dist/indexer/chunkBySymbols.js.map +1 -0
- package/dist/indexer/chunker.d.ts +15 -0
- package/dist/indexer/chunker.d.ts.map +1 -0
- package/dist/indexer/chunker.js +26 -0
- package/dist/indexer/chunker.js.map +1 -0
- package/dist/indexer/classHeader.d.ts +7 -0
- package/dist/indexer/classHeader.d.ts.map +1 -0
- package/dist/indexer/classHeader.js +37 -0
- package/dist/indexer/classHeader.js.map +1 -0
- package/dist/indexer/deps.d.ts +66 -0
- package/dist/indexer/deps.d.ts.map +1 -0
- package/dist/indexer/deps.js +409 -0
- package/dist/indexer/deps.js.map +1 -0
- package/dist/indexer/hasher.d.ts +17 -0
- package/dist/indexer/hasher.d.ts.map +1 -0
- package/dist/indexer/hasher.js +38 -0
- package/dist/indexer/hasher.js.map +1 -0
- package/dist/indexer/parser.d.ts +18 -0
- package/dist/indexer/parser.d.ts.map +1 -0
- package/dist/indexer/parser.js +355 -0
- package/dist/indexer/parser.js.map +1 -0
- package/dist/indexer/scanner.d.ts +18 -0
- package/dist/indexer/scanner.d.ts.map +1 -0
- package/dist/indexer/scanner.js +37 -0
- package/dist/indexer/scanner.js.map +1 -0
- package/dist/indexer/strategy.d.ts +11 -0
- package/dist/indexer/strategy.d.ts.map +1 -0
- package/dist/indexer/strategy.js +15 -0
- package/dist/indexer/strategy.js.map +1 -0
- package/dist/indexer/tests.d.ts +15 -0
- package/dist/indexer/tests.d.ts.map +1 -0
- package/dist/indexer/tests.js +68 -0
- package/dist/indexer/tests.js.map +1 -0
- package/dist/indexer/todos.d.ts +9 -0
- package/dist/indexer/todos.d.ts.map +1 -0
- package/dist/indexer/todos.js +29 -0
- package/dist/indexer/todos.js.map +1 -0
- package/dist/llm/anthropic.d.ts +8 -0
- package/dist/llm/anthropic.d.ts.map +1 -0
- package/dist/llm/anthropic.js +76 -0
- package/dist/llm/anthropic.js.map +1 -0
- package/dist/llm/factory.d.ts +7 -0
- package/dist/llm/factory.d.ts.map +1 -0
- package/dist/llm/factory.js +39 -0
- package/dist/llm/factory.js.map +1 -0
- package/dist/llm/ollama.d.ts +8 -0
- package/dist/llm/ollama.d.ts.map +1 -0
- package/dist/llm/ollama.js +83 -0
- package/dist/llm/ollama.js.map +1 -0
- package/dist/llm/openai.d.ts +8 -0
- package/dist/llm/openai.d.ts.map +1 -0
- package/dist/llm/openai.js +68 -0
- package/dist/llm/openai.js.map +1 -0
- package/dist/llm/provider.d.ts +29 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +3 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/search/bm25.d.ts +3 -0
- package/dist/search/bm25.d.ts.map +1 -0
- package/dist/search/bm25.js +102 -0
- package/dist/search/bm25.js.map +1 -0
- package/dist/search/formatter.d.ts +43 -0
- package/dist/search/formatter.d.ts.map +1 -0
- package/dist/search/formatter.js +208 -0
- package/dist/search/formatter.js.map +1 -0
- package/dist/search/hybrid.d.ts +10 -0
- package/dist/search/hybrid.d.ts.map +1 -0
- package/dist/search/hybrid.js +53 -0
- package/dist/search/hybrid.js.map +1 -0
- package/dist/search/merge.d.ts +33 -0
- package/dist/search/merge.d.ts.map +1 -0
- package/dist/search/merge.js +158 -0
- package/dist/search/merge.js.map +1 -0
- package/dist/search/mmr.d.ts +23 -0
- package/dist/search/mmr.d.ts.map +1 -0
- package/dist/search/mmr.js +95 -0
- package/dist/search/mmr.js.map +1 -0
- package/dist/search/rerank.d.ts +15 -0
- package/dist/search/rerank.d.ts.map +1 -0
- package/dist/search/rerank.js +76 -0
- package/dist/search/rerank.js.map +1 -0
- package/dist/search/signature.d.ts +42 -0
- package/dist/search/signature.d.ts.map +1 -0
- package/dist/search/signature.js +112 -0
- package/dist/search/signature.js.map +1 -0
- package/dist/search/vector.d.ts +41 -0
- package/dist/search/vector.d.ts.map +1 -0
- package/dist/search/vector.js +185 -0
- package/dist/search/vector.js.map +1 -0
- package/dist/storage/cache.d.ts +30 -0
- package/dist/storage/cache.d.ts.map +1 -0
- package/dist/storage/cache.js +160 -0
- package/dist/storage/cache.js.map +1 -0
- package/dist/storage/knowledge.d.ts +17 -0
- package/dist/storage/knowledge.d.ts.map +1 -0
- package/dist/storage/knowledge.js +58 -0
- package/dist/storage/knowledge.js.map +1 -0
- package/dist/storage/repository.d.ts +27 -0
- package/dist/storage/repository.d.ts.map +1 -0
- package/dist/storage/repository.js +95 -0
- package/dist/storage/repository.js.map +1 -0
- package/dist/storage/session.d.ts +38 -0
- package/dist/storage/session.d.ts.map +1 -0
- package/dist/storage/session.js +100 -0
- package/dist/storage/session.js.map +1 -0
- package/dist/storage/summaries.d.ts +19 -0
- package/dist/storage/summaries.d.ts.map +1 -0
- package/dist/storage/summaries.js +66 -0
- package/dist/storage/summaries.js.map +1 -0
- package/dist/storage/usage.d.ts +35 -0
- package/dist/storage/usage.d.ts.map +1 -0
- package/dist/storage/usage.js +55 -0
- package/dist/storage/usage.js.map +1 -0
- package/dist/utils/git.d.ts +15 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/tokenizer.d.ts +24 -0
- package/dist/utils/tokenizer.d.ts.map +1 -0
- package/dist/utils/tokenizer.js +52 -0
- package/dist/utils/tokenizer.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runContext = runContext;
|
|
7
|
+
exports.generateContextString = generateContextString;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const ora_1 = __importDefault(require("ora"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const config_1 = require("../core/config");
|
|
13
|
+
const git_1 = require("../utils/git");
|
|
14
|
+
const factory_1 = require("../embeddings/factory");
|
|
15
|
+
const deps_1 = require("../indexer/deps");
|
|
16
|
+
const tests_1 = require("../indexer/tests");
|
|
17
|
+
const bm25_1 = require("../search/bm25");
|
|
18
|
+
const hybrid_1 = require("../search/hybrid");
|
|
19
|
+
const merge_1 = require("../search/merge");
|
|
20
|
+
const mmr_1 = require("../search/mmr");
|
|
21
|
+
const rerank_1 = require("../search/rerank");
|
|
22
|
+
const formatter_1 = require("../search/formatter");
|
|
23
|
+
const signature_1 = require("../search/signature");
|
|
24
|
+
const vector_1 = require("../search/vector");
|
|
25
|
+
const cache_1 = require("../storage/cache");
|
|
26
|
+
const knowledge_1 = require("../storage/knowledge");
|
|
27
|
+
const repository_1 = require("../storage/repository");
|
|
28
|
+
const session_1 = require("../storage/session");
|
|
29
|
+
const usage_1 = require("../storage/usage");
|
|
30
|
+
const summaries_1 = require("../storage/summaries");
|
|
31
|
+
const tokenizer_1 = require("../utils/tokenizer");
|
|
32
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
33
|
+
async function runContext(rootDir, options = {}) {
|
|
34
|
+
const config = (0, config_1.loadConfig)(rootDir);
|
|
35
|
+
const repo = new repository_1.RepositoryStorage(rootDir);
|
|
36
|
+
const cacheStorage = new cache_1.EmbeddingCacheStorage(config.projectId);
|
|
37
|
+
const summaryStorage = new summaries_1.SummaryStorage(rootDir);
|
|
38
|
+
const topK = options.topK ?? 5;
|
|
39
|
+
// Apply config.display.format as default if no explicit format was passed.
|
|
40
|
+
const resolvedOptions = {
|
|
41
|
+
...options,
|
|
42
|
+
format: options.format ?? config.display?.format ?? "markdown",
|
|
43
|
+
};
|
|
44
|
+
const chunks = repo.loadChunks();
|
|
45
|
+
const symbols = repo.loadSymbols();
|
|
46
|
+
const depGraph = repo.loadDeps();
|
|
47
|
+
const callGraph = repo.loadCallGraph();
|
|
48
|
+
const fileSummaries = summaryStorage.hasFileSummaries()
|
|
49
|
+
? summaryStorage.loadFileSummaries()
|
|
50
|
+
: {};
|
|
51
|
+
const projectSummary = summaryStorage.loadProjectSummary();
|
|
52
|
+
const knowledgeEntries = new knowledge_1.KnowledgeStorage(rootDir).load();
|
|
53
|
+
if (chunks.length === 0) {
|
|
54
|
+
console.error(chalk_1.default.red("No index found. Run `vemora index` first."));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
if (!options.query && !options.file) {
|
|
58
|
+
console.error(chalk_1.default.red("Provide --query <text> and/or --file <path>."));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
let results = [];
|
|
62
|
+
let searchType = "none";
|
|
63
|
+
let tokensSavedSession = 0;
|
|
64
|
+
let tokensSavedDedup = 0;
|
|
65
|
+
let tokensSavedBudget = 0;
|
|
66
|
+
if (typeof options.query === "string" && options.query.length > 0) {
|
|
67
|
+
const useKeyword = options.keyword || config.embedding.provider === "none";
|
|
68
|
+
const useHybrid = options.hybrid;
|
|
69
|
+
// Symbol-aware routing: direct lookup for precise identifier queries.
|
|
70
|
+
const symbolHits = !useHybrid && !useKeyword
|
|
71
|
+
? (0, vector_1.symbolLookup)(options.query, chunks, symbols)
|
|
72
|
+
: [];
|
|
73
|
+
if (symbolHits.length > 0) {
|
|
74
|
+
results = symbolHits;
|
|
75
|
+
searchType = "symbol";
|
|
76
|
+
}
|
|
77
|
+
else if (useKeyword) {
|
|
78
|
+
const spinner = (0, ora_1.default)("Performing hybrid search...").start();
|
|
79
|
+
try {
|
|
80
|
+
const cache = cacheStorage.load();
|
|
81
|
+
if (!cache ||
|
|
82
|
+
(cache.chunkIds?.length === 0 &&
|
|
83
|
+
Object.keys(cache.embeddings ?? {}).length === 0)) {
|
|
84
|
+
spinner.warn("No embeddings — falling back to BM25.");
|
|
85
|
+
results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
|
|
86
|
+
searchType = "bm25";
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const provider = (0, factory_1.createEmbeddingProvider)(config.embedding);
|
|
90
|
+
const [queryEmbedding] = await provider.embed([options.query]);
|
|
91
|
+
spinner.succeed("Embedded");
|
|
92
|
+
results = await (0, hybrid_1.hybridSearch)(options.query, queryEmbedding, chunks, cache, symbols, {
|
|
93
|
+
alpha: options.alpha,
|
|
94
|
+
topK,
|
|
95
|
+
});
|
|
96
|
+
searchType = "hybrid";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
spinner.fail(`Hybrid search failed: ${err.message}`);
|
|
101
|
+
results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
|
|
102
|
+
searchType = "bm25";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const spinner = (0, ora_1.default)("Generating query embedding...").start();
|
|
107
|
+
try {
|
|
108
|
+
const cache = cacheStorage.load();
|
|
109
|
+
const cachedCount = cache
|
|
110
|
+
? cache.chunkIds
|
|
111
|
+
? cache.chunkIds.length
|
|
112
|
+
: Object.keys(cache.embeddings ?? {}).length
|
|
113
|
+
: 0;
|
|
114
|
+
if (!cache || cachedCount === 0) {
|
|
115
|
+
spinner.warn("No embeddings — falling back to BM25.");
|
|
116
|
+
results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
|
|
117
|
+
searchType = "bm25";
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const provider = (0, factory_1.createEmbeddingProvider)(config.embedding);
|
|
121
|
+
const [queryEmbedding] = await provider.embed([options.query]);
|
|
122
|
+
spinner.succeed("Embedded");
|
|
123
|
+
results = (0, vector_1.vectorSearch)(queryEmbedding, chunks, cache, symbols, topK);
|
|
124
|
+
searchType = "vector";
|
|
125
|
+
if (results.length === 0) {
|
|
126
|
+
results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
|
|
127
|
+
searchType = "bm25";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
spinner.fail(`Embedding failed: ${err.message}`);
|
|
133
|
+
results = (0, bm25_1.computeBM25Scores)(options.query, chunks, symbols, topK);
|
|
134
|
+
searchType = "bm25";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ── Reranking (optional) ──────────────────────────────────────────────
|
|
138
|
+
if (options.rerank && results.length > 0) {
|
|
139
|
+
results = await (0, rerank_1.rerankResults)(options.query, results, topK);
|
|
140
|
+
}
|
|
141
|
+
// ── MMR deduplication (optional) ─────────────────────────────────────
|
|
142
|
+
if (options.mmr && results.length > 1) {
|
|
143
|
+
const cache = cacheStorage.load();
|
|
144
|
+
results = (0, mmr_1.applyMMR)(results, cache, topK, options.lambda ?? 0.5);
|
|
145
|
+
}
|
|
146
|
+
// ── Adjacent chunk merge (optional) ──────────────────────────────────
|
|
147
|
+
if (options.merge && results.length > 1) {
|
|
148
|
+
results = (0, merge_1.mergeAdjacentChunks)(results, options.mergeGap);
|
|
149
|
+
}
|
|
150
|
+
// ── Session filter (optional) ─────────────────────────────────────────
|
|
151
|
+
const sessionStorage = new session_1.SessionStorage(config.projectId);
|
|
152
|
+
if (options.fresh)
|
|
153
|
+
sessionStorage.reset();
|
|
154
|
+
if ((options.session || options.fresh) && results.length > 0) {
|
|
155
|
+
const seenIds = sessionStorage.getSeenIds();
|
|
156
|
+
if (seenIds.size > 0) {
|
|
157
|
+
const tBefore = (0, tokenizer_1.sumResultTokens)(results);
|
|
158
|
+
const unseen = results.filter((r) => !seenIds.has(r.chunk.id));
|
|
159
|
+
results = unseen.length > 0 ? unseen : results.slice(0, 1);
|
|
160
|
+
tokensSavedSession = tBefore - (0, tokenizer_1.sumResultTokens)(results);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── Semantic deduplication (always-on) ───────────────────────────────
|
|
164
|
+
if (results.length > 1) {
|
|
165
|
+
const tBefore = (0, tokenizer_1.sumResultTokens)(results);
|
|
166
|
+
const cache = cacheStorage.load();
|
|
167
|
+
results = (0, merge_1.deduplicateBySimilarity)(results, cache);
|
|
168
|
+
tokensSavedDedup = tBefore - (0, tokenizer_1.sumResultTokens)(results);
|
|
169
|
+
}
|
|
170
|
+
// ── Token budget (optional) ───────────────────────────────────────────
|
|
171
|
+
if (options.budget && options.budget > 0) {
|
|
172
|
+
const tBefore = (0, tokenizer_1.sumResultTokens)(results);
|
|
173
|
+
results = (0, tokenizer_1.applyTokenBudget)(results, options.budget);
|
|
174
|
+
tokensSavedBudget = tBefore - (0, tokenizer_1.sumResultTokens)(results);
|
|
175
|
+
}
|
|
176
|
+
// ── Persist session ───────────────────────────────────────────────────
|
|
177
|
+
if ((options.session || options.fresh) && results.length > 0) {
|
|
178
|
+
sessionStorage.markSeen(results.map((r) => r.chunk.id));
|
|
179
|
+
}
|
|
180
|
+
// ── Record usage event ──────────────────────────────────────────────
|
|
181
|
+
try {
|
|
182
|
+
new usage_1.UsageStorage(config.projectId).append({
|
|
183
|
+
ts: new Date().toISOString(),
|
|
184
|
+
command: "context",
|
|
185
|
+
query: options.query?.slice(0, 120),
|
|
186
|
+
searchType,
|
|
187
|
+
format: options.format,
|
|
188
|
+
topK,
|
|
189
|
+
resultsReturned: results.length,
|
|
190
|
+
tokensReturned: (0, tokenizer_1.sumResultTokens)(results),
|
|
191
|
+
tokensSavedDedup,
|
|
192
|
+
tokensSavedSession,
|
|
193
|
+
tokensSavedBudget,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// usage tracking is best-effort — never block the main output
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const contextStr = generateContextString(config, results, depGraph, callGraph, fileSummaries, projectSummary ? projectSummary.overview : null, resolvedOptions, rootDir, chunks, knowledgeEntries);
|
|
201
|
+
console.log(contextStr);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Procedural function to build the actual context string.
|
|
205
|
+
* This is separated from runContext to allow for benchmarking and testing.
|
|
206
|
+
*
|
|
207
|
+
* When `options.structured` is true and results are available, delegates to
|
|
208
|
+
* `generateStructuredContextString` which organises output by semantic role
|
|
209
|
+
* (Entry Point → Direct Dependencies → Called By → Types & Interfaces → Related Patterns)
|
|
210
|
+
* instead of a flat ranked list.
|
|
211
|
+
*
|
|
212
|
+
* @param allChunks - Full chunk corpus, required for structured mode dep resolution.
|
|
213
|
+
* Optional for backward compatibility with bench.ts.
|
|
214
|
+
*/
|
|
215
|
+
function generateContextString(config, results, depGraph, callGraph, fileSummaries, projectOverview, options, rootDir, allChunks = [], knowledgeEntries = []) {
|
|
216
|
+
if (options.structured && results.length > 0) {
|
|
217
|
+
return generateStructuredContextString(config, results, depGraph, callGraph, fileSummaries, projectOverview, options, rootDir, allChunks, knowledgeEntries);
|
|
218
|
+
}
|
|
219
|
+
if (options.format === "terse") {
|
|
220
|
+
const lines = [];
|
|
221
|
+
lines.push(`# ${config.projectName}`);
|
|
222
|
+
lines.push("");
|
|
223
|
+
const relevant = rankKnowledgeEntries(options.query, results, knowledgeEntries);
|
|
224
|
+
for (const entry of relevant) {
|
|
225
|
+
lines.push(`[${entry.category}] ${entry.title}: ${entry.body}`);
|
|
226
|
+
}
|
|
227
|
+
if (relevant.length > 0)
|
|
228
|
+
lines.push("");
|
|
229
|
+
lines.push((0, formatter_1.formatTerse)(results, {}));
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
}
|
|
232
|
+
const fmt = options.format ?? "markdown";
|
|
233
|
+
const lines = [];
|
|
234
|
+
const hr = fmt === "markdown" ? "---" : "=".repeat(60);
|
|
235
|
+
const importedByMap = (0, deps_1.computeImportedBy)(depGraph);
|
|
236
|
+
// ── Section: Project overview ──────────────────────────────────────────────
|
|
237
|
+
lines.push(fmt === "markdown"
|
|
238
|
+
? `# AI Context — ${config.projectName}`
|
|
239
|
+
: `=== AI CONTEXT BLOCK — ${config.projectName.toUpperCase()} ===`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
if (projectOverview) {
|
|
242
|
+
lines.push(fmt === "markdown" ? "## Project Overview" : "[Project Overview]");
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push(projectOverview);
|
|
245
|
+
lines.push("");
|
|
246
|
+
lines.push(hr);
|
|
247
|
+
lines.push("");
|
|
248
|
+
}
|
|
249
|
+
// ── Section: Knowledge entries ────────────────────────────────────────────
|
|
250
|
+
const relevantKnowledge = rankKnowledgeEntries(options.query, results, knowledgeEntries);
|
|
251
|
+
if (relevantKnowledge.length > 0) {
|
|
252
|
+
lines.push(fmt === "markdown" ? "## Knowledge" : "[Knowledge]");
|
|
253
|
+
lines.push("");
|
|
254
|
+
for (const entry of relevantKnowledge) {
|
|
255
|
+
const badge = fmt === "markdown" ? `\`${entry.category}\`` : entry.category;
|
|
256
|
+
lines.push(fmt === "markdown"
|
|
257
|
+
? `**${entry.title}** ${badge}`
|
|
258
|
+
: `${entry.title} [${entry.category}]`);
|
|
259
|
+
lines.push(entry.body);
|
|
260
|
+
if (entry.relatedFiles?.length) {
|
|
261
|
+
lines.push(fmt === "markdown"
|
|
262
|
+
? `_Files: ${entry.relatedFiles.map((f) => `\`${f}\``).join(", ")}_`
|
|
263
|
+
: `Files: ${entry.relatedFiles.join(", ")}`);
|
|
264
|
+
}
|
|
265
|
+
lines.push("");
|
|
266
|
+
}
|
|
267
|
+
lines.push(hr);
|
|
268
|
+
lines.push("");
|
|
269
|
+
}
|
|
270
|
+
// ── Section: Specific file context ────────────────────────────────────────
|
|
271
|
+
if (options.file) {
|
|
272
|
+
const relFile = resolveRelPath(rootDir, options.file);
|
|
273
|
+
lines.push(fmt === "markdown" ? "## File Context" : "[File Context]");
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push(fmt === "markdown" ? `**File:** \`${relFile}\`` : `File: ${relFile}`);
|
|
276
|
+
lines.push("");
|
|
277
|
+
// Full file content
|
|
278
|
+
const absPath = path_1.default.join(rootDir, relFile);
|
|
279
|
+
if (fs_1.default.existsSync(absPath)) {
|
|
280
|
+
// Resolve symlinks before reading to prevent traversal via symlinks
|
|
281
|
+
const realRoot = fs_1.default.realpathSync(path_1.default.resolve(rootDir));
|
|
282
|
+
const realPath = fs_1.default.realpathSync(absPath);
|
|
283
|
+
if (!realPath.startsWith(realRoot + path_1.default.sep) && realPath !== realRoot) {
|
|
284
|
+
throw new Error(`File path escapes project root via symlink: ${options.file}`);
|
|
285
|
+
}
|
|
286
|
+
const content = fs_1.default.readFileSync(realPath, "utf-8");
|
|
287
|
+
const ext = relFile.split(".").pop() ?? "";
|
|
288
|
+
if (fmt === "markdown") {
|
|
289
|
+
lines.push(`\`\`\`${ext}`);
|
|
290
|
+
lines.push(content);
|
|
291
|
+
lines.push("```");
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
lines.push(content);
|
|
295
|
+
}
|
|
296
|
+
lines.push("");
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
lines.push(fmt === "markdown" ? "_File not found._" : "(file not found)");
|
|
300
|
+
lines.push("");
|
|
301
|
+
}
|
|
302
|
+
// Imports and used-by for the file
|
|
303
|
+
const fileDeps = depGraph[relFile];
|
|
304
|
+
const usedBy = importedByMap.get(relFile) ?? [];
|
|
305
|
+
if (fileDeps?.imports.length) {
|
|
306
|
+
lines.push(fmt === "markdown"
|
|
307
|
+
? "**Imports from project:**"
|
|
308
|
+
: "Imports from project:");
|
|
309
|
+
for (const imp of fileDeps.imports) {
|
|
310
|
+
const syms = imp.symbols.length > 0 ? ` (${imp.symbols.join(", ")})` : "";
|
|
311
|
+
lines.push(fmt === "markdown"
|
|
312
|
+
? `- \`${imp.file}\`${syms}`
|
|
313
|
+
: ` - ${imp.file}${syms}`);
|
|
314
|
+
}
|
|
315
|
+
lines.push("");
|
|
316
|
+
}
|
|
317
|
+
if (usedBy.length > 0) {
|
|
318
|
+
lines.push(fmt === "markdown" ? "**Used by:**" : "Used by:");
|
|
319
|
+
for (const caller of usedBy) {
|
|
320
|
+
lines.push(fmt === "markdown" ? `- \`${caller}\`` : ` - ${caller}`);
|
|
321
|
+
}
|
|
322
|
+
lines.push("");
|
|
323
|
+
}
|
|
324
|
+
// Git commit history for this file
|
|
325
|
+
const commits = (0, git_1.getFileGitHistory)(rootDir, relFile);
|
|
326
|
+
if (commits.length > 0) {
|
|
327
|
+
lines.push(fmt === "markdown" ? "**Recent commits:**" : "Recent commits:");
|
|
328
|
+
for (const c of commits) {
|
|
329
|
+
lines.push(fmt === "markdown"
|
|
330
|
+
? `- \`${c.sha}\` ${c.message} _(${c.author}, ${c.date})_`
|
|
331
|
+
: ` - ${c.sha} ${c.message} (${c.author}, ${c.date})`);
|
|
332
|
+
}
|
|
333
|
+
lines.push("");
|
|
334
|
+
}
|
|
335
|
+
// TODOs / FIXMEs in this file
|
|
336
|
+
const fileTodos = new repository_1.RepositoryStorage(rootDir).loadTodos().filter((t) => t.file === relFile);
|
|
337
|
+
if (fileTodos.length > 0) {
|
|
338
|
+
lines.push(fmt === "markdown" ? "**TODOs / FIXMEs:**" : "TODOs / FIXMEs:");
|
|
339
|
+
for (const t of fileTodos) {
|
|
340
|
+
lines.push(fmt === "markdown"
|
|
341
|
+
? `- **${t.type}** (line ${t.line}): ${t.text}`
|
|
342
|
+
: ` - ${t.type} (line ${t.line}): ${t.text}`);
|
|
343
|
+
}
|
|
344
|
+
lines.push("");
|
|
345
|
+
}
|
|
346
|
+
// Test files linked to this source file
|
|
347
|
+
const allFileKeys = [...new Set(allChunks.map((c) => c.file))];
|
|
348
|
+
const testFiles = (0, tests_1.findTestFiles)(relFile, allFileKeys, importedByMap);
|
|
349
|
+
if (testFiles.length > 0) {
|
|
350
|
+
lines.push(fmt === "markdown" ? "**Test files:**" : "Test files:");
|
|
351
|
+
for (const tf of testFiles) {
|
|
352
|
+
lines.push(fmt === "markdown" ? `- \`${tf}\`` : ` - ${tf}`);
|
|
353
|
+
}
|
|
354
|
+
lines.push("");
|
|
355
|
+
}
|
|
356
|
+
// Caller context: symbols exported by this file and who calls them
|
|
357
|
+
const fileSymbols = [
|
|
358
|
+
...new Set(allChunks
|
|
359
|
+
.filter((c) => c.file === relFile && c.symbol)
|
|
360
|
+
.map((c) => c.symbol)),
|
|
361
|
+
];
|
|
362
|
+
const callerRows = [];
|
|
363
|
+
for (const sym of fileSymbols) {
|
|
364
|
+
const callInfo = callGraph[`${relFile}:${sym}`];
|
|
365
|
+
if (callInfo?.calledBy.length) {
|
|
366
|
+
const callers = callInfo.calledBy.slice(0, 5);
|
|
367
|
+
const rest = callInfo.calledBy.length - callers.length;
|
|
368
|
+
const callerList = fmt === "markdown"
|
|
369
|
+
? callers.map((c) => `\`${c}\``).join(", ") +
|
|
370
|
+
(rest > 0 ? ` _+${rest} more_` : "")
|
|
371
|
+
: callers.join(", ") + (rest > 0 ? ` +${rest} more` : "");
|
|
372
|
+
callerRows.push(fmt === "markdown"
|
|
373
|
+
? `- \`${sym}\` ← ${callerList}`
|
|
374
|
+
: ` - ${sym} <- ${callerList}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (callerRows.length > 0) {
|
|
378
|
+
lines.push(fmt === "markdown" ? "**Symbol callers:**" : "Symbol callers:");
|
|
379
|
+
lines.push(...callerRows);
|
|
380
|
+
lines.push("");
|
|
381
|
+
}
|
|
382
|
+
lines.push(hr);
|
|
383
|
+
lines.push("");
|
|
384
|
+
}
|
|
385
|
+
// ── Section: Relevant code via query ──────────────────────────────────────
|
|
386
|
+
if (options.query) {
|
|
387
|
+
lines.push(fmt === "markdown" ? "## Relevant Code" : "[Relevant Code]");
|
|
388
|
+
lines.push("");
|
|
389
|
+
if (results.length === 0) {
|
|
390
|
+
lines.push("_No relevant results found._");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
const seen = new Set();
|
|
394
|
+
for (const { chunk, score, symbol } of results) {
|
|
395
|
+
if (seen.has(chunk.id))
|
|
396
|
+
continue;
|
|
397
|
+
seen.add(chunk.id);
|
|
398
|
+
const ext = chunk.file.split(".").pop() ?? "";
|
|
399
|
+
const symLabel = chunk.symbol
|
|
400
|
+
? ` — ${symbol?.type ?? "symbol"} \`${chunk.symbol}\``
|
|
401
|
+
: "";
|
|
402
|
+
if (fmt === "markdown") {
|
|
403
|
+
lines.push(`### \`${chunk.file}\`${symLabel}`);
|
|
404
|
+
lines.push(`Lines ${chunk.start}–${chunk.end} · Score: ${score.toFixed(4)}`);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
lines.push(`[${chunk.file}${symLabel}]`);
|
|
408
|
+
lines.push(`lines ${chunk.start}–${chunk.end} score: ${score.toFixed(4)}`);
|
|
409
|
+
}
|
|
410
|
+
lines.push("");
|
|
411
|
+
// Deps context (only if graph available)
|
|
412
|
+
const fileDeps = depGraph[chunk.file];
|
|
413
|
+
const usedBy = importedByMap.get(chunk.file) ?? [];
|
|
414
|
+
if (fileDeps?.imports.length) {
|
|
415
|
+
lines.push(fmt === "markdown" ? "**Imports:**" : "Imports:");
|
|
416
|
+
for (const imp of fileDeps.imports.slice(0, 5)) {
|
|
417
|
+
const syms = imp.symbols.length > 0
|
|
418
|
+
? ` (${imp.symbols.slice(0, 4).join(", ")})`
|
|
419
|
+
: "";
|
|
420
|
+
lines.push(fmt === "markdown"
|
|
421
|
+
? `- \`${imp.file}\`${syms}`
|
|
422
|
+
: ` - ${imp.file}${syms}`);
|
|
423
|
+
}
|
|
424
|
+
lines.push("");
|
|
425
|
+
}
|
|
426
|
+
if (usedBy.length > 0) {
|
|
427
|
+
lines.push(fmt === "markdown" ? "**Used by:**" : "Used by:");
|
|
428
|
+
for (const caller of usedBy.slice(0, 3)) {
|
|
429
|
+
lines.push(fmt === "markdown" ? `- \`${caller}\`` : ` - ${caller}`);
|
|
430
|
+
}
|
|
431
|
+
lines.push("");
|
|
432
|
+
}
|
|
433
|
+
// Call Graph context
|
|
434
|
+
const symbolId = chunk.symbol ? `${chunk.file}:${chunk.symbol}` : null;
|
|
435
|
+
const callInfo = symbolId ? callGraph[symbolId] : null;
|
|
436
|
+
if (callInfo) {
|
|
437
|
+
if (callInfo.calls.length > 0) {
|
|
438
|
+
lines.push(fmt === "markdown" ? "**Calls:**" : "Calls:");
|
|
439
|
+
for (const call of callInfo.calls.slice(0, 5)) {
|
|
440
|
+
const loc = call.file ? ` (in \`${call.file}\`)` : "";
|
|
441
|
+
lines.push(fmt === "markdown"
|
|
442
|
+
? `- \`${call.name}\`${loc}`
|
|
443
|
+
: ` - ${call.name}${loc}`);
|
|
444
|
+
}
|
|
445
|
+
lines.push("");
|
|
446
|
+
}
|
|
447
|
+
if (callInfo.calledBy.length > 0) {
|
|
448
|
+
lines.push(fmt === "markdown" ? "**Called by:**" : "Called by:");
|
|
449
|
+
for (const callerId of callInfo.calledBy.slice(0, 5)) {
|
|
450
|
+
lines.push(fmt === "markdown" ? `- \`${callerId}\`` : ` - ${callerId}`);
|
|
451
|
+
}
|
|
452
|
+
lines.push("");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Code
|
|
456
|
+
const codeLines = chunk.content.split("\n");
|
|
457
|
+
const limit = options.showCode ? codeLines.length : signature_1.HIGH_CODE_LINES;
|
|
458
|
+
const preview = codeLines.slice(0, limit).join("\n");
|
|
459
|
+
const truncated = codeLines.length > limit;
|
|
460
|
+
if (fmt === "markdown") {
|
|
461
|
+
lines.push(`\`\`\`${ext}`);
|
|
462
|
+
lines.push(preview);
|
|
463
|
+
if (truncated)
|
|
464
|
+
lines.push(`// … (${codeLines.length - limit} more lines)`);
|
|
465
|
+
lines.push("```");
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
lines.push(preview);
|
|
469
|
+
if (truncated)
|
|
470
|
+
lines.push(`... (${codeLines.length - limit} more lines)`);
|
|
471
|
+
}
|
|
472
|
+
lines.push("");
|
|
473
|
+
lines.push(hr);
|
|
474
|
+
lines.push("");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// ── Footer ────────────────────────────────────────────────────────────────
|
|
479
|
+
lines.push(fmt === "markdown"
|
|
480
|
+
? `_Generated by \`vemora context\` — ${new Date().toISOString()}_`
|
|
481
|
+
: `=== END AI CONTEXT BLOCK — ${new Date().toISOString()} ===`);
|
|
482
|
+
return lines.join("\n");
|
|
483
|
+
}
|
|
484
|
+
// ─── Knowledge ranking ────────────────────────────────────────────────────────
|
|
485
|
+
/**
|
|
486
|
+
* Ranks knowledge entries by relevance to the current query and result set.
|
|
487
|
+
*
|
|
488
|
+
* Scoring (higher = more relevant):
|
|
489
|
+
* +10 relatedFiles overlaps with result files
|
|
490
|
+
* +8 relatedSymbols overlaps with result symbols
|
|
491
|
+
* +2 per query term found in title+body
|
|
492
|
+
* +4/3/2/1 category weight: gotcha > pattern > decision > glossary
|
|
493
|
+
* +1 confidence === 'high'
|
|
494
|
+
*
|
|
495
|
+
* Only entries with score > 0 are returned (max 5, sorted descending).
|
|
496
|
+
*/
|
|
497
|
+
function rankKnowledgeEntries(query, results, entries, maxEntries = 5) {
|
|
498
|
+
if (entries.length === 0)
|
|
499
|
+
return [];
|
|
500
|
+
const resultFiles = new Set(results.map((r) => r.chunk.file));
|
|
501
|
+
const resultSymbols = new Set(results.flatMap((r) => (r.chunk.symbol ? [r.chunk.symbol] : [])));
|
|
502
|
+
const categoryWeight = {
|
|
503
|
+
gotcha: 4,
|
|
504
|
+
pattern: 3,
|
|
505
|
+
decision: 2,
|
|
506
|
+
glossary: 1,
|
|
507
|
+
};
|
|
508
|
+
const queryTerms = query
|
|
509
|
+
? new Set(query
|
|
510
|
+
.toLowerCase()
|
|
511
|
+
.split(/[\s\W]+/)
|
|
512
|
+
.filter((t) => t.length >= 2))
|
|
513
|
+
: new Set();
|
|
514
|
+
const scored = entries.map((entry) => {
|
|
515
|
+
let score = 0;
|
|
516
|
+
if (entry.relatedFiles?.some((f) => resultFiles.has(f)))
|
|
517
|
+
score += 10;
|
|
518
|
+
if (entry.relatedSymbols?.some((s) => resultSymbols.has(s)))
|
|
519
|
+
score += 8;
|
|
520
|
+
if (queryTerms.size > 0) {
|
|
521
|
+
const text = `${entry.title} ${entry.body}`.toLowerCase();
|
|
522
|
+
for (const term of queryTerms) {
|
|
523
|
+
if (text.includes(term))
|
|
524
|
+
score += 2;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
score += categoryWeight[entry.category] ?? 1;
|
|
528
|
+
if (entry.confidence === "high")
|
|
529
|
+
score += 1;
|
|
530
|
+
return { entry, score };
|
|
531
|
+
});
|
|
532
|
+
return scored
|
|
533
|
+
.filter((s) => s.score > (categoryWeight[s.entry.category] ?? 1)) // exclude pure category-weight matches
|
|
534
|
+
.sort((a, b) => b.score - a.score)
|
|
535
|
+
.slice(0, maxEntries)
|
|
536
|
+
.map((s) => s.entry);
|
|
537
|
+
}
|
|
538
|
+
// ─── Structured context renderer ─────────────────────────────────────────────
|
|
539
|
+
/**
|
|
540
|
+
* Renders a structured context block organised by semantic role:
|
|
541
|
+
*
|
|
542
|
+
* ## Entry Point — top-ranked chunk, full code
|
|
543
|
+
* ## Direct Dependencies — signatures of functions called by the entry point (call graph depth-1)
|
|
544
|
+
* ## Called By — callers of the entry point (contract context)
|
|
545
|
+
* ## Types & Interfaces — interface/type chunks from the result set
|
|
546
|
+
* ## Related Patterns — remaining result chunks (signatures to save tokens)
|
|
547
|
+
*
|
|
548
|
+
* This layout reduces token usage by ~20-35% compared to the flat list and
|
|
549
|
+
* helps LLMs navigate the codebase more effectively because the relationships
|
|
550
|
+
* are explicit rather than implied by rank.
|
|
551
|
+
*/
|
|
552
|
+
function generateStructuredContextString(config, results, depGraph, callGraph, fileSummaries, projectOverview, options, _rootDir, allChunks, knowledgeEntries = []) {
|
|
553
|
+
const fmt = options.format ?? "markdown";
|
|
554
|
+
const hr = fmt === "markdown" ? "---" : "=".repeat(60);
|
|
555
|
+
const lines = [];
|
|
556
|
+
// ── Header ────────────────────────────────────────────────────────────────
|
|
557
|
+
lines.push(fmt === "markdown"
|
|
558
|
+
? `# AI Context — ${config.projectName}`
|
|
559
|
+
: `=== AI CONTEXT BLOCK — ${config.projectName.toUpperCase()} ===`);
|
|
560
|
+
if (options.query) {
|
|
561
|
+
lines.push("");
|
|
562
|
+
lines.push(fmt === "markdown"
|
|
563
|
+
? `> **Query:** ${options.query}`
|
|
564
|
+
: `Query: ${options.query}`);
|
|
565
|
+
}
|
|
566
|
+
lines.push("");
|
|
567
|
+
if (projectOverview) {
|
|
568
|
+
lines.push(fmt === "markdown" ? "## Project Overview" : "[Project Overview]");
|
|
569
|
+
lines.push("");
|
|
570
|
+
lines.push(projectOverview);
|
|
571
|
+
lines.push("");
|
|
572
|
+
lines.push(hr);
|
|
573
|
+
lines.push("");
|
|
574
|
+
}
|
|
575
|
+
// ── Section: Knowledge entries ────────────────────────────────────────────
|
|
576
|
+
const relevantKnowledge = rankKnowledgeEntries(options.query, results, knowledgeEntries);
|
|
577
|
+
if (relevantKnowledge.length > 0) {
|
|
578
|
+
lines.push(fmt === "markdown" ? "## Knowledge" : "[Knowledge]");
|
|
579
|
+
lines.push("");
|
|
580
|
+
for (const entry of relevantKnowledge) {
|
|
581
|
+
const badge = fmt === "markdown" ? `\`${entry.category}\`` : entry.category;
|
|
582
|
+
lines.push(fmt === "markdown"
|
|
583
|
+
? `**${entry.title}** ${badge}`
|
|
584
|
+
: `${entry.title} [${entry.category}]`);
|
|
585
|
+
lines.push(entry.body);
|
|
586
|
+
if (entry.relatedFiles?.length) {
|
|
587
|
+
lines.push(fmt === "markdown"
|
|
588
|
+
? `_Files: ${entry.relatedFiles.map((f) => `\`${f}\``).join(", ")}_`
|
|
589
|
+
: `Files: ${entry.relatedFiles.join(", ")}`);
|
|
590
|
+
}
|
|
591
|
+
lines.push("");
|
|
592
|
+
}
|
|
593
|
+
lines.push(hr);
|
|
594
|
+
lines.push("");
|
|
595
|
+
}
|
|
596
|
+
// ── Partition results ─────────────────────────────────────────────────────
|
|
597
|
+
const seen = new Set();
|
|
598
|
+
const dedupedResults = results.filter((r) => {
|
|
599
|
+
if (seen.has(r.chunk.id))
|
|
600
|
+
return false;
|
|
601
|
+
seen.add(r.chunk.id);
|
|
602
|
+
return true;
|
|
603
|
+
});
|
|
604
|
+
const topResult = dedupedResults[0];
|
|
605
|
+
const typeResults = dedupedResults
|
|
606
|
+
.slice(1)
|
|
607
|
+
.filter((r) => r.symbol?.type === "interface" || r.symbol?.type === "type");
|
|
608
|
+
const relatedResults = dedupedResults
|
|
609
|
+
.slice(1)
|
|
610
|
+
.filter((r) => r.symbol?.type !== "interface" && r.symbol?.type !== "type");
|
|
611
|
+
// ── Section 1: Entry Point ────────────────────────────────────────────────
|
|
612
|
+
lines.push(fmt === "markdown" ? "## Entry Point" : "[Entry Point]");
|
|
613
|
+
lines.push("");
|
|
614
|
+
{
|
|
615
|
+
const { chunk, score, symbol } = topResult;
|
|
616
|
+
const ext = chunk.file.split(".").pop() ?? "";
|
|
617
|
+
const symLabel = chunk.symbol
|
|
618
|
+
? ` — ${symbol?.type ?? "symbol"} \`${chunk.symbol}\``
|
|
619
|
+
: "";
|
|
620
|
+
lines.push(fmt === "markdown"
|
|
621
|
+
? `**\`${chunk.file}\`**${symLabel}`
|
|
622
|
+
: `${chunk.file}${symLabel}`);
|
|
623
|
+
lines.push(`Lines ${chunk.start}–${chunk.end} · Score: ${score.toFixed(4)}`);
|
|
624
|
+
lines.push("");
|
|
625
|
+
const codeLines = chunk.content.split("\n");
|
|
626
|
+
const limit = options.showCode ? codeLines.length : signature_1.HIGH_CODE_LINES;
|
|
627
|
+
const preview = codeLines.slice(0, limit).join("\n");
|
|
628
|
+
lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
|
|
629
|
+
lines.push(preview);
|
|
630
|
+
if (codeLines.length > limit) {
|
|
631
|
+
lines.push(`// … (${codeLines.length - limit} more lines — use --show-code to expand)`);
|
|
632
|
+
}
|
|
633
|
+
if (fmt === "markdown")
|
|
634
|
+
lines.push("```");
|
|
635
|
+
lines.push("");
|
|
636
|
+
}
|
|
637
|
+
lines.push(hr);
|
|
638
|
+
lines.push("");
|
|
639
|
+
// ── Section 2: Direct Dependencies (call graph depth-1) ───────────────────
|
|
640
|
+
const topSymbolId = topResult.chunk.symbol
|
|
641
|
+
? `${topResult.chunk.file}:${topResult.chunk.symbol}`
|
|
642
|
+
: null;
|
|
643
|
+
const callInfo = topSymbolId ? callGraph[topSymbolId] : null;
|
|
644
|
+
if (callInfo?.calls.length) {
|
|
645
|
+
lines.push(fmt === "markdown" ? "## Direct Dependencies" : "[Direct Dependencies]");
|
|
646
|
+
lines.push("");
|
|
647
|
+
let depCount = 0;
|
|
648
|
+
for (const call of callInfo.calls.slice(0, 8)) {
|
|
649
|
+
// Look up a matching chunk (prefer same file if provided)
|
|
650
|
+
const matching = allChunks.find((c) => c.symbol === call.name && (!call.file || c.file === call.file)) ?? allChunks.find((c) => c.symbol === call.name);
|
|
651
|
+
const chunkFile = matching?.file ?? call.file ?? "(external)";
|
|
652
|
+
const ext = chunkFile.split(".").pop() ?? "";
|
|
653
|
+
const sig = matching
|
|
654
|
+
? (0, signature_1.extractSignature)(matching.content)
|
|
655
|
+
: `// ${call.name} — not found in index`;
|
|
656
|
+
lines.push(fmt === "markdown"
|
|
657
|
+
? `**\`${chunkFile}\`** — \`${call.name}\``
|
|
658
|
+
: `${chunkFile} — ${call.name}`);
|
|
659
|
+
lines.push("");
|
|
660
|
+
lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
|
|
661
|
+
lines.push(sig);
|
|
662
|
+
if (fmt === "markdown")
|
|
663
|
+
lines.push("```");
|
|
664
|
+
lines.push("");
|
|
665
|
+
depCount++;
|
|
666
|
+
}
|
|
667
|
+
const hiddenDeps = callInfo.calls.length - Math.min(callInfo.calls.length, 8);
|
|
668
|
+
if (hiddenDeps > 0) {
|
|
669
|
+
lines.push(fmt === "markdown"
|
|
670
|
+
? `_…and ${hiddenDeps} more calls (see call graph)_`
|
|
671
|
+
: `...and ${hiddenDeps} more calls`);
|
|
672
|
+
lines.push("");
|
|
673
|
+
}
|
|
674
|
+
if (depCount > 0) {
|
|
675
|
+
lines.push(hr);
|
|
676
|
+
lines.push("");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// ── Section 3: Called By ──────────────────────────────────────────────────
|
|
680
|
+
if (callInfo?.calledBy.length) {
|
|
681
|
+
lines.push(fmt === "markdown" ? "## Called By" : "[Called By]");
|
|
682
|
+
lines.push("");
|
|
683
|
+
for (const callerId of callInfo.calledBy.slice(0, 6)) {
|
|
684
|
+
lines.push(fmt === "markdown" ? `- \`${callerId}\`` : ` - ${callerId}`);
|
|
685
|
+
}
|
|
686
|
+
if (callInfo.calledBy.length > 6) {
|
|
687
|
+
lines.push(fmt === "markdown"
|
|
688
|
+
? `- _…and ${callInfo.calledBy.length - 6} more_`
|
|
689
|
+
: ` ...and ${callInfo.calledBy.length - 6} more`);
|
|
690
|
+
}
|
|
691
|
+
lines.push("");
|
|
692
|
+
lines.push(hr);
|
|
693
|
+
lines.push("");
|
|
694
|
+
}
|
|
695
|
+
// ── Section 4: Types & Interfaces ────────────────────────────────────────
|
|
696
|
+
if (typeResults.length > 0) {
|
|
697
|
+
lines.push(fmt === "markdown" ? "## Types & Interfaces" : "[Types & Interfaces]");
|
|
698
|
+
lines.push("");
|
|
699
|
+
for (const { chunk } of typeResults) {
|
|
700
|
+
const ext = chunk.file.split(".").pop() ?? "";
|
|
701
|
+
lines.push(fmt === "markdown"
|
|
702
|
+
? `**\`${chunk.file}\`** — \`${chunk.symbol}\``
|
|
703
|
+
: `${chunk.file} — ${chunk.symbol}`);
|
|
704
|
+
lines.push("");
|
|
705
|
+
lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
|
|
706
|
+
lines.push((0, signature_1.extractSignature)(chunk.content));
|
|
707
|
+
if (fmt === "markdown")
|
|
708
|
+
lines.push("```");
|
|
709
|
+
lines.push("");
|
|
710
|
+
}
|
|
711
|
+
lines.push(hr);
|
|
712
|
+
lines.push("");
|
|
713
|
+
}
|
|
714
|
+
// ── Section 5: Related Patterns ───────────────────────────────────────────
|
|
715
|
+
if (relatedResults.length > 0) {
|
|
716
|
+
lines.push(fmt === "markdown" ? "## Related Patterns" : "[Related Patterns]");
|
|
717
|
+
lines.push("");
|
|
718
|
+
for (const { chunk, score, symbol } of relatedResults) {
|
|
719
|
+
const ext = chunk.file.split(".").pop() ?? "";
|
|
720
|
+
const symLabel = chunk.symbol
|
|
721
|
+
? ` — ${symbol?.type ?? "symbol"} \`${chunk.symbol}\``
|
|
722
|
+
: "";
|
|
723
|
+
lines.push(fmt === "markdown"
|
|
724
|
+
? `### \`${chunk.file}\`${symLabel}`
|
|
725
|
+
: `${chunk.file}${symLabel}`);
|
|
726
|
+
lines.push(`Score: ${score.toFixed(4)}`);
|
|
727
|
+
lines.push("");
|
|
728
|
+
// Use signature to keep tokens low for secondary results
|
|
729
|
+
const display = chunk.symbol
|
|
730
|
+
? (0, signature_1.extractSignature)(chunk.content)
|
|
731
|
+
: chunk.content.split("\n").slice(0, 10).join("\n") +
|
|
732
|
+
(chunk.content.split("\n").length > 10 ? "\n// …" : "");
|
|
733
|
+
lines.push(fmt === "markdown" ? `\`\`\`${ext}` : "");
|
|
734
|
+
lines.push(display);
|
|
735
|
+
if (fmt === "markdown")
|
|
736
|
+
lines.push("```");
|
|
737
|
+
lines.push("");
|
|
738
|
+
}
|
|
739
|
+
lines.push(hr);
|
|
740
|
+
lines.push("");
|
|
741
|
+
}
|
|
742
|
+
// ── Footer ────────────────────────────────────────────────────────────────
|
|
743
|
+
lines.push(fmt === "markdown"
|
|
744
|
+
? `_Generated by \`vemora context --structured\` — ${new Date().toISOString()}_`
|
|
745
|
+
: `=== END AI CONTEXT BLOCK — ${new Date().toISOString()} ===`);
|
|
746
|
+
return lines.join("\n");
|
|
747
|
+
}
|
|
748
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
749
|
+
/**
|
|
750
|
+
* Resolve a file argument to a project-relative path.
|
|
751
|
+
*
|
|
752
|
+
* Accepts:
|
|
753
|
+
* - Absolute paths → relativised against rootDir
|
|
754
|
+
* - Paths already relative to rootDir → used as-is if the file exists
|
|
755
|
+
* - Paths relative to cwd → resolved to absolute, then relativised
|
|
756
|
+
*/
|
|
757
|
+
function resolveRelPath(rootDir, filePath) {
|
|
758
|
+
const resolvedRoot = path_1.default.resolve(rootDir);
|
|
759
|
+
let absPath;
|
|
760
|
+
if (path_1.default.isAbsolute(filePath)) {
|
|
761
|
+
absPath = filePath;
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
// Try relative to rootDir first, then fall back to cwd
|
|
765
|
+
const fromRoot = path_1.default.join(resolvedRoot, filePath);
|
|
766
|
+
absPath = fs_1.default.existsSync(fromRoot)
|
|
767
|
+
? fromRoot
|
|
768
|
+
: path_1.default.resolve(process.cwd(), filePath);
|
|
769
|
+
}
|
|
770
|
+
// Guard: reject paths that escape the project root (path traversal)
|
|
771
|
+
const normalised = path_1.default.resolve(absPath);
|
|
772
|
+
if (!normalised.startsWith(resolvedRoot + path_1.default.sep) &&
|
|
773
|
+
normalised !== resolvedRoot) {
|
|
774
|
+
throw new Error(`File path escapes project root: ${filePath}`);
|
|
775
|
+
}
|
|
776
|
+
return path_1.default.relative(resolvedRoot, normalised);
|
|
777
|
+
}
|
|
778
|
+
//# sourceMappingURL=context.js.map
|