Files
ftb-project-management/apps/web/lib/ai-decompose-dedupe.ts
2026-06-26 11:01:11 +08:00

126 lines
3.7 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, draft.categoryCode, draft.references, input);
}
function existingItemFingerprint(
title: string,
categoryId: string,
references: Reference[],
input: DecomposeDedupeInput,
): string {
const categoryCode = input.categories.find((category) => category.id === categoryId)?.code ?? categoryId;
return buildFingerprint(title, categoryCode, references, input);
}
function buildFingerprint(
title: string,
categoryCode: string,
references: Array<AgentReference | Reference>,
input: DecomposeDedupeInput,
): string {
const refKey = references
.map((ref) => normalizeReferenceKey(ref, input.requirements))
.filter(Boolean)
.sort()
.join('|');
return `${categoryCode}::${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()}`;
}