@eddacraft/anvil-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +14 -0
  2. package/dist/cache/cache-key.d.ts +45 -0
  3. package/dist/cache/cache-key.d.ts.map +1 -0
  4. package/dist/cache/cache-key.js +135 -0
  5. package/dist/cache/index.d.ts +27 -0
  6. package/dist/cache/index.d.ts.map +1 -0
  7. package/dist/cache/index.js +38 -0
  8. package/dist/cache/providers/file-cache.d.ts +63 -0
  9. package/dist/cache/providers/file-cache.d.ts.map +1 -0
  10. package/dist/cache/providers/file-cache.js +369 -0
  11. package/dist/cache/providers/memory-cache.d.ts +52 -0
  12. package/dist/cache/providers/memory-cache.d.ts.map +1 -0
  13. package/dist/cache/providers/memory-cache.js +197 -0
  14. package/dist/cache/providers/null-cache.d.ts +26 -0
  15. package/dist/cache/providers/null-cache.d.ts.map +1 -0
  16. package/dist/cache/providers/null-cache.js +50 -0
  17. package/dist/cache/types.d.ts +114 -0
  18. package/dist/cache/types.d.ts.map +1 -0
  19. package/dist/cache/types.js +4 -0
  20. package/dist/concurrency/agent.d.ts +137 -0
  21. package/dist/concurrency/agent.d.ts.map +1 -0
  22. package/dist/concurrency/agent.js +440 -0
  23. package/dist/concurrency/atomic.d.ts +93 -0
  24. package/dist/concurrency/atomic.d.ts.map +1 -0
  25. package/dist/concurrency/atomic.js +281 -0
  26. package/dist/concurrency/git-agent.d.ts +114 -0
  27. package/dist/concurrency/git-agent.d.ts.map +1 -0
  28. package/dist/concurrency/git-agent.js +313 -0
  29. package/dist/concurrency/index.d.ts +95 -0
  30. package/dist/concurrency/index.d.ts.map +1 -0
  31. package/dist/concurrency/index.js +127 -0
  32. package/dist/concurrency/lock-manager.d.ts +170 -0
  33. package/dist/concurrency/lock-manager.d.ts.map +1 -0
  34. package/dist/concurrency/lock-manager.js +525 -0
  35. package/dist/concurrency/queue-manager.d.ts +166 -0
  36. package/dist/concurrency/queue-manager.d.ts.map +1 -0
  37. package/dist/concurrency/queue-manager.js +442 -0
  38. package/dist/concurrency/types.d.ts +382 -0
  39. package/dist/concurrency/types.d.ts.map +1 -0
  40. package/dist/concurrency/types.js +204 -0
  41. package/dist/export/constraint-collector.d.ts +175 -0
  42. package/dist/export/constraint-collector.d.ts.map +1 -0
  43. package/dist/export/constraint-collector.js +203 -0
  44. package/dist/export/formatters/llms-txt-formatter.d.ts +89 -0
  45. package/dist/export/formatters/llms-txt-formatter.d.ts.map +1 -0
  46. package/dist/export/formatters/llms-txt-formatter.js +249 -0
  47. package/dist/export/formatters/mcp-resource-formatter.d.ts +186 -0
  48. package/dist/export/formatters/mcp-resource-formatter.d.ts.map +1 -0
  49. package/dist/export/formatters/mcp-resource-formatter.js +139 -0
  50. package/dist/export/formatters/prompt-formatter.d.ts +83 -0
  51. package/dist/export/formatters/prompt-formatter.d.ts.map +1 -0
  52. package/dist/export/formatters/prompt-formatter.js +256 -0
  53. package/dist/export/index.d.ts +10 -0
  54. package/dist/export/index.d.ts.map +1 -0
  55. package/dist/export/index.js +9 -0
  56. package/dist/gate/check.interface.d.ts +15 -0
  57. package/dist/gate/check.interface.d.ts.map +1 -0
  58. package/dist/gate/check.interface.js +18 -0
  59. package/dist/gate/checks/antipattern.check.d.ts +27 -0
  60. package/dist/gate/checks/antipattern.check.d.ts.map +1 -0
  61. package/dist/gate/checks/antipattern.check.js +140 -0
  62. package/dist/gate/checks/architecture/circular-detector.d.ts +33 -0
  63. package/dist/gate/checks/architecture/circular-detector.d.ts.map +1 -0
  64. package/dist/gate/checks/architecture/circular-detector.js +71 -0
  65. package/dist/gate/checks/architecture/dependency-analyzer.d.ts +81 -0
  66. package/dist/gate/checks/architecture/dependency-analyzer.d.ts.map +1 -0
  67. package/dist/gate/checks/architecture/dependency-analyzer.js +136 -0
  68. package/dist/gate/checks/architecture/layer-validator.d.ts +75 -0
  69. package/dist/gate/checks/architecture/layer-validator.d.ts.map +1 -0
  70. package/dist/gate/checks/architecture/layer-validator.js +193 -0
  71. package/dist/gate/checks/architecture.check.d.ts +56 -0
  72. package/dist/gate/checks/architecture.check.d.ts.map +1 -0
  73. package/dist/gate/checks/architecture.check.js +394 -0
  74. package/dist/gate/checks/command-safety.check.d.ts +12 -0
  75. package/dist/gate/checks/command-safety.check.d.ts.map +1 -0
  76. package/dist/gate/checks/command-safety.check.js +230 -0
  77. package/dist/gate/checks/coverage.check.d.ts +9 -0
  78. package/dist/gate/checks/coverage.check.d.ts.map +1 -0
  79. package/dist/gate/checks/coverage.check.js +81 -0
  80. package/dist/gate/checks/dependency.check.d.ts +17 -0
  81. package/dist/gate/checks/dependency.check.d.ts.map +1 -0
  82. package/dist/gate/checks/dependency.check.js +342 -0
  83. package/dist/gate/checks/eslint.check.d.ts +14 -0
  84. package/dist/gate/checks/eslint.check.d.ts.map +1 -0
  85. package/dist/gate/checks/eslint.check.js +79 -0
  86. package/dist/gate/checks/policy.check.d.ts +78 -0
  87. package/dist/gate/checks/policy.check.d.ts.map +1 -0
  88. package/dist/gate/checks/policy.check.js +457 -0
  89. package/dist/gate/checks/secret/entropy-detector.d.ts +44 -0
  90. package/dist/gate/checks/secret/entropy-detector.d.ts.map +1 -0
  91. package/dist/gate/checks/secret/entropy-detector.js +76 -0
  92. package/dist/gate/checks/secret/git-scanner.d.ts +36 -0
  93. package/dist/gate/checks/secret/git-scanner.d.ts.map +1 -0
  94. package/dist/gate/checks/secret/git-scanner.js +90 -0
  95. package/dist/gate/checks/secret/secret-patterns.d.ts +42 -0
  96. package/dist/gate/checks/secret/secret-patterns.d.ts.map +1 -0
  97. package/dist/gate/checks/secret/secret-patterns.js +137 -0
  98. package/dist/gate/checks/secret.check.d.ts +56 -0
  99. package/dist/gate/checks/secret.check.d.ts.map +1 -0
  100. package/dist/gate/checks/secret.check.js +245 -0
  101. package/dist/gate/config/command-safety-config.d.ts +5 -0
  102. package/dist/gate/config/command-safety-config.d.ts.map +1 -0
  103. package/dist/gate/config/command-safety-config.js +69 -0
  104. package/dist/gate/config/index.d.ts +2 -0
  105. package/dist/gate/config/index.d.ts.map +1 -0
  106. package/dist/gate/config/index.js +1 -0
  107. package/dist/gate/formatters/command-safety-formatter.d.ts +10 -0
  108. package/dist/gate/formatters/command-safety-formatter.d.ts.map +1 -0
  109. package/dist/gate/formatters/command-safety-formatter.js +64 -0
  110. package/dist/gate/formatters/index.d.ts +2 -0
  111. package/dist/gate/formatters/index.d.ts.map +1 -0
  112. package/dist/gate/formatters/index.js +1 -0
  113. package/dist/gate/gate-config.d.ts +44 -0
  114. package/dist/gate/gate-config.d.ts.map +1 -0
  115. package/dist/gate/gate-config.js +334 -0
  116. package/dist/gate/gate-runner.d.ts +160 -0
  117. package/dist/gate/gate-runner.d.ts.map +1 -0
  118. package/dist/gate/gate-runner.js +531 -0
  119. package/dist/gate/index.d.ts +20 -0
  120. package/dist/gate/index.d.ts.map +1 -0
  121. package/dist/gate/index.js +14 -0
  122. package/dist/gate/parsers/command-parser.d.ts +18 -0
  123. package/dist/gate/parsers/command-parser.d.ts.map +1 -0
  124. package/dist/gate/parsers/command-parser.js +363 -0
  125. package/dist/gate/parsers/index.d.ts +2 -0
  126. package/dist/gate/parsers/index.d.ts.map +1 -0
  127. package/dist/gate/parsers/index.js +1 -0
  128. package/dist/gate/policy/index.d.ts +12 -0
  129. package/dist/gate/policy/index.d.ts.map +1 -0
  130. package/dist/gate/policy/index.js +10 -0
  131. package/dist/gate/rules/default-filesystem-rules.d.ts +3 -0
  132. package/dist/gate/rules/default-filesystem-rules.d.ts.map +1 -0
  133. package/dist/gate/rules/default-filesystem-rules.js +201 -0
  134. package/dist/gate/rules/default-git-rules.d.ts +3 -0
  135. package/dist/gate/rules/default-git-rules.d.ts.map +1 -0
  136. package/dist/gate/rules/default-git-rules.js +192 -0
  137. package/dist/gate/rules/index.d.ts +5 -0
  138. package/dist/gate/rules/index.d.ts.map +1 -0
  139. package/dist/gate/rules/index.js +3 -0
  140. package/dist/gate/rules/rule-matcher.d.ts +27 -0
  141. package/dist/gate/rules/rule-matcher.d.ts.map +1 -0
  142. package/dist/gate/rules/rule-matcher.js +228 -0
  143. package/dist/gate/rules/types.d.ts +250 -0
  144. package/dist/gate/rules/types.d.ts.map +1 -0
  145. package/dist/gate/rules/types.js +1 -0
  146. package/dist/index.d.ts +19 -0
  147. package/dist/index.d.ts.map +1 -0
  148. package/dist/index.js +35 -0
  149. package/dist/types/gate.types.d.ts +42 -0
  150. package/dist/types/gate.types.d.ts.map +1 -0
  151. package/dist/types/gate.types.js +94 -0
  152. package/dist/watch/debouncer.d.ts +90 -0
  153. package/dist/watch/debouncer.d.ts.map +1 -0
  154. package/dist/watch/debouncer.js +135 -0
  155. package/dist/watch/file-watcher.d.ts +73 -0
  156. package/dist/watch/file-watcher.d.ts.map +1 -0
  157. package/dist/watch/file-watcher.js +121 -0
  158. package/dist/watch/git-status.d.ts +98 -0
  159. package/dist/watch/git-status.d.ts.map +1 -0
  160. package/dist/watch/git-status.js +266 -0
  161. package/dist/watch/index.d.ts +16 -0
  162. package/dist/watch/index.d.ts.map +1 -0
  163. package/dist/watch/index.js +15 -0
  164. package/dist/watch/orchestrator.d.ts +113 -0
  165. package/dist/watch/orchestrator.d.ts.map +1 -0
  166. package/dist/watch/orchestrator.js +409 -0
  167. package/dist/watch/types.d.ts +190 -0
  168. package/dist/watch/types.d.ts.map +1 -0
  169. package/dist/watch/types.js +76 -0
  170. package/package.json +60 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * File-based cache provider
3
+ * Stores cache entries as JSON files in .anvil/cache/
4
+ */
5
+ import { existsSync, unlinkSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
6
+ import { readFile, rm, mkdir } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
9
+ import { homedir } from 'node:os';
10
+ import { z } from 'zod';
11
+ import { createDebugger } from '@eddacraft/anvil-core';
12
+ import { atomicWriteText } from '../../concurrency/atomic.js';
13
+ const debug = createDebugger('cache');
14
+ const CacheEntrySchema = z.object({
15
+ value: z.unknown(),
16
+ created_at: z.number(),
17
+ expires_at: z.number().optional(),
18
+ key: z.string(),
19
+ input_hash: z.string().optional(),
20
+ });
21
+ const CacheIndexEntrySchema = z.object({
22
+ file: z.string(),
23
+ created_at: z.number(),
24
+ expires_at: z.number().optional(),
25
+ size_bytes: z.number(),
26
+ });
27
+ const CacheIndexSchema = z.object({
28
+ version: z.number(),
29
+ entries: z.record(z.string(), CacheIndexEntrySchema),
30
+ stats: z.object({
31
+ hits: z.number(),
32
+ misses: z.number(),
33
+ }),
34
+ });
35
+ /**
36
+ * Default cache directory
37
+ */
38
+ const DEFAULT_CACHE_DIR = '.anvil/cache';
39
+ /**
40
+ * Default TTL: 24 hours
41
+ */
42
+ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
43
+ /**
44
+ * File-based cache provider
45
+ *
46
+ * Storage structure:
47
+ * .anvil/cache/
48
+ * ├── index.json # Cache registry and stats
49
+ * └── entries/ # Cache entry files
50
+ * └── {key-hash}.json
51
+ */
52
+ export class FileCacheProvider {
53
+ name = 'file';
54
+ cacheDir;
55
+ entriesDir;
56
+ indexPath;
57
+ defaultTtl;
58
+ maxSizeBytes;
59
+ hmacKey;
60
+ index = null;
61
+ indexDirty = false;
62
+ constructor(workspaceRoot, config = {}) {
63
+ if (config.useGlobalCache) {
64
+ this.cacheDir = join(homedir(), '.anvil', 'cache');
65
+ }
66
+ else {
67
+ this.cacheDir = config.cacheDir || join(workspaceRoot, DEFAULT_CACHE_DIR);
68
+ }
69
+ this.entriesDir = join(this.cacheDir, 'entries');
70
+ this.indexPath = join(this.cacheDir, 'index.json');
71
+ this.defaultTtl = config.defaultTtl ?? DEFAULT_TTL_MS;
72
+ this.maxSizeBytes = config.maxSizeBytes ?? 100 * 1024 * 1024; // 100MB
73
+ // HMAC key: prefer env var, then persisted random key in cache dir
74
+ this.hmacKey = process.env['ANVIL_CACHE_HMAC_KEY'] ?? this.loadOrCreateHmacKey();
75
+ }
76
+ /**
77
+ * Load a persisted HMAC key from the cache dir, or generate and store one.
78
+ * The key is stored with 0o600 permissions so only the current user can read it.
79
+ */
80
+ loadOrCreateHmacKey() {
81
+ const keyPath = join(this.cacheDir, '.hmac-key');
82
+ try {
83
+ return readFileSync(keyPath, 'utf-8').trim();
84
+ }
85
+ catch {
86
+ const key = randomBytes(32).toString('hex');
87
+ mkdirSync(this.cacheDir, { recursive: true });
88
+ writeFileSync(keyPath, key, { mode: 0o600 });
89
+ return key;
90
+ }
91
+ }
92
+ async get(key) {
93
+ debug(`file-cache get: key=${key}`);
94
+ const index = await this.loadIndex();
95
+ const entryMeta = index.entries[key];
96
+ if (!entryMeta) {
97
+ debug(`file-cache miss: key=${key} (not in index)`);
98
+ index.stats.misses++;
99
+ this.indexDirty = true;
100
+ await this.saveIndex();
101
+ return null;
102
+ }
103
+ // Check expiration
104
+ if (entryMeta.expires_at && Date.now() > entryMeta.expires_at) {
105
+ debug(`file-cache miss: key=${key} (expired)`);
106
+ await this.invalidate(key);
107
+ index.stats.misses++;
108
+ this.indexDirty = true;
109
+ await this.saveIndex();
110
+ return null;
111
+ }
112
+ // Read entry file and verify HMAC integrity
113
+ const entryPath = join(this.entriesDir, entryMeta.file);
114
+ try {
115
+ const raw = await readFile(entryPath, 'utf-8');
116
+ const newlineIndex = raw.indexOf('\n');
117
+ const storedHmac = newlineIndex >= 0 ? raw.slice(0, newlineIndex) : '';
118
+ const content = newlineIndex >= 0 ? raw.slice(newlineIndex + 1) : raw;
119
+ // Verify HMAC integrity (mandatory — reject entries without valid HMAC)
120
+ if (!storedHmac || !/^[0-9a-f]{64}$/i.test(storedHmac)) {
121
+ debug('Cache entry missing HMAC, rejecting to prevent injection', { key });
122
+ await this.invalidate(key);
123
+ index.stats.misses++;
124
+ this.indexDirty = true;
125
+ await this.saveIndex();
126
+ return null;
127
+ }
128
+ const expectedHmac = this.computeHmac(content);
129
+ if (!timingSafeEqual(Buffer.from(storedHmac, 'hex'), Buffer.from(expectedHmac, 'hex'))) {
130
+ debug('Cache entry HMAC verification failed, possible tampering', { key });
131
+ await this.invalidate(key);
132
+ index.stats.misses++;
133
+ this.indexDirty = true;
134
+ await this.saveIndex();
135
+ return null;
136
+ }
137
+ const parseResult = CacheEntrySchema.safeParse(JSON.parse(content));
138
+ if (!parseResult.success) {
139
+ debug('Invalid cache entry schema, removing from index', parseResult.error);
140
+ await this.invalidate(key);
141
+ index.stats.misses++;
142
+ this.indexDirty = true;
143
+ await this.saveIndex();
144
+ return null;
145
+ }
146
+ const entry = parseResult.data;
147
+ debug(`file-cache hit: key=${key}`);
148
+ index.stats.hits++;
149
+ this.indexDirty = true;
150
+ await this.saveIndex();
151
+ return entry;
152
+ }
153
+ catch (error) {
154
+ debug('Cache entry file missing or corrupted, removing from index', error);
155
+ await this.invalidate(key);
156
+ index.stats.misses++;
157
+ this.indexDirty = true;
158
+ await this.saveIndex();
159
+ return null;
160
+ }
161
+ }
162
+ async set(key, value, options) {
163
+ debug(`file-cache set: key=${key} ttl=${options.ttl ?? this.defaultTtl}`);
164
+ await this.ensureCacheDir();
165
+ const index = await this.loadIndex();
166
+ const now = Date.now();
167
+ const expiresAt = options.ttl ? now + options.ttl : now + this.defaultTtl;
168
+ const entry = {
169
+ value,
170
+ created_at: now,
171
+ expires_at: expiresAt,
172
+ key,
173
+ input_hash: options.input_hash,
174
+ };
175
+ // Generate filename from key hash
176
+ const fileHash = this.hashKey(key);
177
+ const fileName = `${fileHash}.json`;
178
+ const filePath = join(this.entriesDir, fileName);
179
+ // Write entry file atomically to prevent corruption in multi-agent scenarios
180
+ // Include HMAC for integrity verification
181
+ const content = JSON.stringify(entry, null, 2);
182
+ const hmac = this.computeHmac(content);
183
+ await atomicWriteText(filePath, `${hmac}\n${content}`);
184
+ // Update index
185
+ index.entries[key] = {
186
+ file: fileName,
187
+ created_at: now,
188
+ expires_at: expiresAt,
189
+ size_bytes: Buffer.byteLength(content, 'utf-8'),
190
+ };
191
+ this.indexDirty = true;
192
+ await this.saveIndex();
193
+ // Check if cache size exceeds limit
194
+ await this.maybeEvict();
195
+ }
196
+ async invalidate(key) {
197
+ debug(`file-cache invalidate: key=${key}`);
198
+ const index = await this.loadIndex();
199
+ const entryMeta = index.entries[key];
200
+ if (!entryMeta) {
201
+ debug(`file-cache invalidate: key=${key} not found`);
202
+ return false;
203
+ }
204
+ // Remove entry file
205
+ const filePath = join(this.entriesDir, entryMeta.file);
206
+ try {
207
+ unlinkSync(filePath);
208
+ }
209
+ catch (error) {
210
+ debug('Cache entry file already deleted or inaccessible', error);
211
+ }
212
+ // Remove from index
213
+ delete index.entries[key];
214
+ this.indexDirty = true;
215
+ await this.saveIndex();
216
+ return true;
217
+ }
218
+ async invalidatePattern(pattern) {
219
+ debug(`file-cache invalidatePattern: pattern=${pattern}`);
220
+ const index = await this.loadIndex();
221
+ const regex = this.patternToRegex(pattern);
222
+ const keysToInvalidate = Object.keys(index.entries).filter((key) => regex.test(key));
223
+ debug(`file-cache invalidatePattern: matched ${keysToInvalidate.length} entries`);
224
+ for (const key of keysToInvalidate) {
225
+ await this.invalidate(key);
226
+ }
227
+ return keysToInvalidate.length;
228
+ }
229
+ async getStats() {
230
+ const index = await this.loadIndex();
231
+ const entries = Object.keys(index.entries).length;
232
+ const sizeBytes = Object.values(index.entries).reduce((sum, e) => sum + e.size_bytes, 0);
233
+ const totalRequests = index.stats.hits + index.stats.misses;
234
+ const hitRate = totalRequests > 0 ? (index.stats.hits / totalRequests) * 100 : 0;
235
+ return {
236
+ hits: index.stats.hits,
237
+ misses: index.stats.misses,
238
+ entries,
239
+ size_bytes: sizeBytes,
240
+ hit_rate: Math.round(hitRate * 100) / 100,
241
+ };
242
+ }
243
+ async clear() {
244
+ try {
245
+ await rm(this.cacheDir, { recursive: true, force: true });
246
+ this.index = null;
247
+ this.indexDirty = false;
248
+ }
249
+ catch (error) {
250
+ debug('Failed to clear cache directory (may not exist)', error);
251
+ }
252
+ }
253
+ async isAvailable() {
254
+ try {
255
+ await this.ensureCacheDir();
256
+ return true;
257
+ }
258
+ catch (error) {
259
+ debug('Cache directory not available or not writable', error);
260
+ return false;
261
+ }
262
+ }
263
+ /**
264
+ * Clean up expired entries
265
+ */
266
+ async cleanup() {
267
+ debug('file-cache cleanup: scanning for expired entries');
268
+ const index = await this.loadIndex();
269
+ const now = Date.now();
270
+ const expiredKeys = Object.entries(index.entries)
271
+ .filter(([, meta]) => meta.expires_at && meta.expires_at < now)
272
+ .map(([key]) => key);
273
+ debug(`file-cache cleanup: found ${expiredKeys.length} expired entries`);
274
+ for (const key of expiredKeys) {
275
+ await this.invalidate(key);
276
+ }
277
+ return expiredKeys.length;
278
+ }
279
+ async ensureCacheDir() {
280
+ if (!existsSync(this.entriesDir)) {
281
+ await mkdir(this.entriesDir, { recursive: true });
282
+ }
283
+ }
284
+ async loadIndex() {
285
+ if (this.index) {
286
+ return this.index;
287
+ }
288
+ try {
289
+ const content = await readFile(this.indexPath, 'utf-8');
290
+ const parseResult = CacheIndexSchema.safeParse(JSON.parse(content));
291
+ if (parseResult.success) {
292
+ this.index = parseResult.data;
293
+ }
294
+ else {
295
+ debug('Invalid cache index schema, creating new one', parseResult.error);
296
+ this.index = {
297
+ version: 1,
298
+ entries: {},
299
+ stats: { hits: 0, misses: 0 },
300
+ };
301
+ this.indexDirty = true;
302
+ }
303
+ }
304
+ catch (error) {
305
+ debug('Cache index missing or corrupted, creating new one', error);
306
+ this.index = {
307
+ version: 1,
308
+ entries: {},
309
+ stats: { hits: 0, misses: 0 },
310
+ };
311
+ this.indexDirty = true;
312
+ }
313
+ return this.index;
314
+ }
315
+ async saveIndex() {
316
+ if (!this.indexDirty || !this.index) {
317
+ return;
318
+ }
319
+ await this.ensureCacheDir();
320
+ // Use atomic write to prevent corruption in multi-agent scenarios
321
+ await atomicWriteText(this.indexPath, JSON.stringify(this.index, null, 2));
322
+ this.indexDirty = false;
323
+ }
324
+ computeHmac(content) {
325
+ return createHmac('sha256', this.hmacKey).update(content).digest('hex');
326
+ }
327
+ hashKey(key) {
328
+ // Simple hash for filename safety
329
+ return createHash('sha256').update(key).digest('hex').slice(0, 32);
330
+ }
331
+ patternToRegex(pattern) {
332
+ const MAX_PATTERN_LENGTH = 200;
333
+ const MAX_WILDCARDS = 10;
334
+ if (pattern.length > MAX_PATTERN_LENGTH) {
335
+ throw new Error(`Cache pattern too long: ${pattern.length} > ${MAX_PATTERN_LENGTH}`);
336
+ }
337
+ const wildcardCount = (pattern.match(/\*/g) || []).length;
338
+ if (wildcardCount > MAX_WILDCARDS) {
339
+ throw new Error(`Too many wildcards in pattern: ${wildcardCount} > ${MAX_WILDCARDS}`);
340
+ }
341
+ const DOUBLE_STAR_PLACEHOLDER = '\x00DOUBLESTAR\x00';
342
+ const escaped = pattern
343
+ .replace(/\*\*/g, DOUBLE_STAR_PLACEHOLDER)
344
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
345
+ .replace(/\*/g, '[^:]*')
346
+ .replace(new RegExp(DOUBLE_STAR_PLACEHOLDER, 'g'), '[^:]*(?::[^:]*)*');
347
+ return new RegExp(`^${escaped}$`);
348
+ }
349
+ async maybeEvict() {
350
+ const stats = await this.getStats();
351
+ if (stats.size_bytes <= this.maxSizeBytes) {
352
+ return;
353
+ }
354
+ const index = await this.loadIndex();
355
+ // Sort entries by creation time (oldest first)
356
+ const sortedEntries = Object.entries(index.entries).sort(([, a], [, b]) => a.created_at - b.created_at);
357
+ // Evict oldest entries until under limit
358
+ let currentSize = stats.size_bytes;
359
+ for (const [key] of sortedEntries) {
360
+ if (currentSize <= this.maxSizeBytes * 0.8) {
361
+ // Evict until 80% of limit
362
+ break;
363
+ }
364
+ const meta = index.entries[key];
365
+ currentSize -= meta.size_bytes;
366
+ await this.invalidate(key);
367
+ }
368
+ }
369
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * In-memory cache provider
3
+ * Used for watch mode and short-lived sessions
4
+ */
5
+ import type { CacheProvider, CacheEntry, CacheSetOptions, CacheStats } from '../types.js';
6
+ /**
7
+ * Memory cache configuration
8
+ */
9
+ export interface MemoryCacheConfig {
10
+ /** Maximum number of entries (default: 1000) */
11
+ maxEntries?: number;
12
+ /** Default TTL in milliseconds (default: 5 minutes for watch mode) */
13
+ defaultTtl?: number;
14
+ }
15
+ /**
16
+ * In-memory cache provider
17
+ *
18
+ * Features:
19
+ * - Fast access for watch mode
20
+ * - LRU eviction when max entries exceeded
21
+ * - Automatic expiration cleanup
22
+ */
23
+ export declare class MemoryCacheProvider implements CacheProvider {
24
+ readonly name = "memory";
25
+ private readonly cache;
26
+ private readonly accessOrder;
27
+ private readonly maxEntries;
28
+ private readonly defaultTtl;
29
+ private stats;
30
+ constructor(config?: MemoryCacheConfig);
31
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
32
+ set<T>(key: string, value: T, options: CacheSetOptions): Promise<void>;
33
+ invalidate(key: string): Promise<boolean>;
34
+ invalidatePattern(pattern: string): Promise<number>;
35
+ getStats(): Promise<CacheStats>;
36
+ clear(): Promise<void>;
37
+ isAvailable(): Promise<boolean>;
38
+ /**
39
+ * Clean up expired entries
40
+ */
41
+ cleanup(): Promise<number>;
42
+ /**
43
+ * Get current entry count
44
+ */
45
+ get size(): number;
46
+ private updateAccessOrder;
47
+ private removeFromAccessOrder;
48
+ private evictLRU;
49
+ private patternToRegex;
50
+ private estimateSize;
51
+ }
52
+ //# sourceMappingURL=memory-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-cache.d.ts","sourceRoot":"","sources":["../../../src/cache/providers/memory-cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK1F;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,qBAAa,mBAAoB,YAAW,aAAa;IACvD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IAEpC,OAAO,CAAC,KAAK,CAGX;gBAEU,MAAM,GAAE,iBAAsB;IAKpC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IA0BlD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBtE,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQzC,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBnD,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC;IAqB/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAIrC;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;IAmBhC;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,QAAQ;IAsBhB,OAAO,CAAC,cAAc;IAsBtB,OAAO,CAAC,YAAY;CAQrB"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * In-memory cache provider
3
+ * Used for watch mode and short-lived sessions
4
+ */
5
+ import { createDebugger } from '@eddacraft/anvil-core';
6
+ const debug = createDebugger('cache');
7
+ /**
8
+ * In-memory cache provider
9
+ *
10
+ * Features:
11
+ * - Fast access for watch mode
12
+ * - LRU eviction when max entries exceeded
13
+ * - Automatic expiration cleanup
14
+ */
15
+ export class MemoryCacheProvider {
16
+ name = 'memory';
17
+ cache = new Map();
18
+ accessOrder = [];
19
+ maxEntries;
20
+ defaultTtl;
21
+ stats = {
22
+ hits: 0,
23
+ misses: 0,
24
+ };
25
+ constructor(config = {}) {
26
+ this.maxEntries = config.maxEntries ?? 1000;
27
+ this.defaultTtl = config.defaultTtl ?? 5 * 60 * 1000; // 5 minutes
28
+ }
29
+ async get(key) {
30
+ debug(`memory-cache get: key=${key} size=${this.cache.size}`);
31
+ const entry = this.cache.get(key);
32
+ if (!entry) {
33
+ debug(`memory-cache miss: key=${key} (not found)`);
34
+ this.stats.misses++;
35
+ return null;
36
+ }
37
+ // Check expiration
38
+ if (entry.expires_at && Date.now() > entry.expires_at) {
39
+ debug(`memory-cache miss: key=${key} (expired)`);
40
+ await this.invalidate(key);
41
+ this.stats.misses++;
42
+ return null;
43
+ }
44
+ // Update access order (move to end)
45
+ this.updateAccessOrder(key);
46
+ debug(`memory-cache hit: key=${key}`);
47
+ this.stats.hits++;
48
+ return entry;
49
+ }
50
+ async set(key, value, options) {
51
+ debug(`memory-cache set: key=${key} size=${this.cache.size}/${this.maxEntries}`);
52
+ const now = Date.now();
53
+ const expiresAt = options.ttl ? now + options.ttl : now + this.defaultTtl;
54
+ const entry = {
55
+ value,
56
+ created_at: now,
57
+ expires_at: expiresAt,
58
+ key,
59
+ input_hash: options.input_hash,
60
+ };
61
+ // Check if we need to evict
62
+ if (!this.cache.has(key) && this.cache.size >= this.maxEntries) {
63
+ debug(`memory-cache set: evicting LRU (at capacity ${this.maxEntries})`);
64
+ this.evictLRU();
65
+ }
66
+ this.cache.set(key, entry);
67
+ this.updateAccessOrder(key);
68
+ }
69
+ async invalidate(key) {
70
+ debug(`memory-cache invalidate: key=${key}`);
71
+ const existed = this.cache.has(key);
72
+ this.cache.delete(key);
73
+ this.removeFromAccessOrder(key);
74
+ return existed;
75
+ }
76
+ async invalidatePattern(pattern) {
77
+ debug(`memory-cache invalidatePattern: pattern=${pattern}`);
78
+ const regex = this.patternToRegex(pattern);
79
+ const keysToDelete = [];
80
+ for (const key of this.cache.keys()) {
81
+ if (regex.test(key)) {
82
+ keysToDelete.push(key);
83
+ }
84
+ }
85
+ debug(`memory-cache invalidatePattern: matched ${keysToDelete.length} entries`);
86
+ for (const key of keysToDelete) {
87
+ await this.invalidate(key);
88
+ }
89
+ return keysToDelete.length;
90
+ }
91
+ async getStats() {
92
+ const entries = this.cache.size;
93
+ let sizeBytes = 0;
94
+ // Estimate size (rough approximation)
95
+ for (const entry of this.cache.values()) {
96
+ sizeBytes += this.estimateSize(entry);
97
+ }
98
+ const totalRequests = this.stats.hits + this.stats.misses;
99
+ const hitRate = totalRequests > 0 ? (this.stats.hits / totalRequests) * 100 : 0;
100
+ return {
101
+ hits: this.stats.hits,
102
+ misses: this.stats.misses,
103
+ entries,
104
+ size_bytes: sizeBytes,
105
+ hit_rate: Math.round(hitRate * 100) / 100,
106
+ };
107
+ }
108
+ async clear() {
109
+ this.cache.clear();
110
+ this.accessOrder.length = 0;
111
+ this.stats = { hits: 0, misses: 0 };
112
+ }
113
+ async isAvailable() {
114
+ return true; // Memory cache is always available
115
+ }
116
+ /**
117
+ * Clean up expired entries
118
+ */
119
+ async cleanup() {
120
+ debug(`memory-cache cleanup: scanning ${this.cache.size} entries`);
121
+ const now = Date.now();
122
+ const expiredKeys = [];
123
+ for (const [key, entry] of this.cache.entries()) {
124
+ if (entry.expires_at && entry.expires_at < now) {
125
+ expiredKeys.push(key);
126
+ }
127
+ }
128
+ debug(`memory-cache cleanup: removing ${expiredKeys.length} expired entries`);
129
+ for (const key of expiredKeys) {
130
+ await this.invalidate(key);
131
+ }
132
+ return expiredKeys.length;
133
+ }
134
+ /**
135
+ * Get current entry count
136
+ */
137
+ get size() {
138
+ return this.cache.size;
139
+ }
140
+ updateAccessOrder(key) {
141
+ this.removeFromAccessOrder(key);
142
+ this.accessOrder.push(key);
143
+ }
144
+ removeFromAccessOrder(key) {
145
+ const index = this.accessOrder.indexOf(key);
146
+ if (index !== -1) {
147
+ this.accessOrder.splice(index, 1);
148
+ }
149
+ }
150
+ evictLRU() {
151
+ // Clean up expired entries first
152
+ const now = Date.now();
153
+ for (const [key, entry] of this.cache.entries()) {
154
+ if (entry.expires_at && entry.expires_at < now) {
155
+ this.cache.delete(key);
156
+ this.removeFromAccessOrder(key);
157
+ if (this.cache.size < this.maxEntries) {
158
+ return;
159
+ }
160
+ }
161
+ }
162
+ // If still over limit, evict least recently used
163
+ while (this.cache.size >= this.maxEntries && this.accessOrder.length > 0) {
164
+ const lruKey = this.accessOrder.shift();
165
+ if (lruKey) {
166
+ this.cache.delete(lruKey);
167
+ }
168
+ }
169
+ }
170
+ patternToRegex(pattern) {
171
+ const MAX_PATTERN_LENGTH = 200;
172
+ const MAX_WILDCARDS = 10;
173
+ if (pattern.length > MAX_PATTERN_LENGTH) {
174
+ throw new Error(`Cache pattern too long: ${pattern.length} > ${MAX_PATTERN_LENGTH}`);
175
+ }
176
+ const wildcardCount = (pattern.match(/\*/g) || []).length;
177
+ if (wildcardCount > MAX_WILDCARDS) {
178
+ throw new Error(`Too many wildcards in pattern: ${wildcardCount} > ${MAX_WILDCARDS}`);
179
+ }
180
+ const DOUBLE_STAR_PLACEHOLDER = '\x00DOUBLESTAR\x00';
181
+ const escaped = pattern
182
+ .replace(/\*\*/g, DOUBLE_STAR_PLACEHOLDER)
183
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
184
+ .replace(/\*/g, '[^:]*')
185
+ .replace(new RegExp(DOUBLE_STAR_PLACEHOLDER, 'g'), '[^:]*(?::[^:]*)*');
186
+ return new RegExp(`^${escaped}$`);
187
+ }
188
+ estimateSize(entry) {
189
+ // Rough estimation of object size in memory
190
+ try {
191
+ return JSON.stringify(entry).length * 2; // UTF-16 chars
192
+ }
193
+ catch {
194
+ return 1024; // Default estimate for non-serialisable objects
195
+ }
196
+ }
197
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Null cache provider (no-op)
3
+ * Used when caching is disabled via --no-cache flag
4
+ */
5
+ import type { CacheProvider, CacheEntry, CacheSetOptions, CacheStats } from '../types.js';
6
+ /**
7
+ * Null cache provider - disables caching
8
+ *
9
+ * All operations are no-ops that return appropriate null/empty values.
10
+ * Used when:
11
+ * - --no-cache flag is passed
12
+ * - Caching is disabled in configuration
13
+ * - Testing scenarios where caching should be bypassed
14
+ */
15
+ export declare class NullCacheProvider implements CacheProvider {
16
+ readonly name = "null";
17
+ private stats;
18
+ get<T>(_key: string): Promise<CacheEntry<T> | null>;
19
+ set<T>(_key: string, _value: T, _options: CacheSetOptions): Promise<void>;
20
+ invalidate(_key: string): Promise<boolean>;
21
+ invalidatePattern(_pattern: string): Promise<number>;
22
+ getStats(): Promise<CacheStats>;
23
+ clear(): Promise<void>;
24
+ isAvailable(): Promise<boolean>;
25
+ }
26
+ //# sourceMappingURL=null-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"null-cache.d.ts","sourceRoot":"","sources":["../../../src/cache/providers/null-cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK1F;;;;;;;;GAQG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IACrD,QAAQ,CAAC,IAAI,UAAU;IAEvB,OAAO,CAAC,KAAK,CAEX;IAEI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAMnD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI1C,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIpD,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC;IAU/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;CAGtC"}