@ngockhoale/ukit 1.4.0 → 1.4.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/CHANGELOG.md +4 -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 +7 -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 +56 -14
- package/src/index/verificationPlan.js +2 -36
- package/templates/.claude/ukit/index/route-catalog.mjs +1 -1
- package/templates/.codex/README.md +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +1 -1
|
@@ -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 },
|
package/src/index/taskRouting.js
CHANGED
|
@@ -53,17 +53,28 @@ export async function deriveTaskRoute({
|
|
|
53
53
|
targetFile: normalizedTarget,
|
|
54
54
|
});
|
|
55
55
|
const preservedPrompt = normalizedPrompt || String(lastExplicitUserPromptText || '').trim();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
const degradedWarnings = [];
|
|
57
|
+
let contextResult = null;
|
|
58
|
+
if (useIndexedContext && (contextIntent || normalizedTarget)) {
|
|
59
|
+
const indexReadError = await findUnreadableIndexArtifact(absoluteRoot);
|
|
60
|
+
if (indexReadError) {
|
|
61
|
+
degradedWarnings.push(`resolve-context failed: ${indexReadError.message}`);
|
|
62
|
+
} else {
|
|
63
|
+
try {
|
|
64
|
+
contextResult = await resolveContext({
|
|
65
|
+
rootDir: absoluteRoot,
|
|
66
|
+
intent: contextIntent,
|
|
67
|
+
targetFile: normalizedTarget,
|
|
68
|
+
taskType: inferredTaskType,
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
degradedWarnings.push(`resolve-context failed: ${error?.message ?? String(error)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
const enrichedContextResult = expandRouteContext(contextResult);
|
|
65
76
|
|
|
66
|
-
const contextRecommendation = useIndexedContext
|
|
77
|
+
const contextRecommendation = useIndexedContext && degradedWarnings.length === 0
|
|
67
78
|
? buildContextRecommendation({
|
|
68
79
|
commandNamespace,
|
|
69
80
|
contextIntent,
|
|
@@ -72,8 +83,10 @@ export async function deriveTaskRoute({
|
|
|
72
83
|
contextResult: enrichedContextResult,
|
|
73
84
|
})
|
|
74
85
|
: null;
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
let verificationPlan = null;
|
|
87
|
+
if (useIndexedContext && contextResult) {
|
|
88
|
+
try {
|
|
89
|
+
verificationPlan = await deriveVerificationPlan({
|
|
77
90
|
rootDir: absoluteRoot,
|
|
78
91
|
intent: contextIntent,
|
|
79
92
|
targetFile: normalizedTarget,
|
|
@@ -81,8 +94,11 @@ export async function deriveTaskRoute({
|
|
|
81
94
|
contextResult: enrichedContextResult,
|
|
82
95
|
skillIds: selectedIds,
|
|
83
96
|
autonomyLevel,
|
|
84
|
-
})
|
|
85
|
-
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
degradedWarnings.push(`verify-context failed: ${error?.message ?? String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
86
102
|
|
|
87
103
|
const verificationRecommendation = verificationPlan
|
|
88
104
|
? {
|
|
@@ -134,6 +150,7 @@ export async function deriveTaskRoute({
|
|
|
134
150
|
verificationRecommendation,
|
|
135
151
|
nextAction,
|
|
136
152
|
routeSummary,
|
|
153
|
+
...(degradedWarnings.length > 0 ? { degradedWarnings } : {}),
|
|
137
154
|
};
|
|
138
155
|
}
|
|
139
156
|
|
|
@@ -857,12 +874,36 @@ function normalizeRelativeFile(rootDir, rawFilePath) {
|
|
|
857
874
|
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
858
875
|
return relative.replaceAll('\\', '/');
|
|
859
876
|
}
|
|
860
|
-
return
|
|
877
|
+
return null;
|
|
861
878
|
}
|
|
862
879
|
|
|
863
880
|
return trimmed.replace(/^\.\/+/, '').replaceAll('\\', '/');
|
|
864
881
|
}
|
|
865
882
|
|
|
883
|
+
async function findUnreadableIndexArtifact(rootDir) {
|
|
884
|
+
const indexDir = path.join(rootDir, '.cache', 'index');
|
|
885
|
+
let entries;
|
|
886
|
+
try {
|
|
887
|
+
entries = await fs.readdir(indexDir);
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
for (const entry of entries) {
|
|
893
|
+
if (!entry.endsWith('.json')) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
try {
|
|
898
|
+
JSON.parse(await fs.readFile(path.join(indexDir, entry), 'utf8'));
|
|
899
|
+
} catch (error) {
|
|
900
|
+
return new Error(`${entry}: ${error?.message ?? String(error)}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
|
|
866
907
|
async function pathExists(filePath) {
|
|
867
908
|
try {
|
|
868
909
|
await fs.access(filePath);
|
|
@@ -879,6 +920,7 @@ function unique(values) {
|
|
|
879
920
|
const DELEGATABLE_IMPLEMENTATION_SKILL_IDS = new Set([
|
|
880
921
|
'delivery',
|
|
881
922
|
'frontend',
|
|
923
|
+
'frontend-design',
|
|
882
924
|
'frontend-vue',
|
|
883
925
|
'backend-api',
|
|
884
926
|
'postgres',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { detectPackageManagerFromFingerprint, getDeclaredPackageManager } from '../core/packageManager.js';
|
|
4
5
|
import { resolveContext } from './resolveContext.js';
|
|
5
6
|
|
|
6
7
|
const RISKY_SKILL_IDS = new Set(['discover-security', 'repo-maintenance']);
|
|
@@ -277,7 +278,7 @@ async function loadProjectVerificationEnvironment({
|
|
|
277
278
|
.then((resolvedPkg) => ({
|
|
278
279
|
pkg: resolvedPkg,
|
|
279
280
|
scripts: extractScripts(resolvedPkg),
|
|
280
|
-
packageManager:
|
|
281
|
+
packageManager: detectPackageManagerFromFingerprint({ fingerprintEntries, pkg: resolvedPkg }),
|
|
281
282
|
}))
|
|
282
283
|
.catch((error) => {
|
|
283
284
|
PROJECT_VERIFICATION_ENV_CACHE.delete(cacheKey);
|
|
@@ -297,41 +298,6 @@ function extractScripts(pkg = null) {
|
|
|
297
298
|
return pkg?.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {};
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
function detectPackageManager({ fingerprintEntries = [], pkg = null } = {}) {
|
|
301
|
-
const declaredPackageManager = getDeclaredPackageManager(pkg);
|
|
302
|
-
if (declaredPackageManager) return declaredPackageManager;
|
|
303
|
-
|
|
304
|
-
const existingFiles = new Set(
|
|
305
|
-
fingerprintEntries
|
|
306
|
-
.filter((entry) => entry && entry.mtimeMs !== null)
|
|
307
|
-
.map((entry) => entry.filePath),
|
|
308
|
-
);
|
|
309
|
-
const checks = [
|
|
310
|
-
['pnpm-lock.yaml', 'pnpm'],
|
|
311
|
-
['yarn.lock', 'yarn'],
|
|
312
|
-
['bun.lockb', 'bun'],
|
|
313
|
-
['bun.lock', 'bun'],
|
|
314
|
-
['package-lock.json', 'npm'],
|
|
315
|
-
];
|
|
316
|
-
|
|
317
|
-
for (const [lockFile, pm] of checks) {
|
|
318
|
-
if (existingFiles.has(lockFile)) {
|
|
319
|
-
return pm;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return 'npm';
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function getDeclaredPackageManager(pkg = null) {
|
|
327
|
-
const declared = String(pkg?.packageManager ?? '').toLowerCase();
|
|
328
|
-
if (declared.startsWith('pnpm')) return 'pnpm';
|
|
329
|
-
if (declared.startsWith('yarn')) return 'yarn';
|
|
330
|
-
if (declared.startsWith('bun')) return 'bun';
|
|
331
|
-
if (declared.startsWith('npm')) return 'npm';
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
301
|
function buildScriptCommand(packageManager, scriptName, args = []) {
|
|
336
302
|
const filteredArgs = args.filter(Boolean);
|
|
337
303
|
|
|
@@ -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 },
|
|
@@ -12,7 +12,7 @@ Auto-generated by UKit for OpenAI Codex.
|
|
|
12
12
|
- Do not make end users memorize skill names, helper scripts, or routing internals unless they are debugging UKit itself.
|
|
13
13
|
- **Treat helper commands as internal orchestration. Do not ask end users to run them.**
|
|
14
14
|
|
|
15
|
-
## UKit v1.4.
|
|
15
|
+
## UKit v1.4.1 Shared Runtime
|
|
16
16
|
|
|
17
17
|
- Shared runtime state lives in `.ukit/storage/`.
|
|
18
18
|
- Treat `.ukit/storage/config.json` as the source of compact, token-pipeline, router, memory, validation, and Safe Patch guardrail toggles.
|
package/templates/CLAUDE.md
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
- **Do not ask normal contributors to run internal helper commands**; run them yourself or tell them to rerun `ukit install`.
|
|
43
43
|
- Do not ask normal contributors to memorize `ukit doctor`, `ukit diff`, `ukit uninstall`, or `ukit index ...` unless they explicitly need maintainer/debug help.
|
|
44
44
|
|
|
45
|
-
## UKit v1.4.
|
|
45
|
+
## UKit v1.4.1 Shared Runtime
|
|
46
46
|
|
|
47
47
|
- Shared runtime state lives in `.ukit/storage/`.
|
|
48
48
|
- Treat `.ukit/storage/config.json` as the source of runtime toggles for compact, token pipeline, router, memory, validation, and Safe Patch behavior.
|
package/templates/ukit/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# UKit Shared Runtime
|
|
2
2
|
|
|
3
|
-
This folder stores shared UKit runtime state for v1.4.
|
|
3
|
+
This folder stores shared UKit runtime state for v1.4.1 features.
|
|
4
4
|
|
|
5
5
|
- `storage/config.json` — runtime feature flags and defaults
|
|
6
6
|
- `storage/cache/` — prompt-cache, compact history, compact pressure state, output summaries, and preserved raw tool outputs under `storage/cache/tee/`
|