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.
Files changed (242) hide show
  1. package/README.md +716 -0
  2. package/dist/cli.d.ts +16 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +589 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/ask.d.ts +14 -0
  7. package/dist/commands/ask.d.ts.map +1 -0
  8. package/dist/commands/ask.js +136 -0
  9. package/dist/commands/ask.js.map +1 -0
  10. package/dist/commands/audit.d.ts +17 -0
  11. package/dist/commands/audit.d.ts.map +1 -0
  12. package/dist/commands/audit.js +408 -0
  13. package/dist/commands/audit.js.map +1 -0
  14. package/dist/commands/brief.d.ts +14 -0
  15. package/dist/commands/brief.d.ts.map +1 -0
  16. package/dist/commands/brief.js +73 -0
  17. package/dist/commands/brief.js.map +1 -0
  18. package/dist/commands/chat.d.ts +7 -0
  19. package/dist/commands/chat.d.ts.map +1 -0
  20. package/dist/commands/chat.js +161 -0
  21. package/dist/commands/chat.js.map +1 -0
  22. package/dist/commands/context.d.ts +61 -0
  23. package/dist/commands/context.d.ts.map +1 -0
  24. package/dist/commands/context.js +778 -0
  25. package/dist/commands/context.js.map +1 -0
  26. package/dist/commands/deps.d.ts +20 -0
  27. package/dist/commands/deps.d.ts.map +1 -0
  28. package/dist/commands/deps.js +138 -0
  29. package/dist/commands/deps.js.map +1 -0
  30. package/dist/commands/focus.d.ts +6 -0
  31. package/dist/commands/focus.d.ts.map +1 -0
  32. package/dist/commands/focus.js +302 -0
  33. package/dist/commands/focus.js.map +1 -0
  34. package/dist/commands/index.d.ts +10 -0
  35. package/dist/commands/index.d.ts.map +1 -0
  36. package/dist/commands/index.js +366 -0
  37. package/dist/commands/index.js.map +1 -0
  38. package/dist/commands/init-agent.d.ts +23 -0
  39. package/dist/commands/init-agent.d.ts.map +1 -0
  40. package/dist/commands/init-agent.js +447 -0
  41. package/dist/commands/init-agent.js.map +1 -0
  42. package/dist/commands/init.d.ts +2 -0
  43. package/dist/commands/init.d.ts.map +1 -0
  44. package/dist/commands/init.js +122 -0
  45. package/dist/commands/init.js.map +1 -0
  46. package/dist/commands/knowledge.d.ts +8 -0
  47. package/dist/commands/knowledge.d.ts.map +1 -0
  48. package/dist/commands/knowledge.js +98 -0
  49. package/dist/commands/knowledge.js.map +1 -0
  50. package/dist/commands/plan.d.ts +16 -0
  51. package/dist/commands/plan.d.ts.map +1 -0
  52. package/dist/commands/plan.js +535 -0
  53. package/dist/commands/plan.js.map +1 -0
  54. package/dist/commands/query.d.ts +39 -0
  55. package/dist/commands/query.d.ts.map +1 -0
  56. package/dist/commands/query.js +389 -0
  57. package/dist/commands/query.js.map +1 -0
  58. package/dist/commands/remember.d.ts +11 -0
  59. package/dist/commands/remember.d.ts.map +1 -0
  60. package/dist/commands/remember.js +174 -0
  61. package/dist/commands/remember.js.map +1 -0
  62. package/dist/commands/report.d.ts +10 -0
  63. package/dist/commands/report.d.ts.map +1 -0
  64. package/dist/commands/report.js +180 -0
  65. package/dist/commands/report.js.map +1 -0
  66. package/dist/commands/status.d.ts +2 -0
  67. package/dist/commands/status.d.ts.map +1 -0
  68. package/dist/commands/status.js +127 -0
  69. package/dist/commands/status.js.map +1 -0
  70. package/dist/commands/summarize.d.ts +14 -0
  71. package/dist/commands/summarize.d.ts.map +1 -0
  72. package/dist/commands/summarize.js +205 -0
  73. package/dist/commands/summarize.js.map +1 -0
  74. package/dist/commands/triage.d.ts +33 -0
  75. package/dist/commands/triage.d.ts.map +1 -0
  76. package/dist/commands/triage.js +419 -0
  77. package/dist/commands/triage.js.map +1 -0
  78. package/dist/commands/usages.d.ts +14 -0
  79. package/dist/commands/usages.d.ts.map +1 -0
  80. package/dist/commands/usages.js +236 -0
  81. package/dist/commands/usages.js.map +1 -0
  82. package/dist/core/config.d.ts +35 -0
  83. package/dist/core/config.d.ts.map +1 -0
  84. package/dist/core/config.js +140 -0
  85. package/dist/core/config.js.map +1 -0
  86. package/dist/core/types.d.ts +251 -0
  87. package/dist/core/types.d.ts.map +1 -0
  88. package/dist/core/types.js +4 -0
  89. package/dist/core/types.js.map +1 -0
  90. package/dist/embeddings/factory.d.ts +9 -0
  91. package/dist/embeddings/factory.d.ts.map +1 -0
  92. package/dist/embeddings/factory.js +26 -0
  93. package/dist/embeddings/factory.js.map +1 -0
  94. package/dist/embeddings/noop.d.ts +17 -0
  95. package/dist/embeddings/noop.d.ts.map +1 -0
  96. package/dist/embeddings/noop.js +22 -0
  97. package/dist/embeddings/noop.js.map +1 -0
  98. package/dist/embeddings/ollama.d.ts +16 -0
  99. package/dist/embeddings/ollama.d.ts.map +1 -0
  100. package/dist/embeddings/ollama.js +41 -0
  101. package/dist/embeddings/ollama.js.map +1 -0
  102. package/dist/embeddings/openai.d.ts +10 -0
  103. package/dist/embeddings/openai.d.ts.map +1 -0
  104. package/dist/embeddings/openai.js +67 -0
  105. package/dist/embeddings/openai.js.map +1 -0
  106. package/dist/embeddings/provider.d.ts +19 -0
  107. package/dist/embeddings/provider.d.ts.map +1 -0
  108. package/dist/embeddings/provider.js +3 -0
  109. package/dist/embeddings/provider.js.map +1 -0
  110. package/dist/indexer/callgraph.d.ts +16 -0
  111. package/dist/indexer/callgraph.d.ts.map +1 -0
  112. package/dist/indexer/callgraph.js +154 -0
  113. package/dist/indexer/callgraph.js.map +1 -0
  114. package/dist/indexer/chunkBySlidingWindow.d.ts +6 -0
  115. package/dist/indexer/chunkBySlidingWindow.d.ts.map +1 -0
  116. package/dist/indexer/chunkBySlidingWindow.js +30 -0
  117. package/dist/indexer/chunkBySlidingWindow.js.map +1 -0
  118. package/dist/indexer/chunkBySymbols.d.ts +7 -0
  119. package/dist/indexer/chunkBySymbols.d.ts.map +1 -0
  120. package/dist/indexer/chunkBySymbols.js +57 -0
  121. package/dist/indexer/chunkBySymbols.js.map +1 -0
  122. package/dist/indexer/chunker.d.ts +15 -0
  123. package/dist/indexer/chunker.d.ts.map +1 -0
  124. package/dist/indexer/chunker.js +26 -0
  125. package/dist/indexer/chunker.js.map +1 -0
  126. package/dist/indexer/classHeader.d.ts +7 -0
  127. package/dist/indexer/classHeader.d.ts.map +1 -0
  128. package/dist/indexer/classHeader.js +37 -0
  129. package/dist/indexer/classHeader.js.map +1 -0
  130. package/dist/indexer/deps.d.ts +66 -0
  131. package/dist/indexer/deps.d.ts.map +1 -0
  132. package/dist/indexer/deps.js +409 -0
  133. package/dist/indexer/deps.js.map +1 -0
  134. package/dist/indexer/hasher.d.ts +17 -0
  135. package/dist/indexer/hasher.d.ts.map +1 -0
  136. package/dist/indexer/hasher.js +38 -0
  137. package/dist/indexer/hasher.js.map +1 -0
  138. package/dist/indexer/parser.d.ts +18 -0
  139. package/dist/indexer/parser.d.ts.map +1 -0
  140. package/dist/indexer/parser.js +355 -0
  141. package/dist/indexer/parser.js.map +1 -0
  142. package/dist/indexer/scanner.d.ts +18 -0
  143. package/dist/indexer/scanner.d.ts.map +1 -0
  144. package/dist/indexer/scanner.js +37 -0
  145. package/dist/indexer/scanner.js.map +1 -0
  146. package/dist/indexer/strategy.d.ts +11 -0
  147. package/dist/indexer/strategy.d.ts.map +1 -0
  148. package/dist/indexer/strategy.js +15 -0
  149. package/dist/indexer/strategy.js.map +1 -0
  150. package/dist/indexer/tests.d.ts +15 -0
  151. package/dist/indexer/tests.d.ts.map +1 -0
  152. package/dist/indexer/tests.js +68 -0
  153. package/dist/indexer/tests.js.map +1 -0
  154. package/dist/indexer/todos.d.ts +9 -0
  155. package/dist/indexer/todos.d.ts.map +1 -0
  156. package/dist/indexer/todos.js +29 -0
  157. package/dist/indexer/todos.js.map +1 -0
  158. package/dist/llm/anthropic.d.ts +8 -0
  159. package/dist/llm/anthropic.d.ts.map +1 -0
  160. package/dist/llm/anthropic.js +76 -0
  161. package/dist/llm/anthropic.js.map +1 -0
  162. package/dist/llm/factory.d.ts +7 -0
  163. package/dist/llm/factory.d.ts.map +1 -0
  164. package/dist/llm/factory.js +39 -0
  165. package/dist/llm/factory.js.map +1 -0
  166. package/dist/llm/ollama.d.ts +8 -0
  167. package/dist/llm/ollama.d.ts.map +1 -0
  168. package/dist/llm/ollama.js +83 -0
  169. package/dist/llm/ollama.js.map +1 -0
  170. package/dist/llm/openai.d.ts +8 -0
  171. package/dist/llm/openai.d.ts.map +1 -0
  172. package/dist/llm/openai.js +68 -0
  173. package/dist/llm/openai.js.map +1 -0
  174. package/dist/llm/provider.d.ts +29 -0
  175. package/dist/llm/provider.d.ts.map +1 -0
  176. package/dist/llm/provider.js +3 -0
  177. package/dist/llm/provider.js.map +1 -0
  178. package/dist/search/bm25.d.ts +3 -0
  179. package/dist/search/bm25.d.ts.map +1 -0
  180. package/dist/search/bm25.js +102 -0
  181. package/dist/search/bm25.js.map +1 -0
  182. package/dist/search/formatter.d.ts +43 -0
  183. package/dist/search/formatter.d.ts.map +1 -0
  184. package/dist/search/formatter.js +208 -0
  185. package/dist/search/formatter.js.map +1 -0
  186. package/dist/search/hybrid.d.ts +10 -0
  187. package/dist/search/hybrid.d.ts.map +1 -0
  188. package/dist/search/hybrid.js +53 -0
  189. package/dist/search/hybrid.js.map +1 -0
  190. package/dist/search/merge.d.ts +33 -0
  191. package/dist/search/merge.d.ts.map +1 -0
  192. package/dist/search/merge.js +158 -0
  193. package/dist/search/merge.js.map +1 -0
  194. package/dist/search/mmr.d.ts +23 -0
  195. package/dist/search/mmr.d.ts.map +1 -0
  196. package/dist/search/mmr.js +95 -0
  197. package/dist/search/mmr.js.map +1 -0
  198. package/dist/search/rerank.d.ts +15 -0
  199. package/dist/search/rerank.d.ts.map +1 -0
  200. package/dist/search/rerank.js +76 -0
  201. package/dist/search/rerank.js.map +1 -0
  202. package/dist/search/signature.d.ts +42 -0
  203. package/dist/search/signature.d.ts.map +1 -0
  204. package/dist/search/signature.js +112 -0
  205. package/dist/search/signature.js.map +1 -0
  206. package/dist/search/vector.d.ts +41 -0
  207. package/dist/search/vector.d.ts.map +1 -0
  208. package/dist/search/vector.js +185 -0
  209. package/dist/search/vector.js.map +1 -0
  210. package/dist/storage/cache.d.ts +30 -0
  211. package/dist/storage/cache.d.ts.map +1 -0
  212. package/dist/storage/cache.js +160 -0
  213. package/dist/storage/cache.js.map +1 -0
  214. package/dist/storage/knowledge.d.ts +17 -0
  215. package/dist/storage/knowledge.d.ts.map +1 -0
  216. package/dist/storage/knowledge.js +58 -0
  217. package/dist/storage/knowledge.js.map +1 -0
  218. package/dist/storage/repository.d.ts +27 -0
  219. package/dist/storage/repository.d.ts.map +1 -0
  220. package/dist/storage/repository.js +95 -0
  221. package/dist/storage/repository.js.map +1 -0
  222. package/dist/storage/session.d.ts +38 -0
  223. package/dist/storage/session.d.ts.map +1 -0
  224. package/dist/storage/session.js +100 -0
  225. package/dist/storage/session.js.map +1 -0
  226. package/dist/storage/summaries.d.ts +19 -0
  227. package/dist/storage/summaries.d.ts.map +1 -0
  228. package/dist/storage/summaries.js +66 -0
  229. package/dist/storage/summaries.js.map +1 -0
  230. package/dist/storage/usage.d.ts +35 -0
  231. package/dist/storage/usage.d.ts.map +1 -0
  232. package/dist/storage/usage.js +55 -0
  233. package/dist/storage/usage.js.map +1 -0
  234. package/dist/utils/git.d.ts +15 -0
  235. package/dist/utils/git.d.ts.map +1 -0
  236. package/dist/utils/git.js +38 -0
  237. package/dist/utils/git.js.map +1 -0
  238. package/dist/utils/tokenizer.d.ts +24 -0
  239. package/dist/utils/tokenizer.d.ts.map +1 -0
  240. package/dist/utils/tokenizer.js +52 -0
  241. package/dist/utils/tokenizer.js.map +1 -0
  242. 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