@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
|
@@ -12,7 +12,7 @@ import { summarizeDiff, toDiffRows } from './report.js';
|
|
|
12
12
|
import { writeInstallMetadata } from './metadata.js';
|
|
13
13
|
import { cleanupLegacyPaths, migrateLegacyRuntimeRoot } from './migrateLegacy.js';
|
|
14
14
|
import { ensureGitignore } from './ensureGitignore.js';
|
|
15
|
-
import { readJsonIfExists, removeFileOrLinkOnly, resolveProjectRelativePath } from './fileOps.js';
|
|
15
|
+
import { cleanupEmptyParents, readJsonIfExists, removeFileOrLinkOnly, resolveProjectRelativePath } from './fileOps.js';
|
|
16
16
|
|
|
17
17
|
const AUTO_PRUNE_OBSOLETE_PREFIXES = [
|
|
18
18
|
'.claude/skills/',
|
|
@@ -57,41 +57,6 @@ function shouldAutoPruneObsoletePath(relativePath) {
|
|
|
57
57
|
|| AUTO_PRUNE_OBSOLETE_PREFIXES.some((prefix) => relativePath.startsWith(prefix));
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
async function isDirEmpty(dirPath) {
|
|
61
|
-
try {
|
|
62
|
-
const entries = await fs.readdir(dirPath);
|
|
63
|
-
return entries.length === 0;
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function cleanupEmptyParents(targetPath, projectRoot) {
|
|
70
|
-
let currentDir = path.dirname(targetPath);
|
|
71
|
-
const stopDir = path.resolve(projectRoot);
|
|
72
|
-
|
|
73
|
-
while (currentDir.startsWith(stopDir) && currentDir !== stopDir) {
|
|
74
|
-
let stat;
|
|
75
|
-
try {
|
|
76
|
-
stat = await fs.lstat(currentDir);
|
|
77
|
-
} catch {
|
|
78
|
-
currentDir = path.dirname(currentDir);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!stat.isDirectory()) {
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!(await isDirEmpty(currentDir))) {
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await fs.rmdir(currentDir);
|
|
91
|
-
currentDir = path.dirname(currentDir);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
60
|
async function isTrackedManagedDirectoryTree({
|
|
96
61
|
dirPath,
|
|
97
62
|
projectRoot,
|
|
@@ -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.2',
|
|
53
53
|
agent: 'claude-code',
|
|
54
54
|
autonomy: {
|
|
55
55
|
level: 'balanced',
|
|
@@ -112,6 +112,59 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
|
|
|
112
112
|
advisorEnabled: true,
|
|
113
113
|
maxAdvisorCalls: 3,
|
|
114
114
|
},
|
|
115
|
+
orchestration: {
|
|
116
|
+
enabled: true,
|
|
117
|
+
orchestratorModel: 'claude-sonnet-4-6',
|
|
118
|
+
advisorEnabled: true,
|
|
119
|
+
contracts: {
|
|
120
|
+
'tiny-fix': {
|
|
121
|
+
maxReadPasses: 0,
|
|
122
|
+
maxContextPulls: 0,
|
|
123
|
+
verificationPolicy: 'minimal-or-targeted',
|
|
124
|
+
completionRule: 'never-claim-done-without-write',
|
|
125
|
+
delegationPolicy: 'disallow',
|
|
126
|
+
},
|
|
127
|
+
'local-fix': {
|
|
128
|
+
maxReadPasses: 1,
|
|
129
|
+
maxContextPulls: 1,
|
|
130
|
+
verificationPolicy: 'targeted-if-covered',
|
|
131
|
+
completionRule: 'require-write',
|
|
132
|
+
delegationPolicy: 'disallow',
|
|
133
|
+
},
|
|
134
|
+
'local-build': {
|
|
135
|
+
maxReadPasses: 2,
|
|
136
|
+
maxContextPulls: 1,
|
|
137
|
+
verificationPolicy: 'targeted-if-covered',
|
|
138
|
+
completionRule: 'require-write',
|
|
139
|
+
delegationPolicy: 'disallow-by-default',
|
|
140
|
+
},
|
|
141
|
+
'find-cause': {
|
|
142
|
+
maxReadPassesBeforeReassess: 3,
|
|
143
|
+
verificationPolicy: 'root-cause-then-targeted',
|
|
144
|
+
completionRule: 'never-claim-fixed-without-write-and-verification',
|
|
145
|
+
delegationPolicy: 'allow-specialized-debug-lane',
|
|
146
|
+
},
|
|
147
|
+
'shared-edit': {
|
|
148
|
+
maxReadPasses: 2,
|
|
149
|
+
maxContextPulls: 2,
|
|
150
|
+
verificationPolicy: 'targeted-then-widen-on-risk',
|
|
151
|
+
completionRule: 'require-write-and-verification',
|
|
152
|
+
delegationPolicy: 'allow-qualified-sidecar',
|
|
153
|
+
},
|
|
154
|
+
'map-impact': {
|
|
155
|
+
maxReadPasses: 3,
|
|
156
|
+
maxContextPulls: 3,
|
|
157
|
+
verificationPolicy: 'impact-first-then-targeted-then-widen-on-risk',
|
|
158
|
+
completionRule: 'require-impact-evidence-before-edit-claim',
|
|
159
|
+
delegationPolicy: 'allow-impact-sidecar',
|
|
160
|
+
},
|
|
161
|
+
'review-release': {
|
|
162
|
+
verificationPolicy: 'evidence-first',
|
|
163
|
+
completionRule: 'report-findings-not-implementation',
|
|
164
|
+
delegationPolicy: 'allow-review-sidecar',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
115
168
|
memory: {
|
|
116
169
|
enabled: true,
|
|
117
170
|
autoCapture: true,
|
|
@@ -268,16 +321,27 @@ export function validateRuntimeConfig(config) {
|
|
|
268
321
|
errors.push('router must be an object.');
|
|
269
322
|
} else {
|
|
270
323
|
pushBooleanError(errors, config.router.enabled, 'router.enabled');
|
|
271
|
-
if (typeof config.router.defaultModel !== 'string' || config.router.defaultModel.trim()
|
|
324
|
+
if (typeof config.router.defaultModel !== 'string' || config.router.defaultModel.trim() === '') {
|
|
272
325
|
errors.push('router.defaultModel must be a non-empty string.');
|
|
273
326
|
}
|
|
274
|
-
if (typeof config.router.advisorModel !== 'string' || config.router.advisorModel.trim()
|
|
327
|
+
if (typeof config.router.advisorModel !== 'string' || config.router.advisorModel.trim() === '') {
|
|
275
328
|
errors.push('router.advisorModel must be a non-empty string.');
|
|
276
329
|
}
|
|
277
330
|
pushBooleanError(errors, config.router.advisorEnabled, 'router.advisorEnabled');
|
|
278
331
|
pushPositiveNumberError(errors, config.router.maxAdvisorCalls, 'router.maxAdvisorCalls');
|
|
279
332
|
}
|
|
280
333
|
|
|
334
|
+
if (!isPlainObject(config.orchestration)) {
|
|
335
|
+
errors.push('orchestration must be an object.');
|
|
336
|
+
} else {
|
|
337
|
+
pushBooleanError(errors, config.orchestration.enabled, 'orchestration.enabled');
|
|
338
|
+
pushNonEmptyStringError(errors, config.orchestration.orchestratorModel, 'orchestration.orchestratorModel');
|
|
339
|
+
pushBooleanError(errors, config.orchestration.advisorEnabled, 'orchestration.advisorEnabled');
|
|
340
|
+
if (!isPlainObject(config.orchestration.contracts)) {
|
|
341
|
+
errors.push('orchestration.contracts must be an object.');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
281
345
|
if (!isPlainObject(config.memory)) {
|
|
282
346
|
errors.push('memory must be an object.');
|
|
283
347
|
} else {
|
|
@@ -372,5 +436,9 @@ export async function inspectRuntimeConfig(projectRoot) {
|
|
|
372
436
|
|
|
373
437
|
export async function loadRuntimeConfig(projectRoot) {
|
|
374
438
|
const inspection = await inspectRuntimeConfig(projectRoot);
|
|
439
|
+
if (inspection.exists && !inspection.valid) {
|
|
440
|
+
console.warn(`[UKit] Invalid UKit runtime config: ${inspection.errors.join('; ')}`);
|
|
441
|
+
return buildDefaultRuntimeConfig();
|
|
442
|
+
}
|
|
375
443
|
return inspection.config;
|
|
376
444
|
}
|
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',
|