feat(ext): add trigger-detector for goal-driven-loop
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Trigger detection for goal-driven-loop.
|
||||
*
|
||||
* Detects goal-setting phrases in user input and validates them
|
||||
* against anti-trigger rules.
|
||||
*/
|
||||
|
||||
export interface TriggerMatch {
|
||||
goalText: string;
|
||||
rawCriteria?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger phrases — order matters (longer patterns first to avoid
|
||||
* "当前目标是" matching "是" alone).
|
||||
*/
|
||||
const TRIGGER_PATTERNS: RegExp[] = [
|
||||
// Chinese
|
||||
/设定当前目标为\s*(.+)/,
|
||||
/把当前目标设为\s*(.+)/,
|
||||
/当前目标是\s*(.+)/,
|
||||
/我的目标是\s*(.+)/,
|
||||
// English
|
||||
/set\s+(?:the\s+)?current\s+goal\s+to\s+(.+)/i,
|
||||
/current\s+goal\s+is\s+(.+)/i,
|
||||
/my\s+goal\s+is\s+(.+)/i,
|
||||
/the\s+goal\s+is\s+(.+)/i,
|
||||
];
|
||||
|
||||
const TRIVIAL_KEYWORDS = [
|
||||
"hotfix",
|
||||
"tweak",
|
||||
"fix a bug",
|
||||
"small change",
|
||||
"quick fix",
|
||||
"修个bug",
|
||||
"小改",
|
||||
];
|
||||
|
||||
const ARCHITECTURAL_KEYWORDS = [
|
||||
"architecture",
|
||||
"architectural",
|
||||
"module",
|
||||
"system",
|
||||
"refactor",
|
||||
"integration",
|
||||
"重构",
|
||||
"模块",
|
||||
"系统",
|
||||
"集成",
|
||||
];
|
||||
|
||||
const REPORTED_SPEECH_MARKERS = [
|
||||
"he said",
|
||||
"she said",
|
||||
"they said",
|
||||
"he says",
|
||||
"she says",
|
||||
"they say",
|
||||
"他说",
|
||||
"她说",
|
||||
"他们说",
|
||||
];
|
||||
|
||||
/**
|
||||
* Detect if the input is a goal-setting trigger and extract the goal text.
|
||||
* Returns null if no trigger or if anti-trigger rules match.
|
||||
*/
|
||||
export function detectTrigger(input: string): TriggerMatch | null {
|
||||
// Anti-trigger: code block
|
||||
if (/```[\s\S]*?```/.test(input) && containsTriggerInCodeBlock(input)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Anti-trigger: markdown quote block
|
||||
const lines = input.split("\n");
|
||||
for (const line of lines) {
|
||||
if (/^\s*>/.test(line)) {
|
||||
const triggerInQuote = TRIGGER_PATTERNS.some((p) => p.test(line));
|
||||
if (triggerInQuote) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anti-trigger: reported speech
|
||||
const lowerInput = input.toLowerCase();
|
||||
const hasReportedSpeech = REPORTED_SPEECH_MARKERS.some((marker) =>
|
||||
lowerInput.includes(marker)
|
||||
);
|
||||
if (hasReportedSpeech) {
|
||||
const sentences = input.split(/[.!?。!?]/);
|
||||
for (const sentence of sentences) {
|
||||
const lowerSentence = sentence.toLowerCase();
|
||||
const hasMarker = REPORTED_SPEECH_MARKERS.some((m) =>
|
||||
lowerSentence.includes(m)
|
||||
);
|
||||
const hasTrigger = TRIGGER_PATTERNS.some((p) => p.test(sentence));
|
||||
if (hasMarker && hasTrigger) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anti-trigger: trivial task keywords
|
||||
const hasTrivialKeyword = TRIVIAL_KEYWORDS.some((kw) =>
|
||||
lowerInput.includes(kw.toLowerCase())
|
||||
);
|
||||
if (hasTrivialKeyword) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to match trigger pattern
|
||||
for (const pattern of TRIGGER_PATTERNS) {
|
||||
const match = input.match(pattern);
|
||||
if (match) {
|
||||
const fullText = match[1].trim();
|
||||
|
||||
// Validate: must be non-empty and not a short fragment
|
||||
if (fullText.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Anti-trigger: short description without architectural keywords
|
||||
if (fullText.length <= 10) {
|
||||
const hasArchitectural = ARCHITECTURAL_KEYWORDS.some((kw) =>
|
||||
fullText.toLowerCase().includes(kw.toLowerCase())
|
||||
);
|
||||
if (!hasArchitectural) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract acceptance criteria
|
||||
const criteriaMatch = fullText.match(
|
||||
/(?:验收条件[是为::]\s*|acceptance\s*(?:criteria)?\s*[::]\s*)(.+)$/i
|
||||
);
|
||||
|
||||
if (criteriaMatch) {
|
||||
const goalText = fullText.substring(0, criteriaMatch.index).trim();
|
||||
return {
|
||||
goalText: cleanGoalText(goalText),
|
||||
rawCriteria: criteriaMatch[1].trim(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
goalText: cleanGoalText(fullText),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function containsTriggerInCodeBlock(input: string): boolean {
|
||||
const codeBlocks = input.match(/```[\s\S]*?```/g) || [];
|
||||
for (const block of codeBlocks) {
|
||||
for (const pattern of TRIGGER_PATTERNS) {
|
||||
if (pattern.test(block)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function cleanGoalText(text: string): string {
|
||||
return text.replace(/[.,;:。;,]+$/, "").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a goal is coarse-grained (should delegate to comet).
|
||||
*/
|
||||
export function isCoarseGrained(goalText: string): boolean {
|
||||
const lower = goalText.toLowerCase();
|
||||
|
||||
const multiModuleSignals = [
|
||||
"across",
|
||||
"throughout",
|
||||
"整个",
|
||||
"全",
|
||||
"all modules",
|
||||
"multiple",
|
||||
"multiple files",
|
||||
"多模块",
|
||||
];
|
||||
for (const signal of multiModuleSignals) {
|
||||
if (lower.includes(signal)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const fileCountMatch = goalText.match(/(\d+)\s*(?:个)?\s*(?:file|files|文件)/i);
|
||||
if (fileCountMatch) {
|
||||
const count = parseInt(fileCountMatch[1], 10);
|
||||
if (count > 5) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const architecturalSignals = [
|
||||
"redesign",
|
||||
"rewrite",
|
||||
"migrate",
|
||||
"重构",
|
||||
"迁移",
|
||||
"重写",
|
||||
"重新设计",
|
||||
];
|
||||
for (const signal of architecturalSignals) {
|
||||
if (lower.includes(signal.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user