dedup-storage.ts 2.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. /**
  2. * Persistence for the dedup tool's "not duplicates" whitelist.
  3. *
  4. * When the user reviews a candidate group and says "these are NOT
  5. * the same thing", we record the group so the next detector run
  6. * doesn't re-suggest it. Stored as a JSON array-of-arrays where
  7. * each inner array is one whitelisted group of slugs (lowercased,
  8. * sorted — see the canonical key logic in `dedup.ts`).
  9. *
  10. * Lives next to ingest-cache.json / image-caption-cache.json /
  11. * lexical-graph.json (when added) — same `.qmai/` directory,
  12. * same JSON-on-disk pattern.
  13. */
  14. import { readFile, writeFile, fileExists } from "@/commands/fs"
  15. import { normalizePath } from "@/lib/path-utils"
  16. const FILE_NAME = ".qmai/dedup-not-duplicates.json"
  17. export async function loadNotDuplicates(projectPath: string): Promise<string[][]> {
  18. const pp = normalizePath(projectPath)
  19. const filePath = `${pp}/${FILE_NAME}`
  20. try {
  21. if (!(await fileExists(filePath))) return []
  22. } catch {
  23. return []
  24. }
  25. try {
  26. const content = await readFile(filePath)
  27. const parsed = JSON.parse(content)
  28. if (!Array.isArray(parsed)) return []
  29. return parsed.filter(
  30. (g): g is string[] =>
  31. Array.isArray(g) && g.every((s) => typeof s === "string"),
  32. )
  33. } catch {
  34. return []
  35. }
  36. }
  37. async function saveNotDuplicates(
  38. projectPath: string,
  39. list: string[][],
  40. ): Promise<void> {
  41. const pp = normalizePath(projectPath)
  42. await writeFile(`${pp}/${FILE_NAME}`, JSON.stringify(list, null, 2))
  43. }
  44. /**
  45. * Add a group to the whitelist. Idempotent — if the same group
  46. * (in any order, any casing) is already present, this is a no-op.
  47. */
  48. export async function addNotDuplicate(
  49. projectPath: string,
  50. slugs: string[],
  51. ): Promise<void> {
  52. if (slugs.length < 2) return
  53. const list = await loadNotDuplicates(projectPath)
  54. const normNew = canonicalKey(slugs)
  55. for (const existing of list) {
  56. if (canonicalKey(existing) === normNew) return // already there
  57. }
  58. list.push([...slugs].sort())
  59. await saveNotDuplicates(projectPath, list)
  60. }
  61. function canonicalKey(slugs: string[]): string {
  62. return [...slugs].map((s) => s.toLowerCase()).sort().join(",")
  63. }