/**
* Canonical refactor impact preview for rename/move dialogs.
* Scholarly Dusk vocabulary: rail-style labels, mono paths, amber caution.
*
* @module src/serve/public/components/RefactorImpactPreview
*/
import type { FileRefactorPreviewPlan } from "../../../core/file-refactor-contract";
const UNRESOLVED_CLASSIFICATIONS = new Set([
"ambiguous",
"unsupported",
"malformed",
"invalid",
]);
function shortDigest(digest: string): string {
return `${digest.slice(0, 8)}…${digest.slice(-6)}`;
}
export interface RefactorImpactPreviewProps {
plan: FileRefactorPreviewPlan | null;
loading?: boolean;
confirmed: boolean;
onConfirmedChange: (confirmed: boolean) => void;
/** Accessible outcome message after apply (sync-pending, recovery, etc.). */
outcomeMessage?: string | null;
outcomeTone?: "warning" | "error" | "success";
}
export function RefactorImpactPreview({
plan,
loading = false,
confirmed,
onConfirmedChange,
outcomeMessage,
outcomeTone = "warning",
}: RefactorImpactPreviewProps) {
if (loading && !plan) {
return (
Computing reference impact…
);
}
if (!plan) {
return null;
}
const rewriteable = plan.examinedReferences.filter(
(ref) => ref.classification === "rewriteable"
);
const unresolved = plan.examinedReferences.filter((ref) =>
UNRESOLVED_CLASSIFICATIONS.has(ref.classification)
);
const affectedPaths = plan.affectedDocuments.map((doc) => doc.relPath);
const confirmId = `refactor-confirm-${plan.planDigest.slice(0, 12)}`;
const outcomeClass =
outcomeTone === "error"
? "border-destructive/40 bg-destructive/10 text-destructive"
: outcomeTone === "success"
? "border-primary/30 bg-primary/10 text-primary"
: "border-amber-500/30 bg-amber-500/10 text-amber-500";
return (
Safety
{plan.canApply
? "Plan is safe to apply with the exact digest below."
: `Cannot apply: ${(plan.safety.blockingReasonCodes[0] ?? "blocked").replaceAll("_", " ")}.`}
Digest {shortDigest(plan.planDigest)}
Target
{plan.source.relPath} → {plan.target.relPath}
{plan.safety.rewriteableCount} rewrite
{plan.safety.rewriteableCount === 1 ? "" : "s"} ·{" "}
{plan.safety.unchangedCount} unchanged · {unresolved.length}{" "}
unresolved
{affectedPaths.length > 0 && (
Affected documents
{affectedPaths.map((path) => (
{path}
))}
)}
{rewriteable.length > 0 && (
Rewrites
{rewriteable.slice(0, 12).map((ref) => (
{ref.documentRelPath}
·
{ref.originalDestination ?? "?"} →{" "}
{ref.proposedDestination ?? "?"}
))}
{rewriteable.length > 12 && (
+{rewriteable.length - 12} more
)}
)}
{unresolved.length > 0 && (
Unresolved / unsupported
{unresolved.slice(0, 12).map((ref) => (
{ref.documentRelPath}
{ref.startLine ? `:${ref.startLine}` : ""}
· {ref.classification}
{ref.reasonCode && (
· {ref.reasonCode}
)}
))}
{unresolved.length > 12 && (
+{unresolved.length - 12} more
)}
)}
{plan.safety.warnings.length > 0 && (
{plan.safety.warnings.map((warning) => (
{warning}
))}
)}
onConfirmedChange(event.target.checked)}
type="checkbox"
/>
Confirm exact plan digest{" "}
{shortDigest(plan.planDigest)}
{outcomeMessage && (
{outcomeMessage}
)}
);
}