fix(ai): 收紧AI任务类型入库规则
This commit is contained in:
@@ -423,6 +423,8 @@ describe('AiService', () => {
|
|||||||
expect(devCategoryCodeSchema).toEqual({ type: 'string' });
|
expect(devCategoryCodeSchema).toEqual({ type: 'string' });
|
||||||
expect(testCaseCategoryCodeSchema).toEqual({ type: 'string' });
|
expect(testCaseCategoryCodeSchema).toEqual({ type: 'string' });
|
||||||
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('taskTypeName');
|
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('taskTypeName');
|
||||||
|
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('taskTypeName 是任务类型字典项,不是任务标题或任务概述');
|
||||||
|
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('必须可复用');
|
||||||
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('categoryCode 只作为可选映射');
|
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('categoryCode 只作为可选映射');
|
||||||
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('不得因为任务类型字典没有覆盖就不拆解');
|
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('不得因为任务类型字典没有覆盖就不拆解');
|
||||||
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('采纳时自动追加到任务类型字典');
|
expect(DECOMPOSE_SYSTEM_PROMPT).toContain('采纳时自动追加到任务类型字典');
|
||||||
|
|||||||
@@ -56,12 +56,14 @@ export const DECOMPOSE_SYSTEM_PROMPT = `你是 FTB 项目管理系统的产品
|
|||||||
- 测试用例 description 必须写成结构化短文本,至少包含:前置条件、操作步骤、预期结果;涉及边界/异常/数据时要写明测试数据或状态
|
- 测试用例 description 必须写成结构化短文本,至少包含:前置条件、操作步骤、预期结果;涉及边界/异常/数据时要写明测试数据或状态
|
||||||
|
|
||||||
5. 任务类型
|
5. 任务类型
|
||||||
- 每条开发任务和测试用例都必须输出 taskTypeName,使用中文业务类型名,例如"人员名片交互"、"预入职数据源"、"姓名展示兼容测试"
|
- 每条开发任务和测试用例都必须输出 taskTypeName
|
||||||
|
- taskTypeName 是任务类型字典项,不是任务标题或任务概述;必须可复用,不能写成当前文档里的业务对象或场景名称
|
||||||
|
- 正确示例:前端开发、前端交互、后端接口、数据库设计、数据处理、功能测试、接口测试、数据一致性测试、权限测试、状态流转测试、异常场景测试
|
||||||
|
- 错误示例:排行榜测试、人员名片识别交互、预入职人员选择展示、姓名展示兼容测试;这些应写进 title 或 description
|
||||||
- categoryCode 只作为可选映射;只有能明确映射到已有稳定语义码时才输出
|
- categoryCode 只作为可选映射;只有能明确映射到已有稳定语义码时才输出
|
||||||
- 不得因为任务类型字典没有覆盖就不拆解;如果没有合适 categoryCode,省略 categoryCode,但必须保留准确的 taskTypeName
|
- 不得因为任务类型字典没有覆盖就不拆解;如果没有合适 categoryCode,省略 categoryCode,但必须保留准确的 taskTypeName
|
||||||
- 采纳时自动追加到任务类型字典,AI 不需要也不得输出数据库 categoryId
|
- 采纳时自动追加到任务类型字典仅限可跨需求复用的新增类型;不要为单个原型或单条 QY 造一次性类型
|
||||||
- 开发任务建议类型:前端开发、前端交互、后端开发、后端接口、数据库设计、接口联调、数据处理、实施支持、文档,或按原型识别出更准确的类型名
|
- 测试用例优先使用测试用例建议类型:功能测试、UI交互测试、表单校验测试、接口测试、数据一致性测试、权限测试、异常场景测试、边界值测试、状态流转测试、兼容性测试、回归测试
|
||||||
- 测试用例建议类型:功能测试、UI交互测试、表单校验测试、接口测试、数据一致性测试、权限测试、异常场景测试、边界值测试、状态流转测试、兼容性测试、回归测试,或按验收点识别出更准确的类型名
|
|
||||||
- 不输出数据库 categoryId
|
- 不输出数据库 categoryId
|
||||||
|
|
||||||
6. 推荐负责人(可选字段)
|
6. 推荐负责人(可选字段)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
|||||||
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||||
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
||||||
import { useAuthStore } from '@/stores/useAuthStore';
|
import { useAuthStore } from '@/stores/useAuthStore';
|
||||||
import { findCategoryByCode, type CategoryGroup } from '@/lib/task-category';
|
import { findCategoryByCode, resolveAiTaskCategoryByName, type CategoryGroup } from '@/lib/task-category';
|
||||||
import { clampDevAiEstimateHours, clampTestCaseAiEstimateHours } from '@/lib/ai-estimation-policy';
|
import { clampDevAiEstimateHours, clampTestCaseAiEstimateHours } from '@/lib/ai-estimation-policy';
|
||||||
import { resolveDecomposeDraftRequirementFields } from '@/lib/ai-decompose-adoption';
|
import { resolveDecomposeDraftRequirementFields } from '@/lib/ai-decompose-adoption';
|
||||||
import { formatReportRequirementLabel } from '@/lib/ai-decompose-report';
|
import { formatReportRequirementLabel } from '@/lib/ai-decompose-report';
|
||||||
@@ -102,6 +102,15 @@ export function DecomposeReportModal({
|
|||||||
return fallbackGroup === 'testing' ? '功能测试' : '开发任务';
|
return fallbackGroup === 'testing' ? '功能测试' : '开发任务';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveDraftCategoryName = (
|
||||||
|
draft: AgentDevTaskDraft | AgentTestCaseDraft,
|
||||||
|
fallbackGroup: Extract<CategoryGroup, 'development' | 'testing'>,
|
||||||
|
) =>
|
||||||
|
resolveAiTaskCategoryByName(categories, resolveDraftTaskTypeName(draft, fallbackGroup), fallbackGroup, {
|
||||||
|
allowCreate: fallbackGroup !== 'testing',
|
||||||
|
fallbackCode: draft.categoryCode,
|
||||||
|
}).category.name;
|
||||||
|
|
||||||
const handleAdopt = () => {
|
const handleAdopt = () => {
|
||||||
if (submitting) return;
|
if (submitting) return;
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@@ -124,7 +133,7 @@ export function DecomposeReportModal({
|
|||||||
const refs = normalizeRefs(draft.references);
|
const refs = normalizeRefs(draft.references);
|
||||||
const requirementFields = resolveDecomposeDraftRequirementFields({ ...draft, references: refs }, requirements);
|
const requirementFields = resolveDecomposeDraftRequirementFields({ ...draft, references: refs }, requirements);
|
||||||
const taskTypeName = resolveDraftTaskTypeName(draft, 'development');
|
const taskTypeName = resolveDraftTaskTypeName(draft, 'development');
|
||||||
const categoryId = ensureCategory(taskTypeName, 'development').id;
|
const categoryId = ensureCategory(taskTypeName, 'development', { fallbackCode: draft.categoryCode }).id;
|
||||||
const aiEstimateHours = clampDevAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
|
const aiEstimateHours = clampDevAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
|
||||||
const recommendedAssignee = resolveRecommendedAssignee(draft, version.members ?? []);
|
const recommendedAssignee = resolveRecommendedAssignee(draft, version.members ?? []);
|
||||||
|
|
||||||
@@ -166,7 +175,10 @@ export function DecomposeReportModal({
|
|||||||
const refs = normalizeRefs(draft.references);
|
const refs = normalizeRefs(draft.references);
|
||||||
const requirementFields = resolveDecomposeDraftRequirementFields({ ...draft, references: refs }, requirements);
|
const requirementFields = resolveDecomposeDraftRequirementFields({ ...draft, references: refs }, requirements);
|
||||||
const taskTypeName = resolveDraftTaskTypeName(draft, 'testing');
|
const taskTypeName = resolveDraftTaskTypeName(draft, 'testing');
|
||||||
const categoryId = ensureCategory(taskTypeName, 'testing').id;
|
const categoryId = ensureCategory(taskTypeName, 'testing', {
|
||||||
|
allowCreate: false,
|
||||||
|
fallbackCode: draft.categoryCode,
|
||||||
|
}).id;
|
||||||
const aiEstimateHours = clampTestCaseAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
|
const aiEstimateHours = clampTestCaseAiEstimateHours(draft.categoryCode, draft.aiEstimateHours);
|
||||||
const recommendedAssignee = resolveRecommendedAssignee(draft, version.members ?? []);
|
const recommendedAssignee = resolveRecommendedAssignee(draft, version.members ?? []);
|
||||||
|
|
||||||
@@ -351,7 +363,7 @@ export function DecomposeReportModal({
|
|||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="text-[13px] font-medium text-[var(--ink)]">{d.title}</span>
|
<span className="text-[13px] font-medium text-[var(--ink)]">{d.title}</span>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-subtle)] text-[var(--ink-soft)]">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-subtle)] text-[var(--ink-soft)]">
|
||||||
{resolveDraftTaskTypeName(d, 'development')}
|
{resolveDraftCategoryName(d, 'development')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600">
|
||||||
{d.priority}
|
{d.priority}
|
||||||
@@ -422,7 +434,7 @@ export function DecomposeReportModal({
|
|||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="text-[13px] font-medium text-[var(--ink)]">{d.title}</span>
|
<span className="text-[13px] font-medium text-[var(--ink)]">{d.title}</span>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-subtle)] text-[var(--ink-soft)]">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-[var(--bg-subtle)] text-[var(--ink-soft)]">
|
||||||
{resolveDraftTaskTypeName(d, 'testing')}
|
{resolveDraftCategoryName(d, 'testing')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600">
|
||||||
{d.priority}
|
{d.priority}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
getDefaultCategoryByGroup,
|
getDefaultCategoryByGroup,
|
||||||
ensureTaskCategoryByName,
|
ensureTaskCategoryByName,
|
||||||
normalizeTaskCategories,
|
normalizeTaskCategories,
|
||||||
|
resolveAiTaskCategoryByName,
|
||||||
resolveCategoryIdFromCode,
|
resolveCategoryIdFromCode,
|
||||||
} from './task-category';
|
} from './task-category';
|
||||||
|
|
||||||
@@ -68,3 +69,25 @@ test('creates non-system category for missing AI task type name', () => {
|
|||||||
assert.ok(result.category.id.startsWith('cat-ai-'));
|
assert.ok(result.category.id.startsWith('cat-ai-'));
|
||||||
assert.equal(result.categories.at(-1)?.id, result.category.id);
|
assert.equal(result.categories.at(-1)?.id, result.category.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('does not create document-specific testing type when auto creation is disabled', () => {
|
||||||
|
const result = resolveAiTaskCategoryByName(PRESET_CATEGORIES, '排行榜测试', 'testing', {
|
||||||
|
allowCreate: false,
|
||||||
|
fallbackCode: 'test_data_consistency',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.created, false);
|
||||||
|
assert.equal(result.category.code, 'test_data_consistency');
|
||||||
|
assert.equal(result.categories.length, PRESET_CATEGORIES.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps testing fallback within the testing category group', () => {
|
||||||
|
const result = resolveAiTaskCategoryByName(PRESET_CATEGORIES, '排行榜测试', 'testing', {
|
||||||
|
allowCreate: false,
|
||||||
|
fallbackCode: 'frontend_development',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.created, false);
|
||||||
|
assert.equal(result.category.id, DEFAULT_TEST_CATEGORY_ID);
|
||||||
|
assert.equal(result.category.group, 'testing');
|
||||||
|
});
|
||||||
|
|||||||
@@ -123,6 +123,15 @@ export function findCategoryByCode(categories: TaskCategory[], code?: string): T
|
|||||||
return categories.find((c) => c.code === code);
|
return categories.find((c) => c.code === code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findCategoryByCodeInGroup(
|
||||||
|
categories: TaskCategory[],
|
||||||
|
code: string | undefined,
|
||||||
|
group: CategoryGroup,
|
||||||
|
): TaskCategory | undefined {
|
||||||
|
if (!code) return undefined;
|
||||||
|
return categories.find((category) => category.group === group && category.code === code);
|
||||||
|
}
|
||||||
|
|
||||||
export function getDefaultCategoryByGroup(categories: TaskCategory[], group: CategoryGroup): TaskCategory {
|
export function getDefaultCategoryByGroup(categories: TaskCategory[], group: CategoryGroup): TaskCategory {
|
||||||
return getCategoriesByGroup(categories, group)[0] ?? categories[0] ?? PRESET_CATEGORIES[0];
|
return getCategoriesByGroup(categories, group)[0] ?? categories[0] ?? PRESET_CATEGORIES[0];
|
||||||
}
|
}
|
||||||
@@ -135,13 +144,24 @@ export function ensureTaskCategoryByName(
|
|||||||
categories: TaskCategory[],
|
categories: TaskCategory[],
|
||||||
name: string | undefined,
|
name: string | undefined,
|
||||||
group: CategoryGroup,
|
group: CategoryGroup,
|
||||||
|
): { categories: TaskCategory[]; category: TaskCategory; created: boolean } {
|
||||||
|
return resolveAiTaskCategoryByName(categories, name, group, { allowCreate: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAiTaskCategoryByName(
|
||||||
|
categories: TaskCategory[],
|
||||||
|
name: string | undefined,
|
||||||
|
group: CategoryGroup,
|
||||||
|
options: { allowCreate?: boolean; fallbackCode?: string } = {},
|
||||||
): { categories: TaskCategory[]; category: TaskCategory; created: boolean } {
|
): { categories: TaskCategory[]; category: TaskCategory; created: boolean } {
|
||||||
const normalizedCategories = normalizeTaskCategories(categories);
|
const normalizedCategories = normalizeTaskCategories(categories);
|
||||||
const trimmedName = name?.trim() ?? '';
|
const trimmedName = name?.trim() ?? '';
|
||||||
if (!trimmedName) {
|
if (!trimmedName) {
|
||||||
return {
|
return {
|
||||||
categories: normalizedCategories,
|
categories: normalizedCategories,
|
||||||
category: getDefaultCategoryByGroup(normalizedCategories, group),
|
category:
|
||||||
|
findCategoryByCodeInGroup(normalizedCategories, options.fallbackCode, group) ??
|
||||||
|
getDefaultCategoryByGroup(normalizedCategories, group),
|
||||||
created: false,
|
created: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -153,6 +173,15 @@ export function ensureTaskCategoryByName(
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
return { categories: normalizedCategories, category: existing, created: false };
|
return { categories: normalizedCategories, category: existing, created: false };
|
||||||
}
|
}
|
||||||
|
if (options.allowCreate === false) {
|
||||||
|
return {
|
||||||
|
categories: normalizedCategories,
|
||||||
|
category:
|
||||||
|
findCategoryByCodeInGroup(normalizedCategories, options.fallbackCode, group) ??
|
||||||
|
getDefaultCategoryByGroup(normalizedCategories, group),
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const codeBase = slugifyCategoryName(trimmedName, normalizedCategories.length);
|
const codeBase = slugifyCategoryName(trimmedName, normalizedCategories.length);
|
||||||
const category: TaskCategory = {
|
const category: TaskCategory = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { TaskCategory, CategoryGroup } from '@/lib/task-category';
|
import type { TaskCategory, CategoryGroup } from '@/lib/task-category';
|
||||||
import { PRESET_CATEGORIES, ensureTaskCategoryByName, normalizeTaskCategories } from '@/lib/task-category';
|
import { PRESET_CATEGORIES, normalizeTaskCategories, resolveAiTaskCategoryByName } from '@/lib/task-category';
|
||||||
import { loadServerData, saveServerData } from '@/lib/server-data';
|
import { loadServerData, saveServerData } from '@/lib/server-data';
|
||||||
|
|
||||||
function saveStored(items: TaskCategory[]) {
|
function saveStored(items: TaskCategory[]) {
|
||||||
@@ -19,7 +19,7 @@ interface TaskCategoryState {
|
|||||||
categories: TaskCategory[];
|
categories: TaskCategory[];
|
||||||
fetchCategories: () => Promise<void>;
|
fetchCategories: () => Promise<void>;
|
||||||
addCategory: (name: string, group: CategoryGroup, color?: string) => void;
|
addCategory: (name: string, group: CategoryGroup, color?: string) => void;
|
||||||
ensureCategory: (name: string, group: CategoryGroup) => TaskCategory;
|
ensureCategory: (name: string, group: CategoryGroup, options?: { allowCreate?: boolean; fallbackCode?: string }) => TaskCategory;
|
||||||
updateCategory: (id: string, data: Partial<TaskCategory>) => void;
|
updateCategory: (id: string, data: Partial<TaskCategory>) => void;
|
||||||
deleteCategory: (id: string) => boolean;
|
deleteCategory: (id: string) => boolean;
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,8 @@ export const useTaskCategoryStore = create<TaskCategoryState>((set, get) => ({
|
|||||||
saveStored(updated);
|
saveStored(updated);
|
||||||
},
|
},
|
||||||
|
|
||||||
ensureCategory: (name, group) => {
|
ensureCategory: (name, group, options) => {
|
||||||
const result = ensureTaskCategoryByName(get().categories, name, group);
|
const result = resolveAiTaskCategoryByName(get().categories, name, group, options);
|
||||||
if (result.created) {
|
if (result.created) {
|
||||||
set({ categories: result.categories });
|
set({ categories: result.categories });
|
||||||
saveStored(result.categories);
|
saveStored(result.categories);
|
||||||
|
|||||||
@@ -89,12 +89,12 @@
|
|||||||
- 不允许用一条“验证 XX 完整流程”覆盖多个交互、多个接口或多个规则。
|
- 不允许用一条“验证 XX 完整流程”覆盖多个交互、多个接口或多个规则。
|
||||||
- 一条 QY 若同时涉及 UI、接口、数据、异常和状态变化,通常应拆出 3-8 条 TestCase。
|
- 一条 QY 若同时涉及 UI、接口、数据、异常和状态变化,通常应拆出 3-8 条 TestCase。
|
||||||
- TestCase `description` 必填,至少包含前置条件、操作步骤和预期结果;涉及边界、异常或数据一致性时,必须写明测试数据或状态。
|
- TestCase `description` 必填,至少包含前置条件、操作步骤和预期结果;涉及边界、异常或数据一致性时,必须写明测试数据或状态。
|
||||||
- DevTask / TestCase 必须输出 `taskTypeName`,用于展示和任务类型字典写入。`categoryCode` 只作为可选的兼容映射字段;没有合适稳定码时可以省略,不能因此停止拆解。
|
- DevTask / TestCase 必须输出 `taskTypeName`,但它必须是可复用的任务类型字典项,不是任务标题或任务概述。`categoryCode` 只作为可选的兼容映射字段;没有合适稳定码时可以省略,不能因此停止拆解。
|
||||||
|
|
||||||
**写入**:
|
**写入**:
|
||||||
- 用户确认后,调用 `useDevTaskStore.createTask` 和 `useTestCaseStore.createTestCase`
|
- 用户确认后,调用 `useDevTaskStore.createTask` 和 `useTestCaseStore.createTestCase`
|
||||||
- 打开采纳弹窗前,前端先过滤当前版本已采纳过的重复 DevTask/TestCase 草案
|
- 打开采纳弹窗前,前端先过滤当前版本已采纳过的重复 DevTask/TestCase 草案
|
||||||
- 写入前,前端按 `taskTypeName` 检查 `TaskCategory` 字典;不存在时自动追加非系统任务类型,再用新 `categoryId` 写入 DevTask/TestCase
|
- 写入前,前端按 `taskTypeName` 检查 `TaskCategory` 字典;可复用开发类型不存在时自动追加非系统任务类型。测试用例未知类型不自动入库,优先按 `categoryCode` 或默认测试类型回退。
|
||||||
- 写入字段中 `aiDraft: true`、`aiDraftAt: ISO时间戳`
|
- 写入字段中 `aiDraft: true`、`aiDraftAt: ISO时间戳`
|
||||||
- 写入 `aiEstimateHours`,不写入执行人预估 `estimateHours`
|
- 写入 `aiEstimateHours`,不写入执行人预估 `estimateHours`
|
||||||
- DevTask / TestCase 必须写入 `versionId` 作为执行归属;`requirementId` 可选
|
- DevTask / TestCase 必须写入 `versionId` 作为执行归属;`requirementId` 可选
|
||||||
@@ -264,7 +264,7 @@ interface DecomposeOutput {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
要求:AI 不输出数据库 `categoryId`。`taskTypeName` 是必填的展示和字典类型名;`categoryCode` 只是可选映射提示,不限制 AI 拆解。前端确认写入时按 `taskTypeName` 自动确保 `TaskCategory` 存在,缺失则追加到任务类型字典,再写入对应 `categoryId`。AI 不输出 `estimateHours`、预计开始或预计截止。草案没有 `requirement` 引用时,必须有 `requirementName` 和至少一个 `prototype_note` 引用。
|
要求:AI 不输出数据库 `categoryId`。`taskTypeName` 是必填的可复用类型名,不能写成“排行榜测试”这类当前文档专属概述;`categoryCode` 只是可选映射提示,不限制 AI 拆解。前端确认写入时按 `taskTypeName` 解析 `TaskCategory`,可复用开发类型缺失时才追加到任务类型字典;测试用例未知类型回退到已有测试分类。AI 不输出 `estimateHours`、预计开始或预计截止。草案没有 `requirement` 引用时,必须有 `requirementName` 和至少一个 `prototype_note` 引用。
|
||||||
|
|
||||||
### 任务/用例分组契约
|
### 任务/用例分组契约
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ V2 接入后端后改为基于 `ProjectMember` 表的 RBAC(Owner/Admin/Member/
|
|||||||
|
|
||||||
- `version-plan-workflow.ts`:调研/产品方案/UI 设计的子任务、需求覆盖、成果提交和完成条件。
|
- `version-plan-workflow.ts`:调研/产品方案/UI 设计的子任务、需求覆盖、成果提交和完成条件。
|
||||||
- `requirement-selector.ts`:当前版本所属项目下可关联需求的候选筛选,默认只返回 `status === 'adopted'` 的项目需求。
|
- `requirement-selector.ts`:当前版本所属项目下可关联需求的候选筛选,默认只返回 `status === 'adopted'` 的项目需求。
|
||||||
- `task-category.ts`:DevTask/TestCase 共用任务类型字典,`id` 用于存储,AI 输出的 `taskTypeName` 可在采纳时自动追加到字典,`code` 仅作可选语义映射。
|
- `task-category.ts`:DevTask/TestCase 共用任务类型字典,`id` 用于存储,AI 输出的 `taskTypeName` 必须是可复用类型;可复用开发类型可在采纳时自动追加到字典,测试用例未知类型回退到已有测试分类,`code` 仅作可选语义映射。
|
||||||
|
|
||||||
页面组件只消费规则层输出,不直接拼完成条件或候选筛选条件。
|
页面组件只消费规则层输出,不直接拼完成条件或候选筛选条件。
|
||||||
## Work Activity Daily Report Layer (2026-06-26)
|
## Work Activity Daily Report Layer (2026-06-26)
|
||||||
|
|||||||
@@ -509,10 +509,10 @@
|
|||||||
**问题**:任务类型字典最初服务于人工新建任务表单,覆盖的是常见开发和测试类型。AI 从原型里识别出的真实交付切片可能更细,例如“人员名片交互”“预入职数据源”“姓名展示兼容测试”。如果继续要求 AI 只能输出既有 `categoryCode` 枚举,就会把拆解能力绑死在人工表单选项上,字典没覆盖时容易漏拆或被迫归到错误类型。
|
**问题**:任务类型字典最初服务于人工新建任务表单,覆盖的是常见开发和测试类型。AI 从原型里识别出的真实交付切片可能更细,例如“人员名片交互”“预入职数据源”“姓名展示兼容测试”。如果继续要求 AI 只能输出既有 `categoryCode` 枚举,就会把拆解能力绑死在人工表单选项上,字典没覆盖时容易漏拆或被迫归到错误类型。
|
||||||
|
|
||||||
**决策**:
|
**决策**:
|
||||||
- AI DevTask / TestCase 草案必须输出 `taskTypeName`,这是展示给用户的任务类型名称。
|
- AI DevTask / TestCase 草案必须输出 `taskTypeName`,这是展示给用户的可复用任务类型名称,不是任务标题或当前文档的场景概述。
|
||||||
- `categoryCode` 改为可选兼容映射;只有能明确对应已有稳定语义码时才输出,不再作为 schema 必填项,也不再限制为固定枚举。
|
- `categoryCode` 改为可选兼容映射;只有能明确对应已有稳定语义码时才输出,不再作为 schema 必填项,也不再限制为固定枚举。
|
||||||
- 前端采纳 AI 结果时,按 `taskTypeName` 在当前 `TaskCategory` 同分组内查找;不存在则自动追加一条 `isSystem=false` 的任务类型,并用该类型的 `id` 写入 DevTask/TestCase。
|
- 前端采纳 AI 结果时,按 `taskTypeName` 在当前 `TaskCategory` 同分组内查找;可复用开发类型不存在时自动追加一条 `isSystem=false` 的任务类型,并用该类型的 `id` 写入 DevTask。测试用例未知类型不自动入库,优先按 `categoryCode` 或默认测试类型回退。
|
||||||
- 去重逻辑按任务类型名称、标题和引用来源判断;已采纳过的自定义 AI 类型再次生成时也能过滤重复草案。
|
- 去重逻辑按任务类型名称、标题和引用来源判断;已采纳过的自定义 AI 类型再次生成时也能过滤重复草案。
|
||||||
- 人工新建任务表单继续使用任务类型字典作为可选项,但这个字典会随着 AI 草案采纳自动扩展。
|
- 人工新建任务表单继续使用任务类型字典作为可选项,但这个字典会随着 AI 草案采纳自动扩展。
|
||||||
|
|
||||||
**理由**:人工表单的任务类型是录入辅助,不应该成为 AI 拆解边界。`taskTypeName` 让模型按真实工作切片命名,采纳时自动补字典让后续筛选、统计和人工创建都能复用新类型,同时避免 AI 直接写数据库 `categoryId`。
|
**理由**:人工表单的任务类型是录入辅助,不应该成为 AI 拆解边界。但任务类型字典也不能被单个原型的业务对象污染。`taskTypeName` 只承载可复用分类,具体功能点放在 `title` 和 `description`;采纳时有门槛地补字典,让后续筛选、统计和人工创建复用真正稳定的类型,同时避免 AI 直接写数据库 `categoryId`。
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ AI 估时约束:
|
|||||||
|
|
||||||
- `version-plan-workflow.ts` 是调研/产品方案/UI 设计完成条件的唯一入口。
|
- `version-plan-workflow.ts` 是调研/产品方案/UI 设计完成条件的唯一入口。
|
||||||
- `requirement-selector.ts` 是版本内关联需求候选的唯一入口。
|
- `requirement-selector.ts` 是版本内关联需求候选的唯一入口。
|
||||||
- `TaskCategory.name` 是 AI 草案的任务类型展示和自动入库锚点;`TaskCategory.code` 仅作为可选兼容映射,`id` 只作为存储主键。
|
- `TaskCategory.name` 是 AI 草案的可复用任务类型锚点;可复用开发类型可自动入库,测试用例未知类型回退到已有测试分类。`TaskCategory.code` 仅作为可选兼容映射,`id` 只作为存储主键。
|
||||||
- DevTask 新数据必须有 `versionId`;`requirementId` 作为正式需求语义标签可选。版本级聚合走 `versionId`,需求级进度只统计带 `requirementId` 的任务。
|
- DevTask 新数据必须有 `versionId`;`requirementId` 作为正式需求语义标签可选。版本级聚合走 `versionId`,需求级进度只统计带 `requirementId` 的任务。
|
||||||
- 产品方案和 UI 设计的引用需求不再用 checkbox 直接标记完成,必须通过 `requirementCoverage[]` 记录 `not_started / partial / completed`、本次已完成内容和剩余内容;只有 `completed` 计入成果提交门禁。
|
- 产品方案和 UI 设计的引用需求不再用 checkbox 直接标记完成,必须通过 `requirementCoverage[]` 记录 `not_started / partial / completed`、本次已完成内容和剩余内容;只有 `completed` 计入成果提交门禁。
|
||||||
- 产品/UI 计划右侧展示计划日志,需求进度更新和 AI 拆解触发/完成/失败都写入 `VersionPlan.logs[]`,页面只消费日志数据,不临时拼历史。
|
- 产品/UI 计划右侧展示计划日志,需求进度更新和 AI 拆解触发/完成/失败都写入 `VersionPlan.logs[]`,页面只消费日志数据,不临时拼历史。
|
||||||
|
|||||||
Reference in New Issue
Block a user