@ngockhoale/ukit 1.1.6 → 1.1.8
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 +22 -0
- package/README.md +9 -4
- package/manifests/platform.full.yaml +55 -0
- package/package.json +1 -1
- package/src/cli/commands/doctor.js +2 -0
- package/src/cli/commands/install.js +3 -2
- package/src/cli/commands/uninstall.js +1 -1
- package/src/core/runtimeConfig.js +1 -1
- package/src/core/uninstall.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/routeCatalog.js +24 -2
- package/src/index/taskRouting.js +147 -4
- 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/skills/docs-quality/SKILL.md +9 -1
- package/templates/.claude/skills/next-step/SKILL.md +78 -0
- package/templates/.claude/skills/update-status/SKILL.md +88 -0
- 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-catalog.mjs +24 -2
- package/templates/.claude/ukit/index/route-task.mjs +166 -4
- package/templates/.codex/README.md +6 -1
- package/templates/.codex/settings.json +8 -1
- package/templates/AGENTS.md +12 -1
- package/templates/CLAUDE.md +12 -1
- package/templates/docs/INSTALL.md +2 -0
- package/templates/docs/PROJECT.md +5 -4
- package/templates/docs/STATUS.md +81 -0
- package/templates/docs/UKIT_USAGE_GUIDE.md +16 -0
- package/templates/ukit/README.md +1 -1
- package/templates/ukit/storage/config.json +1 -1
|
@@ -31,12 +31,16 @@ const INDEX_SCHEMA_VERSION = 6;
|
|
|
31
31
|
export const DEFAULT_INDEX_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
32
32
|
const INDEX_PARSE_BATCH_SIZE = 8;
|
|
33
33
|
const MAX_IMPORTER_HOPS = 2;
|
|
34
|
+
const CALL_IGNORE_WORDS = new Set([
|
|
35
|
+
'if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'typeof',
|
|
36
|
+
]);
|
|
34
37
|
|
|
35
38
|
export const INDEX_ARTIFACTS = {
|
|
36
39
|
meta: 'meta.json',
|
|
37
40
|
files: 'files.json',
|
|
38
41
|
symbols: 'symbols.json',
|
|
39
42
|
imports: 'imports.json',
|
|
43
|
+
calls: 'calls.json',
|
|
40
44
|
testsMap: 'tests-map.json',
|
|
41
45
|
hotspots: 'hotspots.json',
|
|
42
46
|
archetypes: 'archetypes.json',
|
|
@@ -159,6 +163,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
159
163
|
const codeFiles = fileRecords.filter((f) => CODE_EXTENSIONS.has(f.ext));
|
|
160
164
|
const symbols = [];
|
|
161
165
|
const imports = [];
|
|
166
|
+
const calls = [];
|
|
162
167
|
const reusableCodeFiles = [];
|
|
163
168
|
let filesToParse = [];
|
|
164
169
|
let reusedCodeFileCount = 0;
|
|
@@ -179,20 +184,24 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
if (reusableCodeFiles.length > 0) {
|
|
182
|
-
const [previousSymbolsArtifact, previousImportsArtifact] = await Promise.all([
|
|
187
|
+
const [previousSymbolsArtifact, previousImportsArtifact, previousCallsArtifact] = await Promise.all([
|
|
183
188
|
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.symbols),
|
|
184
189
|
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
190
|
+
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
185
191
|
]);
|
|
186
192
|
canReuseParsedArtifacts = previousSymbolsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
187
|
-
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
193
|
+
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
194
|
+
&& previousCallsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION;
|
|
188
195
|
|
|
189
196
|
if (canReuseParsedArtifacts) {
|
|
190
197
|
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item.filePath);
|
|
191
198
|
const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item.from);
|
|
199
|
+
const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item.filePath);
|
|
192
200
|
|
|
193
201
|
for (const filePath of reusableCodeFiles) {
|
|
194
202
|
symbols.push(...(previousSymbolsByPath.get(filePath) ?? []));
|
|
195
203
|
imports.push(...(previousImportsByPath.get(filePath) ?? []));
|
|
204
|
+
calls.push(...(previousCallsByPath.get(filePath) ?? []));
|
|
196
205
|
reusedCodeFileCount += 1;
|
|
197
206
|
}
|
|
198
207
|
} else {
|
|
@@ -215,6 +224,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
215
224
|
...extractImports(filePath, scriptContent),
|
|
216
225
|
...extractSupplementalImports(filePath, content),
|
|
217
226
|
],
|
|
227
|
+
calls: extractFunctionCalls(filePath, scriptContent),
|
|
218
228
|
};
|
|
219
229
|
} catch {
|
|
220
230
|
return null;
|
|
@@ -224,6 +234,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
224
234
|
for (const parsedFile of parsedBatch) {
|
|
225
235
|
symbols.push(...parsedFile.symbols);
|
|
226
236
|
imports.push(...parsedFile.imports);
|
|
237
|
+
calls.push(...parsedFile.calls);
|
|
227
238
|
}
|
|
228
239
|
}
|
|
229
240
|
|
|
@@ -329,6 +340,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
329
340
|
if (!canReuseAllParsedArtifacts) {
|
|
330
341
|
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.symbols, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: symbols });
|
|
331
342
|
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.imports, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: imports });
|
|
343
|
+
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.calls, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: calls });
|
|
332
344
|
}
|
|
333
345
|
if (!canReuseTestsMap) {
|
|
334
346
|
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.testsMap, { schemaVersion: INDEX_SCHEMA_VERSION, generatedAt, items: testsMap });
|
|
@@ -559,6 +571,7 @@ function escapeJsonString(value) {
|
|
|
559
571
|
const TASK_TYPE_BUDGETS = {
|
|
560
572
|
trivial: { minFiles: 1, maxFiles: 2 },
|
|
561
573
|
simple: { minFiles: 2, maxFiles: 5 },
|
|
574
|
+
'shared-simple': { minFiles: 3, maxFiles: 8 },
|
|
562
575
|
'non-trivial': { minFiles: 4, maxFiles: 8 },
|
|
563
576
|
};
|
|
564
577
|
|
|
@@ -1663,6 +1676,69 @@ function extractSupplementalImports(filePath, content) {
|
|
|
1663
1676
|
return out;
|
|
1664
1677
|
}
|
|
1665
1678
|
|
|
1679
|
+
function extractFunctionBlocks(content) {
|
|
1680
|
+
const blocks = [];
|
|
1681
|
+
const seenStarts = new Set();
|
|
1682
|
+
|
|
1683
|
+
for (const match of content.matchAll(/export\s+(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g)) {
|
|
1684
|
+
blocks.push({ symbol: match[1], exported: true, bodyStart: match.index + match[0].length });
|
|
1685
|
+
seenStarts.add(match.index);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
for (const match of content.matchAll(/(?:^|[^\w$])(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g)) {
|
|
1689
|
+
if (seenStarts.has(match.index + (match[1] ? 0 : 0))) {
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
blocks.push({ symbol: match[1], exported: false, bodyStart: match.index + match[0].length });
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
for (const match of content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*\{/g)) {
|
|
1696
|
+
blocks.push({ symbol: match[1], exported: /^export\s+/.test(match[0]), bodyStart: match.index + match[0].length });
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
return blocks
|
|
1700
|
+
.map((block) => ({ ...block, body: sliceBalancedBlock(content, block.bodyStart) }))
|
|
1701
|
+
.filter((block) => block.body);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function sliceBalancedBlock(content, startIndex) {
|
|
1705
|
+
let depth = 1;
|
|
1706
|
+
for (let index = startIndex; index < content.length; index += 1) {
|
|
1707
|
+
const char = content[index];
|
|
1708
|
+
if (char === '{') {
|
|
1709
|
+
depth += 1;
|
|
1710
|
+
}
|
|
1711
|
+
if (char === '}') {
|
|
1712
|
+
depth -= 1;
|
|
1713
|
+
}
|
|
1714
|
+
if (depth === 0) {
|
|
1715
|
+
return content.slice(startIndex, index);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return '';
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function extractFunctionCalls(filePath, content) {
|
|
1722
|
+
return extractFunctionBlocks(content).map((block) => {
|
|
1723
|
+
const calls = [];
|
|
1724
|
+
for (const match of block.body.matchAll(/(?:\b|\.)([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g)) {
|
|
1725
|
+
const name = match[1];
|
|
1726
|
+
if (!name || CALL_IGNORE_WORDS.has(name)) {
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
if (!calls.includes(name)) {
|
|
1730
|
+
calls.push(name);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return {
|
|
1734
|
+
filePath,
|
|
1735
|
+
symbol: block.symbol,
|
|
1736
|
+
exported: block.exported,
|
|
1737
|
+
calls,
|
|
1738
|
+
};
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1666
1742
|
function buildTestsMap(fileRecords) {
|
|
1667
1743
|
const tests = fileRecords.filter((x) => isLikelyTestFile(x.filePath));
|
|
1668
1744
|
const sources = fileRecords.filter((x) => CODE_EXTENSIONS.has(x.ext) && !isLikelyTestFile(x.filePath));
|
|
@@ -2751,6 +2827,280 @@ async function detectPackageManager(rootDir) {
|
|
|
2751
2827
|
return 'npm';
|
|
2752
2828
|
}
|
|
2753
2829
|
|
|
2830
|
+
const IMPACT_SHARED_PATTERNS = [
|
|
2831
|
+
{ regex: /^src\/index\//, label: 'shared-index-runtime' },
|
|
2832
|
+
{
|
|
2833
|
+
regex: /^src\/core\/(runInstallPipeline|applyPlan|buildPlan|diffPlan|metadata|migrateLegacy|uninstall|runtimeConfig|runtimePaths)\.js$/,
|
|
2834
|
+
label: 'shared-install-runtime',
|
|
2835
|
+
},
|
|
2836
|
+
{ regex: /^src\/core\/(output|token|compact)\//, label: 'shared-token-runtime' },
|
|
2837
|
+
{ regex: /^templates\/\.claude\/hooks\//, label: 'installed-hook-runtime' },
|
|
2838
|
+
{ regex: /^templates\/\.claude\/ukit\//, label: 'installed-helper-runtime' },
|
|
2839
|
+
{ regex: /^manifests\/platform\.full\.yaml$/, label: 'install-manifest' },
|
|
2840
|
+
{ regex: /^templates\//, label: 'published-template-surface' },
|
|
2841
|
+
];
|
|
2842
|
+
|
|
2843
|
+
const IMPACT_MIRROR_PAIRS = [
|
|
2844
|
+
{
|
|
2845
|
+
source: 'src/index/buildIndex.js',
|
|
2846
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
2847
|
+
reason: 'installed index-core mirror embeds source index behavior',
|
|
2848
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
source: 'src/index/queryIndex.js',
|
|
2851
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
2852
|
+
reason: 'installed index-core mirror embeds source query behavior',
|
|
2853
|
+
},
|
|
2854
|
+
{
|
|
2855
|
+
source: 'src/index/resolveContext.js',
|
|
2856
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
2857
|
+
reason: 'installed index-core mirror embeds context resolution behavior',
|
|
2858
|
+
},
|
|
2859
|
+
{
|
|
2860
|
+
source: 'src/index/verificationPlan.js',
|
|
2861
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
2862
|
+
reason: 'installed index-core mirror embeds verification planning behavior',
|
|
2863
|
+
},
|
|
2864
|
+
{
|
|
2865
|
+
source: 'src/index/taskRouting.js',
|
|
2866
|
+
mirror: 'templates/.claude/ukit/index/route-task.mjs',
|
|
2867
|
+
reason: 'installed route-task helper mirrors source task routing behavior',
|
|
2868
|
+
},
|
|
2869
|
+
{
|
|
2870
|
+
source: 'src/index/routeCatalog.js',
|
|
2871
|
+
mirror: 'templates/.claude/ukit/index/route-catalog.mjs',
|
|
2872
|
+
reason: 'installed route catalog must match source route catalog',
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
source: 'src/core/output/index.js',
|
|
2876
|
+
mirror: 'templates/.claude/ukit/runtime/output-compression.mjs',
|
|
2877
|
+
reason: 'source output compression runtime should stay aligned with installed hook runtime',
|
|
2878
|
+
},
|
|
2879
|
+
{
|
|
2880
|
+
source: 'src/core/token/index.js',
|
|
2881
|
+
mirror: 'templates/.claude/ukit/runtime/token-utils.mjs',
|
|
2882
|
+
reason: 'source token runtime should stay aligned with installed token utilities',
|
|
2883
|
+
},
|
|
2884
|
+
{
|
|
2885
|
+
source: 'src/core/compact/threshold.js',
|
|
2886
|
+
mirror: 'templates/.claude/ukit/runtime/compact-threshold.mjs',
|
|
2887
|
+
reason: 'source compact threshold runtime should stay aligned with installed compact threshold hook',
|
|
2888
|
+
},
|
|
2889
|
+
];
|
|
2890
|
+
|
|
2891
|
+
const IMPACT_RISK_TEST_RECOMMENDATIONS = {
|
|
2892
|
+
'shared-index-runtime': ['tests/index/indexing.test.js', 'tests/index/taskRouting.test.js'],
|
|
2893
|
+
'shared-install-runtime': ['tests/integration/installPipeline.test.js'],
|
|
2894
|
+
'installed-hook-runtime': ['tests/hooks/skillRouterHook.test.js', 'tests/integration/installPipeline.test.js'],
|
|
2895
|
+
'published-template-surface': ['tests/integration/packageArtifact.test.js', 'tests/integration/installPipeline.test.js'],
|
|
2896
|
+
'install-manifest': ['tests/integration/packageArtifact.test.js', 'tests/integration/installPipeline.test.js'],
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
const DEFAULT_IMPACT_LIMITS = {
|
|
2900
|
+
maxCallers: 8,
|
|
2901
|
+
maxCallees: 8,
|
|
2902
|
+
maxImporters: 8,
|
|
2903
|
+
maxTests: 8,
|
|
2904
|
+
maxMirrors: 4,
|
|
2905
|
+
};
|
|
2906
|
+
|
|
2907
|
+
function normalizeImpactPath(filePath) {
|
|
2908
|
+
return String(filePath ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
function uniqueImpact(values = []) {
|
|
2912
|
+
return [...new Set(values.filter(Boolean))];
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
function classifyImpactRisk(filePaths = []) {
|
|
2916
|
+
const normalizedPaths = uniqueImpact(filePaths.map(normalizeImpactPath));
|
|
2917
|
+
const riskLabels = [];
|
|
2918
|
+
|
|
2919
|
+
for (const filePath of normalizedPaths) {
|
|
2920
|
+
for (const entry of IMPACT_SHARED_PATTERNS) {
|
|
2921
|
+
if (entry.regex.test(filePath) && !riskLabels.includes(entry.label)) {
|
|
2922
|
+
riskLabels.push(entry.label);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
return {
|
|
2928
|
+
changedFiles: normalizedPaths,
|
|
2929
|
+
riskLabels,
|
|
2930
|
+
requiresImpactCheck: riskLabels.length > 0,
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
function findMirrorCounterparts(filePaths = []) {
|
|
2935
|
+
const normalizedSet = new Set(filePaths.map(normalizeImpactPath).filter(Boolean));
|
|
2936
|
+
const counterparts = [];
|
|
2937
|
+
|
|
2938
|
+
for (const pair of IMPACT_MIRROR_PAIRS) {
|
|
2939
|
+
if (normalizedSet.has(pair.source)) {
|
|
2940
|
+
counterparts.push({ filePath: pair.mirror, reason: pair.reason, source: pair.source });
|
|
2941
|
+
}
|
|
2942
|
+
if (normalizedSet.has(pair.mirror)) {
|
|
2943
|
+
counterparts.push({ filePath: pair.source, reason: pair.reason, source: pair.mirror });
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
return counterparts.filter((entry, index, list) => (
|
|
2948
|
+
list.findIndex((candidate) => candidate.filePath === entry.filePath && candidate.source === entry.source) === index
|
|
2949
|
+
));
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
function resolveImpactImportTargets(fromFilePath, specifier) {
|
|
2953
|
+
const from = normalizeImpactPath(fromFilePath);
|
|
2954
|
+
const to = String(specifier ?? '').trim();
|
|
2955
|
+
if (!from || !to || !to.startsWith('.')) {
|
|
2956
|
+
return [];
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
const fromDir = path.posix.dirname(from);
|
|
2960
|
+
const base = path.posix.normalize(path.posix.join(fromDir, to));
|
|
2961
|
+
return uniqueImpact([
|
|
2962
|
+
base,
|
|
2963
|
+
`${base}.js`,
|
|
2964
|
+
`${base}.mjs`,
|
|
2965
|
+
`${base}.cjs`,
|
|
2966
|
+
`${base}.ts`,
|
|
2967
|
+
`${base}.tsx`,
|
|
2968
|
+
`${base}.jsx`,
|
|
2969
|
+
`${base}.vue`,
|
|
2970
|
+
path.posix.join(base, 'index.js'),
|
|
2971
|
+
path.posix.join(base, 'index.ts'),
|
|
2972
|
+
path.posix.join(base, 'index.mjs'),
|
|
2973
|
+
]);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
export async function resolveImpactContext({
|
|
2977
|
+
rootDir = process.cwd(),
|
|
2978
|
+
changedFiles = [],
|
|
2979
|
+
changedSymbols = [],
|
|
2980
|
+
limits = {},
|
|
2981
|
+
} = {}) {
|
|
2982
|
+
const absoluteRoot = path.resolve(rootDir);
|
|
2983
|
+
const budget = { ...DEFAULT_IMPACT_LIMITS, ...(limits ?? {}) };
|
|
2984
|
+
const normalizedChangedFiles = uniqueImpact(changedFiles.map(normalizeImpactPath));
|
|
2985
|
+
const normalizedChangedSymbols = uniqueImpact(changedSymbols.map((symbol) => String(symbol ?? '').trim()));
|
|
2986
|
+
|
|
2987
|
+
const [callsArtifact, importsArtifact, relatedArtifacts] = await Promise.all([
|
|
2988
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
2989
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
2990
|
+
loadRelatedTestArtifacts({ rootDir: absoluteRoot }),
|
|
2991
|
+
]);
|
|
2992
|
+
|
|
2993
|
+
const callItems = callsArtifact.items ?? [];
|
|
2994
|
+
const importItems = importsArtifact.items ?? [];
|
|
2995
|
+
const risk = classifyImpactRisk(normalizedChangedFiles);
|
|
2996
|
+
const mirrorCounterparts = findMirrorCounterparts(normalizedChangedFiles).slice(0, budget.maxMirrors);
|
|
2997
|
+
const changedFileSet = new Set(normalizedChangedFiles);
|
|
2998
|
+
const changedSymbolSet = new Set(normalizedChangedSymbols);
|
|
2999
|
+
const changedRecords = callItems.filter((item) => {
|
|
3000
|
+
const fileMatched = changedFileSet.has(normalizeImpactPath(item.filePath));
|
|
3001
|
+
if (!fileMatched) return false;
|
|
3002
|
+
return changedSymbolSet.size === 0 || changedSymbolSet.has(item.symbol);
|
|
3003
|
+
});
|
|
3004
|
+
|
|
3005
|
+
const callees = [];
|
|
3006
|
+
for (const record of changedRecords) {
|
|
3007
|
+
for (const symbol of record.calls ?? []) {
|
|
3008
|
+
callees.push({ filePath: record.filePath, fromSymbol: record.symbol, symbol });
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
const callers = [];
|
|
3013
|
+
const callerTargets = new Set([
|
|
3014
|
+
...normalizedChangedSymbols,
|
|
3015
|
+
...changedRecords.map((record) => record.symbol),
|
|
3016
|
+
]);
|
|
3017
|
+
if (callerTargets.size > 0) {
|
|
3018
|
+
for (const record of callItems) {
|
|
3019
|
+
const matchedSymbol = (record.calls ?? []).find((name) => callerTargets.has(name));
|
|
3020
|
+
if (!matchedSymbol) continue;
|
|
3021
|
+
if (changedFileSet.has(normalizeImpactPath(record.filePath)) && changedSymbolSet.has(record.symbol)) continue;
|
|
3022
|
+
callers.push({ filePath: record.filePath, symbol: record.symbol, reason: `calls ${matchedSymbol}` });
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
const importers = [];
|
|
3027
|
+
const dependencies = [];
|
|
3028
|
+
for (const imp of importItems) {
|
|
3029
|
+
const from = normalizeImpactPath(imp.from);
|
|
3030
|
+
const matched = resolveImpactImportTargets(from, imp.to)
|
|
3031
|
+
.find((candidate) => changedFileSet.has(normalizeImpactPath(candidate)));
|
|
3032
|
+
if (matched) {
|
|
3033
|
+
importers.push(from);
|
|
3034
|
+
dependencies.push(matched);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
const candidateFiles = uniqueImpact([
|
|
3039
|
+
...normalizedChangedFiles,
|
|
3040
|
+
...importers,
|
|
3041
|
+
...callers.map((item) => item.filePath),
|
|
3042
|
+
...mirrorCounterparts.map((item) => item.filePath),
|
|
3043
|
+
]);
|
|
3044
|
+
const relatedTests = inferRelatedTestsFromArtifacts({
|
|
3045
|
+
candidateFiles,
|
|
3046
|
+
analogsMap: relatedArtifacts.analogsMap,
|
|
3047
|
+
relationsMap: relatedArtifacts.relationsMap,
|
|
3048
|
+
limit: budget.maxTests,
|
|
3049
|
+
}).map((entry) => entry.filePath);
|
|
3050
|
+
const recommendedTestFiles = uniqueImpact([
|
|
3051
|
+
...relatedTests,
|
|
3052
|
+
...risk.riskLabels.flatMap((label) => IMPACT_RISK_TEST_RECOMMENDATIONS[label] ?? []),
|
|
3053
|
+
]);
|
|
3054
|
+
|
|
3055
|
+
return {
|
|
3056
|
+
changedFiles: normalizedChangedFiles,
|
|
3057
|
+
changedSymbols: normalizedChangedSymbols,
|
|
3058
|
+
riskLabels: risk.riskLabels,
|
|
3059
|
+
requiresImpactCheck: risk.requiresImpactCheck,
|
|
3060
|
+
callees: uniqueImpact(callees.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallees),
|
|
3061
|
+
callers: uniqueImpact(callers.map((item) => JSON.stringify(item))).map((value) => JSON.parse(value)).slice(0, budget.maxCallers),
|
|
3062
|
+
importers: uniqueImpact(importers).slice(0, budget.maxImporters),
|
|
3063
|
+
dependencies: uniqueImpact(dependencies).slice(0, budget.maxImporters),
|
|
3064
|
+
mirrorCounterparts,
|
|
3065
|
+
relatedTests,
|
|
3066
|
+
recommendedTestFiles,
|
|
3067
|
+
explanations: {
|
|
3068
|
+
callees: changedRecords.map((record) => `${record.filePath}:${record.symbol}`),
|
|
3069
|
+
callers: callers.map((entry) => `${entry.filePath}:${entry.symbol}`),
|
|
3070
|
+
importers,
|
|
3071
|
+
relatedTests,
|
|
3072
|
+
mirrorCounterparts: mirrorCounterparts.map((entry) => `${entry.source} -> ${entry.filePath}`),
|
|
3073
|
+
},
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
export function formatImpactConfidenceSummary(impact = {}) {
|
|
3078
|
+
const lines = ['Impact checked:'];
|
|
3079
|
+
if ((impact.changedFiles ?? []).length > 0) lines.push(`- Changed: ${(impact.changedFiles ?? []).slice(0, 3).join(', ')}`);
|
|
3080
|
+
if ((impact.changedSymbols ?? []).length > 0) lines.push(`- Symbols: ${(impact.changedSymbols ?? []).slice(0, 5).join(', ')}`);
|
|
3081
|
+
if ((impact.riskLabels ?? []).length > 0) lines.push(`- Risk: ${(impact.riskLabels ?? []).join(', ')}`);
|
|
3082
|
+
|
|
3083
|
+
const calleeSymbols = (impact.callees ?? []).map((item) => item?.symbol).filter(Boolean).slice(0, 5);
|
|
3084
|
+
if (calleeSymbols.length > 0) lines.push(`- Callees: ${calleeSymbols.join(', ')}`);
|
|
3085
|
+
|
|
3086
|
+
const callerFiles = (impact.callers ?? []).map((item) => item?.filePath).filter(Boolean).slice(0, 5);
|
|
3087
|
+
if (callerFiles.length > 0) lines.push(`- Callers: ${callerFiles.join(', ')}`);
|
|
3088
|
+
|
|
3089
|
+
const importers = (impact.importers ?? []).slice(0, 5);
|
|
3090
|
+
if (importers.length > 0) lines.push(`- Importers: ${importers.join(', ')}`);
|
|
3091
|
+
|
|
3092
|
+
const mirrors = (impact.mirrorCounterparts ?? []).map((item) => item?.filePath).filter(Boolean).slice(0, 3);
|
|
3093
|
+
if (mirrors.length > 0) lines.push(`- Mirrors: ${mirrors.join(', ')}`);
|
|
3094
|
+
|
|
3095
|
+
const relatedTests = (impact.relatedTests ?? []).slice(0, 4);
|
|
3096
|
+
if (relatedTests.length > 0) lines.push(`- Related tests: ${relatedTests.join(', ')}`);
|
|
3097
|
+
|
|
3098
|
+
const recommendedTests = (impact.recommendedTestFiles ?? []).slice(0, 4);
|
|
3099
|
+
if (recommendedTests.length > 0) lines.push(`- Recommended tests: ${recommendedTests.join(', ')}`);
|
|
3100
|
+
|
|
3101
|
+
return lines.join('\n');
|
|
3102
|
+
}
|
|
3103
|
+
|
|
2754
3104
|
function parseArtifactGeneratedAt(artifact) {
|
|
2755
3105
|
const generatedAtRaw = String(artifact?.generatedAt ?? '').trim();
|
|
2756
3106
|
|
|
@@ -223,8 +223,30 @@ export const ROUTE_CATALOG = [
|
|
|
223
223
|
path: '.claude/skills/docs-quality/SKILL.md',
|
|
224
224
|
order: 12,
|
|
225
225
|
signals: [
|
|
226
|
-
{ type: 'prompt', regex: /\b(docs|documentation|readme|changelog|handoff|worklog|memory|code map)\b/i, score: 4 },
|
|
227
|
-
{ type: 'file', regex: /\bdocs\/|readme\.md$|project\.md$|memory\.md$|worklog\.md$|code_map\.md$/i, score: 4 },
|
|
226
|
+
{ type: 'prompt', regex: /\b(docs|documentation|readme|changelog|handoff|worklog|memory|code map|status\.md)\b/i, score: 4 },
|
|
227
|
+
{ type: 'file', regex: /\bdocs\/|readme\.md$|project\.md$|memory\.md$|status\.md$|worklog\.md$|code_map\.md$/i, score: 4 },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'next-step',
|
|
232
|
+
path: '.claude/skills/next-step/SKILL.md',
|
|
233
|
+
order: 12.1,
|
|
234
|
+
contextMode: 'standalone',
|
|
235
|
+
signals: [
|
|
236
|
+
{ type: 'prompt', regex: /\b(what(?:'s| is)? next|next steps?|project status|current status|where are we|continue(?: from)?(?: last session)?|roadmap|status\.md)\b/i, score: 7 },
|
|
237
|
+
{ type: 'prompt', regex: /\b(làm gì tiếp|bước tiếp theo|tiếp theo làm gì|làm tiếp|đang ở đâu|trạng thái project|tình trạng project)\b/i, score: 7 },
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'update-status',
|
|
242
|
+
path: '.claude/skills/update-status/SKILL.md',
|
|
243
|
+
order: 12.2,
|
|
244
|
+
contextMode: 'standalone',
|
|
245
|
+
signals: [
|
|
246
|
+
{ type: 'prompt', regex: /\b(update|refresh|write|sync|record|capture|summari[sz]e).{0,48}\b(status\.md|project status|current state|next candidates?|session state)\b/i, score: 9 },
|
|
247
|
+
{ type: 'prompt', regex: /\b(status\.md|project status).{0,48}\b(update|refresh|write|sync|record|capture|summari[sz]e)\b/i, score: 9 },
|
|
248
|
+
{ type: 'prompt', regex: /\b(wrap up|handoff|end(?:ing)? this session|session summary|before final)\b/i, score: 7 },
|
|
249
|
+
{ type: 'prompt', regex: /\b(cập nhật|ghi lại|tổng kết|chốt session|bàn giao).{0,48}\b(status|trạng thái|việc tiếp theo)\b/i, score: 9 },
|
|
228
250
|
],
|
|
229
251
|
},
|
|
230
252
|
{
|