@ngockhoale/ukit 2.3.23 → 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 CHANGED
@@ -2,6 +2,40 @@
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
+
5
39
  ## 2.3.23 - 2026-09-13
6
40
 
7
41
  Self-install no longer clobbers UKit's own canonical docs. Running `ukit install` inside the UKit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.3.23",
3
+ "version": "2.3.24",
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",
@@ -45,22 +45,25 @@ function safeResolve(basePath, relativePath) {
45
45
  return resolvedPath;
46
46
  }
47
47
 
48
- function isPathInside(parentPath, childPath) {
49
- const relative = path.relative(parentPath, childPath);
50
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
51
- }
52
-
53
48
  function isDocsTarget(targetPath, projectRoot) {
54
49
  const relative = path.relative(projectRoot, targetPath);
55
50
  return relative === 'docs' || relative.startsWith(`docs${path.sep}`);
56
51
  }
57
52
 
58
53
  // 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).
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.
62
65
  async function isSelfInstall({ templatesRoot, projectRoot }) {
63
- if (isPathInside(projectRoot, templatesRoot)) {
66
+ if (path.dirname(path.resolve(templatesRoot)) === path.resolve(projectRoot)) {
64
67
  return true;
65
68
  }
66
69
  try {
@@ -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)