forge-server 0.1.0 → 0.1.1

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 (119) hide show
  1. package/bin/setup-forge.sh +1 -1
  2. package/bin/setup.js +99 -0
  3. package/dist/cli.js +37 -37
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +4 -4
  6. package/dist/index.js.map +1 -1
  7. package/dist/storage/schema.js +113 -113
  8. package/dist/storage/schema.js.map +1 -1
  9. package/dist/storage/sqlite.js +1 -1
  10. package/dist/storage/sqlite.js.map +1 -1
  11. package/dist/util/logger.d.ts +1 -1
  12. package/dist/util/logger.js +1 -1
  13. package/dist/util/types.js +1 -1
  14. package/dist/util/types.js.map +1 -1
  15. package/package.json +8 -2
  16. package/plugin/.mcp.json +1 -1
  17. package/.claude/hooks/worktree-create.sh +0 -64
  18. package/.claude/hooks/worktree-remove.sh +0 -57
  19. package/.claude/settings.local.json +0 -29
  20. package/.forge/knowledge/conventions.yaml +0 -1
  21. package/.forge/knowledge/decisions.yaml +0 -1
  22. package/.forge/knowledge/gotchas.yaml +0 -1
  23. package/.forge/knowledge/patterns.yaml +0 -1
  24. package/.forge/manifest.yaml +0 -6
  25. package/CLAUDE.md +0 -144
  26. package/docker-compose.yml +0 -20
  27. package/docs/plans/2026-02-27-swarm-coordination/architecture.md +0 -203
  28. package/docs/plans/2026-02-27-swarm-coordination/vision.md +0 -57
  29. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +0 -1
  30. package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +0 -300
  31. package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +0 -480
  32. package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +0 -462
  33. package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +0 -181
  34. package/src/cli.ts +0 -655
  35. package/src/context/.gitkeep +0 -0
  36. package/src/context/codebase.ts +0 -393
  37. package/src/context/injector.ts +0 -797
  38. package/src/context/memory.ts +0 -187
  39. package/src/context/session-index.ts +0 -327
  40. package/src/context/session.ts +0 -152
  41. package/src/index.ts +0 -47
  42. package/src/ingestion/.gitkeep +0 -0
  43. package/src/ingestion/chunker.ts +0 -277
  44. package/src/ingestion/embedder.ts +0 -167
  45. package/src/ingestion/git-analyzer.ts +0 -545
  46. package/src/ingestion/indexer.ts +0 -984
  47. package/src/ingestion/markdown-chunker.ts +0 -337
  48. package/src/ingestion/markdown-knowledge.ts +0 -175
  49. package/src/ingestion/parser.ts +0 -475
  50. package/src/ingestion/watcher.ts +0 -182
  51. package/src/knowledge/.gitkeep +0 -0
  52. package/src/knowledge/hydrator.ts +0 -246
  53. package/src/knowledge/registry.ts +0 -463
  54. package/src/knowledge/search.ts +0 -565
  55. package/src/knowledge/store.ts +0 -262
  56. package/src/learning/.gitkeep +0 -0
  57. package/src/learning/confidence.ts +0 -193
  58. package/src/learning/patterns.ts +0 -360
  59. package/src/learning/trajectory.ts +0 -268
  60. package/src/memory/.gitkeep +0 -0
  61. package/src/memory/memory-compat.ts +0 -233
  62. package/src/memory/observation-store.ts +0 -224
  63. package/src/memory/session-tracker.ts +0 -332
  64. package/src/pipeline/.gitkeep +0 -0
  65. package/src/pipeline/engine.ts +0 -1139
  66. package/src/pipeline/events.ts +0 -253
  67. package/src/pipeline/parallel.ts +0 -394
  68. package/src/pipeline/state-machine.ts +0 -199
  69. package/src/query/.gitkeep +0 -0
  70. package/src/query/graph-queries.ts +0 -262
  71. package/src/query/hybrid-search.ts +0 -337
  72. package/src/query/intent-detector.ts +0 -131
  73. package/src/query/ranking.ts +0 -161
  74. package/src/server.ts +0 -352
  75. package/src/storage/.gitkeep +0 -0
  76. package/src/storage/falkordb-store.ts +0 -388
  77. package/src/storage/file-cache.ts +0 -141
  78. package/src/storage/interfaces.ts +0 -201
  79. package/src/storage/qdrant-store.ts +0 -557
  80. package/src/storage/schema.ts +0 -139
  81. package/src/storage/sqlite.ts +0 -168
  82. package/src/tools/.gitkeep +0 -0
  83. package/src/tools/collaboration-tools.ts +0 -208
  84. package/src/tools/context-tools.ts +0 -493
  85. package/src/tools/graph-tools.ts +0 -295
  86. package/src/tools/ingestion-tools.ts +0 -122
  87. package/src/tools/learning-tools.ts +0 -181
  88. package/src/tools/memory-tools.ts +0 -234
  89. package/src/tools/phase-tools.ts +0 -1452
  90. package/src/tools/pipeline-tools.ts +0 -188
  91. package/src/tools/registration-tools.ts +0 -450
  92. package/src/util/.gitkeep +0 -0
  93. package/src/util/circuit-breaker.ts +0 -193
  94. package/src/util/config.ts +0 -177
  95. package/src/util/logger.ts +0 -53
  96. package/src/util/token-counter.ts +0 -52
  97. package/src/util/types.ts +0 -710
  98. package/tests/context/.gitkeep +0 -0
  99. package/tests/integration/.gitkeep +0 -0
  100. package/tests/knowledge/.gitkeep +0 -0
  101. package/tests/learning/.gitkeep +0 -0
  102. package/tests/pipeline/.gitkeep +0 -0
  103. package/tests/tools/.gitkeep +0 -0
  104. package/tsconfig.json +0 -21
  105. package/vitest.config.ts +0 -10
  106. package/vscode-extension/.vscodeignore +0 -7
  107. package/vscode-extension/README.md +0 -43
  108. package/vscode-extension/out/edge-collector.js +0 -274
  109. package/vscode-extension/out/edge-collector.js.map +0 -1
  110. package/vscode-extension/out/extension.js +0 -264
  111. package/vscode-extension/out/extension.js.map +0 -1
  112. package/vscode-extension/out/forge-client.js +0 -318
  113. package/vscode-extension/out/forge-client.js.map +0 -1
  114. package/vscode-extension/package-lock.json +0 -59
  115. package/vscode-extension/package.json +0 -71
  116. package/vscode-extension/src/edge-collector.ts +0 -320
  117. package/vscode-extension/src/extension.ts +0 -269
  118. package/vscode-extension/src/forge-client.ts +0 -364
  119. package/vscode-extension/tsconfig.json +0 -19
@@ -1,388 +0,0 @@
1
- /**
2
- * FalkorDB implementation of GraphStore.
3
- * FalkorDB speaks Redis protocol + GRAPH commands.
4
- * Connects to localhost:6380 (remapped from container port 6379 per ADR-7).
5
- *
6
- * FalkorDB GRAPH.QUERY --compact response format:
7
- * [
8
- * ["header", [...column_names]],
9
- * ["data", [[row], [row], ...]],
10
- * ["stats", ["Query internal execution time: X ms"]]
11
- * ]
12
- *
13
- * Without --compact:
14
- * [header_array, data_array, stats_array]
15
- * where header_array = [column_names]
16
- * and data_array = [[cell, cell, ...], ...]
17
- */
18
-
19
- import { createClient, type RedisClientType } from 'redis';
20
- import type { GraphStore } from './interfaces.js';
21
- import type { GraphQueryResult } from '../util/types.js';
22
- import { logger } from '../util/logger.js';
23
- import { CircuitBreaker } from '../util/circuit-breaker.js';
24
-
25
- const DEFAULT_QUERY_TIMEOUT_MS = 5000;
26
- const RECONNECT_DELAY_MS = 1000;
27
- const MAX_RECONNECT_ATTEMPTS = 3;
28
-
29
- export class FalkorDBGraphStore implements GraphStore {
30
- private client: RedisClientType | null = null;
31
- private readonly url: string;
32
- private readonly graphName: string;
33
- private readonly queryTimeoutMs: number;
34
- private connected = false;
35
- private reconnectAttempts = 0;
36
- private readonly breaker: CircuitBreaker;
37
-
38
- constructor(
39
- url: string = 'redis://localhost:6380',
40
- graphName: string = 'forge_knowledge',
41
- queryTimeoutMs: number = DEFAULT_QUERY_TIMEOUT_MS
42
- ) {
43
- this.url = url;
44
- this.graphName = graphName;
45
- this.queryTimeoutMs = queryTimeoutMs;
46
- this.breaker = new CircuitBreaker({
47
- name: 'falkordb',
48
- failureThreshold: 5,
49
- resetTimeoutMs: 30_000,
50
- });
51
- }
52
-
53
- async connect(): Promise<void> {
54
- if (this.connected) return;
55
-
56
- this.client = createClient({
57
- url: this.url,
58
- socket: {
59
- connectTimeout: this.queryTimeoutMs,
60
- reconnectStrategy: (retries) => {
61
- if (retries >= MAX_RECONNECT_ATTEMPTS) {
62
- logger.error('FalkorDB max reconnect attempts reached');
63
- return new Error('Max reconnect attempts reached');
64
- }
65
- const delay = RECONNECT_DELAY_MS * Math.pow(2, retries);
66
- logger.warn('FalkorDB reconnecting', { attempt: retries + 1, delayMs: delay });
67
- return delay;
68
- },
69
- },
70
- }) as RedisClientType;
71
-
72
- this.client.on('error', (err: Error) => {
73
- logger.error('FalkorDB connection error', { error: err.message });
74
- this.connected = false;
75
- });
76
-
77
- this.client.on('ready', () => {
78
- this.connected = true;
79
- this.reconnectAttempts = 0;
80
- logger.info('FalkorDB ready', { url: this.url, graph: this.graphName });
81
- });
82
-
83
- this.client.on('reconnecting', () => {
84
- this.reconnectAttempts++;
85
- logger.warn('FalkorDB reconnecting', { attempt: this.reconnectAttempts });
86
- });
87
-
88
- await this.client.connect();
89
- this.connected = true;
90
- logger.info('FalkorDB connected', { url: this.url, graph: this.graphName });
91
- }
92
-
93
- async disconnect(): Promise<void> {
94
- if (this.client && this.connected) {
95
- await this.client.disconnect();
96
- this.client = null;
97
- this.connected = false;
98
- logger.info('FalkorDB disconnected');
99
- }
100
- }
101
-
102
- async isHealthy(): Promise<boolean> {
103
- // Circuit breaker short-circuits when OPEN — no ping needed
104
- if (!this.breaker.isHealthy()) return false;
105
- if (!this.client || !this.connected) return false;
106
- try {
107
- const result = await this.withTimeout(
108
- this.client.ping(),
109
- this.queryTimeoutMs
110
- );
111
- return result === 'PONG';
112
- } catch {
113
- return false;
114
- }
115
- }
116
-
117
- /**
118
- * Execute a raw Cypher query and return typed results.
119
- * Parses FalkorDB's non-compact response format.
120
- */
121
- async query(cypher: string, params?: Record<string, unknown>): Promise<GraphQueryResult> {
122
- try {
123
- const raw = await this.graphQuery(cypher, params);
124
- return this.parseGraphQueryResult(raw);
125
- } catch (err) {
126
- logger.error('Graph query error', { error: String(err), cypher: cypher.slice(0, 100) });
127
- return { nodes: [], edges: [], raw: [] };
128
- }
129
- }
130
-
131
- async upsertNode(
132
- label: string,
133
- matchProps: Record<string, unknown>,
134
- setProps: Record<string, unknown>
135
- ): Promise<void> {
136
- const matchClause = this.propsToMatch(matchProps);
137
- const setClause = this.propsToSet(setProps, 'n');
138
- const cypher = `MERGE (n:${label} {${matchClause}}) SET ${setClause}`;
139
-
140
- try {
141
- await this.graphQuery(cypher);
142
- } catch (err) {
143
- logger.warn('upsertNode failed', { label, error: String(err) });
144
- }
145
- }
146
-
147
- async upsertEdge(
148
- fromLabel: string,
149
- fromProps: Record<string, unknown>,
150
- edgeType: string,
151
- edgeProps: Record<string, unknown>,
152
- toLabel: string,
153
- toProps: Record<string, unknown>
154
- ): Promise<void> {
155
- const fromMatch = this.propsToMatch(fromProps);
156
- const toMatch = this.propsToMatch(toProps);
157
- const edgeSet = Object.keys(edgeProps).length > 0
158
- ? ` {${this.propsToMatch(edgeProps)}}`
159
- : '';
160
-
161
- const cypher = `
162
- MATCH (a:${fromLabel} {${fromMatch}})
163
- MATCH (b:${toLabel} {${toMatch}})
164
- MERGE (a)-[r:${edgeType}${edgeSet}]->(b)
165
- `;
166
-
167
- try {
168
- await this.graphQuery(cypher);
169
- } catch (err) {
170
- logger.warn('upsertEdge failed', { edgeType, error: String(err) });
171
- }
172
- }
173
-
174
- async deleteFile(filePath: string, repoId: string): Promise<void> {
175
- // Delete child nodes (symbols contained in the file)
176
- const deleteChildren = `
177
- MATCH (f:File {path: "${this.escape(filePath)}", repo_id: "${this.escape(repoId)}"})-[:CONTAINS]->(s)
178
- DETACH DELETE s
179
- `;
180
- // Delete the file node itself
181
- const deleteFile = `
182
- MATCH (f:File {path: "${this.escape(filePath)}", repo_id: "${this.escape(repoId)}"})
183
- DETACH DELETE f
184
- `;
185
-
186
- try {
187
- await this.graphQuery(deleteChildren);
188
- await this.graphQuery(deleteFile);
189
- } catch (err) {
190
- logger.warn('deleteFile failed', { filePath, error: String(err) });
191
- }
192
- }
193
-
194
- async deleteRepo(repoId: string): Promise<void> {
195
- const cypher = `
196
- MATCH (n {repo_id: "${this.escape(repoId)}"})
197
- DETACH DELETE n
198
- `;
199
- try {
200
- await this.graphQuery(cypher);
201
- } catch (err) {
202
- logger.warn('deleteRepo failed', { repoId, error: String(err) });
203
- }
204
- }
205
-
206
- async getCounts(): Promise<{ totalNodes: number; totalEdges: number; byLabel: Record<string, number> }> {
207
- try {
208
- // FalkorDB GRAPH.QUERY returns [headers, data, stats]
209
- // MATCH (n) RETURN count(n) returns [[count_value]]
210
- const nodeResult = await this.graphQuery('MATCH (n) RETURN count(n) AS cnt');
211
- const edgeResult = await this.graphQuery('MATCH ()-[r]->() RETURN count(r) AS cnt');
212
-
213
- const totalNodes = this.extractScalarNumber(nodeResult);
214
- const totalEdges = this.extractScalarNumber(edgeResult);
215
-
216
- // Get per-label counts for known node labels
217
- const labels = ['File', 'Function', 'Class', 'Interface', 'Module', 'TypeAlias', 'Variable', 'Observation'];
218
- const byLabel: Record<string, number> = {};
219
-
220
- await Promise.all(labels.map(async (label) => {
221
- try {
222
- const result = await this.graphQuery(`MATCH (n:${label}) RETURN count(n) AS cnt`);
223
- const count = this.extractScalarNumber(result);
224
- if (count > 0) byLabel[label] = count;
225
- } catch {
226
- // Label might not exist in graph yet - skip
227
- }
228
- }));
229
-
230
- return { totalNodes, totalEdges, byLabel };
231
- } catch (err) {
232
- logger.warn('getCounts failed', { error: String(err) });
233
- return { totalNodes: 0, totalEdges: 0, byLabel: {} };
234
- }
235
- }
236
-
237
- async ensureIndexes(): Promise<void> {
238
- const indexes = [
239
- 'CREATE INDEX ON :File(path, repo_id)',
240
- 'CREATE INDEX ON :Function(name, repo_id)',
241
- 'CREATE INDEX ON :Class(name, repo_id)',
242
- 'CREATE INDEX ON :Interface(name, repo_id)',
243
- 'CREATE INDEX ON :Module(name, repo_id)',
244
- 'CREATE INDEX ON :Observation(id)',
245
- 'CREATE INDEX ON :Observation(session_id)',
246
- ];
247
-
248
- for (const idx of indexes) {
249
- try {
250
- await this.graphQuery(idx);
251
- } catch {
252
- // Index may already exist - FalkorDB throws on duplicate index creation.
253
- // This is expected and safe to ignore.
254
- }
255
- }
256
- logger.info('FalkorDB indexes ensured');
257
- }
258
-
259
- // ============================================================
260
- // Private helpers
261
- // ============================================================
262
-
263
- /**
264
- * Execute a GRAPH.QUERY command with timeout, wrapped in the circuit breaker.
265
- * Returns the raw FalkorDB response array.
266
- */
267
- private async graphQuery(cypher: string, _params?: Record<string, unknown>): Promise<unknown[]> {
268
- if (!this.client) throw new Error('FalkorDB not connected. Call connect() first.');
269
-
270
- return this.breaker.execute(async () => {
271
- const queryPromise = (this.client as RedisClientType).sendCommand([
272
- 'GRAPH.QUERY',
273
- this.graphName,
274
- cypher,
275
- ]) as Promise<unknown[]>;
276
-
277
- return this.withTimeout(queryPromise, this.queryTimeoutMs);
278
- });
279
- }
280
-
281
- /**
282
- * Wrap a promise with a timeout that rejects after timeoutMs.
283
- */
284
- private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
285
- return new Promise<T>((resolve, reject) => {
286
- const timer = setTimeout(() => {
287
- reject(new Error(`FalkorDB query timed out after ${timeoutMs}ms`));
288
- }, timeoutMs);
289
-
290
- promise.then(
291
- (value) => { clearTimeout(timer); resolve(value); },
292
- (err) => { clearTimeout(timer); reject(err); }
293
- );
294
- });
295
- }
296
-
297
- /**
298
- * Parse FalkorDB GRAPH.QUERY response into GraphQueryResult.
299
- * FalkorDB non-compact format: [header_row, data_rows, stats_row]
300
- * header_row: ["column1", "column2", ...]
301
- * data_rows: [[val1, val2, ...], [val1, val2, ...], ...]
302
- */
303
- private parseGraphQueryResult(raw: unknown[]): GraphQueryResult {
304
- if (!Array.isArray(raw) || raw.length < 2) {
305
- return { nodes: [], edges: [], raw };
306
- }
307
-
308
- const headers = raw[0] as string[];
309
- const dataRows = raw[1] as unknown[][];
310
-
311
- if (!Array.isArray(headers) || !Array.isArray(dataRows)) {
312
- return { nodes: [], edges: [], raw };
313
- }
314
-
315
- const nodes = [];
316
- const edges = [];
317
-
318
- for (const row of dataRows) {
319
- if (!Array.isArray(row)) continue;
320
- for (let i = 0; i < headers.length; i++) {
321
- const cell = row[i];
322
- if (cell && typeof cell === 'object' && !Array.isArray(cell)) {
323
- const obj = cell as Record<string, unknown>;
324
- // FalkorDB node: { id, labels, properties }
325
- if (Array.isArray(obj.labels)) {
326
- nodes.push({
327
- id: String(obj.id ?? ''),
328
- label: (obj.labels as string[])[0] ?? '',
329
- properties: (obj.properties as Record<string, unknown>) ?? {},
330
- });
331
- }
332
- // FalkorDB edge: { id, type, src_node, dest_node, properties }
333
- if (typeof obj.type === 'string' && obj.src_node !== undefined) {
334
- edges.push({
335
- type: obj.type,
336
- from: String(obj.src_node),
337
- to: String(obj.dest_node ?? ''),
338
- properties: (obj.properties as Record<string, unknown>) ?? {},
339
- });
340
- }
341
- }
342
- }
343
- }
344
-
345
- return { nodes, edges, raw };
346
- }
347
-
348
- /**
349
- * Extract a single scalar number from a GRAPH.QUERY count result.
350
- * Response: [["cnt"], [[42]], [...stats]]
351
- */
352
- private extractScalarNumber(raw: unknown[]): number {
353
- if (!Array.isArray(raw) || raw.length < 2) return 0;
354
- const dataRows = raw[1] as unknown[][];
355
- if (!Array.isArray(dataRows) || dataRows.length === 0) return 0;
356
- const firstRow = dataRows[0] as unknown[];
357
- if (!Array.isArray(firstRow) || firstRow.length === 0) return 0;
358
- const val = firstRow[0];
359
- if (typeof val === 'number') return val;
360
- if (typeof val === 'string') return parseInt(val, 10) || 0;
361
- return 0;
362
- }
363
-
364
- private propsToMatch(props: Record<string, unknown>): string {
365
- return Object.entries(props)
366
- .map(([k, v]) => `${k}: ${this.valueToLiteral(v)}`)
367
- .join(', ');
368
- }
369
-
370
- private propsToSet(props: Record<string, unknown>, alias: string): string {
371
- return Object.entries(props)
372
- .map(([k, v]) => `${alias}.${k} = ${this.valueToLiteral(v)}`)
373
- .join(', ');
374
- }
375
-
376
- private valueToLiteral(v: unknown): string {
377
- if (typeof v === 'string') return `"${this.escape(v)}"`;
378
- if (typeof v === 'number') return String(v);
379
- if (typeof v === 'boolean') return v ? 'true' : 'false';
380
- if (v === null || v === undefined) return 'null';
381
- if (Array.isArray(v)) return `[${v.map(x => this.valueToLiteral(x)).join(', ')}]`;
382
- return `"${this.escape(JSON.stringify(v))}"`;
383
- }
384
-
385
- private escape(s: string): string {
386
- return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
387
- }
388
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * In-memory LRU file content cache.
3
- * Provides the cross-agent shared context (ADR-8, section 4.6).
4
- * Size-aware eviction with content-hash keying for invalidation.
5
- */
6
-
7
- import { LRUCache } from 'lru-cache';
8
- import { createHash } from 'crypto';
9
- import type { FileContentCache } from './interfaces.js';
10
- import type { CacheEntry, CacheStats } from '../util/types.js';
11
- import { logger } from '../util/logger.js';
12
-
13
- const DEFAULT_MAX_SIZE_BYTES = 100 * 1024 * 1024; // 100 MB
14
- const DEFAULT_MAX_ENTRIES = 2000;
15
- const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 min
16
-
17
- export interface FileCacheOptions {
18
- maxSizeBytes?: number;
19
- maxEntries?: number;
20
- ttlMs?: number;
21
- }
22
-
23
- export class LRUFileContentCache implements FileContentCache {
24
- private cache: LRUCache<string, CacheEntry>;
25
- private hits = 0;
26
- private misses = 0;
27
- private evictionCount = 0;
28
- private startedAt = Date.now();
29
-
30
- constructor(options: FileCacheOptions = {}) {
31
- const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
32
- const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
33
-
34
- this.cache = new LRUCache<string, CacheEntry>({
35
- maxSize: maxSizeBytes,
36
- // LRU cache requires sizeCalculation to return a positive integer.
37
- // Use minimum of 1 so zero-byte content (empty string) is still accepted.
38
- sizeCalculation: (entry) => Math.max(1, entry.sizeBytes),
39
- ttl: ttlMs,
40
- max: options.maxEntries ?? DEFAULT_MAX_ENTRIES,
41
- // Use Date.now() for TTL tracking. This makes the cache compatible with
42
- // vi.useFakeTimers() in tests (which replaces Date.now but not performance.now).
43
- // lru-cache v11 defaults to performance.now which isn't affected by fake timers.
44
- perf: { now: () => Date.now() },
45
- dispose: () => {
46
- this.evictionCount++;
47
- },
48
- });
49
- }
50
-
51
- private cacheKey(repoId: string, filePath: string): string {
52
- return `${repoId}:${filePath}`;
53
- }
54
-
55
- get(repoId: string, filePath: string): CacheEntry | null {
56
- const key = this.cacheKey(repoId, filePath);
57
- const entry = this.cache.get(key);
58
-
59
- if (entry) {
60
- this.hits++;
61
- // Update access count in-place
62
- entry.accessCount++;
63
- return entry;
64
- }
65
-
66
- this.misses++;
67
- return null;
68
- }
69
-
70
- set(repoId: string, filePath: string, content: string, contentHash: string): void {
71
- const key = this.cacheKey(repoId, filePath);
72
- const sizeBytes = Buffer.byteLength(content, 'utf8');
73
-
74
- const entry: CacheEntry = {
75
- content,
76
- contentHash,
77
- cachedAt: Date.now(),
78
- accessCount: 0,
79
- sizeBytes, // Report the actual byte size (may be 0 for empty string)
80
- };
81
-
82
- this.cache.set(key, entry);
83
- logger.debug('File cached', { filePath, sizeBytes, repoId });
84
- }
85
-
86
- invalidate(repoId: string, filePath: string): void {
87
- const key = this.cacheKey(repoId, filePath);
88
- this.cache.delete(key);
89
- logger.debug('Cache invalidated', { filePath, repoId });
90
- }
91
-
92
- invalidateRepo(repoId: string): void {
93
- const prefix = `${repoId}:`;
94
- let count = 0;
95
- for (const key of this.cache.keys()) {
96
- if (key.startsWith(prefix)) {
97
- this.cache.delete(key);
98
- count++;
99
- }
100
- }
101
- logger.info('Repo cache invalidated', { repoId, count });
102
- }
103
-
104
- clear(): void {
105
- this.cache.clear();
106
- this.hits = 0;
107
- this.misses = 0;
108
- logger.info('File content cache cleared');
109
- }
110
-
111
- getStats(): CacheStats {
112
- const total = this.hits + this.misses;
113
- const hitRate = total > 0 ? this.hits / total : 0;
114
-
115
- // Calculate current memory usage
116
- let totalBytes = 0;
117
- let oldestAge = 0;
118
- const now = Date.now();
119
-
120
- for (const entry of this.cache.values()) {
121
- totalBytes += entry.sizeBytes;
122
- const age = (now - entry.cachedAt) / 1000;
123
- if (age > oldestAge) oldestAge = age;
124
- }
125
-
126
- return {
127
- entries: this.cache.size,
128
- memoryUsageMb: totalBytes / (1024 * 1024),
129
- hitRate,
130
- evictionCount: this.evictionCount,
131
- oldestEntryAgeSeconds: oldestAge,
132
- };
133
- }
134
- }
135
-
136
- /**
137
- * Utility: compute SHA-256 hash of content string.
138
- */
139
- export function hashContent(content: string): string {
140
- return createHash('sha256').update(content, 'utf8').digest('hex');
141
- }