feat(版本详情): 完善流程日志与需求覆盖

This commit is contained in:
Script Generator
2026-06-30 11:00:07 +08:00
parent d754585fe0
commit 1cd595c42e
38 changed files with 4123 additions and 237 deletions

View File

@@ -3,6 +3,9 @@ import type { AgentDecomposeTarget } from '@ftb/shared';
export type PlanTaskStatus = 'pending' | 'in_progress' | 'completed';
export type ProductPlanKind = 'design' | 'review';
export type ProductPlanReviewResult = 'passed' | 'failed';
export type RequirementCoverageStatus = 'not_started' | 'partial' | 'completed';
export type VersionPlanLogType = 'requirement_progress' | 'ai_decompose' | 'system';
export type AiDecomposeLogStatus = 'started' | 'completed' | 'error';
export type ProductPlanReviewFailureType =
| 'requirement_mismatch'
| 'information_architecture'
@@ -44,6 +47,52 @@ export interface PlanTask {
status: PlanTaskStatus;
}
export interface VersionPlanRequirementCoverage {
requirementId: string;
status: RequirementCoverageStatus;
completedContent?: string;
remainingContent?: string;
updatedAt: string;
updatedBy: string;
}
export interface VersionPlanLog {
id: string;
type: VersionPlanLogType;
createdAt: string;
actor: string;
title: string;
detail?: string;
requirementId?: string;
requirementCode?: string;
requirementTitle?: string;
coverageStatus?: RequirementCoverageStatus;
aiTarget?: AgentDecomposeTarget;
aiStatus?: AiDecomposeLogStatus;
}
export interface RequirementCoverageUpdateInput {
requirementId: string;
status: RequirementCoverageStatus;
completedContent?: string;
remainingContent?: string;
updatedAt?: string;
updatedBy: string;
requirementCode?: string;
requirementTitle?: string;
}
export type PlanLogDraft = Omit<VersionPlanLog, 'id' | 'createdAt'> & {
id?: string;
createdAt?: string;
};
export const REQUIREMENT_COVERAGE_LABEL: Record<RequirementCoverageStatus, string> = {
not_started: '未开始',
partial: '部分完成',
completed: '完全完成',
};
export interface VersionPlan {
id: string;
versionId: string;
@@ -56,6 +105,8 @@ export interface VersionPlan {
tasks?: PlanTask[];
completedRequirementIds?: string[];
linkedRequirementIds?: string[];
requirementCoverage?: VersionPlanRequirementCoverage[];
logs?: VersionPlanLog[];
productPlanKind?: ProductPlanKind;
resultType?: 'link' | 'file';
resultTitle?: string;
@@ -81,6 +132,106 @@ export interface VersionPlan {
export type PlanType = VersionPlan['type'];
function makePlanLogId(createdAt: string): string {
const time = new Date(createdAt).getTime();
const suffix = Math.random().toString(36).slice(2, 8);
return `plan-log-${Number.isFinite(time) ? time : Date.now()}-${suffix}`;
}
export function getRequirementCoverage(plan: VersionPlan, requirementId: string): VersionPlanRequirementCoverage | undefined {
const explicit = plan.requirementCoverage?.find((item) => item.requirementId === requirementId);
if (explicit) return explicit;
if ((plan.completedRequirementIds ?? []).includes(requirementId)) {
return {
requirementId,
status: 'completed',
updatedAt: plan.completedAt ?? plan.createdAt,
updatedBy: plan.owner,
};
}
return undefined;
}
export function getRequirementCoverageStatus(plan: VersionPlan, requirementId: string): RequirementCoverageStatus {
return getRequirementCoverage(plan, requirementId)?.status ?? 'not_started';
}
export function getRequirementCoverageSummary(plan: VersionPlan): {
total: number;
completed: number;
partial: number;
notStarted: number;
percent: number;
} {
const linkedIds = plan.linkedRequirementIds ?? [];
const total = linkedIds.length;
const completed = linkedIds.filter((id) => getRequirementCoverageStatus(plan, id) === 'completed').length;
const partial = linkedIds.filter((id) => getRequirementCoverageStatus(plan, id) === 'partial').length;
const notStarted = Math.max(total - completed - partial, 0);
return {
total,
completed,
partial,
notStarted,
percent: total === 0 ? 0 : Math.round((completed / total) * 100),
};
}
export function appendPlanLog(plan: VersionPlan, draft: PlanLogDraft): VersionPlanLog[] {
const createdAt = draft.createdAt ?? new Date().toISOString();
const log: VersionPlanLog = {
...draft,
id: draft.id ?? makePlanLogId(createdAt),
createdAt,
};
return [log, ...(plan.logs ?? [])];
}
export function updateRequirementCoverage(
plan: VersionPlan,
input: RequirementCoverageUpdateInput,
): Pick<VersionPlan, 'requirementCoverage' | 'completedRequirementIds' | 'logs'> {
const updatedAt = input.updatedAt ?? new Date().toISOString();
const nextCoverage: VersionPlanRequirementCoverage = {
requirementId: input.requirementId,
status: input.status,
completedContent: input.completedContent?.trim() || undefined,
remainingContent: input.remainingContent?.trim() || undefined,
updatedAt,
updatedBy: input.updatedBy,
};
const requirementCoverage = [
nextCoverage,
...(plan.requirementCoverage ?? []).filter((item) => item.requirementId !== input.requirementId),
];
const completedSet = new Set(plan.completedRequirementIds ?? []);
if (input.status === 'completed') completedSet.add(input.requirementId);
else completedSet.delete(input.requirementId);
const detail = [
nextCoverage.completedContent ? `已完成:${nextCoverage.completedContent}` : '',
nextCoverage.remainingContent ? `剩余:${nextCoverage.remainingContent}` : '',
].filter(Boolean).join('\n');
const reqLabel = [input.requirementCode, input.requirementTitle].filter(Boolean).join(' ');
return {
requirementCoverage,
completedRequirementIds: Array.from(completedSet),
logs: appendPlanLog(plan, {
type: 'requirement_progress',
createdAt: updatedAt,
actor: input.updatedBy,
title: `${reqLabel || '需求'}更新为${REQUIREMENT_COVERAGE_LABEL[input.status]}`,
detail: detail || undefined,
requirementId: input.requirementId,
requirementCode: input.requirementCode,
requirementTitle: input.requirementTitle,
coverageStatus: input.status,
}),
};
}
function getPlanCreatedAtTime(plan: VersionPlan): number {
const time = new Date(plan.createdAt).getTime();
return Number.isFinite(time) ? time : 0;