diff --git a/apps/server/src/modules/ai/ai.service.spec.ts b/apps/server/src/modules/ai/ai.service.spec.ts index b391bb6..f55a78b 100644 --- a/apps/server/src/modules/ai/ai.service.spec.ts +++ b/apps/server/src/modules/ai/ai.service.spec.ts @@ -480,6 +480,32 @@ describe('AiService', () => { expect(testCaseRequired).not.toContain('estimateHours'); }); + it('requires structured descriptions for dev task and test case drafts', () => { + const devRequired = (DECOMPOSE_TOOL_INPUT_SCHEMA.properties.devTaskDrafts as any).items.required; + const testCaseRequired = (DECOMPOSE_TOOL_INPUT_SCHEMA.properties.testCaseDrafts as any).items.required; + + expect(devRequired).toContain('description'); + expect(testCaseRequired).toContain('description'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('实现范围'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('验收点'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('前置条件'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('操作步骤'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('预期结果'); + }); + + it('tells the model to build deliverable slices before drafting tasks and tests', () => { + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('交付切片'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('UI/交互/接口/数据/异常/边界/状态流转/兼容/回归'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('每条业务规则'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('至少映射到 1 条开发任务或 1 条测试用例'); + }); + + it('forbids classifying clear prototype notes as ambiguous', () => { + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('包含标题、详细说明、字段、异常或验收标准之一'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('不得进入 ambiguous'); + expect(DECOMPOSE_SYSTEM_PROMPT).toContain('QY0005/QY0019/QY0020'); + }); + it('passes decomposition target into the model prompt', async () => { const callTool = jest.fn().mockResolvedValue({ toolName: 'submit_decompose', diff --git a/apps/server/src/modules/ai/prompts/decompose.ts b/apps/server/src/modules/ai/prompts/decompose.ts index 62b30ed..3a7a720 100644 --- a/apps/server/src/modules/ai/prompts/decompose.ts +++ b/apps/server/src/modules/ai/prompts/decompose.ts @@ -12,6 +12,13 @@ export const DECOMPOSE_SYSTEM_PROMPT = `你是 FTB 项目管理系统的产品 输入:原型 HTML/文档内容 + 关联需求列表 + 版本成员清单 输出:开发任务草案 + 测试用例草案 + 对账报告 +【拆解工作法】 +- 先在内部把每条 QY 或命中的需求拆成"交付切片",再生成 DevTask/TestCase;交付切片不要输出到工具结果中 +- 交付切片维度固定按 UI/交互/接口/数据/异常/边界/状态流转/兼容/回归 扫描 +- 每条业务规则、字段规则、交互规则、异常规则和验收标准都必须至少映射到 1 条开发任务或 1 条测试用例;本次 target 不包含的一侧可以不输出,但另一侧必须覆盖 +- 若某条规则只能测试不能开发,输出测试用例即可;若某条规则只涉及实现不涉及可独立验证场景,输出开发任务即可 +- 不允许用一个大标题覆盖多个交付切片;宁可多条小草案,也不要一条含糊大草案 + 【硬规则】 1. 引用必须真实 @@ -26,6 +33,8 @@ export const DECOMPOSE_SYSTEM_PROMPT = `你是 FTB 项目管理系统的产品 - 没有 QY 编号但文本已命中需求编号或需求概述时,不要因为缺少 QY 就丢弃;可只引用 requirement,并在 matched.noteIds 返回空数组 - 既匹配不到需求编号,也匹配不到需求概述语义,但能形成明确功能名称和任务/用例范围的原型批注,必须进入 prototypeOnly 无需求ID分组并继续拆解 - 只有无法形成稳定任务/用例的含糊批注才进入 ambiguous + - 原型批注包含标题、详细说明、字段、异常或验收标准之一,且能判断用户动作或系统行为时,不得进入 ambiguous;QY0005/QY0019/QY0020 这类有详细说明、字段和验收标准的批注必须拆解 + - ambiguous 只能用于缺少动作、对象、结果或无法判断改动方向的批注,例如只圈出字段但没有任何说明 3. 任务来源限定 - 只为以下情况拆任务: @@ -43,6 +52,8 @@ export const DECOMPOSE_SYSTEM_PROMPT = `你是 FTB 项目管理系统的产品 - 测试用例粒度要和开发任务一样细:每个明确功能点、UI 交互、表单校验、接口、数据保存、权限、状态流转、异常、边界、兼容性或回归点都应拆成独立 TestCase - 不要用一条"验证 XX 完整流程"覆盖多个交互或多个规则 - 一条 QY 若同时涉及 UI、接口、数据、异常和状态变化,通常应拆出 3-8 个 TestCase + - 开发任务 description 必须写成结构化短文本,至少包含:实现范围、验收点;如有非目标范围或依赖也要说明 + - 测试用例 description 必须写成结构化短文本,至少包含:前置条件、操作步骤、预期结果;涉及边界/异常/数据时要写明测试数据或状态 5. 任务类型 - 每条开发任务和测试用例都必须输出 categoryCode @@ -186,7 +197,7 @@ export const DECOMPOSE_TOOL_INPUT_SCHEMA = { minItems: 1, }, }, - required: ['title', 'categoryCode', 'priority', 'aiEstimateHours', 'references'], + required: ['title', 'description', 'categoryCode', 'priority', 'aiEstimateHours', 'references'], }, }, testCaseDrafts: { diff --git a/apps/web/lib/ai-decompose-dedupe.test.ts b/apps/web/lib/ai-decompose-dedupe.test.ts index 18dbb3f..b0a8439 100644 --- a/apps/web/lib/ai-decompose-dedupe.test.ts +++ b/apps/web/lib/ai-decompose-dedupe.test.ts @@ -55,6 +55,7 @@ test('filters dev task drafts already adopted into existing tasks', () => { devTaskDrafts: [ { title: '实现拖拽排序', + description: '实现范围:支持列表拖拽调整顺序。验收点:拖拽后顺序保持一致。', categoryCode: 'frontend_interaction', priority: 'P2', aiEstimateHours: 0.25, @@ -123,6 +124,7 @@ test('keeps drafts with same references but different normalized title', () => { devTaskDrafts: [ { title: '保存拖拽排序结果', + description: '实现范围:保存拖拽后的排序结果。验收点:刷新后顺序不丢失。', categoryCode: 'frontend_interaction', priority: 'P2', aiEstimateHours: 0.5, diff --git a/apps/web/lib/ai-decompose-target.test.ts b/apps/web/lib/ai-decompose-target.test.ts index b3152b3..73ea1b6 100644 --- a/apps/web/lib/ai-decompose-target.test.ts +++ b/apps/web/lib/ai-decompose-target.test.ts @@ -10,6 +10,7 @@ function makeResult(): AgentDecomposeResult { devTaskDrafts: [ { title: '实现登录表单', + description: '实现范围:渲染账号密码登录表单。验收点:输入合法信息后可提交。', categoryCode: 'frontend_development', priority: 'P2', aiEstimateHours: 0.5, diff --git a/docs/agent-spec.md b/docs/agent-spec.md index 4ee9c21..5ccb400 100644 --- a/docs/agent-spec.md +++ b/docs/agent-spec.md @@ -65,7 +65,7 @@ **输出**: - 对账报告(结构化文本,含"完美对应/需求未见原型/无需求ID分组/含糊") -- DevTask 草案数组(每条带 `aiDraft: true` + `references[]` + `aiEstimateHours`,可选 `recommendedAssigneeName` / `recommendedAssigneeReason`) +- DevTask 草案数组(每条带结构化 `description` + `aiDraft: true` + `references[]` + `aiEstimateHours`,可选 `recommendedAssigneeName` / `recommendedAssigneeReason`) - TestCase 草案数组(每条带 `aiDraft: true` + `references[]` + `aiEstimateHours`,可选 `recommendedAssigneeName` / `recommendedAssigneeReason`) - `target = dev_tasks` 时 `testCaseDrafts` 必须为空数组;`target = test_cases` 时 `devTaskDrafts` 必须为空数组 @@ -76,11 +76,19 @@ - 没有 QY 编号但原型文本已命中需求编号或需求概述时,不应丢弃;可只引用 `requirement`,并在 `matched.noteIds` 返回空数组。 - 既匹配不到需求编号,也匹配不到需求概述语义,但能形成明确功能名称和任务范围的原型批注,进入 `prototypeOnly`,对应草案带 `requirementName` 且不带 `requirement` 引用。 - 只有无法形成稳定任务/用例的含糊批注才进入 `ambiguous`。 +- 原型批注只要包含标题、详细说明、字段、异常或验收标准之一,且能判断用户动作或系统行为,就不能进入 `ambiguous`;这类内容必须进入 `matched` 或 `prototypeOnly` 并继续拆解。 + +**开发任务与测试用例拆解粒度**: +- Agent 先在内部把每条 QY 或命中的需求拆成交付切片,再生成 DevTask/TestCase;交付切片维度按 UI、交互、接口、数据、异常、边界、状态流转、兼容和回归扫描。 +- 每条业务规则、字段规则、交互规则、异常规则和验收标准都必须至少映射到 1 条开发任务或 1 条测试用例;本次 target 不包含的一侧可以不输出,但另一侧必须覆盖。 +- DevTask 必须是一个可交付工程动作。前端、后端、接口、数据库、数据处理和集成支持要按职责拆开。 +- DevTask `description` 必填,至少包含实现范围和验收点;如存在非目标范围或依赖,也必须写明。 **测试用例拆解粒度**: - TestCase 要按功能点、UI 交互、表单校验、接口、数据一致性、权限、异常、边界、状态流转、兼容性和回归点拆细。 - 不允许用一条“验证 XX 完整流程”覆盖多个交互、多个接口或多个规则。 - 一条 QY 若同时涉及 UI、接口、数据、异常和状态变化,通常应拆出 3-8 条 TestCase。 +- TestCase `description` 必填,至少包含前置条件、操作步骤和预期结果;涉及边界、异常或数据一致性时,必须写明测试数据或状态。 - 测试用例 `categoryCode` 可使用:`test_functional`、`test_ui_interaction`、`test_form_validation`、`test_api`、`test_data_consistency`、`test_permission`、`test_exception`、`test_boundary`、`test_state_flow`、`test_compatibility`、`test_regression`。 **写入**: @@ -250,7 +258,7 @@ interface DecomposeOutput { prototypeOnly: Array<{ requirementName: string; noteIds: string[]; taskCount: number }>; ambiguous: Array<{ noteId: string; reason: string }>; }; - devTaskDrafts: Array<{ title: string; description?: string; categoryCode: string; priority: Priority; aiEstimateHours: number; requirementName?: string; recommendedAssigneeName?: string; recommendedAssigneeReason?: string; references: Reference[] }>; + devTaskDrafts: Array<{ title: string; description: string; categoryCode: string; priority: Priority; aiEstimateHours: number; requirementName?: string; recommendedAssigneeName?: string; recommendedAssigneeReason?: string; references: Reference[] }>; testCaseDrafts: Array<{ title: string; description: string; categoryCode: string; priority: Priority; aiEstimateHours: number; requirementName?: string; recommendedAssigneeName?: string; recommendedAssigneeReason?: string; references: Reference[] }>; } ``` diff --git a/docs/decisions.md b/docs/decisions.md index 7be284f..17f51b4 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -490,3 +490,16 @@ - 冲突响应返回 `currentVersion` 和 `currentValue`,但当前前端不自动合并、不自动重试,避免把旧本地副本用新版本号再次覆盖服务端数据。 **理由**:这是 AppData 阶段成本最低、收益最高的一致性补强。它不能提供字段级协同编辑,但能阻止最危险的“静默最后写入覆盖”。后续拆成关系表和领域 API 后,再在具体实体上做更细粒度的事务、唯一约束、审计日志和冲突合并 UI。 + +## 40. AI 拆解借鉴 Superpowers 的交付切片和测试倒逼粒度 + +**问题**:AI 原型拆解虽然已经要求细颗粒任务和测试用例,但模型仍可能把清晰 QY 批注压成一条大任务、一条“完整流程验证”,或者把带详细说明、字段和验收标准的批注误判为含糊。 + +**决策**: +- Prototype Decompose Agent 在生成 DevTask/TestCase 前,必须先按 UI、交互、接口、数据、异常、边界、状态流转、兼容和回归做内部交付切片。 +- 每条业务规则、字段规则、交互规则、异常规则和验收标准,必须至少映射到 1 条开发任务或 1 条测试用例;本次 target 不包含的一侧可以不输出,但另一侧必须覆盖。 +- DevTask `description` 从可选变为必填,至少包含实现范围和验收点。 +- TestCase `description` 必须包含前置条件、操作步骤和预期结果;涉及边界、异常或数据一致性时写明测试数据或状态。 +- 原型批注只要包含标题、详细说明、字段、异常或验收标准之一,且能判断用户动作或系统行为,就不得进入 `ambiguous`;必须进入 `matched` 或 `prototypeOnly` 继续拆解。 + +**理由**:Superpowers 的任务计划和 TDD 流程本质上是“先把工作拆成可验证切片,再用测试约束倒逼粒度”。把这套方法沉淀到 Agent 契约里,能减少粗拆、漏拆和误判含糊,同时不改变现有数据模型,只提高 AI 草案的结构质量。 diff --git a/packages/shared/src/agent.ts b/packages/shared/src/agent.ts index d2b32d7..22b41e6 100644 --- a/packages/shared/src/agent.ts +++ b/packages/shared/src/agent.ts @@ -34,7 +34,7 @@ export interface AgentReference { export interface AgentDevTaskDraft { title: string; - description?: string; + description: string; categoryCode: AgentTaskCategoryCode; priority: 'P0' | 'P1' | 'P2' | 'P3'; aiEstimateHours: number;