@ngockhoale/ukit 3.4.29 → 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 +16 -0
- package/manifests/documentation.yaml +88 -0
- package/package.json +1 -1
- package/src/core/permissionPolicy.js +1 -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 +453 -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
|
@@ -125,3 +125,129 @@ function foldTextForSearch(value) {
|
|
|
125
125
|
.replace(/\p{M}+/gu, '')
|
|
126
126
|
.toLowerCase();
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
// ── Fuzzy identifier resolution ──
|
|
130
|
+
// Shared by resolveSymbolCandidates (queryIndex.js) and scoreDirectFileMatch's
|
|
131
|
+
// symbol_fuzzy tier: camelCase/snake_case/kebab-case identifiers normalize to
|
|
132
|
+
// the same folded token sequence, ordered subsequences bridge compacted queries
|
|
133
|
+
// ('checknull' ⊂ check_if_is_null), and a bounded Damerau distance catches
|
|
134
|
+
// misspellings ('custmer_code' → 'customer_code').
|
|
135
|
+
|
|
136
|
+
export function normalizeIdentifierTokens(value) {
|
|
137
|
+
return tokenizeFoldedText(foldTextForSearch(addCamelBoundaries(value)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Query variants for phrase-level matching: the folded text as-is, then the
|
|
141
|
+
// Vietnamese phrase aliases rewritten in place ('kiem tra null' → 'check null'),
|
|
142
|
+
// then token-level aliases. Each variant is an ordered, deduped token list.
|
|
143
|
+
export function expandIdentifierQueryVariants(value) {
|
|
144
|
+
const folded = foldTextForSearch(value);
|
|
145
|
+
const variants = [tokenizeFoldedText(folded)];
|
|
146
|
+
|
|
147
|
+
for (const entry of VIETNAMESE_PHRASE_ALIASES) {
|
|
148
|
+
entry.regex.lastIndex = 0;
|
|
149
|
+
if (!entry.regex.test(folded)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const expansion of entry.expansions) {
|
|
153
|
+
entry.regex.lastIndex = 0;
|
|
154
|
+
variants.push(tokenizeFoldedText(folded.replace(entry.regex, expansion)));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
variants.push(variants[0].map((token) => VIETNAMESE_TOKEN_ALIASES.get(token)?.[0] ?? token));
|
|
159
|
+
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
return variants
|
|
162
|
+
.map((tokens) => [...new Set(tokens)])
|
|
163
|
+
.filter((tokens) => tokens.length > 0)
|
|
164
|
+
.filter((tokens) => {
|
|
165
|
+
const key = tokens.join('\u0000');
|
|
166
|
+
if (seen.has(key)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
seen.add(key);
|
|
170
|
+
return true;
|
|
171
|
+
})
|
|
172
|
+
.slice(0, 8);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// `queryTokens` fits inside `symbolTokens` in order ('check null' ⊆ check,if,null).
|
|
176
|
+
export function isOrderedTokenSubsequence(queryTokens, symbolTokens) {
|
|
177
|
+
if (queryTokens.length === 0 || queryTokens.length > symbolTokens.length) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
let cursor = 0;
|
|
181
|
+
for (const token of symbolTokens) {
|
|
182
|
+
if (token === queryTokens[cursor]) {
|
|
183
|
+
cursor += 1;
|
|
184
|
+
if (cursor === queryTokens.length) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Character-level ordered containment on compacted forms ('checknull' ⊂ 'checkifnull').
|
|
193
|
+
export function isCompactCharSubsequence(queryCompact, symbolCompact) {
|
|
194
|
+
if (!queryCompact || queryCompact.length > symbolCompact.length) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
let cursor = 0;
|
|
198
|
+
for (const char of symbolCompact) {
|
|
199
|
+
if (char === queryCompact[cursor]) {
|
|
200
|
+
cursor += 1;
|
|
201
|
+
if (cursor === queryCompact.length) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Damerau-Levenshtein (with transpositions), bounded: anything farther than
|
|
210
|
+
// `maxDistance` short-circuits to maxDistance + 1 so callers can treat the
|
|
211
|
+
// result as "beyond threshold" without walking the full matrix.
|
|
212
|
+
export function boundedDamerauDistance(a, b, maxDistance = 2) {
|
|
213
|
+
const left = String(a ?? '');
|
|
214
|
+
const right = String(b ?? '');
|
|
215
|
+
if (left === right) {
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
if (Math.abs(left.length - right.length) > maxDistance) {
|
|
219
|
+
return maxDistance + 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let prevPrev = null;
|
|
223
|
+
let prev = Array.from({ length: right.length + 1 }, (_, j) => j);
|
|
224
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
225
|
+
const current = [i];
|
|
226
|
+
let rowMin = i;
|
|
227
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
228
|
+
const substitutionCost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
229
|
+
let value = Math.min(
|
|
230
|
+
prev[j] + 1,
|
|
231
|
+
current[j - 1] + 1,
|
|
232
|
+
prev[j - 1] + substitutionCost,
|
|
233
|
+
);
|
|
234
|
+
if (i > 1 && j > 1 && left[i - 1] === right[j - 2] && left[i - 2] === right[j - 1]) {
|
|
235
|
+
value = Math.min(value, prevPrev[j - 2] + 1);
|
|
236
|
+
}
|
|
237
|
+
current[j] = value;
|
|
238
|
+
if (value < rowMin) {
|
|
239
|
+
rowMin = value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (rowMin > maxDistance) {
|
|
243
|
+
return maxDistance + 1;
|
|
244
|
+
}
|
|
245
|
+
prevPrev = prev;
|
|
246
|
+
prev = current;
|
|
247
|
+
}
|
|
248
|
+
return prev[right.length];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Minimum length both compacts must reach before edit-distance fuzzy matching
|
|
252
|
+
// applies — short identifiers (< 6) produce too many false neighbors.
|
|
253
|
+
export const FUZZY_EDIT_DISTANCE_MIN_LENGTH = 6;
|
package/src/index/paths.js
CHANGED
|
@@ -15,7 +15,11 @@ export const INDEX_ARTIFACTS = {
|
|
|
15
15
|
codegraph: 'codegraph.json',
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// v9: symbols.json gains global-registered ('global') and Vue 'method' rows,
|
|
19
|
+
// SQL 'column' rows with parent/schema, and calls.json gains per-object SQL
|
|
20
|
+
// reference records — a v8 cached artifact must not be reused for unchanged
|
|
21
|
+
// files or the new extractions silently vanish.
|
|
22
|
+
export const INDEX_SCHEMA_VERSION = 9;
|
|
19
23
|
|
|
20
24
|
export function getIndexDir(rootDir) {
|
|
21
25
|
return path.join(rootDir, '.cache', 'index');
|
|
@@ -22,6 +22,7 @@ import os from 'node:os';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
|
+
import { isRunAllRequest } from '../../template_project/.claude/ukit/runtime/handoff-intent.mjs';
|
|
25
26
|
import { deriveEscalationTriggers, deriveRiskFloor } from './routeResolver.js';
|
|
26
27
|
|
|
27
28
|
const PLAYBOOK_NAME_RE = /\.md$/i;
|
|
@@ -48,7 +49,10 @@ export const PLAYBOOK_ID_ALIASES = Object.freeze({
|
|
|
48
49
|
// Compiled once; every row's discriminator reads the same normalized signals so
|
|
49
50
|
// confusable pairs split on the discriminator column, never trigger text.
|
|
50
51
|
const CREATE_VERB_RE = /\b(create|add|write|build|implement|scaffold|tạo|thêm)\b/i;
|
|
51
|
-
|
|
52
|
+
// Vietnamese words never match `\b` (JS word chars are ASCII-only — 'sửa' ends
|
|
53
|
+
// in a non-word char, 'đổi' starts with one), so the diacritic alternates use
|
|
54
|
+
// explicit lookarounds instead. Same word list as the original literal.
|
|
55
|
+
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;
|
|
52
56
|
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;
|
|
53
57
|
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;
|
|
54
58
|
const CHECK_VERB_RE = /\b(verify|check|confirm|prove|validate|assert)\b/i;
|
|
@@ -77,6 +81,68 @@ const AUTONOMOUS_TRIGGER_RE = /\b(finish (?:this|the) backlog|bounded (?:long )?
|
|
|
77
81
|
const RELEASE_TRIGGER_RE = /\b(publish|ship it|release|bump (?:the )?version|deploy|cut a release|tag (?:the )?release)\b/i;
|
|
78
82
|
const HANDOFF_CYCLE_TRIGGER_RE = /\b(handoff|multi[- ]?task cycle|task pipeline|execute the plan|worktree)\b/i;
|
|
79
83
|
|
|
84
|
+
// --- Routing-orchestration signals (reference/clone/SQL/reuse/run-all) -------
|
|
85
|
+
// Shared vocabulary for the discriminators below AND for the additive route
|
|
86
|
+
// summary fields route-task.mjs emits (referenceTargets/cloneIntent/sqlIntent/
|
|
87
|
+
// reuseFirst/goalScope). Kept here so both router surfaces read one definition.
|
|
88
|
+
// Reference-based ask: "like the Customer page", "làm giống trang Customer",
|
|
89
|
+
// "same as module Y", "using validation from Z" — resolve the named source.
|
|
90
|
+
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;
|
|
91
|
+
// Clone ask: duplicate a named surface ("clone the Customer page", "nhân bản
|
|
92
|
+
// trang này", "copy the login screen"). A bare "clone the repo" (git) and a
|
|
93
|
+
// bare "copy" without a surface noun stay unmatched.
|
|
94
|
+
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;
|
|
95
|
+
// SQL-work ask: the prompt lives in the database layer (a .sql file, a view,
|
|
96
|
+
// column, table, stored function). Vietnamese table words break \b, hence the
|
|
97
|
+
// lookarounds.
|
|
98
|
+
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;
|
|
99
|
+
// Bare UI-ambiguous words (view/column/table/schema/cột/bảng) only count as SQL work
|
|
100
|
+
// next to SQL context: a DB word, a DDL phrase, a snake_case identifier, or a .sql file.
|
|
101
|
+
// "View the logs" / "a column of buttons" stay ordinary UI language.
|
|
102
|
+
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;
|
|
103
|
+
const SQL_WEAK_RE = /(?<![A-Za-z0-9_])(?:view|column|table|schema|cột|bảng|cot|bang)(?![A-Za-z0-9_])/gi;
|
|
104
|
+
const SQL_IDENT_RE = /\b[a-z]+(?:_[a-z0-9]+){1,}\b/;
|
|
105
|
+
function hasSqlIntent(text) {
|
|
106
|
+
const t = String(text ?? '');
|
|
107
|
+
if (SQL_STRONG_RE.test(t)) return true;
|
|
108
|
+
const weak = new Set((t.match(SQL_WEAK_RE) || []).map((w) => w.toLowerCase()));
|
|
109
|
+
if (weak.size === 0 || /\bcolumn\s+of\b/i.test(t)) return false;
|
|
110
|
+
// A column+table pair, or a bare DB word beside a snake_case identifier, is schema work.
|
|
111
|
+
const pair = (weak.has('column') || weak.has('cột') || weak.has('cot')) && (weak.has('table') || weak.has('bảng') || weak.has('bang'));
|
|
112
|
+
return pair || SQL_IDENT_RE.test(t);
|
|
113
|
+
}
|
|
114
|
+
// The SQL ask touches shape/data rather than a read-only query — routes the
|
|
115
|
+
// migration row instead of a feature build.
|
|
116
|
+
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;
|
|
117
|
+
// Reuse-first ask: "use the existing/core function", "dùng hàm có sẵn",
|
|
118
|
+
// "tái sử dụng" — search for the function before writing a new one.
|
|
119
|
+
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;
|
|
120
|
+
// Run-all goal scope: the prompt queues the WHOLE remaining backlog, never a
|
|
121
|
+
// single named task (single-task refs veto below). A scope signal only — it
|
|
122
|
+
// must never set mutation permission by itself.
|
|
123
|
+
|
|
124
|
+
// One bounded derivation for the route summary signals. Pure — no I/O. Both
|
|
125
|
+
// router surfaces call this; the discriminators above read the same booleans
|
|
126
|
+
// through buildRouteSignals so the route fields and playbook row selection
|
|
127
|
+
// cannot diverge.
|
|
128
|
+
export function deriveRouteOrchestrationSignals({ promptText = '', commandText = '' } = {}) {
|
|
129
|
+
const signalText = [promptText, commandText]
|
|
130
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join('\n');
|
|
133
|
+
if (!signalText) {
|
|
134
|
+
return { referenceTarget: false, cloneIntent: false, sqlIntent: false, sqlSchemaChange: false, reuseFirst: false, runAll: false };
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
referenceTarget: REFERENCE_TARGET_RE.test(signalText),
|
|
138
|
+
cloneIntent: CLONE_INTENT_RE.test(signalText),
|
|
139
|
+
sqlIntent: hasSqlIntent(signalText),
|
|
140
|
+
sqlSchemaChange: hasSqlIntent(signalText) && SQL_SCHEMA_RE.test(signalText),
|
|
141
|
+
reuseFirst: REUSE_FIRST_RE.test(signalText),
|
|
142
|
+
runAll: isRunAllRequest(signalText),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
80
146
|
// session-pickup discriminator needs the resumable-record check. ASYNC scan
|
|
81
147
|
// (W3-PB2): the resolve path runs inside hook processes where a sync
|
|
82
148
|
// readdirSync parks the event loop and can starve the hook self-deadline.
|
|
@@ -105,18 +171,32 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
105
171
|
// confusable pair 1: no escalation trigger AND single-surface scope. The
|
|
106
172
|
// `!s.escalated` clause is the BL-010 fast-path guard — any derived
|
|
107
173
|
// floor-raising trigger (the six listed triggers map to risk codes +
|
|
108
|
-
// contract signals) escalates to feature-implementation.
|
|
109
|
-
|
|
174
|
+
// contract signals) escalates to feature-implementation. Reference-driven,
|
|
175
|
+
// clone, reuse-first and SQL-schema asks take the same escape: they name a
|
|
176
|
+
// source of truth or a data contract the small path must not gloss over.
|
|
177
|
+
discriminator: (s) => !s.escalated && !s.contractSignal && !s.multiSurface && !s.sqlSchemaChange && !s.referenceTarget && !s.cloneIntent && !s.reuseFirst && s.changeVerbOrCreate,
|
|
110
178
|
fastPath: 'small-feature',
|
|
111
179
|
},
|
|
112
180
|
{
|
|
113
181
|
workGroup: 'feature-implementation',
|
|
114
182
|
playbookId: 'feature-implementation',
|
|
115
183
|
lanes: ['local-build', 'shared-edit', 'map-impact'],
|
|
116
|
-
triggers: [
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
184
|
+
triggers: [
|
|
185
|
+
/implement|add|create|build|integrat|ship|deliver|wir(?:e|ed|ing)/i,
|
|
186
|
+
REFERENCE_TARGET_RE,
|
|
187
|
+
CLONE_INTENT_RE,
|
|
188
|
+
REUSE_FIRST_RE,
|
|
189
|
+
],
|
|
190
|
+
// confusable pairs 1/5/8: articulated contract OR ≥1 escalation trigger OR
|
|
191
|
+
// a reference/clone/reuse-driven ask — and the ask is not a disposable
|
|
192
|
+
// spike nor the ship chain itself. The reference/clone/reuse clauses only
|
|
193
|
+
// claim when no defect verb leads the ask ("fix login like signup" stays
|
|
194
|
+
// bug-fix); an escalation raised by SQL schema work yields to the
|
|
195
|
+
// migration row instead of capturing the build lane.
|
|
196
|
+
discriminator: (s) => (
|
|
197
|
+
(s.contractSignal || s.multiSurface || (s.escalated && !s.sqlSchemaChange) || s.wiredShipAsk)
|
|
198
|
+
|| ((s.referenceTarget || s.cloneIntent || s.reuseFirst) && s.defectIdx === -1)
|
|
199
|
+
) && !s.disposable && !s.releaseAsk,
|
|
120
200
|
},
|
|
121
201
|
{
|
|
122
202
|
workGroup: 'bug-fix',
|
|
@@ -191,9 +271,19 @@ export const PLAYBOOK_ROUTING_TABLE = Object.freeze([
|
|
|
191
271
|
workGroup: 'migration',
|
|
192
272
|
playbookId: 'migration',
|
|
193
273
|
lanes: ['shared-edit', 'map-impact'],
|
|
194
|
-
triggers: [
|
|
274
|
+
triggers: [
|
|
275
|
+
/migrat|move (?:the |this |all )?[\w-]*|new schema|port (?:the|to)/i,
|
|
276
|
+
// SQL schema work pairs a change verb with a shape noun even without a
|
|
277
|
+
// "migrate" verb — "sửa cột so_created_date trong view", "alter the
|
|
278
|
+
// users table". A bare noun ("fix the users table") is not enough —
|
|
279
|
+
// defect-led asks stay bug-fix.
|
|
280
|
+
/(?:\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,
|
|
281
|
+
],
|
|
195
282
|
// confusable pair 7: behavior change + data/contract move (rollback needed).
|
|
196
|
-
|
|
283
|
+
// A SQL ask that touches shape (column/table/schema vocabulary) is the same
|
|
284
|
+
// class even without an explicit move verb — unless a defect verb leads the
|
|
285
|
+
// ask ("fix the users table" stays bug-fix).
|
|
286
|
+
discriminator: (s) => s.dataMove || (s.sqlIntent && s.sqlSchemaChange && s.defectIdx === -1),
|
|
197
287
|
},
|
|
198
288
|
{
|
|
199
289
|
workGroup: 'verification',
|
|
@@ -333,6 +423,10 @@ async function buildRouteSignals({
|
|
|
333
423
|
|| /(?: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)
|
|
334
424
|
|| /(?:chưa|chua|đúng không|dung khong|không|khong)\s*[?.!]?\s*$/.test(signalText);
|
|
335
425
|
const questionAsk = questionShaped && !changeRequested;
|
|
426
|
+
const referenceTarget = REFERENCE_TARGET_RE.test(signalText);
|
|
427
|
+
const cloneIntent = CLONE_INTENT_RE.test(signalText);
|
|
428
|
+
const sqlIntent = hasSqlIntent(signalText);
|
|
429
|
+
const reuseFirst = REUSE_FIRST_RE.test(signalText);
|
|
336
430
|
return {
|
|
337
431
|
signalText,
|
|
338
432
|
executionMode,
|
|
@@ -347,6 +441,11 @@ async function buildRouteSignals({
|
|
|
347
441
|
questionAsk,
|
|
348
442
|
checkFirst,
|
|
349
443
|
changeVerbOrCreate: changeIdx !== -1 || createIdx !== -1,
|
|
444
|
+
referenceTarget,
|
|
445
|
+
cloneIntent,
|
|
446
|
+
sqlIntent,
|
|
447
|
+
sqlSchemaChange: sqlIntent && SQL_SCHEMA_RE.test(signalText),
|
|
448
|
+
reuseFirst,
|
|
350
449
|
contractSignal: CONTRACT_SIGNAL_RE.test(signalText) || /\boauth\b/i.test(signalText),
|
|
351
450
|
multiSurface: MULTI_SURFACE_RE.test(signalText),
|
|
352
451
|
escalated: Array.isArray(escalationTriggers) && escalationTriggers.length > 0,
|