diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-12 09:53:28 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-12 09:53:28 -0500 |
| commit | ee73da1046f0a89b90f3fe2705901fb70cdb427a (patch) | |
| tree | 6163e84c26cb778a83b84e56236b6b672d1e1d6d | |
| parent | c53c75a967ee09e38105d810770103308cd85793 (diff) | |
| download | dotemacs-ee73da1046f0a89b90f3fe2705901fb70cdb427a.tar.gz dotemacs-ee73da1046f0a89b90f3fe2705901fb70cdb427a.zip | |
fix(ui-navigation): correct cj/undo-kill-buffer off-by-one on plain invocation
(interactive "p") forces arg >= 1, so the old (if arg (nth arg ...) (car ...)) always took the nth branch and plain M-S-z re-opened the second-most-recently-killed file. Index with (nth (1- arg) ...) so a numeric prefix is 1-based and no prefix re-opens the most recent. The two undo-kill tests now exercise the real no-prefix path (arg=1 reopens the first) and a 1-based numeric prefix; both failed against the bug and pass after. Filed a follow-up for a separate delq-vs-delete skip-visited defect found in the same function.
| -rw-r--r-- | modules/ui-navigation.el | 7 | ||||
| -rw-r--r-- | tests/test-ui-navigation-split-follow-undo-kill.el | 16 | ||||
| -rw-r--r-- | todo.org | 57 |
3 files changed, 17 insertions, 63 deletions
diff --git a/modules/ui-navigation.el b/modules/ui-navigation.el index f1324c16..f0d2ef52 100644 --- a/modules/ui-navigation.el +++ b/modules/ui-navigation.el @@ -160,7 +160,9 @@ This function won't work with more than one split window." ;; UNDO KILL BUFFER (defun cj/undo-kill-buffer (arg) - "Re-open the last buffer killed. With ARG, re-open the nth buffer." + "Re-open the last buffer killed. +With numeric prefix ARG, re-open the ARGth most-recently-killed file +\(1-based, so no prefix re-opens the most recent)." (interactive "p") (require 'recentf) (unless recentf-mode @@ -178,8 +180,7 @@ This function won't work with more than one split window." buffer-files-list) (when recently-killed-list (find-file - (if arg (nth arg recently-killed-list) - (car recently-killed-list)))))) + (nth (1- arg) recently-killed-list))))) (keymap-global-set "M-S-z" #'cj/undo-kill-buffer) ;; was M-Z, overrides zap-to-char ;; ---------------------------- Undo Layout Changes ---------------------------- diff --git a/tests/test-ui-navigation-split-follow-undo-kill.el b/tests/test-ui-navigation-split-follow-undo-kill.el index 74c1e2fc..b1d99020 100644 --- a/tests/test-ui-navigation-split-follow-undo-kill.el +++ b/tests/test-ui-navigation-split-follow-undo-kill.el @@ -54,8 +54,9 @@ ;;; cj/undo-kill-buffer -(ert-deftest test-ui-navigation-undo-kill-buffer-opens-most-recent () - "Normal: with no arg, opens the head of recentf-list that isn't currently visited." +(ert-deftest test-ui-navigation-undo-kill-buffer-no-prefix-opens-most-recent () + "Normal: no prefix (arg=1, the value `\"p\"' yields) opens the most-recent +non-visited entry, not the second." (let ((opened nil) (recentf-mode t) (recentf-list '("/tmp/dead.org" "/tmp/alive.txt"))) @@ -71,12 +72,12 @@ ((symbol-function 'find-file) (lambda (f) (setq opened f)))) (unwind-protect - (cj/undo-kill-buffer 0) + (cj/undo-kill-buffer 1) (when (get-buffer "*test-alive*") (kill-buffer "*test-alive*")))) (should (equal opened "/tmp/dead.org")))) -(ert-deftest test-ui-navigation-undo-kill-buffer-honors-numeric-arg () - "Normal: with N=1, opens the second non-visited entry from recentf-list." +(ert-deftest test-ui-navigation-undo-kill-buffer-numeric-arg-is-one-based () + "Normal: a numeric prefix is 1-based — N=2 opens the second non-visited entry." (let ((opened nil) (recentf-mode t) (recentf-list '("/tmp/a.org" "/tmp/b.org" "/tmp/c.org"))) @@ -85,10 +86,7 @@ ((symbol-function 'buffer-list) (lambda (&rest _) nil)) ((symbol-function 'find-file) (lambda (f) (setq opened f)))) - ;; cj/undo-kill-buffer takes a prefix `arg' and indexes into the list - ;; with `(nth arg ...)` when arg is non-nil. Passing 1 grabs the 2nd - ;; entry. - (cj/undo-kill-buffer 1)) + (cj/undo-kill-buffer 2)) (should (equal opened "/tmp/b.org")))) (ert-deftest test-ui-navigation-undo-kill-buffer-no-op-when-list-empty () @@ -761,8 +761,12 @@ Tie this into the existing coverage work: ** TODO [#B] Messenger window/key unification :feature: Spec: [[file:docs/design/messenger-unification-spec.org][messenger-unification-spec.org]] (Draft, 2026-06-11). One library (=cj-messenger-lib.el=) gives every messenger the same shape: chat windows rise from the bottom (the signel rule, generalized), C-c C-c confirms, C-c C-k cancels, C-c C-a attaches — dispatched per backend through a registry + minor mode. Signel already conforms (reference backend); telega and slack join in phases 2-3; ERC later. All eight decisions settled 2026-06-11 (cancel closes an idle window; telega's filter-cancel shadow accepted; slack rooms join the bottom rule). Spec held open — Craig has more ideas to fold in before it's marked Ready. -** TODO [#B] cj/undo-kill-buffer off-by-one on plain invocation :bug:quick:solo: -=modules/ui-navigation.el:181= — =(interactive "p")= makes arg always ≥1, and the body does =(if arg (nth arg list) (car list))=, so the nth branch always runs and plain M-S-z reopens the SECOND-most-recently-killed file. The existing test passes 0 explicitly, masking it. Fix the indexing (=(interactive "P")= + =prefix-numeric-value=, or =nth (1- arg)=) and fix the test to cover the no-prefix path. From the 2026-06 config audit. +** DONE [#B] cj/undo-kill-buffer off-by-one on plain invocation :bug:quick:solo: +CLOSED: [2026-06-12 Fri] +Fixed in =modules/ui-navigation.el=: indexing is now =(nth (1- arg) ...)=, so a numeric prefix is 1-based and plain M-S-z re-opens the most-recently-killed file (was opening the second). Rewrote the two undo-kill tests to exercise the real no-prefix path (arg=1 -> first) and a 1-based numeric prefix; both red against the bug, green after. Full suite: no new failures (the 4 pre-existing dupre-theme failures are the separate task below). Live-reloaded into the daemon. + +** TODO [#C] cj/undo-kill-buffer skip-visited uses delq (eq) on path strings :bug:quick:solo: +=modules/ui-navigation.el= — the visited-file filter calls =(delq buf-file recently-killed-list)= where =buf-file= is a fresh string from =expand-file-name=, never =eq= to the =recentf-list= entries, so already-open files are never skipped (the skip logic is dead). Use =delete= (equal-based). Found 2026-06-12 while fixing the off-by-one above; the two bugs cancel exactly when one file is open, which is why it went unnoticed. ** TODO [#B] reconcile-open-repos skips any repo with a dot in its name :bug:solo: =modules/reconcile-open-repos.el:174= — discovery regexp ="^[^.]+$"= matches only dot-free names, so =~/code/mcp.el=, =capture.el=, =google-contacts.el=, =auto-dim-other-buffers.el= etc. are never reconciled while M-P still reports "Complete." Replace with =directory-files-no-dot-files-regexp= + a hidden-dir check; add a regression test with a dotted repo name. From the 2026-06 config audit. @@ -1115,55 +1119,6 @@ Full findings delivered as handoffs to each repo's inbox/ (2026-06-12-0057-from- - chime (10 findings; suite green; the 2026-06-11 watchdog handoff VERIFIED landed in full): lookahead vars never injected into the async child (documented feature silently capped at 8 days — one-line fix); days-until-event nil crash on mixed timed/all-day events; stale-callback race after watchdog interrupt (generation counter needed); default test run prints green integration banner over "Ran 0 tests". - emacs-wttrin (10 findings; ~56 ERT files, CI; the face-flood reminder VERIFIED resolved — test 8f3c770 + fix c5e5e1d, reminder cleared from notes.org): no network timeouts (wttr.in stalls hang the loading buffer); error-path response-buffer leak; non-favorite cache never expires; 17 unreleased commits incl. two features — tag v0.4.0. -** PROJECT [#B] Implement ai-kb :feature: -Build v1 of the AI knowledge base per [[file:docs/design/ai-kb.org][docs/design/ai-kb.org]] (Ready; six reviews incorporated, all decisions resolved 2026-05-24). Step 1 splits into 1a (the safe write path — minimum usable) and 1b (retrieval, maintenance, push), since =remember= depends on =index=+=lint= and the adapter depends on =remember=. Step 2 is the Emacs layer: a full org-roam profile on switch, the human-edit safety model (same write path as the agent), and the browsing surface. Step 3 and the LLM-Wiki layer are vNext. Children are ordered by build sequence; the server bootstrap is the prerequisite. - -*** TODO [#B] ai-kb bare repo on cjennings.net -Prerequisite, one-time server bootstrap (not doable by the local script): =sudo git init --bare /var/git/ai-kb.git= + chown on cjennings.net. Leave the github-mirror hook OFF — this repo is private. Required before every per-machine clone. - -*** TODO [#B] ai-kb store + contract + seed -Step 1a. Clone =git@cjennings.net:ai-kb.git= to =~/.local/share/ai-kb=. Author =AGENT_CONTRACT.org= (canonical repo-resident contract: node format, write protocol, operations, routing) and seed =index.org= + a README/index node with a generated =:ID:=. Node format per spec — a *required* one-line =:SUMMARY:= (the index/query read it straight, no inference/LLM), provenance (=:CREATED_BY:/:CONFIDENCE:/:VISIBILITY:/:SOURCE:/:STATUS:=), =:PROJECTS:= slugs, type filetags, relation labels. Define the durable external-pointer format as *ID-first*: =ai-kb: <Title> (<UUID>)=, resolved by ID with title fallback (filenames can change in curation). - -*** TODO [#B] ai-kb CLI 1a: index, lint, remember, doctor -Step 1a. Shell wrapper calling Emacs for org work — =emacsclient= when a daemon is up, =emacs --batch= fallback, lint+index in *one* invocation per =remember=. =index= regenerates =index.org= from node properties incl. =:SUMMARY:= (never hand-maintained); the index references nodes as plain =Title (UUID)= text, never =[[id:]]= links, and is excluded from the scan so it can't manufacture backlinks or hide orphans. =lint= = org-lint fatal checks + duplicate IDs + broken id-links (excl =raw/= + index) + missing required props (incl =:SUMMARY:=) + bad project slugs + stale/incomplete index + credential scan of nodes *and* =raw/= text files (binaries skipped). =remember= = the write protocol: fetch + =pull --ff-only= (abort on diverge/dirty), write, regenerate index, then run the *full =ai-kb lint=* over the change as the commit gate (not just node org-lint — this is the safety boundary), commit locally, =flock=; no push. =doctor= / =status= = health + push-state + raw-dir-size report (repo, private remote, CLI on PATH, =graphviz= if the map needs it, adapter linked, db buildable, no secrets, "ahead N"/"push failed"/"diverged"); =status= is the fast non-diagnostic mode for the dashboard/nudge. - -*** TODO [#B] claude-rules/ai-kb.md adapter -Step 1a. Global L1 rule in rulesets pointing at the repo-resident =AGENT_CONTRACT.org=: path, routing (T1/T2/T3 tiers; per-project =MEMORY.md= shrinks to ID-first pointers into ai-kb), proactive + contradiction rules, concrete "read the index first" triggers, link-grep recipes, "use =ai-kb remember=, never bypass =ai-kb lint=", one-line nudge on unpushed commits / recorded push rejection. =make install= symlinks it into =~/.claude/rules/=. - -*** TODO [#B] ai-kb provisioning: setup-ai-kb.sh + make ai-kb-init -Step 1a (core; the timer-install line is added with 1b). Idempotent =scripts/setup-ai-kb.sh=: clone (or init+add-remote on first machine), seed, install the CLI on PATH, =ai-kb index=, =ai-kb doctor=. =make ai-kb-init= wraps it. The one-time server bootstrap stays a separate documented step. - -*** TODO [#B] ai-kb Step-1a tests :test: -Write-path: a write with the remote unreachable still commits locally and does not error; =flock= serializes concurrent =remember=; each org-lint *fatal* check (malformed drawer, missing/dup =:ID:=, invalid required property, missing =#+title:=, unparseable org) rejects the commit, a style warning does not; a node missing =:SUMMARY:= fails lint; =remember= aborts the commit when the *full* lint fails (stale index, broken link, secret in a node or =raw/= text file); the credential scan skips binaries. Index: regen from a fixture produces expected entries; an out-of-band node appears only after regen; a node referenced only by =index.org= still reports as an orphan (the index is not a backlink source). Link recipes: backlink (excl =raw/= + index) + forward correct. Provisioning (bats): idempotent, valid =:ID:= + =:SUMMARY:=, =doctor= passes. - -*** TODO [#B] ai-kb CLI 1b: query, curate, sync -Step 1b. =query <context>= with a *testable contract*: plain-text default + =--json=; fields title/ID/summary/projects/status/updated/path + *match reason*; searches index rows + title/tags/properties/body; ranks by lexical score — sum of each matched field's weight, counted once per field: title 100, tag/project/status 50 each, summary 20, body 5; no term-frequency weighting in v1 — with most-recently-updated (=:UPDATED:=) only as the *tie-break* on equal scores (recency alone buries stable old preferences); default max-results; =raw/= paths only as source references; exit codes for no-match / invalid KB / lint-index failure. =show <id-or-title>= (resolve ID-first, print the node) and =backlinks <id>= (excl =raw/= + index) as the inspection primitives the Emacs commands wrap. =curate --dry-run= (four buckets; also flags orphan =raw/= captures and any =raw/= file over 256 KB; destructive ops human-only). =sync= (=org-roam-db-sync= against ai-kb) only when the db is missing/stale or forced. - -*** TODO [#B] ai-kb push timer + failure observability -Step 1b. =ai-kb-push.timer= + =ai-kb-push.service= =systemd --user= units: push only if ahead, ~15 min; installed + =enable --now= by the setup script (add this line to =setup-ai-kb.sh=). A failed push is logged to a state file (=$XDG_STATE_HOME/ai-kb=), never fatal; surfaced by =ai-kb doctor= and the adapter's startup nudge. - -*** TODO [#B] ai-kb-curate workflow in rulesets -Step 1b. =~/code/rulesets/.ai/workflows/ai-kb-curate.org= — human-gated curation: the four buckets, node-count trigger (nudge at 150 nodes, re-fire every +50), =:LAST_CURATED:= rotation, pointer-integrity (merge/supersede changes the canonical ID, so grep inbound =[[id:]]= + =MEMORY.md= =ai-kb: ... (UUID)= refs and repoint before deleting). Surfaced by =ai-kb doctor= + session startup when due. - -*** TODO [#B] ai-kb Step-1b tests :test: -=query --json= returns the specified fields (incl. match reason)/exit-codes on a fixture KB and =raw/= appears only as a source ref; a title match outranks a body-only match with recency only breaking ties (an old preference is not buried under a newer body-only hit); a simulated push failure is recorded to the state file and surfaced by =ai-kb doctor= / =status=. Performance (=:perf= tag): 100- and 1,000-node fixtures keep =index=/=query=/=lint=/=remember= under a stated time budget (catches an accidental per-check Emacs startup or an O(n²) scan). - -*** TODO [#B] Emacs: org-roam ai-kb profile + switch -Step 2. -=org-roam-config.el=: =cj/org-roam-switch-to-ai-kb= / =cj/org-roam-switch-to-personal= install a full org-roam *profile*, not a two-variable swap — dir + =org-roam-ai.db= + =org-roam-file-exclude-regexp= (=raw/= + =index*.org=), and dailies, capture templates, topic/project/recipe find wrappers, and the agenda/refile + completed-task→daily hooks all rescoped or neutralized so ai-kb nodes never leak into personal journals/agenda. Restore everything exactly on exit; re-assert personal state at startup (abnormal-exit safety). =cj/ai-kb-db-sync= syncs only when the db is missing/stale or forced, with a status indicator. - -*** TODO [#B] Emacs: ai-kb edit safety (same write path) -Step 2. An =ai-kb= minor mode whose =after-save-hook= runs the agent's post-write sequence under =flock= — =ai-kb index=, full =ai-kb lint=, commit, push-state update — so a human Emacs edit can't bypass index/lint/commit. One write path for both agent and human. Failure UX: the save always writes to disk and the buffer stays editable (never read-only/blocked); on lint failure it does *not* commit, pops findings to a =*ai-kb-lint*= buffer (no focus steal), and shows the uncommitted-failing state in the modeline + dashboard — Craig fixes and re-saves, a clean save commits. Recursion guard, two layers: the mode's activation predicate excludes =index*.org= + =raw/=, and the pipeline binds a re-entrancy flag (=cj/ai-kb--in-pipeline=) the hook early-returns on; index regen prefers =write-region= over =save-buffer=. - -*** TODO [#B] Emacs: ai-kb browsing surface -Step 2. =cj/ai-kb-dashboard= (status banner: active KB, node count, unpushed commits, push-failure state, curation due, last index/sync), =cj/ai-kb-find-node= (=org-roam-node-find= in the ai-kb profile), =cj/ai-kb-search= (=ai-kb query= or scoped =consult-ripgrep=), =cj/ai-kb-show-node= (resolve ID-first, open), =cj/ai-kb-backlinks= (excl =raw/= + index), =cj/ai-kb-map= (built-in =org-roam-graph= *first* — the profile's exclude regexp already keeps =raw/= + index out of the db, so the graph inherits the right scope; custom DOT export only if project/tag/status filtering proves necessary; =graphviz= dep). Simple wrappers over the CLI primitives where possible. - -*** TODO [#B] Emacs: ai-kb keybindings + which-key -Bind the switch + sync + browsing commands under the =C-c n= roam prefix (e.g. =C-c n a= → ai-kb, =C-c n A= → personal, a small transient for the browsing commands), avoiding the dense existing set; which-key labels. - -*** TODO [#B] Emacs: ai-kb Step-2 ERT tests :test: -Profile: switch installs the ai-kb dir + db + exclude regexp and switch-back restores personal *exactly* — completed-task hook, agenda/refile finalize hook, dailies, and capture templates all untouched by ai-kb while switched; startup re-asserts personal state after a simulated abnormal exit. Edit path: a save in an ai-kb buffer runs index+lint+commit (a bad save surfaces the lint failure rather than committing). Sync runs only when stale. - ** PROJECT [#B] Architecture review follow-up from 2026-05-03 :refactor: High-level pass over =init.el=, =early-init.el=, and all 104 files in |
