perf: add O(returned) raft-log-from log accessor to std/raft

ober

60f51d6b8edf5d3d577adbaf5388541411a473ef

diff --git a/data/api-signatures.sexp b/data/api-signatures.sexp
index 941d0fc..d6e4c69 100644
--- a/data/api-signatures.sexp
+++ b/data/api-signatures.sexp
@@ -5529,8 +5529,9 @@
      ("exports" "log-entry-command" "log-entry-index"
       "log-entry-term" "log-entry?" "make-raft-cluster"
       "make-raft-node" "raft-cluster-leader" "raft-cluster-nodes"
-      "raft-commit-index" "raft-leader?" "raft-log"
-      "raft-message->wire" "raft-message-from-wire"
+       "raft-commit-index" "raft-leader?" "raft-log"
+       "raft-log-from" "raft-log-last-index"
+       "raft-message->wire" "raft-message-from-wire"
       "raft-message-wire-self-test" "raft-node-add-peer!"
       "raft-node-inbox" "raft-node-peers" "raft-node-peers-set!"
       "raft-propose!" "raft-start!" "raft-state" "raft-stop!"
@@ -15198,8 +15199,10 @@
    ("raft-cluster-leader" "(std raft)")
    ("raft-cluster-nodes" "(std raft)")
    ("raft-commit-index" "(std raft)")
-   ("raft-leader?" "(std raft)") ("raft-log" "(std raft)")
-   ("raft-message->wire" "(std raft)")
+    ("raft-leader?" "(std raft)") ("raft-log" "(std raft)")
+    ("raft-log-from" "(std raft)")
+    ("raft-log-last-index" "(std raft)")
+    ("raft-message->wire" "(std raft)")
    ("raft-message-from-wire" "(std raft)")
    ("raft-message-wire-self-test" "(std raft)")
    ("raft-node-add-peer!" "(std raft)")
diff --git a/data/changelog.sexp b/data/changelog.sexp
index 9292a0e..df77eda 100644
--- a/data/changelog.sexp
+++ b/data/changelog.sexp
@@ -2,7 +2,19 @@
    .
    "Machine-readable changelog of Jerboa API drift. Consumers (LLM tooling, lints, jerboa_verify) use this to invalidate stale recommendations and to suggest migrations when a symbol is renamed or relocated.")
   ("entries"
-    (("added" ("JERBOA_MCP_LOG" "JERBOA_MCP_LOG_FILE"
+    (("added" ("raft-log-from" "raft-log-last-index"))
+      ("date" . "2026-07-22")
+      ("modules_added")
+      ("moved")
+      ("notes"
+        .
+        "(std raft) adds raft-log-from and raft-log-last-index, backed by a parallel log-index vector kept in sync on every log mutation (append/truncate). raft-log-from returns the log suffix from a start index in O(returned) time so consumers such as the jerboa-db apply fiber no longer scan the full raft log on every 50 ms poll. The consensus log list stays the source of truth; the index vector is read-only inspection state.")
+      ("removed")
+      ("renamed")
+      ("tier_changes")
+      ("tools_added")
+      ("version" . "v0.2.5"))
+     (("added" ("JERBOA_MCP_LOG" "JERBOA_MCP_LOG_FILE"
                "JERBOA_MCP_SLOW_MS"))
       ("date" . "2026-07-19")
       ("modules_added")
diff --git a/lib/std/raft.ss b/lib/std/raft.ss
index 1fddced..1a42550 100644
--- a/lib/std/raft.ss
+++ b/lib/std/raft.ss
@@ -23,6 +23,8 @@
     raft-term
     raft-state
     raft-log
+    raft-log-from
+    raft-log-last-index
     raft-commit-index
     log-entry?
     log-entry-index
@@ -333,6 +335,7 @@
       (mutable current-term)     ;; current term
       (mutable voted-for)        ;; candidate voted for in current term (#f if none)
       (mutable log)              ;; list of log-entry (index 1-based, stored in order)
+      (mutable log-vec)          ;; vector of log-entry indexed by (log-index - 1); read accelerator for raft-log-from (kept in sync with log by set-log!)
       (mutable commit-index)     ;; highest committed log index
       (mutable last-applied)     ;; highest applied log index
       (mutable state)            ;; 'follower | 'candidate | 'leader
@@ -355,6 +358,7 @@
       0          ;; current-term
       #f         ;; voted-for
       '()        ;; log
+      (vector)   ;; log-vec
       0          ;; commit-index
       0          ;; last-applied
       'follower  ;; state
@@ -378,8 +382,41 @@
   (def (raft-commit-index node) (raft-node-commit-index node))
   (def (raft-leader? node) (eq? (raft-node-state node) 'leader))
 
+  ;; raft-log-from : raft-node integer -> list
+  ;; Returns the log entries whose index is >= start-index, in log order, in
+  ;; O(returned) time.  Uses the parallel log-vec index rather than scanning the
+  ;; log list from the head, so callers that only need the unapplied tail (e.g.
+  ;; the jerboa-db apply fiber) avoid an O(log-size) scan on every poll.
+  ;; Log indices are 1-based and contiguous, so vector position = index - 1.
+  (def (raft-log-from node start-index)
+    (let* ([vec (raft-node-log-vec node)]
+           [len (vector-length vec)]
+           [start-pos (if (< start-index 1) 0 (- start-index 1))])
+      (if (>= start-pos len)
+        '()
+        (let loop ([i (- len 1)] [acc '()])
+          (if (< i start-pos)
+            acc
+            (loop (- i 1) (cons (vector-ref vec i) acc)))))))
+
+  ;; raft-log-last-index : raft-node -> integer
+  ;; Highest log index (0 when the log is empty), in O(1).  Because indices are
+  ;; contiguous from 1, the last index equals the entry count.
+  (def (raft-log-last-index node)
+    (vector-length (raft-node-log-vec node)))
+
   ;; ========== Utility Helpers ==========
 
+  ;; set-log! : raft-node list -> void
+  ;; Single mutation point for the consensus log.  Replaces the log list AND
+  ;; rebuilds the parallel log-vec index so raft-log-from / raft-log-last-index
+  ;; stay consistent on every append / truncate.  All log mutations MUST go
+  ;; through here.  The log list remains the consensus source of truth; log-vec
+  ;; is only read by the inspection accessors above.
+  (def (set-log! node entries)
+    (raft-node-log-set! node entries)
+    (raft-node-log-vec-set! node (list->vector entries)))
+
   (def (log-last-index node)
     (let ([log (raft-node-log node)])
       (if (null? log) 0 (log-entry-index (car (last-pair log))))))
@@ -703,7 +740,7 @@
                                       (<= (log-entry-index e) prev-idx))
                                     existing)])
                  ;; Append entries from leader
-                 (raft-node-log-set! node (append keep entries))))
+                 (set-log! node (append keep entries))))
              ;; Update commit index
              (when (> leader-commit (raft-node-commit-index node))
                (raft-node-commit-index-set! node
@@ -753,7 +790,7 @@
                            new-index
                            (raft-node-current-term node)
                            cmd)])
-             (raft-node-log-set! node
+             (set-log! node
                (append (raft-node-log node) (list entry)))
              (when reply-ch
                (channel-put reply-ch (cons 'ok new-index)))
diff --git a/tests/test-raft.ss b/tests/test-raft.ss
index 961ac2a..196d65c 100644
--- a/tests/test-raft.ss
+++ b/tests/test-raft.ss
@@ -139,5 +139,68 @@
       #t)
     (raft-stop! node)))
 
+;; Test 11: raft-log-from returns the correct suffix and raft-log-last-index is O(1)-correct
+(let ([cluster (make-raft-cluster 1)])
+  (let ([node (car (raft-cluster-nodes cluster))])
+    (raft-start! node)
+    (sleep (make-time 'time-duration 400000000 0))
+    (when (raft-leader? node)
+      (raft-propose! node 'cmd-a)
+      (raft-propose! node 'cmd-b)
+      (raft-propose! node 'cmd-c)
+      (sleep (make-time 'time-duration 100000000 0))
+      (test "log-from-1-is-full-log"
+        (map log-entry-index (raft-log-from node 1))
+        (map log-entry-index (raft-log node)))
+      (test "log-from-2-is-suffix"
+        (map log-entry-index (raft-log-from node 2))
+        '(2 3))
+      (test "log-from-3-is-tail"
+        (map log-entry-index (raft-log-from node 3))
+        '(3))
+      (test "log-from-past-end-empty"
+        (raft-log-from node 4)
+        '())
+      (test "log-from-0-is-full-log"
+        (map log-entry-index (raft-log-from node 0))
+        '(1 2 3))
+      (test "log-from-suffix-commands"
+        (map log-entry-command (raft-log-from node 2))
+        '(cmd-b cmd-c))
+      (test "log-last-index"
+        (raft-log-last-index node)
+        3))
+    (raft-stop! node)))
+
+;; Test 12: raft-log-from stays consistent with raft-log on every node of a
+;; running cluster.  Followers build their log via the AppendEntries
+;; truncate+append path, so this exercises set-log! on both mutation sites.
+(let ([cluster (make-raft-cluster 3)])
+  (for-each raft-start! (raft-cluster-nodes cluster))
+  (let wait ([attempts 0])
+    (if (or (>= attempts 20) (raft-cluster-leader cluster))
+      (void)
+      (begin (sleep (make-time 'time-duration 50000000 0)) (wait (+ attempts 1)))))
+  (let ([leader (raft-cluster-leader cluster)])
+    (when leader
+      (raft-propose! leader 'tx-1)
+      (raft-propose! leader 'tx-2)
+      (raft-propose! leader 'tx-3)
+      (sleep (make-time 'time-duration 500000000 0))
+      (for-each
+        (lambda (n)
+          (test "cluster-log-from-consistent"
+            (map log-entry-index (raft-log-from n 1))
+            (map log-entry-index (raft-log n)))
+          (test "cluster-log-from-suffix-consistent"
+            (map log-entry-index (raft-log-from n 2))
+            (map log-entry-index
+              (filter (lambda (e) (>= (log-entry-index e) 2)) (raft-log n))))
+          (test "cluster-log-last-index-consistent"
+            (raft-log-last-index n)
+            (length (raft-log n))))
+        (raft-cluster-nodes cluster))))
+  (for-each raft-stop! (raft-cluster-nodes cluster)))
+
 (printf "~%Results: ~a passed, ~a failed~%" pass fail)
 (when (> fail 0) (exit 1))