@llm-translate/cli 1.0.0-next.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.
- package/.dockerignore +51 -0
- package/.env.example +33 -0
- package/.github/workflows/docs-pages.yml +57 -0
- package/.github/workflows/release.yml +49 -0
- package/.translaterc.json +44 -0
- package/CLAUDE.md +243 -0
- package/Dockerfile +55 -0
- package/README.md +371 -0
- package/RFC.md +1595 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4494 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1152 -0
- package/dist/index.js +3841 -0
- package/dist/index.js.map +1 -0
- package/docker-compose.yml +56 -0
- package/docs/.vitepress/config.ts +161 -0
- package/docs/api/agent.md +262 -0
- package/docs/api/engine.md +274 -0
- package/docs/api/index.md +171 -0
- package/docs/api/providers.md +304 -0
- package/docs/changelog.md +64 -0
- package/docs/cli/dir.md +243 -0
- package/docs/cli/file.md +213 -0
- package/docs/cli/glossary.md +273 -0
- package/docs/cli/index.md +129 -0
- package/docs/cli/init.md +158 -0
- package/docs/cli/serve.md +211 -0
- package/docs/glossary.json +235 -0
- package/docs/guide/chunking.md +272 -0
- package/docs/guide/configuration.md +139 -0
- package/docs/guide/cost-optimization.md +237 -0
- package/docs/guide/docker.md +371 -0
- package/docs/guide/getting-started.md +150 -0
- package/docs/guide/glossary.md +241 -0
- package/docs/guide/index.md +86 -0
- package/docs/guide/ollama.md +515 -0
- package/docs/guide/prompt-caching.md +221 -0
- package/docs/guide/providers.md +232 -0
- package/docs/guide/quality-control.md +206 -0
- package/docs/guide/vitepress-integration.md +265 -0
- package/docs/index.md +63 -0
- package/docs/ja/api/agent.md +262 -0
- package/docs/ja/api/engine.md +274 -0
- package/docs/ja/api/index.md +171 -0
- package/docs/ja/api/providers.md +304 -0
- package/docs/ja/changelog.md +64 -0
- package/docs/ja/cli/dir.md +243 -0
- package/docs/ja/cli/file.md +213 -0
- package/docs/ja/cli/glossary.md +273 -0
- package/docs/ja/cli/index.md +111 -0
- package/docs/ja/cli/init.md +158 -0
- package/docs/ja/guide/chunking.md +271 -0
- package/docs/ja/guide/configuration.md +139 -0
- package/docs/ja/guide/cost-optimization.md +30 -0
- package/docs/ja/guide/getting-started.md +150 -0
- package/docs/ja/guide/glossary.md +214 -0
- package/docs/ja/guide/index.md +32 -0
- package/docs/ja/guide/ollama.md +410 -0
- package/docs/ja/guide/prompt-caching.md +221 -0
- package/docs/ja/guide/providers.md +232 -0
- package/docs/ja/guide/quality-control.md +137 -0
- package/docs/ja/guide/vitepress-integration.md +265 -0
- package/docs/ja/index.md +58 -0
- package/docs/ko/api/agent.md +262 -0
- package/docs/ko/api/engine.md +274 -0
- package/docs/ko/api/index.md +171 -0
- package/docs/ko/api/providers.md +304 -0
- package/docs/ko/changelog.md +64 -0
- package/docs/ko/cli/dir.md +243 -0
- package/docs/ko/cli/file.md +213 -0
- package/docs/ko/cli/glossary.md +273 -0
- package/docs/ko/cli/index.md +111 -0
- package/docs/ko/cli/init.md +158 -0
- package/docs/ko/guide/chunking.md +271 -0
- package/docs/ko/guide/configuration.md +139 -0
- package/docs/ko/guide/cost-optimization.md +30 -0
- package/docs/ko/guide/getting-started.md +150 -0
- package/docs/ko/guide/glossary.md +214 -0
- package/docs/ko/guide/index.md +32 -0
- package/docs/ko/guide/ollama.md +410 -0
- package/docs/ko/guide/prompt-caching.md +221 -0
- package/docs/ko/guide/providers.md +232 -0
- package/docs/ko/guide/quality-control.md +137 -0
- package/docs/ko/guide/vitepress-integration.md +265 -0
- package/docs/ko/index.md +58 -0
- package/docs/zh/api/agent.md +262 -0
- package/docs/zh/api/engine.md +274 -0
- package/docs/zh/api/index.md +171 -0
- package/docs/zh/api/providers.md +304 -0
- package/docs/zh/changelog.md +64 -0
- package/docs/zh/cli/dir.md +243 -0
- package/docs/zh/cli/file.md +213 -0
- package/docs/zh/cli/glossary.md +273 -0
- package/docs/zh/cli/index.md +111 -0
- package/docs/zh/cli/init.md +158 -0
- package/docs/zh/guide/chunking.md +271 -0
- package/docs/zh/guide/configuration.md +139 -0
- package/docs/zh/guide/cost-optimization.md +30 -0
- package/docs/zh/guide/getting-started.md +150 -0
- package/docs/zh/guide/glossary.md +214 -0
- package/docs/zh/guide/index.md +32 -0
- package/docs/zh/guide/ollama.md +410 -0
- package/docs/zh/guide/prompt-caching.md +221 -0
- package/docs/zh/guide/providers.md +232 -0
- package/docs/zh/guide/quality-control.md +137 -0
- package/docs/zh/guide/vitepress-integration.md +265 -0
- package/docs/zh/index.md +58 -0
- package/package.json +91 -0
- package/release.config.mjs +15 -0
- package/schemas/glossary.schema.json +110 -0
- package/src/cli/commands/dir.ts +469 -0
- package/src/cli/commands/file.ts +291 -0
- package/src/cli/commands/glossary.ts +221 -0
- package/src/cli/commands/init.ts +68 -0
- package/src/cli/commands/serve.ts +60 -0
- package/src/cli/index.ts +64 -0
- package/src/cli/options.ts +59 -0
- package/src/core/agent.ts +1119 -0
- package/src/core/chunker.ts +391 -0
- package/src/core/engine.ts +634 -0
- package/src/errors.ts +188 -0
- package/src/index.ts +147 -0
- package/src/integrations/vitepress.ts +549 -0
- package/src/parsers/markdown.ts +383 -0
- package/src/providers/claude.ts +259 -0
- package/src/providers/interface.ts +109 -0
- package/src/providers/ollama.ts +379 -0
- package/src/providers/openai.ts +308 -0
- package/src/providers/registry.ts +153 -0
- package/src/server/index.ts +152 -0
- package/src/server/middleware/auth.ts +93 -0
- package/src/server/middleware/logger.ts +90 -0
- package/src/server/routes/health.ts +84 -0
- package/src/server/routes/translate.ts +210 -0
- package/src/server/types.ts +138 -0
- package/src/services/cache.ts +899 -0
- package/src/services/config.ts +217 -0
- package/src/services/glossary.ts +247 -0
- package/src/types/analysis.ts +164 -0
- package/src/types/index.ts +265 -0
- package/src/types/modes.ts +121 -0
- package/src/types/mqm.ts +157 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/tokens.ts +116 -0
- package/tests/fixtures/glossaries/ml-glossary.json +53 -0
- package/tests/fixtures/input/lynq-installation.ko.md +350 -0
- package/tests/fixtures/input/lynq-installation.md +350 -0
- package/tests/fixtures/input/simple.ko.md +27 -0
- package/tests/fixtures/input/simple.md +27 -0
- package/tests/unit/chunker.test.ts +229 -0
- package/tests/unit/glossary.test.ts +146 -0
- package/tests/unit/markdown.test.ts +205 -0
- package/tests/unit/tokens.test.ts +81 -0
- package/tsconfig.json +28 -0
- package/tsup.config.ts +34 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Cache Service
|
|
3
|
+
*
|
|
4
|
+
* File-based caching using content hashes to avoid re-translating unchanged content.
|
|
5
|
+
* Cache entries are indexed by a composite key of content hash, target language,
|
|
6
|
+
* glossary hash, provider, and model.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import type { CacheEntry, CacheIndex } from '../types/index.js';
|
|
13
|
+
import { logger } from '../utils/logger.js';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Constants
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
const CACHE_VERSION = '1.0';
|
|
20
|
+
const INDEX_FILE = 'index.json';
|
|
21
|
+
const ENTRIES_DIR = 'entries';
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Invalidation Policy Types
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Context provided to invalidation policies for evaluation
|
|
29
|
+
*/
|
|
30
|
+
export interface InvalidationContext {
|
|
31
|
+
/** Current glossary hash (if glossary is used) */
|
|
32
|
+
glossaryHash?: string;
|
|
33
|
+
/** Previous glossary hash (stored in cache metadata) */
|
|
34
|
+
previousGlossaryHash?: string;
|
|
35
|
+
/** Provider name */
|
|
36
|
+
provider?: string;
|
|
37
|
+
/** Model name */
|
|
38
|
+
model?: string;
|
|
39
|
+
/** Cache entry creation timestamp */
|
|
40
|
+
entryCreatedAt?: string;
|
|
41
|
+
/** Current timestamp */
|
|
42
|
+
currentTime?: Date;
|
|
43
|
+
/** Custom metadata for policy-specific checks */
|
|
44
|
+
metadata?: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Result of an invalidation check
|
|
49
|
+
*/
|
|
50
|
+
export interface InvalidationResult {
|
|
51
|
+
/** Whether invalidation should occur */
|
|
52
|
+
shouldInvalidate: boolean;
|
|
53
|
+
/** Reason for invalidation (for logging) */
|
|
54
|
+
reason?: string;
|
|
55
|
+
/** Scope of invalidation */
|
|
56
|
+
scope: 'all' | 'matching' | 'none';
|
|
57
|
+
/** Filter function for 'matching' scope - returns true if entry should be invalidated */
|
|
58
|
+
filter?: (entry: CacheEntry, key: string) => boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Abstract invalidation policy interface
|
|
63
|
+
*/
|
|
64
|
+
export interface InvalidationPolicy {
|
|
65
|
+
/** Policy name for logging */
|
|
66
|
+
readonly name: string;
|
|
67
|
+
/** Check if cache should be invalidated */
|
|
68
|
+
check(context: InvalidationContext): InvalidationResult;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Built-in Invalidation Policies
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Invalidates all cache entries when glossary changes
|
|
77
|
+
*/
|
|
78
|
+
export class GlossaryChangePolicy implements InvalidationPolicy {
|
|
79
|
+
readonly name = 'GlossaryChangePolicy';
|
|
80
|
+
|
|
81
|
+
private readonly mode: 'all' | 'matching';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param mode - 'all' invalidates entire cache, 'matching' only entries with old glossary hash
|
|
85
|
+
*/
|
|
86
|
+
constructor(mode: 'all' | 'matching' = 'all') {
|
|
87
|
+
this.mode = mode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
check(context: InvalidationContext): InvalidationResult {
|
|
91
|
+
const { glossaryHash, previousGlossaryHash } = context;
|
|
92
|
+
|
|
93
|
+
// No glossary or no previous hash - no invalidation needed
|
|
94
|
+
if (!glossaryHash || !previousGlossaryHash) {
|
|
95
|
+
return { shouldInvalidate: false, scope: 'none' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Glossary hasn't changed
|
|
99
|
+
if (glossaryHash === previousGlossaryHash) {
|
|
100
|
+
return { shouldInvalidate: false, scope: 'none' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Glossary changed
|
|
104
|
+
if (this.mode === 'all') {
|
|
105
|
+
return {
|
|
106
|
+
shouldInvalidate: true,
|
|
107
|
+
reason: `Glossary changed (${previousGlossaryHash.slice(0, 8)} → ${glossaryHash.slice(0, 8)})`,
|
|
108
|
+
scope: 'all',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Matching mode - only invalidate entries with old glossary hash
|
|
113
|
+
return {
|
|
114
|
+
shouldInvalidate: true,
|
|
115
|
+
reason: `Glossary changed, invalidating matching entries`,
|
|
116
|
+
scope: 'matching',
|
|
117
|
+
filter: (entry) => entry.glossaryHash === previousGlossaryHash,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Invalidates cache entries older than specified TTL
|
|
124
|
+
*/
|
|
125
|
+
export class TTLPolicy implements InvalidationPolicy {
|
|
126
|
+
readonly name = 'TTLPolicy';
|
|
127
|
+
|
|
128
|
+
private readonly ttlMs: number;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param ttlMs - Time-to-live in milliseconds
|
|
132
|
+
*/
|
|
133
|
+
constructor(ttlMs: number) {
|
|
134
|
+
this.ttlMs = ttlMs;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create policy with TTL in hours
|
|
139
|
+
*/
|
|
140
|
+
static hours(hours: number): TTLPolicy {
|
|
141
|
+
return new TTLPolicy(hours * 60 * 60 * 1000);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create policy with TTL in days
|
|
146
|
+
*/
|
|
147
|
+
static days(days: number): TTLPolicy {
|
|
148
|
+
return new TTLPolicy(days * 24 * 60 * 60 * 1000);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
check(context: InvalidationContext): InvalidationResult {
|
|
152
|
+
const currentTime = context.currentTime ?? new Date();
|
|
153
|
+
const ttlMs = this.ttlMs;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
shouldInvalidate: true,
|
|
157
|
+
reason: `TTL check (${this.ttlMs}ms)`,
|
|
158
|
+
scope: 'matching',
|
|
159
|
+
filter: (entry) => {
|
|
160
|
+
const createdAt = new Date(entry.createdAt);
|
|
161
|
+
const age = currentTime.getTime() - createdAt.getTime();
|
|
162
|
+
return age > ttlMs;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Invalidates cache entries when provider or model changes
|
|
170
|
+
*/
|
|
171
|
+
export class ProviderChangePolicy implements InvalidationPolicy {
|
|
172
|
+
readonly name = 'ProviderChangePolicy';
|
|
173
|
+
|
|
174
|
+
private readonly checkModel: boolean;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param checkModel - If true, also invalidate when model changes within same provider
|
|
178
|
+
*/
|
|
179
|
+
constructor(checkModel: boolean = true) {
|
|
180
|
+
this.checkModel = checkModel;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
check(context: InvalidationContext): InvalidationResult {
|
|
184
|
+
const { provider, model } = context;
|
|
185
|
+
|
|
186
|
+
if (!provider) {
|
|
187
|
+
return { shouldInvalidate: false, scope: 'none' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
shouldInvalidate: true,
|
|
192
|
+
reason: `Provider/model mismatch check`,
|
|
193
|
+
scope: 'matching',
|
|
194
|
+
filter: (entry) => {
|
|
195
|
+
if (entry.provider !== provider) return true;
|
|
196
|
+
if (this.checkModel && model && entry.model !== model) return true;
|
|
197
|
+
return false;
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Invalidates cache entries below a quality threshold
|
|
205
|
+
*/
|
|
206
|
+
export class QualityThresholdPolicy implements InvalidationPolicy {
|
|
207
|
+
readonly name = 'QualityThresholdPolicy';
|
|
208
|
+
|
|
209
|
+
private readonly threshold: number;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param threshold - Minimum quality score (0-100)
|
|
213
|
+
*/
|
|
214
|
+
constructor(threshold: number) {
|
|
215
|
+
this.threshold = threshold;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
check(_context: InvalidationContext): InvalidationResult {
|
|
219
|
+
const threshold = this.threshold;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
shouldInvalidate: true,
|
|
223
|
+
reason: `Quality below threshold (${threshold})`,
|
|
224
|
+
scope: 'matching',
|
|
225
|
+
filter: (entry) => entry.qualityScore < threshold,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Combines multiple policies - invalidates if ANY policy triggers
|
|
232
|
+
*/
|
|
233
|
+
export class CompositePolicy implements InvalidationPolicy {
|
|
234
|
+
readonly name = 'CompositePolicy';
|
|
235
|
+
|
|
236
|
+
private readonly policies: InvalidationPolicy[];
|
|
237
|
+
private readonly mode: 'any' | 'all';
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param policies - Array of policies to combine
|
|
241
|
+
* @param mode - 'any' triggers if any policy matches, 'all' requires all policies to match
|
|
242
|
+
*/
|
|
243
|
+
constructor(policies: InvalidationPolicy[], mode: 'any' | 'all' = 'any') {
|
|
244
|
+
this.policies = policies;
|
|
245
|
+
this.mode = mode;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
check(context: InvalidationContext): InvalidationResult {
|
|
249
|
+
const results = this.policies.map((p) => ({
|
|
250
|
+
policy: p,
|
|
251
|
+
result: p.check(context),
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
// Collect all filters that want to invalidate
|
|
255
|
+
const activeResults = results.filter((r) => r.result.shouldInvalidate);
|
|
256
|
+
|
|
257
|
+
if (activeResults.length === 0) {
|
|
258
|
+
return { shouldInvalidate: false, scope: 'none' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check for 'all' scope - takes priority
|
|
262
|
+
const allScope = activeResults.find((r) => r.result.scope === 'all');
|
|
263
|
+
if (allScope) {
|
|
264
|
+
return {
|
|
265
|
+
shouldInvalidate: true,
|
|
266
|
+
reason: `${allScope.policy.name}: ${allScope.result.reason}`,
|
|
267
|
+
scope: 'all',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Combine matching filters
|
|
272
|
+
const filters = activeResults
|
|
273
|
+
.filter((r) => r.result.filter)
|
|
274
|
+
.map((r) => r.result.filter!);
|
|
275
|
+
|
|
276
|
+
if (filters.length === 0) {
|
|
277
|
+
return { shouldInvalidate: false, scope: 'none' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const reasons = activeResults.map((r) => `${r.policy.name}`).join(', ');
|
|
281
|
+
|
|
282
|
+
if (this.mode === 'any') {
|
|
283
|
+
return {
|
|
284
|
+
shouldInvalidate: true,
|
|
285
|
+
reason: `Composite (any): ${reasons}`,
|
|
286
|
+
scope: 'matching',
|
|
287
|
+
filter: (entry, key) => filters.some((f) => f(entry, key)),
|
|
288
|
+
};
|
|
289
|
+
} else {
|
|
290
|
+
return {
|
|
291
|
+
shouldInvalidate: true,
|
|
292
|
+
reason: `Composite (all): ${reasons}`,
|
|
293
|
+
scope: 'matching',
|
|
294
|
+
filter: (entry, key) => filters.every((f) => f(entry, key)),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Types
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
export interface CacheOptions {
|
|
305
|
+
/** Cache directory path */
|
|
306
|
+
cacheDir: string;
|
|
307
|
+
/** Enable verbose logging */
|
|
308
|
+
verbose?: boolean;
|
|
309
|
+
/** Invalidation policies to apply */
|
|
310
|
+
invalidationPolicies?: InvalidationPolicy[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface CacheKey {
|
|
314
|
+
/** Source content (will be hashed) */
|
|
315
|
+
content: string;
|
|
316
|
+
/** Source language */
|
|
317
|
+
sourceLang: string;
|
|
318
|
+
/** Target language */
|
|
319
|
+
targetLang: string;
|
|
320
|
+
/** Glossary content or hash (will be hashed if string) */
|
|
321
|
+
glossary?: string;
|
|
322
|
+
/** Provider name */
|
|
323
|
+
provider: string;
|
|
324
|
+
/** Model name */
|
|
325
|
+
model: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface CacheStats {
|
|
329
|
+
/** Total number of cached entries */
|
|
330
|
+
entries: number;
|
|
331
|
+
/** Total cache size in bytes */
|
|
332
|
+
sizeBytes: number;
|
|
333
|
+
/** Cache version */
|
|
334
|
+
version: string;
|
|
335
|
+
/** Number of entries invalidated by policies */
|
|
336
|
+
invalidated?: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Cache metadata stored separately for tracking state across sessions
|
|
341
|
+
*/
|
|
342
|
+
export interface CacheMetadata {
|
|
343
|
+
/** Last known glossary hash */
|
|
344
|
+
glossaryHash?: string;
|
|
345
|
+
/** Last known provider */
|
|
346
|
+
provider?: string;
|
|
347
|
+
/** Last known model */
|
|
348
|
+
model?: string;
|
|
349
|
+
/** Last invalidation timestamp */
|
|
350
|
+
lastInvalidation?: string;
|
|
351
|
+
/** Custom metadata */
|
|
352
|
+
custom?: Record<string, unknown>;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export interface CacheResult {
|
|
356
|
+
/** Whether the entry was found in cache */
|
|
357
|
+
hit: boolean;
|
|
358
|
+
/** The cached entry if found */
|
|
359
|
+
entry?: CacheEntry;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// Hash Utilities
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Generate SHA-256 hash of content
|
|
368
|
+
*/
|
|
369
|
+
export function hashContent(content: string): string {
|
|
370
|
+
return createHash('sha256').update(content, 'utf8').digest('hex').slice(0, 16);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Generate cache key from components
|
|
375
|
+
*/
|
|
376
|
+
export function generateCacheKey(key: CacheKey): string {
|
|
377
|
+
const contentHash = hashContent(key.content);
|
|
378
|
+
const glossaryHash = key.glossary ? hashContent(key.glossary) : 'none';
|
|
379
|
+
|
|
380
|
+
// Create composite key: content_source_target_glossary_provider_model
|
|
381
|
+
return `${contentHash}_${key.sourceLang}_${key.targetLang}_${glossaryHash}_${key.provider}_${key.model}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// Cache Manager
|
|
386
|
+
// ============================================================================
|
|
387
|
+
|
|
388
|
+
export class CacheManager {
|
|
389
|
+
private cacheDir: string;
|
|
390
|
+
private indexPath: string;
|
|
391
|
+
private entriesDir: string;
|
|
392
|
+
private metadataPath: string;
|
|
393
|
+
private verbose: boolean;
|
|
394
|
+
private index: CacheIndex | null = null;
|
|
395
|
+
private policies: InvalidationPolicy[];
|
|
396
|
+
private metadata: CacheMetadata | null = null;
|
|
397
|
+
|
|
398
|
+
constructor(options: CacheOptions) {
|
|
399
|
+
this.cacheDir = options.cacheDir;
|
|
400
|
+
this.indexPath = join(this.cacheDir, INDEX_FILE);
|
|
401
|
+
this.entriesDir = join(this.cacheDir, ENTRIES_DIR);
|
|
402
|
+
this.metadataPath = join(this.cacheDir, 'metadata.json');
|
|
403
|
+
this.verbose = options.verbose ?? false;
|
|
404
|
+
this.policies = options.invalidationPolicies ?? [];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Initialize cache directory and load index
|
|
409
|
+
*/
|
|
410
|
+
private ensureInitialized(): void {
|
|
411
|
+
if (this.index !== null) return;
|
|
412
|
+
|
|
413
|
+
// Create cache directories if needed
|
|
414
|
+
if (!existsSync(this.cacheDir)) {
|
|
415
|
+
mkdirSync(this.cacheDir, { recursive: true });
|
|
416
|
+
}
|
|
417
|
+
if (!existsSync(this.entriesDir)) {
|
|
418
|
+
mkdirSync(this.entriesDir, { recursive: true });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Load or create index
|
|
422
|
+
if (existsSync(this.indexPath)) {
|
|
423
|
+
try {
|
|
424
|
+
const data = readFileSync(this.indexPath, 'utf-8');
|
|
425
|
+
this.index = JSON.parse(data) as CacheIndex;
|
|
426
|
+
|
|
427
|
+
// Check version compatibility
|
|
428
|
+
if (this.index.version !== CACHE_VERSION) {
|
|
429
|
+
if (this.verbose) {
|
|
430
|
+
logger.warn(`Cache version mismatch (${this.index.version} vs ${CACHE_VERSION}), clearing cache`);
|
|
431
|
+
}
|
|
432
|
+
this.clearSync();
|
|
433
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
if (this.verbose) {
|
|
437
|
+
logger.warn('Failed to load cache index, creating new one');
|
|
438
|
+
}
|
|
439
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Load metadata
|
|
446
|
+
this.loadMetadata();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Load cache metadata from disk
|
|
451
|
+
*/
|
|
452
|
+
private loadMetadata(): void {
|
|
453
|
+
if (existsSync(this.metadataPath)) {
|
|
454
|
+
try {
|
|
455
|
+
const data = readFileSync(this.metadataPath, 'utf-8');
|
|
456
|
+
this.metadata = JSON.parse(data) as CacheMetadata;
|
|
457
|
+
} catch {
|
|
458
|
+
this.metadata = {};
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
this.metadata = {};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Save cache metadata to disk
|
|
467
|
+
*/
|
|
468
|
+
private saveMetadata(): void {
|
|
469
|
+
if (!this.metadata) return;
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
writeFileSync(this.metadataPath, JSON.stringify(this.metadata, null, 2), 'utf-8');
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (this.verbose) {
|
|
475
|
+
logger.error(`Failed to save cache metadata: ${error}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Update cache metadata
|
|
482
|
+
*/
|
|
483
|
+
updateMetadata(updates: Partial<CacheMetadata>): void {
|
|
484
|
+
this.ensureInitialized();
|
|
485
|
+
this.metadata = { ...this.metadata, ...updates };
|
|
486
|
+
this.saveMetadata();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get current cache metadata
|
|
491
|
+
*/
|
|
492
|
+
getMetadata(): CacheMetadata {
|
|
493
|
+
this.ensureInitialized();
|
|
494
|
+
return { ...this.metadata! };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Apply all configured invalidation policies
|
|
499
|
+
* @returns Number of entries invalidated
|
|
500
|
+
*/
|
|
501
|
+
applyPolicies(context: InvalidationContext): number {
|
|
502
|
+
this.ensureInitialized();
|
|
503
|
+
|
|
504
|
+
if (this.policies.length === 0) {
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let totalInvalidated = 0;
|
|
509
|
+
|
|
510
|
+
for (const policy of this.policies) {
|
|
511
|
+
const result = policy.check({
|
|
512
|
+
...context,
|
|
513
|
+
previousGlossaryHash: this.metadata?.glossaryHash,
|
|
514
|
+
currentTime: context.currentTime ?? new Date(),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (!result.shouldInvalidate) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (this.verbose) {
|
|
522
|
+
logger.info(`Applying ${policy.name}: ${result.reason}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (result.scope === 'all') {
|
|
526
|
+
const count = Object.keys(this.index!.entries).length;
|
|
527
|
+
this.clear();
|
|
528
|
+
totalInvalidated += count;
|
|
529
|
+
break; // No need to check other policies after full clear
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (result.scope === 'matching' && result.filter) {
|
|
533
|
+
const invalidated = this.invalidateMatching(result.filter);
|
|
534
|
+
totalInvalidated += invalidated;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Update metadata after applying policies
|
|
539
|
+
if (context.glossaryHash) {
|
|
540
|
+
this.metadata!.glossaryHash = context.glossaryHash;
|
|
541
|
+
}
|
|
542
|
+
if (context.provider) {
|
|
543
|
+
this.metadata!.provider = context.provider;
|
|
544
|
+
}
|
|
545
|
+
if (context.model) {
|
|
546
|
+
this.metadata!.model = context.model;
|
|
547
|
+
}
|
|
548
|
+
if (totalInvalidated > 0) {
|
|
549
|
+
this.metadata!.lastInvalidation = new Date().toISOString();
|
|
550
|
+
}
|
|
551
|
+
this.saveMetadata();
|
|
552
|
+
|
|
553
|
+
return totalInvalidated;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Invalidate entries matching a filter function
|
|
558
|
+
* @returns Number of entries invalidated
|
|
559
|
+
*/
|
|
560
|
+
invalidateMatching(filter: (entry: CacheEntry, key: string) => boolean): number {
|
|
561
|
+
this.ensureInitialized();
|
|
562
|
+
|
|
563
|
+
const keysToDelete: string[] = [];
|
|
564
|
+
|
|
565
|
+
for (const [key, entry] of Object.entries(this.index!.entries)) {
|
|
566
|
+
if (filter(entry, key)) {
|
|
567
|
+
keysToDelete.push(key);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
for (const key of keysToDelete) {
|
|
572
|
+
// Remove entry file
|
|
573
|
+
const entryPath = join(this.entriesDir, `${key}.json`);
|
|
574
|
+
try {
|
|
575
|
+
if (existsSync(entryPath)) {
|
|
576
|
+
rmSync(entryPath);
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
// Ignore file deletion errors
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Remove from index
|
|
583
|
+
delete this.index!.entries[key];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (keysToDelete.length > 0) {
|
|
587
|
+
this.saveIndex();
|
|
588
|
+
if (this.verbose) {
|
|
589
|
+
logger.info(`Invalidated ${keysToDelete.length} cache entries`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return keysToDelete.length;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Add an invalidation policy at runtime
|
|
598
|
+
*/
|
|
599
|
+
addPolicy(policy: InvalidationPolicy): void {
|
|
600
|
+
this.policies.push(policy);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Remove an invalidation policy by name
|
|
605
|
+
*/
|
|
606
|
+
removePolicy(name: string): boolean {
|
|
607
|
+
const index = this.policies.findIndex((p) => p.name === name);
|
|
608
|
+
if (index !== -1) {
|
|
609
|
+
this.policies.splice(index, 1);
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Get all configured policies
|
|
617
|
+
*/
|
|
618
|
+
getPolicies(): InvalidationPolicy[] {
|
|
619
|
+
return [...this.policies];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Save index to disk
|
|
624
|
+
*/
|
|
625
|
+
private saveIndex(): void {
|
|
626
|
+
if (!this.index) return;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const dir = dirname(this.indexPath);
|
|
630
|
+
if (!existsSync(dir)) {
|
|
631
|
+
mkdirSync(dir, { recursive: true });
|
|
632
|
+
}
|
|
633
|
+
writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2), 'utf-8');
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (this.verbose) {
|
|
636
|
+
logger.error(`Failed to save cache index: ${error}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Get cached translation if available
|
|
643
|
+
*/
|
|
644
|
+
get(key: CacheKey): CacheResult {
|
|
645
|
+
this.ensureInitialized();
|
|
646
|
+
|
|
647
|
+
const cacheKey = generateCacheKey(key);
|
|
648
|
+
const entry = this.index!.entries[cacheKey];
|
|
649
|
+
|
|
650
|
+
if (entry) {
|
|
651
|
+
// Verify the entry file exists
|
|
652
|
+
const entryPath = join(this.entriesDir, `${cacheKey}.json`);
|
|
653
|
+
if (existsSync(entryPath)) {
|
|
654
|
+
if (this.verbose) {
|
|
655
|
+
logger.info(`Cache hit: ${cacheKey.slice(0, 20)}...`);
|
|
656
|
+
}
|
|
657
|
+
return { hit: true, entry };
|
|
658
|
+
} else {
|
|
659
|
+
// Entry in index but file missing, remove from index
|
|
660
|
+
delete this.index!.entries[cacheKey];
|
|
661
|
+
this.saveIndex();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (this.verbose) {
|
|
666
|
+
logger.debug(`Cache miss: ${cacheKey.slice(0, 20)}...`);
|
|
667
|
+
}
|
|
668
|
+
return { hit: false };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Store translation in cache
|
|
673
|
+
*/
|
|
674
|
+
set(key: CacheKey, translation: string, qualityScore: number): void {
|
|
675
|
+
this.ensureInitialized();
|
|
676
|
+
|
|
677
|
+
const cacheKey = generateCacheKey(key);
|
|
678
|
+
const contentHash = hashContent(key.content);
|
|
679
|
+
const glossaryHash = key.glossary ? hashContent(key.glossary) : '';
|
|
680
|
+
|
|
681
|
+
const entry: CacheEntry = {
|
|
682
|
+
sourceHash: contentHash,
|
|
683
|
+
sourceLang: key.sourceLang,
|
|
684
|
+
targetLang: key.targetLang,
|
|
685
|
+
glossaryHash,
|
|
686
|
+
translation,
|
|
687
|
+
qualityScore,
|
|
688
|
+
createdAt: new Date().toISOString(),
|
|
689
|
+
provider: key.provider,
|
|
690
|
+
model: key.model,
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Save entry file
|
|
694
|
+
const entryPath = join(this.entriesDir, `${cacheKey}.json`);
|
|
695
|
+
try {
|
|
696
|
+
writeFileSync(entryPath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
697
|
+
|
|
698
|
+
// Update index
|
|
699
|
+
this.index!.entries[cacheKey] = entry;
|
|
700
|
+
this.saveIndex();
|
|
701
|
+
|
|
702
|
+
if (this.verbose) {
|
|
703
|
+
logger.info(`Cached: ${cacheKey.slice(0, 20)}...`);
|
|
704
|
+
}
|
|
705
|
+
} catch (error) {
|
|
706
|
+
if (this.verbose) {
|
|
707
|
+
logger.error(`Failed to cache entry: ${error}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Check if entry exists in cache
|
|
714
|
+
*/
|
|
715
|
+
has(key: CacheKey): boolean {
|
|
716
|
+
return this.get(key).hit;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Remove entry from cache
|
|
721
|
+
*/
|
|
722
|
+
delete(key: CacheKey): boolean {
|
|
723
|
+
this.ensureInitialized();
|
|
724
|
+
|
|
725
|
+
const cacheKey = generateCacheKey(key);
|
|
726
|
+
|
|
727
|
+
if (this.index!.entries[cacheKey]) {
|
|
728
|
+
// Remove entry file
|
|
729
|
+
const entryPath = join(this.entriesDir, `${cacheKey}.json`);
|
|
730
|
+
try {
|
|
731
|
+
if (existsSync(entryPath)) {
|
|
732
|
+
rmSync(entryPath);
|
|
733
|
+
}
|
|
734
|
+
} catch {
|
|
735
|
+
// Ignore file deletion errors
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Remove from index
|
|
739
|
+
delete this.index!.entries[cacheKey];
|
|
740
|
+
this.saveIndex();
|
|
741
|
+
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Clear entire cache (synchronous)
|
|
750
|
+
*/
|
|
751
|
+
private clearSync(): void {
|
|
752
|
+
try {
|
|
753
|
+
if (existsSync(this.entriesDir)) {
|
|
754
|
+
rmSync(this.entriesDir, { recursive: true, force: true });
|
|
755
|
+
}
|
|
756
|
+
if (existsSync(this.indexPath)) {
|
|
757
|
+
rmSync(this.indexPath);
|
|
758
|
+
}
|
|
759
|
+
mkdirSync(this.entriesDir, { recursive: true });
|
|
760
|
+
} catch {
|
|
761
|
+
// Ignore errors during clear
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Clear entire cache
|
|
767
|
+
*/
|
|
768
|
+
clear(): void {
|
|
769
|
+
this.clearSync();
|
|
770
|
+
this.index = { version: CACHE_VERSION, entries: {} };
|
|
771
|
+
this.saveIndex();
|
|
772
|
+
|
|
773
|
+
if (this.verbose) {
|
|
774
|
+
logger.info('Cache cleared');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Get cache statistics
|
|
780
|
+
*/
|
|
781
|
+
getStats(): CacheStats {
|
|
782
|
+
this.ensureInitialized();
|
|
783
|
+
|
|
784
|
+
let sizeBytes = 0;
|
|
785
|
+
|
|
786
|
+
// Calculate size of all entry files
|
|
787
|
+
if (existsSync(this.entriesDir)) {
|
|
788
|
+
try {
|
|
789
|
+
const files = readdirSync(this.entriesDir);
|
|
790
|
+
for (const file of files) {
|
|
791
|
+
const filePath = join(this.entriesDir, file);
|
|
792
|
+
try {
|
|
793
|
+
const stat = statSync(filePath);
|
|
794
|
+
sizeBytes += stat.size;
|
|
795
|
+
} catch {
|
|
796
|
+
// Ignore stat errors
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
} catch {
|
|
800
|
+
// Ignore read errors
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Add index file size
|
|
805
|
+
if (existsSync(this.indexPath)) {
|
|
806
|
+
try {
|
|
807
|
+
const stat = statSync(this.indexPath);
|
|
808
|
+
sizeBytes += stat.size;
|
|
809
|
+
} catch {
|
|
810
|
+
// Ignore stat errors
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
entries: Object.keys(this.index?.entries ?? {}).length,
|
|
816
|
+
sizeBytes,
|
|
817
|
+
version: CACHE_VERSION,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Get all cached entries (for debugging)
|
|
823
|
+
*/
|
|
824
|
+
getAllEntries(): Record<string, CacheEntry> {
|
|
825
|
+
this.ensureInitialized();
|
|
826
|
+
return { ...this.index!.entries };
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ============================================================================
|
|
831
|
+
// Factory Function
|
|
832
|
+
// ============================================================================
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Create a cache manager instance
|
|
836
|
+
*/
|
|
837
|
+
export function createCacheManager(options: CacheOptions): CacheManager {
|
|
838
|
+
return new CacheManager(options);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Create a no-op cache manager that never caches (for --no-cache mode)
|
|
843
|
+
*/
|
|
844
|
+
export function createNullCacheManager(): CacheManager & { isNull: true } {
|
|
845
|
+
const nullManager = {
|
|
846
|
+
isNull: true as const,
|
|
847
|
+
get: () => ({ hit: false }),
|
|
848
|
+
set: () => {},
|
|
849
|
+
has: () => false,
|
|
850
|
+
delete: () => false,
|
|
851
|
+
clear: () => {},
|
|
852
|
+
getStats: () => ({ entries: 0, sizeBytes: 0, version: CACHE_VERSION }),
|
|
853
|
+
getAllEntries: () => ({}),
|
|
854
|
+
updateMetadata: () => {},
|
|
855
|
+
getMetadata: () => ({}),
|
|
856
|
+
applyPolicies: () => 0,
|
|
857
|
+
invalidateMatching: () => 0,
|
|
858
|
+
addPolicy: () => {},
|
|
859
|
+
removePolicy: () => false,
|
|
860
|
+
getPolicies: () => [],
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
return nullManager as unknown as CacheManager & { isNull: true };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// ============================================================================
|
|
867
|
+
// Preset Policy Configurations
|
|
868
|
+
// ============================================================================
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Create default invalidation policies for typical translation workflow
|
|
872
|
+
*/
|
|
873
|
+
export function createDefaultPolicies(): InvalidationPolicy[] {
|
|
874
|
+
return [
|
|
875
|
+
new GlossaryChangePolicy('all'),
|
|
876
|
+
TTLPolicy.days(30),
|
|
877
|
+
];
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Create strict invalidation policies (more aggressive invalidation)
|
|
882
|
+
*/
|
|
883
|
+
export function createStrictPolicies(qualityThreshold: number = 85): InvalidationPolicy[] {
|
|
884
|
+
return [
|
|
885
|
+
new GlossaryChangePolicy('all'),
|
|
886
|
+
new ProviderChangePolicy(true),
|
|
887
|
+
new QualityThresholdPolicy(qualityThreshold),
|
|
888
|
+
TTLPolicy.days(7),
|
|
889
|
+
];
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Create minimal invalidation policies (only glossary changes)
|
|
894
|
+
*/
|
|
895
|
+
export function createMinimalPolicies(): InvalidationPolicy[] {
|
|
896
|
+
return [
|
|
897
|
+
new GlossaryChangePolicy('matching'),
|
|
898
|
+
];
|
|
899
|
+
}
|