feat(版本): 记录计划需求进展活动

关键改动:

- 记录产品/UI 计划需求覆盖进展,并写入工作活动与实体日志

- 日报和小宝预警补充需求进展活动证据

- 调整版本列表与需求覆盖面板展示,并补充测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 15:35:36 +08:00
parent f82ec1ea6f
commit 04520bd8c5
13 changed files with 173 additions and 82 deletions

View File

@@ -12,6 +12,7 @@ export const WORK_ACTIVITY_ACTION_LABEL: Record<WorkActivityAction, string> = {
version_plan_created: '新建计划',
version_plan_started: '开始计划',
version_plan_completed: '完成计划',
version_plan_requirement_progress: '需求进度',
dev_task_created: '新建开发任务',
dev_task_started: '开始开发',
dev_task_self_testing: '进入自测',

View File

@@ -163,6 +163,8 @@ test('updates requirement coverage, syncs legacy completed ids, and creates a pl
assert.equal(next.logs?.[0]?.type, 'requirement_progress');
assert.equal(next.logs?.[0]?.actor, 'PM');
assert.equal(next.logs?.[0]?.requirementCode, 'QY0001');
assert.equal(next.logs?.[0]?.completedContent, '完成移动端主流程');
assert.equal(next.logs?.[0]?.remainingContent, 'PC 端筛选规则未完成');
});
test('validates requirement coverage record drafts by status', () => {
@@ -178,3 +180,16 @@ test('validates requirement coverage record drafts by status', () => {
assert.equal(canSaveRequirementCoverageDraft!('partial', '完成主流程', '补充异常状态'), true);
assert.equal(canSaveRequirementCoverageDraft!('not_started'), false);
});
test('hides coverage record action after a requirement is completed', () => {
const canOpenRequirementCoverageRecord = (versionPlan as any).canOpenRequirementCoverageRecord as undefined | ((
status: string,
canEdit: boolean,
) => boolean);
assert.equal(typeof canOpenRequirementCoverageRecord, 'function');
assert.equal(canOpenRequirementCoverageRecord!('not_started', true), true);
assert.equal(canOpenRequirementCoverageRecord!('partial', true), true);
assert.equal(canOpenRequirementCoverageRecord!('completed', true), false);
assert.equal(canOpenRequirementCoverageRecord!('partial', false), false);
});

View File

@@ -67,6 +67,8 @@ export interface VersionPlanLog {
requirementCode?: string;
requirementTitle?: string;
coverageStatus?: RequirementCoverageStatus;
completedContent?: string;
remainingContent?: string;
aiTarget?: AgentDecomposeTarget;
aiStatus?: AiDecomposeLogStatus;
}
@@ -194,6 +196,13 @@ export function canSaveRequirementCoverageDraft(
return false;
}
export function canOpenRequirementCoverageRecord(
status: RequirementCoverageStatus,
canEdit: boolean,
): boolean {
return canEdit && status !== 'completed';
}
export function appendPlanLog(plan: VersionPlan, draft: PlanLogDraft): VersionPlanLog[] {
const createdAt = draft.createdAt ?? new Date().toISOString();
const log: VersionPlanLog = {
@@ -260,6 +269,8 @@ export function updateRequirementCoverage(
requirementCode: input.requirementCode,
requirementTitle: input.requirementTitle,
coverageStatus: input.status,
completedContent: nextCoverage.completedContent,
remainingContent: nextCoverage.remainingContent,
}),
};
}

View File

@@ -10,6 +10,7 @@ import {
makeDevTaskStatusActivity,
makeTestCaseStatusActivity,
makeVersionPlanCompletedActivity,
makeVersionPlanRequirementProgressActivity,
} from './work-activity-factory';
function devTask(patch: Partial<DevTask> = {}): DevTask {
@@ -105,6 +106,32 @@ test('makeVersionPlanCompletedActivity marks product plan completion as delivery
assert.equal(activity.summary, '完成产品方案:提交产品方案');
});
test('makeVersionPlanRequirementProgressActivity records partial requirement coverage details', () => {
const activity = makeVersionPlanRequirementProgressActivity(plan({ type: 'ui', title: '绘制个人中心' }), {
id: 'log-1',
type: 'requirement_progress',
createdAt: '2026-06-30T03:00:00.000Z',
actor: '张三',
title: 'QY-001 个人中心更新为部分完成',
detail: '已完成:头像和基本信息\n剩余安全设置入口',
requirementId: 'req-1',
requirementCode: 'QY-001',
requirementTitle: '个人中心',
coverageStatus: 'partial',
completedContent: '头像和基本信息',
remainingContent: '安全设置入口',
});
assert.equal(activity?.action, 'version_plan_requirement_progress');
assert.equal(activity?.category, 'progress');
assert.equal(activity?.sourceType, 'version_plan');
assert.equal(activity?.sourceId, 'plan-1');
assert.equal(activity?.occurredAt, '2026-06-30T03:00:00.000Z');
assert.equal(activity?.summary, '推进UI设计需求QY-001 个人中心(部分完成)');
assert.equal(activity?.metadata?.completedContent, '头像和基本信息');
assert.equal(activity?.metadata?.remainingContent, '安全设置入口');
});
test('makeBugStatusActivity maps fixing and fixed actions', () => {
const fixing = makeBugStatusActivity(bug({ status: 'open' }), 'open', 'fixing', '张三');
const fixed = makeBugStatusActivity(bug(), 'fixing', 'fixed', '张三');

View File

@@ -1,7 +1,7 @@
import type { Bug, BugStatus } from './bug';
import type { DevTask, DevTaskStatus } from './dev-task';
import type { TestCase, TestCaseStatus } from './test-case';
import type { VersionPlan } from './version-plan';
import { REQUIREMENT_COVERAGE_LABEL, type VersionPlan, type VersionPlanLog } from './version-plan';
import type { WorkActivityDraft } from './work-activity';
const PLAN_TYPE_LABEL: Record<VersionPlan['type'], string> = {
@@ -46,6 +46,33 @@ export function makeVersionPlanCompletedActivity(plan: VersionPlan, actorId: str
};
}
export function makeVersionPlanRequirementProgressActivity(plan: VersionPlan, log: VersionPlanLog): WorkActivityDraft | undefined {
if (log.type !== 'requirement_progress') return undefined;
if (plan.type !== 'product' && plan.type !== 'ui') return undefined;
const requirementLabel = [log.requirementCode, log.requirementTitle].filter(Boolean).join(' ') || '需求';
const statusLabel = log.coverageStatus ? REQUIREMENT_COVERAGE_LABEL[log.coverageStatus] : '进度更新';
return {
actorId: log.actor,
sourceType: 'version_plan',
sourceId: plan.id,
action: 'version_plan_requirement_progress',
category: 'progress',
title: plan.title,
summary: `推进${PLAN_TYPE_LABEL[plan.type]}需求:${requirementLabel}${statusLabel}`,
occurredAt: log.createdAt,
metadata: {
requirementId: log.requirementId,
requirementCode: log.requirementCode,
requirementTitle: log.requirementTitle,
coverageStatus: log.coverageStatus,
completedContent: log.completedContent,
remainingContent: log.remainingContent,
},
};
}
export function makeDevTaskCreatedActivity(task: DevTask, actorId: string): WorkActivityDraft {
return {
actorId,

View File

@@ -6,6 +6,7 @@ export type WorkActivityAction =
| 'version_plan_created'
| 'version_plan_started'
| 'version_plan_completed'
| 'version_plan_requirement_progress'
| 'dev_task_created'
| 'dev_task_started'
| 'dev_task_self_testing'

View File

@@ -6,6 +6,7 @@ import {
filterXiaobaoRiskWarnings,
filterXiaobaoWarningVersions,
formatRemainingWork,
getRiskScoreTone,
getXiaobaoWarningRiskCount,
sanitizeRiskInsight,
} from './xiaobao-warning-view';
@@ -135,6 +136,12 @@ test('formatRemainingWork keeps hours and adds work-day conversion', () => {
assert.equal(formatRemainingWork(0.5), '0.5h / 0.1天');
});
test('getRiskScoreTone follows Xiaobao warning score thresholds', () => {
assert.equal(getRiskScoreTone(75), 'danger');
assert.equal(getRiskScoreTone(55), 'warn');
assert.equal(getRiskScoreTone(54), 'ok');
});
test('sanitizeRiskInsight filters invalid page refresh suggested actions', () => {
const result = sanitizeRiskInsight({
summary: '风险上升',

View File

@@ -26,6 +26,8 @@ export interface XiaobaoWarningRiskListFilter {
projectId?: string;
}
export type RiskScoreTone = 'danger' | 'warn' | 'ok';
export function filterXiaobaoWarningVersions(
versions: VersionWithContext[],
filter: XiaobaoWarningVersionFilter,
@@ -56,6 +58,12 @@ export function getXiaobaoWarningRiskCount(risks: XiaobaoVersionRisk[]): number
return filterXiaobaoRiskWarnings(risks).length;
}
export function getRiskScoreTone(score: number): RiskScoreTone {
if (score >= 75) return 'danger';
if (score >= 55) return 'warn';
return 'ok';
}
export function formatRemainingWork(hours: number): string {
const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 0;
const days = safeHours / WORK_HOURS.hoursPerDay;