Files
adilalloandCursor bcbc6f30d9 Import the remade decks on stable ids and recommend one card per job, so a saved draft still loads after a chip is renamed.
The real-group profiles stay as a test, so a change that moves a pick fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 14:22:54 -07:00

176 lines
5.7 KiB
TypeScript

import type { CreateFlowState } from "../../app/(app)/create/types";
import idAliasesFile from "../../data/create/customRule/_idAliases.json";
import type { DeckId } from "./deckTypes";
import { DECK_IDS } from "./deckTypes";
type MovedTo = { deck: string; id: string };
type MethodAlias =
| string
| {
target: string | null;
movedTo?: MovedTo[];
};
type IdAliasesFile = {
methods: Record<string, Record<string, MethodAlias>>;
coreValues: {
oldPositionToNewId: Record<string, string | null>;
labelMerges: Record<string, string | null>;
};
};
const aliases = idAliasesFile as IdAliasesFile;
function isDeckId(value: string): value is DeckId {
return (DECK_IDS as readonly string[]).includes(value);
}
export type ResolvedMethodId = { deck: DeckId; id: string };
/**
* Old card id → new card id. A cut card follows `movedTo` when that card
* lives in a deck. `null` means the card was cut and has nowhere to go.
*/
export function resolveMethodId(
deck: DeckId,
id: string,
): ResolvedMethodId | null {
const raw = aliases.methods[deck]?.[id];
if (raw === undefined) return { deck, id };
if (typeof raw === "string") return { deck, id: raw };
if (typeof raw.target === "string" && raw.target.length > 0) {
return { deck, id: raw.target };
}
const moved = raw.movedTo?.find((row) => isDeckId(row.deck));
if (moved && isDeckId(moved.deck)) return { deck: moved.deck, id: moved.id };
return null;
}
/** Old 1-based core-value position → stable id. `null` means the value was cut. */
export function resolveCoreValueId(id: string): string | null {
if (Object.prototype.hasOwnProperty.call(aliases.coreValues.oldPositionToNewId, id)) {
return aliases.coreValues.oldPositionToNewId[id] ?? null;
}
return id;
}
export function resolveCoreValueLabel(label: string): string | null {
if (Object.prototype.hasOwnProperty.call(aliases.coreValues.labelMerges, label)) {
return aliases.coreValues.labelMerges[label];
}
return label;
}
function remapIdList(
ids: string[] | undefined,
deck: DeckId,
into: Map<DeckId, string[]>,
): void {
if (!ids) return;
for (const id of ids) {
const next = resolveMethodId(deck, id);
if (!next) continue;
const list = into.get(next.deck) ?? [];
if (!list.includes(next.id)) list.push(next.id);
into.set(next.deck, list);
}
}
const METHOD_ID_KEYS = {
decisionApproaches: "selectedDecisionApproachIds",
membership: "selectedMembershipMethodIds",
conflictManagement: "selectedConflictManagementIds",
communication: "selectedCommunicationMethodIds",
} as const satisfies Record<DeckId, keyof CreateFlowState>;
const DETAIL_KEYS = {
decisionApproaches: "decisionApproachDetailsById",
membership: "membershipMethodDetailsById",
conflictManagement: "conflictManagementDetailsById",
communication: "communicationMethodDetailsById",
} as const satisfies Record<DeckId, keyof CreateFlowState>;
/**
* Remap saved method and core-value ids onto the remade decks.
* Safe to run more than once: stable ids pass through.
*/
export function migrateSavedDeckIds(
state: CreateFlowState,
): CreateFlowState {
const byDeck = new Map<DeckId, string[]>();
for (const deck of DECK_IDS) {
const key = METHOD_ID_KEYS[deck];
const ids = state[key];
remapIdList(Array.isArray(ids) ? (ids as string[]) : undefined, deck, byDeck);
}
const detailsByDeck = new Map<DeckId, Record<string, unknown>>();
for (const deck of DECK_IDS) {
const key = DETAIL_KEYS[deck];
const details = state[key];
if (!details || typeof details !== "object") continue;
for (const [id, value] of Object.entries(details as Record<string, unknown>)) {
const resolved = resolveMethodId(deck, id);
if (!resolved) continue;
const next = detailsByDeck.get(resolved.deck) ?? {};
if (next[resolved.id] === undefined) next[resolved.id] = value;
detailsByDeck.set(resolved.deck, next);
}
}
const detailsOut: Partial<CreateFlowState> = {};
for (const deck of DECK_IDS) {
if (!detailsByDeck.has(deck) && state[DETAIL_KEYS[deck]] === undefined) continue;
(detailsOut as Record<string, unknown>)[DETAIL_KEYS[deck]] =
detailsByDeck.get(deck) ?? {};
}
const coreIds: string[] = [];
const seenValues = new Set<string>();
for (const id of state.selectedCoreValueIds ?? []) {
const next = resolveCoreValueId(id);
if (!next || seenValues.has(next)) continue;
seenValues.add(next);
coreIds.push(next);
}
const coreDetails = state.coreValueDetailsByChipId;
let nextCoreDetails = coreDetails;
if (coreDetails) {
nextCoreDetails = {};
for (const [id, value] of Object.entries(coreDetails)) {
const next = resolveCoreValueId(id);
if (!next || nextCoreDetails[next]) continue;
nextCoreDetails[next] = value;
}
}
const seenSnapshot = new Set<string>();
const snapshot = state.coreValuesChipsSnapshot?.flatMap((row) => {
const nextId = resolveCoreValueId(row.id);
if (!nextId || seenSnapshot.has(nextId)) return [];
seenSnapshot.add(nextId);
const mergedLabel = resolveCoreValueLabel(row.label);
if (mergedLabel === null) return [];
return [{ ...row, id: nextId, label: mergedLabel }];
});
const methodPatch: Partial<CreateFlowState> = {};
for (const deck of DECK_IDS) {
const key = METHOD_ID_KEYS[deck];
if (state[key] !== undefined || byDeck.has(deck)) {
(methodPatch as Record<string, unknown>)[key] = byDeck.get(deck) ?? [];
}
}
return {
...state,
...methodPatch,
...detailsOut,
...(state.selectedCoreValueIds
? { selectedCoreValueIds: coreIds }
: {}),
...(snapshot ? { coreValuesChipsSnapshot: snapshot } : {}),
...(nextCoreDetails ? { coreValueDetailsByChipId: nextCoreDetails } : {}),
};
}