From dfef1e6dfa0b959ffffc95cb4d99e52a5ae2304e Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Mon, 9 Feb 2026 17:22:28 -0500 Subject: [PATCH] Working blocker --- doom/.config/doom/README.org | 87 +++++++++++++++++++++++++----------- doom/.config/doom/config.el | 87 +++++++++++++++++++++++++----------- 2 files changed, 120 insertions(+), 54 deletions(-) diff --git a/doom/.config/doom/README.org b/doom/.config/doom/README.org index d7c72f5..772e423 100644 --- a/doom/.config/doom/README.org +++ b/doom/.config/doom/README.org @@ -469,8 +469,8 @@ change ~org-directory~. It must be set before org loads! ((org-agenda-span 60) (org-agenda-start-day "2026-01-29") (org-agenda-overriding-header "📅 Installation Bombardier") - (org-agenda-prefix-format "%12t") ;; reserve time space - (org-agenda-todo-keyword-format " %-12s ") ;; fixed-width TODO + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO (org-agenda-tags-column -100) ;; right-align tags (org-agenda-time-grid nil) ))) @@ -2248,6 +2248,29 @@ If FILE is nil, refile in the current file." (nthcdr 3 (decode-time next))))))) (t t1)))) +(defun gortium/org--get-range-end (pos) + "Extract the END timestamp from an existing range like -- at POS. +POS can be a marker or a cons cell (file . position)." + (let ((marker (if (markerp pos) pos + (let ((file (car pos)) + (p (cdr pos))) + (with-current-buffer (find-file-noselect file) + (copy-marker p)))))) + (with-current-buffer (marker-buffer marker) + (save-excursion + (goto-char marker) + (org-back-to-heading t) + (let ((subtree-end (save-excursion (org-end-of-subtree t) (point))) + (end-time nil)) + (save-restriction + (narrow-to-region (point) subtree-end) + (goto-char (point-min)) + (when (re-search-forward "<[^>]+>--<\\([^>]+\\)>" nil t) + (condition-case nil + (setq end-time (org-time-string-to-time (match-string 1))) + (error nil)))) + end-time))))) + (defun gortium/org--get-range-start (pos) "Extract the start timestamp from an existing range like --. Returns nil if no range found (safe, non-blocking)." @@ -2321,7 +2344,7 @@ Returns nil if no range found (safe, non-blocking)." ;; Helper: Find dependency end time ;; ------------------------------------------------------------ (defun gortium/internal--get-blocker-end (blocker-str task-end-map) - "Return latest end time only if blockers are DONE or have been recalculated." + "Return the latest end time of all blockers if they are all resolved." (let ((clean (s-trim (format "%s" blocker-str))) (latest-time nil) (all-resolved t)) @@ -2329,23 +2352,29 @@ Returns nil if no range found (safe, non-blocking)." (not (string-empty-p (s-trim (match-string 1 clean))))) (dolist (tid (split-string (match-string 1 clean) "[ ,]+" t)) (let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid)) - (pos (org-id-find clean-id t)) + (pos (org-id-find clean-id t)) ;; Returns marker OR (file . pos) (computed-end (gethash clean-id task-end-map)) (blocker-end (cond - ;; 1) Use the new time we just calculated in this run (Priority!) (computed-end computed-end) - - ;; 2) If it's DONE, use the CLOSED timestamp - ((and pos (org-entry-get pos "CLOSED")) - (org-time-string-to-time (org-entry-get pos "CLOSED"))) - - ;; 3) If it's FIXED, use the existing range/scheduled time - ((and pos (string-equal "t" (org-entry-get pos "FIXED"))) - (or (gortium/org--get-range-start pos) ;; Note: This needs range end logic, but start is a fallback - (org-get-scheduled-time pos))) - - ;; Otherwise: We MUST wait for this blocker to be recalculated + + ;; Use a temporary buffer context to check properties if pos is a list + ((and pos (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) + (with-current-buffer (marker-buffer m) + (org-with-point-at m + (cond + ((org-entry-get nil "CLOSED") + (org-time-string-to-time (org-entry-get nil "CLOSED"))) + ((string-equal "t" (org-entry-get nil "FIXED")) + (gortium/org--get-range-end m)) + (t nil)))))) + ;; This captures the result of the let/with-current-buffer block + (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) + (with-current-buffer (marker-buffer m) + (org-with-point-at m + (if (org-entry-get nil "CLOSED") + (org-time-string-to-time (org-entry-get nil "CLOSED")) + (gortium/org--get-range-end m)))))) (t nil)))) (if blocker-end @@ -2485,17 +2514,16 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." ;; --- MAIN SCHEDULER --- (defun gortium/org-schedule-subtree-chains () - "Optimized scheduler: Ignores stale buffer ranges to ensure correct dependency flow." + "Standard Gortium scheduler: Correctly calculates Finish-to-Start dependencies." (interactive) (message "=== Starting Gortium Scheduler ===") - ;; 1. Global deactivations to prevent the "Parser Error" - (let ((org-element-use-cache nil) - (all-tasks '()) + (let ((all-tasks '()) (task-end-times (make-hash-table :test 'equal)) - (start-time (current-time))) + (start-time (current-time)) + (org-element-use-cache nil)) ;; Disable buggy cache - ;; 2. COLLECTION + ;; 1. COLLECT (org-map-entries (lambda () (when (org-get-todo-state) @@ -2512,7 +2540,7 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (setq all-tasks (nreverse all-tasks)) - ;; 3. THE LOOP + ;; 2. ITERATE (let* ((remaining all-tasks) (limit (* 20 (length remaining))) (iter 0)) @@ -2524,7 +2552,6 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (pcase-let ((`(,buf ,pos ,id ,effort ,blocker ,fixed ,sched ,offset) task)) (let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times)) (has-blocker (and blocker (not (string-empty-p (s-trim blocker))))) - ;; A task is ready if it's FIXED or all blockers are in the task-end-times map (is-fixed (string-equal fixed "t")) (ready (or is-fixed (not has-blocker) blocker-end))) @@ -2532,10 +2559,15 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (with-current-buffer buf (org-element-with-disabled-cache (let* ((off-days (if (stringp offset) (string-to-number offset) 0)) + ;; CRITICAL: For fixed tasks, base-start is THEIR start. + ;; For dependent tasks, base-start is the blocker's END. (base-start (cond (is-fixed (or (gortium/org--get-range-start pos) sched (current-time))) - (t (or blocker-end sched (current-time))))) + (t (or blocker-end (or sched (current-time)))))) + (final-start (if is-fixed base-start - (gortium/internal--snap-to-working-hours (time-add base-start (days-to-time off-days))))) + (gortium/internal--snap-to-working-hours + (time-add base-start (days-to-time off-days))))) + (span (gortium/internal--calculate-task-span final-start effort)) (final-end (car span)) (wknd (cadr span))) @@ -2544,10 +2576,11 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (push task done-this-loop)))))))) (setq remaining (cl-set-difference remaining done-this-loop)))) + ;; 3. CLEANUP + (setq org-element-use-cache t) (org-element-cache-reset 'all) (message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter)))) - ;; --------------------------------------------- (defun gortium/org-ensure-task-properties () "Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.) diff --git a/doom/.config/doom/config.el b/doom/.config/doom/config.el index 48768bc..49ecdea 100644 --- a/doom/.config/doom/config.el +++ b/doom/.config/doom/config.el @@ -339,8 +339,8 @@ ((org-agenda-span 60) (org-agenda-start-day "2026-01-29") (org-agenda-overriding-header "📅 Installation Bombardier") - (org-agenda-prefix-format "%12t") ;; reserve time space - (org-agenda-todo-keyword-format " %-12s ") ;; fixed-width TODO + (org-agenda-prefix-format " %12t") ;; reserve time space + (org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO (org-agenda-tags-column -100) ;; right-align tags (org-agenda-time-grid nil) ))) @@ -1594,6 +1594,29 @@ If FILE is nil, refile in the current file." (nthcdr 3 (decode-time next))))))) (t t1)))) +(defun gortium/org--get-range-end (pos) + "Extract the END timestamp from an existing range like -- at POS. +POS can be a marker or a cons cell (file . position)." + (let ((marker (if (markerp pos) pos + (let ((file (car pos)) + (p (cdr pos))) + (with-current-buffer (find-file-noselect file) + (copy-marker p)))))) + (with-current-buffer (marker-buffer marker) + (save-excursion + (goto-char marker) + (org-back-to-heading t) + (let ((subtree-end (save-excursion (org-end-of-subtree t) (point))) + (end-time nil)) + (save-restriction + (narrow-to-region (point) subtree-end) + (goto-char (point-min)) + (when (re-search-forward "<[^>]+>--<\\([^>]+\\)>" nil t) + (condition-case nil + (setq end-time (org-time-string-to-time (match-string 1))) + (error nil)))) + end-time))))) + (defun gortium/org--get-range-start (pos) "Extract the start timestamp from an existing range like --. Returns nil if no range found (safe, non-blocking)." @@ -1667,7 +1690,7 @@ Returns nil if no range found (safe, non-blocking)." ;; Helper: Find dependency end time ;; ------------------------------------------------------------ (defun gortium/internal--get-blocker-end (blocker-str task-end-map) - "Return latest end time only if blockers are DONE or have been recalculated." + "Return the latest end time of all blockers if they are all resolved." (let ((clean (s-trim (format "%s" blocker-str))) (latest-time nil) (all-resolved t)) @@ -1675,23 +1698,29 @@ Returns nil if no range found (safe, non-blocking)." (not (string-empty-p (s-trim (match-string 1 clean))))) (dolist (tid (split-string (match-string 1 clean) "[ ,]+" t)) (let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid)) - (pos (org-id-find clean-id t)) + (pos (org-id-find clean-id t)) ;; Returns marker OR (file . pos) (computed-end (gethash clean-id task-end-map)) (blocker-end (cond - ;; 1) Use the new time we just calculated in this run (Priority!) (computed-end computed-end) - - ;; 2) If it's DONE, use the CLOSED timestamp - ((and pos (org-entry-get pos "CLOSED")) - (org-time-string-to-time (org-entry-get pos "CLOSED"))) - - ;; 3) If it's FIXED, use the existing range/scheduled time - ((and pos (string-equal "t" (org-entry-get pos "FIXED"))) - (or (gortium/org--get-range-start pos) ;; Note: This needs range end logic, but start is a fallback - (org-get-scheduled-time pos))) - - ;; Otherwise: We MUST wait for this blocker to be recalculated + + ;; Use a temporary buffer context to check properties if pos is a list + ((and pos (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) + (with-current-buffer (marker-buffer m) + (org-with-point-at m + (cond + ((org-entry-get nil "CLOSED") + (org-time-string-to-time (org-entry-get nil "CLOSED"))) + ((string-equal "t" (org-entry-get nil "FIXED")) + (gortium/org--get-range-end m)) + (t nil)))))) + ;; This captures the result of the let/with-current-buffer block + (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos)))))) + (with-current-buffer (marker-buffer m) + (org-with-point-at m + (if (org-entry-get nil "CLOSED") + (org-time-string-to-time (org-entry-get nil "CLOSED")) + (gortium/org--get-range-end m)))))) (t nil)))) (if blocker-end @@ -1831,17 +1860,16 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." ;; --- MAIN SCHEDULER --- (defun gortium/org-schedule-subtree-chains () - "Optimized scheduler: Ignores stale buffer ranges to ensure correct dependency flow." + "Standard Gortium scheduler: Correctly calculates Finish-to-Start dependencies." (interactive) (message "=== Starting Gortium Scheduler ===") - ;; 1. Global deactivations to prevent the "Parser Error" - (let ((org-element-use-cache nil) - (all-tasks '()) + (let ((all-tasks '()) (task-end-times (make-hash-table :test 'equal)) - (start-time (current-time))) + (start-time (current-time)) + (org-element-use-cache nil)) ;; Disable buggy cache - ;; 2. COLLECTION + ;; 1. COLLECT (org-map-entries (lambda () (when (org-get-todo-state) @@ -1858,7 +1886,7 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (setq all-tasks (nreverse all-tasks)) - ;; 3. THE LOOP + ;; 2. ITERATE (let* ((remaining all-tasks) (limit (* 20 (length remaining))) (iter 0)) @@ -1870,7 +1898,6 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (pcase-let ((`(,buf ,pos ,id ,effort ,blocker ,fixed ,sched ,offset) task)) (let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times)) (has-blocker (and blocker (not (string-empty-p (s-trim blocker))))) - ;; A task is ready if it's FIXED or all blockers are in the task-end-times map (is-fixed (string-equal fixed "t")) (ready (or is-fixed (not has-blocker) blocker-end))) @@ -1878,10 +1905,15 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (with-current-buffer buf (org-element-with-disabled-cache (let* ((off-days (if (stringp offset) (string-to-number offset) 0)) + ;; CRITICAL: For fixed tasks, base-start is THEIR start. + ;; For dependent tasks, base-start is the blocker's END. (base-start (cond (is-fixed (or (gortium/org--get-range-start pos) sched (current-time))) - (t (or blocker-end sched (current-time))))) + (t (or blocker-end (or sched (current-time)))))) + (final-start (if is-fixed base-start - (gortium/internal--snap-to-working-hours (time-add base-start (days-to-time off-days))))) + (gortium/internal--snap-to-working-hours + (time-add base-start (days-to-time off-days))))) + (span (gortium/internal--calculate-task-span final-start effort)) (final-end (car span)) (wknd (cadr span))) @@ -1890,10 +1922,11 @@ Returns list of task IDs involved in cycles, or nil if no cycles found." (push task done-this-loop)))))))) (setq remaining (cl-set-difference remaining done-this-loop)))) + ;; 3. CLEANUP + (setq org-element-use-cache t) (org-element-cache-reset 'all) (message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter)))) - ;; --------------------------------------------- (defun gortium/org-ensure-task-properties () "Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.)