feat(FN-5220): guard explicit duplicate markers in triage and self-healing

Adds an explicit duplicate-marker guard (FN-5220) spanning core helper, dashboard API endpoint, triage short-circuit, and self-healing sweep to detect and handle duplicate task creation attempts; includes comprehensive test coverage across unit, API, and integration layers plus documentation.

Fusion-Task-Id: FN-5220
This commit is contained in:
Fusion (runfusion.ai)
2026-05-20 13:46:34 -07:00
committed by gsxdsm
parent 9eea9f48f0
commit 8f2d5e7e61
12 changed files with 869 additions and 12 deletions

View File

@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import { parseExplicitDuplicateMarker } from "../explicit-duplicate-marker.js";
const FULL_PROMPT = `# Task: FN-5211 - Example
## Mission
This is a duplicate-handling task, but it is a full spec body.
- Implement the fix
- Verify the result
`;
describe("parseExplicitDuplicateMarker", () => {
it("parses a canonical marker", () => {
expect(parseExplicitDuplicateMarker("DUPLICATE: FN-5211")).toEqual({
canonicalId: "FN-5211",
});
});
it("normalizes case and surrounding whitespace", () => {
expect(parseExplicitDuplicateMarker(" duplicate: fn-5211\n")).toEqual({
canonicalId: "FN-5211",
});
});
it("parses a backtick-wrapped marker", () => {
expect(parseExplicitDuplicateMarker("`DUPLICATE: FN-5211`")).toEqual({
canonicalId: "FN-5211",
});
});
it("parses a bold-wrapped marker", () => {
expect(parseExplicitDuplicateMarker("**DUPLICATE: FN-5211**")).toEqual({
canonicalId: "FN-5211",
});
});
it("parses a marker padded by blank lines", () => {
expect(parseExplicitDuplicateMarker("\n\n\nDUPLICATE: FN-5211\n\n")).toEqual({
canonicalId: "FN-5211",
});
});
it("parses a fenced marker", () => {
expect(parseExplicitDuplicateMarker("```text\nDUPLICATE: FN-5211\n```")).toEqual({
canonicalId: "FN-5211",
});
});
it("rejects a full prompt body that merely mentions duplicate", () => {
expect(parseExplicitDuplicateMarker(FULL_PROMPT)).toBeNull();
});
it("rejects extra prose after the marker", () => {
expect(parseExplicitDuplicateMarker("DUPLICATE: FN-5211\n\nSee also FN-5212")).toBeNull();
});
it("rejects multiple markers", () => {
expect(parseExplicitDuplicateMarker("DUPLICATE: FN-5211\nDUPLICATE: FN-5212")).toBeNull();
});
it("rejects empty input", () => {
expect(parseExplicitDuplicateMarker(" ")).toBeNull();
});
it("rejects non-FN identifiers", () => {
expect(parseExplicitDuplicateMarker("DUPLICATE: NOT-1234")).toBeNull();
});
});

View File

@@ -0,0 +1,52 @@
export interface ExplicitDuplicateMarker {
canonicalId: string;
}
function stripCodeFenceLayer(content: string): string {
const fenceMatch = content.match(/^```(?:[\t ]*(?:text|markdown))?[\t ]*\n([\s\S]*?)\n```$/i);
if (!fenceMatch) {
return content;
}
return fenceMatch[1] ?? "";
}
function stripSingleWrapper(line: string): string {
if (line.startsWith("`") && line.endsWith("`") && line.length >= 2) {
return line.slice(1, -1).trim();
}
if (line.startsWith("**") && line.endsWith("**") && line.length >= 4) {
return line.slice(2, -2).trim();
}
return line;
}
/**
* Detects the canonical triage "redirect" marker emitted by the planning
* agent when the new task duplicates an existing one.
*/
export function parseExplicitDuplicateMarker(content: string): ExplicitDuplicateMarker | null {
const trimmed = content.trim();
if (!trimmed) {
return null;
}
const withoutFence = stripCodeFenceLayer(trimmed).trim();
const nonBlankLines = withoutFence
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
if (nonBlankLines.length !== 1) {
return null;
}
const candidate = stripSingleWrapper(nonBlankLines[0] ?? "");
const match = candidate.match(/^DUPLICATE:\s*(FN-\d+)\s*$/i);
if (!match) {
return null;
}
return {
canonicalId: match[1].toUpperCase(),
};
}

View File

@@ -156,6 +156,10 @@ export {
type NearDuplicateMatch,
} from "./near-duplicate.js";
export { getTaskDuplicateLineage } from "./duplicate-lineage.js";
export {
parseExplicitDuplicateMarker,
type ExplicitDuplicateMarker,
} from "./explicit-duplicate-marker.js";
export {
__getDeterministicGuardMutexSize,
deterministicGuardLocks,