Persist git-revert provenance and display it on completed task cards. - Stamp source tasks after clean or already-reverted git outcomes. - Render localized Reverted badges in completed and archived task views. - Cover metadata, route, and card behavior with regression tests. Files changed: .changeset/fn-8066-reverted-badge.md | 7 +++ docs/dashboard-guide.md | 2 + packages/dashboard/app/components/TaskCard.css | 14 ++++- packages/dashboard/app/components/TaskCard.tsx | 20 ++++++- .../app/components/__tests__/TaskCard.test.tsx | 67 ++++++++++++++++++++++ .../app/utils/__tests__/taskRevert.test.ts | 17 ++++++ packages/dashboard/app/utils/taskRevert.ts | 11 ++++ .../src/__tests__/task-revert-route.test.ts | 35 +++++++++++ .../src/routes/register-task-workflow-routes.ts | 30 ++++++++++ packages/i18n/locales/en/app.json | 2 + packages/i18n/locales/es/app.json | 2 + packages/i18n/locales/fr/app.json | 2 + packages/i18n/locales/ko/app.json | 2 + packages/i18n/locales/zh-CN/app.json | 2 + packages/i18n/locales/zh-TW/app.json | 2 + packages/i18n/src/resources.d.ts | 2 + 16 files changed, 213 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-8066 Fusion-Task-Lineage: 2a071c14-7e5d-4161-ab0c-158f47ad9c22 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
92 lines
3.8 KiB
TypeScript
92 lines
3.8 KiB
TypeScript
import type { Task } from "@fusion/core";
|
|
|
|
/**
|
|
* FNXC:TaskRevert 2026-07-04-00:00:
|
|
* FN-7524 stamps an AI-undo task with `sourceMetadata.revertOf = <sourceTaskId>`
|
|
* (see `REVERT_OF_METADATA_KEY` in `packages/engine/src/task-revert.ts`) — this is
|
|
* the sole authoritative undo→source pointer written by `createAiUndoTask`. The
|
|
* backend deliberately does NOT set `sourceParentTaskId` for undo tasks (that field
|
|
* is owned by refine/duplicate lineage and child-task counting), so this helper only
|
|
* falls back to `sourceParentTaskId` defensively for forward-compatibility with a
|
|
* future backend shape; today it always resolves via `revertOf`.
|
|
*
|
|
* The `sourceParentTaskId` fallback is gated to `sourceType === "recovery"` (the
|
|
* sourceType `createAiUndoTask` stamps). `task_refine`/`task_duplicate` tasks also
|
|
* set `sourceParentTaskId`, but for an UNRELATED lineage relationship that already
|
|
* renders its own "Created via Refinement/Duplicate of <id>" provenance clause
|
|
* (`getProvenanceLabel` in TaskDetailModal.tsx) — without this gate, an undo-of
|
|
* clause would double-render alongside it for the same id.
|
|
*
|
|
* FN-7555 (this task) surfaces this marker bi-directionally in the dashboard only —
|
|
* no new API, no backend changes. Both `TaskCard` and `TaskDetailModal` import this
|
|
* helper (and `findOpenUndoTaskForSource` below) so the forward/reverse affordances
|
|
* never disagree about what counts as "an undo relationship".
|
|
*/
|
|
/**
|
|
* FNXC:TaskRevert 2026-07-16-00:00:
|
|
* FN-8066 considers a source task reverted only when the route persisted a
|
|
* non-blank `revertedAt` marker after a clean or already-reverted git outcome.
|
|
* Keep this defensive predicate shared so every card/detail consumer applies the
|
|
* same provenance contract to untyped historical source metadata.
|
|
*/
|
|
export function isTaskReverted(sourceMetadata: Task["sourceMetadata"] | undefined): boolean {
|
|
return typeof sourceMetadata?.revertedAt === "string" && sourceMetadata.revertedAt.trim().length > 0;
|
|
}
|
|
|
|
export function getRevertOfId(
|
|
sourceMetadata: Task["sourceMetadata"] | undefined,
|
|
sourceParentTaskId?: string | null,
|
|
sourceType?: string,
|
|
): string | undefined {
|
|
const revertOf = sourceMetadata?.revertOf;
|
|
if (typeof revertOf === "string" && revertOf.trim().length > 0) {
|
|
return revertOf.trim();
|
|
}
|
|
|
|
if (
|
|
sourceType === "recovery"
|
|
&& typeof sourceParentTaskId === "string"
|
|
&& sourceParentTaskId.trim().length > 0
|
|
) {
|
|
return sourceParentTaskId.trim();
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* FNXC:TaskRevert 2026-07-04-00:00:
|
|
* Reverse lookup: given the full loaded `tasks` list and a source task id, find the
|
|
* most recently created OPEN undo task that points back at it via `revertOf`. This
|
|
* mirrors `TaskStore.findOpenRevertTaskForSource` (packages/core/src/store.ts)
|
|
* client-side: `done`/`archived`/soft-deleted undo tasks are intentionally excluded
|
|
* so a completed or discarded undo attempt never renders as an active "Undo task"
|
|
* link (no stale/leftover affordance). When multiple open undo tasks exist (should
|
|
* not normally happen given the route's own dedup guard, but the UI must stay
|
|
* defensive), the most recently created one wins.
|
|
*/
|
|
export function findOpenUndoTaskForSource(tasks: readonly Task[], sourceTaskId: string): Task | undefined {
|
|
const trimmedSourceId = sourceTaskId.trim();
|
|
if (trimmedSourceId.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
let best: Task | undefined;
|
|
for (const candidate of tasks) {
|
|
if (candidate.deletedAt) {
|
|
continue;
|
|
}
|
|
if (candidate.column === "done" || candidate.column === "archived") {
|
|
continue;
|
|
}
|
|
if (getRevertOfId(candidate.sourceMetadata) !== trimmedSourceId) {
|
|
continue;
|
|
}
|
|
if (!best || new Date(candidate.createdAt).getTime() > new Date(best.createdAt).getTime()) {
|
|
best = candidate;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|