@ngockhoale/ukit 2.3.22 → 2.3.24
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 +53 -0
- package/package.json +1 -1
- package/src/core/buildPlan.js +52 -0
- package/src/index/taskRouting.js +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.3.24 - 2026-09-13
|
|
6
|
+
|
|
7
|
+
Post-2.3.23 routing and self-install audit. Two real defects fixed, plus the repo-local
|
|
8
|
+
Release Policy mirror restored and guarded.
|
|
9
|
+
|
|
10
|
+
- **`isInformationalPrompt` ported into the src canonical copy.** `src/index/taskRouting.js`
|
|
11
|
+
was missing the informational-prompt guard that the shipped copies in
|
|
12
|
+
`templates/.claude/ukit/index/route-task.mjs` and `templates/.claude/hooks/skill-router.sh`
|
|
13
|
+
received in 2.2.14. The 2.3.22 `consultationOnlySignal` only catches question-shaped
|
|
14
|
+
prompts (trailing `?`, a leading interrogative, or Vietnamese `…là gì / được không`), so a
|
|
15
|
+
prompt with an **embedded** interrogative and no leading marker — `"please tell me what the
|
|
16
|
+
auth module does"`, `"explain what files the installer writes"` — routed to `shared-edit`
|
|
17
|
+
with a write+verification contract in src while the shipped templates routed it
|
|
18
|
+
`informational`. That is exactly the fabricated-mutation-debt class 2.3.22 set out to fix,
|
|
19
|
+
still live in the copy that drives `ukit index route`. The helper is now byte-equivalent
|
|
20
|
+
across all three copies; implement/debug-worded questions still keep their lanes. Locked by
|
|
21
|
+
a new `tests/index/taskRouting.test.js` regression (verified RED without the call site).
|
|
22
|
+
- **Self-install detection no longer misfires on a local install.** The 2.3.23 guard treated
|
|
23
|
+
any `templates/` directory inside the install root as a self-install. A downstream project
|
|
24
|
+
that installs UKit locally (`node_modules/@ngockhoale/ukit/templates`) therefore matched,
|
|
25
|
+
and every `docs/` entry was downgraded to `skip` — so `ukit update` silently stopped
|
|
26
|
+
refreshing that project's docs. `isSelfInstall` now requires the package root itself to be
|
|
27
|
+
the project root (`<projectRoot>/templates` exactly) or the target `package.json` to name
|
|
28
|
+
`@ngockhoale/ukit`. Locked by a new `buildPlan` regression for the `node_modules` shape
|
|
29
|
+
(verified RED under the old containment test).
|
|
30
|
+
- **Release Policy mirror restored.** A `ukit install` run inside this repo (self-install)
|
|
31
|
+
had overwritten root `CLAUDE.md`/`AGENTS.md` from `templates/`, wiping the repo-local
|
|
32
|
+
Release Policy section that `docs/PROJECT.md` mandates at the top of both files. Restored
|
|
33
|
+
from the byte-identical pre-wipe copy; a new `tests/consistency/configDocsSync.test.js`
|
|
34
|
+
guard fails loudly if a future self-install wipes either copy again.
|
|
35
|
+
- **Verification.** `yarn test` 87 files / 1,553 tests green (also under `--sequence.shuffle`);
|
|
36
|
+
`yarn release:verify` exit 0; artifact unpacked ceiling raised once to 5,960,000 with
|
|
37
|
+
rationale in `tests/integration/packageArtifact.test.js`.
|
|
38
|
+
|
|
39
|
+
## 2.3.23 - 2026-09-13
|
|
40
|
+
|
|
41
|
+
Self-install no longer clobbers UKit's own canonical docs. Running `ukit install` inside the UKit
|
|
42
|
+
source repository (e.g. the global CLI invoked from the repo) overwrote `docs/PROMPT_CACHING.md` —
|
|
43
|
+
the canonical 368-line ruleset — with the distilled 127-line copy shipped under `templates/docs/`,
|
|
44
|
+
because the manifest item is `overwrite_with_backup` and self-install went undetected.
|
|
45
|
+
|
|
46
|
+
- **Self-install detection.** `buildInstallPlan` now recognizes an install targeting the UKit repo
|
|
47
|
+
by two independent signals: the templates resolving inside the install target (the repo has a
|
|
48
|
+
`templates/` directory, the global CLI does not), or the target `package.json` naming
|
|
49
|
+
`@ngockhoale/ukit`. This covers the common case where the global CLI runs inside the repo, which
|
|
50
|
+
the path-containment check alone misses.
|
|
51
|
+
- **Docs downgraded to seed-only.** On a detected self-install, every `overwrite_with_backup`
|
|
52
|
+
entry whose target lives under `docs/` is downgraded to `skip`: a missing doc is seeded, but an
|
|
53
|
+
existing canonical doc is never overwritten. Downstream projects are unaffected — their docs
|
|
54
|
+
refresh on `ukit update` as before.
|
|
55
|
+
- **Verification.** New `buildInstallPlan` regressions cover the containment signal, the
|
|
56
|
+
package-name signal, and the unchanged downstream path; the full suite stays green.
|
|
57
|
+
|
|
5
58
|
## 2.3.22 - 2026-09-13
|
|
6
59
|
|
|
7
60
|
Consultation-question routing fix — pure questions no longer fabricate mutation debt that the
|
package/package.json
CHANGED
package/src/core/buildPlan.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { selectManifestItems } from '../manifest/selectItems.js';
|
|
4
4
|
import { renderTemplateString } from '../render/renderTemplate.js';
|
|
5
|
+
import { UKIT_PACKAGE_NAME } from './update.js';
|
|
5
6
|
|
|
6
7
|
const BINARY_TEMPLATE_EXTENSIONS = new Set([
|
|
7
8
|
'.ttf',
|
|
@@ -44,6 +45,55 @@ function safeResolve(basePath, relativePath) {
|
|
|
44
45
|
return resolvedPath;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
function isDocsTarget(targetPath, projectRoot) {
|
|
49
|
+
const relative = path.relative(projectRoot, targetPath);
|
|
50
|
+
return relative === 'docs' || relative.startsWith(`docs${path.sep}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Detect `ukit install` running against the UKit source repository itself. Two independent
|
|
54
|
+
// signals: the templates resolve directly under the install target (`<projectRoot>/templates`
|
|
55
|
+
// — the repo layout, which the global CLI does not have), or the target's package.json
|
|
56
|
+
// declares UKIT_PACKAGE_NAME (the path check misses the common case where the global CLI runs
|
|
57
|
+
// inside the repo).
|
|
58
|
+
//
|
|
59
|
+
// The path signal deliberately requires `<projectRoot>/templates` exactly, not merely
|
|
60
|
+
// "templates somewhere inside projectRoot". A downstream project that installs UKit locally
|
|
61
|
+
// (`node_modules/@ngockhoale/ukit/templates`) also has templates inside its root; treating
|
|
62
|
+
// that as a self-install downgraded every docs entry to `skip`, so `ukit update` never
|
|
63
|
+
// refreshed that project's docs. Only the package root itself being the project root is a
|
|
64
|
+
// self-install.
|
|
65
|
+
async function isSelfInstall({ templatesRoot, projectRoot }) {
|
|
66
|
+
if (path.dirname(path.resolve(templatesRoot)) === path.resolve(projectRoot)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const raw = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8');
|
|
71
|
+
return JSON.parse(raw)?.name === UKIT_PACKAGE_NAME;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// In the UKit source repo, docs/ holds canonical sources that deliberately diverge from the
|
|
78
|
+
// shipped copies under templates/ (docs/PROMPT_CACHING.md is the full ruleset while
|
|
79
|
+
// templates/docs/ ships a distilled version). The refresh-on-update strategy would silently
|
|
80
|
+
// clobber those canonical files, so downgrade docs overwrites to skip: seed a missing doc, but
|
|
81
|
+
// never overwrite one that already exists.
|
|
82
|
+
async function downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot }) {
|
|
83
|
+
if (!(await isSelfInstall({ templatesRoot, projectRoot }))) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const entry of planEntries) {
|
|
88
|
+
if (entry.type === 'link' || entry.mergeStrategy !== 'overwrite_with_backup') {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (isDocsTarget(entry.targetPath, projectRoot)) {
|
|
92
|
+
entry.mergeStrategy = 'skip';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
47
97
|
async function readTemplateFile(filePath, { binary = false } = {}) {
|
|
48
98
|
if (binary) {
|
|
49
99
|
return fs.readFile(filePath);
|
|
@@ -263,6 +313,8 @@ export async function buildInstallPlan({
|
|
|
263
313
|
}
|
|
264
314
|
}
|
|
265
315
|
|
|
316
|
+
await downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot });
|
|
317
|
+
|
|
266
318
|
return {
|
|
267
319
|
selectedItems,
|
|
268
320
|
entries: planEntries,
|
package/src/index/taskRouting.js
CHANGED
|
@@ -406,6 +406,56 @@ function deriveExecutionScores({
|
|
|
406
406
|
};
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
// A prompt that only asks a question (no command, no action verb, no error report) is
|
|
410
|
+
// asking for an explanation, not a repository change. Routing it to an investigation or
|
|
411
|
+
// edit lane fabricates write/verification debt the completion gate then enforces against an
|
|
412
|
+
// answer turn. Kept byte-equivalent to the shipped copies in
|
|
413
|
+
// templates/.claude/ukit/index/route-task.mjs and templates/.claude/hooks/skill-router.sh;
|
|
414
|
+
// this is the canonical copy those are mirror-tested against. It deliberately ignores
|
|
415
|
+
// targetFile: an edit order phrased as a question is caught by implementWords below, while a
|
|
416
|
+
// pure targeted question ("how does X work?") is still just a question.
|
|
417
|
+
function isInformationalPrompt({
|
|
418
|
+
promptText = '',
|
|
419
|
+
commandText = '',
|
|
420
|
+
scores = null,
|
|
421
|
+
} = {}) {
|
|
422
|
+
if (String(commandText || '').trim()) return false;
|
|
423
|
+
const raw = String(promptText || '').toLowerCase().trim();
|
|
424
|
+
if (!raw) return false;
|
|
425
|
+
const questionSignal = raw.includes('?')
|
|
426
|
+
|| /\b(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
|
|
427
|
+
|| /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu|nghĩa là|được không|không\b)/.test(raw);
|
|
428
|
+
// '?' or a leading/Vietnamese interrogative — required to overrule error wording.
|
|
429
|
+
const strongQuestion = raw.includes('?')
|
|
430
|
+
|| /^(what|how|when|which|where|who|is|are|does|do|did|can|could|would|should|will)\b/.test(raw)
|
|
431
|
+
|| /(là\s+(?:[^\s]+\s+){0,2}gì|thế nào|như thế nào|bao nhiêu|khi nào|bao giờ|ở đâu)/.test(raw);
|
|
432
|
+
if (scores) {
|
|
433
|
+
if (
|
|
434
|
+
scores.implementSignal
|
|
435
|
+
|| scores.reviewSignal
|
|
436
|
+
|| scores.debugSignal
|
|
437
|
+
|| scores.impactSignal
|
|
438
|
+
|| scores.smallFixSignal
|
|
439
|
+
|| scores.directTransformSignal
|
|
440
|
+
) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
if (scores.failureSignal && !strongQuestion) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const implementWords = /(?<![A-Za-z0-9_])(implement|apply|update|modify|add|create|ship|deliver|fix|refactor|remove|delete|rename|change|write|build|make|install|run|deploy|execute|sửa|thêm|tạo|xóa|đổi|thay thế|cập nhật|viết|build|chạy|cài)(?![A-Za-z0-9_])/.test(raw);
|
|
448
|
+
if (implementWords) return false;
|
|
449
|
+
const investigationWords = /\b(why|debug|triage|root cause|investigate|tại sao)\b/.test(raw);
|
|
450
|
+
if (investigationWords) return false;
|
|
451
|
+
// \b never matches around 'lỗi' (diacritics are not \w), so test it as a plain
|
|
452
|
+
// substring: an error report is informational only when phrased as a question.
|
|
453
|
+
if (raw.includes('lỗi') && !strongQuestion) return false;
|
|
454
|
+
const reviewWords = /\b(review|audit|verify)\b/.test(raw);
|
|
455
|
+
if (reviewWords) return false;
|
|
456
|
+
return questionSignal;
|
|
457
|
+
}
|
|
458
|
+
|
|
409
459
|
function deriveExecutionMode({
|
|
410
460
|
promptText = '',
|
|
411
461
|
commandText = '',
|
|
@@ -494,6 +544,10 @@ function deriveExecutionMode({
|
|
|
494
544
|
&& !scores.sharedRisk
|
|
495
545
|
&& !targetFile;
|
|
496
546
|
|
|
547
|
+
if (isInformationalPrompt({ promptText, commandText, scores })) {
|
|
548
|
+
return 'informational';
|
|
549
|
+
}
|
|
550
|
+
|
|
497
551
|
if (
|
|
498
552
|
releaseVerificationContinuation
|
|
499
553
|
|| ((intentMode === 'review-specific' || explicitReviewLead) && !scores.implementSignal)
|