sequant 1.12.0 → 1.13.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 (53) hide show
  1. package/README.md +10 -8
  2. package/dist/bin/cli.js +19 -9
  3. package/dist/src/commands/doctor.js +42 -20
  4. package/dist/src/commands/init.js +152 -65
  5. package/dist/src/commands/logs.js +7 -6
  6. package/dist/src/commands/run.d.ts +13 -1
  7. package/dist/src/commands/run.js +122 -32
  8. package/dist/src/commands/stats.js +67 -48
  9. package/dist/src/commands/status.js +30 -12
  10. package/dist/src/commands/sync.d.ts +28 -0
  11. package/dist/src/commands/sync.js +102 -0
  12. package/dist/src/index.d.ts +6 -0
  13. package/dist/src/index.js +4 -0
  14. package/dist/src/lib/cli-ui.d.ts +196 -0
  15. package/dist/src/lib/cli-ui.js +544 -0
  16. package/dist/src/lib/content-analyzer.d.ts +89 -0
  17. package/dist/src/lib/content-analyzer.js +437 -0
  18. package/dist/src/lib/phase-signal.d.ts +94 -0
  19. package/dist/src/lib/phase-signal.js +171 -0
  20. package/dist/src/lib/phase-spinner.d.ts +146 -0
  21. package/dist/src/lib/phase-spinner.js +255 -0
  22. package/dist/src/lib/solve-comment-parser.d.ts +84 -0
  23. package/dist/src/lib/solve-comment-parser.js +200 -0
  24. package/dist/src/lib/stack-config.d.ts +51 -0
  25. package/dist/src/lib/stack-config.js +77 -0
  26. package/dist/src/lib/stacks.d.ts +52 -0
  27. package/dist/src/lib/stacks.js +173 -0
  28. package/dist/src/lib/templates.d.ts +2 -0
  29. package/dist/src/lib/templates.js +9 -2
  30. package/dist/src/lib/upstream/assessment.d.ts +70 -0
  31. package/dist/src/lib/upstream/assessment.js +385 -0
  32. package/dist/src/lib/upstream/index.d.ts +11 -0
  33. package/dist/src/lib/upstream/index.js +14 -0
  34. package/dist/src/lib/upstream/issues.d.ts +38 -0
  35. package/dist/src/lib/upstream/issues.js +267 -0
  36. package/dist/src/lib/upstream/relevance.d.ts +50 -0
  37. package/dist/src/lib/upstream/relevance.js +209 -0
  38. package/dist/src/lib/upstream/report.d.ts +29 -0
  39. package/dist/src/lib/upstream/report.js +391 -0
  40. package/dist/src/lib/upstream/types.d.ts +207 -0
  41. package/dist/src/lib/upstream/types.js +5 -0
  42. package/dist/src/lib/workflow/log-writer.d.ts +1 -1
  43. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  44. package/dist/src/lib/workflow/qa-cache.d.ts +199 -0
  45. package/dist/src/lib/workflow/qa-cache.js +440 -0
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +34 -6
  47. package/dist/src/lib/workflow/run-log-schema.js +12 -1
  48. package/dist/src/lib/workflow/state-schema.d.ts +4 -4
  49. package/dist/src/lib/workflow/types.d.ts +4 -0
  50. package/package.json +6 -1
  51. package/templates/skills/qa/scripts/quality-checks.sh +509 -53
  52. package/templates/skills/solve/SKILL.md +375 -83
  53. package/templates/skills/spec/SKILL.md +107 -5
@@ -0,0 +1,199 @@
1
+ /**
2
+ * QA Cache Module - Caches expensive QA check results to skip unchanged checks on re-run
3
+ *
4
+ * Provides caching for type safety, security, and test checks keyed by:
5
+ * - File content hash (git diff hash)
6
+ * - Check type
7
+ * - Configuration hash
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { QACache } from './qa-cache';
12
+ *
13
+ * const cache = new QACache();
14
+ *
15
+ * // Check cache before running expensive check
16
+ * const cached = await cache.get('security');
17
+ * if (cached && !cached.isStale) {
18
+ * console.log('Using cached security scan results');
19
+ * return cached.result;
20
+ * }
21
+ *
22
+ * // Run check and cache result
23
+ * const result = await runSecurityScan();
24
+ * await cache.set('security', result);
25
+ * ```
26
+ */
27
+ import { z } from "zod";
28
+ /**
29
+ * Check types that can be cached
30
+ */
31
+ export declare const CHECK_TYPES: readonly ["type-safety", "deleted-tests", "scope", "size", "security", "semgrep", "build", "tests"];
32
+ export type CheckType = (typeof CHECK_TYPES)[number];
33
+ /**
34
+ * Schema for a single cached check result
35
+ */
36
+ export declare const CachedCheckResultSchema: z.ZodObject<{
37
+ checkType: z.ZodEnum<{
38
+ build: "build";
39
+ security: "security";
40
+ semgrep: "semgrep";
41
+ "type-safety": "type-safety";
42
+ "deleted-tests": "deleted-tests";
43
+ scope: "scope";
44
+ size: "size";
45
+ tests: "tests";
46
+ }>;
47
+ diffHash: z.ZodString;
48
+ configHash: z.ZodString;
49
+ cachedAt: z.ZodString;
50
+ ttl: z.ZodNumber;
51
+ result: z.ZodObject<{
52
+ passed: z.ZodBoolean;
53
+ message: z.ZodString;
54
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
55
+ }, z.core.$strip>;
56
+ }, z.core.$strip>;
57
+ export type CachedCheckResult = z.infer<typeof CachedCheckResultSchema>;
58
+ /**
59
+ * Schema for the complete cache file
60
+ */
61
+ export declare const QACacheSchema: z.ZodObject<{
62
+ version: z.ZodLiteral<1>;
63
+ lastUpdated: z.ZodString;
64
+ checks: z.ZodRecord<z.ZodString, z.ZodObject<{
65
+ checkType: z.ZodEnum<{
66
+ build: "build";
67
+ security: "security";
68
+ semgrep: "semgrep";
69
+ "type-safety": "type-safety";
70
+ "deleted-tests": "deleted-tests";
71
+ scope: "scope";
72
+ size: "size";
73
+ tests: "tests";
74
+ }>;
75
+ diffHash: z.ZodString;
76
+ configHash: z.ZodString;
77
+ cachedAt: z.ZodString;
78
+ ttl: z.ZodNumber;
79
+ result: z.ZodObject<{
80
+ passed: z.ZodBoolean;
81
+ message: z.ZodString;
82
+ details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
83
+ }, z.core.$strip>;
84
+ }, z.core.$strip>>;
85
+ }, z.core.$strip>;
86
+ export type QACacheState = z.infer<typeof QACacheSchema>;
87
+ /**
88
+ * Cache configuration options
89
+ */
90
+ export interface QACacheOptions {
91
+ /** Path to cache directory (default: .sequant/.cache/qa/) */
92
+ cacheDir?: string;
93
+ /** Default TTL in milliseconds (default: 1 hour) */
94
+ defaultTtl?: number;
95
+ /** Enable verbose logging */
96
+ verbose?: boolean;
97
+ }
98
+ /**
99
+ * Result of cache lookup
100
+ */
101
+ export interface CacheLookupResult {
102
+ /** Whether cache hit occurred */
103
+ hit: boolean;
104
+ /** Whether cached result is stale (expired TTL) */
105
+ isStale: boolean;
106
+ /** The cached result (if hit) */
107
+ result?: CachedCheckResult["result"];
108
+ /** Reason for cache miss */
109
+ missReason?: "not-found" | "hash-mismatch" | "expired" | "corrupted";
110
+ }
111
+ /**
112
+ * QA Cache Manager
113
+ *
114
+ * Manages caching of expensive QA check results to improve re-run performance.
115
+ */
116
+ export declare class QACache {
117
+ private cacheDir;
118
+ private defaultTtl;
119
+ private verbose;
120
+ private cachedState;
121
+ constructor(options?: QACacheOptions);
122
+ /**
123
+ * Get the cache file path
124
+ */
125
+ getCachePath(): string;
126
+ /**
127
+ * Compute hash of git diff between main and HEAD
128
+ *
129
+ * This provides a content-based cache key that changes when code changes.
130
+ */
131
+ computeDiffHash(): string;
132
+ /**
133
+ * Compute hash of configuration files that affect checks
134
+ */
135
+ computeConfigHash(checkType: CheckType): string;
136
+ /**
137
+ * Get configuration files that affect a specific check type
138
+ */
139
+ private getConfigFilesForCheck;
140
+ /**
141
+ * Check if global invalidation files have changed
142
+ */
143
+ checkGlobalInvalidation(): Promise<boolean>;
144
+ /**
145
+ * Check if files matching check-specific patterns have changed
146
+ */
147
+ checkTypeSpecificInvalidation(checkType: CheckType): Promise<boolean>;
148
+ /**
149
+ * Read the current cache state
150
+ */
151
+ getState(): Promise<QACacheState>;
152
+ /**
153
+ * Write cache state to disk
154
+ */
155
+ saveState(state: QACacheState): Promise<void>;
156
+ /**
157
+ * Get cached result for a check type
158
+ */
159
+ get(checkType: CheckType): Promise<CacheLookupResult>;
160
+ /**
161
+ * Cache a check result
162
+ */
163
+ set(checkType: CheckType, result: CachedCheckResult["result"], ttl?: number): Promise<void>;
164
+ /**
165
+ * Clear cache for a specific check type
166
+ */
167
+ clear(checkType: CheckType): Promise<void>;
168
+ /**
169
+ * Clear all cached results
170
+ */
171
+ clearAll(): Promise<void>;
172
+ /**
173
+ * Get cache status for all check types
174
+ */
175
+ getStatus(): Promise<Record<CheckType, {
176
+ hit: boolean;
177
+ missReason?: string;
178
+ }>>;
179
+ /**
180
+ * Create an empty cache state
181
+ */
182
+ private createEmptyState;
183
+ /**
184
+ * Log a message if verbose mode is enabled
185
+ */
186
+ private log;
187
+ /**
188
+ * Clear the in-memory cache (forces re-read on next access)
189
+ */
190
+ clearMemoryCache(): void;
191
+ }
192
+ /**
193
+ * Get the default QA cache instance
194
+ */
195
+ export declare function getQACache(options?: QACacheOptions): QACache;
196
+ /**
197
+ * Reset the default cache instance (for testing)
198
+ */
199
+ export declare function resetQACache(): void;
@@ -0,0 +1,440 @@
1
+ /**
2
+ * QA Cache Module - Caches expensive QA check results to skip unchanged checks on re-run
3
+ *
4
+ * Provides caching for type safety, security, and test checks keyed by:
5
+ * - File content hash (git diff hash)
6
+ * - Check type
7
+ * - Configuration hash
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { QACache } from './qa-cache';
12
+ *
13
+ * const cache = new QACache();
14
+ *
15
+ * // Check cache before running expensive check
16
+ * const cached = await cache.get('security');
17
+ * if (cached && !cached.isStale) {
18
+ * console.log('Using cached security scan results');
19
+ * return cached.result;
20
+ * }
21
+ *
22
+ * // Run check and cache result
23
+ * const result = await runSecurityScan();
24
+ * await cache.set('security', result);
25
+ * ```
26
+ */
27
+ import * as fs from "fs";
28
+ import * as path from "path";
29
+ import * as crypto from "crypto";
30
+ import { execSync } from "child_process";
31
+ import { z } from "zod";
32
+ /**
33
+ * Check types that can be cached
34
+ */
35
+ export const CHECK_TYPES = [
36
+ "type-safety",
37
+ "deleted-tests",
38
+ "scope",
39
+ "size",
40
+ "security",
41
+ "semgrep",
42
+ "build",
43
+ "tests",
44
+ ];
45
+ /**
46
+ * Schema for a single cached check result
47
+ */
48
+ export const CachedCheckResultSchema = z.object({
49
+ /** Type of check */
50
+ checkType: z.enum(CHECK_TYPES),
51
+ /** Git diff hash when check was run */
52
+ diffHash: z.string(),
53
+ /** Config hash when check was run */
54
+ configHash: z.string(),
55
+ /** Timestamp when cached */
56
+ cachedAt: z.string().datetime(),
57
+ /** Time-to-live in milliseconds */
58
+ ttl: z.number().positive(),
59
+ /** The actual check result */
60
+ result: z.object({
61
+ /** Whether the check passed */
62
+ passed: z.boolean(),
63
+ /** Summary message */
64
+ message: z.string(),
65
+ /** Optional details (counts, warnings, etc.) */
66
+ details: z.record(z.string(), z.unknown()).optional(),
67
+ }),
68
+ });
69
+ /**
70
+ * Schema for the complete cache file
71
+ */
72
+ export const QACacheSchema = z.object({
73
+ /** Cache version for backwards compatibility */
74
+ version: z.literal(1),
75
+ /** When the cache was last updated */
76
+ lastUpdated: z.string().datetime(),
77
+ /** Cached check results keyed by check type */
78
+ checks: z.record(z.string(), CachedCheckResultSchema),
79
+ });
80
+ /**
81
+ * Default TTL: 1 hour
82
+ */
83
+ const DEFAULT_TTL = 60 * 60 * 1000;
84
+ /**
85
+ * Default cache directory
86
+ */
87
+ const DEFAULT_CACHE_DIR = ".sequant/.cache/qa";
88
+ /**
89
+ * Files that invalidate all caches when changed
90
+ */
91
+ const GLOBAL_INVALIDATION_FILES = [
92
+ "package-lock.json",
93
+ "package.json",
94
+ "tsconfig.json",
95
+ ".sequant/settings.json",
96
+ ];
97
+ /**
98
+ * Per-check invalidation file patterns
99
+ */
100
+ const CHECK_INVALIDATION_PATTERNS = {
101
+ "type-safety": [/tsconfig.*\.json$/, /\.ts$/, /\.tsx$/],
102
+ "deleted-tests": [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/],
103
+ scope: [], // Uses git diff hash
104
+ size: [], // Uses git diff hash
105
+ security: [/\.ts$/, /\.tsx$/, /\.js$/, /\.jsx$/],
106
+ semgrep: [/\.ts$/, /\.tsx$/, /\.js$/, /\.jsx$/, /semgrep.*\.ya?ml$/],
107
+ build: [/\.ts$/, /\.tsx$/, /\.js$/, /\.jsx$/, /tsconfig.*\.json$/],
108
+ tests: [/\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /jest\.config/],
109
+ };
110
+ /**
111
+ * QA Cache Manager
112
+ *
113
+ * Manages caching of expensive QA check results to improve re-run performance.
114
+ */
115
+ export class QACache {
116
+ cacheDir;
117
+ defaultTtl;
118
+ verbose;
119
+ cachedState = null;
120
+ constructor(options = {}) {
121
+ this.cacheDir = options.cacheDir ?? DEFAULT_CACHE_DIR;
122
+ this.defaultTtl = options.defaultTtl ?? DEFAULT_TTL;
123
+ this.verbose = options.verbose ?? false;
124
+ }
125
+ /**
126
+ * Get the cache file path
127
+ */
128
+ getCachePath() {
129
+ return path.join(this.cacheDir, "cache.json");
130
+ }
131
+ /**
132
+ * Compute hash of git diff between main and HEAD
133
+ *
134
+ * This provides a content-based cache key that changes when code changes.
135
+ */
136
+ computeDiffHash() {
137
+ try {
138
+ // Get the diff content between main and HEAD
139
+ const diff = execSync("git diff main...HEAD", {
140
+ encoding: "utf-8",
141
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
142
+ });
143
+ // Hash the diff content
144
+ return crypto
145
+ .createHash("sha256")
146
+ .update(diff)
147
+ .digest("hex")
148
+ .slice(0, 16);
149
+ }
150
+ catch {
151
+ // If git command fails, return a unique hash to force fresh run
152
+ this.log("Failed to compute diff hash, using timestamp fallback");
153
+ return crypto
154
+ .createHash("sha256")
155
+ .update(Date.now().toString())
156
+ .digest("hex")
157
+ .slice(0, 16);
158
+ }
159
+ }
160
+ /**
161
+ * Compute hash of configuration files that affect checks
162
+ */
163
+ computeConfigHash(checkType) {
164
+ const files = this.getConfigFilesForCheck(checkType);
165
+ const contents = [];
166
+ for (const file of files) {
167
+ if (fs.existsSync(file)) {
168
+ try {
169
+ contents.push(fs.readFileSync(file, "utf-8"));
170
+ }
171
+ catch {
172
+ // Ignore read errors
173
+ }
174
+ }
175
+ }
176
+ return crypto
177
+ .createHash("sha256")
178
+ .update(contents.join("\n"))
179
+ .digest("hex")
180
+ .slice(0, 16);
181
+ }
182
+ /**
183
+ * Get configuration files that affect a specific check type
184
+ */
185
+ getConfigFilesForCheck(checkType) {
186
+ const baseFiles = ["tsconfig.json", "package.json"];
187
+ switch (checkType) {
188
+ case "type-safety":
189
+ case "build":
190
+ return [...baseFiles, "tsconfig.build.json"];
191
+ case "semgrep":
192
+ return [
193
+ ...baseFiles,
194
+ ".semgrep.yml",
195
+ ".semgrepignore",
196
+ "semgrep.config.yaml",
197
+ ];
198
+ case "tests":
199
+ return [
200
+ ...baseFiles,
201
+ "jest.config.js",
202
+ "jest.config.ts",
203
+ "vitest.config.ts",
204
+ ];
205
+ case "security":
206
+ return baseFiles;
207
+ default:
208
+ return baseFiles;
209
+ }
210
+ }
211
+ /**
212
+ * Check if global invalidation files have changed
213
+ */
214
+ async checkGlobalInvalidation() {
215
+ try {
216
+ const changedFiles = execSync("git diff main...HEAD --name-only", {
217
+ encoding: "utf-8",
218
+ })
219
+ .trim()
220
+ .split("\n")
221
+ .filter(Boolean);
222
+ for (const file of changedFiles) {
223
+ if (GLOBAL_INVALIDATION_FILES.includes(file)) {
224
+ this.log(`Global invalidation triggered by: ${file}`);
225
+ return true;
226
+ }
227
+ }
228
+ return false;
229
+ }
230
+ catch {
231
+ return false;
232
+ }
233
+ }
234
+ /**
235
+ * Check if files matching check-specific patterns have changed
236
+ */
237
+ async checkTypeSpecificInvalidation(checkType) {
238
+ const patterns = CHECK_INVALIDATION_PATTERNS[checkType];
239
+ if (patterns.length === 0) {
240
+ return false;
241
+ }
242
+ try {
243
+ const changedFiles = execSync("git diff main...HEAD --name-only", {
244
+ encoding: "utf-8",
245
+ })
246
+ .trim()
247
+ .split("\n")
248
+ .filter(Boolean);
249
+ for (const file of changedFiles) {
250
+ for (const pattern of patterns) {
251
+ if (pattern.test(file)) {
252
+ this.log(`Check ${checkType} invalidated by: ${file}`);
253
+ return true;
254
+ }
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ catch {
260
+ return false;
261
+ }
262
+ }
263
+ /**
264
+ * Read the current cache state
265
+ */
266
+ async getState() {
267
+ if (this.cachedState) {
268
+ return this.cachedState;
269
+ }
270
+ const cachePath = this.getCachePath();
271
+ if (!fs.existsSync(cachePath)) {
272
+ const emptyState = this.createEmptyState();
273
+ this.cachedState = emptyState;
274
+ return emptyState;
275
+ }
276
+ try {
277
+ const content = fs.readFileSync(cachePath, "utf-8");
278
+ const parsed = JSON.parse(content);
279
+ const state = QACacheSchema.parse(parsed);
280
+ this.cachedState = state;
281
+ return state;
282
+ }
283
+ catch {
284
+ // Graceful degradation: corrupted cache -> fresh state (AC-6)
285
+ this.log("Cache corrupted or invalid, creating fresh cache");
286
+ const emptyState = this.createEmptyState();
287
+ this.cachedState = emptyState;
288
+ return emptyState;
289
+ }
290
+ }
291
+ /**
292
+ * Write cache state to disk
293
+ */
294
+ async saveState(state) {
295
+ QACacheSchema.parse(state);
296
+ state.lastUpdated = new Date().toISOString();
297
+ // Ensure cache directory exists
298
+ if (!fs.existsSync(this.cacheDir)) {
299
+ fs.mkdirSync(this.cacheDir, { recursive: true });
300
+ }
301
+ const cachePath = this.getCachePath();
302
+ // Write atomically using temp file
303
+ const tempPath = `${cachePath}.tmp.${process.pid}`;
304
+ try {
305
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2));
306
+ fs.renameSync(tempPath, cachePath);
307
+ this.cachedState = state;
308
+ this.log(`Cache saved: ${cachePath}`);
309
+ }
310
+ catch (error) {
311
+ // Clean up temp file on error
312
+ if (fs.existsSync(tempPath)) {
313
+ fs.unlinkSync(tempPath);
314
+ }
315
+ throw error;
316
+ }
317
+ }
318
+ /**
319
+ * Get cached result for a check type
320
+ */
321
+ async get(checkType) {
322
+ const state = await this.getState();
323
+ const cached = state.checks[checkType];
324
+ if (!cached) {
325
+ return { hit: false, isStale: false, missReason: "not-found" };
326
+ }
327
+ // Check if TTL has expired
328
+ const cachedTime = new Date(cached.cachedAt).getTime();
329
+ const now = Date.now();
330
+ if (now - cachedTime > cached.ttl) {
331
+ return { hit: false, isStale: true, missReason: "expired" };
332
+ }
333
+ // Check if diff hash matches
334
+ const currentDiffHash = this.computeDiffHash();
335
+ if (cached.diffHash !== currentDiffHash) {
336
+ return { hit: false, isStale: false, missReason: "hash-mismatch" };
337
+ }
338
+ // Check if config hash matches
339
+ const currentConfigHash = this.computeConfigHash(checkType);
340
+ if (cached.configHash !== currentConfigHash) {
341
+ return { hit: false, isStale: false, missReason: "hash-mismatch" };
342
+ }
343
+ return {
344
+ hit: true,
345
+ isStale: false,
346
+ result: cached.result,
347
+ };
348
+ }
349
+ /**
350
+ * Cache a check result
351
+ */
352
+ async set(checkType, result, ttl) {
353
+ const state = await this.getState();
354
+ const cachedResult = {
355
+ checkType,
356
+ diffHash: this.computeDiffHash(),
357
+ configHash: this.computeConfigHash(checkType),
358
+ cachedAt: new Date().toISOString(),
359
+ ttl: ttl ?? this.defaultTtl,
360
+ result,
361
+ };
362
+ state.checks[checkType] = cachedResult;
363
+ await this.saveState(state);
364
+ this.log(`Cached ${checkType} result`);
365
+ }
366
+ /**
367
+ * Clear cache for a specific check type
368
+ */
369
+ async clear(checkType) {
370
+ const state = await this.getState();
371
+ delete state.checks[checkType];
372
+ await this.saveState(state);
373
+ this.log(`Cleared cache for ${checkType}`);
374
+ }
375
+ /**
376
+ * Clear all cached results
377
+ */
378
+ async clearAll() {
379
+ const emptyState = this.createEmptyState();
380
+ await this.saveState(emptyState);
381
+ this.log("Cleared all cache");
382
+ }
383
+ /**
384
+ * Get cache status for all check types
385
+ */
386
+ async getStatus() {
387
+ const status = {};
388
+ for (const checkType of CHECK_TYPES) {
389
+ const result = await this.get(checkType);
390
+ status[checkType] = {
391
+ hit: result.hit,
392
+ missReason: result.missReason,
393
+ };
394
+ }
395
+ return status;
396
+ }
397
+ /**
398
+ * Create an empty cache state
399
+ */
400
+ createEmptyState() {
401
+ return {
402
+ version: 1,
403
+ lastUpdated: new Date().toISOString(),
404
+ checks: {},
405
+ };
406
+ }
407
+ /**
408
+ * Log a message if verbose mode is enabled
409
+ */
410
+ log(message) {
411
+ if (this.verbose) {
412
+ console.log(`[qa-cache] ${message}`);
413
+ }
414
+ }
415
+ /**
416
+ * Clear the in-memory cache (forces re-read on next access)
417
+ */
418
+ clearMemoryCache() {
419
+ this.cachedState = null;
420
+ }
421
+ }
422
+ /**
423
+ * Default cache instance
424
+ */
425
+ let defaultCache = null;
426
+ /**
427
+ * Get the default QA cache instance
428
+ */
429
+ export function getQACache(options) {
430
+ if (!defaultCache || options) {
431
+ defaultCache = new QACache(options);
432
+ }
433
+ return defaultCache;
434
+ }
435
+ /**
436
+ * Reset the default cache instance (for testing)
437
+ */
438
+ export function resetQACache() {
439
+ defaultCache = null;
440
+ }