@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
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.
|
|
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.
|
|
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.
|
|
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
|
@@ -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
|
}
|
package/src/core/uninstall.js
CHANGED
|
@@ -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);
|
package/src/index/buildIndex.js
CHANGED
|
@@ -34,6 +34,9 @@ const TRACKED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'
|
|
|
34
34
|
const DISCOVERED_EXTENSIONS = new Set([...TRACKED_EXTENSIONS, ...STYLE_EXTENSIONS]);
|
|
35
35
|
export const DEFAULT_INDEX_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
36
36
|
const INDEX_PARSE_BATCH_SIZE = 8;
|
|
37
|
+
const CALL_IGNORE_WORDS = new Set([
|
|
38
|
+
'if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'typeof',
|
|
39
|
+
]);
|
|
37
40
|
|
|
38
41
|
export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
39
42
|
const absoluteRoot = path.resolve(rootDir);
|
|
@@ -89,6 +92,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
89
92
|
|
|
90
93
|
const symbols = [];
|
|
91
94
|
const imports = [];
|
|
95
|
+
const calls = [];
|
|
92
96
|
const reusableCodeFiles = [];
|
|
93
97
|
let filesToParse = [];
|
|
94
98
|
let reusedCodeFileCount = 0;
|
|
@@ -109,20 +113,24 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
if (reusableCodeFiles.length > 0) {
|
|
112
|
-
const [previousSymbolsArtifact, previousImportsArtifact] = await Promise.all([
|
|
116
|
+
const [previousSymbolsArtifact, previousImportsArtifact, previousCallsArtifact] = await Promise.all([
|
|
113
117
|
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.symbols),
|
|
114
118
|
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
119
|
+
readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
115
120
|
]);
|
|
116
121
|
canReuseParsedArtifacts = previousSymbolsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
117
|
-
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
122
|
+
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
123
|
+
&& previousCallsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION;
|
|
118
124
|
|
|
119
125
|
if (canReuseParsedArtifacts) {
|
|
120
126
|
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item.filePath);
|
|
121
127
|
const previousImportsByPath = groupBy(previousImportsArtifact.items ?? [], (item) => item.from);
|
|
128
|
+
const previousCallsByPath = groupBy(previousCallsArtifact.items ?? [], (item) => item.filePath);
|
|
122
129
|
|
|
123
130
|
for (const filePath of reusableCodeFiles) {
|
|
124
131
|
symbols.push(...(previousSymbolsByPath.get(filePath) ?? []));
|
|
125
132
|
imports.push(...(previousImportsByPath.get(filePath) ?? []));
|
|
133
|
+
calls.push(...(previousCallsByPath.get(filePath) ?? []));
|
|
126
134
|
reusedCodeFileCount += 1;
|
|
127
135
|
}
|
|
128
136
|
} else {
|
|
@@ -145,6 +153,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
145
153
|
...extractImports(filePath, scriptContent),
|
|
146
154
|
...extractSupplementalImports(filePath, content),
|
|
147
155
|
],
|
|
156
|
+
calls: extractFunctionCalls(filePath, scriptContent),
|
|
148
157
|
};
|
|
149
158
|
} catch {
|
|
150
159
|
return null;
|
|
@@ -154,6 +163,7 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
154
163
|
for (const parsedFile of parsedBatch) {
|
|
155
164
|
symbols.push(...parsedFile.symbols);
|
|
156
165
|
imports.push(...parsedFile.imports);
|
|
166
|
+
calls.push(...parsedFile.calls);
|
|
157
167
|
}
|
|
158
168
|
}
|
|
159
169
|
|
|
@@ -279,6 +289,12 @@ export async function buildCodeIndex({ rootDir = process.cwd() } = {}) {
|
|
|
279
289
|
rootDir: absoluteRoot,
|
|
280
290
|
items: imports,
|
|
281
291
|
});
|
|
292
|
+
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.calls, {
|
|
293
|
+
schemaVersion: INDEX_SCHEMA_VERSION,
|
|
294
|
+
generatedAt,
|
|
295
|
+
rootDir: absoluteRoot,
|
|
296
|
+
items: calls,
|
|
297
|
+
});
|
|
282
298
|
}
|
|
283
299
|
if (!canReuseTestsMap) {
|
|
284
300
|
await writeArtifact(absoluteRoot, INDEX_ARTIFACTS.testsMap, {
|
|
@@ -579,6 +595,76 @@ function extractSupplementalImports(filePath, content) {
|
|
|
579
595
|
return imports;
|
|
580
596
|
}
|
|
581
597
|
|
|
598
|
+
function extractFunctionBlocks(content) {
|
|
599
|
+
const blocks = [];
|
|
600
|
+
|
|
601
|
+
for (const match of content.matchAll(/export\s+(async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g)) {
|
|
602
|
+
blocks.push({ symbol: match[2], exported: true, bodyStart: match.index + match[0].length });
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
for (const match of content.matchAll(/function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g)) {
|
|
606
|
+
const prefix = content.slice(Math.max(0, match.index - 16), match.index);
|
|
607
|
+
if (/export\s+$/.test(prefix)) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
blocks.push({ symbol: match[1], exported: false, bodyStart: match.index + match[0].length });
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
for (const match of content.matchAll(/export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*\{/g)) {
|
|
614
|
+
blocks.push({ symbol: match[1], exported: true, bodyStart: match.index + match[0].length });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
for (const match of content.matchAll(/const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>\s*\{/g)) {
|
|
618
|
+
const prefix = content.slice(Math.max(0, match.index - 16), match.index);
|
|
619
|
+
if (/export\s+$/.test(prefix)) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
blocks.push({ symbol: match[1], exported: false, bodyStart: match.index + match[0].length });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return blocks
|
|
626
|
+
.map((block) => ({ ...block, body: sliceBalancedBlock(content, block.bodyStart) }))
|
|
627
|
+
.filter((block) => block.body);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function sliceBalancedBlock(content, startIndex) {
|
|
631
|
+
let depth = 1;
|
|
632
|
+
for (let index = startIndex; index < content.length; index += 1) {
|
|
633
|
+
const char = content[index];
|
|
634
|
+
if (char === '{') {
|
|
635
|
+
depth += 1;
|
|
636
|
+
}
|
|
637
|
+
if (char === '}') {
|
|
638
|
+
depth -= 1;
|
|
639
|
+
}
|
|
640
|
+
if (depth === 0) {
|
|
641
|
+
return content.slice(startIndex, index);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return '';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractFunctionCalls(filePath, content) {
|
|
648
|
+
return extractFunctionBlocks(content).map((block) => {
|
|
649
|
+
const calls = [];
|
|
650
|
+
for (const match of block.body.matchAll(/(?:\b|\.)([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g)) {
|
|
651
|
+
const name = match[1];
|
|
652
|
+
if (!name || CALL_IGNORE_WORDS.has(name)) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (!calls.includes(name)) {
|
|
656
|
+
calls.push(name);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
filePath,
|
|
661
|
+
symbol: block.symbol,
|
|
662
|
+
exported: block.exported,
|
|
663
|
+
calls,
|
|
664
|
+
};
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
582
668
|
function buildTestsMap(fileRecords) {
|
|
583
669
|
const testFiles = fileRecords.filter((file) => isLikelyTestFile(file.filePath));
|
|
584
670
|
const sourceFiles = fileRecords.filter((file) => CODE_EXTENSIONS.has(file.ext) && !isLikelyTestFile(file.filePath));
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const SHARED_IMPACT_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
regex: /^src\/index\//,
|
|
4
|
+
label: 'shared-index-runtime',
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
regex: /^src\/core\/(runInstallPipeline|applyPlan|buildPlan|diffPlan|metadata|migrateLegacy|uninstall|runtimeConfig|runtimePaths)\.js$/,
|
|
8
|
+
label: 'shared-install-runtime',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
regex: /^src\/core\/(output|token|compact)\//,
|
|
12
|
+
label: 'shared-token-runtime',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
regex: /^templates\/\.claude\/hooks\//,
|
|
16
|
+
label: 'installed-hook-runtime',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
regex: /^templates\/\.claude\/ukit\//,
|
|
20
|
+
label: 'installed-helper-runtime',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
regex: /^manifests\/platform\.full\.yaml$/,
|
|
24
|
+
label: 'install-manifest',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
regex: /^templates\//,
|
|
28
|
+
label: 'published-template-surface',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const MIRROR_PAIRS = [
|
|
33
|
+
{
|
|
34
|
+
source: 'src/index/buildIndex.js',
|
|
35
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
36
|
+
reason: 'installed index-core mirror embeds source index behavior',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
source: 'src/index/queryIndex.js',
|
|
40
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
41
|
+
reason: 'installed index-core mirror embeds source query behavior',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
source: 'src/index/resolveContext.js',
|
|
45
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
46
|
+
reason: 'installed index-core mirror embeds context resolution behavior',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
source: 'src/index/verificationPlan.js',
|
|
50
|
+
mirror: 'templates/.claude/ukit/index/lib/index-core.mjs',
|
|
51
|
+
reason: 'installed index-core mirror embeds verification planning behavior',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
source: 'src/index/taskRouting.js',
|
|
55
|
+
mirror: 'templates/.claude/ukit/index/route-task.mjs',
|
|
56
|
+
reason: 'installed route-task helper mirrors source task routing behavior',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
source: 'src/index/routeCatalog.js',
|
|
60
|
+
mirror: 'templates/.claude/ukit/index/route-catalog.mjs',
|
|
61
|
+
reason: 'installed route catalog must match source route catalog',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
source: 'src/core/output/index.js',
|
|
65
|
+
mirror: 'templates/.claude/ukit/runtime/output-compression.mjs',
|
|
66
|
+
reason: 'source output compression runtime should stay aligned with installed hook runtime',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
source: 'src/core/token/index.js',
|
|
70
|
+
mirror: 'templates/.claude/ukit/runtime/token-utils.mjs',
|
|
71
|
+
reason: 'source token runtime should stay aligned with installed token utilities',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
source: 'src/core/compact/threshold.js',
|
|
75
|
+
mirror: 'templates/.claude/ukit/runtime/compact-threshold.mjs',
|
|
76
|
+
reason: 'source compact threshold runtime should stay aligned with installed compact threshold hook',
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function normalizePath(filePath) {
|
|
81
|
+
return String(filePath ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isSharedImpactFile(filePath) {
|
|
85
|
+
const normalized = normalizePath(filePath);
|
|
86
|
+
return SHARED_IMPACT_PATTERNS.some((entry) => entry.regex.test(normalized));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function classifyImpactRisk(filePaths = []) {
|
|
90
|
+
const normalizedPaths = [...new Set(filePaths.map(normalizePath).filter(Boolean))];
|
|
91
|
+
const riskLabels = [];
|
|
92
|
+
|
|
93
|
+
for (const filePath of normalizedPaths) {
|
|
94
|
+
for (const entry of SHARED_IMPACT_PATTERNS) {
|
|
95
|
+
if (entry.regex.test(filePath) && !riskLabels.includes(entry.label)) {
|
|
96
|
+
riskLabels.push(entry.label);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
changedFiles: normalizedPaths,
|
|
103
|
+
riskLabels,
|
|
104
|
+
requiresImpactCheck: riskLabels.length > 0,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function findMirrorCounterparts(filePaths = []) {
|
|
109
|
+
const normalizedSet = new Set(filePaths.map(normalizePath).filter(Boolean));
|
|
110
|
+
const counterparts = [];
|
|
111
|
+
|
|
112
|
+
for (const pair of MIRROR_PAIRS) {
|
|
113
|
+
if (normalizedSet.has(pair.source)) {
|
|
114
|
+
counterparts.push({ filePath: pair.mirror, reason: pair.reason, source: pair.source });
|
|
115
|
+
}
|
|
116
|
+
if (normalizedSet.has(pair.mirror)) {
|
|
117
|
+
counterparts.push({ filePath: pair.source, reason: pair.reason, source: pair.mirror });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return counterparts.filter((entry, index, list) => (
|
|
122
|
+
list.findIndex((candidate) => candidate.filePath === entry.filePath && candidate.source === entry.source) === index
|
|
123
|
+
));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { MIRROR_PAIRS, SHARED_IMPACT_PATTERNS };
|