source-delete-decision.ts 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. /**
  2. * Decide what to do with a wiki page when the user deletes a source
  3. * document. This is a pure function so the branching is unit-testable
  4. * in isolation — the previous inline version in sources-view.tsx was
  5. * entangled with React and silently harboured a data-loss bug (pages
  6. * whose sole source was NOT the one being deleted got wiped anyway,
  7. * because findRelatedWikiPages had returned them via a loose
  8. * substring match).
  9. */
  10. export type DeleteDecision =
  11. /** Keep the page on disk; rewrite its sources to the returned list. */
  12. | { action: "keep"; updatedSources: string[] }
  13. /** Delete the page — the deleting source was its sole contributor. */
  14. | { action: "delete" }
  15. /** Leave the page alone entirely — it ended up in findRelatedWikiPages'
  16. * results by accident (loose frontmatter substring match) but its
  17. * sources list doesn't actually include the deleting source. */
  18. | { action: "skip"; reason: string }
  19. /**
  20. * Decide whether a page should be kept, deleted, or skipped in response
  21. * to the user removing `deletingSource`. Case-insensitive throughout so
  22. * "Test.md" and "test.md" are treated as the same source.
  23. *
  24. * - If `deletingSource` is NOT in `frontmatterSources` → skip.
  25. * Guards against findRelatedWikiPages' loose match taking out
  26. * innocent pages.
  27. * - If it IS and there are OTHER sources too → keep, return the
  28. * filtered list so caller can rewrite the frontmatter.
  29. * - If it IS and it's the ONLY source → delete.
  30. */
  31. export function decidePageFate(
  32. frontmatterSources: readonly string[],
  33. deletingSource: string,
  34. ): DeleteDecision {
  35. const targetLower = deletingSource.toLowerCase()
  36. const inList = frontmatterSources.some(
  37. (s) => s.toLowerCase() === targetLower,
  38. )
  39. if (!inList) {
  40. return {
  41. action: "skip",
  42. reason: `page sources do not include "${deletingSource}"`,
  43. }
  44. }
  45. const survivors = frontmatterSources.filter(
  46. (s) => s.toLowerCase() !== targetLower,
  47. )
  48. if (survivors.length > 0) {
  49. return { action: "keep", updatedSources: survivors }
  50. }
  51. return { action: "delete" }
  52. }