Working blocker

This commit is contained in:
2026-02-09 17:22:28 -05:00
parent 6939353f9c
commit dfef1e6dfa
2 changed files with 120 additions and 54 deletions

View File

@@ -470,7 +470,7 @@ change ~org-directory~. It must be set before org loads!
(org-agenda-start-day "2026-01-29") (org-agenda-start-day "2026-01-29")
(org-agenda-overriding-header "📅 Installation Bombardier") (org-agenda-overriding-header "📅 Installation Bombardier")
(org-agenda-prefix-format " %12t") ;; reserve time space (org-agenda-prefix-format " %12t") ;; reserve time space
(org-agenda-todo-keyword-format " %-12s ") ;; fixed-width TODO (org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
(org-agenda-tags-column -100) ;; right-align tags (org-agenda-tags-column -100) ;; right-align tags
(org-agenda-time-grid nil) (org-agenda-time-grid nil)
))) )))
@@ -2248,6 +2248,29 @@ If FILE is nil, refile in the current file."
(nthcdr 3 (decode-time next))))))) (nthcdr 3 (decode-time next)))))))
(t t1)))) (t t1))))
(defun gortium/org--get-range-end (pos)
"Extract the END timestamp from an existing range like <A>--<B> 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) (defun gortium/org--get-range-start (pos)
"Extract the start timestamp from an existing range like <A>--<B>. "Extract the start timestamp from an existing range like <A>--<B>.
Returns nil if no range found (safe, non-blocking)." 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 ;; Helper: Find dependency end time
;; ------------------------------------------------------------ ;; ------------------------------------------------------------
(defun gortium/internal--get-blocker-end (blocker-str task-end-map) (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))) (let ((clean (s-trim (format "%s" blocker-str)))
(latest-time nil) (latest-time nil)
(all-resolved t)) (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))))) (not (string-empty-p (s-trim (match-string 1 clean)))))
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t)) (dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
(let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid)) (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)) (computed-end (gethash clean-id task-end-map))
(blocker-end (blocker-end
(cond (cond
;; 1) Use the new time we just calculated in this run (Priority!)
(computed-end computed-end) (computed-end computed-end)
;; 2) If it's DONE, use the CLOSED timestamp ;; Use a temporary buffer context to check properties if pos is a list
((and pos (org-entry-get pos "CLOSED")) ((and pos (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos))))))
(org-time-string-to-time (org-entry-get pos "CLOSED"))) (with-current-buffer (marker-buffer m)
(org-with-point-at m
;; 3) If it's FIXED, use the existing range/scheduled time (cond
((and pos (string-equal "t" (org-entry-get pos "FIXED"))) ((org-entry-get nil "CLOSED")
(or (gortium/org--get-range-start pos) ;; Note: This needs range end logic, but start is a fallback (org-time-string-to-time (org-entry-get nil "CLOSED")))
(org-get-scheduled-time pos))) ((string-equal "t" (org-entry-get nil "FIXED"))
(gortium/org--get-range-end m))
;; Otherwise: We MUST wait for this blocker to be recalculated (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)))) (t nil))))
(if blocker-end (if blocker-end
@@ -2485,17 +2514,16 @@ Returns list of task IDs involved in cycles, or nil if no cycles found."
;; --- MAIN SCHEDULER --- ;; --- MAIN SCHEDULER ---
(defun gortium/org-schedule-subtree-chains () (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) (interactive)
(message "=== Starting Gortium Scheduler ===") (message "=== Starting Gortium Scheduler ===")
;; 1. Global deactivations to prevent the "Parser Error" (let ((all-tasks '())
(let ((org-element-use-cache nil)
(all-tasks '())
(task-end-times (make-hash-table :test 'equal)) (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 (org-map-entries
(lambda () (lambda ()
(when (org-get-todo-state) (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)) (setq all-tasks (nreverse all-tasks))
;; 3. THE LOOP ;; 2. ITERATE
(let* ((remaining all-tasks) (let* ((remaining all-tasks)
(limit (* 20 (length remaining))) (limit (* 20 (length remaining)))
(iter 0)) (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)) (pcase-let ((`(,buf ,pos ,id ,effort ,blocker ,fixed ,sched ,offset) task))
(let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times)) (let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times))
(has-blocker (and blocker (not (string-empty-p (s-trim blocker))))) (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")) (is-fixed (string-equal fixed "t"))
(ready (or is-fixed (not has-blocker) blocker-end))) (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 (with-current-buffer buf
(org-element-with-disabled-cache (org-element-with-disabled-cache
(let* ((off-days (if (stringp offset) (string-to-number offset) 0)) (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))) (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 (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)) (span (gortium/internal--calculate-task-span final-start effort))
(final-end (car span)) (final-end (car span))
(wknd (cadr 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)))))))) (push task done-this-loop))))))))
(setq remaining (cl-set-difference remaining 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) (org-element-cache-reset 'all)
(message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter)))) (message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter))))
;; --------------------------------------------- ;; ---------------------------------------------
(defun gortium/org-ensure-task-properties () (defun gortium/org-ensure-task-properties ()
"Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.) "Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.)

View File

@@ -340,7 +340,7 @@
(org-agenda-start-day "2026-01-29") (org-agenda-start-day "2026-01-29")
(org-agenda-overriding-header "📅 Installation Bombardier") (org-agenda-overriding-header "📅 Installation Bombardier")
(org-agenda-prefix-format " %12t") ;; reserve time space (org-agenda-prefix-format " %12t") ;; reserve time space
(org-agenda-todo-keyword-format " %-12s ") ;; fixed-width TODO (org-agenda-todo-keyword-format " %-5s") ;; fixed-width TODO
(org-agenda-tags-column -100) ;; right-align tags (org-agenda-tags-column -100) ;; right-align tags
(org-agenda-time-grid nil) (org-agenda-time-grid nil)
))) )))
@@ -1594,6 +1594,29 @@ If FILE is nil, refile in the current file."
(nthcdr 3 (decode-time next))))))) (nthcdr 3 (decode-time next)))))))
(t t1)))) (t t1))))
(defun gortium/org--get-range-end (pos)
"Extract the END timestamp from an existing range like <A>--<B> 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) (defun gortium/org--get-range-start (pos)
"Extract the start timestamp from an existing range like <A>--<B>. "Extract the start timestamp from an existing range like <A>--<B>.
Returns nil if no range found (safe, non-blocking)." 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 ;; Helper: Find dependency end time
;; ------------------------------------------------------------ ;; ------------------------------------------------------------
(defun gortium/internal--get-blocker-end (blocker-str task-end-map) (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))) (let ((clean (s-trim (format "%s" blocker-str)))
(latest-time nil) (latest-time nil)
(all-resolved t)) (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))))) (not (string-empty-p (s-trim (match-string 1 clean)))))
(dolist (tid (split-string (match-string 1 clean) "[ ,]+" t)) (dolist (tid (split-string (match-string 1 clean) "[ ,]+" t))
(let* ((clean-id (replace-regexp-in-string "[\"']\\|id:" "" tid)) (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)) (computed-end (gethash clean-id task-end-map))
(blocker-end (blocker-end
(cond (cond
;; 1) Use the new time we just calculated in this run (Priority!)
(computed-end computed-end) (computed-end computed-end)
;; 2) If it's DONE, use the CLOSED timestamp ;; Use a temporary buffer context to check properties if pos is a list
((and pos (org-entry-get pos "CLOSED")) ((and pos (let ((m (if (markerp pos) pos (set-marker (make-marker) (cdr pos) (find-file-noselect (car pos))))))
(org-time-string-to-time (org-entry-get pos "CLOSED"))) (with-current-buffer (marker-buffer m)
(org-with-point-at m
;; 3) If it's FIXED, use the existing range/scheduled time (cond
((and pos (string-equal "t" (org-entry-get pos "FIXED"))) ((org-entry-get nil "CLOSED")
(or (gortium/org--get-range-start pos) ;; Note: This needs range end logic, but start is a fallback (org-time-string-to-time (org-entry-get nil "CLOSED")))
(org-get-scheduled-time pos))) ((string-equal "t" (org-entry-get nil "FIXED"))
(gortium/org--get-range-end m))
;; Otherwise: We MUST wait for this blocker to be recalculated (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)))) (t nil))))
(if blocker-end (if blocker-end
@@ -1831,17 +1860,16 @@ Returns list of task IDs involved in cycles, or nil if no cycles found."
;; --- MAIN SCHEDULER --- ;; --- MAIN SCHEDULER ---
(defun gortium/org-schedule-subtree-chains () (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) (interactive)
(message "=== Starting Gortium Scheduler ===") (message "=== Starting Gortium Scheduler ===")
;; 1. Global deactivations to prevent the "Parser Error" (let ((all-tasks '())
(let ((org-element-use-cache nil)
(all-tasks '())
(task-end-times (make-hash-table :test 'equal)) (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 (org-map-entries
(lambda () (lambda ()
(when (org-get-todo-state) (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)) (setq all-tasks (nreverse all-tasks))
;; 3. THE LOOP ;; 2. ITERATE
(let* ((remaining all-tasks) (let* ((remaining all-tasks)
(limit (* 20 (length remaining))) (limit (* 20 (length remaining)))
(iter 0)) (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)) (pcase-let ((`(,buf ,pos ,id ,effort ,blocker ,fixed ,sched ,offset) task))
(let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times)) (let* ((blocker-end (gortium/internal--get-blocker-end blocker task-end-times))
(has-blocker (and blocker (not (string-empty-p (s-trim blocker))))) (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")) (is-fixed (string-equal fixed "t"))
(ready (or is-fixed (not has-blocker) blocker-end))) (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 (with-current-buffer buf
(org-element-with-disabled-cache (org-element-with-disabled-cache
(let* ((off-days (if (stringp offset) (string-to-number offset) 0)) (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))) (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 (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)) (span (gortium/internal--calculate-task-span final-start effort))
(final-end (car span)) (final-end (car span))
(wknd (cadr 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)))))))) (push task done-this-loop))))))))
(setq remaining (cl-set-difference remaining 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) (org-element-cache-reset 'all)
(message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter)))) (message "=== Scheduler completed (%d tasks, %d iterations) ===" (length all-tasks) iter))))
;; --------------------------------------------- ;; ---------------------------------------------
(defun gortium/org-ensure-task-properties () (defun gortium/org-ensure-task-properties ()
"Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.) "Iterate through all tasks (TODO, NEXT, STRT, WAIT, HOLD, DONE, etc.)