@ngockhoale/ukit 3.4.28 → 3.4.30
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 +23 -0
- package/manifests/documentation.yaml +88 -0
- package/package.json +1 -1
- package/src/core/buildPlan.js +11 -1
- package/src/core/permissionPolicy.js +1 -1
- package/src/core/status.js +3 -1
- package/src/index/buildIndex.js +244 -4
- package/src/index/impactContext.js +91 -11
- package/src/index/languageTools.js +126 -0
- package/src/index/paths.js +5 -1
- package/src/index/playbookRegistry.js +108 -9
- package/src/index/queryIndex.js +415 -1
- package/src/index/routeCatalog.js +1 -1
- package/src/index/taskRouting.js +25 -3
- package/template_project/.claude/commands/ukit/handoff-create.md +5 -0
- package/template_project/.claude/commands/ukit/handoff-fullstack.md +43 -4
- package/template_project/.claude/hooks/block-dangerous.mjs +7 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +235 -21
- package/template_project/.claude/hooks/record-execution.mjs +23 -0
- package/template_project/.claude/hooks/skill-router.sh +105 -7
- package/template_project/.claude/settings.json +21 -1
- package/template_project/.claude/ukit/index/cleanup-task-worktree.mjs +1 -1
- package/template_project/.claude/ukit/index/impact-context.mjs +70 -15
- package/template_project/.claude/ukit/index/land-worktree.mjs +2 -2
- package/template_project/.claude/ukit/index/lib/index-core.mjs +883 -14
- package/template_project/.claude/ukit/index/playbook-registry.mjs +108 -9
- package/template_project/.claude/ukit/index/query-index.mjs +100 -7
- package/template_project/.claude/ukit/index/route-catalog.mjs +1 -1
- package/template_project/.claude/ukit/index/route-task.mjs +208 -31
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +455 -24
- package/template_project/.claude/ukit/runtime/handoff-intent.mjs +122 -6
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +417 -0
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +635 -28
- package/template_project/.omp/hooks/pre/ukit-bridge.js +32 -4
- package/template_user/playbooks/feature-implementation.md +9 -0
- package/template_user/playbooks/migration.md +4 -0
|
@@ -21,6 +21,7 @@ import os from 'node:os';
|
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
|
|
24
|
+
import { isRunAllRequest } from '../runtime/handoff-intent.mjs';
|
|
24
25
|
import { deriveEscalationTriggers, deriveRiskFloor } from './route-resolver.mjs';
|
|
25
26
|
|
|
26
27
|
const PLAYBOOK_NAME_RE = /\.md$/i;
|
|
@@ -47,7 +48,10 @@ export const PLAYBOOK_ID_ALIASES = Object.freeze({
|
|
|
47
48
|
// Compiled once; every row's discriminator reads the same normalized signals so
|
|
48
49
|
// confusable pairs split on the discriminator column, never trigger text.
|
|
49
50
|
const CREATE_VERB_RE = /\b(create|add|write|build|implement|scaffold|tạo|thêm)\b/i;
|
|
50
|
-
|
|
51
|
+
// Vietnamese words never match `\b` (JS word chars are ASCII-only — 'sửa' ends
|
|
52
|
+
// in a non-word char, 'đổi' starts with one), so the diacritic alternates use
|
|
53
|
+
// explicit lookarounds instead. Same word list as the original literal.
|
|
54
|
+
const CHANGE_VERB_RE = /(?:\b(?:fix|update|modify|change|rename|refactor|restructure|remove|delete|move|migrate|improve|patch|rework)\b|(?<![A-Za-z0-9_])(?:sửa|sua|đổi|doi|cập\s+nhật|cap\s+nhat|thay\s+thế|thay\s+the|thay|chỉnh(?:\s+sửa|\s+sua)?|chinh(?:\s+sua)?|xóa|xoa)(?![A-Za-z0-9_]))/i;
|
|
51
55
|
const INVESTIGATION_TRIGGER_RE = /\b(how does|how do|why does|why do|why is|why are|explain|what happens|investigate|understand how|validate the assumption|tại sao|như thế nào|thế nào)\b/i;
|
|
52
56
|
const DEFECT_TRIGGER_RE = /\b(fix|bug|defect|broken|crash(?:es|ed)?|regression|not working|doesn['’]?t work|won['’]?t work|fails?|failing|không hoạt động|lỗi)\b/i;
|
|
53
57
|
const CHECK_VERB_RE = /\b(verify|check|confirm|prove|validate|assert)\b/i;
|
|
@@ -76,6 +80,68 @@ const AUTONOMOUS_TRIGGER_RE = /\b(finish (?:this|the) backlog|bounded (?:long )?
|
|
|
76
80
|
const RELEASE_TRIGGER_RE = /\b(publish|ship it|release|bump (?:the )?version|deploy|cut a release|tag (?:the )?release)\b/i;
|
|
77
81
|
const HANDOFF_CYCLE_TRIGGER_RE = /\b(handoff|multi[- ]?task cycle|task pipeline|execute the plan|worktree)\b/i;
|
|
78
82
|
|
|
83
|
+
// --- Routing-orchestration signals (reference/clone/SQL/reuse/run-all) -------
|
|
84
|
+
// Shared vocabulary for the discriminators below AND for the additive route
|
|
85
|
+
// summary fields route-task.mjs emits (referenceTargets/cloneIntent/sqlIntent/
|
|
86
|
+
// reuseFirst/goalScope). Kept here so both router surfaces read one definition.
|
|
87
|
+
// Reference-based ask: "like the Customer page", "làm giống trang Customer",
|
|
88
|
+
// "same as module Y", "using validation from Z" — resolve the named source.
|
|
89
|
+
const REFERENCE_TARGET_RE = /(?:\b(?:like|same\s+as|similar\s+to|based\s+on|modell?ed\s+(?:on|after))\s+(?:the\s+|this\s+|that\s+|our\s+)?(?:existing\s+)?(?:[\w-]{1,32}\s){0,2}(?:page|screen|module|component|view|flow|dropdown|select|form|report)\b)|\b(?:page|screen|module|component|view|flow)\b[^\n?]{0,32}\b(?:like|same\s+as)\b|(?<![A-Za-z0-9_])(?:giống|giong|tương\s+tự|tuong\s+tu|như)\s+(?:cái\s+)?(?:trang|page|module|component|tính\s+năng|tinh\s+nang|man\s+hình|view)|(?<![A-Za-z0-9_])(?:trang|page|module|component|man\s+hình)\s+(?:giống|giong|tương\s+tự|tuong\s+tu)|\b(?:using|reuse(?:d)?)\s+(?:the\s+)?(?:validation|logic|pattern|structure|style)\b[^\n?]{0,32}\bfrom\b/i;
|
|
90
|
+
// Clone ask: duplicate a named surface ("clone the Customer page", "nhân bản
|
|
91
|
+
// trang này", "copy the login screen"). A bare "clone the repo" (git) and a
|
|
92
|
+
// bare "copy" without a surface noun stay unmatched.
|
|
93
|
+
const CLONE_INTENT_RE = /(?:\b(?:clone|duplicate|copy)\s+(?:the\s+|this\s+|that\s+|our\s+|my\s+)?(?:existing\s+)?[\w-]{0,32}\s?(?:page|screen|module|component|feature|view|flow|form)\b)|(?<![A-Za-z0-9_])(?:nhân\s+bản|nhan\s+ban|sao\s+chép|sao\s+chep)(?![A-Za-z0-9_])/i;
|
|
94
|
+
// SQL-work ask: the prompt lives in the database layer (a .sql file, a view,
|
|
95
|
+
// column, table, stored function). Vietnamese table words break \b, hence the
|
|
96
|
+
// lookarounds.
|
|
97
|
+
const SQL_INTENT_RE = /(?:\.sql\b)|(?<![A-Za-z0-9_])(?:view|column|table|schema|cột|bảng|cot|bang|stored\s+(?:procedure|function))(?![A-Za-z0-9_])/i;
|
|
98
|
+
// Bare UI-ambiguous words (view/column/table/schema/cột/bảng) only count as SQL work
|
|
99
|
+
// next to SQL context: a DB word, a DDL phrase, a snake_case identifier, or a .sql file.
|
|
100
|
+
// "View the logs" / "a column of buttons" stay ordinary UI language.
|
|
101
|
+
const SQL_STRONG_RE = /(?:\.sql\b)|stored\s+(?:procedure|function)|\b(?:sql|ddl|dml|plpgsql|postgres(?:ql)?|mysql|sqlite)\b|\b(?:create|alter|drop|truncate)\s+(?:or\s+replace\s+)?(?:(?:a|an|the|new)\s+)?(?:\w+\s+)?(?:table|view|schema|function|procedure)\b/i;
|
|
102
|
+
const SQL_WEAK_RE = /(?<![A-Za-z0-9_])(?:view|column|table|schema|cột|bảng|cot|bang)(?![A-Za-z0-9_])/gi;
|
|
103
|
+
const SQL_IDENT_RE = /\b[a-z]+(?:_[a-z0-9]+){1,}\b/;
|
|
104
|
+
function hasSqlIntent(text) {
|
|
105
|
+
const t = String(text ?? '');
|
|
106
|
+
if (SQL_STRONG_RE.test(t)) return true;
|
|
107
|
+
const weak = new Set((t.match(SQL_WEAK_RE) || []).map((w) => w.toLowerCase()));
|
|
108
|
+
if (weak.size === 0 || /\bcolumn\s+of\b/i.test(t)) return false;
|
|
109
|
+
// A column+table pair, or a bare DB word beside a snake_case identifier, is schema work.
|
|
110
|
+
const pair = (weak.has('column') || weak.has('cột') || weak.has('cot')) && (weak.has('table') || weak.has('bảng') || weak.has('bang'));
|
|
111
|
+
return pair || SQL_IDENT_RE.test(t);
|
|
112
|
+
}
|
|
113
|
+
// The SQL ask touches shape/data rather than a read-only query — routes the
|
|
114
|
+
// migration row instead of a feature build.
|
|
115
|
+
const SQL_SCHEMA_RE = /(?<![A-Za-z0-9_])(?:ddl|alter|drop|(?:create|alter|drop|truncate)\s+table|(?:add|drop|rename|change|modify|update|thêm|xóa|xoá|sửa|sua|đổi|doi)\s+(?:a\s+|the\s+|new\s+|một\s+)*(?:column|cột|cot)(?!\s+of\b)|kiểu\s+dữ\s+liệu|kieu\s+du\s+lieu|data\s?type|not\s+null|constraint|migration|migrate)(?![A-Za-z0-9_])/i;
|
|
116
|
+
// Reuse-first ask: "use the existing/core function", "dùng hàm có sẵn",
|
|
117
|
+
// "tái sử dụng" — search for the function before writing a new one.
|
|
118
|
+
const REUSE_FIRST_RE = /(?:\buse\s+(?:the\s+)?(?:existing|core|shared|same|current)\b)|(?:\buse\s+[\w.]+\s+from\s+(?:the\s+)?(?:core|shared|utils?|common|lib)\b)|\b(?:existing|core|shared)\s+(?:function|helper|util(?:ity|ities)?|service|component)\b|\breuse\b|(?<![A-Za-z0-9_])(?:tái\s+sử\s+dụng|tai\s+su\s+dung)|(?<![A-Za-z0-9_])(?:dùng|dung|dựa|dua)\s+(?:vào\s+|vao\s+)?(?:hàm|ham)(?:\s+(?:có\s+sẵn|co\s+san|sẵn\s+có|san\s+co|đã\s+có|da\s+co|cũ|cu))?(?![A-Za-z0-9_])/i;
|
|
119
|
+
// Run-all goal scope: the prompt queues the WHOLE remaining backlog, never a
|
|
120
|
+
// single named task (single-task refs veto below). A scope signal only — it
|
|
121
|
+
// must never set mutation permission by itself.
|
|
122
|
+
|
|
123
|
+
// One bounded derivation for the route summary signals. Pure — no I/O. Both
|
|
124
|
+
// router surfaces call this; the discriminators above read the same booleans
|
|
125
|
+
// through buildRouteSignals so the route fields and playbook row selection
|
|
126
|
+
// cannot diverge.
|
|
127
|
+
export function deriveRouteOrchestrationSignals({ promptText = '', commandText = '' } = {}) {
|
|
128
|
+
const signalText = [promptText, commandText]
|
|
129
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join('\n');
|
|
132
|
+
if (!signalText) {
|
|
133
|
+
return { referenceTarget: false, cloneIntent: false, sqlIntent: false, sqlSchemaChange: false, reuseFirst: false, runAll: false };
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
referenceTarget: REFERENCE_TARGET_RE.test(signalText),
|
|
137
|
+
cloneIntent: CLONE_INTENT_RE.test(signalText),
|
|
138
|
+
sqlIntent: hasSqlIntent(signalText),
|
|
139
|
+
sqlSchemaChange: hasSqlIntent(signalText) && SQL_SCHEMA_RE.test(signalText),
|
|
140
|
+
reuseFirst: REUSE_FIRST_RE.test(signalText),
|
|
141
|
+
runAll: isRunAllRequest(signalText),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
79
145
|
// session-pickup discriminator needs the resumable-record check. ASYNC scan
|
|
80
146
|
// (W3-PB2): the resolve path runs inside hook processes where a sync
|
|
81
147
|
// readdirSync parks the event loop and can starve the hook self-deadline.
|
|
@@ -104,18 +170,32 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
104
170
|
// confusable pair 1: no escalation trigger AND single-surface scope. The
|
|
105
171
|
// `!s.escalated` clause is the BL-010 fast-path guard — any derived
|
|
106
172
|
// floor-raising trigger (the six listed triggers map to risk codes +
|
|
107
|
-
// contract signals) escalates to feature-implementation.
|
|
108
|
-
|
|
173
|
+
// contract signals) escalates to feature-implementation. Reference-driven,
|
|
174
|
+
// clone, reuse-first and SQL-schema asks take the same escape: they name a
|
|
175
|
+
// source of truth or a data contract the small path must not gloss over.
|
|
176
|
+
discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && !s.sqlSchemaChange && !s.referenceTarget && !s.cloneIntent && !s.reuseFirst && s.changeVerbOrCreate,
|
|
109
177
|
fastPath: 'small-feature',
|
|
110
178
|
},
|
|
111
179
|
{
|
|
112
180
|
workGroup: 'feature-implementation',
|
|
113
181
|
playbookId: 'feature-implementation',
|
|
114
182
|
lanes: ['local-build', 'shared-edit', 'map-impact'],
|
|
115
|
-
triggers: [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
183
|
+
triggers: [
|
|
184
|
+
/implement|add|create|build|integrat|ship|deliver|wir(?:e|ed|ing)/i,
|
|
185
|
+
REFERENCE_TARGET_RE,
|
|
186
|
+
CLONE_INTENT_RE,
|
|
187
|
+
REUSE_FIRST_RE,
|
|
188
|
+
],
|
|
189
|
+
// confusable pairs 1/5/8: articulated contract OR ≥1 escalation trigger OR
|
|
190
|
+
// a reference/clone/reuse-driven ask — and the ask is not a disposable
|
|
191
|
+
// spike nor the ship chain itself. The reference/clone/reuse clauses only
|
|
192
|
+
// claim when no defect verb leads the ask ("fix login like signup" stays
|
|
193
|
+
// bug-fix); an escalation raised by SQL schema work yields to the
|
|
194
|
+
// migration row instead of capturing the build lane.
|
|
195
|
+
discriminator: (s) => (
|
|
196
|
+
(s.contractSignal || s.multiSurface || (s.escalated && !s.sqlSchemaChange) || s.wiredShipAsk)
|
|
197
|
+
|| ((s.referenceTarget || s.cloneIntent || s.reuseFirst) && s.defectIdx === -1)
|
|
198
|
+
) && !s.disposable && !s.releaseAsk,
|
|
119
199
|
},
|
|
120
200
|
{
|
|
121
201
|
workGroup: 'bug-fix',
|
|
@@ -190,9 +270,19 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
190
270
|
workGroup: 'migration',
|
|
191
271
|
playbookId: 'migration',
|
|
192
272
|
lanes: ['shared-edit', 'map-impact'],
|
|
193
|
-
triggers: [
|
|
273
|
+
triggers: [
|
|
274
|
+
/migrat|move (?:the |this |all )?[\w-]*|new schema|port (?:the|to)/i,
|
|
275
|
+
// SQL schema work pairs a change verb with a shape noun even without a
|
|
276
|
+
// "migrate" verb — "sửa cột so_created_date trong view", "alter the
|
|
277
|
+
// users table". A bare noun ("fix the users table") is not enough —
|
|
278
|
+
// defect-led asks stay bug-fix.
|
|
279
|
+
/(?:\b(?:alter|drop|add|remove|rename|modify|change|resize)\b[^\n]{0,24}?\b(?:column|table)s?\b)|(?<![A-Za-z0-9_])(?:sửa|sua|đổi|doi|thay|chỉnh|chinh|thêm|them|xóa|xoa|cập\s+nhật|cap\s+nhat)\s+(?:cột|bảng|cot|bang)/i,
|
|
280
|
+
],
|
|
194
281
|
// confusable pair 7: behavior change + data/contract move (rollback needed).
|
|
195
|
-
|
|
282
|
+
// A SQL ask that touches shape (column/table/schema vocabulary) is the same
|
|
283
|
+
// class even without an explicit move verb — unless a defect verb leads the
|
|
284
|
+
// ask ("fix the users table" stays bug-fix).
|
|
285
|
+
discriminator: (s) => s.dataMove || (s.sqlIntent && s.sqlSchemaChange && s.defectIdx === -1),
|
|
196
286
|
},
|
|
197
287
|
{
|
|
198
288
|
workGroup: 'verification',
|
|
@@ -332,6 +422,10 @@ async function buildRouteSignals({
|
|
|
332
422
|
|| /(?:chưa|chua|được không|duoc khong|đúng không|dung khong|không nhỉ|khong nhi|thế nào|the nao|như thế nào|nhu the nao|tại sao|tai sao|là gì|la gi|bao giờ|bao gio|khi nào|khi nao|ở đâu|o dau|không biết|khong biet)/.test(signalText)
|
|
333
423
|
|| /(?:chưa|chua|đúng không|dung khong|không|khong)\s*[?.!]?\s*$/.test(signalText);
|
|
334
424
|
const questionAsk = questionShaped && !changeRequested;
|
|
425
|
+
const referenceTarget = REFERENCE_TARGET_RE.test(signalText);
|
|
426
|
+
const cloneIntent = CLONE_INTENT_RE.test(signalText);
|
|
427
|
+
const sqlIntent = hasSqlIntent(signalText);
|
|
428
|
+
const reuseFirst = REUSE_FIRST_RE.test(signalText);
|
|
335
429
|
return {
|
|
336
430
|
signalText,
|
|
337
431
|
executionMode,
|
|
@@ -346,6 +440,11 @@ async function buildRouteSignals({
|
|
|
346
440
|
questionAsk,
|
|
347
441
|
checkFirst,
|
|
348
442
|
changeVerbOrCreate: changeIdx !== -1 || createIdx !== -1,
|
|
443
|
+
referenceTarget,
|
|
444
|
+
cloneIntent,
|
|
445
|
+
sqlIntent,
|
|
446
|
+
sqlSchemaChange: sqlIntent && SQL_SCHEMA_RE.test(signalText),
|
|
447
|
+
reuseFirst,
|
|
349
448
|
contractSignal: CONTRACT_SIGNAL_RE.test(signalText) || /\boauth\b/i.test(signalText),
|
|
350
449
|
multiSurface: MULTI_SURFACE_RE.test(signalText),
|
|
351
450
|
escalated: Array.isArray(escalationTriggers) && escalationTriggers.length > 0,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import crypto from 'node:crypto';
|
|
4
5
|
import * as indexCore from './lib/index-core.mjs';
|
|
@@ -23,12 +24,15 @@ const args = process.argv.slice(2);
|
|
|
23
24
|
const rootDir = getRootDir(args);
|
|
24
25
|
const limitArg = readFlagValue(args, '--limit');
|
|
25
26
|
const limit = Number.parseInt(limitArg ?? '5', 10);
|
|
27
|
+
const symbolQuery = readFlagValue(args, '--symbol');
|
|
26
28
|
const showOutline = !args.includes('--no-outline');
|
|
27
29
|
const query = collectPositionalArgs(args, ['--no-outline']);
|
|
28
30
|
|
|
29
|
-
if (!query) {
|
|
30
|
-
console.error('Usage: node .claude/ukit/index/query-index.mjs "<error|symbol|path>"');
|
|
31
|
+
if (!query && !symbolQuery) {
|
|
32
|
+
console.error('Usage: node .claude/ukit/index/query-index.mjs "<error|symbol|path>" [--symbol <name>]');
|
|
31
33
|
process.exitCode = 1;
|
|
34
|
+
} else if (symbolQuery) {
|
|
35
|
+
await runSymbolMode(symbolQuery);
|
|
32
36
|
} else try {
|
|
33
37
|
const indexGeneratedAtMs = await ensureFreshIndex({ rootDir, logPrefix: 'index:query' });
|
|
34
38
|
const effectiveLimit = Number.isNaN(limit) ? 5 : limit;
|
|
@@ -60,11 +64,16 @@ if (!query) {
|
|
|
60
64
|
});
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
// Stale-entry guard: entries for files that no longer exist (renamed/deleted
|
|
68
|
+
// after the last build) are dropped from the output instead of pointing at
|
|
69
|
+
// ghosts.
|
|
70
|
+
const existingResults = await filterExistingResults(results);
|
|
71
|
+
|
|
72
|
+
if (existingResults.length === 0) {
|
|
64
73
|
console.log('[ukit:index:query] no matches');
|
|
65
74
|
} else {
|
|
66
|
-
console.log(`[ukit:index:query] top ${
|
|
67
|
-
for (const [idx, item] of
|
|
75
|
+
console.log(`[ukit:index:query] top ${existingResults.length} matches for: ${query}`);
|
|
76
|
+
for (const [idx, item] of existingResults.entries()) {
|
|
68
77
|
console.log(`${idx + 1}. ${item.filePath} (score=${item.score})`);
|
|
69
78
|
if ((item.tests ?? []).length > 0) {
|
|
70
79
|
console.log(` tests: ${item.tests.slice(0, 2).join(', ')}`);
|
|
@@ -80,8 +89,92 @@ if (!query) {
|
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
catch (error) {
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
// Index missing, rebuild failed, or lock timed out — answer from source with
|
|
93
|
+
// a bounded search instead of exiting 1.
|
|
94
|
+
const fallbackRows = await runFallbackSearch(query);
|
|
95
|
+
if (fallbackRows === null) {
|
|
96
|
+
console.error(error && error.message ? error.message : error);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function runSymbolMode(symbolQuery) {
|
|
102
|
+
try {
|
|
103
|
+
await ensureFreshIndex({ rootDir, logPrefix: 'index:query' });
|
|
104
|
+
const effectiveLimit = Number.isNaN(limit) ? 8 : limit;
|
|
105
|
+
const resolveSymbolCandidates = typeof indexCore.querySymbolCandidates === 'function'
|
|
106
|
+
? indexCore.querySymbolCandidates
|
|
107
|
+
: null;
|
|
108
|
+
if (!resolveSymbolCandidates) {
|
|
109
|
+
throw new Error('querySymbolCandidates unavailable in index-core');
|
|
110
|
+
}
|
|
111
|
+
const candidates = await resolveSymbolCandidates({ rootDir, query: symbolQuery, limit: effectiveLimit });
|
|
112
|
+
printSymbolCandidates(candidates, 'index');
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const fallbackRows = await runFallbackSearch(symbolQuery);
|
|
115
|
+
if (fallbackRows === null) {
|
|
116
|
+
console.error(error && error.message ? error.message : error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function printSymbolCandidates(candidates, origin) {
|
|
123
|
+
if (!candidates || candidates.length === 0) {
|
|
124
|
+
console.log('[ukit:index:symbol] no matches');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
console.log(`[ukit:index:symbol] top ${candidates.length} candidates`);
|
|
128
|
+
console.log(`source: ${origin === 'fallback' ? 'fallback-search' : 'index'}`);
|
|
129
|
+
for (const [idx, candidate] of (candidates ?? []).entries()) {
|
|
130
|
+
const location = candidate.line ? `${candidate.filePath}:${candidate.line}` : String(candidate.filePath ?? '');
|
|
131
|
+
const parentLabel = candidate.parent ? ` parent=${candidate.parent}${candidate.schema ? `.${candidate.schema}` : ''}` : '';
|
|
132
|
+
console.log(`${idx + 1}. ${candidate.name} ${location} kind=${candidate.kind} score=${candidate.score} reason=${candidate.reason}${parentLabel}`);
|
|
133
|
+
if (candidate.needsConfirmation) {
|
|
134
|
+
console.log(' needs-confirmation: true (candidate, not a substitution)');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runFallbackSearch(searchQuery) {
|
|
140
|
+
if (typeof indexCore.boundedSourceSearch !== 'function' || !searchQuery) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const effectiveLimit = Number.isNaN(limit) ? 5 : limit;
|
|
145
|
+
const rows = await indexCore.boundedSourceSearch({ rootDir, query: searchQuery, limit: effectiveLimit });
|
|
146
|
+
console.log(`[ukit:index:query] index unavailable — source: fallback-search (${rows.length} matches for: ${searchQuery})`);
|
|
147
|
+
for (const [idx, row] of rows.entries()) {
|
|
148
|
+
console.log(`${idx + 1}. ${row.filePath}${row.line ? `:${row.line}` : ''} (reason=${row.reason})`);
|
|
149
|
+
}
|
|
150
|
+
if (rows.truncated?.length) {
|
|
151
|
+
console.log(`[ukit:index:query] search truncated (${rows.truncated.join(',')}) — results are NOT exhaustive`);
|
|
152
|
+
}
|
|
153
|
+
return rows;
|
|
154
|
+
} catch (fallbackError) {
|
|
155
|
+
console.error(`fallback-search failed: ${fallbackError && fallbackError.message ? fallbackError.message : fallbackError}`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function filterExistingResults(results) {
|
|
161
|
+
if (!Array.isArray(results)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const checks = await Promise.all(results.map(async (item) => ({
|
|
165
|
+
item,
|
|
166
|
+
exists: await fileExists(path.join(rootDir, item.filePath)),
|
|
167
|
+
})));
|
|
168
|
+
return checks.filter((check) => check.exists).map((check) => check.item);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function fileExists(absolutePath) {
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(absolutePath);
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
85
178
|
}
|
|
86
179
|
|
|
87
180
|
async function printOutlineForFile(root, filePath) {
|
|
@@ -68,7 +68,7 @@ export const ROUTE_CATALOG = [
|
|
|
68
68
|
path: '.claude/skills/postgres/SKILL.md',
|
|
69
69
|
order: 4.5,
|
|
70
70
|
signals: [
|
|
71
|
-
{ type: 'prompt', regex: /\b(sql|postgres|postgresql|migration|schema|
|
|
71
|
+
{ type: 'prompt', regex: /\b(sql|postgres|postgresql|migration|schema|trigger|stored (?:procedure|function)|materialized view|query plan|explain|(?:create|alter|drop|truncate) (?:or replace )?(?:table|view|index|function))\b/i, score: 5 },
|
|
72
72
|
{ type: 'command', regex: /\b(psql|prisma|drizzle|knex|sequelize|typeorm)\b/i, score: 4 },
|
|
73
73
|
{ type: 'file', regex: /\.(?:sql|psql)$/i, score: 5 },
|
|
74
74
|
{ type: 'file', regex: /(?:^|\/)(?:SQL\/|sql\/|database\/|db\/|migrations?\/|prisma\/schema\.prisma$)/i, score: 4 },
|