@ngockhoale/ukit 1.4.0 → 1.4.2

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 (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/src/bug/triageBug.js +1 -33
  4. package/src/cli/commands/install.js +5 -10
  5. package/src/context/detectProjectContext.js +3 -24
  6. package/src/core/compact/index.js +19 -27
  7. package/src/core/ensureGitignore.js +1 -1
  8. package/src/core/fileOps.js +41 -2
  9. package/src/core/memory/hygiene.js +17 -1
  10. package/src/core/memory/store.js +14 -36
  11. package/src/core/metadata.js +5 -5
  12. package/src/core/output/index.js +20 -20
  13. package/src/core/packageManager.js +51 -0
  14. package/src/core/router/router.js +22 -6
  15. package/src/core/runInstallPipeline.js +1 -36
  16. package/src/core/runtimeConfig.js +71 -3
  17. package/src/core/token/index.js +21 -1
  18. package/src/core/uninstall.js +15 -38
  19. package/src/index/buildIndex.js +217 -49
  20. package/src/index/gitHooks.js +32 -7
  21. package/src/index/impactContext.js +16 -6
  22. package/src/index/importResolution.js +105 -28
  23. package/src/index/paths.js +29 -0
  24. package/src/index/queryIndex.js +20 -35
  25. package/src/index/relatedTests.js +15 -2
  26. package/src/index/routeCatalog.js +1 -1
  27. package/src/index/taskRouting.js +438 -18
  28. package/src/index/verificationPlan.js +2 -36
  29. package/templates/.claude/hooks/reinject-context.sh +2 -0
  30. package/templates/.claude/hooks/session-start.md +2 -0
  31. package/templates/.claude/hooks/skill-router.sh +657 -15
  32. package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
  33. package/templates/.claude/ukit/index/route-task.mjs +475 -5
  34. package/templates/.claude/ukit/runtime/reinject-context.mjs +120 -3
  35. package/templates/.codex/README.md +8 -1
  36. package/templates/.codex/settings.json +53 -0
  37. package/templates/AGENTS.md +3 -0
  38. package/templates/CLAUDE.md +5 -1
  39. package/templates/ukit/README.md +1 -1
  40. package/templates/ukit/storage/config.json +61 -2
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
+ import { escapeRegExp } from '../core/fileOps.js';
5
+
4
6
  const HOOK_NAMES = ['post-commit', 'post-merge', 'post-checkout'];
5
7
  const BEGIN = '# UKit index auto-refresh (begin)';
6
8
  const END = '# UKit index auto-refresh (end)';
@@ -17,12 +19,13 @@ export async function installIndexRefreshHooks({ projectRoot }) {
17
19
  const existing = await readTextOrEmpty(hookPath);
18
20
  const base = existing || '#!/bin/sh\n';
19
21
 
20
- if (hasManagedBlock(base)) {
22
+ if (hasCompleteManagedBlock(base)) {
21
23
  unchanged += 1;
22
24
  continue;
23
25
  }
24
26
 
25
- const next = `${base.trimEnd()}\n\n${buildManagedBlock()}\n`;
27
+ const cleanedBase = removeManagedBlockFragments(base);
28
+ const next = `${cleanedBase.trimEnd()}\n\n${buildManagedBlock()}\n`;
26
29
  await fs.writeFile(hookPath, next, { mode: 0o755 });
27
30
  await fs.chmod(hookPath, 0o755);
28
31
  installed += 1;
@@ -45,7 +48,7 @@ export async function removeIndexRefreshHooks({ projectRoot }) {
45
48
  for (const hookName of HOOK_NAMES) {
46
49
  const hookPath = path.join(hooksDir, hookName);
47
50
  const existing = await readTextOrEmpty(hookPath);
48
- if (!existing || !hasManagedBlock(existing)) {
51
+ if (!existing || !hasCompleteManagedBlock(existing)) {
49
52
  unchanged += 1;
50
53
  continue;
51
54
  }
@@ -75,8 +78,10 @@ function buildManagedBlock() {
75
78
  ].join('\n');
76
79
  }
77
80
 
78
- function hasManagedBlock(content) {
79
- return content.includes(BEGIN) && content.includes(END);
81
+ function hasCompleteManagedBlock(content) {
82
+ const beginIndex = content.indexOf(BEGIN);
83
+ const endIndex = content.indexOf(END, beginIndex + BEGIN.length);
84
+ return beginIndex >= 0 && endIndex > beginIndex;
80
85
  }
81
86
 
82
87
  function removeManagedBlock(content) {
@@ -84,8 +89,28 @@ function removeManagedBlock(content) {
84
89
  return content.replace(pattern, '').replace(/\n{3,}/g, '\n\n');
85
90
  }
86
91
 
87
- function escapeRegExp(input) {
88
- return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ function removeManagedBlockFragments(content) {
93
+ const lines = content.split('\n');
94
+ const kept = [];
95
+ let skipping = false;
96
+
97
+ for (const line of lines) {
98
+ if (line === BEGIN) {
99
+ skipping = true;
100
+ continue;
101
+ }
102
+
103
+ if (line === END) {
104
+ skipping = false;
105
+ continue;
106
+ }
107
+
108
+ if (!skipping) {
109
+ kept.push(line);
110
+ }
111
+ }
112
+
113
+ return kept.join('\n').replace(/\n{3,}/g, '\n\n');
89
114
  }
90
115
 
91
116
  async function ensureHooksDir(hooksDir) {
@@ -38,6 +38,17 @@ function unique(values = []) {
38
38
  return [...new Set(values.filter(Boolean))];
39
39
  }
40
40
 
41
+ function dedupeCallers(callers = []) {
42
+ const byKey = new Map();
43
+ for (const caller of callers) {
44
+ const key = `${caller.filePath}\0${caller.symbol}`;
45
+ if (!byKey.has(key)) {
46
+ byKey.set(key, caller);
47
+ }
48
+ }
49
+ return [...byKey.values()];
50
+ }
51
+
41
52
  function resolveImportTarget(fromFilePath, specifier) {
42
53
  const from = normalizePath(fromFilePath);
43
54
  const to = String(specifier ?? '').trim();
@@ -119,10 +130,7 @@ export async function resolveImpactContext({
119
130
  continue;
120
131
  }
121
132
 
122
- if (
123
- changedFileSet.has(normalizePath(record.filePath))
124
- && changedSymbolSet.has(record.symbol)
125
- ) {
133
+ if (changedFileSet.has(normalizePath(record.filePath))) {
126
134
  continue;
127
135
  }
128
136
 
@@ -161,13 +169,15 @@ export async function resolveImpactContext({
161
169
  ...risk.riskLabels.flatMap((label) => RISK_TEST_RECOMMENDATIONS[label] ?? []),
162
170
  ]);
163
171
 
172
+ const dedupedCallers = dedupeCallers(callers).slice(0, budget.maxCallers);
173
+
164
174
  return {
165
175
  changedFiles: normalizedChangedFiles,
166
176
  changedSymbols: normalizedChangedSymbols,
167
177
  riskLabels: risk.riskLabels,
168
178
  requiresImpactCheck: risk.requiresImpactCheck,
169
179
  callees: unique(callees.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallees),
170
- callers: unique(callers.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallers),
180
+ callers: dedupedCallers,
171
181
  importers: unique(importers).slice(0, budget.maxImporters),
172
182
  dependencies: unique(dependencies).slice(0, budget.maxImporters),
173
183
  mirrorCounterparts,
@@ -175,7 +185,7 @@ export async function resolveImpactContext({
175
185
  recommendedTestFiles,
176
186
  explanations: {
177
187
  callees: changedRecords.map((record) => `${record.filePath}:${record.symbol}`),
178
- callers: callers.map((entry) => `${entry.filePath}:${entry.symbol}`),
188
+ callers: dedupedCallers.map((entry) => `${entry.filePath}:${entry.symbol}`),
179
189
  importers,
180
190
  relatedTests,
181
191
  mirrorCounterparts: mirrorCounterparts.map((entry) => `${entry.source} -> ${entry.filePath}`),
@@ -27,6 +27,17 @@ const DEFAULT_ALIAS_RULES = [
27
27
  ];
28
28
  const ALIAS_CONTEXT_CACHE = new Map();
29
29
  const ROOT_ALIAS_CONFIG_FILES = ['tsconfig.json', 'jsconfig.json'];
30
+ const MAX_CACHE_ENTRIES = 100;
31
+
32
+ function setBoundedCacheEntry(map, key, value, maxEntries = MAX_CACHE_ENTRIES) {
33
+ if (map.has(key)) {
34
+ map.delete(key);
35
+ }
36
+ map.set(key, value);
37
+ while (map.size > maxEntries) {
38
+ map.delete(map.keys().next().value);
39
+ }
40
+ }
30
41
 
31
42
  export async function loadImportAliasContext({ rootDir }) {
32
43
  const loaded = await loadImportAliasContextState({ rootDir });
@@ -49,7 +60,7 @@ export async function loadImportAliasContextState({ rootDir }) {
49
60
  ALIAS_CONTEXT_CACHE.delete(absoluteRoot);
50
61
  throw error;
51
62
  });
52
- ALIAS_CONTEXT_CACHE.set(absoluteRoot, contextPromise);
63
+ setBoundedCacheEntry(ALIAS_CONTEXT_CACHE, absoluteRoot, contextPromise);
53
64
 
54
65
  return contextPromise;
55
66
  }
@@ -225,9 +236,9 @@ async function loadAliasConfig(configPath, visited = new Set(), tracker = null)
225
236
 
226
237
  let inherited = null;
227
238
  const extendsValue = typeof parsed?.extends === 'string' ? parsed.extends.trim() : '';
228
- if (extendsValue && (extendsValue.startsWith('.') || extendsValue.startsWith('/'))) {
229
- const inheritedPath = resolveExtendedConfigPath(configDir, extendsValue);
230
- inherited = await loadAliasConfig(inheritedPath, visited, tracker);
239
+ if (extendsValue) {
240
+ const inheritedPath = await resolveExtendedConfigPath(configDir, extendsValue);
241
+ inherited = inheritedPath ? await loadAliasConfig(inheritedPath, visited, tracker) : null;
231
242
  }
232
243
 
233
244
  const inheritedRules = inherited?.pathRules ?? [];
@@ -279,11 +290,48 @@ function buildBaseUrlDirs({ rootDir, compilerOptions }) {
279
290
  return [path.resolve(rootDir, baseUrl)];
280
291
  }
281
292
 
282
- function resolveExtendedConfigPath(configDir, extendsValue) {
293
+ async function resolveExtendedConfigPath(configDir, extendsValue) {
283
294
  const withExtension = extendsValue.endsWith('.json')
284
295
  ? extendsValue
285
296
  : `${extendsValue}.json`;
286
- return path.resolve(configDir, withExtension);
297
+ if (withExtension.startsWith('.') || withExtension.startsWith('/')) {
298
+ return path.resolve(configDir, withExtension);
299
+ }
300
+
301
+ const parts = withExtension.split('/').filter(Boolean);
302
+ if (parts.length === 0) {
303
+ return null;
304
+ }
305
+
306
+ const packageName = withExtension.startsWith('@')
307
+ ? parts.slice(0, 2).join('/')
308
+ : parts[0];
309
+ const packageRest = parts.slice(packageName.startsWith('@') ? 2 : 1).join('/');
310
+ const packageRoot = await findNodeModulePackageRoot(configDir, packageName);
311
+ if (!packageRoot) {
312
+ return null;
313
+ }
314
+
315
+ return path.join(packageRoot, packageRest || 'tsconfig.json');
316
+ }
317
+
318
+ async function findNodeModulePackageRoot(startDir, packageName) {
319
+ let currentDir = path.resolve(startDir);
320
+ while (true) {
321
+ const candidate = path.join(currentDir, 'node_modules', packageName);
322
+ try {
323
+ const stat = await fs.stat(candidate);
324
+ if (stat.isDirectory()) {
325
+ return candidate;
326
+ }
327
+ } catch {}
328
+
329
+ const parentDir = path.dirname(currentDir);
330
+ if (parentDir === currentDir) {
331
+ return null;
332
+ }
333
+ currentDir = parentDir;
334
+ }
287
335
  }
288
336
 
289
337
  async function isAliasSnapshotValid(snapshot = []) {
@@ -319,18 +367,16 @@ function isSameSnapshotEntry(current, previous) {
319
367
 
320
368
  function parseJsonc(raw) {
321
369
  const withoutBom = raw.replace(/^\uFEFF/, '');
322
- const withoutBlockComments = withoutBom.replace(/\/\*[\s\S]*?\*\//g, '');
323
- const withoutLineComments = stripLineComments(withoutBlockComments);
324
- const withoutTrailingCommas = withoutLineComments.replace(/,\s*([}\]])/g, '$1');
370
+ const withoutComments = stripJsoncComments(withoutBom);
371
+ const withoutTrailingCommas = stripJsoncTrailingCommas(withoutComments);
325
372
 
326
373
  return JSON.parse(withoutTrailingCommas);
327
374
  }
328
375
 
329
- function stripLineComments(raw) {
376
+ function stripJsoncComments(raw) {
330
377
  let result = '';
331
378
  let inString = false;
332
- let stringQuote = '';
333
- let isEscaped = false;
379
+ let escaped = false;
334
380
 
335
381
  for (let index = 0; index < raw.length; index += 1) {
336
382
  const char = raw[index];
@@ -338,31 +384,28 @@ function stripLineComments(raw) {
338
384
 
339
385
  if (inString) {
340
386
  result += char;
341
- if (isEscaped) {
342
- isEscaped = false;
343
- } else if (char === '\\') {
344
- isEscaped = true;
345
- } else if (char === stringQuote) {
346
- inString = false;
347
- stringQuote = '';
348
- }
387
+ if (escaped) escaped = false;
388
+ else if (char === '\\') escaped = true;
389
+ else if (char === '"') inString = false;
349
390
  continue;
350
391
  }
351
392
 
352
- if (char === '"' || char === '\'') {
393
+ if (char === '"') {
353
394
  inString = true;
354
- stringQuote = char;
355
395
  result += char;
356
396
  continue;
357
397
  }
358
398
 
359
399
  if (char === '/' && nextChar === '/') {
360
- while (index < raw.length && raw[index] !== '\n') {
361
- index += 1;
362
- }
363
- if (index < raw.length) {
364
- result += '\n';
365
- }
400
+ while (index < raw.length && raw[index] !== '\n') index += 1;
401
+ if (index < raw.length) result += '\n';
402
+ continue;
403
+ }
404
+
405
+ if (char === '/' && nextChar === '*') {
406
+ index += 2;
407
+ while (index < raw.length && !(raw[index] === '*' && raw[index + 1] === '/')) index += 1;
408
+ index += 1;
366
409
  continue;
367
410
  }
368
411
 
@@ -372,6 +415,40 @@ function stripLineComments(raw) {
372
415
  return result;
373
416
  }
374
417
 
418
+ function stripJsoncTrailingCommas(raw) {
419
+ let result = '';
420
+ let inString = false;
421
+ let escaped = false;
422
+
423
+ for (let index = 0; index < raw.length; index += 1) {
424
+ const char = raw[index];
425
+
426
+ if (inString) {
427
+ result += char;
428
+ if (escaped) escaped = false;
429
+ else if (char === '\\') escaped = true;
430
+ else if (char === '"') inString = false;
431
+ continue;
432
+ }
433
+
434
+ if (char === '"') {
435
+ inString = true;
436
+ result += char;
437
+ continue;
438
+ }
439
+
440
+ if (char === ',') {
441
+ let nextIndex = index + 1;
442
+ while (/\s/.test(raw[nextIndex] ?? '')) nextIndex += 1;
443
+ if (raw[nextIndex] === '}' || raw[nextIndex] === ']') continue;
444
+ }
445
+
446
+ result += char;
447
+ }
448
+
449
+ return result;
450
+ }
451
+
375
452
  function dedupe(values) {
376
453
  return [...new Set(values)];
377
454
  }
@@ -26,3 +26,32 @@ export function getArtifactPath(rootDir, artifactName) {
26
26
  export function normalizeRelative(rootDir, absolutePath) {
27
27
  return path.relative(rootDir, absolutePath).replace(/\\/g, '/');
28
28
  }
29
+
30
+ export function isLikelyTestFilePath(filePath) {
31
+ const lower = filePath.toLowerCase();
32
+ return (
33
+ lower.startsWith('__tests__/')
34
+ || lower.startsWith('test/')
35
+ || lower.startsWith('tests/')
36
+ || lower.startsWith('spec/')
37
+ || lower.startsWith('specs/')
38
+ || lower.includes('/__tests__/')
39
+ || lower.includes('/test/')
40
+ || lower.includes('/spec/')
41
+ || lower.includes('/specs/')
42
+ || lower.endsWith('.test.js')
43
+ || lower.endsWith('.test.ts')
44
+ || lower.endsWith('.test.vue')
45
+ || lower.endsWith('.test.tsx')
46
+ || lower.endsWith('.test.jsx')
47
+ || lower.endsWith('.test.mjs')
48
+ || lower.endsWith('.test.cjs')
49
+ || lower.endsWith('.spec.js')
50
+ || lower.endsWith('.spec.ts')
51
+ || lower.endsWith('.spec.vue')
52
+ || lower.endsWith('.spec.tsx')
53
+ || lower.endsWith('.spec.jsx')
54
+ || lower.endsWith('.spec.mjs')
55
+ || lower.endsWith('.spec.cjs')
56
+ );
57
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
- import { getArtifactPath, getIndexDir, INDEX_ARTIFACTS } from './paths.js';
4
+ import { getArtifactPath, getIndexDir, INDEX_ARTIFACTS, isLikelyTestFilePath } from './paths.js';
5
5
  import { importsNeedAliasContext, loadImportAliasContext, resolveImportSpecifier } from './importResolution.js';
6
6
  import { buildSearchDescriptor } from './languageTools.js';
7
7
 
@@ -10,8 +10,19 @@ const QUERY_SEARCH_BUNDLE_CACHE = new Map();
10
10
  const QUERY_SUPPORT_BUNDLE_CACHE = new Map();
11
11
  const QUERY_RESULT_CACHE = new Map();
12
12
  const ANALOG_RESULT_CACHE = new Map();
13
+ const MAX_CACHE_ENTRIES = 100;
13
14
  const MAX_IMPORTER_HOPS = 2;
14
15
 
16
+ function setBoundedCacheEntry(map, key, value, maxEntries = MAX_CACHE_ENTRIES) {
17
+ if (map.has(key)) {
18
+ map.delete(key);
19
+ }
20
+ map.set(key, value);
21
+ while (map.size > maxEntries) {
22
+ map.delete(map.keys().next().value);
23
+ }
24
+ }
25
+
15
26
  export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5 } = {}) {
16
27
  if (!query || !query.trim()) {
17
28
  return [];
@@ -115,7 +126,7 @@ export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5
115
126
  throw error;
116
127
  });
117
128
 
118
- QUERY_RESULT_CACHE.set(queryCacheKey, queryPromise);
129
+ setBoundedCacheEntry(QUERY_RESULT_CACHE, queryCacheKey, queryPromise);
119
130
  return queryPromise;
120
131
  }
121
132
 
@@ -123,7 +134,8 @@ async function readArtifact(rootDir, artifactName) {
123
134
  const artifactPath = getArtifactPath(path.resolve(rootDir), artifactName);
124
135
 
125
136
  if (!ARTIFACT_CACHE.has(artifactPath)) {
126
- ARTIFACT_CACHE.set(
137
+ setBoundedCacheEntry(
138
+ ARTIFACT_CACHE,
127
139
  artifactPath,
128
140
  fs.readFile(artifactPath, 'utf8')
129
141
  .then((content) => {
@@ -148,7 +160,8 @@ async function loadQuerySearchBundle(rootDir) {
148
160
  const absoluteRoot = path.resolve(rootDir);
149
161
 
150
162
  if (!QUERY_SEARCH_BUNDLE_CACHE.has(absoluteRoot)) {
151
- QUERY_SEARCH_BUNDLE_CACHE.set(
163
+ setBoundedCacheEntry(
164
+ QUERY_SEARCH_BUNDLE_CACHE,
152
165
  absoluteRoot,
153
166
  Promise.all([
154
167
  readArtifact(absoluteRoot, INDEX_ARTIFACTS.files),
@@ -177,7 +190,8 @@ async function loadQuerySupportBundle(rootDir) {
177
190
  const absoluteRoot = path.resolve(rootDir);
178
191
 
179
192
  if (!QUERY_SUPPORT_BUNDLE_CACHE.has(absoluteRoot)) {
180
- QUERY_SUPPORT_BUNDLE_CACHE.set(
193
+ setBoundedCacheEntry(
194
+ QUERY_SUPPORT_BUNDLE_CACHE,
181
195
  absoluteRoot,
182
196
  loadQuerySearchBundle(absoluteRoot)
183
197
  .then(({ files }) => Promise.all([
@@ -517,35 +531,6 @@ function buildResolvedImportGraphs(rootDir, imports, indexedFileSet, importAlias
517
531
  };
518
532
  }
519
533
 
520
- function isLikelyTestFilePath(filePath) {
521
- const lower = filePath.toLowerCase();
522
- return (
523
- lower.startsWith('__tests__/')
524
- || lower.startsWith('test/')
525
- || lower.startsWith('tests/')
526
- || lower.startsWith('spec/')
527
- || lower.startsWith('specs/')
528
- || lower.includes('/__tests__/')
529
- || lower.includes('/test/')
530
- || lower.includes('/spec/')
531
- || lower.includes('/specs/')
532
- || lower.endsWith('.test.js')
533
- || lower.endsWith('.test.ts')
534
- || lower.endsWith('.test.tsx')
535
- || lower.endsWith('.test.jsx')
536
- || lower.endsWith('.test.vue')
537
- || lower.endsWith('.test.mjs')
538
- || lower.endsWith('.test.cjs')
539
- || lower.endsWith('.spec.js')
540
- || lower.endsWith('.spec.ts')
541
- || lower.endsWith('.spec.tsx')
542
- || lower.endsWith('.spec.jsx')
543
- || lower.endsWith('.spec.vue')
544
- || lower.endsWith('.spec.mjs')
545
- || lower.endsWith('.spec.cjs')
546
- );
547
- }
548
-
549
534
  // ── Index V2: Analog Query ──
550
535
 
551
536
  export async function queryAnalog({ rootDir = process.cwd(), filePath, limit = 5 } = {}) {
@@ -577,7 +562,7 @@ export async function queryAnalog({ rootDir = process.cwd(), filePath, limit = 5
577
562
  throw error;
578
563
  });
579
564
 
580
- ANALOG_RESULT_CACHE.set(analogCacheKey, analogPromise);
565
+ setBoundedCacheEntry(ANALOG_RESULT_CACHE, analogCacheKey, analogPromise);
581
566
  return analogPromise;
582
567
  }
583
568
 
@@ -12,6 +12,17 @@ const RELATED_TEST_RELATION_TYPES = [
12
12
  ];
13
13
  const RELATED_TEST_ARTIFACT_CACHE = new Map();
14
14
  const RELATED_TEST_LOOKUP_CACHE = new Map();
15
+ const MAX_CACHE_ENTRIES = 100;
16
+
17
+ function setBoundedCacheEntry(map, key, value, maxEntries = MAX_CACHE_ENTRIES) {
18
+ if (map.has(key)) {
19
+ map.delete(key);
20
+ }
21
+ map.set(key, value);
22
+ while (map.size > maxEntries) {
23
+ map.delete(map.keys().next().value);
24
+ }
25
+ }
15
26
 
16
27
  export async function inferRelatedTests({
17
28
  rootDir = process.cwd(),
@@ -48,7 +59,8 @@ export async function loadRelatedTestArtifacts({
48
59
 
49
60
  if (!analogsArtifact && !relationsArtifact) {
50
61
  if (!RELATED_TEST_LOOKUP_CACHE.has(absoluteRoot)) {
51
- RELATED_TEST_LOOKUP_CACHE.set(
62
+ setBoundedCacheEntry(
63
+ RELATED_TEST_LOOKUP_CACHE,
52
64
  absoluteRoot,
53
65
  Promise.all([
54
66
  readArtifact(absoluteRoot, INDEX_ARTIFACTS.analogs),
@@ -193,7 +205,8 @@ async function readArtifact(rootDir, artifactName) {
193
205
  const artifactPath = getArtifactPath(path.resolve(rootDir), artifactName);
194
206
 
195
207
  if (!RELATED_TEST_ARTIFACT_CACHE.has(artifactPath)) {
196
- RELATED_TEST_ARTIFACT_CACHE.set(
208
+ setBoundedCacheEntry(
209
+ RELATED_TEST_ARTIFACT_CACHE,
197
210
  artifactPath,
198
211
  fs.readFile(artifactPath, 'utf8')
199
212
  .then((content) => JSON.parse(content))
@@ -66,7 +66,7 @@ export const ROUTE_CATALOG = [
66
66
  {
67
67
  id: 'postgres',
68
68
  path: '.claude/skills/postgres/SKILL.md',
69
- order: 4,
69
+ order: 4.5,
70
70
  signals: [
71
71
  { type: 'prompt', regex: /\b(sql|postgres|postgresql|migration|schema|table|view|index|trigger|function|stored procedure|materialized view|query plan|explain)\b/i, score: 5 },
72
72
  { type: 'command', regex: /\b(psql|prisma|drizzle|knex|sequelize|typeorm)\b/i, score: 4 },