@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
|
@@ -49,7 +49,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
49
49
|
const safeOverrides = isPlainObject(overrides) ? overrides : {};
|
|
50
50
|
|
|
51
51
|
return mergeObjects({
|
|
52
|
-
version: '1.4.
|
|
52
|
+
version: '1.4.1',
|
|
53
53
|
agent: 'claude-code',
|
|
54
54
|
autonomy: {
|
|
55
55
|
level: 'balanced',
|
|
@@ -268,10 +268,10 @@ export function validateRuntimeConfig(config) {
|
|
|
268
268
|
errors.push('router must be an object.');
|
|
269
269
|
} else {
|
|
270
270
|
pushBooleanError(errors, config.router.enabled, 'router.enabled');
|
|
271
|
-
if (typeof config.router.defaultModel !== 'string' || config.router.defaultModel.trim()
|
|
271
|
+
if (typeof config.router.defaultModel !== 'string' || config.router.defaultModel.trim() === '') {
|
|
272
272
|
errors.push('router.defaultModel must be a non-empty string.');
|
|
273
273
|
}
|
|
274
|
-
if (typeof config.router.advisorModel !== 'string' || config.router.advisorModel.trim()
|
|
274
|
+
if (typeof config.router.advisorModel !== 'string' || config.router.advisorModel.trim() === '') {
|
|
275
275
|
errors.push('router.advisorModel must be a non-empty string.');
|
|
276
276
|
}
|
|
277
277
|
pushBooleanError(errors, config.router.advisorEnabled, 'router.advisorEnabled');
|
|
@@ -372,5 +372,9 @@ export async function inspectRuntimeConfig(projectRoot) {
|
|
|
372
372
|
|
|
373
373
|
export async function loadRuntimeConfig(projectRoot) {
|
|
374
374
|
const inspection = await inspectRuntimeConfig(projectRoot);
|
|
375
|
+
if (inspection.exists && !inspection.valid) {
|
|
376
|
+
console.warn(`[UKit] Invalid UKit runtime config: ${inspection.errors.join('; ')}`);
|
|
377
|
+
return buildDefaultRuntimeConfig();
|
|
378
|
+
}
|
|
375
379
|
return inspection.config;
|
|
376
380
|
}
|
package/src/core/token/index.js
CHANGED
|
@@ -30,6 +30,25 @@ export function estimateTokenCount(value) {
|
|
|
30
30
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function normalizeLineForDedupe(line) {
|
|
34
|
+
return String(line ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function uniqueLines(lines) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const unique = [];
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const trimmed = String(line ?? '').trim();
|
|
42
|
+
const normalized = normalizeLineForDedupe(trimmed);
|
|
43
|
+
if (!normalized || seen.has(normalized)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
seen.add(normalized);
|
|
47
|
+
unique.push(trimmed);
|
|
48
|
+
}
|
|
49
|
+
return unique;
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
export function buildCompactMachineKey(prefix, payload = {}) {
|
|
34
53
|
return `${prefix}:${crypto.createHash('sha256').update(JSON.stringify(payload)).digest('base64url')}`;
|
|
35
54
|
}
|
|
@@ -47,7 +66,8 @@ function isStructuredLine(line) {
|
|
|
47
66
|
function normalizeWhitespace(value) {
|
|
48
67
|
return value
|
|
49
68
|
.replace(/\s+\|\s+/g, ' | ')
|
|
50
|
-
.replace(/\s
|
|
69
|
+
.replace(/\s*:\s*/g, ': ')
|
|
70
|
+
.replace(/\s*;\s*/g, '; ')
|
|
51
71
|
.replace(/\s*,\s*/g, ', ')
|
|
52
72
|
.replace(/\s+/g, ' ')
|
|
53
73
|
.trim();
|
package/src/core/uninstall.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
|
+
cleanupEmptyParents,
|
|
4
5
|
readJsonIfExists,
|
|
5
6
|
removeLinkOrDir,
|
|
6
7
|
removeLinkOnly,
|
|
@@ -8,41 +9,6 @@ import {
|
|
|
8
9
|
} from './fileOps.js';
|
|
9
10
|
import { removeGitignoreBlock } from './ensureGitignore.js';
|
|
10
11
|
|
|
11
|
-
async function isDirEmpty(dirPath) {
|
|
12
|
-
try {
|
|
13
|
-
const entries = await fs.readdir(dirPath);
|
|
14
|
-
return entries.length === 0;
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function cleanupEmptyParents(targetPath, projectRoot) {
|
|
21
|
-
let currentDir = path.dirname(targetPath);
|
|
22
|
-
const stopDir = path.resolve(projectRoot);
|
|
23
|
-
|
|
24
|
-
while (currentDir.startsWith(stopDir) && currentDir !== stopDir) {
|
|
25
|
-
let stat;
|
|
26
|
-
try {
|
|
27
|
-
stat = await fs.lstat(currentDir);
|
|
28
|
-
} catch {
|
|
29
|
-
currentDir = path.dirname(currentDir);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!stat.isDirectory()) {
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!(await isDirEmpty(currentDir))) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await fs.rmdir(currentDir);
|
|
42
|
-
currentDir = path.dirname(currentDir);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
12
|
// Use lstat (not access) so broken symlinks are also detected as existing.
|
|
47
13
|
async function pathExistsLstat(targetPath) {
|
|
48
14
|
try {
|
|
@@ -134,7 +100,12 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
|
|
|
134
100
|
// An attacker (or confused user) could forge the file to trigger uninstall of
|
|
135
101
|
// the hardcoded managed paths. Requiring 'tool: ukit' ensures the file was
|
|
136
102
|
// written by the UKit installer, not created manually.
|
|
137
|
-
|
|
103
|
+
let installData;
|
|
104
|
+
try {
|
|
105
|
+
installData = await readJsonIfExists(installMetaPath);
|
|
106
|
+
} catch {
|
|
107
|
+
return { removed: 0, attempted: 0, wasInstalled: false };
|
|
108
|
+
}
|
|
138
109
|
if (!installData || installData.tool !== 'ukit') {
|
|
139
110
|
return { removed: 0, attempted: 0, wasInstalled: false };
|
|
140
111
|
}
|
|
@@ -233,14 +204,20 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
|
|
|
233
204
|
// Clean up .gitignore block added during install
|
|
234
205
|
await removeGitignoreBlock(projectRoot);
|
|
235
206
|
|
|
236
|
-
// Cleanup empty parent directories sequentially (order matters)
|
|
237
207
|
let removed = 0;
|
|
208
|
+
const removedPaths = [];
|
|
238
209
|
for (const { abs, didRemove } of results) {
|
|
239
210
|
if (didRemove) {
|
|
240
211
|
removed += 1;
|
|
241
|
-
|
|
212
|
+
removedPaths.push(abs);
|
|
242
213
|
}
|
|
243
214
|
}
|
|
244
215
|
|
|
216
|
+
const deepestRemovedPaths = [...new Set(removedPaths)]
|
|
217
|
+
.sort((left, right) => right.length - left.length);
|
|
218
|
+
for (const removedPath of deepestRemovedPaths) {
|
|
219
|
+
await cleanupEmptyParents(removedPath, projectRoot);
|
|
220
|
+
}
|
|
221
|
+
|
|
245
222
|
return { removed, attempted: allEntries.length, wasInstalled: true };
|
|
246
223
|
}
|
package/src/index/buildIndex.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
|
|
5
|
-
import { getArtifactPath, getIndexDir, INDEX_ARTIFACTS, INDEX_SCHEMA_VERSION, normalizeRelative } from './paths.js';
|
|
5
|
+
import { getArtifactPath, getIndexDir, INDEX_ARTIFACTS, INDEX_SCHEMA_VERSION, isLikelyTestFilePath, normalizeRelative } from './paths.js';
|
|
6
6
|
import { importsNeedAliasContext, loadImportAliasContextState, resolveImportSpecifier } from './importResolution.js';
|
|
7
7
|
import { clearIndexArtifactCache } from './queryIndex.js';
|
|
8
8
|
import { clearRelatedTestArtifactCache } from './relatedTests.js';
|
|
@@ -466,16 +466,163 @@ async function collectFiles(scanRoots) {
|
|
|
466
466
|
return result;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
|
|
470
|
+
function stripCommentsStringAware(content) {
|
|
471
|
+
let result = '';
|
|
472
|
+
let state = 'normal';
|
|
473
|
+
let escaped = false;
|
|
474
|
+
let templateExpressionDepth = 0;
|
|
475
|
+
let regexCharClass = false;
|
|
476
|
+
|
|
477
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
478
|
+
const char = content[index];
|
|
479
|
+
const next = content[index + 1];
|
|
480
|
+
|
|
481
|
+
if (state === 'line-comment') {
|
|
482
|
+
if (char === '\n') {
|
|
483
|
+
result += char;
|
|
484
|
+
state = 'normal';
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (state === 'block-comment') {
|
|
490
|
+
if (char === '*' && next === '/') {
|
|
491
|
+
index += 1;
|
|
492
|
+
state = 'normal';
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
result += char;
|
|
498
|
+
|
|
499
|
+
if (state === 'single' || state === 'double' || state === 'template' || state === 'regex') {
|
|
500
|
+
if (escaped) {
|
|
501
|
+
escaped = false;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (char === '\\') {
|
|
505
|
+
escaped = true;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (state === 'single' && char === "'") state = 'normal';
|
|
509
|
+
if (state === 'double' && char === '"') state = 'normal';
|
|
510
|
+
if (state === 'template') {
|
|
511
|
+
if (char === '`' && templateExpressionDepth === 0) state = 'normal';
|
|
512
|
+
else if (char === '{' && content[index - 1] === '$') templateExpressionDepth += 1;
|
|
513
|
+
else if (char === '}' && templateExpressionDepth > 0) templateExpressionDepth -= 1;
|
|
514
|
+
}
|
|
515
|
+
if (state === 'regex') {
|
|
516
|
+
if (char === '[') regexCharClass = true;
|
|
517
|
+
else if (char === ']') regexCharClass = false;
|
|
518
|
+
else if (char === '/' && !regexCharClass) state = 'normal';
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (char === '/' && next === '/') {
|
|
524
|
+
result = result.slice(0, -1);
|
|
525
|
+
state = 'line-comment';
|
|
526
|
+
index += 1;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (char === '/' && next === '*') {
|
|
530
|
+
result = result.slice(0, -1);
|
|
531
|
+
state = 'block-comment';
|
|
532
|
+
index += 1;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (char === "'") state = 'single';
|
|
536
|
+
else if (char === '"') state = 'double';
|
|
537
|
+
else if (char === '`') state = 'template';
|
|
538
|
+
else if (char === '/' && isRegexLiteralStart(result.slice(0, -1))) {
|
|
539
|
+
state = 'regex';
|
|
540
|
+
regexCharClass = false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function isRegexLiteralStart(prefix) {
|
|
548
|
+
const trimmed = prefix.trimEnd();
|
|
549
|
+
return !trimmed || /(?:[=(:,!&|?{};\[\n]|\b(?:return|throw|case|delete|typeof|void|new|in|of|yield|await)\s*)$/.test(trimmed);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function findScriptClose(content, startIndex) {
|
|
553
|
+
const lower = content.toLowerCase();
|
|
554
|
+
let state = 'normal';
|
|
555
|
+
let escaped = false;
|
|
556
|
+
let templateExpressionDepth = 0;
|
|
557
|
+
|
|
558
|
+
for (let index = startIndex; index < content.length; index += 1) {
|
|
559
|
+
const char = content[index];
|
|
560
|
+
const next = content[index + 1];
|
|
561
|
+
|
|
562
|
+
if (state === 'line-comment') {
|
|
563
|
+
if (char === '\n') state = 'normal';
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (state === 'block-comment') {
|
|
567
|
+
if (char === '*' && next === '/') {
|
|
568
|
+
index += 1;
|
|
569
|
+
state = 'normal';
|
|
570
|
+
}
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (state === 'single' || state === 'double' || state === 'template') {
|
|
574
|
+
if (escaped) {
|
|
575
|
+
escaped = false;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (char === '\\') {
|
|
579
|
+
escaped = true;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (state === 'single' && char === "'") state = 'normal';
|
|
583
|
+
else if (state === 'double' && char === '"') state = 'normal';
|
|
584
|
+
else if (state === 'template') {
|
|
585
|
+
if (char === '`' && templateExpressionDepth === 0) state = 'normal';
|
|
586
|
+
else if (char === '{' && content[index - 1] === '$') templateExpressionDepth += 1;
|
|
587
|
+
else if (char === '}' && templateExpressionDepth > 0) templateExpressionDepth -= 1;
|
|
588
|
+
}
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (lower.startsWith('</script>', index)) return index;
|
|
593
|
+
if (char === '/' && next === '/') {
|
|
594
|
+
state = 'line-comment';
|
|
595
|
+
index += 1;
|
|
596
|
+
} else if (char === '/' && next === '*') {
|
|
597
|
+
state = 'block-comment';
|
|
598
|
+
index += 1;
|
|
599
|
+
} else if (char === "'") state = 'single';
|
|
600
|
+
else if (char === '"') state = 'double';
|
|
601
|
+
else if (char === '`') state = 'template';
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return -1;
|
|
605
|
+
}
|
|
606
|
+
|
|
469
607
|
function extractScriptContent(filePath, content) {
|
|
470
608
|
if (!filePath.endsWith('.vue')) {
|
|
471
609
|
return content;
|
|
472
610
|
}
|
|
473
611
|
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
612
|
+
const scripts = [];
|
|
613
|
+
const startTagPattern = /<script[^>]*>/gi;
|
|
614
|
+
for (const match of content.matchAll(startTagPattern)) {
|
|
615
|
+
const bodyStart = match.index + match[0].length;
|
|
616
|
+
const bodyEnd = findScriptClose(content, bodyStart);
|
|
617
|
+
if (bodyEnd !== -1) {
|
|
618
|
+
scripts.push(content.slice(bodyStart, bodyEnd));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (scripts.length === 0) return '';
|
|
622
|
+
return scripts.join('\n');
|
|
477
623
|
}
|
|
478
624
|
|
|
625
|
+
|
|
479
626
|
function extractSymbols(filePath, content) {
|
|
480
627
|
const symbols = [];
|
|
481
628
|
const addSymbol = (name, type) => {
|
|
@@ -529,13 +676,10 @@ function extractSymbols(filePath, content) {
|
|
|
529
676
|
}
|
|
530
677
|
|
|
531
678
|
function extractImports(filePath, content) {
|
|
532
|
-
|
|
533
|
-
const stripped = content
|
|
534
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
|
535
|
-
.replace(/\/\/.*$/gm, ''); // line comments
|
|
679
|
+
const stripped = stripCommentsStringAware(content);
|
|
536
680
|
const imports = [];
|
|
537
681
|
|
|
538
|
-
for (const match of stripped.matchAll(/import\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
682
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*import\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
539
683
|
imports.push({
|
|
540
684
|
from: filePath,
|
|
541
685
|
to: match[1],
|
|
@@ -559,7 +703,7 @@ function extractImports(filePath, content) {
|
|
|
559
703
|
});
|
|
560
704
|
}
|
|
561
705
|
|
|
562
|
-
for (const match of stripped.matchAll(/export\s+(?:type\s+)?\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
706
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+(?:type\s+)?\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
563
707
|
imports.push({
|
|
564
708
|
from: filePath,
|
|
565
709
|
to: match[1],
|
|
@@ -567,7 +711,7 @@ function extractImports(filePath, content) {
|
|
|
567
711
|
});
|
|
568
712
|
}
|
|
569
713
|
|
|
570
|
-
for (const match of stripped.matchAll(/export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
714
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
571
715
|
imports.push({
|
|
572
716
|
from: filePath,
|
|
573
717
|
to: match[1],
|
|
@@ -629,18 +773,71 @@ function extractFunctionBlocks(content) {
|
|
|
629
773
|
|
|
630
774
|
function sliceBalancedBlock(content, startIndex) {
|
|
631
775
|
let depth = 1;
|
|
776
|
+
let state = 'normal';
|
|
777
|
+
let escaped = false;
|
|
778
|
+
let templateExpressionDepth = 0;
|
|
779
|
+
|
|
632
780
|
for (let index = startIndex; index < content.length; index += 1) {
|
|
633
781
|
const char = content[index];
|
|
634
|
-
|
|
635
|
-
|
|
782
|
+
const next = content[index + 1];
|
|
783
|
+
|
|
784
|
+
if (state === 'line-comment') {
|
|
785
|
+
if (char === '\n') state = 'normal';
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (state === 'block-comment') {
|
|
789
|
+
if (char === '*' && next === '/') {
|
|
790
|
+
index += 1;
|
|
791
|
+
state = 'normal';
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
636
794
|
}
|
|
637
|
-
if (
|
|
638
|
-
|
|
795
|
+
if (state === 'single' || state === 'double' || state === 'template') {
|
|
796
|
+
if (escaped) {
|
|
797
|
+
escaped = false;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (char === '\\') {
|
|
801
|
+
escaped = true;
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (state === 'single' && char === "'") state = 'normal';
|
|
805
|
+
else if (state === 'double' && char === '"') state = 'normal';
|
|
806
|
+
else if (state === 'template') {
|
|
807
|
+
if (char === '`' && templateExpressionDepth === 0) state = 'normal';
|
|
808
|
+
else if (char === '{' && content[index - 1] === '$') templateExpressionDepth += 1;
|
|
809
|
+
else if (char === '}' && templateExpressionDepth > 0) templateExpressionDepth -= 1;
|
|
810
|
+
}
|
|
811
|
+
continue;
|
|
639
812
|
}
|
|
640
|
-
|
|
641
|
-
|
|
813
|
+
|
|
814
|
+
if (char === '/' && next === '/') {
|
|
815
|
+
state = 'line-comment';
|
|
816
|
+
index += 1;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (char === '/' && next === '*') {
|
|
820
|
+
state = 'block-comment';
|
|
821
|
+
index += 1;
|
|
822
|
+
continue;
|
|
642
823
|
}
|
|
824
|
+
if (char === "'") {
|
|
825
|
+
state = 'single';
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (char === '"') {
|
|
829
|
+
state = 'double';
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (char === '`') {
|
|
833
|
+
state = 'template';
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (char === '{') depth += 1;
|
|
837
|
+
if (char === '}') depth -= 1;
|
|
838
|
+
if (depth === 0) return content.slice(startIndex, index);
|
|
643
839
|
}
|
|
840
|
+
|
|
644
841
|
return '';
|
|
645
842
|
}
|
|
646
843
|
|
|
@@ -666,8 +863,8 @@ function extractFunctionCalls(filePath, content) {
|
|
|
666
863
|
}
|
|
667
864
|
|
|
668
865
|
function buildTestsMap(fileRecords) {
|
|
669
|
-
const testFiles = fileRecords.filter((file) =>
|
|
670
|
-
const sourceFiles = fileRecords.filter((file) => CODE_EXTENSIONS.has(file.ext) && !
|
|
866
|
+
const testFiles = fileRecords.filter((file) => isLikelyTestFilePath(file.filePath));
|
|
867
|
+
const sourceFiles = fileRecords.filter((file) => CODE_EXTENSIONS.has(file.ext) && !isLikelyTestFilePath(file.filePath));
|
|
671
868
|
|
|
672
869
|
return sourceFiles.map((sourceFile) => {
|
|
673
870
|
const matches = testFiles
|
|
@@ -771,35 +968,6 @@ function shouldSkipDirectory(name) {
|
|
|
771
968
|
return name.startsWith('.') && name !== '.claude' && name !== '.codex' && name !== '.antigravity';
|
|
772
969
|
}
|
|
773
970
|
|
|
774
|
-
function isLikelyTestFile(filePath) {
|
|
775
|
-
const lower = filePath.toLowerCase();
|
|
776
|
-
return (
|
|
777
|
-
lower.startsWith('__tests__/')
|
|
778
|
-
|| lower.startsWith('test/')
|
|
779
|
-
|| lower.startsWith('tests/')
|
|
780
|
-
|| lower.startsWith('spec/')
|
|
781
|
-
|| lower.startsWith('specs/')
|
|
782
|
-
|| lower.includes('/__tests__/')
|
|
783
|
-
|| lower.includes('/test/')
|
|
784
|
-
|| lower.includes('/spec/')
|
|
785
|
-
|| lower.includes('/specs/')
|
|
786
|
-
|| lower.endsWith('.test.js')
|
|
787
|
-
|| lower.endsWith('.test.ts')
|
|
788
|
-
|| lower.endsWith('.test.vue')
|
|
789
|
-
|| lower.endsWith('.test.tsx')
|
|
790
|
-
|| lower.endsWith('.test.jsx')
|
|
791
|
-
|| lower.endsWith('.test.mjs')
|
|
792
|
-
|| lower.endsWith('.test.cjs')
|
|
793
|
-
|| lower.endsWith('.spec.js')
|
|
794
|
-
|| lower.endsWith('.spec.ts')
|
|
795
|
-
|| lower.endsWith('.spec.vue')
|
|
796
|
-
|| lower.endsWith('.spec.tsx')
|
|
797
|
-
|| lower.endsWith('.spec.jsx')
|
|
798
|
-
|| lower.endsWith('.spec.mjs')
|
|
799
|
-
|| lower.endsWith('.spec.cjs')
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
971
|
function createSourceFingerprint(rootDir, discoveredFiles) {
|
|
804
972
|
const hash = crypto.createHash('sha256');
|
|
805
973
|
const normalizedEntries = discoveredFiles
|
|
@@ -985,7 +1153,7 @@ function classifyArchetypes(fileRecords, symbols) {
|
|
|
985
1153
|
return fileRecords
|
|
986
1154
|
.filter((f) => CODE_EXTENSIONS.has(f.ext))
|
|
987
1155
|
.map((file) => {
|
|
988
|
-
if (
|
|
1156
|
+
if (isLikelyTestFilePath(file.filePath)) {
|
|
989
1157
|
return {
|
|
990
1158
|
filePath: file.filePath,
|
|
991
1159
|
archetype: 'test',
|
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}`),
|