From 14b60f601ba5598cbc5ca617e0f5b67af89ed495 Mon Sep 17 00:00:00 2001 From: "yu.zhenyu" Date: Tue, 24 Feb 2026 10:57:03 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(signals):=20=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E4=BF=A1=E5=8F=B7=E6=8F=90=E5=8F=96=20+=20=E9=9C=80?= =?UTF-8?q?=E6=B1=82/=E6=94=B9=E8=BF=9B=E6=A0=87=E7=AD=BE=E6=90=BA?= =?UTF-8?q?=E5=B8=A6=E6=8F=8F=E8=BF=B0=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - signals.js: user_feature_request / user_improvement_suggestion 支持简中、繁中、英、日四语言 pattern 识别,提取后以 baseName:snippet 格式携带需求描述(最长 200 字) - signals.js: hasOpportunitySignal / errorHit 同步支持 baseName:snippet 格式及中文关键词 - mutation.js: hasOpportunitySignal 兼容 name:snippet 信号(startsWith 判断) - questionGenerator.js: user_feature_request 检测兼容带 snippet 前缀的信号格式 - test/signals.test.js: 新增四语言基础用例 + 13 条边界条件测试 (超长截断、我想…、空输入、仅标点、换行、多信号共存等) - test/selector.test.js: 补充 baseName:snippet 格式的基因匹配用例 Co-authored-by: Cursor --- src/gep/mutation.js | 4 +- src/gep/questionGenerator.js | 2 +- src/gep/signals.js | 95 ++++++++++++--- test/selector.test.js | 15 ++- test/signals.test.js | 218 +++++++++++++++++++++++++++++++++++ 5 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 test/signals.test.js diff --git a/src/gep/mutation.js b/src/gep/mutation.js index 84605cc4..a7c6bed5 100644 --- a/src/gep/mutation.js +++ b/src/gep/mutation.js @@ -45,7 +45,9 @@ var OPPORTUNITY_SIGNALS = [ function hasOpportunitySignal(signals) { var list = Array.isArray(signals) ? signals.map(function (s) { return String(s || ''); }) : []; for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) { - if (list.includes(OPPORTUNITY_SIGNALS[i])) return true; + var name = OPPORTUNITY_SIGNALS[i]; + if (list.includes(name)) return true; + if (list.some(function (s) { return s.startsWith(name + ':'); })) return true; } return false; } diff --git a/src/gep/questionGenerator.js b/src/gep/questionGenerator.js index 965588bc..fa4dcbd0 100644 --- a/src/gep/questionGenerator.js +++ b/src/gep/questionGenerator.js @@ -145,7 +145,7 @@ function generateQuestions(opts) { } // --- Strategy 5: User feature requests the agent can amplify --- - if (signalSet.has('user_feature_request')) { + if (signalSet.has('user_feature_request') || signals.some(function (s) { return String(s).startsWith('user_feature_request:'); })) { var featureLines = transcript.split('\n').filter(function(l) { return /\b(add|implement|create|build|i want|i need|please add)\b/i.test(l); }); diff --git a/src/gep/signals.js b/src/gep/signals.js index 1d79e490..2638847a 100644 --- a/src/gep/signals.js +++ b/src/gep/signals.js @@ -16,7 +16,10 @@ var OPPORTUNITY_SIGNALS = [ function hasOpportunitySignal(signals) { var list = Array.isArray(signals) ? signals : []; for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) { - if (list.includes(OPPORTUNITY_SIGNALS[i])) return true; + var name = OPPORTUNITY_SIGNALS[i]; + if (list.includes(name)) return true; + // Signals may carry extra as "name:snippet" + if (list.some(function (s) { return String(s).startsWith(name + ':'); })) return true; } return false; } @@ -137,7 +140,6 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user String(userSnippet || ''), ].join('\n'); var lower = corpus.toLowerCase(); - // Analyze recent evolution history for de-duplication var history = analyzeRecentHistory(recentEvents || []); @@ -145,7 +147,8 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user // Refined error detection regex to avoid false positives on "fail"/"failed" in normal text. // We prioritize structured error markers ([error], error:, exception:) and specific JSON patterns. - var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"/.test(lower); + // Chinese: 错误、异常、失败、报错 (common in logs and stack traces). + var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误|异常|报错|失败\s*[::]/.test(lower); if (errorHit) signals.push('log_error'); // Error signature (more reproducible than a coarse "log_error" tag). @@ -156,7 +159,7 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user .filter(Boolean); var errLine = - lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error/i.test(l); }) || + lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error|错误|异常\s*[::]|报错|失败\s*[::]/i.test(l); }) || null; if (errLine) { @@ -200,27 +203,77 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user } // --- Opportunity signals (innovation / feature requests) --- - - // user_feature_request: user explicitly asks for a new capability - // Look for action verbs + object patterns that indicate a feature request - if (/\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,60}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i.test(corpus)) { - signals.push('user_feature_request'); + // Support 4 languages: 简中、繁中、英、日. Attach extra info (snippet) for selector/prompt use. + + var featureRequestSnippet = ''; + // English + var featEn = corpus.match(/\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,120}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i); + if (featEn) featureRequestSnippet = featEn[0].replace(/\s+/g, ' ').trim().slice(0, 200); + if (!featureRequestSnippet && /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) { + var featWant = corpus.match(/.{0,80}\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b.{0,80}/i); + featureRequestSnippet = featWant ? featWant[0].replace(/\s+/g, ' ').trim().slice(0, 200) : 'feature request'; + } + // 简中(含「我想……」:截取描述至 200 字) + if (!featureRequestSnippet && /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus)) { + var featZh = corpus.match(/.{0,100}(加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能).{0,100}/); + if (featZh) featureRequestSnippet = featZh[0].replace(/\s+/g, ' ').trim().slice(0, 200); + if (!featureRequestSnippet && /我想/.test(corpus)) { + var featWantZh = corpus.match(/我想\s*[,,\.。、\s]*([\s\S]{0,400})/); + featureRequestSnippet = featWantZh ? (featWantZh[1].replace(/\s+/g, ' ').trim().slice(0, 200) || '功能需求') : '功能需求'; + } + if (!featureRequestSnippet) featureRequestSnippet = '功能需求'; } - // Also catch direct "I want/need X" patterns - if (/\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) { - signals.push('user_feature_request'); + // 繁中 + if (!featureRequestSnippet && /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus)) { + var featTw = corpus.match(/.{0,100}(加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加).{0,100}/); + featureRequestSnippet = featTw ? featTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '功能需求'; + } + // 日 + if (!featureRequestSnippet && /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) { + var featJa = corpus.match(/.{0,100}(追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい).{0,100}/); + featureRequestSnippet = featJa ? featJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '機能要望'; + } + if (featureRequestSnippet || /\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,60}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i.test(corpus) || + /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower) || + /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus) || + /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus) || + /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) { + signals.push('user_feature_request:' + (featureRequestSnippet || '')); } - // user_improvement_suggestion: user suggests making something better - if (/\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower)) { - // Only fire if there is no active error (to distinguish from repair requests) - if (!errorHit) signals.push('user_improvement_suggestion'); + // user_improvement_suggestion: 4 languages + extra + var improvementSnippet = ''; + if (!errorHit) { + var impEn = lower.match(/.{0,80}\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b.{0,80}/); + if (impEn) improvementSnippet = impEn[0].replace(/\s+/g, ' ').trim().slice(0, 200); + if (!improvementSnippet && /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus)) { + var impZh = corpus.match(/.{0,100}(改进一下|优化一下|简化|重构|整理一下|弄得更好).{0,100}/); + improvementSnippet = impZh ? impZh[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改进建议'; + } + if (!improvementSnippet && /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus)) { + var impTw = corpus.match(/.{0,100}(改進一下|優化一下|簡化|重構|整理一下|弄得更好).{0,100}/); + improvementSnippet = impTw ? impTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改進建議'; + } + if (!improvementSnippet && /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus)) { + var impJa = corpus.match(/.{0,100}(改善|最適化|簡素化|リファクタ|良くして|改良).{0,100}/); + improvementSnippet = impJa ? impJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改善要望'; + } + var hasImprovement = improvementSnippet || + /\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower) || + /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus) || + /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus) || + /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus); + if (hasImprovement) signals.push('user_improvement_suggestion:' + (improvementSnippet || '')); } // perf_bottleneck: performance issues detected if (/\b(slow|timeout|timed?\s*out|latency|bottleneck|took too long|performance issue|high cpu|high memory|oom|out of memory)\b/i.test(lower)) { signals.push('perf_bottleneck'); } + // Chinese: 慢/超时/卡顿/性能/内存溢出 + if (/太慢|超时|卡顿|性能问题|内存溢出|跑不动|很慢/.test(corpus)) { + signals.push('perf_bottleneck'); + } // capability_gap: something is explicitly unsupported or missing if (/\b(not supported|cannot|doesn'?t support|no way to|missing feature|unsupported|not available|not implemented|no support for)\b/i.test(lower)) { @@ -229,6 +282,12 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user signals.push('capability_gap'); } } + // Chinese: 不支持/没法/无法/没有这个功能 + if (/不支持|没法|无法\s*实现|没有这个功能|还不支持/.test(corpus)) { + if (!signals.includes('memory_missing') && !signals.includes('user_missing') && !signals.includes('session_logs_missing')) { + signals.push('capability_gap'); + } + } // --- Tool Usage Analytics --- var toolUsage = {}; @@ -273,7 +332,9 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user var beforeDedup = signals.length; signals = signals.filter(function (s) { // Normalize signal key for comparison - var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' : s; + var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' + : s.startsWith('user_feature_request:') ? 'user_feature_request' : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion' + : s; return !history.suppressedSignals.has(key); }); if (beforeDedup > 0 && signals.length === 0) { diff --git a/test/selector.test.js b/test/selector.test.js index dbd82690..28d350fb 100644 --- a/test/selector.test.js +++ b/test/selector.test.js @@ -23,7 +23,7 @@ const GENES = [ type: 'Gene', id: 'gene_innovate', category: 'innovate', - signals_match: ['user_feature_request', 'capability_gap', 'stable_success_plateau'], + signals_match: ['user_feature_request', 'user_improvement_suggestion', 'capability_gap', 'stable_success_plateau'], strategy: ['build it'], validation: ['node -e "true"'], }, @@ -81,6 +81,19 @@ describe('selectGene', () => { // With preference, it should be selected even if gene_repair scores higher assert.equal(result.selected.id, 'gene_optimize'); }); + + it('matches gene when signal carries extra info (user_feature_request:snippet)', () => { + // Signal format: "user_feature_request:加个支付模块" — selector matches by substring includes(pattern) + const result = selectGene(GENES, ['user_feature_request:加个支付模块,要支持微信和支付宝'], {}); + assert.ok(result.selected, 'should select a gene'); + assert.equal(result.selected.id, 'gene_innovate', 'innovate gene has signals_match user_feature_request'); + }); + + it('matches gene when signal carries extra info (user_improvement_suggestion:snippet)', () => { + const result = selectGene(GENES, ['user_improvement_suggestion:refactor the payment module and simplify the API'], {}); + assert.ok(result.selected); + assert.equal(result.selected.id, 'gene_innovate', 'innovate gene has signals_match user_improvement_suggestion'); + }); }); describe('selectCapsule', () => { diff --git a/test/signals.test.js b/test/signals.test.js new file mode 100644 index 00000000..926b8e4d --- /dev/null +++ b/test/signals.test.js @@ -0,0 +1,218 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { extractSignals } = require('../src/gep/signals'); + +const emptyInput = { + recentSessionTranscript: '', + todayLog: '', + memorySnippet: '', + userSnippet: '', + recentEvents: [], +}; + +function hasSignal(signals, name) { + return Array.isArray(signals) && signals.some(s => String(s).startsWith(name)); +} + +function getSignalExtra(signals, name) { + const s = Array.isArray(signals) ? signals.find(x => String(x).startsWith(name + ':')) : undefined; + if (!s) return undefined; + const i = String(s).indexOf(':'); + return i === -1 ? '' : String(s).slice(i + 1).trim(); +} + +describe('extractSignals — user_feature_request (4 languages)', () => { + it('recognizes English feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'Please add a dark mode toggle to the settings page.', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('recognizes Simplified Chinese (简中) feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '加个支付模块,要支持微信和支付宝。', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('recognizes Traditional Chinese (繁中) feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '請加一個匯出報表的功能,要支援 PDF。', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('recognizes Japanese (日) feature request', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'ダークモードのトグルを追加してほしいです。', + }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request in ' + JSON.stringify(r)); + }); + + it('user_feature_request signal carries extra info when present', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'Please add a dark mode toggle to the settings page.', + }); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined, 'expected user_feature_request:extra form'); + assert.ok(extra.length > 0, 'extra should not be empty'); + assert.ok(extra.toLowerCase().includes('dark') || extra.includes('toggle') || extra.includes('add'), 'extra should reflect request content'); + }); +}); + +describe('extractSignals — user_improvement_suggestion (4 languages)', () => { + it('recognizes English improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'The UI could be better; we should simplify the onboarding flow.', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('recognizes Simplified Chinese (简中) improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '改进一下登录流程,优化一下性能。', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('recognizes Traditional Chinese (繁中) improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '建議改進匯出速度,優化一下介面。', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('recognizes Japanese (日) improvement suggestion', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'ログインの流れを改善してほしい。', + }); + assert.ok(hasSignal(r, 'user_improvement_suggestion'), 'expected user_improvement_suggestion in ' + JSON.stringify(r)); + }); + + it('user_improvement_suggestion signal carries extra info when present', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: 'We should refactor the payment module and simplify the API.', + }); + const extra = getSignalExtra(r, 'user_improvement_suggestion'); + assert.ok(extra !== undefined, 'expected user_improvement_suggestion:extra form'); + assert.ok(extra.length > 0, 'extra should not be empty'); + }); +}); + +describe('extractSignals — edge cases (snippet length, 我想, empty, punctuation)', () => { + it('「我想」+ 超长描述:snippet 截断至 200 字以内', () => { + const long = '我想让系统支持批量导入用户、导出报表、自定义工作流、多语言切换、主题切换、权限组、审计日志、Webhook 通知、API 限流、缓存策略配置、数据库备份恢复、灰度发布、A/B 测试、埋点统计、性能监控、告警规则、工单流转、知识库搜索、智能推荐、以及一大堆其他功能以便我们能够更好地管理业务。'; + const r = extractSignals({ ...emptyInput, userSnippet: long }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request'); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0, 'extra should be present'); + assert.ok(extra.length <= 200, 'snippet must be truncated to 200 chars, got ' + extra.length); + }); + + it('「我想」+ 短描述:能识别且带 snippet', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '我想加一个导出 Excel 的功能。' }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0); + }); + + it('「我想。」后无内容:仍识别为 feature request,snippet 可为默认或空', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '我想。' }); + assert.ok(hasSignal(r, 'user_feature_request'), 'expected user_feature_request for 我想。'); + }); + + it('仅「我想」无标点无后续:仍识别', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '我想' }); + assert.ok(hasSignal(r, 'user_feature_request')); + }); + + it('空 userSnippet:不产生 user_feature_request / user_improvement_suggestion(仅来自 user)', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '' }); + const hasFeat = hasSignal(r, 'user_feature_request'); + const hasImp = hasSignal(r, 'user_improvement_suggestion'); + assert.ok(!hasFeat && !hasImp, 'empty userSnippet should not yield feature/improvement from user input'); + }); + + it('仅空格与标点:不匹配为功能/改进', () => { + const r = extractSignals({ ...emptyInput, userSnippet: ' \n\t 。,、 \n' }); + assert.ok(!hasSignal(r, 'user_feature_request'), 'whitespace/punctuation only should not match'); + assert.ok(!hasSignal(r, 'user_improvement_suggestion')); + }); + + it('I want + 超长英文描述:snippet 截断', () => { + const long = 'I want to add a feature that allows users to export data in CSV and Excel formats, with custom column mapping, date range filters, scheduled exports, email delivery, and integration with our analytics pipeline so that we can reduce manual reporting work. This is critical for Q2.'; + const r = extractSignals({ ...emptyInput, userSnippet: long }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra === undefined || extra.length <= 200, 'snippet if present should be <= 200'); + }); + + it('改进一下 + 超长描述:user_improvement_suggestion snippet 截断至 200', () => { + // 避免含「错误/失败/异常/报错」以免触发 errorHit 压制 improvement + const long = '改进一下登录流程:首先支持扫码登录、然后记住设备、然后支持多因素认证、然后审计日志、然后限流防刷、然后国际化提示、然后无障碍优化、然后性能优化、然后安全加固、然后文档补全。'; + const r = extractSignals({ ...emptyInput, userSnippet: long }); + assert.ok(hasSignal(r, 'user_improvement_suggestion')); + const extra = getSignalExtra(r, 'user_improvement_suggestion'); + assert.ok(extra !== undefined && extra.length > 0); + assert.ok(extra.length <= 200, 'improvement snippet <= 200, got ' + extra.length); + }); + + it('多句混合:首句为功能需求时仍识别并带 snippet', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '加个支付模块,要支持微信和支付宝。另外昨天那个 bug 修了吗?', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0); + }); + + it('描述中含换行与制表符:正则能匹配并归一化空格', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '我想\n加一个\t导出\n报表的功能。', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined); + assert.ok(!/\n/.test(extra) || extra.length <= 200, 'snippet should be normalized (no newlines in stored form or truncated)'); + }); + + it('「我想」出现在段落中间:仍能识别', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '前面是一些背景说明。我想加一个暗色模式开关,方便夜间使用。', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + const extra = getSignalExtra(r, 'user_feature_request'); + assert.ok(extra !== undefined && extra.length > 0); + }); + + it('仅标点句「。。。。」不触发功能/改进', () => { + const r = extractSignals({ ...emptyInput, userSnippet: '。。。。' }); + assert.ok(!hasSignal(r, 'user_feature_request')); + assert.ok(!hasSignal(r, 'user_improvement_suggestion')); + }); + + it('user_feature_request 与 user_improvement_suggestion 均带描述时两条都有 extra', () => { + const r = extractSignals({ + ...emptyInput, + userSnippet: '加个支付模块。另外改进一下登录流程,简化步骤。', + }); + assert.ok(hasSignal(r, 'user_feature_request')); + assert.ok(hasSignal(r, 'user_improvement_suggestion')); + assert.ok(getSignalExtra(r, 'user_feature_request')); + assert.ok(getSignalExtra(r, 'user_improvement_suggestion')); + }); +}); From 689c0aeba4b1cd561b6cfe73406674829ed85a03 Mon Sep 17 00:00:00 2001 From: "yu.zhenyu" Date: Tue, 24 Feb 2026 11:19:00 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix(candidates):=20=E5=85=BC=E5=AE=B9=20sig?= =?UTF-8?q?nal:snippet=20=E6=A0=BC=E5=BC=8F=EF=BC=8C=E4=BF=AE=E5=A4=8D=20u?= =?UTF-8?q?ser=5Ffeature=5Frequest=20/=20user=5Fimprovement=5Fsuggestion?= =?UTF-8?q?=20=E5=80=99=E9=80=89=E9=A1=B9=E6=B0=B8=E4=B8=8D=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit signals.js 已将信号格式改为 'name:snippet',但 candidates.js 仍用 includes() 精确匹配裸名,导致这两类候选项静默失效。改用 some(s => s === name || s.startsWith(name + ':')) 与 mutation.js 保持一致。 Co-authored-by: Cursor --- src/gep/candidates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gep/candidates.js b/src/gep/candidates.js index c6309116..d7a56f73 100644 --- a/src/gep/candidates.js +++ b/src/gep/candidates.js @@ -95,7 +95,7 @@ function extractCapabilityCandidates({ recentSessionTranscript, signals }) { ]; for (const sc of signalCandidates) { - if (!signalList.includes(sc.signal)) continue; + if (!signalList.some(s => s === sc.signal || s.startsWith(sc.signal + ':'))) continue; const evidence = `Signal present: ${sc.signal}`; const shape = buildFiveQuestionsShape({ title: sc.title, signals, evidence }); candidates.push({ From 840474531ff191f8bf599b8e8b21a8391d2ae884 Mon Sep 17 00:00:00 2001 From: "yu.zhenyu" Date: Tue, 24 Feb 2026 11:27:40 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix(signals):=20=E4=BF=AE=E5=A4=8D=20user?= =?UTF-8?q?=5Ffeature=5Frequest/user=5Fimprovement=5Fsuggestion=20?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E5=A4=B1=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit analyzeRecentHistory 频率统计时未对 user_feature_request: 和 user_improvement_suggestion: 前缀做归一化,导致 suppressedSignals 中 存储的是完整 key(如 user_feature_request:snippet),而去重过滤器检查的是 裸 key(user_feature_request),两侧不一致造成去重永远失效。 补齐两个前缀的归一化规则,与去重过滤器保持一致。 Co-authored-by: Cursor --- src/gep/signals.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/gep/signals.js b/src/gep/signals.js index 2638847a..fcbfe932 100644 --- a/src/gep/signals.js +++ b/src/gep/signals.js @@ -52,8 +52,12 @@ function analyzeRecentHistory(recentEvents) { var sigs = Array.isArray(evt.signals) ? evt.signals : []; for (var k = 0; k < sigs.length; k++) { var s = String(sigs[k]); - // Normalize: ignore errsig details for frequency counting - var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' : s; + // Normalize: strip details suffix so frequency keys match dedup filter keys + var key = s.startsWith('errsig:') ? 'errsig' + : s.startsWith('recurring_errsig') ? 'recurring_errsig' + : s.startsWith('user_feature_request:') ? 'user_feature_request' + : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion' + : s; signalFreq[key] = (signalFreq[key] || 0) + 1; } var genes = Array.isArray(evt.genes_used) ? evt.genes_used : []; From a601455ba71f9f5f8bb5232cdc647a171cd31610 Mon Sep 17 00:00:00 2001 From: "yu.zhenyu" Date: Tue, 24 Feb 2026 11:31:04 +0800 Subject: [PATCH 4/6] fix(signals): match English improvement snippet against corpus with /i to preserve casing Match user_improvement_suggestion English snippet against corpus (with /i) instead of lower, so API/class names and identifiers keep original casing for selector and GEP prompt context; aligns with other language branches. Co-authored-by: Cursor --- src/gep/signals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gep/signals.js b/src/gep/signals.js index fcbfe932..e7b2c75d 100644 --- a/src/gep/signals.js +++ b/src/gep/signals.js @@ -248,7 +248,7 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user // user_improvement_suggestion: 4 languages + extra var improvementSnippet = ''; if (!errorHit) { - var impEn = lower.match(/.{0,80}\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b.{0,80}/); + var impEn = corpus.match(/.{0,80}\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b.{0,80}/i); if (impEn) improvementSnippet = impEn[0].replace(/\s+/g, ' ').trim().slice(0, 200); if (!improvementSnippet && /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus)) { var impZh = corpus.match(/.{0,100}(改进一下|优化一下|简化|重构|整理一下|弄得更好).{0,100}/); From 64a05465d892cd6730e45a4b404cee88ec823daf Mon Sep 17 00:00:00 2001 From: "yu.zhenyu" Date: Tue, 24 Feb 2026 12:30:10 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(signals):=20require=20colon=20after=20?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=20in=20errorHit=20regex=20to=20avoid=20false?= =?UTF-8?q?=20positives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match errLine and English exception: pattern; bare 异常 (e.g. in 优化一下异常处理) no longer triggers log_error or blocks user_improvement_suggestion. Co-authored-by: Cursor --- src/gep/signals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gep/signals.js b/src/gep/signals.js index e7b2c75d..62acdc07 100644 --- a/src/gep/signals.js +++ b/src/gep/signals.js @@ -152,7 +152,7 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user // Refined error detection regex to avoid false positives on "fail"/"failed" in normal text. // We prioritize structured error markers ([error], error:, exception:) and specific JSON patterns. // Chinese: 错误、异常、失败、报错 (common in logs and stack traces). - var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误|异常|报错|失败\s*[::]/.test(lower); + var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误|异常\s*[::]|报错|失败\s*[::]/.test(lower); if (errorHit) signals.push('log_error'); // Error signature (more reproducible than a coarse "log_error" tag). From 437f02d015497804d565247e16939a284bd81dbd Mon Sep 17 00:00:00 2001 From: "yu.zhenyu" Date: Tue, 24 Feb 2026 16:07:52 +0800 Subject: [PATCH 6/6] chore(signals): tweak comment for auto-review trigger Co-authored-by: Cursor --- src/gep/signals.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gep/signals.js b/src/gep/signals.js index 62acdc07..e17f527b 100644 --- a/src/gep/signals.js +++ b/src/gep/signals.js @@ -151,8 +151,8 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user // Refined error detection regex to avoid false positives on "fail"/"failed" in normal text. // We prioritize structured error markers ([error], error:, exception:) and specific JSON patterns. - // Chinese: 错误、异常、失败、报错 (common in logs and stack traces). - var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误|异常\s*[::]|报错|失败\s*[::]/.test(lower); + // Chinese: 错误、异常、失败、报错 — all require contextual colon [::] so improvement text (e.g. "改进一下错误处理") is not treated as log_error. + var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/.test(lower); if (errorHit) signals.push('log_error'); // Error signature (more reproducible than a coarse "log_error" tag). @@ -163,7 +163,7 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user .filter(Boolean); var errLine = - lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error|错误|异常\s*[::]|报错|失败\s*[::]/i.test(l); }) || + lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/i.test(l); }) || null; if (errLine) {