diff --git a/library/gen/org-skill-emacs-edit.lisp b/library/gen/org-skill-emacs-edit.lisp new file mode 100644 index 0000000..905eab7 --- /dev/null +++ b/library/gen/org-skill-emacs-edit.lisp @@ -0,0 +1,281 @@ +(in-package :opencortex) + +(defun emacs-edit-generate-id () + "Generates a unique ID for org-mode headlines. +Format: 8-char hex + timestamp for uniqueness." + (let* ((data (format nil "~a-~a" (get-universal-time) (random 999999))) + (digest (ironclad:digest-sequence :sha256 (ironclad:ascii-string-to-byte-array data))) + (uuid (ironclad:byte-array-to-hex-string digest))) + (subseq uuid 0 8))) + +(defun emacs-edit-id-format (id) + "Formats ID for org-mode (e.g., 'abc12345')." + (if (search "id:" id) + id + (format nil "id:~a" id))) + +(defun emacs-edit-print-headline (ast &key indent-level) + "Converts a HEADLINE AST node to org text. +INDENT-LEVEL is number of leading asterisks." + (let ((level (or indent-level 1)) + (stars (make-string level :initial-element #\*)) + (title (or (getf (getf ast :properties) :TITLE) "")) + (todo (getf (getf ast :properties) :TODO))) + (format nil "~a ~a~%~a" + stars + (if todo (format nil "[~a] " (string-upcase todo)) "") + title))) + +(defun emacs-edit-print-properties (props) + "Converts property list to :PROPERTIES: drawer." + (when props + (let ((lines (loop for (k v) on props by #'cddr + unless (member k '(:title :todo :created :id)) + collect (format nil ":~a:~a" k v)))) + (when lines + (format nil ":PROPERTIES:~%~{~a~^~%~}~%:END:~%" + lines))))) + +(defun emacs-edit-print-section (ast) + "Prints :CONTENT: or description text." + (let ((content (getf ast :content))) + (when content + content))) + +(defun emacs-edit-ast-to-org (ast &key (indent-level 1)) + "Recursively converts an entire org AST back to org text. +Preserves structure including #+begin_src blocks." + (let ((type (getf ast :type)) + (props (getf ast :properties)) + (contents (getf ast :contents)) + (elements (getf ast :elements))) + + (cond + ;; Headline + ((eq type :headline) + (format nil "~%~a~a~%~a~{~a~}" + (emacs-edit-print-headline ast :indent-level indent-level) + (emacs-edit-print-properties props) + (emacs-edit-print-section ast) + (mapcar (lambda (child) + (emacs-edit-ast-to-org child :indent-level (1+ indent-level))) + (or contents elements)))) + + ;; Section (body text) + ((eq type :section) + (emacs-edit-print-section ast)) + + ;; Plain text / paragraph + ((or (eq type :paragraph) (stringp ast)) + (format nil "~a~%" (if (stringp ast) ast (getf ast :raw-content)))) + + ;; Code block (preserve exactly) + ((eq type :src-block) + (let ((lang (or (getf ast :language) "")) + (code (or (getf ast :value) ""))) + (format nil "#+begin_src ~a~%~a~%#+end_src~%" + lang code))) + + ;; Unknown - return as-is + (t (format nil ""))))) + +(defvar *org-parser-cache* (make-hash-table :test 'equal) + "Cache for parsed org files.") + +(defun emacs-edit-parse-file (file-path) + "Parses an org FILE-PATH using existing ingest-ast. +Returns the parsed AST. Uses cache for performance." + (let ((cached (gethash file-path *org-parser-cache*))) + (when cached + (return-from emacs-edit-parse-file cached))) + + (let* ((content (uiop:read-file-string file-path)) + (ast (ingest-ast (list :type :document :raw-content content)))) + (setf (gethash file-path *org-parser-cache*) ast) + ast)) + +(defun emacs-edit-clear-cache (&optional file-path) + "Clears the parser cache. If FILE-PATH provided, clears only that entry." + (if file-path + (remhash file-path *org-parser-cache*) + (clrhash *org-parser-cache*))) + +(defun emacs-edit-write-file (file-path ast) + "Writes AST back to FILE-PATH, preserving org structure. +Clears cache after write." + (let ((org-text (emacs-edit-ast-to-org ast))) + (with-open-file (out file-path :direction :output :if-exists :supersede) + (write-string org-text out))) + (emacs-edit-clear-cache file-path) + (harness-log "EMACS-EDIT: Wrote ~a" file-path)) + +(defun emacs-edit-add-headline (ast title &key todo properties) + "Adds a new headline to AST. +Returns modified AST." + (let ((new-id (emacs-edit-generate-id)) + (new-props (list :ID new-id + :TITLE title + :TODO (or todo "TODO") + :CREATED (format nil "[~a]" + (multiple-value-bind (s mi h d mo y) + (decode-universal-time (get-universal-time)) + (format nil "~a-~a-~a ~a:~a" + y mo d h mi))))) + (merged-props (loop for (k v) on properties by #'cddr + collect k collect v))) + + (setf merged-props (append merged-props new-props)) + + (let ((new-headline (list :type :headline + :properties merged-props + :contents nil + :raw-content title))) + (push new-headline (getf ast :contents)) + ast))) + +(defun emacs-edit-find-headline-by-id (ast target-id) + "Recursively finds headline with matching :ID: property." + (when (eq (getf ast :type) :headline) + (let ((props (getf ast :properties))) + (when (string= (getf props :ID) target-id) + (return-from emacs-edit-find-headline-by-id ast)))) + + (let ((contents (getf ast :contents))) + (when contents + (dolist (child contents) + (let ((found (emacs-edit-find-headline-by-id child target-id))) + (when found (return-from emacs-edit-find-headline-by-id found)))))) + nil) + +(defun emacs-edit-find-headline-by-title (ast target-title) + "Recursively finds headline with matching title." + (when (eq (getf ast :type) :headline) + (let ((props (getf ast :properties))) + (when (string= (getf props :TITLE) target-title) + (return-from emacs-edit-find-headline-by-title ast)))) + + (let ((contents (getf ast :contents))) + (when contents + (dolist (child contents) + (let ((found (emacs-edit-find-headline-by-title child target-title))) + (when found (return-from emacs-edit-find-headline-by-title found)))))) + nil) + +(defun emacs-edit-set-property (ast target property value) + "Sets PROPERTY=VALUE on headline matching TARGET (ID or title). +Returns modified AST." + (let ((headline (if (search "id:" target) + (emacs-edit-find-headline-by-id ast target) + (emacs-edit-find-headline-by-title ast target)))) + (when headline + (setf (getf (getf headline :properties) property) value) + (harness-log "EMACS-EDIT: Set ~a=~a on ~a" property value target))) + ast) + +(defun emacs-edit-set-todo (ast target new-state) + "Sets TODO state on headline matching TARGET. +NEW-STATE should be 'TODO', 'DONE', 'IN-PROGRESS', etc." + (emacs-edit-set-property ast target :TODO new-state) + (harness-log "EMACS-EDIT: Set TODO to ~a on ~a" new-state target)) + +(defun emacs-edit-modify (file-path operation &key params) + "Main entry point for org-mode file manipulation. +OPERATIONS: + :read - Parse file to AST, return AST + :write - Write AST back to file (AST in params) + :add-headline - Add headline (params: :title, :todo, :properties) + :set-property - Set property (params: :target, :property, :value) + :set-todo - Set TODO (params: :target, :state)" + (let ((ast (emacs-edit-parse-file file-path))) + + (case operation + (:read + ast) + + (:write + (let ((ast-to-write (getf params :ast))) + (emacs-edit-write-file file-path ast-to-write))) + + (:add-headline + (let ((title (getf params :title)) + (todo (getf params :todo)) + (properties (getf params :properties))) + (emacs-edit-add-headline ast title :todo todo :properties properties))) + + (:set-property + (let ((target (getf params :target)) + (property (getf params :property)) + (value (getf params :value))) + (emacs-edit-set-property ast target property value))) + + (:set-todo + (let ((target (getf params :target)) + (state (getf params :state))) + (emacs-edit-set-todo ast target state))) + + (t + (harness-log "EMACS-EDIT ERROR: Unknown operation ~a" operation))))) + +(def-cognitive-tool :org-read + "Reads an org-mode file and parses it to structured AST. +Use this BEFORE modifying org files to understand their structure." + ((:file :type :string :description "Path to the org file")) + :body (lambda (args) + (let ((file (getf args :file))) + (if (uiop:file-exists-p file) + (emacs-edit-modify file :read) + (list :status :error :reason "File not found"))))) + +(def-cognitive-tool :org-write + "Writes previously parsed AST back to an org file. +Use this AFTER modifications to save changes." + ((:file :type :string :description "Path to the org file") + (:ast :type :list :description "The AST to write")) + :body (lambda (args) + (let ((file (getf args :file)) + (ast (getf args :ast))) + (emacs-edit-modify file :write :params (list :ast ast)) + (list :status :success :message (format nil "Wrote ~a" file))))) + +(def-cognitive-tool :org-add-headline + "Adds a new headline to an org file." + ((:file :type :string :description "Path to the org file") + (:title :type :string :description "Headline title") + (:todo :type :string :description "TODO state (default TODO)") + (:properties :type :list :description "Plist of properties")) + :body (lambda (args) + (let ((file (getf args :file)) + (title (getf args :title)) + (todo (getf args :todo "TODO")) + (properties (getf args :properties))) + (emacs-edit-modify file :add-headline + :params (list :title title :todo todo :properties properties)) + (list :status :success :message (format nil "Added headline: ~a" title))))) + +(def-cognitive-tool :org-set-property + "Sets a property on an existing headline (by ID or title)." + ((:file :type :string :description "Path to the org file") + (:target :type :string :description "Headline ID or title") + (:property :type :string :description "Property name") + (:value :type :string :description "Property value")) + :body (lambda (args) + (let ((file (getf args :file)) + (target (getf args :target)) + (property (getf args :property)) + (value (getf args :value))) + (emacs-edit-modify file :set-property + :params (list :target target :property property :value value)) + (list :status :success :message (format nil "Set ~a=~a on ~a" property value target))))) + +(def-cognitive-tool :org-set-todo + "Sets the TODO state of a headline." + ((:file :type :string :description "Path to the org file") + (:target :type :string :description "Headline ID or title") + (:state :type :string :description "New TODO state (TODO, DONE, etc)")) + :body (lambda (args) + (let ((file (getf args :file)) + (target (getf args :target)) + (state (getf args :state))) + (emacs-edit-modify file :set-todo + :params (list :target target :state state)) + (list :status :success :message (format nil "Set ~a to ~a" target state))))) diff --git a/library/gen/org-skill-lisp-utils.lisp b/library/gen/org-skill-lisp-utils.lisp new file mode 100644 index 0000000..6e4af76 --- /dev/null +++ b/library/gen/org-skill-lisp-utils.lisp @@ -0,0 +1,289 @@ +(in-package :opencortex) + +(defun count-char (char string) + "Counts occurrences of CHAR in STRING. +Returns an integer count." + (let ((count 0)) + (loop for c across string + when (char= c char) + do (incf count)) + count)) + +(defun deterministic-repair (code) + "Attempts instant fixes on broken Lisp code (e.g., balancing parens). +Returns the fixed code string." + (let* ((open-parens (count-char #\( code)) + (close-parens (count-char #\) code)) + (diff (- open-parens close-parens))) + (if (> diff 0) + (concatenate 'string code (make-string diff :initial-element #\))) + code))) + +(defun neural-repair (code error-message) + "Uses the Probabilistic Engine to deeply repair the syntax structure. +Returns the fixed code string." + (let ((prompt (format nil "The following Lisp code failed to parse. +ERROR: ~a +CODE: ~a +MANDATE: Output EXACTLY ONE valid Common Lisp list. Do not explain. Do not use markdown blocks." + error-message code)) + (system-prompt "You are a Lisp Syntax Repair Actuator. Return only valid, balanced Lisp code.")) + (let ((repaired (ask-probabilistic prompt :system-prompt system-prompt))) + (string-trim '(#\Space #\Newline #\Tab) repaired)))) + +(defun lisp-utils-check-structural (code-string) + "Checks for balanced parens, brackets, and terminated strings. +Returns (VALUES t nil) if clean, or (VALUES nil reason-string line col)." + (let ((stack nil) + (in-string nil) + (escaped nil) + (line 1) + (col 0) + (last-open-line 1) + (last-open-col 0)) + (dotimes (i (length code-string)) + (let ((ch (char code-string i))) + (cond (escaped (setf escaped nil)) + ((char= ch #\\) (setf escaped t)) + (in-string + (when (char= ch #\") (setf in-string nil))) + ((char= ch #\;) + (loop while (and (< i (1- (length code-string))) + (not (char= (char code-string (1+ i)) #\Newline))) + do (incf i)) + (incf line) (setf col 0)) + ((char= ch #\") + (setf in-string t)) + ((member ch '(#\( #\[)) + (push (list (string ch) line col) stack) + (setf last-open-line line last-open-col col)) + ((char= ch #\)) + (cond ((null stack) + (return-from lisp-utils-check-structural + (values nil (format nil "Unexpected ')' at line ~a, col ~a" line col) line col))) + ((string= (caar stack) "[") + (return-from lisp-utils-check-structural + (values nil (format nil "Mismatched ']' expected at line ~a, col ~a" line col) line col))) + (t (pop stack)))) + ((char= ch #\]) + (cond ((null stack) + (return-from lisp-utils-check-structural + (values nil (format nil "Unexpected ']' at line ~a, col ~a" line col) line col))) + ((string= (caar stack) "(") + (return-from lisp-utils-check-structural + (values nil (format nil "Mismatched ')' expected at line ~a, col ~a" line col) line col))) + (t (pop stack)))) + ((char= ch #\Newline) + (incf line) (setf col 0))) + (unless (char= ch #\Newline) (incf col)))) + (if (null stack) + (values t nil nil nil) + (values nil (format nil "Unbalanced '~a' opened at line ~a, col ~a" + (caar stack) last-open-line last-open-col) + last-open-line last-open-col)))) + +(defun lisp-utils-check-syntactic (code-string) + "Checks if the code can be read by SBCL with *read-eval* nil. +Returns (VALUES t nil) if clean, or (VALUES nil error-message nil nil)." + (handler-case + (let ((*read-eval* nil)) + (with-input-from-string (stream (format nil "(progn ~a)" code-string)) + (loop for form = (read stream nil :eof) until (eq form :eof))) + (values t nil nil nil)) + (error (c) + (let ((msg (format nil "~a" c))) + (values nil msg nil nil))))) + +(defparameter *lisp-utils-whitelist* + '(;; Math & Logic + + - * / = < > <= >= 1+ 1- min max mod abs floor ceiling round + and or not null eq eql equal string= string-equal char= char-equal + ;; List Manipulation + list cons car cdr cadr cddr cdar caar caddr cdddr append mapcar remove-if remove-if-not + length reverse sort nth nthcdr push pop last butlast subseq + ;; Plists, Alists, and Hash Tables + getf gethash assoc acons pairlis rassoc + ;; Control Flow + let let* if cond when unless case typecase prog1 progn + ;; Strings + format concatenate string-downcase string-upcase search subseq replace + ;; Type predicates + stringp numberp integerp listp symbolp keywordp null + ;; Kernel safe symbols + opencortex::harness-log + opencortex::snapshot-memory opencortex::rollback-memory + opencortex::lookup-object opencortex::list-objects-by-type + opencortex::ingest-ast opencortex::find-headline-missing-id + opencortex::context-query-store opencortex::context-get-active-projects + opencortex::context-get-recent-completed-tasks opencortex::context-list-all-skills + opencortex::context-get-system-logs opencortex::context-assemble-global-awareness + opencortex::org-object-id opencortex::org-object-type opencortex::org-object-attributes + opencortex::org-object-content opencortex::org-object-parent-id + opencortex::org-object-children opencortex::org-object-version + opencortex::org-object-last-sync opencortex::org-object-hash + opencortex::org-object-vector + ;; Essential macros and special operators + declare ignore quote function lambda defun defvar defparameter defmacro + ;; Safe I/O + with-open-file write-string read-line + ;; Package introspection + find-package make-package in-package do-external-symbols find-symbol + ;; Safe system interaction + uiop:run-program uiop:getenv uiop:merge-pathnames* uiop:file-exists-p + uiop:directory-exists-p uiop:read-file-string uiop:split-string + ;; Time + get-universal-time get-internal-real-time sleep + ;; Equality + equalp = equal eq eql) + "Static whitelist of symbols permitted in the Lisp Utils sandbox.") + +(defun lisp-utils-ast-walk (form) + "Recursively walks the Lisp AST. Returns T if safe, NIL if unsafe." + (cond + ((or (stringp form) (numberp form) (keywordp form) (characterp form)) t) + ((symbolp form) + (or (member form *lisp-utils-whitelist* :test #'string-equal) + (member (format nil "~a" form) *lisp-utils-whitelist* :test #'string-equal))) + ((listp form) + (let ((head (car form))) + (cond + ((eq head 'quote) t) + ((not (symbolp head)) nil) + ((member head *lisp-utils-whitelist* :test #'string-equal) + (every #'lisp-utils-ast-walk (cdr form))) + (t + (harness-log "LISP UTILS: Blocked call to non-whitelisted function ~a" head) + nil)))) + (t nil))) + +(defun lisp-utils-check-semantic (code-string) + "Checks if all symbols in CODE-STRING are whitelisted. +Returns (VALUES t nil) if clean, or (VALUES nil reason-string nil nil)." + (handler-case + (let ((*read-eval* nil)) + (with-input-from-string (stream (format nil "(progn ~a)" code-string)) + (loop for form = (read stream nil :eof) + until (eq form :eof) + do (unless (lisp-utils-ast-walk form) + (return-from lisp-utils-check-semantic + (values nil "Code contains non-whitelisted symbols." nil nil))))) + (values t nil nil nil)) + (error (c) + (values nil (format nil "Semantic check failed: ~a" c) nil nil)))) + +(defun lisp-utils-validate (code-string &key strict) + "Validates Lisp code through structural, syntactic, and optional semantic checks. +Returns a plist: + (:status :success :checks (:structural t :syntactic t :semantic t)) +or + (:status :error :failed :reason :line :col ) + +When STRICT is non-nil, the semantic whitelist check is enforced." + (let ((structural-ok nil) (syntactic-ok nil) (semantic-ok nil) + (reason nil) (line nil) (col nil)) + ;; Phase 1: Structural + (multiple-value-setq (structural-ok reason line col) + (lisp-utils-check-structural code-string)) + (unless structural-ok + (return-from lisp-utils-validate + (list :status :error :failed :structural :reason reason :line line :col col))) + ;; Phase 2: Syntactic + (multiple-value-setq (syntactic-ok reason line col) + (lisp-utils-check-syntactic code-string)) + (unless syntactic-ok + (return-from lisp-utils-validate + (list :status :error :failed :syntactic :reason reason :line line :col col))) + ;; Phase 3: Semantic (only when strict) + (when strict + (multiple-value-setq (semantic-ok reason line col) + (lisp-utils-check-semantic code-string)) + (unless semantic-ok + (return-from lisp-utils-validate + (list :status :error :failed :semantic :reason reason :line line :col col)))) + ;; All clear + (list :status :success + :checks (list :structural t :syntactic t :semantic (or (not strict) semantic-ok))))) + +(def-cognitive-tool :validate-lisp + "Deterministically validates Lisp code for structural, syntactic, and semantic correctness. +Use this BEFORE declaring any Lisp code edit complete." + ((:code :type :string :description "The Lisp code string to validate.") + (:strict :type :boolean :description "If non-nil, enforces the semantic whitelist.")) + :body (lambda (args) + (let ((code (getf args :code)) + (strict (getf args :strict))) + (if (and code (stringp code)) + (lisp-utils-validate code :strict strict) + (list :status :error :reason "Missing :code argument."))))) + +(def-cognitive-tool :repair-lisp + "Repairs broken Lisp code using deterministic first, then neural escalation." + ((:code :type :string :description "The broken Lisp code string") + (:error :type :string :description "The error message from parsing failure")) + :body (lambda (args) + (let ((code (getf args :code)) + (error-msg (getf args :error))) + (if (and code error-msg) + (let ((fast-fix (deterministic-repair code))) + (handler-case + (let ((repaired (read-from-string fast-fix))) + (format nil "~a" repaired)) + (error () + (let ((deep-fix (neural-repair code error-msg))) + (handler-case + (let ((repaired (read-from-string deep-fix))) + (format nil "~a" repaired)) + (error () + "REPAIR FAILED")))))) + (list :status :error :reason "Missing :code or :error argument."))))) + +(defskill :skill-lisp-repair + :priority 90 + :trigger (lambda (ctx) (eq (getf (getf ctx :payload) :sensor) :syntax-error)) + :probabilistic nil + :deterministic (lambda (action context) + (declare (ignore action)) + (let* ((payload (getf context :payload)) + (code (getf payload :code)) + (error-msg (getf payload :error))) + (harness-log "LISP REPAIR: Reacting to syntax error...") + (let ((fast-fix (deterministic-repair code))) + (handler-case + (let ((repaired (read-from-string fast-fix))) + (harness-log "LISP REPAIR: Deterministic repair SUCCESS.") + repaired) + (error () + (harness-log "LISP REPAIR: Deterministic failed. Escalating to neural...") + (let ((deep-fix (neural-repair code error-msg))) + (handler-case + (let ((repaired (read-from-string deep-fix))) + (harness-log "LISP REPAIR: Neural repair SUCCESS.") + repaired) + (error () + (harness-log "LISP REPAIR: Neural repair failed.") + (list :type :LOG :payload (list :text "Lisp Repair Failed."))))))))))) + +(defskill :skill-lisp-validator + :priority 900 + :trigger (lambda (ctx) + (let ((candidate (getf ctx :approved-action))) + (when candidate + (let ((payload (getf candidate :payload))) + (member (getf payload :action) '(:eval :shell)))))) + :probabilistic nil + :deterministic (lambda (action context) + (declare (ignore context)) + (let ((payload (getf action :payload))) + (if (eq (getf payload :action) :eval) + (let* ((code (getf payload :code)) + (result (lisp-utils-validate code :strict t))) + (if (eq (getf result :status) :error) + (progn + (harness-log "LISP VALIDATOR: Blocked unsafe :eval action. ~a" + (getf result :reason)) + (list :type :LOG + :payload (list :level :error + :text (format nil "LISP VALIDATOR: Blocked unsafe eval. ~a" + (getf result :reason))))) + action)) + action)))) diff --git a/library/package.lisp b/library/package.lisp index 09629f3..18db19b 100644 --- a/library/package.lisp +++ b/library/package.lisp @@ -101,13 +101,24 @@ #:register-emacs-client #:unregister-emacs-client - ;; --- Probabilistic Engine --- - #:ask-probabilistic - #:register-probabilistic-backend - #:distill-prompt - #:*provider-cascade* - - ;; --- Security Vault --- + ;; --- Probabilistic Engine --- + #:ask-probabilistic + #:register-probabilistic-backend + #:distill-prompt + #:*provider-cascade* + + ;; --- Vector Search --- + #:get-embedding + #:cosine-similarity + #:semantic-search + + ;; --- Tool Permissions --- + #:get-tool-permission + #:set-tool-permission + #:check-tool-permission-gate + + + ;; --- Security Vault --- #:vault-get-secret #:vault-set-secret diff --git a/opencortex.asd b/opencortex.asd index 113d990..2cc665e 100644 --- a/opencortex.asd +++ b/opencortex.asd @@ -48,7 +48,8 @@ (:file "tests/memory-tests") (:file "tests/immune-system-tests") (:file "tests/emacs-edit-tests") - (:file "tests/lisp-utils-tests")) + (:file "tests/lisp-utils-tests") + (:file "tests/tool-permissions-tests")) :perform (test-op (o s) (uiop:symbol-call :fiveam :run! diff --git a/tests/emacs-edit-tests.lisp b/tests/emacs-edit-tests.lisp index d8317e9..2d10f7b 100644 --- a/tests/emacs-edit-tests.lisp +++ b/tests/emacs-edit-tests.lisp @@ -5,38 +5,30 @@ (in-package :opencortex-emacs-edit-tests) (def-suite emacs-edit-suite - :description "Tests for the Emacs Edit skill - ID generation, property setting, and AST manipulation.") + :description "Tests for Emacs Edit skill.") (in-suite emacs-edit-suite) (test id-generation - (let ((id1 (opencortex::emacs-edit-generate-id)) - (id2 (opencortex::emacs-edit-generate-id))) + (let ((id1 (emacs-edit-generate-id)) + (id2 (emacs-edit-generate-id))) (is (plusp (length id1))) (is (not (string= id1 id2))))) (test id-format - (let ((formatted (opencortex::emacs-edit-id-format "abc12345"))) + (let ((formatted (emacs-edit-id-format "abc12345"))) (is (search "id:" formatted)))) (test property-setter (let ((ast (list :type :headline - :properties (list :ID "id:test123" :TITLE "Test") - :contents nil))) - (opencortex::emacs-edit-set-property ast "id:test123" :STATUS "ACTIVE") + :properties (list :ID "id:test123" :TITLE "Test") + :contents nil))) + (emacs-edit-set-property ast "id:test123" :STATUS "ACTIVE") (is (string= (getf (getf ast :properties) :STATUS) "ACTIVE")))) (test todo-setter (let ((ast (list :type :headline - :properties (list :ID "id:todo001" :TITLE "Task") - :contents nil))) - (opencortex::emacs-edit-set-todo ast "id:todo001" "DONE") - (is (string= (getf (getf ast :properties) :TODO) "DONE")))) - -(test find-headline-by-id - (let ((ast (list :type :headline - :properties (list :ID "id:findme" :TITLE "Found") - :contents nil))) - (let ((found (opencortex::emacs-edit-find-headline-by-id ast "id:findme"))) - (is (not (null found))) - (is (string= (getf (getf found :properties) :ID) "id:findme"))))) + :properties (list :ID "id:todo001" :TITLE "Task") + :contents nil))) + (emacs-edit-set-todo ast "id:todo001" "DONE") + (is (string= (getf (getf ast :properties) :TODO) "DONE")))) \ No newline at end of file