@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.
@@ -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
 
@@ -247,6 +247,7 @@ async function prepareTaskRoute({
247
247
  promptText: normalizedPrompt,
248
248
  commandText: normalizedCommand,
249
249
  selectedIds,
250
+ targetFile: normalizedTarget,
250
251
  });
251
252
  const preservedPrompt = normalizedPrompt || String(lastExplicitUserPromptText || '').trim();
252
253
 
@@ -1143,7 +1144,7 @@ function deriveContextIntent({ promptText, commandText, targetFile, selectedIds
1143
1144
  return '';
1144
1145
  }
1145
1146
 
1146
- function inferTaskType({ promptText, commandText, selectedIds }) {
1147
+ function inferTaskType({ promptText, commandText, selectedIds, targetFile = null }) {
1147
1148
  const lower = buildNormalizedRouteSignalText(promptText, commandText);
1148
1149
  if (
1149
1150
  selectedIds.includes('discover-security')
@@ -1152,10 +1153,17 @@ function inferTaskType({ promptText, commandText, selectedIds }) {
1152
1153
  ) {
1153
1154
  return 'non-trivial';
1154
1155
  }
1156
+
1157
+ let inferred = 'simple';
1155
1158
  if (/\b(typo|label|text|rename|color|spacing|toggle|comment)\b/.test(lower)) {
1156
- return 'trivial';
1159
+ inferred = 'trivial';
1160
+ }
1161
+
1162
+ if ((inferred === 'simple' || inferred === 'trivial') && isSharedImpactFile(targetFile)) {
1163
+ return 'shared-simple';
1157
1164
  }
1158
- return 'simple';
1165
+
1166
+ return inferred;
1159
1167
  }
1160
1168
 
1161
1169
  function buildHelperCommand({ commandNamespace = '.claude', scriptName, intent = '', targetFile = null, taskType = null }) {
@@ -1166,6 +1174,24 @@ function buildHelperCommand({ commandNamespace = '.claude', scriptName, intent =
1166
1174
  return parts.join(' ');
1167
1175
  }
1168
1176
 
1177
+ const SHARED_IMPACT_PATTERNS = [
1178
+ /^src\/index\//,
1179
+ /^src\/core\/(runInstallPipeline|applyPlan|buildPlan|diffPlan|metadata|migrateLegacy|uninstall|runtimeConfig|runtimePaths)\.js$/,
1180
+ /^src\/core\/(output|token|compact)\//,
1181
+ /^templates\/\.claude\/hooks\//,
1182
+ /^templates\/\.claude\/ukit\//,
1183
+ /^manifests\/platform\.full\.yaml$/,
1184
+ /^templates\//,
1185
+ ];
1186
+
1187
+ function isSharedImpactFile(filePath) {
1188
+ const normalized = String(filePath ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
1189
+ if (!normalized) {
1190
+ return false;
1191
+ }
1192
+ return SHARED_IMPACT_PATTERNS.some((pattern) => pattern.test(normalized));
1193
+ }
1194
+
1169
1195
  function isTestLikeFile(filePath) {
1170
1196
  return /\.(test|spec)\.[a-z0-9]+$/i.test(filePath)
1171
1197
  || /(^|\/)(?:__tests__|tests?|specs?)\//i.test(filePath);
@@ -12,7 +12,7 @@ Auto-generated by UKit for OpenAI Codex.
12
12
  - Do not make end users memorize skill names, helper scripts, or routing internals unless they are debugging UKit itself.
13
13
  - **Treat helper commands as internal orchestration. Do not ask end users to run them.**
14
14
 
15
- ## UKit v1.1.6 Shared Runtime
15
+ ## UKit v1.1.7 Shared Runtime
16
16
 
17
17
  - Shared runtime state lives in `.ukit/storage/`.
18
18
  - Treat `.ukit/storage/config.json` as the source of compact, token-pipeline, router, memory, and validation toggles.
@@ -41,7 +41,7 @@
41
41
  - If a concrete verification lane is needed, prefer `node .claude/ukit/index/verify-context.mjs ...`.
42
42
  - These helper/index commands are internal orchestration. Run them yourself when needed; never turn them into required end-user workflow.
43
43
 
44
- ## UKit v1.1.6 Shared Runtime
44
+ ## UKit v1.1.7 Shared Runtime
45
45
 
46
46
  - Shared runtime state lives in `.ukit/storage/`.
47
47
  - Treat `.ukit/storage/config.json` as the source of runtime toggles for compact, token pipeline, router, memory, and validation behavior.
@@ -40,7 +40,7 @@
40
40
  - **Do not ask normal contributors to run internal helper commands**; run them yourself or tell them to rerun `ukit install`.
41
41
  - Do not ask normal contributors to memorize `ukit doctor`, `ukit diff`, `ukit uninstall`, or `ukit index ...` unless they explicitly need maintainer/debug help.
42
42
 
43
- ## UKit v1.1.6 Shared Runtime
43
+ ## UKit v1.1.7 Shared Runtime
44
44
 
45
45
  - Shared runtime state lives in `.ukit/storage/`.
46
46
  - Treat `.ukit/storage/config.json` as the source of runtime toggles for compact, token pipeline, router, memory, and validation behavior.
@@ -1,6 +1,6 @@
1
1
  # UKit Shared Runtime
2
2
 
3
- This folder stores shared UKit runtime state for v1.1.6 features.
3
+ This folder stores shared UKit runtime state for v1.1.7 features.
4
4
 
5
5
  - `storage/config.json` — runtime feature flags and defaults
6
6
  - `storage/cache/` — prompt-cache, compact history, compact pressure state, output summaries, and preserved raw tool outputs under `storage/cache/tee/`
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.6",
2
+ "version": "1.1.7",
3
3
  "agent": "claude-code",
4
4
  "compact": {
5
5
  "enabled": true,