FN-8901: restore duplicate conflict responses

Require matching named proposal claims before a deterministic duplicate can be reused.

- Restore 409 duplicate responses for ordinary or unrelated proposal intake.
- Preserve duplicate reuse for matching trusted recommendation claims.
- Cover active and terminal canonical task cases.
- Add a patch changeset for the intake fix.

Files changed:
 .changeset/fn-8901-deterministic-duplicate-409.md  |  7 ++++
 .../__tests__/routes-tasks-near-duplicate.test.ts  | 47 ++++++++++++++++++++++
 .../src/routes/register-task-workflow-routes.ts    | 11 ++++-
 3 files changed, 64 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8901

Fusion-Task-Lineage: f9a41ceb-46aa-4589-9fdc-7f1cc35a01e1

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-11 18:21:24 -07:00
parent 167e17c697
commit 12eeeaf046
3 changed files with 64 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Restore duplicate conflict responses for ordinary task intake.
category: fix
dev: createTaskThroughGuardedIntake now requires a named proposalClaimId before trusted duplicate reuse.

View File

@@ -268,6 +268,53 @@ describe("routes /api/tasks near duplicate", () => {
expect(match.reason).toBeUndefined(); expect(match.reason).toBeUndefined();
}); });
it.each([
["no proposal claim", undefined],
["an unrelated proposal claim", "recommendation:FN-1:rec-1"],
])("blocks an ordinary deterministic duplicate with %s", async (_label, proposalClaimId) => {
const title = routeIncoming.title;
const description = routeIncoming.description;
const fingerprint = core.computeContentFingerprint({ title, description }) as string;
const { app, tasks } = buildApp([
mkTask({
id: "FN-8002",
title,
description,
column: "todo",
proposalClaimId,
source: { sourceType: "api", sourceMetadata: { contentFingerprint: fingerprint } },
}),
]);
const res = await performRequest(app, "POST", "/api/tasks", JSON.stringify({ title, description }), { "content-type": "application/json" });
expect(res.status).toBe(409);
const match = (res.body as { details: { matches: Array<{ id: string; deterministic?: boolean; reason?: string }> } }).details.matches[0];
expect(match).toMatchObject({ id: "FN-8002", deterministic: true });
expect(match.reason).toBeUndefined();
expect(tasks).toHaveLength(1);
});
it.each(["done", "archived"] as const)("allows an ordinary deterministic create when the canonical is %s", async (column) => {
const title = routeIncoming.title;
const description = routeIncoming.description;
const fingerprint = core.computeContentFingerprint({ title, description }) as string;
const { app, tasks } = buildApp([
mkTask({
id: "FN-8003",
title,
description,
column,
source: { sourceType: "api", sourceMetadata: { contentFingerprint: fingerprint } },
}),
]);
const res = await performRequest(app, "POST", "/api/tasks", JSON.stringify({ title, description }), { "content-type": "application/json" });
expect(res.status).toBe(201);
expect(tasks).toHaveLength(2);
});
it("fails open when intent extraction throws", async () => { it("fails open when intent extraction throws", async () => {
const extractorSpy = vi.spyOn(core, "extractIntentSignature").mockImplementation(() => { const extractorSpy = vi.spyOn(core, "extractIntentSignature").mockImplementation(() => {
throw new Error("boom"); throw new Error("boom");

View File

@@ -1910,7 +1910,11 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
if ( if (
deterministicGuard.action === "duplicate" deterministicGuard.action === "duplicate"
&& deterministicGuard.existing && deterministicGuard.existing
&& deterministicGuard.existing.proposalClaimId === trusted?.proposalClaimId && typeof trusted?.proposalClaimId === "string"
&& trusted.proposalClaimId.length > 0
&& typeof deterministicGuard.existing.proposalClaimId === "string"
&& deterministicGuard.existing.proposalClaimId.length > 0
&& deterministicGuard.existing.proposalClaimId === trusted.proposalClaimId
) { ) {
/* /*
FNXC:TaskRecommendations 2026-08-08-05:27: FNXC:TaskRecommendations 2026-08-08-05:27:
@@ -1918,6 +1922,11 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
A second process can observe its newly-created child while checking the normal content A second process can observe its newly-created child while checking the normal content
guard; reuse that immutable same-recommendation winner and repair the parent link rather guard; reuse that immutable same-recommendation winner and repair the parent link rather
than surfacing the ordinary duplicate conflict reserved for distinct recommendations. than surfacing the ordinary duplicate conflict reserved for distinct recommendations.
FNXC:TaskRecommendations 2026-08-12-00:58:
Reuse is reserved for a named proposal claim on both the trusted request and canonical.
Comparing absent ids made every ordinary deterministic duplicate return 200 and skipped
the duplicate-blocker classification that preserves legitimate done or archived creates.
*/ */
const trustedCreateResult = await trusted?.onCreated?.(deterministicGuard.existing); const trustedCreateResult = await trusted?.onCreated?.(deterministicGuard.existing);
res.status(200).json(trusted?.responseForCreated?.(deterministicGuard.existing, trustedCreateResult) ?? deterministicGuard.existing); res.status(200).json(trusted?.responseForCreated?.(deterministicGuard.existing, trustedCreateResult) ?? deterministicGuard.existing);