137 lines
4.1 KiB
TypeScript
137 lines
4.1 KiB
TypeScript
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()}`;
|
||
}
|