const YAML = require("yaml"); import extractGtdBlock from "./gtdBlock"; import resolveScopedFolderIds from "./folderScope"; import { moveMatrixItem } from "./matrixMutation"; import { isMatrixDestination } from "./moveProtocol"; import parseMatrixConfig from "./parseMatrixConfig"; import resolveNotebook from "./resolveNotebook"; import { DataAdapter, MoveCardResult, MoveMatrixCardIntent, MutationAdapter, RawNote, RawTag, } from "./types"; export interface MatrixMoveHost { id: string; parentId: string; } class StaleMatrixMoveError extends Error {} function result( status: "stale" | "error", viewInstanceId: string, message: string ): MoveCardResult { return { status, viewInstanceId, message }; } function validId(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; } function validateFreshEligibility( note: RawNote, tags: RawTag[], host: MatrixMoveHost, folderIds: Set, config: ReturnType ): void { if (note.id === host.id) throw new StaleMatrixMoveError("The dashboard note cannot be moved."); if (!folderIds.has(note.parent_id)) throw new StaleMatrixMoveError("The card is no longer in scope."); if (!note.is_todo && config.notes === "none") { throw new StaleMatrixMoveError("The card is no longer admitted by notes."); } const block = extractGtdBlock(note.body); if (note.is_todo) { if (config.todos === "none") throw new StaleMatrixMoveError("The card is no longer admitted by todos."); if (config.todos === "gtd-only" && !block.found) { throw new StaleMatrixMoveError("The to-do no longer contains a gtd block."); } if (note.todo_completed > 0) { throw new StaleMatrixMoveError("The to-do is now complete and no longer belongs in the matrix."); } } else { if (!block.found) throw new StaleMatrixMoveError("The note no longer contains a gtd block."); const titles = tags.map((tag) => (tag.title || "").trim().toLowerCase()); if (titles.includes(config.doneTag)) { throw new StaleMatrixMoveError("The note is now complete and no longer belongs in the matrix."); } } } export async function validateAndMoveMatrixCard( intent: unknown, host: MatrixMoveHost | null, readAdapter: DataAdapter, mutationAdapter: MutationAdapter, logDiagnostic: (error: unknown) => void = () => undefined ): Promise { const candidate = intent as Partial | null; const viewInstanceId = validId(candidate?.viewInstanceId) ? candidate.viewInstanceId : "unknown"; const allowedKeys = new Set([ "type", "view", "hostNoteId", "cardId", "destination", "rawConfig", "viewInstanceId", ]); if ( !candidate || Array.isArray(candidate) || Object.keys(candidate).some((key) => !allowedKeys.has(key)) || candidate.type !== "moveMatrixCard" || candidate.view !== "matrix" || !validId(candidate.hostNoteId) || !validId(candidate.cardId) || !validId(candidate.viewInstanceId) || typeof candidate.rawConfig !== "string" || !isMatrixDestination(candidate.destination) ) { return result("error", viewInstanceId, "Invalid matrix move request."); } if (!host || candidate.hostNoteId !== host.id) { return result("stale", viewInstanceId, "The rendered dashboard is no longer current."); } let parsed: unknown; try { parsed = YAML.parse(candidate.rawConfig || "") || {}; } catch (_parseError) { return result("stale", viewInstanceId, "The matrix configuration is no longer valid."); } const config = parseMatrixConfig(parsed); if (!config.editable || config.mode !== "eisenhower") { return result("error", viewInstanceId, "This matrix is read-only."); } try { const folders = await readAdapter.getFolders(); let scanFolderId = host.parentId; if (!config.scopeAll && config.notebook) { scanFolderId = resolveNotebook(folders, config.notebook, host.parentId).folderId; } const folderIds = new Set( resolveScopedFolderIds(folders, scanFolderId, config.scopeDepth, config.scopeAll) ); const outcome = await moveMatrixItem( mutationAdapter, candidate.cardId, candidate.destination, config, (note, tags) => validateFreshEligibility(note, tags, host, folderIds, config) ); return { status: "success", viewInstanceId, changed: outcome.changed }; } catch (moveError) { if (moveError instanceof StaleMatrixMoveError || (moveError instanceof Error && moveError.message === "The card no longer exists.")) { return result("stale", viewInstanceId, (moveError as Error).message); } logDiagnostic(moveError); return result("error", viewInstanceId, "Could not apply the move. Refresh and try again."); } }