@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.
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/src/bug/triageBug.js +1 -33
- package/src/cli/commands/install.js +5 -10
- package/src/context/detectProjectContext.js +3 -24
- package/src/core/compact/index.js +19 -27
- package/src/core/ensureGitignore.js +1 -1
- package/src/core/fileOps.js +41 -2
- package/src/core/memory/hygiene.js +17 -1
- package/src/core/memory/store.js +14 -36
- package/src/core/metadata.js +5 -5
- package/src/core/output/index.js +20 -20
- package/src/core/packageManager.js +51 -0
- package/src/core/router/router.js +22 -6
- package/src/core/runInstallPipeline.js +1 -36
- package/src/core/runtimeConfig.js +71 -3
- package/src/core/token/index.js +21 -1
- package/src/core/uninstall.js +15 -38
- package/src/index/buildIndex.js +217 -49
- package/src/index/gitHooks.js +32 -7
- package/src/index/impactContext.js +16 -6
- package/src/index/importResolution.js +105 -28
- package/src/index/paths.js +29 -0
- package/src/index/queryIndex.js +20 -35
- package/src/index/relatedTests.js +15 -2
- package/src/index/routeCatalog.js +1 -1
- package/src/index/taskRouting.js +438 -18
- package/src/index/verificationPlan.js +2 -36
- package/templates/.claude/hooks/reinject-context.sh +2 -0
- package/templates/.claude/hooks/session-start.md +2 -0
- package/templates/.claude/hooks/skill-router.sh +657 -15
- package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
- package/templates/.claude/ukit/index/route-task.mjs +475 -5
- package/templates/.claude/ukit/runtime/reinject-context.mjs +120 -3
- package/templates/.codex/README.md +8 -1
- package/templates/.codex/settings.json +53 -0
- package/templates/AGENTS.md +3 -0
- package/templates/CLAUDE.md +5 -1
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +61 -2
package/src/index/gitHooks.js
CHANGED
|
@@ -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 (
|
|
22
|
+
if (hasCompleteManagedBlock(base)) {
|
|
21
23
|
unchanged += 1;
|
|
22
24
|
continue;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
const
|
|
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 || !
|
|
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
|
|
79
|
-
|
|
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
|
|
88
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
323
|
-
const
|
|
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
|
|
376
|
+
function stripJsoncComments(raw) {
|
|
330
377
|
let result = '';
|
|
331
378
|
let inString = false;
|
|
332
|
-
let
|
|
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 (
|
|
342
|
-
|
|
343
|
-
|
|
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 === '"'
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
}
|
package/src/index/paths.js
CHANGED
|
@@ -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
|
+
}
|
package/src/index/queryIndex.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 },
|