@ngockhoale/ukit 1.1.6 → 1.1.7
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 +11 -0
- package/README.md +5 -3
- package/manifests/platform.full.yaml +13 -0
- package/package.json +1 -1
- package/src/core/runtimeConfig.js +1 -1
- package/src/index/buildIndex.js +88 -2
- package/src/index/impactCatalog.js +126 -0
- package/src/index/impactContext.js +232 -0
- package/src/index/paths.js +1 -0
- package/src/index/resolveContext.js +1 -0
- package/src/index/taskRouting.js +10 -3
- package/src/index/verificationPlan.js +18 -1
- package/templates/.claude/hooks/skill-router.sh +1 -1
- package/templates/.claude/hooks/verification-guard.sh +150 -12
- package/templates/.claude/ukit/index/impact-context.mjs +122 -0
- package/templates/.claude/ukit/index/lib/index-core.mjs +352 -2
- package/templates/.claude/ukit/index/route-task.mjs +29 -3
- package/templates/.codex/README.md +1 -1
- package/templates/AGENTS.md +1 -1
- package/templates/CLAUDE.md +1 -1
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to UKit are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.1.7 - 2026-04-25
|
|
8
|
+
|
|
9
|
+
### Impact Confidence
|
|
10
|
+
|
|
11
|
+
- Added impact-confidence indexing for changed shared/runtime files, including lightweight call extraction and `calls.json` artifacts.
|
|
12
|
+
- Added shared-impact classification so source index runtime, install/runtime core, shipped hooks/helpers, manifests, and templates are routed as higher-confidence verification work instead of ordinary simple edits.
|
|
13
|
+
- Added mirror-counterpart guidance between source modules and installed helper mirrors, helping agents catch source/template drift before release.
|
|
14
|
+
- Added shipped `impact-context.mjs` helper and manifest coverage so installed workspaces can resolve impact context after `ukit install`.
|
|
15
|
+
- Tightened verification planning and guard behavior so impact-test lanes run targeted checks first, while broad verification remains advisory unless explicitly enforced.
|
|
16
|
+
- Added package-artifact, hook, indexing, impact-context, and impact-catalog coverage for the new release surface.
|
|
17
|
+
|
|
7
18
|
## 1.1.6 - 2026-04-23
|
|
8
19
|
|
|
9
20
|
### Hidden Runtime Root
|
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ If maintainers roll out a newer CLI build, the in-project workflow still stays t
|
|
|
66
66
|
- `.ukit/` — hidden shared runtime storage for config, cache, and cross-agent memory
|
|
67
67
|
- `docs/` — PROJECT / MEMORY / WORKLOG baseline
|
|
68
68
|
|
|
69
|
-
## UKit v1.1.
|
|
69
|
+
## UKit v1.1.7 Runtime
|
|
70
70
|
|
|
71
71
|
UKit now installs a hidden shared local runtime at `.ukit/` for production-oriented state that should survive across agent sessions:
|
|
72
72
|
|
|
@@ -83,13 +83,15 @@ When long sessions approach the compact threshold, UKit now uses a conservative
|
|
|
83
83
|
- compact only safe-zone history/noise
|
|
84
84
|
- preserve active task, rules, decisions, and current code focus
|
|
85
85
|
|
|
86
|
-
UKit v1.1.
|
|
86
|
+
UKit v1.1.7 keeps the same shared runtime contract while adding impact-confidence routing for shared runtime/template changes:
|
|
87
87
|
|
|
88
88
|
- install globally with `npm install -g @ngockhoale/ukit`
|
|
89
89
|
- keep using the exact same human workflow inside projects: `ukit install`
|
|
90
90
|
- preserve the same `ukit` binary, hooks, and install-first orchestration while standardizing the runtime root as hidden `.ukit/`
|
|
91
|
+
- route shared index/helper/hook/template edits as higher-impact work instead of treating them like ordinary simple changes
|
|
92
|
+
- surface mirror-counterpart and impact-test hints so agents verify source/runtime/template changes with better confidence
|
|
91
93
|
|
|
92
|
-
UKit v1.1.
|
|
94
|
+
UKit v1.1.7 also keeps the fast path improvements from the recent runtime releases:
|
|
93
95
|
|
|
94
96
|
- Vietnamese prompts now normalize more effectively for English-heavy code symbols and paths
|
|
95
97
|
- localized simple direct-target lanes skip extra previous-context / recent-output work when it would not change the next action
|
|
@@ -1000,6 +1000,19 @@ items:
|
|
|
1000
1000
|
packs:
|
|
1001
1001
|
- core
|
|
1002
1002
|
|
|
1003
|
+
- id: ukit-index-impact-context-script
|
|
1004
|
+
type: config
|
|
1005
|
+
sourceTemplate: .claude/ukit/index/impact-context.mjs
|
|
1006
|
+
targetPath: .claude/ukit/index/impact-context.mjs
|
|
1007
|
+
requires:
|
|
1008
|
+
- ukit-index-core-lib
|
|
1009
|
+
- ukit-index-cache-utils-script
|
|
1010
|
+
mergeStrategy: overwrite_with_backup
|
|
1011
|
+
variables: []
|
|
1012
|
+
enabledByDefault: true
|
|
1013
|
+
packs:
|
|
1014
|
+
- core
|
|
1015
|
+
|
|
1003
1016
|
- id: ukit-index-cache-utils-script
|
|
1004
1017
|
type: config
|
|
1005
1018
|
sourceTemplate: .claude/ukit/index/cache-utils.mjs
|
package/package.json
CHANGED
package/src/index/buildIndex.js
CHANGED
|
@@ -34,6 +34,9 @@ const TRACKED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'
|
|
|
34
34
|
const DISCOVERED_EXTENSIONS = new Set([...TRACKED_EXTENSIONS, ...STYLE_EXTENSIONS]);
|
|
35
35
|
export const DEFAULT_INDEX_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
36
36
|
const INDEX_PARSE_BATCH_SIZE = 8;
|
|
37
|
+
const CALL_IGNORE_WORDS = new Set([
|
|
38
|
+
'if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'typeof',
|
|
39
|
+
]);
|
|
37
40
|
|
|
38
41
|
export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
39
42
|
const absoluteRoot = path.resolve(rootDir);
|
|
@@ -89,6 +92,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
89
92
|
|
|
90
93
|
const symbols = [];
|
|
91
94
|
const imports = [];
|
|
95
|
+
const calls = [];
|
|
92
96
|
const reusableCodeFiles = [];
|
|
93
97
|
let filesToParse = [];
|
|
94
98
|
let reusedCodeFileCount = 0;
|
|
@@ -109,20 +113,24 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
if (reusableCodeFiles.length > 0) {
|
|
112
|
-
const [previousSymbolsArtifact, previousImportsArtifact] = await Promise.all([
|
|
116
|
+
const [previousSymbolsArtifact, previousImportsArtifact, previousCallsArtifact] = await Promise.all([
|
|
113
117
|
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.symbols),
|
|
114
118
|
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
119
|
+
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
115
120
|
]);
|
|
116
121
|
canReuseParsedArtifacts = previousSymbolsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
117
|
-
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
122
|
+
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
123
|
+
&& previousCallsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION;
|
|
118
124
|
|
|
119
125
|
if (canReuseParsedArtifacts) {
|
|
120
126
|
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item.filePath);
|
|
121
127
|
const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item.from);
|
|
128
|
+
const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item.filePath);
|
|
122
129
|
|
|
123
130
|
for (const filePath of reusableCodeFiles) {
|
|
124
131
|
symbols.push(...(previousSymbolsByPath.get(filePath) ?? []));
|
|
125
132
|
imports.push(...(previousImportsByPath.get(filePath) ?? []));
|
|
133
|
+
calls.push(...(previousCallsByPath.get(filePath) ?? []));
|
|
126
134
|
reusedCodeFileCount += 1;
|
|
127
135
|
}
|
|
128
136
|
} else {
|
|
@@ -145,6 +153,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
145
153
|
...extractImports(filePath, scriptContent),
|
|
146
154
|
...extractSupplementalImports(filePath, content),
|
|
147
155
|
],
|
|
156
|
+
calls: extractFunctionCalls(filePath, scriptContent),
|
|
148
157
|
};
|
|
149
158
|
} catch {
|
|
150
159
|
return null;
|
|
@@ -154,6 +163,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
154
163
|
for (const parsedFile of parsedBatch) {
|
|
155
164
|
symbols.push(...parsedFile.symbols);
|
|
156
165
|
imports.push(...parsedFile.imports);
|
|
166
|
+
calls.push(...parsedFile.calls);
|
|
157
167
|
}
|
|
158
168
|
}
|
|
159
169
|
|
|
@@ -279,6 +289,12 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
279
289
|
rootDir: absoluteRoot,
|
|
280
290
|
items: imports,
|
|
281
291
|
});
|
|
292
|
+
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.calls, {
|
|
293
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
294
|
+
generatedAt,
|
|
295
|
+
rootDir: absoluteRoot,
|
|
296
|
+
items: calls,
|
|
297
|
+
});
|
|
282
298
|
}
|
|
283
299
|
if (!canReuseTestsMap) {
|
|
284
300
|
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.testsMap, {
|
|
@@ -579,6 +595,76 @@ function extractSupplementalImports(filePath, content) {
|
|
|
579
595
|
return imports;
|
|
580
596
|
}
|
|
581
597
|
|
|
598
|
+
function extractFunctionBlocks(content) {
|
|
599
|
+
const blocks = [];
|
|
600
|
+
|
|
601
|
+
for (const match of content.matchAll(/export\s+(async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g)) {
|
|
602
|
+
blocks.push({ symbol: match[2], exported: true, bodyStart: match.index + match[0].length });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
for (const match of content.matchAll(/function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g)) {
|
|
606
|
+
const prefix = content.slice(Math.max(0, match.index - 16), match.index);
|
|
607
|
+
if (/export\s+$/.test(prefix)) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
blocks.push({ symbol: match[1], exported: false, bodyStart: match.index + match[0].length });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const match of content.matchAll(/export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*\{/g)) {
|
|
614
|
+
blocks.push({ symbol: match[1], exported: true, bodyStart: match.index + match[0].length });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
for (const match of content.matchAll(/const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*\{/g)) {
|
|
618
|
+
const prefix = content.slice(Math.max(0, match.index - 16), match.index);
|
|
619
|
+
if (/export\s+$/.test(prefix)) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
blocks.push({ symbol: match[1], exported: false, bodyStart: match.index + match[0].length });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return blocks
|
|
626
|
+
.map((block) => ({ ...block, body: sliceBalancedBlock(content, block.bodyStart) }))
|
|
627
|
+
.filter((block) => block.body);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function sliceBalancedBlock(content, startIndex) {
|
|
631
|
+
let depth = 1;
|
|
632
|
+
for (let index = startIndex; index < content.length; index += 1) {
|
|
633
|
+
const char = content[index];
|
|
634
|
+
if (char === '{') {
|
|
635
|
+
depth += 1;
|
|
636
|
+
}
|
|
637
|
+
if (char === '}') {
|
|
638
|
+
depth -= 1;
|
|
639
|
+
}
|
|
640
|
+
if (depth === 0) {
|
|
641
|
+
return content.slice(startIndex, index);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return '';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractFunctionCalls(filePath, content) {
|
|
648
|
+
return extractFunctionBlocks(content).map((block) => {
|
|
649
|
+
const calls = [];
|
|
650
|
+
for (const match of block.body.matchAll(/(?:\b|\.)([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g)) {
|
|
651
|
+
const name = match[1];
|
|
652
|
+
if (!name || CALL_IGNORE_WORDS.has(name)) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (!calls.includes(name)) {
|
|
656
|
+
calls.push(name);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
filePath,
|
|
661
|
+
symbol: block.symbol,
|
|
662
|
+
exported: block.exported,
|
|
663
|
+
calls,
|
|
664
|
+
};
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
582
668
|
function buildTestsMap(fileRecords) {
|
|
583
669
|
const testFiles = fileRecords.filter((file) => isLikelyTestFile(file.filePath));
|
|
584
670
|
const sourceFiles = fileRecords.filter((file) => CODE_EXTENSIONS.has(file.ext) && !isLikelyTestFile(file.filePath));
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const SHARED_IMPACT_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
regex: /^src\/index\//,
|
|
4
|
+
label: 'shared-index-runtime',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
regex: /^src\/core\/(runInstallPipeline|applyPlan|buildPlan|diffPlan|metadata|migrateLegacy|uninstall|runtimeConfig|runtimePaths)\.js$/,
|
|
8
|
+
label: 'shared-install-runtime',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
regex: /^src\/core\/(output|token|compact)\//,
|
|
12
|
+
label: 'shared-token-runtime',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
regex: /^templates\/\.claude\/hooks\//,
|
|
16
|
+
label: 'installed-hook-runtime',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
regex: /^templates\/\.claude\/ukit\//,
|
|
20
|
+
label: 'installed-helper-runtime',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
regex: /^manifests\/platform\.full\.yaml$/,
|
|
24
|
+
label: 'install-manifest',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
regex: /^templates\//,
|
|
28
|
+
label: 'published-template-surface',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const MIRROR_PAIRS = [
|
|
33
|
+
{
|
|
34
|
+
source: 'src/index/buildIndex.js',
|
|
35
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
36
|
+
reason: 'installed index-core mirror embeds source index behavior',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
source: 'src/index/queryIndex.js',
|
|
40
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
41
|
+
reason: 'installed index-core mirror embeds source query behavior',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
source: 'src/index/resolveContext.js',
|
|
45
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
46
|
+
reason: 'installed index-core mirror embeds context resolution behavior',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
source: 'src/index/verificationPlan.js',
|
|
50
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
51
|
+
reason: 'installed index-core mirror embeds verification planning behavior',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
source: 'src/index/taskRouting.js',
|
|
55
|
+
mirror: 'templates/.claude/ukit/index/route-task.mjs',
|
|
56
|
+
reason: 'installed route-task helper mirrors source task routing behavior',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
source: 'src/index/routeCatalog.js',
|
|
60
|
+
mirror: 'templates/.claude/ukit/index/route-catalog.mjs',
|
|
61
|
+
reason: 'installed route catalog must match source route catalog',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
source: 'src/core/output/index.js',
|
|
65
|
+
mirror: 'templates/.claude/ukit/runtime/output-compression.mjs',
|
|
66
|
+
reason: 'source output compression runtime should stay aligned with installed hook runtime',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
source: 'src/core/token/index.js',
|
|
70
|
+
mirror: 'templates/.claude/ukit/runtime/token-utils.mjs',
|
|
71
|
+
reason: 'source token runtime should stay aligned with installed token utilities',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
source: 'src/core/compact/threshold.js',
|
|
75
|
+
mirror: 'templates/.claude/ukit/runtime/compact-threshold.mjs',
|
|
76
|
+
reason: 'source compact threshold runtime should stay aligned with installed compact threshold hook',
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function normalizePath(filePath) {
|
|
81
|
+
return String(filePath ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isSharedImpactFile(filePath) {
|
|
85
|
+
const normalized = normalizePath(filePath);
|
|
86
|
+
return SHARED_IMPACT_PATTERNS.some((entry) => entry.regex.test(normalized));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function classifyImpactRisk(filePaths = []) {
|
|
90
|
+
const normalizedPaths = [...new Set(filePaths.map(normalizePath).filter(Boolean))];
|
|
91
|
+
const riskLabels = [];
|
|
92
|
+
|
|
93
|
+
for (const filePath of normalizedPaths) {
|
|
94
|
+
for (const entry of SHARED_IMPACT_PATTERNS) {
|
|
95
|
+
if (entry.regex.test(filePath) && !riskLabels.includes(entry.label)) {
|
|
96
|
+
riskLabels.push(entry.label);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
changedFiles: normalizedPaths,
|
|
103
|
+
riskLabels,
|
|
104
|
+
requiresImpactCheck: riskLabels.length > 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function findMirrorCounterparts(filePaths = []) {
|
|
109
|
+
const normalizedSet = new Set(filePaths.map(normalizePath).filter(Boolean));
|
|
110
|
+
const counterparts = [];
|
|
111
|
+
|
|
112
|
+
for (const pair of MIRROR_PAIRS) {
|
|
113
|
+
if (normalizedSet.has(pair.source)) {
|
|
114
|
+
counterparts.push({ filePath: pair.mirror, reason: pair.reason, source: pair.source });
|
|
115
|
+
}
|
|
116
|
+
if (normalizedSet.has(pair.mirror)) {
|
|
117
|
+
counterparts.push({ filePath: pair.source, reason: pair.reason, source: pair.mirror });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return counterparts.filter((entry, index, list) => (
|
|
122
|
+
list.findIndex((candidate) => candidate.filePath === entry.filePath && candidate.source === entry.source) === index
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { MIRROR_PAIRS, SHARED_IMPACT_PATTERNS };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getArtifactPath, INDEX_ARTIFACTS } from './paths.js';
|
|
5
|
+
import { classifyImpactRisk, findMirrorCounterparts } from './impactCatalog.js';
|
|
6
|
+
import { inferRelatedTestsFromArtifacts, loadRelatedTestArtifacts } from './relatedTests.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_IMPACT_LIMITS = {
|
|
9
|
+
maxCallers: 8,
|
|
10
|
+
maxCallees: 8,
|
|
11
|
+
maxImporters: 8,
|
|
12
|
+
maxTests: 8,
|
|
13
|
+
maxMirrors: 4,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const RISK_TEST_RECOMMENDATIONS = {
|
|
17
|
+
'shared-index-runtime': ['tests/index/indexing.test.js', 'tests/index/taskRouting.test.js'],
|
|
18
|
+
'shared-install-runtime': ['tests/integration/installPipeline.test.js'],
|
|
19
|
+
'installed-hook-runtime': ['tests/hooks/skillRouterHook.test.js', 'tests/integration/installPipeline.test.js'],
|
|
20
|
+
'published-template-surface': ['tests/integration/packageArtifact.test.js', 'tests/integration/installPipeline.test.js'],
|
|
21
|
+
'install-manifest': ['tests/integration/packageArtifact.test.js', 'tests/integration/installPipeline.test.js'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function readArtifact(rootDir, artifactName) {
|
|
25
|
+
try {
|
|
26
|
+
const raw = await fs.readFile(getArtifactPath(path.resolve(rootDir), artifactName), 'utf8');
|
|
27
|
+
return JSON.parse(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
return { items: [] };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizePath(filePath) {
|
|
34
|
+
return String(filePath ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function unique(values = []) {
|
|
38
|
+
return [...new Set(values.filter(Boolean))];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveImportTarget(fromFilePath, specifier) {
|
|
42
|
+
const from = normalizePath(fromFilePath);
|
|
43
|
+
const to = String(specifier ?? '').trim();
|
|
44
|
+
if (!from || !to || !to.startsWith('.')) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fromDir = path.posix.dirname(from);
|
|
49
|
+
const base = path.posix.normalize(path.posix.join(fromDir, to));
|
|
50
|
+
const candidates = [
|
|
51
|
+
base,
|
|
52
|
+
`${base}.js`,
|
|
53
|
+
`${base}.mjs`,
|
|
54
|
+
`${base}.cjs`,
|
|
55
|
+
`${base}.ts`,
|
|
56
|
+
`${base}.tsx`,
|
|
57
|
+
`${base}.jsx`,
|
|
58
|
+
`${base}.vue`,
|
|
59
|
+
path.posix.join(base, 'index.js'),
|
|
60
|
+
path.posix.join(base, 'index.ts'),
|
|
61
|
+
path.posix.join(base, 'index.mjs'),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
return unique(candidates);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolveImpactContext({
|
|
68
|
+
rootDir = process.cwd(),
|
|
69
|
+
changedFiles = [],
|
|
70
|
+
changedSymbols = [],
|
|
71
|
+
limits = {},
|
|
72
|
+
} = {}) {
|
|
73
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
74
|
+
const budget = { ...DEFAULT_IMPACT_LIMITS, ...(limits ?? {}) };
|
|
75
|
+
const normalizedChangedFiles = unique(changedFiles.map(normalizePath));
|
|
76
|
+
const normalizedChangedSymbols = unique(changedSymbols.map((symbol) => String(symbol ?? '').trim()));
|
|
77
|
+
|
|
78
|
+
const [callsArtifact, importsArtifact, relatedArtifacts] = await Promise.all([
|
|
79
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
80
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
81
|
+
loadRelatedTestArtifacts({ rootDir: absoluteRoot }),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const callItems = callsArtifact.items ?? [];
|
|
85
|
+
const importItems = importsArtifact.items ?? [];
|
|
86
|
+
const risk = classifyImpactRisk(normalizedChangedFiles);
|
|
87
|
+
const mirrorCounterparts = findMirrorCounterparts(normalizedChangedFiles).slice(0, budget.maxMirrors);
|
|
88
|
+
|
|
89
|
+
const changedFileSet = new Set(normalizedChangedFiles);
|
|
90
|
+
const changedSymbolSet = new Set(normalizedChangedSymbols);
|
|
91
|
+
|
|
92
|
+
const changedRecords = callItems.filter((item) => {
|
|
93
|
+
const fileMatched = changedFileSet.has(normalizePath(item.filePath));
|
|
94
|
+
if (!fileMatched) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (changedSymbolSet.size === 0) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return changedSymbolSet.has(item.symbol);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const callees = [];
|
|
104
|
+
for (const record of changedRecords) {
|
|
105
|
+
for (const symbol of record.calls ?? []) {
|
|
106
|
+
callees.push({ filePath: record.filePath, fromSymbol: record.symbol, symbol });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const callers = [];
|
|
111
|
+
const callerTargets = new Set([
|
|
112
|
+
...normalizedChangedSymbols,
|
|
113
|
+
...changedRecords.map((record) => record.symbol),
|
|
114
|
+
]);
|
|
115
|
+
if (callerTargets.size > 0) {
|
|
116
|
+
for (const record of callItems) {
|
|
117
|
+
const matchedSymbol = (record.calls ?? []).find((name) => callerTargets.has(name));
|
|
118
|
+
if (!matchedSymbol) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
changedFileSet.has(normalizePath(record.filePath))
|
|
124
|
+
&& changedSymbolSet.has(record.symbol)
|
|
125
|
+
) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
callers.push({ filePath: record.filePath, symbol: record.symbol, reason: `calls ${matchedSymbol}` });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const importers = [];
|
|
134
|
+
const dependencies = [];
|
|
135
|
+
for (const imp of importItems) {
|
|
136
|
+
const from = normalizePath(imp.from);
|
|
137
|
+
const targets = resolveImportTarget(from, imp.to) ?? [];
|
|
138
|
+
const matched = targets.find((candidate) => changedFileSet.has(normalizePath(candidate)));
|
|
139
|
+
|
|
140
|
+
if (matched) {
|
|
141
|
+
importers.push(from);
|
|
142
|
+
dependencies.push(matched);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const candidateFiles = unique([
|
|
147
|
+
...normalizedChangedFiles,
|
|
148
|
+
...importers,
|
|
149
|
+
...callers.map((item) => item.filePath),
|
|
150
|
+
...mirrorCounterparts.map((item) => item.filePath),
|
|
151
|
+
]);
|
|
152
|
+
const relatedTests = inferRelatedTestsFromArtifacts({
|
|
153
|
+
candidateFiles,
|
|
154
|
+
analogsMap: relatedArtifacts.analogsMap,
|
|
155
|
+
relationsMap: relatedArtifacts.relationsMap,
|
|
156
|
+
limit: budget.maxTests,
|
|
157
|
+
}).map((entry) => entry.filePath);
|
|
158
|
+
|
|
159
|
+
const recommendedTestFiles = unique([
|
|
160
|
+
...relatedTests,
|
|
161
|
+
...risk.riskLabels.flatMap((label) => RISK_TEST_RECOMMENDATIONS[label] ?? []),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
changedFiles: normalizedChangedFiles,
|
|
166
|
+
changedSymbols: normalizedChangedSymbols,
|
|
167
|
+
riskLabels: risk.riskLabels,
|
|
168
|
+
requiresImpactCheck: risk.requiresImpactCheck,
|
|
169
|
+
callees: unique(callees.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallees),
|
|
170
|
+
callers: unique(callers.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallers),
|
|
171
|
+
importers: unique(importers).slice(0, budget.maxImporters),
|
|
172
|
+
dependencies: unique(dependencies).slice(0, budget.maxImporters),
|
|
173
|
+
mirrorCounterparts,
|
|
174
|
+
relatedTests,
|
|
175
|
+
recommendedTestFiles,
|
|
176
|
+
explanations: {
|
|
177
|
+
callees: changedRecords.map((record) => `${record.filePath}:${record.symbol}`),
|
|
178
|
+
callers: callers.map((entry) => `${entry.filePath}:${entry.symbol}`),
|
|
179
|
+
importers,
|
|
180
|
+
relatedTests,
|
|
181
|
+
mirrorCounterparts: mirrorCounterparts.map((entry) => `${entry.source} -> ${entry.filePath}`),
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatImpactConfidenceSummary(impact = {}) {
|
|
187
|
+
const lines = ['Impact checked:'];
|
|
188
|
+
|
|
189
|
+
if ((impact.changedFiles ?? []).length > 0) {
|
|
190
|
+
lines.push(`- Changed: ${(impact.changedFiles ?? []).slice(0, 3).join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
if ((impact.changedSymbols ?? []).length > 0) {
|
|
193
|
+
lines.push(`- Symbols: ${(impact.changedSymbols ?? []).slice(0, 5).join(', ')}`);
|
|
194
|
+
}
|
|
195
|
+
if ((impact.riskLabels ?? []).length > 0) {
|
|
196
|
+
lines.push(`- Risk: ${(impact.riskLabels ?? []).join(', ')}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const calleeSymbols = (impact.callees ?? []).map((item) => item?.symbol).filter(Boolean).slice(0, 5);
|
|
200
|
+
if (calleeSymbols.length > 0) {
|
|
201
|
+
lines.push(`- Callees: ${calleeSymbols.join(', ')}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const callerFiles = (impact.callers ?? []).map((item) => item?.filePath).filter(Boolean).slice(0, 5);
|
|
205
|
+
if (callerFiles.length > 0) {
|
|
206
|
+
lines.push(`- Callers: ${callerFiles.join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const importers = (impact.importers ?? []).slice(0, 5);
|
|
210
|
+
if (importers.length > 0) {
|
|
211
|
+
lines.push(`- Importers: ${importers.join(', ')}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const mirrors = (impact.mirrorCounterparts ?? []).map((item) => item?.filePath).filter(Boolean).slice(0, 3);
|
|
215
|
+
if (mirrors.length > 0) {
|
|
216
|
+
lines.push(`- Mirrors: ${mirrors.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const relatedTests = (impact.relatedTests ?? []).slice(0, 4);
|
|
220
|
+
if (relatedTests.length > 0) {
|
|
221
|
+
lines.push(`- Related tests: ${relatedTests.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const recommendedTests = (impact.recommendedTestFiles ?? []).slice(0, 4);
|
|
225
|
+
if (recommendedTests.length > 0) {
|
|
226
|
+
lines.push(`- Recommended tests: ${recommendedTests.join(', ')}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return lines.join('\n');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export { DEFAULT_IMPACT_LIMITS };
|
package/src/index/paths.js
CHANGED
|
@@ -4,6 +4,7 @@ import { inferRelatedTestsFromArtifacts, loadRelatedTestArtifacts } from './rela
|
|
|
4
4
|
const TASK_TYPE_BUDGETS = {
|
|
5
5
|
trivial: { minFiles: 1, maxFiles: 2 },
|
|
6
6
|
simple: { minFiles: 2, maxFiles: 5 },
|
|
7
|
+
'shared-simple': { minFiles: 3, maxFiles: 8 },
|
|
7
8
|
'non-trivial': { minFiles: 4, maxFiles: 8 },
|
|
8
9
|
};
|
|
9
10
|
|
package/src/index/taskRouting.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ROUTE_CATALOG } from './routeCatalog.js';
|
|
|
5
5
|
import { buildRouteSignalText } from './languageTools.js';
|
|
6
6
|
import { resolveContext } from './resolveContext.js';
|
|
7
7
|
import { deriveVerificationPlan } from './verificationPlan.js';
|
|
8
|
+
import { isSharedImpactFile } from './impactCatalog.js';
|
|
8
9
|
|
|
9
10
|
const MAX_ACTIVE_ROUTE_SKILLS = 2;
|
|
10
11
|
|
|
@@ -42,6 +43,7 @@ export async function deriveTaskRoute({
|
|
|
42
43
|
promptText: normalizedPrompt,
|
|
43
44
|
commandText: normalizedCommand,
|
|
44
45
|
selectedIds,
|
|
46
|
+
targetFile: normalizedTarget,
|
|
45
47
|
});
|
|
46
48
|
const preservedPrompt = normalizedPrompt || String(lastExplicitUserPromptText || '').trim();
|
|
47
49
|
const contextResult = useIndexedContext && (contextIntent || normalizedTarget)
|
|
@@ -531,7 +533,7 @@ function deriveContextIntent({ promptText, commandText, targetFile, selectedIds
|
|
|
531
533
|
return '';
|
|
532
534
|
}
|
|
533
535
|
|
|
534
|
-
function inferTaskType({ promptText, commandText, selectedIds }) {
|
|
536
|
+
function inferTaskType({ promptText, commandText, selectedIds, targetFile = null }) {
|
|
535
537
|
const lower = buildRouteSignalText(promptText, commandText);
|
|
536
538
|
|
|
537
539
|
if (
|
|
@@ -542,11 +544,16 @@ function inferTaskType({ promptText, commandText, selectedIds }) {
|
|
|
542
544
|
return 'non-trivial';
|
|
543
545
|
}
|
|
544
546
|
|
|
547
|
+
let inferred = 'simple';
|
|
545
548
|
if (/\b(typo|label|text|rename|color|spacing|toggle|comment)\b/.test(lower)) {
|
|
546
|
-
|
|
549
|
+
inferred = 'trivial';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if ((inferred === 'simple' || inferred === 'trivial') && isSharedImpactFile(targetFile)) {
|
|
553
|
+
return 'shared-simple';
|
|
547
554
|
}
|
|
548
555
|
|
|
549
|
-
return
|
|
556
|
+
return inferred;
|
|
550
557
|
}
|
|
551
558
|
|
|
552
559
|
function shellEscape(str) {
|