diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-02 01:01:38 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-02 01:01:38 -0400 |
| commit | 25e74fcb57ae467a39ecabe41568a986db08ebe4 (patch) | |
| tree | 38cf1aa6201e4d2fc59078f088bdda3e60d145ed /modules | |
| parent | 6252ec01350bd211e043bb84b31498cc00a11046 (diff) | |
| download | dotemacs-25e74fcb57ae467a39ecabe41568a986db08ebe4.tar.gz dotemacs-25e74fcb57ae467a39ecabe41568a986db08ebe4.zip | |
feat(org): resolve org-id links into project spec docs
The docs-lifecycle convention gives every formal spec under a project's docs/specs/ an :ID: and links cross-project with [[id:...]], but org-id-locations only indexes agenda files and visited files, so a fresh spec's id never resolved on click. org-spec-links.el enumerates every project's docs/specs/*.org into org-id-extra-files once org-id loads (a literal file list; org-id doesn't glob), and cj/org-id-refresh-spec-locations re-scans and updates org-id-locations for immediacy after new specs land. Verified live against a known cross-project spec id.
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/org-spec-links.el | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/modules/org-spec-links.el b/modules/org-spec-links.el new file mode 100644 index 00000000..369a4c23 --- /dev/null +++ b/modules/org-spec-links.el @@ -0,0 +1,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 |
