fix(web): prefer intake markers over the shorter URS title
「初始用户需求表」contains「用户需求表」, so title matching sent V0.1 wording to the V0.5 task and the stage gate rejected it. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -239,3 +239,30 @@ test('v3 bd_ task key wins over unprefixed intake alias', () => {
|
||||
const resolved = resolveTaskTemplateForMessage('开始编写用户初始需求表', [bdIntake]);
|
||||
assert.equal(resolved?.taskKey, 'bd_intake');
|
||||
});
|
||||
|
||||
test('word-order variants of the intake sheet stay on intake, not the shorter URS title', () => {
|
||||
const bdIntake: DeliverableItem = {
|
||||
...base,
|
||||
docId: 'doc_proj6579_bd_intake',
|
||||
projectId: 6579,
|
||||
taskKey: 'bd_intake',
|
||||
title: '用户初始需求收集表',
|
||||
isTemplateOnly: true,
|
||||
};
|
||||
const bdUrs: DeliverableItem = {
|
||||
...base,
|
||||
docId: 'doc_proj6579_bd_user_requirements',
|
||||
projectId: 6579,
|
||||
taskKey: 'bd_user_requirements',
|
||||
title: '用户需求表',
|
||||
stage: 'V0.5',
|
||||
isTemplateOnly: true,
|
||||
};
|
||||
const pool = [bdIntake, bdUrs];
|
||||
for (const query of ['编写初始用户需求表', '生成需求表', '编写用户初始需求收集表']) {
|
||||
assert.equal(resolveTaskKeyFromMessage(query), 'intake', query);
|
||||
assert.equal(resolveTaskTemplateForMessage(query, pool)?.taskKey, 'bd_intake', query);
|
||||
assert.equal(resolveTaskTemplateForMessage(query, bothPool)?.taskKey, 'intake', query);
|
||||
}
|
||||
assert.equal(resolveTaskTemplateForMessage('请更新用户需求表 URS', pool)?.taskKey, 'bd_user_requirements');
|
||||
});
|
||||
|
||||
@@ -7,7 +7,10 @@ const WRITE_INTENT =
|
||||
const CURRENT_DOC_DEIXIS = /当前受控文档|当前文档|本文件|这份文档|本文档|本受控文档/;
|
||||
|
||||
const NAMED_DELIVERABLE_ALIASES: { taskKey: string; patterns: RegExp[] }[] = [
|
||||
{ taskKey: 'intake', patterns: [/用户初始需求收集表/, /初始需求表/, /需求摸底表/] },
|
||||
{
|
||||
taskKey: 'intake',
|
||||
patterns: [/用户初始需求收集表/, /初始用户需求表/, /用户初始需求/, /初始需求表/, /需求摸底表/],
|
||||
},
|
||||
{ taskKey: 'user_requirements', patterns: [/用户需求表/, /\bURS\b/, /用户需求规格/] },
|
||||
{ taskKey: 'research', patterns: [/调研报告/, /竞品调研/] },
|
||||
{ taskKey: 'feasibility_reg', patterns: [/注册路径可行性/, /法规可行性评估/, /注册可行性评估/] },
|
||||
@@ -34,6 +37,29 @@ export function hasDocumentWriteIntent(text: string): boolean {
|
||||
return WRITE_INTENT.test(normalized);
|
||||
}
|
||||
|
||||
function isRequirementsTopic(text: string): boolean {
|
||||
return (
|
||||
text.includes('需求') ||
|
||||
text.includes('摸底') ||
|
||||
text.includes('用户') ||
|
||||
text.includes('首轮')
|
||||
);
|
||||
}
|
||||
|
||||
/** V0.1 摸底的强标记。语序换了也要赢过更短的「用户需求表」子串。 */
|
||||
export function hasIntakeSemanticMarker(text: string): boolean {
|
||||
return (
|
||||
text.includes('首轮') ||
|
||||
text.includes('初始') ||
|
||||
text.includes('需求摸底') ||
|
||||
text.includes('摸底')
|
||||
);
|
||||
}
|
||||
|
||||
function isUserRequirementsKey(taskKey: string | undefined): boolean {
|
||||
return taskKey === 'user_requirements' || Boolean(taskKey?.endsWith('_user_requirements'));
|
||||
}
|
||||
|
||||
export function refersToCurrentControlledDocument(text: string): boolean {
|
||||
return CURRENT_DOC_DEIXIS.test(text.trim());
|
||||
}
|
||||
@@ -64,7 +90,12 @@ function explicitNamedDeliverableTaskKey(
|
||||
.filter((d) => d.taskKey && d.taskKey !== excludeTaskKey && d.title?.trim())
|
||||
.sort((a, b) => (b.title?.length || 0) - (a.title?.length || 0));
|
||||
for (const d of titled) {
|
||||
if (text.includes(d.title)) return d.taskKey;
|
||||
if (!text.includes(d.title)) continue;
|
||||
// 「用户需求表」是「初始用户需求表」的子串,不能盖过 V0.1 强标记。
|
||||
if (isUserRequirementsKey(d.taskKey) && isRequirementsTopic(text) && hasIntakeSemanticMarker(text)) {
|
||||
continue;
|
||||
}
|
||||
return d.taskKey;
|
||||
}
|
||||
return namedAliasTaskKey(text, excludeTaskKey);
|
||||
}
|
||||
@@ -117,6 +148,9 @@ function pickRequirementsTask(text: string, pool: DeliverableItem[]): Deliverabl
|
||||
function namedAliasTaskKey(text: string, excludeTaskKey?: string): string | undefined {
|
||||
for (const alias of NAMED_DELIVERABLE_ALIASES) {
|
||||
if (alias.taskKey === excludeTaskKey) continue;
|
||||
if (isUserRequirementsKey(alias.taskKey) && isRequirementsTopic(text) && hasIntakeSemanticMarker(text)) {
|
||||
continue;
|
||||
}
|
||||
if (alias.patterns.some((p) => p.test(text))) return alias.taskKey;
|
||||
}
|
||||
return undefined;
|
||||
@@ -146,11 +180,16 @@ export function resolveTaskKeyFromMessage(text: string): string | undefined {
|
||||
// 模板名只是操作对象,不代表用户授权写入。只读动词优先;只有明确写动作才走任务 API。
|
||||
if (!hasDocumentWriteIntent(normalized)) return undefined;
|
||||
|
||||
// 「初始 / 首轮 / 摸底」优先于别名子串,避免「初始用户需求表」被「用户需求表」截走。
|
||||
if (isRequirementsTopic(normalized) && hasIntakeSemanticMarker(normalized)) {
|
||||
return 'intake';
|
||||
}
|
||||
|
||||
// 明确点名的交付物优先于否定背景/旧文档里的「需求」等关键词。
|
||||
const named = namedAliasTaskKey(normalized);
|
||||
if (named) return named;
|
||||
|
||||
if (normalized.includes('需求') || normalized.includes('摸底') || normalized.includes('用户') || normalized.includes('首轮')) {
|
||||
if (isRequirementsTopic(normalized)) {
|
||||
return resolveRequirementsTaskKey(normalized);
|
||||
}
|
||||
if (normalized.includes('调研') || normalized.includes('竞品')) return 'research';
|
||||
@@ -214,6 +253,12 @@ export function resolveTaskTemplateForMessage(
|
||||
}
|
||||
}
|
||||
|
||||
if (isRequirementsTopic(text) && hasIntakeSemanticMarker(text)) {
|
||||
const taskKey = normalizeTaskKeyToProject('intake', deliverables);
|
||||
const template = matchTemplateDeliverableByKey(taskKey, deliverables, text);
|
||||
return { taskKey, template };
|
||||
}
|
||||
|
||||
const named = explicitNamedDeliverableTaskKey(text, deliverables);
|
||||
if (named) {
|
||||
const taskKey = normalizeTaskKeyToProject(named, deliverables);
|
||||
|
||||
Reference in New Issue
Block a user