feat(ext): add trigger-detector for goal-driven-loop

This commit is contained in:
张宗平
2026-06-10 17:34:04 +08:00
parent 640a0075d6
commit 4eda494624
@@ -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;
}