@ngockhoale/ukit 2.3.21 → 2.3.23

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 CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.3.23 - 2026-09-13
6
+
7
+ Self-install no longer clobbers UKit's own canonical docs. Running `ukit install` inside the UKit
8
+ source repository (e.g. the global CLI invoked from the repo) overwrote `docs/PROMPT_CACHING.md` —
9
+ the canonical 368-line ruleset — with the distilled 127-line copy shipped under `templates/docs/`,
10
+ because the manifest item is `overwrite_with_backup` and self-install went undetected.
11
+
12
+ - **Self-install detection.** `buildInstallPlan` now recognizes an install targeting the UKit repo
13
+ by two independent signals: the templates resolving inside the install target (the repo has a
14
+ `templates/` directory, the global CLI does not), or the target `package.json` naming
15
+ `@ngockhoale/ukit`. This covers the common case where the global CLI runs inside the repo, which
16
+ the path-containment check alone misses.
17
+ - **Docs downgraded to seed-only.** On a detected self-install, every `overwrite_with_backup`
18
+ entry whose target lives under `docs/` is downgraded to `skip`: a missing doc is seeded, but an
19
+ existing canonical doc is never overwritten. Downstream projects are unaffected — their docs
20
+ refresh on `ukit update` as before.
21
+ - **Verification.** New `buildInstallPlan` regressions cover the containment signal, the
22
+ package-name signal, and the unchanged downstream path; the full suite stays green.
23
+
24
+ ## 2.3.22 - 2026-09-13
25
+
26
+ Consultation-question routing fix — pure questions no longer fabricate mutation debt that the
27
+ completion gate then enforces against an answer turn. Observed live the same day: two Vietnamese
28
+ consultation questions (no English signal word, no target file) scored every candidate mode ≤ 0,
29
+ the mode ladder's upward bias resolved the find-cause/shared-edit tie to `shared-edit`, and the
30
+ gate blocked the answer turn six times ("missing write-evidence / verification-evidence") until
31
+ the continuation cap released it.
32
+
33
+ - **Consultation questions carry no completion contract.** A question-shaped prompt (trailing `?`,
34
+ a leading English or ASCII-folded Vietnamese interrogative, or a `là gì / được không` tail) with
35
+ no implement/review/debug/failure/impact signal and no target file routes `informational` — the
36
+ same no-mutation-debt lane delivery-only requests already use. Applied to
37
+ `src/index/taskRouting.js`, the standalone router `route-task.mjs`, and the hook-embedded
38
+ routing copy in `skill-router.sh` (live twins byte-identical).
39
+ - **Leading interrogatives override action words.** The router's signal regexes are English-only,
40
+ so Vietnamese how-questions mentioning an action ("Làm sao mà đo được khi tôi cài hệ thống
41
+ này…?") kept a mutation contract. A leading interrogative (làm sao / thế nào / tại sao / phải
42
+ làm gì / …) now marks the prompt consultative even when it contains implement verbs
43
+ (sửa / thêm / cài / cài đặt / …) — asking HOW something is done is not ordering the action
44
+ performed. Question-phrased edit orders ("bạn sửa giúp tôi… không?") keep their write contract.
45
+ - **Verification.** New routing regressions lock the two incident prompts to `informational` with
46
+ empty contract evidence, and lock both guard directions (English signal-word questions and
47
+ question-phrased Vietnamese edit orders stay in their lanes); full suite green.
48
+
5
49
  ## 2.3.21 - 2026-09-13
6
50
 
7
51
  C16 release — the prompt-caching ruleset turns from research into shipped guidance, and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.3.21",
3
+ "version": "2.3.23",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,52 @@ function safeResolve(basePath, relativePath) {
44
45
  return resolvedPath;
45
46
  }
46
47
 
48
+ function isPathInside(parentPath, childPath) {
49
+ const relative = path.relative(parentPath, childPath);
50
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
51
+ }
52
+
53
+ function isDocsTarget(targetPath, projectRoot) {
54
+ const relative = path.relative(projectRoot, targetPath);
55
+ return relative === 'docs' || relative.startsWith(`docs${path.sep}`);
56
+ }
57
+
58
+ // Detect `ukit install` running against the UKit source repository itself. Two independent
59
+ // signals: the templates resolve inside the install target (the repo has a templates/ dir, the
60
+ // global CLI does not), or the target's package.json declares UKIT_PACKAGE_NAME (the path check
61
+ // misses the common case where the global CLI runs inside the repo).
62
+ async function isSelfInstall({ templatesRoot, projectRoot }) {
63
+ if (isPathInside(projectRoot, templatesRoot)) {
64
+ return true;
65
+ }
66
+ try {
67
+ const raw = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8');
68
+ return JSON.parse(raw)?.name === UKIT_PACKAGE_NAME;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // In the UKit source repo, docs/ holds canonical sources that deliberately diverge from the
75
+ // shipped copies under templates/ (docs/PROMPT_CACHING.md is the full ruleset while
76
+ // templates/docs/ ships a distilled version). The refresh-on-update strategy would silently
77
+ // clobber those canonical files, so downgrade docs overwrites to skip: seed a missing doc, but
78
+ // never overwrite one that already exists.
79
+ async function downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot }) {
80
+ if (!(await isSelfInstall({ templatesRoot, projectRoot }))) {
81
+ return;
82
+ }
83
+
84
+ for (const entry of planEntries) {
85
+ if (entry.type === 'link' || entry.mergeStrategy !== 'overwrite_with_backup') {
86
+ continue;
87
+ }
88
+ if (isDocsTarget(entry.targetPath, projectRoot)) {
89
+ entry.mergeStrategy = 'skip';
90
+ }
91
+ }
92
+ }
93
+
47
94
  async function readTemplateFile(filePath, { binary = false } = {}) {
48
95
  if (binary) {
49
96
  return fs.readFile(filePath);
@@ -263,6 +310,8 @@ export async function buildInstallPlan({
263
310
  }
264
311
  }
265
312
 
313
+ await downgradeSelfInstallDocOverwrites(planEntries, { templatesRoot, projectRoot });
314
+
266
315
  return {
267
316
  selectedItems,
268
317
  entries: planEntries,
@@ -452,6 +452,47 @@ function deriveExecutionMode({
452
452
  && !scores.smallFixSignal
453
453
  && !scores.sharedRisk
454
454
  && !targetFile;
455
+ // A pure consultation question ("Rồi việc kế tiếp của tôi phải làm gì…?", "how do I
456
+ // measure this on another machine?") asks for an answer, not a repository change. The
457
+ // signal regexes are English-only, so a Vietnamese (or signal-free English) question
458
+ // with no target file scores every candidate mode ≤ 0 and the ladder's upward bias
459
+ // resolves the 0-score find-cause/shared-edit tie to shared-edit — fabricating
460
+ // write+verification debt that a text answer can never satisfy, after which the
461
+ // completion gate blocks the answer turn until the continuation cap releases it.
462
+ // Consultation questions carry no completion contract instead. A question that also
463
+ // carries an implement/review/debug/failure signal keeps its lane; targeted questions
464
+ // (explicit file target) keep their contract so a Vietnamese edit order against a
465
+ // named file stays enforced.
466
+ const trimmedPromptText = String(promptText || '').trim();
467
+ const foldedPromptText = trimmedPromptText
468
+ .toLowerCase()
469
+ .normalize('NFD')
470
+ .replace(/[\u0300-\u036f]/g, '')
471
+ .replace(/\u0111/g, 'd');
472
+ const leadingInterrogative = /^(?:what|how|which|when|where|who)\b/.test(foldedPromptText)
473
+ || /^(?:lam sao|the nao|nhu the nao|nhu nao|vi sao|tai sao|khi nao|bao gio|bao nhieu|o dau|phai lam gi|lam gi|lam cach nao)\b/.test(foldedPromptText);
474
+ const questionShapedPrompt = /\?\s*$/.test(trimmedPromptText)
475
+ || leadingInterrogative
476
+ || /\b(?:la gi|duoc khong)\s*$/.test(foldedPromptText);
477
+ // Implement verbs (Vietnamese included — the English-only scores above cannot see
478
+ // them) mark an action order even when phrased as a question ("ban sua giup toi…?" is
479
+ // an edit order, not a consultation). A leading interrogative overrides them:
480
+ // "Lam sao ma do duoc khi toi cai…?" asks HOW something is done, it does not order
481
+ // the action performed.
482
+ const consultationImplementWords = /(?<![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|chạy|cài)(?![A-Za-z0-9_])/.test(trimmedPromptText);
483
+ const consultationOnlySignal = questionShapedPrompt
484
+ && (leadingInterrogative || !consultationImplementWords)
485
+ && scores.editCertainty === 0
486
+ && !scores.implementSignal
487
+ && !scores.reviewSignal
488
+ && !scores.debugSignal
489
+ && !scores.failureSignal
490
+ && !scores.impactSignal
491
+ && !scores.buildSignal
492
+ && !scores.directTransformSignal
493
+ && !scores.smallFixSignal
494
+ && !scores.sharedRisk
495
+ && !targetFile;
455
496
 
456
497
  if (
457
498
  releaseVerificationContinuation
@@ -460,7 +501,7 @@ function deriveExecutionMode({
460
501
  return 'review-release';
461
502
  }
462
503
 
463
- if (deliveryOnlySignal) {
504
+ if (deliveryOnlySignal || consultationOnlySignal) {
464
505
  return 'informational';
465
506
  }
466
507
 
@@ -1652,7 +1652,38 @@ const { pathToFileURL } = require('url');
1652
1652
  return 'review-release';
1653
1653
  }
1654
1654
 
1655
- if (deliveryOnlySignal) {
1655
+ const trimmedPromptText = String(promptText || '').trim();
1656
+ const foldedPromptText = trimmedPromptText
1657
+ .toLowerCase()
1658
+ .normalize('NFD')
1659
+ .replace(/[\u0300-\u036f]/g, '')
1660
+ .replace(/\u0111/g, 'd');
1661
+ const leadingInterrogative = /^(?:what|how|which|when|where|who)\b/.test(foldedPromptText)
1662
+ || /^(?:lam sao|the nao|nhu the nao|nhu nao|vi sao|tai sao|khi nao|bao gio|bao nhieu|o dau|phai lam gi|lam gi|lam cach nao)\b/.test(foldedPromptText);
1663
+ const questionShapedPrompt = /\?\s*$/.test(trimmedPromptText)
1664
+ || leadingInterrogative
1665
+ || /\b(?:la gi|duoc khong)\s*$/.test(foldedPromptText);
1666
+ // Implement verbs (Vietnamese included — the English-only scores above cannot see
1667
+ // them) mark an action order even when phrased as a question ("ban sua giup toi…?" is
1668
+ // an edit order, not a consultation). A leading interrogative overrides them:
1669
+ // "Lam sao ma do duoc khi toi cai…?" asks HOW something is done, it does not order
1670
+ // the action performed.
1671
+ const consultationImplementWords = /(?<![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|chạy|cài)(?![A-Za-z0-9_])/.test(trimmedPromptText);
1672
+ const consultationOnlySignal = questionShapedPrompt
1673
+ && (leadingInterrogative || !consultationImplementWords)
1674
+ && scores.editCertainty === 0
1675
+ && !scores.implementSignal
1676
+ && !scores.reviewSignal
1677
+ && !scores.debugSignal
1678
+ && !scores.failureSignal
1679
+ && !scores.impactSignal
1680
+ && !scores.buildSignal
1681
+ && !scores.directTransformSignal
1682
+ && !scores.smallFixSignal
1683
+ && !scores.sharedRisk
1684
+ && !targetFile;
1685
+
1686
+ if (deliveryOnlySignal || consultationOnlySignal) {
1656
1687
  return 'informational';
1657
1688
  }
1658
1689
 
@@ -1832,7 +1832,38 @@ function deriveExecutionMode({
1832
1832
  return 'review-release';
1833
1833
  }
1834
1834
 
1835
- if (deliveryOnlySignal) {
1835
+ const trimmedPromptText = String(promptText || '').trim();
1836
+ const foldedPromptText = trimmedPromptText
1837
+ .toLowerCase()
1838
+ .normalize('NFD')
1839
+ .replace(/[\u0300-\u036f]/g, '')
1840
+ .replace(/\u0111/g, 'd');
1841
+ const leadingInterrogative = /^(?:what|how|which|when|where|who)\b/.test(foldedPromptText)
1842
+ || /^(?:lam sao|the nao|nhu the nao|nhu nao|vi sao|tai sao|khi nao|bao gio|bao nhieu|o dau|phai lam gi|lam gi|lam cach nao)\b/.test(foldedPromptText);
1843
+ const questionShapedPrompt = /\?\s*$/.test(trimmedPromptText)
1844
+ || leadingInterrogative
1845
+ || /\b(?:la gi|duoc khong)\s*$/.test(foldedPromptText);
1846
+ // Implement verbs (Vietnamese included — the English-only scores above cannot see
1847
+ // them) mark an action order even when phrased as a question ("ban sua giup toi…?" is
1848
+ // an edit order, not a consultation). A leading interrogative overrides them:
1849
+ // "Lam sao ma do duoc khi toi cai…?" asks HOW something is done, it does not order
1850
+ // the action performed.
1851
+ const consultationImplementWords = /(?<![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|chạy|cài)(?![A-Za-z0-9_])/.test(trimmedPromptText);
1852
+ const consultationOnlySignal = questionShapedPrompt
1853
+ && (leadingInterrogative || !consultationImplementWords)
1854
+ && scores.editCertainty === 0
1855
+ && !scores.implementSignal
1856
+ && !scores.reviewSignal
1857
+ && !scores.debugSignal
1858
+ && !scores.failureSignal
1859
+ && !scores.impactSignal
1860
+ && !scores.buildSignal
1861
+ && !scores.directTransformSignal
1862
+ && !scores.smallFixSignal
1863
+ && !scores.sharedRisk
1864
+ && !targetFile;
1865
+
1866
+ if (deliveryOnlySignal || consultationOnlySignal) {
1836
1867
  return 'informational';
1837
1868
  }
1838
1869