@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to UKit are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.1.8 - 2026-05-05
8
+
9
+ ### Living Project Status
10
+
11
+ - Added `docs/STATUS.md` as a compact living project-state baseline for active work, debug threads, blockers, verification, and next candidates.
12
+ - Added `next-step` and `update-status` skills so Claude/Codex/OpenCode can use project status automatically without adding any user-facing commands beyond `ukit install`.
13
+ - Made next-step routing freshness-aware: stale or missing status must be treated as orientation only and verified against source/index before recommendations.
14
+ - Added concrete-task precedence so prompts like “fix login bug but not sure next step” stay on the debug/implementation/review workflow instead of being hijacked by global roadmap suggestions.
15
+ - Registered status docs/skills in the manifest with skip semantics for the living doc, and kept source route catalogs aligned with installed helper mirrors.
16
+ - Updated install/routing/manifest regression coverage for open-ended status prompts, explicit handoff updates, concrete debug edge cases, and default skill installation.
17
+
18
+ ## 1.1.7 - 2026-04-25
19
+
20
+ ### Impact Confidence
21
+
22
+ - Added impact-confidence indexing for changed shared/runtime files, including lightweight call extraction and `calls.json` artifacts.
23
+ - 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.
24
+ - Added mirror-counterpart guidance between source modules and installed helper mirrors, helping agents catch source/template drift before release.
25
+ - Added shipped `impact-context.mjs` helper and manifest coverage so installed workspaces can resolve impact context after `ukit install`.
26
+ - Tightened verification planning and guard behavior so impact-test lanes run targeted checks first, while broad verification remains advisory unless explicitly enforced.
27
+ - Added package-artifact, hook, indexing, impact-context, and impact-catalog coverage for the new release surface.
28
+
7
29
  ## 1.1.6 - 2026-04-23
8
30
 
9
31
  ### Hidden Runtime Root
package/README.md CHANGED
@@ -32,6 +32,7 @@ ukit install
32
32
  4. Fill in the generated docs baseline:
33
33
  - `docs/PROJECT.md`
34
34
  - `docs/MEMORY.md`
35
+ - `docs/STATUS.md`
35
36
  - `docs/WORKLOG.md`
36
37
  5. Open your AI tool and work in natural language.
37
38
 
@@ -64,9 +65,9 @@ If maintainers roll out a newer CLI build, the in-project workflow still stays t
64
65
  **Project support files**
65
66
  - `.claude/ukit/.ukit/` — installer manifests, metadata, backups
66
67
  - `.ukit/` — hidden shared runtime storage for config, cache, and cross-agent memory
67
- - `docs/` — PROJECT / MEMORY / WORKLOG baseline
68
+ - `docs/` — PROJECT / MEMORY / STATUS / WORKLOG baseline
68
69
 
69
- ## UKit v1.1.6 Runtime
70
+ ## UKit v1.1.8 Runtime
70
71
 
71
72
  UKit now installs a hidden shared local runtime at `.ukit/` for production-oriented state that should survive across agent sessions:
72
73
 
@@ -83,13 +84,17 @@ When long sessions approach the compact threshold, UKit now uses a conservative
83
84
  - compact only safe-zone history/noise
84
85
  - preserve active task, rules, decisions, and current code focus
85
86
 
86
- UKit v1.1.6 keeps the same shared runtime contract while the npm distribution now lives under the owned maintainer scope:
87
+ UKit v1.1.8 keeps the same shared runtime contract while adding living project status routing for continuity-safe AI sessions:
87
88
 
88
89
  - install globally with `npm install -g @ngockhoale/ukit`
89
90
  - keep using the exact same human workflow inside projects: `ukit install`
90
91
  - preserve the same `ukit` binary, hooks, and install-first orchestration while standardizing the runtime root as hidden `.ukit/`
92
+ - install `docs/STATUS.md` as a compact living state file for active work, debug threads, blockers, verification, and next candidates
93
+ - auto-route open-ended “what next?” / “continue” prompts to the `next-step` skill with a visible freshness cue when status may be stale
94
+ - auto-route explicit handoff/wrap-up requests to the `update-status` skill while skipping trivial/no-state-change tasks
95
+ - keep concrete debug/implementation/review prompts primary, so project status never replaces source/index-first task work
91
96
 
92
- UKit v1.1.6 also keeps the fast path improvements from the recent runtime releases:
97
+ UKit v1.1.8 also keeps the fast path improvements from the recent runtime releases:
93
98
 
94
99
  - Vietnamese prompts now normalize more effectively for English-heavy code symbols and paths
95
100
  - localized simple direct-target lanes skip extra previous-context / recent-output work when it would not change the next action
@@ -143,6 +143,22 @@ items:
143
143
  packs:
144
144
  - core
145
145
 
146
+ - id: docs-status
147
+ type: config
148
+ sourceTemplate: docs/STATUS.md
149
+ targetPath: docs/STATUS.md
150
+ requires:
151
+ - docs-project
152
+ - docs-memory
153
+ - docs-worklog
154
+ - docs-code-map
155
+ mergeStrategy: skip
156
+ variables:
157
+ - project.name
158
+ enabledByDefault: true
159
+ packs:
160
+ - core
161
+
146
162
  - id: docs-bugfix
147
163
  type: config
148
164
  sourceTemplate: docs/BUGFIX.md
@@ -460,6 +476,32 @@ items:
460
476
  packs:
461
477
  - core
462
478
 
479
+ - id: next-step-skill
480
+ type: skill
481
+ sourceTemplate: .claude/skills/next-step/SKILL.md
482
+ targetPath: .claude/skills/next-step/SKILL.md
483
+ requires:
484
+ - docs-quality-skill
485
+ - docs-status
486
+ mergeStrategy: overwrite_with_backup
487
+ variables: []
488
+ enabledByDefault: true
489
+ packs:
490
+ - core
491
+
492
+ - id: update-status-skill
493
+ type: skill
494
+ sourceTemplate: .claude/skills/update-status/SKILL.md
495
+ targetPath: .claude/skills/update-status/SKILL.md
496
+ requires:
497
+ - docs-quality-skill
498
+ - docs-status
499
+ mergeStrategy: overwrite_with_backup
500
+ variables: []
501
+ enabledByDefault: true
502
+ packs:
503
+ - core
504
+
463
505
  - id: docs-manager-skill
464
506
  type: skill
465
507
  sourceTemplate: .claude/skills/docs-manager
@@ -1000,6 +1042,19 @@ items:
1000
1042
  packs:
1001
1043
  - core
1002
1044
 
1045
+ - id: ukit-index-impact-context-script
1046
+ type: config
1047
+ sourceTemplate: .claude/ukit/index/impact-context.mjs
1048
+ targetPath: .claude/ukit/index/impact-context.mjs
1049
+ requires:
1050
+ - ukit-index-core-lib
1051
+ - ukit-index-cache-utils-script
1052
+ mergeStrategy: overwrite_with_backup
1053
+ variables: []
1054
+ enabledByDefault: true
1055
+ packs:
1056
+ - core
1057
+
1003
1058
  - id: ukit-index-cache-utils-script
1004
1059
  type: config
1005
1060
  sourceTemplate: .claude/ukit/index/cache-utils.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, Antigravity, OpenAI Codex, and OpenCode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -62,6 +62,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
62
62
  sessionMemoryDirExists: await pathExists(runtimePaths.sessionsDir),
63
63
  docsProjectExists: await pathExists(path.join(projectRoot, 'docs', 'PROJECT.md')),
64
64
  docsMemoryExists: await pathExists(path.join(projectRoot, 'docs', 'MEMORY.md')),
65
+ docsStatusExists: await pathExists(path.join(projectRoot, 'docs', 'STATUS.md')),
65
66
  docsWorklogExists: await pathExists(path.join(projectRoot, 'docs', 'WORKLOG.md')),
66
67
  allProvidersConfigured: providers.allSupported,
67
68
  ...(codexAdapterTracked
@@ -103,6 +104,7 @@ export async function runDoctor({ packageRoot, projectRoot, argv = [] }) {
103
104
  console.log(`[UKit] ${ok(checks.sessionMemoryDirExists)} .ukit/storage/memory/sessions/`);
104
105
  console.log(`[UKit] ${ok(checks.docsProjectExists)} docs/PROJECT.md`);
105
106
  console.log(`[UKit] ${ok(checks.docsMemoryExists)} docs/MEMORY.md`);
107
+ console.log(`[UKit] ${ok(checks.docsStatusExists)} docs/STATUS.md`);
106
108
  console.log(`[UKit] ${ok(checks.docsWorklogExists)} docs/WORKLOG.md`);
107
109
  if (codexAdapterTracked) {
108
110
  console.log(`[UKit] ${ok(checks.codexReadmeExists)} .codex/README.md`);
@@ -244,9 +244,10 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
244
244
  const docsPaths = [
245
245
  path.join(projectRoot, 'docs', 'PROJECT.md'),
246
246
  path.join(projectRoot, 'docs', 'MEMORY.md'),
247
+ path.join(projectRoot, 'docs', 'STATUS.md'),
247
248
  path.join(projectRoot, 'docs', 'WORKLOG.md'),
248
249
  ];
249
- const docsLabels = ['docs/PROJECT.md', 'docs/MEMORY.md', 'docs/WORKLOG.md'];
250
+ const docsLabels = ['docs/PROJECT.md', 'docs/MEMORY.md', 'docs/STATUS.md', 'docs/WORKLOG.md'];
250
251
  const missingDocs = [];
251
252
  for (let i = 0; i < docsPaths.length; i++) {
252
253
  if (!(await pathExists(docsPaths[i]))) {
@@ -257,7 +258,7 @@ export async function runInstall({ packageRoot, projectRoot, packageVersion, arg
257
258
  if (missingDocs.length > 0) {
258
259
  console.log(`[UKit] Missing docs — fill these in before first use: ${missingDocs.join(', ')}`);
259
260
  } else {
260
- console.log('[UKit] Docs baseline ready: docs/PROJECT.md, docs/MEMORY.md, docs/WORKLOG.md');
261
+ console.log('[UKit] Docs baseline ready: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/WORKLOG.md');
261
262
  console.log('[UKit] Fill them once with real project context for the best results.');
262
263
  }
263
264
 
@@ -47,5 +47,5 @@ export async function runUninstall({ projectRoot, argv = [] }) {
47
47
  }
48
48
 
49
49
  console.log(`[UKit] Uninstall complete. Removed ${result.removed}/${result.attempted} managed paths.`);
50
- console.log('[UKit] Note: docs/PROJECT.md, docs/MEMORY.md, docs/WORKLOG.md contain user content and were preserved. Delete manually if needed.');
50
+ console.log('[UKit] Note: docs/PROJECT.md, docs/MEMORY.md, docs/STATUS.md, docs/WORKLOG.md contain user content and were preserved. Delete manually if needed.');
51
51
  }
@@ -42,7 +42,7 @@ export function buildDefaultRuntimeConfig(overrides = {}) {
42
42
  const safeOverrides = isPlainObject(overrides) ? overrides : {};
43
43
 
44
44
  return mergeObjects({
45
- version: '1.1.6',
45
+ version: '1.1.8',
46
46
  agent: 'claude-code',
47
47
  compact: {
48
48
  enabled: true,
@@ -200,7 +200,7 @@ export async function uninstallUkit({ projectRoot, dryRun = false }) {
200
200
  }
201
201
  } else {
202
202
  // Old format (no files list): fall back to hardcoded managed paths.
203
- // NOTE: docs/PROJECT.md, MEMORY.md, WORKLOG.md are intentionally excluded.
203
+ // NOTE: docs/PROJECT.md, MEMORY.md, STATUS.md, WORKLOG.md are intentionally excluded.
204
204
  // These are user-created content (mergeStrategy: skip) — deleting them
205
205
  // would cause data loss. Users must remove them manually if desired.
206
206
  const fallback = buildFallbackPaths(projectRoot);
@@ -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 };