aboutsummaryrefslogtreecommitdiff
path: root/modules/org-spec-links.el
blob: 369a4c2383bc5423a54a6fab00e9c69d3004abb6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
;;; org-spec-links.el --- Resolve org-id links into project spec docs -*- lexical-binding: t -*-
;; author: Craig Jennings <c@cjennings.net>

;;; Commentary:
;;
;; Layer: 3 (Domain Workflow).
;; Category: D/S.
;; Load shape: eager (cheap: defuns + one after-load registration).
;; Eager reason: none beyond the after-load hook; the scan itself runs
;;   only when org-id loads.
;; Top-level side effects: one with-eval-after-load (org-id).
;; Runtime requires: none (org-id is configured after it loads).
;; Direct test load: yes.
;;
;; The docs-lifecycle convention (rulesets, 2026-07-01) gives every
;; formal spec under a project's docs/specs/ an :ID: UUID, and
;; cross-project links use [[id:...]].  org-id-locations only indexes
;; agenda files and files Emacs has visited, so a fresh spec's ID won't
;; resolve on click.  This module enumerates every project's
;; docs/specs/*.org into `org-id-extra-files' (a literal file list --
;; org-id doesn't glob) so org-id's own scans can find them, and
;; provides `cj/org-id-refresh-spec-locations' to re-scan after new
;; specs land and teach `org-id-locations' immediately.
;;
;; Projects are directories carrying .ai/protocols.org under ~/code/,
;; ~/projects/, and ~/.emacs.d -- the same discovery the `ai' launcher
;; and inbox-send use.

;;; Code:

(defun cj/--org-spec-project-base-dirs ()
  "Return the base directories scanned for projects."
  (list (expand-file-name "code/" "~")
        (expand-file-name "projects/" "~")))

(defun cj/--org-spec-project-roots (&optional base-dirs)
  "Return project roots: dirs carrying .ai/protocols.org.
Scans one level under each of BASE-DIRS (default
`cj/--org-spec-project-base-dirs'), plus `user-emacs-directory'."
  (let ((candidates
         (append
          (mapcan (lambda (base)
                    (when (file-directory-p base)
                      (directory-files base t "\\`[^.]" t)))
                  (or base-dirs (cj/--org-spec-project-base-dirs)))
          (unless base-dirs (list user-emacs-directory)))))
    (seq-filter (lambda (dir)
                  (and (file-directory-p dir)
                       (file-exists-p (expand-file-name ".ai/protocols.org" dir))))
                candidates)))

(defun cj/--org-spec-files (&optional base-dirs)
  "Return every project's docs/specs/*.org as absolute file names.
BASE-DIRS overrides the default project discovery (for tests)."
  (mapcan (lambda (root)
            (let ((specs (expand-file-name "docs/specs/" root)))
              (when (file-directory-p specs)
                (directory-files specs t "\\.org\\'" t))))
          (cj/--org-spec-project-roots base-dirs)))

(defun cj/org-id-refresh-spec-locations ()
  "Point `org-id-extra-files' at every project's spec docs and rescan.
Run after new specs land (a spec-sort pass, a new project) so their
:ID:s resolve.  The rescan also walks agenda files, so it can take a
few seconds; the `org-id-extra-files' assignment alone is instant."
  (interactive)
  (let ((files (cj/--org-spec-files)))
    (setq org-id-extra-files files)
    (when (and files (fboundp 'org-id-update-id-locations))
      (org-id-update-id-locations files))
    (message "org-id: %d spec file(s) registered" (length files))))

;; Teach org-id the spec files as soon as it loads: the extra-files list
;; is enough for org-id's own fallback scans; the full refresh command
;; is for immediacy after new specs land.
(with-eval-after-load 'org-id
  (setq org-id-extra-files (cj/--org-spec-files)))

(provide 'org-spec-links)
;;; org-spec-links.el ends here