Files
ftb-project-management/apps/web/lib/ai-decompose-dedupe.ts
2026-07-02 18:01:38 +08:00

137 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
AgentDecomposeResult,
AgentDevTaskDraft,
AgentReference,
AgentTestCaseDraft,
} from '@ftb/shared';
import type { DevTask, Reference } from './dev-task';
import type { TestCase } from './test-case';
import type { TaskCategory } from './task-category';
interface RequirementIdentity {
id: string;
code: string;
}
export interface DecomposeDedupeInput {
existingDevTasks: DevTask[];
existingTestCases: TestCase[];
categories: TaskCategory[];
requirements: RequirementIdentity[];
}
export interface DecomposeDedupeResult {
result: AgentDecomposeResult;
removedDevTaskCount: number;
removedTestCaseCount: number;
}
export function filterDuplicateDecomposeDrafts(
result: AgentDecomposeResult,
input: DecomposeDedupeInput,
): DecomposeDedupeResult {
const devFingerprints = new Set(
input.existingDevTasks
.map((task) => existingItemFingerprint(task.title, task.categoryId, task.references ?? [], input))
.filter(Boolean),
);
const testCaseFingerprints = new Set(
input.existingTestCases
.map((testCase) => existingItemFingerprint(testCase.title, testCase.categoryId, testCase.references ?? [], input))
.filter(Boolean),
);
const filteredDevTaskDrafts = filterDrafts(result.devTaskDrafts, devFingerprints, input, draftFingerprint);
const filteredTestCaseDrafts = filterDrafts(result.testCaseDrafts, testCaseFingerprints, input, draftFingerprint);
return {
result: {
...result,
devTaskDrafts: filteredDevTaskDrafts.items,
testCaseDrafts: filteredTestCaseDrafts.items,
},
removedDevTaskCount: result.devTaskDrafts.length - filteredDevTaskDrafts.items.length,
removedTestCaseCount: result.testCaseDrafts.length - filteredTestCaseDrafts.items.length,
};
}
function filterDrafts<T extends AgentDevTaskDraft | AgentTestCaseDraft>(
drafts: T[],
existingFingerprints: Set<string>,
input: DecomposeDedupeInput,
getFingerprint: (draft: T, input: DecomposeDedupeInput) => string,
): { items: T[] } {
const seen = new Set<string>();
const items: T[] = [];
for (const draft of drafts) {
const fingerprint = getFingerprint(draft, input);
if (existingFingerprints.has(fingerprint) || seen.has(fingerprint)) continue;
seen.add(fingerprint);
items.push(draft);
}
return { items };
}
function draftFingerprint(
draft: AgentDevTaskDraft | AgentTestCaseDraft,
input: DecomposeDedupeInput,
): string {
return buildFingerprint(draft.title, draftCategoryIdentity(draft, input), draft.references, input);
}
function existingItemFingerprint(
title: string,
categoryId: string,
references: Reference[],
input: DecomposeDedupeInput,
): string {
const categoryName = input.categories.find((category) => category.id === categoryId)?.name ?? categoryId;
return buildFingerprint(title, categoryName, references, input);
}
function draftCategoryIdentity(
draft: AgentDevTaskDraft | AgentTestCaseDraft,
input: DecomposeDedupeInput,
): string {
const explicitName = draft.taskTypeName?.trim();
if (explicitName) return explicitName;
if (draft.categoryCode) {
return input.categories.find((category) => category.code === draft.categoryCode)?.name ?? draft.categoryCode;
}
return '';
}
function buildFingerprint(
title: string,
categoryName: string,
references: Array<AgentReference | Reference>,
input: DecomposeDedupeInput,
): string {
const refKey = references
.map((ref) => normalizeReferenceKey(ref, input.requirements))
.filter(Boolean)
.sort()
.join('|');
return `${normalizeTitle(categoryName)}::${normalizeTitle(title)}::${refKey}`;
}
function normalizeTitle(title: string): string {
return title
.trim()
.toLowerCase()
.replace(/[\s:,.。;;、\-—_]/g, '');
}
function normalizeReferenceKey(
ref: AgentReference | Reference,
requirements: RequirementIdentity[],
): string {
const rawId = String(ref.id || '').trim();
if (!rawId) return '';
if (ref.type === 'requirement') {
const matched = requirements.find((requirement) => requirement.id === rawId || requirement.code === rawId);
return `requirement:${matched?.id ?? rawId}`;
}
return `${ref.type}:${rawId.toLowerCase()}`;
}