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