feat(FN-5152): complete Step 5 — add triage near-duplicate backstop

Fusion-Task-Id: FN-5152
Fusion-Task-Lineage: 954570d8-82e1-4519-a739-c52ee790d2db
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 09:09:02 -07:00
committed by gsxdsm
parent 6431c972e8
commit d9a66571d0

View File

@@ -16,6 +16,9 @@ import {
sortTasksByPriorityThenAgeAndId,
compareTaskIdNumeric,
resolveAgentMemoryInclusionMode,
extractIntentSignature,
findNearDuplicates,
type NearDuplicateCandidate,
} from "@fusion/core";
import type { ImageContent } from "@mariozechner/pi-ai";
import { Type, type Static } from "@mariozechner/pi-ai";
@@ -2253,6 +2256,31 @@ export class TriageProcessor {
taskUpdates.noCommitsExpected = true;
}
const parsedFileScope = parseFileScopeFromPrompt(written);
let taskIntentSignature: ReturnType<typeof extractIntentSignature> = {
routePaths: [],
filePaths: [],
identifiers: [],
titleTokens: [],
};
try {
taskIntentSignature = extractIntentSignature({
title: task.title ?? "",
description: task.description ?? "",
fileScope: parsedFileScope,
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
planLog.warn(`${task.id}: near-duplicate signature extraction failed open: ${message}`);
}
if (parsedFileScope.length > 0 || taskIntentSignature.routePaths.length + taskIntentSignature.filePaths.length + taskIntentSignature.identifiers.length > 0) {
taskUpdates.sourceMetadataPatch = {
...(taskUpdates.sourceMetadataPatch ?? {}),
intentSignature: taskIntentSignature,
...(parsedFileScope.length > 0 ? { fileScope: parsedFileScope } : {}),
};
}
// Apply non-title metadata first. The title is held back and applied AFTER
// the column transition (see below) because store.updateTask regenerates
// PROMPT.md when title/description change, and the triage-stub regen path
@@ -2303,6 +2331,96 @@ export class TriageProcessor {
planLog.warn(`${task.id}: ghost-bug preflight failed open: ${message}`);
}
// FN-5152: post-PROMPT near-duplicate backstop (fail-open, bounded) before triage→todo transition.
try {
const nearDuplicateResult = await Promise.race([
(async () => {
const signalCount = taskIntentSignature.routePaths.length + taskIntentSignature.filePaths.length + taskIntentSignature.identifiers.length;
if (signalCount === 0 && parsedFileScope.length === 0) {
return;
}
const nowMs = Date.now();
const candidates = (await this.store.listTasks({ slim: false, includeArchived: false }))
.filter((candidate) => candidate.id !== task.id)
.filter((candidate) => candidate.column !== "done")
.filter((candidate) => Date.parse(candidate.createdAt) >= nowMs - 7 * 24 * 60 * 60 * 1000)
.map((candidate) => ({
id: candidate.id,
title: candidate.title ?? "",
description: candidate.description ?? "",
column: candidate.column,
createdAt: Date.parse(candidate.createdAt),
fileScope: Array.isArray(candidate.sourceMetadata?.fileScope)
? candidate.sourceMetadata.fileScope.filter((entry): entry is string => typeof entry === "string")
: undefined,
} satisfies NearDuplicateCandidate));
const matches = findNearDuplicates(
{ title: task.title ?? "", description: task.description ?? "", fileScope: parsedFileScope },
candidates,
{ windowMs: 7 * 24 * 60 * 60 * 1000, nowMs },
);
if (matches.length === 0) {
return;
}
const taskCreatedAt = Date.parse(task.createdAt);
const candidatesById = new Map(candidates.map((candidate) => [candidate.id, candidate]));
const olderMatches = matches.filter((match) => {
const candidate = candidatesById.get(match.id);
return candidate ? candidate.createdAt <= taskCreatedAt : false;
});
const canonical = (olderMatches[0] ?? matches[0]);
const canonicalTask = candidatesById.get(canonical.id);
if (!canonicalTask) {
return;
}
if (canonicalTask.createdAt <= taskCreatedAt) {
await this.store.updateTask(task.id, {
sourceMetadataPatch: {
nearDuplicateOf: canonical.id,
intentSignature: taskIntentSignature,
...(parsedFileScope.length > 0 ? { fileScope: parsedFileScope } : {}),
},
});
await this.store.logEntry(
task.id,
`Auto-archived as near-duplicate of ${canonical.id}`,
`Shared tokens: ${canonical.sharedTokens.join(", ")}`,
);
await this.store.moveTask(task.id, "archived");
await this.store.recordActivity({
type: "task:auto-archived-near-duplicate",
taskId: task.id,
taskTitle: task.title ?? "",
details: `Near-duplicate of ${canonical.id}`,
metadata: {
canonicalTaskId: canonical.id,
sharedTokens: canonical.sharedTokens,
score: canonical.score,
},
});
planLog.log(`${task.id} auto-archived as near-duplicate of ${canonical.id}`);
return "archived" as const;
}
planLog.warn(`${task.id}: near-duplicate candidate ${canonical.id} is newer; skipping auto-archive`);
})(),
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 5_000)),
]);
if (nearDuplicateResult === "archived") {
return;
}
if (nearDuplicateResult === "timeout") {
planLog.warn(`${task.id}: near-duplicate backstop timed out; proceeding`);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
planLog.warn(`${task.id}: near-duplicate backstop failed open: ${message}`);
}
if (settings.requirePlanApproval) {
const approvalUpdates: Record<string, unknown> = { status: "awaiting-approval" };
if (shouldApplyPromptDeclaredTitle && promptDeclaredTitle) {
@@ -2338,6 +2456,23 @@ export class TriageProcessor {
}
}
function parseFileScopeFromPrompt(text: string): string[] {
const match = text.match(/^##\s+File Scope\s*\n([\s\S]*?)(?=^##\s+|\Z)/m);
if (!match) return [];
const entries: string[] = [];
for (const rawLine of match[1].split("\n")) {
const trimmed = rawLine.trim();
if (!trimmed.startsWith("-")) continue;
const line = trimmed.replace(/^-+\s*/, "").replace(/`/g, "").trim();
if (!line || /^out of scope/i.test(line)) break;
const pathOnly = line.split(" ")[0]?.trim();
if (!pathOnly) continue;
entries.push(pathOnly);
if (entries.length >= 50) break;
}
return entries;
}
function extractPromptDeclaredTitle(prompt: string, taskId: string): string | null {
const headingMatch = prompt.match(/^#\s+Task:\s+([A-Z]+-\d+)\s+-\s+(.+)$/m);
if (!headingMatch) return null;