So I didn’t do a good job writing much about it in my archive as I was reading about it, but I recently did a lot of reading into the Semantic Web, which led me to reading about resource description format and finally onto things like Fregean first-order predicates.
This all coalesced around something I’d been slowly doing in my nodes anyway, which was writing a property into each Org-roam node, which has changed name and shape a few times, but I’m currently calling it the node-description property.
One way I’m thinking of this property is as a node’s secondary intension toward relational dynamics, and given that, the structure that seemed best was Fregian subject-predicate-object triplets, where each part of the triplet is another node in the database.
Conveniently, this lines up with where a lot of contemporary Web and large language model technologies are moving.
Here’s a demonstration of what a node’s property drawer looks like, in GnoponEmacs:
:PROPERTIES:
:ID: 39a3beb3-898b-4494-b1d2-144c7a38c3dd
:node-description: ((resource type . babble) (author . emsenn) (publication . emsenn.net) (creation date . 2025-10-06)
:END:That probably isn’t super readable like that. To the computer, it can parse as a list of two-part items: a predicate and an object, and it knows the subject for each is the ID, forming that subject-predicate-object triplet.
But in plainer rendering, here’s what that says the node description is:
- resource type :: babble
- author :: emsenn
- publication :: emsenn.net
- creation date :: 2025-10-06
As you can see, every part of every triplet is itself has two parts: an ID and sign (d542…ecb and babble).
(defun gpe/build-predicates ()
(cl-loop
for (id file level pos todo priority scheduled deadline title props)
in (org-roam-db-query
[:select [id file level pos todo priority scheduled deadline title properties]
:from nodes
:where (like properties '"%resource-description%")])
collect
(let* ((raw-rd (alist-get "RESOURCE-DESCRIPTION" props nil nil #'string=))
(alist (when raw-rd (ignore-errors (read raw-rd)))))
(when alist
(cl-loop
for (pred . obj) in alist
collect
(let* ((parse-link
(lambda (s)
(when (string-match "\\[\\[id:\\([^]]+\\)\\]\\[\\([^]]+\\)\\]\\]" s)
(list :id (match-string 1 s)
:sign (match-string 2 s)))))
(pred-str (replace-regexp-in-string "] \\[" "][" (format "%s" pred)))
(obj-str (replace-regexp-in-string "] \\[" "][" (format "%s" obj)))
(pred-link (funcall parse-link pred-str))
(obj-link (funcall parse-link obj-str)))
(list
:subject-id id
:subject-sign title
:predicate-id (plist-get pred-link :id)
:predicate-sign (plist-get pred-link :sign)
:object-id (plist-get obj-link :id)
:object-sign (plist-get obj-link :sign))))))))
(gpe/build-predicates) (defun gpe/collect-predicates-from-node (node)
(let* ((raw-rd (alist-get "RESOURCE-DESCRIPTION"
(org-roam-node-properties node) nil nil #'string=))
(alist (when raw-rd (ignore-errors (read raw-rd)))))
(when alist
(cl-loop
for (pred . obj) in alist
collect
(let* ((parse-link
(lambda (s)
(when (string-match "\\[\\[id:\\([^]]+\\)\\]\\[\\([^]]+\\)\\]\\]" s)
(list :id (match-string 1 s)
:sign (match-string 2 s)))))
(pred-str
(replace-regexp-in-string "] \\[" "]["
(format "%s" pred)))
(obj-str
(replace-regexp-in-string "] \\[" "]["
(format "%s" obj)))
(pred-link (funcall parse-link pred-str))
(obj-link (funcall parse-link obj-str)))
(list
:subject-id id
:subject-sign title
:predicate-id (plist-get pred-link :id)
:predicate-sign (plist-get pred-link :sign)
:object-id (plist-get obj-link :id)
:object-sign (plist-get obj-link :sign)))))))
(defun gpe/filter-predicates-by (predicates predicate-id object-id)
(cl-loop for entry in predicates
when (and (string= (plist-get (car entry) :predicate-id)
predicate-id)
(string= (plist-get (car entry) :object-id)
object-id))
collect (plist-get (car entry) :subject-id)))
(let* ((ids
(gpe/filter-predicates-by
(gpe/build-predicates)
"edaef069-c68d-46aa-a970-d3cb7fe3165c"
"d542e8d2-3512-47a0-adbb-4a393e161ecb"))
(nodes (mapcar #'org-roam-node-from-id ids))
(output
(mapconcat
(lambda (node)
(format "- %s"
(org-roam-node-id node)
(org-roam-node-title node)))
nodes
"\n")))
output) (gpe/ritm/render-list-of-matches '(("edaef069-c68d-46aa-a970-d3cb7fe3165c" . "d542e8d2-3512-47a0-adbb-4a393e161ecb")))