@git.zone/tsdoc 1.5.2 → 1.6.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.
@@ -0,0 +1,285 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as fs from 'fs';
3
+ import type { ICacheEntry, ICacheConfig } from './types.js';
4
+
5
+ /**
6
+ * ContextCache provides persistent caching of file contents and token counts
7
+ * with automatic invalidation on file changes
8
+ */
9
+ export class ContextCache {
10
+ private cacheDir: string;
11
+ private cache: Map<string, ICacheEntry> = new Map();
12
+ private config: Required<ICacheConfig>;
13
+ private cacheIndexPath: string;
14
+
15
+ /**
16
+ * Creates a new ContextCache
17
+ * @param projectRoot - Root directory of the project
18
+ * @param config - Cache configuration
19
+ */
20
+ constructor(projectRoot: string, config: Partial<ICacheConfig> = {}) {
21
+ this.config = {
22
+ enabled: config.enabled ?? true,
23
+ ttl: config.ttl ?? 3600, // 1 hour default
24
+ maxSize: config.maxSize ?? 100, // 100MB default
25
+ directory: config.directory ?? plugins.path.join(projectRoot, '.nogit', 'context-cache'),
26
+ };
27
+
28
+ this.cacheDir = this.config.directory;
29
+ this.cacheIndexPath = plugins.path.join(this.cacheDir, 'index.json');
30
+ }
31
+
32
+ /**
33
+ * Initializes the cache by loading from disk
34
+ */
35
+ public async init(): Promise<void> {
36
+ if (!this.config.enabled) {
37
+ return;
38
+ }
39
+
40
+ // Ensure cache directory exists
41
+ await plugins.smartfile.fs.ensureDir(this.cacheDir);
42
+
43
+ // Load cache index if it exists
44
+ try {
45
+ const indexExists = await plugins.smartfile.fs.fileExists(this.cacheIndexPath);
46
+ if (indexExists) {
47
+ const indexContent = await plugins.smartfile.fs.toStringSync(this.cacheIndexPath);
48
+ const indexData = JSON.parse(indexContent) as ICacheEntry[];
49
+ if (Array.isArray(indexData)) {
50
+ for (const entry of indexData) {
51
+ this.cache.set(entry.path, entry);
52
+ }
53
+ }
54
+ }
55
+ } catch (error) {
56
+ console.warn('Failed to load cache index:', error.message);
57
+ // Start with empty cache if loading fails
58
+ }
59
+
60
+ // Clean up expired and invalid entries
61
+ await this.cleanup();
62
+ }
63
+
64
+ /**
65
+ * Gets a cached entry if it's still valid
66
+ * @param filePath - Absolute path to the file
67
+ * @returns Cache entry if valid, null otherwise
68
+ */
69
+ public async get(filePath: string): Promise<ICacheEntry | null> {
70
+ if (!this.config.enabled) {
71
+ return null;
72
+ }
73
+
74
+ const entry = this.cache.get(filePath);
75
+ if (!entry) {
76
+ return null;
77
+ }
78
+
79
+ // Check if entry is expired
80
+ const now = Date.now();
81
+ if (now - entry.cachedAt > this.config.ttl * 1000) {
82
+ this.cache.delete(filePath);
83
+ return null;
84
+ }
85
+
86
+ // Check if file has been modified
87
+ try {
88
+ const stats = await fs.promises.stat(filePath);
89
+ const currentMtime = Math.floor(stats.mtimeMs);
90
+
91
+ if (currentMtime !== entry.mtime) {
92
+ // File has changed, invalidate cache
93
+ this.cache.delete(filePath);
94
+ return null;
95
+ }
96
+
97
+ return entry;
98
+ } catch (error) {
99
+ // File doesn't exist anymore
100
+ this.cache.delete(filePath);
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Stores a cache entry
107
+ * @param entry - Cache entry to store
108
+ */
109
+ public async set(entry: ICacheEntry): Promise<void> {
110
+ if (!this.config.enabled) {
111
+ return;
112
+ }
113
+
114
+ this.cache.set(entry.path, entry);
115
+
116
+ // Check cache size and evict old entries if needed
117
+ await this.enforceMaxSize();
118
+
119
+ // Persist to disk (async, don't await)
120
+ this.persist().catch((error) => {
121
+ console.warn('Failed to persist cache:', error.message);
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Stores multiple cache entries
127
+ * @param entries - Array of cache entries
128
+ */
129
+ public async setMany(entries: ICacheEntry[]): Promise<void> {
130
+ if (!this.config.enabled) {
131
+ return;
132
+ }
133
+
134
+ for (const entry of entries) {
135
+ this.cache.set(entry.path, entry);
136
+ }
137
+
138
+ await this.enforceMaxSize();
139
+ await this.persist();
140
+ }
141
+
142
+ /**
143
+ * Checks if a file is cached and valid
144
+ * @param filePath - Absolute path to the file
145
+ * @returns True if cached and valid
146
+ */
147
+ public async has(filePath: string): Promise<boolean> {
148
+ const entry = await this.get(filePath);
149
+ return entry !== null;
150
+ }
151
+
152
+ /**
153
+ * Gets cache statistics
154
+ */
155
+ public getStats(): {
156
+ entries: number;
157
+ totalSize: number;
158
+ oldestEntry: number | null;
159
+ newestEntry: number | null;
160
+ } {
161
+ let totalSize = 0;
162
+ let oldestEntry: number | null = null;
163
+ let newestEntry: number | null = null;
164
+
165
+ for (const entry of this.cache.values()) {
166
+ totalSize += entry.contents.length;
167
+
168
+ if (oldestEntry === null || entry.cachedAt < oldestEntry) {
169
+ oldestEntry = entry.cachedAt;
170
+ }
171
+
172
+ if (newestEntry === null || entry.cachedAt > newestEntry) {
173
+ newestEntry = entry.cachedAt;
174
+ }
175
+ }
176
+
177
+ return {
178
+ entries: this.cache.size,
179
+ totalSize,
180
+ oldestEntry,
181
+ newestEntry,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Clears all cache entries
187
+ */
188
+ public async clear(): Promise<void> {
189
+ this.cache.clear();
190
+ await this.persist();
191
+ }
192
+
193
+ /**
194
+ * Clears specific cache entries
195
+ * @param filePaths - Array of file paths to clear
196
+ */
197
+ public async clearPaths(filePaths: string[]): Promise<void> {
198
+ for (const path of filePaths) {
199
+ this.cache.delete(path);
200
+ }
201
+ await this.persist();
202
+ }
203
+
204
+ /**
205
+ * Cleans up expired and invalid cache entries
206
+ */
207
+ private async cleanup(): Promise<void> {
208
+ const now = Date.now();
209
+ const toDelete: string[] = [];
210
+
211
+ for (const [path, entry] of this.cache.entries()) {
212
+ // Check expiration
213
+ if (now - entry.cachedAt > this.config.ttl * 1000) {
214
+ toDelete.push(path);
215
+ continue;
216
+ }
217
+
218
+ // Check if file still exists and hasn't changed
219
+ try {
220
+ const stats = await fs.promises.stat(path);
221
+ const currentMtime = Math.floor(stats.mtimeMs);
222
+
223
+ if (currentMtime !== entry.mtime) {
224
+ toDelete.push(path);
225
+ }
226
+ } catch (error) {
227
+ // File doesn't exist
228
+ toDelete.push(path);
229
+ }
230
+ }
231
+
232
+ for (const path of toDelete) {
233
+ this.cache.delete(path);
234
+ }
235
+
236
+ if (toDelete.length > 0) {
237
+ await this.persist();
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Enforces maximum cache size by evicting oldest entries
243
+ */
244
+ private async enforceMaxSize(): Promise<void> {
245
+ const stats = this.getStats();
246
+ const maxSizeBytes = this.config.maxSize * 1024 * 1024; // Convert MB to bytes
247
+
248
+ if (stats.totalSize <= maxSizeBytes) {
249
+ return;
250
+ }
251
+
252
+ // Sort entries by age (oldest first)
253
+ const entries = Array.from(this.cache.entries()).sort(
254
+ (a, b) => a[1].cachedAt - b[1].cachedAt
255
+ );
256
+
257
+ // Remove oldest entries until we're under the limit
258
+ let currentSize = stats.totalSize;
259
+ for (const [path, entry] of entries) {
260
+ if (currentSize <= maxSizeBytes) {
261
+ break;
262
+ }
263
+
264
+ currentSize -= entry.contents.length;
265
+ this.cache.delete(path);
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Persists cache index to disk
271
+ */
272
+ private async persist(): Promise<void> {
273
+ if (!this.config.enabled) {
274
+ return;
275
+ }
276
+
277
+ try {
278
+ const entries = Array.from(this.cache.values());
279
+ const content = JSON.stringify(entries, null, 2);
280
+ await plugins.smartfile.memory.toFs(content, this.cacheIndexPath);
281
+ } catch (error) {
282
+ console.warn('Failed to persist cache index:', error.message);
283
+ }
284
+ }
285
+ }
@@ -243,4 +243,68 @@ export class ContextTrimmer {
243
243
  ...config
244
244
  };
245
245
  }
246
+
247
+ /**
248
+ * Trim a file based on its importance tier
249
+ * @param filePath The path to the file
250
+ * @param content The file's contents
251
+ * @param level The trimming level to apply ('none', 'light', 'aggressive')
252
+ * @returns The trimmed file contents
253
+ */
254
+ public trimFileWithLevel(
255
+ filePath: string,
256
+ content: string,
257
+ level: 'none' | 'light' | 'aggressive'
258
+ ): string {
259
+ // No trimming for essential files
260
+ if (level === 'none') {
261
+ return content;
262
+ }
263
+
264
+ // Create a temporary config based on level
265
+ const originalConfig = { ...this.config };
266
+
267
+ try {
268
+ if (level === 'light') {
269
+ // Light trimming: preserve signatures, remove only complex implementations
270
+ this.config = {
271
+ ...this.config,
272
+ removeImplementations: false,
273
+ preserveInterfaces: true,
274
+ preserveTypeDefs: true,
275
+ preserveJSDoc: true,
276
+ maxFunctionLines: 10,
277
+ removeComments: false,
278
+ removeBlankLines: true
279
+ };
280
+ } else if (level === 'aggressive') {
281
+ // Aggressive trimming: remove all implementations, keep only signatures
282
+ this.config = {
283
+ ...this.config,
284
+ removeImplementations: true,
285
+ preserveInterfaces: true,
286
+ preserveTypeDefs: true,
287
+ preserveJSDoc: true,
288
+ maxFunctionLines: 3,
289
+ removeComments: true,
290
+ removeBlankLines: true
291
+ };
292
+ }
293
+
294
+ // Process based on file type
295
+ let result = content;
296
+ if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
297
+ result = this.trimTypeScriptFile(content);
298
+ } else if (filePath.endsWith('.md')) {
299
+ result = this.trimMarkdownFile(content);
300
+ } else if (filePath.endsWith('.json')) {
301
+ result = this.trimJsonFile(content);
302
+ }
303
+
304
+ return result;
305
+ } finally {
306
+ // Restore original config
307
+ this.config = originalConfig;
308
+ }
309
+ }
246
310
  }