@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
|
@@ -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
|
-
|
|
133
|
-
process.
|
|
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
|
|
199
|
-
|
|
200
|
-
:
|
|
201
|
-
|
|
310
|
+
const attemptedCommands = collectAttemptedCommands(progress, {
|
|
311
|
+
currentFingerprint: state?.fingerprint || null,
|
|
312
|
+
maxAgeMs: STATE_FRESH_MS,
|
|
313
|
+
});
|
|
202
314
|
|
|
203
315
|
function persistAttempt(commandText) {
|
|
204
|
-
|
|
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 =
|
|
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 (
|
|
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();
|