@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +9 -4
  3. package/manifests/platform.full.yaml +55 -0
  4. package/package.json +1 -1
  5. package/src/cli/commands/doctor.js +2 -0
  6. package/src/cli/commands/install.js +3 -2
  7. package/src/cli/commands/uninstall.js +1 -1
  8. package/src/core/runtimeConfig.js +1 -1
  9. package/src/core/uninstall.js +1 -1
  10. package/src/index/buildIndex.js +88 -2
  11. package/src/index/impactCatalog.js +126 -0
  12. package/src/index/impactContext.js +232 -0
  13. package/src/index/paths.js +1 -0
  14. package/src/index/resolveContext.js +1 -0
  15. package/src/index/routeCatalog.js +24 -2
  16. package/src/index/taskRouting.js +147 -4
  17. package/src/index/verificationPlan.js +18 -1
  18. package/templates/.claude/hooks/skill-router.sh +1 -1
  19. package/templates/.claude/hooks/verification-guard.sh +150 -12
  20. package/templates/.claude/skills/docs-quality/SKILL.md +9 -1
  21. package/templates/.claude/skills/next-step/SKILL.md +78 -0
  22. package/templates/.claude/skills/update-status/SKILL.md +88 -0
  23. package/templates/.claude/ukit/index/impact-context.mjs +122 -0
  24. package/templates/.claude/ukit/index/lib/index-core.mjs +352 -2
  25. package/templates/.claude/ukit/index/route-catalog.mjs +24 -2
  26. package/templates/.claude/ukit/index/route-task.mjs +166 -4
  27. package/templates/.codex/README.md +6 -1
  28. package/templates/.codex/settings.json +8 -1
  29. package/templates/AGENTS.md +12 -1
  30. package/templates/CLAUDE.md +12 -1
  31. package/templates/docs/INSTALL.md +2 -0
  32. package/templates/docs/PROJECT.md +5 -4
  33. package/templates/docs/STATUS.md +81 -0
  34. package/templates/docs/UKIT_USAGE_GUIDE.md +16 -0
  35. package/templates/ukit/README.md +1 -1
  36. 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
  {