branch: externals/gnosis
commit 47b1252cf29978072caed448a5bb8810012360d8
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>

    [Feature] Add link integrity detection and repair commands.
---
 gnosis.el | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 150 insertions(+)

diff --git a/gnosis.el b/gnosis.el
index 8c0264b8e4..0dbe118f1a 100644
--- a/gnosis.el
+++ b/gnosis.el
@@ -2597,5 +2597,155 @@ Reopens the gnosis database after successful pull."
           (message "Updated %d themata with links to '%s'" (length updates) 
string)
           (length updates))))))
 
+;;; Link integrity
+
+(defun gnosis--all-link-dests ()
+  "Return all unique dest UUIDs from gnosis links table."
+  (cl-remove-duplicates (gnosis-select 'dest 'links nil t) :test #'equal))
+
+(defun gnosis--orphaned-link-dests ()
+  "Return dest UUIDs in gnosis links that have no matching org-gnosis node."
+  (let ((link-dests (gnosis--all-link-dests))
+        (node-ids (org-gnosis-select 'id 'nodes nil t)))
+    (cl-set-difference link-dests node-ids :test #'equal)))
+
+(defun gnosis--orphaned-links ()
+  "Return (source dest) rows where dest has no matching org-gnosis node."
+  (let ((orphaned-dests (gnosis--orphaned-link-dests)))
+    (when orphaned-dests
+      (gnosis-select '[source dest] 'links
+                     `(in dest ,(vconcat orphaned-dests))))))
+
+(defun gnosis--thema-expected-links (keimenon parathema)
+  "Extract expected link IDs from KEIMENON and PARATHEMA text."
+  (cl-remove-duplicates
+   (append (gnosis-extract-id-links keimenon)
+           (gnosis-extract-id-links parathema))
+   :test #'equal))
+
+(defun gnosis--stale-links ()
+  "Return (source dest) pairs in DB but not in thema text.
+Fetches all themata, extras, and links in bulk queries."
+  (let* ((themata (gnosis-select '[id keimenon] 'themata nil))
+         (extras (gnosis-select '[id parathema] 'extras nil))
+         (all-links (gnosis-select '[source dest] 'links nil))
+         (extras-map (make-hash-table :test 'equal)))
+    ;; Build extras lookup
+    (dolist (extra extras)
+      (puthash (car extra) (cadr extra) extras-map))
+    ;; Find links in DB that aren't in text
+    (cl-loop for (source dest) in all-links
+             for keimenon = (cadr (cl-find source themata :key #'car))
+             for parathema = (gethash source extras-map "")
+             for expected = (gnosis--thema-expected-links
+                             (or keimenon "") (or parathema ""))
+             unless (member dest expected)
+             collect (list source dest))))
+
+(defun gnosis--missing-links ()
+  "Return (source dest) pairs in thema text but not in DB.
+Fetches all themata, extras, and links in bulk queries."
+  (let* ((themata (gnosis-select '[id keimenon] 'themata nil))
+         (extras (gnosis-select '[id parathema] 'extras nil))
+         (all-links (gnosis-select '[source dest] 'links nil))
+         (extras-map (make-hash-table :test 'equal))
+         (links-set (make-hash-table :test 'equal)))
+    ;; Build extras lookup
+    (dolist (extra extras)
+      (puthash (car extra) (cadr extra) extras-map))
+    ;; Build existing links set
+    (dolist (link all-links)
+      (puthash (format "%s-%s" (car link) (cadr link)) t links-set))
+    ;; Find links in text that aren't in DB
+    (cl-loop for (id keimenon) in themata
+             for parathema = (gethash id extras-map "")
+             for expected = (gnosis--thema-expected-links
+                             (or keimenon "") (or parathema ""))
+             append (cl-loop for dest in expected
+                             for key = (format "%s-%s" id dest)
+                             unless (gethash key links-set)
+                             collect (list id dest)))))
+
+;;;###autoload
+(defun gnosis-links-check ()
+  "Report link health between gnosis and org-gnosis databases."
+  (interactive)
+  (let ((orphaned (gnosis--orphaned-link-dests))
+        (stale (gnosis--stale-links))
+        (missing (gnosis--missing-links)))
+    (message "Link health: %d orphaned, %d stale, %d missing"
+             (length orphaned) (length stale) (length missing))))
+
+(defun gnosis--delete-orphaned-links (orphaned-dests)
+  "Delete links whose dest is in ORPHANED-DESTS."
+  (when orphaned-dests
+    (emacsql-with-transaction gnosis-db
+      (emacsql gnosis-db `[:delete :from links
+                           :where (in dest ,(vconcat orphaned-dests))]))))
+
+(defun gnosis--delete-stale-links (stale-links)
+  "Delete STALE-LINKS list of (source dest) from links table."
+  (when stale-links
+    (emacsql-with-transaction gnosis-db
+      (dolist (link stale-links)
+        (emacsql gnosis-db `[:delete :from links
+                             :where (and (= source ,(car link))
+                                         (= dest ,(cadr link)))])))))
+
+(defun gnosis--insert-missing-links (missing-links)
+  "Insert MISSING-LINKS list of (source dest) into links table."
+  (when missing-links
+    (emacsql-with-transaction gnosis-db
+      (dolist (link missing-links)
+        (gnosis--insert-into 'links `([,(car link) ,(cadr link)]))))))
+
+(defun gnosis--commit-link-cleanup (orphaned stale missing)
+  "Commit link cleanup changes for ORPHANED, STALE, and MISSING counts."
+  (let ((git (executable-find "git"))
+        (default-directory gnosis-dir))
+    (unless gnosis-testing
+      (unless (file-exists-p (expand-file-name ".git" gnosis-dir))
+        (vc-git-create-repo))
+      (shell-command (format "%s add gnosis.db" git))
+      (gnosis--shell-cmd-with-password
+       (format "%s commit -m 'Link cleanup: %d orphaned, %d stale removed, %d 
missing added'"
+               git orphaned stale missing)))
+    (when (and gnosis-vc-auto-push (not gnosis-testing))
+      (gnosis-vc-push))))
+
+;;;###autoload
+(defun gnosis-links-cleanup ()
+  "Remove orphaned and stale links from gnosis database."
+  (interactive)
+  (let ((orphaned-dests (gnosis--orphaned-link-dests))
+        (stale (gnosis--stale-links)))
+    (if (and (null orphaned-dests) (null stale))
+        (message "No orphaned or stale links found")
+      (when (y-or-n-p (format "Remove %d orphaned + %d stale links? "
+                              (length orphaned-dests) (length stale)))
+        (gnosis--delete-orphaned-links orphaned-dests)
+        (gnosis--delete-stale-links stale)
+        (gnosis--commit-link-cleanup (length orphaned-dests) (length stale) 0)
+        (message "Removed %d orphaned + %d stale links"
+                 (length orphaned-dests) (length stale))))))
+
+;;;###autoload
+(defun gnosis-links-sync ()
+  "Full re-sync: remove orphaned/stale links and insert missing ones."
+  (interactive)
+  (let ((orphaned-dests (gnosis--orphaned-link-dests))
+        (stale (gnosis--stale-links))
+        (missing (gnosis--missing-links)))
+    (if (and (null orphaned-dests) (null stale) (null missing))
+        (message "All links are in sync")
+      (when (y-or-n-p (format "Sync links: remove %d orphaned + %d stale, add 
%d missing? "
+                              (length orphaned-dests) (length stale) (length 
missing)))
+        (gnosis--delete-orphaned-links orphaned-dests)
+        (gnosis--delete-stale-links stale)
+        (gnosis--insert-missing-links missing)
+        (gnosis--commit-link-cleanup (length orphaned-dests) (length stale) 
(length missing))
+        (message "Synced: removed %d orphaned + %d stale, added %d missing"
+                 (length orphaned-dests) (length stale) (length missing))))))
+
 (provide 'gnosis)
 ;;; gnosis.el ends here

Reply via email to