@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
@@ -5,9 +5,10 @@
5
5
  INPUT=$(cat)
6
6
  PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
7
7
  STATE_FILE="$PROJECT_ROOT/.claude/ukit/skill-router-state.json"
8
+ ROUTE_CACHE_FILE="$PROJECT_ROOT/.claude/ukit/route-cache.json"
8
9
  PROGRESS_FILE="$PROJECT_ROOT/.claude/ukit/verification-progress.json"
9
10
 
10
- INPUT="$INPUT" STATE_FILE="$STATE_FILE" PROGRESS_FILE="$PROGRESS_FILE" node <<'NODE'
11
+ INPUT="$INPUT" STATE_FILE="$STATE_FILE" ROUTE_CACHE_FILE="$ROUTE_CACHE_FILE" PROGRESS_FILE="$PROGRESS_FILE" node <<'NODE'
11
12
  const fs = require('fs');
12
13
  const path = require('path');
13
14
 
@@ -125,12 +126,122 @@ function isExplicitBroadVerificationRequest(promptText) {
125
126
  /chạy full test suite/,
126
127
  /verify toàn bộ/,
127
128
  /kiểm tra toàn bộ/,
129
+ // Plan-driven execution prompts often authorize the plan's final broad
130
+ // verification without spelling it as "full test suite".
131
+ /(?:run|execute|implement|do)\b.*\bfull\b.*\b(?:prd\s+)?plan\b/,
132
+ /\bfull\b.*\b(?:prd\s+)?plan\b.*\b(?:verify|verification|test|tests)\b/,
133
+ /(?:chạy|lam|làm|thực hiện|triển khai)\b.*\bfull\b.*\b(?:prd|plan|ke hoach|kế hoạch)\b/,
134
+ /\bfull\b.*\b(?:prd|plan|ke hoach|kế hoạch)\b.*\b(?:verify|verification|test|tests|kiểm tra)\b/,
128
135
  ].some((pattern) => pattern.test(text));
129
136
  }
130
137
 
138
+ function collectRecentPromptTexts(state, routeCache, maxAgeMs) {
139
+ const prompts = [
140
+ state?.routingContext?.lastExplicitUserPromptText,
141
+ state?.routingContext?.promptText,
142
+ ];
143
+ const now = Date.now();
144
+
145
+ for (const entry of routeCache?.entries ?? []) {
146
+ const updatedAt = Number(entry?.updatedAt ?? entry?.ts ?? 0);
147
+ if (Number.isFinite(updatedAt) && updatedAt > 0 && now - updatedAt > maxAgeMs) {
148
+ continue;
149
+ }
150
+ prompts.push(
151
+ entry?.routingContext?.lastExplicitUserPromptText,
152
+ entry?.routingContext?.promptText,
153
+ );
154
+ }
155
+
156
+ return unique(prompts.map((prompt) => String(prompt || '').trim()).filter(Boolean));
157
+ }
158
+
159
+ function hasRecentExplicitBroadVerificationRequest(state, routeCache, maxAgeMs) {
160
+ return collectRecentPromptTexts(state, routeCache, maxAgeMs)
161
+ .some((promptText) => isExplicitBroadVerificationRequest(promptText));
162
+ }
163
+
164
+ function collectRecentRouteSummaries(routeCache, maxAgeMs) {
165
+ const now = Date.now();
166
+ const summaries = [];
167
+
168
+ for (const entry of routeCache?.entries ?? []) {
169
+ const updatedAt = Number(entry?.updatedAt ?? entry?.ts ?? 0);
170
+ if (Number.isFinite(updatedAt) && updatedAt > 0 && now - updatedAt > maxAgeMs) {
171
+ continue;
172
+ }
173
+ if (entry?.routeSummary && typeof entry.routeSummary === 'object') {
174
+ summaries.push(entry.routeSummary);
175
+ }
176
+ }
177
+
178
+ return summaries;
179
+ }
180
+
181
+ function collectAttemptedCommands(progress, {
182
+ currentFingerprint = null,
183
+ maxAgeMs,
184
+ } = {}) {
185
+ const now = Date.now();
186
+ const attempted = [];
187
+ const progressUpdatedAt = Number(progress?.updatedAt ?? 0);
188
+ const legacyProgressIsFresh = Number.isFinite(progressUpdatedAt)
189
+ && progressUpdatedAt > 0
190
+ && now - progressUpdatedAt <= maxAgeMs;
191
+
192
+ if (progress?.fingerprint === currentFingerprint || legacyProgressIsFresh) {
193
+ attempted.push(...(progress?.attemptedCommands ?? []));
194
+ }
195
+
196
+ for (const entry of progress?.recentAttempts ?? []) {
197
+ const ts = Number(entry?.ts ?? 0);
198
+ if (Number.isFinite(ts) && ts > 0 && now - ts <= maxAgeMs) {
199
+ attempted.push(entry.command);
200
+ }
201
+ }
202
+
203
+ return new Set(attempted.map(normalizeCommand).filter(Boolean));
204
+ }
205
+
206
+ function hasCompletedRecentTargetedLane({
207
+ routeCache,
208
+ attemptedCommands,
209
+ command,
210
+ maxAgeMs,
211
+ } = {}) {
212
+ const normalizedCommand = normalizeCommand(command);
213
+ if (!isBroadTestCommand(normalizedCommand)) {
214
+ return false;
215
+ }
216
+
217
+ return collectRecentRouteSummaries(routeCache, maxAgeMs).some((summary) => {
218
+ const mode = summary?.policyMode;
219
+ if (mode !== 'auto-run-targeted' && mode !== 'auto-run-targeted-then-fallback') {
220
+ return false;
221
+ }
222
+
223
+ const recentPrimary = unique((summary?.primaryCommands ?? []).map(normalizeCommand));
224
+ if (recentPrimary.length === 0 || !recentPrimary.every((item) => attemptedCommands.has(item))) {
225
+ return false;
226
+ }
227
+
228
+ const recentFallbacks = unique((summary?.fallbackCommands ?? []).map(normalizeCommand));
229
+ const recentPreferred = unique((summary?.preferredOrder ?? []).map(normalizeCommand));
230
+ return recentFallbacks.includes(normalizedCommand)
231
+ || recentPreferred.includes(normalizedCommand)
232
+ || recentPrimary.includes(normalizedCommand);
233
+ });
234
+ }
235
+
131
236
  function deny(message) {
132
- process.stderr.write(`${message}\n`);
133
- process.exit(2);
237
+ const advisory = message.replace(/^BLOCKED:/, 'ADVISORY:');
238
+ if (process.env.UKIT_VERIFICATION_GUARD_ENFORCE === '1') {
239
+ fs.writeSync(2, `${message}\n`);
240
+ process.exit(2);
241
+ }
242
+
243
+ fs.writeSync(1, `${advisory}\n`);
244
+ process.exit(0);
134
245
  }
135
246
 
136
247
  const payload = (() => {
@@ -148,6 +259,7 @@ if (!command) {
148
259
  const statePath = process.env.STATE_FILE;
149
260
  const progressPath = process.env.PROGRESS_FILE;
150
261
  const state = readJson(statePath, null);
262
+ const routeCache = readJson(process.env.ROUTE_CACHE_FILE, null);
151
263
  const recommendation = state?.verificationRecommendation ?? null;
152
264
  const routeSummary = state?.routeSummary ?? null;
153
265
  const helpers = state?.helpers ?? null;
@@ -195,24 +307,39 @@ if (!policyMode && primaryCommands.length === 0 && fallbackCommands.length === 0
195
307
  }
196
308
 
197
309
  const progress = readJson(progressPath, {});
198
- const normalizedProgress = progress?.fingerprint === state?.fingerprint
199
- ? progress
200
- : { fingerprint: state?.fingerprint || null, attemptedCommands: [] };
201
- const attemptedCommands = new Set((normalizedProgress.attemptedCommands ?? []).map(normalizeCommand));
310
+ const attemptedCommands = collectAttemptedCommands(progress, {
311
+ currentFingerprint: state?.fingerprint || null,
312
+ maxAgeMs: STATE_FRESH_MS,
313
+ });
202
314
 
203
315
  function persistAttempt(commandText) {
204
- attemptedCommands.add(commandText);
316
+ const normalizedCommandText = normalizeCommand(commandText);
317
+ attemptedCommands.add(normalizedCommandText);
318
+ const now = Date.now();
319
+ const recentAttempts = [
320
+ ...(progress?.recentAttempts ?? []),
321
+ { command: normalizedCommandText, ts: now },
322
+ ]
323
+ .map((entry) => ({
324
+ command: normalizeCommand(entry?.command),
325
+ ts: Number(entry?.ts ?? now),
326
+ }))
327
+ .filter((entry) => entry.command && Number.isFinite(entry.ts) && now - entry.ts <= STATE_FRESH_MS)
328
+ .filter((entry, index, list) => (
329
+ list.findLastIndex((candidate) => candidate.command === entry.command) === index
330
+ ));
205
331
  writeJson(progressPath, {
206
332
  fingerprint: state?.fingerprint || null,
333
+ updatedAt: now,
207
334
  attemptedCommands: [...attemptedCommands],
335
+ recentAttempts,
208
336
  });
209
337
  }
210
338
 
211
339
  const isPrimaryCommand = primaryCommands.includes(command);
212
- const explicitBroadRequested = isExplicitBroadVerificationRequest(
213
- state?.routingContext?.lastExplicitUserPromptText || state?.routingContext?.promptText || '',
214
- );
340
+ const explicitBroadRequested = hasRecentExplicitBroadVerificationRequest(state, routeCache, STATE_FRESH_MS);
215
341
  const isFallbackCommand = fallbackCommands.includes(command);
342
+ const isTargetedVerification = looksTargetedTestCommand(command);
216
343
  if (!isPrimaryCommand && !isFallbackCommand && !isVerificationCommand(command)) {
217
344
  process.exit(0);
218
345
  }
@@ -223,10 +350,21 @@ const helperSource = routeSummary?.helperHint
223
350
  || '';
224
351
  const helperCommand = helperSource ? ` Helper: ${helperSource}` : '';
225
352
  const missingPrimary = primaryCommands.filter((item) => !attemptedCommands.has(item));
353
+ const recentTargetedLaneCompleted = hasCompletedRecentTargetedLane({
354
+ routeCache,
355
+ attemptedCommands,
356
+ command,
357
+ maxAgeMs: STATE_FRESH_MS,
358
+ });
226
359
  const preferredText = formatCompactCommandList(preferredOrder, { separator: ' -> ' });
227
360
  const missingPrimaryText = formatCompactCommandList(missingPrimary);
228
361
 
229
- if (policyMode === 'confirm-then-broad' && !explicitBroadRequested) {
362
+ if (isTargetedVerification) {
363
+ persistAttempt(command);
364
+ process.exit(0);
365
+ }
366
+
367
+ if (policyMode === 'confirm-then-broad' && isBroadTestCommand(command) && !explicitBroadRequested && !recentTargetedLaneCompleted) {
230
368
  deny(
231
369
  `BLOCKED: Ask the user before broad verification. Routed policy is confirm-then-broad because the index could not localize related tests confidently.${helperCommand}`,
232
370
  );
@@ -61,7 +61,7 @@ Docs should **not** become:
61
61
 
62
62
  - update README / docs after code changes
63
63
  - fix source-vs-doc drift
64
- - improve `docs/PROJECT.md`, `docs/MEMORY.md`, `docs/WORKLOG.md`, `docs/CODE_MAP.md`
64
+ - improve `docs/PROJECT.md`, `docs/MEMORY.md`, `docs/STATUS.md`, `docs/WORKLOG.md`, `docs/CODE_MAP.md`
65
65
  - prepare durable handoff notes
66
66
  - tighten or remove stale setup instructions
67
67
  - document newly discovered module boundaries, dangerous areas, or bug patterns
@@ -97,6 +97,14 @@ Use for durable knowledge:
97
97
 
98
98
  Prefer short entries that future sessions can scan in seconds.
99
99
 
100
+ #### `docs/STATUS.md`
101
+ Use for compact current state:
102
+ - current focus / active work
103
+ - live debug threads, blockers, and verification status
104
+ - next candidates for open-ended continuation
105
+
106
+ Do **not** treat it as source truth or duplicate full session history here.
107
+
100
108
  #### `docs/WORKLOG.md`
101
109
  Use for session-level execution history:
102
110
  - what changed
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: next-step
3
+ description: Use when the user asks what to do next, asks for project status, says continue/làm tiếp without a concrete target, or needs a lightweight session-start orientation from docs/STATUS.md. Do not use as the primary skill for concrete debug, implementation, review, or docs tasks.
4
+ ---
5
+
6
+ # Next Step
7
+
8
+ ## Purpose
9
+
10
+ Suggest the next useful project action without scanning the whole repository.
11
+
12
+ This skill is for open-ended continuity prompts such as:
13
+ - "what next?", "project status?", "where are we?"
14
+ - "continue", "continue from last session"
15
+ - "làm gì tiếp?", "làm tiếp", "project đang ở đâu?"
16
+
17
+ It is **not** a replacement for concrete workflows. If the user names a bug, feature, file, test, PR, or review target, keep the concrete workflow primary and use this skill only as background if useful.
18
+
19
+ ## Hard Guardrails
20
+
21
+ - End users should not need to know this skill name.
22
+ - Do not ask users to run a `next-step` command. The human workflow remains `ukit install` + natural language.
23
+ - `docs/STATUS.md` is orientation, not truth. Source code, tests, and the UKit index win.
24
+ - Concrete task beats open-ended wording. Example: "I'm fixing login bug but unsure next step" is a debug approach question, not a global roadmap request.
25
+ - Do not scan the whole repo unless `docs/STATUS.md` is missing/stale and the user still wants project-level direction.
26
+
27
+ ## Freshness Check
28
+
29
+ Before relying on status content, check both:
30
+ 1. filesystem modified time for `docs/STATUS.md` (fallback: root `STATUS.md` if present)
31
+ 2. the `Last meaningful update` field inside the file
32
+
33
+ Use this cue in your response:
34
+
35
+ - 🟢 fresh: modified within 24h
36
+ - 🟡 possibly stale: 24-72h old
37
+ - 🔴 stale: older than 72h
38
+ - ⚪ missing: no status file yet
39
+
40
+ If stale or missing, downgrade confidence and verify with the smallest current tree/index/context signal before recommending work.
41
+
42
+ ## Input Order
43
+
44
+ Read only what is needed:
45
+ 1. `docs/STATUS.md` (or existing root `STATUS.md` fallback)
46
+ 2. `docs/CODE_MAP.md` only when navigation is needed
47
+ 3. `docs/MEMORY.md` only when constraints/decisions affect the suggestion
48
+ 4. routed index/tree summary only if status is stale, missing, or contradicted
49
+
50
+ ## Output Shape
51
+
52
+ Keep output compact:
53
+
54
+ ```md
55
+ STATUS freshness: 🟡 possibly stale
56
+ - last modified: YYYY-MM-DD HH:mm
57
+ - confidence: medium-low
58
+
59
+ Suggested next steps:
60
+ 1. Candidate — why now — likely files — verification — risk
61
+ 2. Candidate — why now — likely files — verification — risk
62
+
63
+ Recommended: ...
64
+ Why: ...
65
+ First check: ...
66
+ ```
67
+
68
+ ## Intent Handling
69
+
70
+ - `open-ended-status`: read status and suggest 1-3 candidates.
71
+ - `continue-existing-work`: prefer Active Work / Current Debug Threads, then Next Candidates.
72
+ - `scoped-advice`: do not produce a global roadmap; hand off to the concrete skill and suggest the immediate approach only.
73
+ - `debug-specific`, `implement-specific`, `review-specific`: do not use this skill as primary.
74
+
75
+ ## Missing or Stale Status
76
+
77
+ If missing, suggest creating/updating `docs/STATUS.md` after the current task, but do not block the user.
78
+ If stale, say so visibly and treat old Next Candidates as hypotheses, not fact.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: update-status
3
+ description: Use near the end of a meaningful session or explicit handoff/status request to update docs/STATUS.md with compact current state, verification, blockers, and next candidates. Skip trivial/no-state-change tasks.
4
+ ---
5
+
6
+ # Update Status
7
+
8
+ ## Purpose
9
+
10
+ Keep `docs/STATUS.md` useful as a compact living state document for future AI sessions.
11
+
12
+ This skill should run when:
13
+ - the user explicitly asks to update status / handoff / wrap up
14
+ - meaningful source/docs/tests/package state changed
15
+ - a task is paused, blocked, completed, or has a new verified next action
16
+ - a bug root cause, verification result, or durable blocker was discovered
17
+
18
+ Do **not** run for trivial typo-only edits, pure Q&A, or exploration with no durable finding.
19
+
20
+ ## Hard Guardrails
21
+
22
+ - End users should not need to know this skill name.
23
+ - Do not ask users to run a `update-status` command. The human workflow remains `ukit install` + natural language.
24
+ - Keep `docs/STATUS.md` compact. It is not `docs/WORKLOG.md` and not a raw session transcript.
25
+ - Source code and tests are truth. If status conflicts with source, update status to match source or mark uncertainty.
26
+ - Do not invent verification. Record only commands actually run and their outcome.
27
+ - Preserve user edits and rewrite the smallest relevant section.
28
+
29
+ ## Update Decision
30
+
31
+ Before editing, ask internally:
32
+
33
+ 1. Did project state actually change?
34
+ 2. Is any work still active/blocked?
35
+ 3. Was a root cause, decision, blocker, or next action confirmed?
36
+ 4. What verification ran?
37
+ 5. Will future sessions be faster if this is recorded?
38
+
39
+ If all answers are no, skip the update and mention that no status change was needed.
40
+
41
+ ## Freshness Fields
42
+
43
+ When updating meaningful state, update the header:
44
+
45
+ ```md
46
+ ## Freshness
47
+
48
+ - Last meaningful update: YYYY-MM-DD HH:mm
49
+ - Updated by: Claude / Codex / OpenCode / human
50
+ - Status confidence: high / medium / low
51
+ - Stale after: 72h
52
+ ```
53
+
54
+ Confidence guide:
55
+ - `high`: implementation/fix verified by targeted tests or stronger
56
+ - `medium`: state changed but verification was partial or docs-only
57
+ - `low`: investigation/hypothesis only, blocked, or stale recovery
58
+
59
+ ## Section Rules
60
+
61
+ - `Snapshot`: only current focus, health, branch/state, release/version.
62
+ - `Active Work`: only live unfinished work.
63
+ - `Current Debug Threads`: compact bug state; detailed context should link to future `docs/context/<slug>.md` files when available.
64
+ - `Decisions Pending`: unresolved choices only.
65
+ - `Next Candidates`: max 3-5 actionable candidates.
66
+ - `Recently Completed`: max 10 compact lines; detailed session history belongs in `docs/WORKLOG.md`.
67
+
68
+ ## What to Record
69
+
70
+ Prefer concrete bullets:
71
+
72
+ - files changed or likely involved
73
+ - verification commands and pass/fail/blocked result
74
+ - current blocker
75
+ - next action that can be started immediately
76
+ - uncertainty labels (`unknown`, `unverified`, `suspected`) when needed
77
+
78
+ Avoid:
79
+
80
+ - raw command output
81
+ - every file opened/read
82
+ - speculative TODO spam
83
+ - duplicate history already better suited for WORKLOG
84
+ - global roadmap changes caused by a concrete one-file task
85
+
86
+ ## Future Context Note
87
+
88
+ Do not create task-scoped `CONTEXT.md` as part of v1.1.8 unless the project already has that convention. If detailed bug/feature context is needed, note it as a v1.2 candidate or link to an existing `docs/context/<slug>.md` file.
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import * as indexCore from './lib/index-core.mjs';
4
+
5
+ const {
6
+ buildCodeIndex,
7
+ DEFAULT_INDEX_CACHE_MAX_AGE_MS,
8
+ getIndexArtifactGeneratedAt,
9
+ isIndexStale,
10
+ } = indexCore;
11
+
12
+ const resolveImpactContext = typeof indexCore.resolveImpactContext === 'function'
13
+ ? indexCore.resolveImpactContext
14
+ : null;
15
+ const formatImpactConfidenceSummary = typeof indexCore.formatImpactConfidenceSummary === 'function'
16
+ ? indexCore.formatImpactConfidenceSummary
17
+ : null;
18
+
19
+ const CALLEE_CAP = 5;
20
+ const CALLER_CAP = 5;
21
+ const MIRROR_CAP = 3;
22
+ const TEST_CAP = 4;
23
+ const VERIFY_CAP = 3;
24
+
25
+ async function main() {
26
+ const args = process.argv.slice(2);
27
+ const rootDir = getRootDir(args);
28
+ const changedFiles = parseCsv(readFlagValue(args, '--changed'));
29
+ const changedSymbols = parseCsv(readFlagValue(args, '--symbol'));
30
+
31
+ if (changedFiles.length === 0) {
32
+ console.error('Usage: node .claude/ukit/index/impact-context.mjs --changed <file[,file]> [--symbol <name[,name]>] [--root <dir>]');
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+
37
+ await ensureFreshIndex(rootDir);
38
+
39
+ const impact = resolveImpactContext
40
+ ? await resolveImpactContext({ rootDir, changedFiles, changedSymbols })
41
+ : {
42
+ changedFiles,
43
+ changedSymbols,
44
+ riskLabels: [],
45
+ callees: [],
46
+ callers: [],
47
+ importers: [],
48
+ mirrorCounterparts: [],
49
+ relatedTests: [],
50
+ recommendedTestFiles: [],
51
+ };
52
+
53
+ printCompact(impact);
54
+
55
+ if (formatImpactConfidenceSummary) {
56
+ console.log('');
57
+ console.log(formatImpactConfidenceSummary(impact));
58
+ }
59
+ }
60
+
61
+ async function ensureFreshIndex(rootDir) {
62
+ const generatedAtMs = await getIndexArtifactGeneratedAt({ rootDir });
63
+ const stale = generatedAtMs === null
64
+ ? true
65
+ : await isIndexStale({
66
+ rootDir,
67
+ maxAgeMs: DEFAULT_INDEX_CACHE_MAX_AGE_MS,
68
+ now: Date.now(),
69
+ generatedAtMs,
70
+ });
71
+
72
+ if (stale) {
73
+ await buildCodeIndex({ rootDir });
74
+ }
75
+ }
76
+
77
+ function printCompact(impact) {
78
+ const changed = (impact.changedFiles ?? []).slice(0, 5).join(', ');
79
+ const symbols = (impact.changedSymbols ?? []).slice(0, 5).join(', ');
80
+ const risk = (impact.riskLabels ?? []).slice(0, 5).join(', ');
81
+ const callees = unique((impact.callees ?? []).map((entry) => entry?.symbol).filter(Boolean)).slice(0, CALLEE_CAP).join(', ');
82
+ const callers = unique((impact.callers ?? []).map((entry) => entry?.filePath).filter(Boolean)).slice(0, CALLER_CAP).join(', ');
83
+ const mirrors = unique((impact.mirrorCounterparts ?? []).map((entry) => entry?.filePath).filter(Boolean)).slice(0, MIRROR_CAP).join(', ');
84
+ const tests = (impact.recommendedTestFiles ?? impact.relatedTests ?? []).slice(0, TEST_CAP);
85
+ const verify = tests.slice(0, VERIFY_CAP).map((testFile) => `yarn test ${testFile}`).join(' | ');
86
+
87
+ console.log('[ukit:impact]');
88
+ console.log(`changed: ${changed || '(none)'}`);
89
+ if (symbols) console.log(`symbols: ${symbols}`);
90
+ if (risk) console.log(`risk: ${risk}`);
91
+ if (callees) console.log(`callees: ${callees}`);
92
+ if (callers) console.log(`callers: ${callers}`);
93
+ if (mirrors) console.log(`mirrors: ${mirrors}`);
94
+ if (tests.length > 0) console.log(`related-tests: ${tests.join(', ')}`);
95
+ if (verify) console.log(`verify: ${verify}`);
96
+ }
97
+
98
+ function parseCsv(value) {
99
+ return unique(
100
+ String(value ?? '')
101
+ .split(',')
102
+ .map((entry) => entry.trim().replace(/^\.\//, ''))
103
+ .filter(Boolean),
104
+ );
105
+ }
106
+
107
+ function unique(values = []) {
108
+ return [...new Set(values)];
109
+ }
110
+
111
+ function readFlagValue(args, flag) {
112
+ const index = args.indexOf(flag);
113
+ if (index === -1) return null;
114
+ return args[index + 1] ?? null;
115
+ }
116
+
117
+ function getRootDir(args) {
118
+ const explicitRoot = readFlagValue(args, '--root');
119
+ return path.resolve(explicitRoot || process.cwd());
120
+ }
121
+
122
+ await main();