Files
pi-skills/.pi/extensions/goal-driven-loop/trigger-detector.ts
T

219 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}