diff options
| -rw-r--r-- | AGENTS.md | 4 | ||||
| -rw-r--r-- | README.md | 37 | ||||
| -rw-r--r-- | config.org | 816 | ||||
| -rw-r--r-- | docs/plans/2026-04-07-people-rolodex-implementation.md | 239 | ||||
| -rw-r--r-- | name-dictionary.el | 25 |
5 files changed, 619 insertions, 502 deletions
@@ -9,7 +9,7 @@ This repository is for the creation and maintenance of Emacs 30 configuration fi - `config.org` is the single hand-edited source of truth for the configuration. - `init.el` and `early-init.el` are generated from `config.org` and should not be edited directly. - Structure the literate config by startup responsibility first, then by workflow domain. -- The current Org workflow lives under `~/org/` with `daily/`, `projects/`, `areas/`, `areas/people/`, `resources/`, and `archives/`. +- The current Org workflow lives under `~/org/` with `daily/`, `projects/`, `areas/`, `people.org`, `resources/`, and `archives/`. - Daily notes stay as plain Org files under `~/org/daily/`; longer-lived notes are handled through Denote in the same root. - Agenda files are discovered dynamically from the project, area, and resource directories rather than from a fixed file list. - The config also includes a small gptel workflow that uses GitHub Copilot as the backend. @@ -49,4 +49,4 @@ This repository is for the creation and maintenance of Emacs 30 configuration fi - `README.md` must describe the current configuration truthfully. Do not leave stale documentation behind and do not document planned behavior as current behavior. - Before claiming a change is complete or asking to commit it, review whether `README.md` needs an update; if it does not, say so explicitly in the summary. - Update AGENTS.md when the repo workflow, Org layout, or verification expectations materially change. -- Keep AGENTS.md concise and update it only when the repository structure or working rules actually change.
\ No newline at end of file +- Keep AGENTS.md concise and update it only when the repository structure or working rules actually change. @@ -39,7 +39,7 @@ The note system lives under `~/org/` and is organized like this: - `daily/` for plain daily Org files. - `projects/` for project notes. - `areas/` for area notes. -- `areas/people/` for people-related notes. +- `people.org` for the lightweight people rolodex. - `resources/` for reference material. - `archives/` for archived notes. @@ -56,20 +56,24 @@ The minibuffer stack is intentionally small: - `marginalia` adds annotations. - `corfu` handles in-buffer completion popups for text and Org buffers. -Name entry uses fixed abbrevs plus the roster: +Name entry uses fixed abbrevs plus the rolodex: - `abbrev` provides deterministic one-shot shortcuts for fixed name expansions. -- a CAPF feeds Corfu name variants from the legacy shortcut list and from the - structured people roster in `~/org/areas/people/roster.org`. -- `M-x ss/name-dictionary-add-name` and `M-x ss/name-dictionary-remove-name` update the legacy shortcut file and refresh the current prose buffers. -- `M-x ss/name-dictionary-add-name-from-region` uses the active region as the name being added. -- `M-x ss/people-find` opens a roster entry. -- `M-x ss/people-insert-summary` inserts a compact roster summary at point. +- people-specific abbrevs are generated dynamically from top-level cards in `~/org/people.org`. +- a CAPF feeds Corfu canonical names from `people.org`, while alias matching stays available for lookup and completion. +- Marginalia annotates person candidates with `role | location | engagement | current focus`. +- `M-x ss/people-open` opens `people.org` in overview mode. +- `M-x ss/people-find` opens a person card narrowed to that subtree. +- `M-x ss/people-overview` exits card view by widening and restoring the overview. +- `M-x ss/people-insert-name` inserts the canonical name at point. +- `M-x ss/people-insert-summary` inserts a compact single-line summary at point. +- `M-x ss/people-add` adds a new person card directly to `people.org`. +- `M-x ss/people-report-by-relationship`, `M-x ss/people-report-by-engagement`, `M-x ss/people-report-by-role`, and `M-x ss/people-report-by-location` render grouped Org reports in a read-only buffer. ### Persistent abbrevs Persistent abbrevs live in `abbrev_defs` at the repository root. The config loads that file on startup, enables abbrev mode only in text-like buffers, and saves learned abbrevs back to the same file silently when buffers are saved. -The legacy name shortcut list lives in `name-dictionary.el`, and the structured roster in `~/org/areas/people/roster.org` is the source of truth for people records, including name, role, employee time, engagement, team, abbrev, aliases, manager, email, and location. +People-specific abbrevs are not stored in a separate file. They are rebuilt from `~/org/people.org` whenever the rolodex changes, and missing `ABBREV` properties fall back to a generated default trigger. ### Babel tangle process @@ -120,13 +124,12 @@ This means the agenda reflects the current project and area files at runtime ins - daily tasks - daily notes - daily meetings -- Denote-backed captures for generic notes, projects, areas, people, and resources -- structured roster captures for manager-facing people data +- Denote-backed captures for generic notes, projects, areas, and resources Daily task capture writes under `Tasks`. Daily note capture and daily meeting capture both write under `Notes`, and the meeting template prefixes the heading with a timestamp and the word `meeting`. -Denote captures still prompt for title, keywords, and subdirectory placement where appropriate, but folder placement does most of the classification work. The project capture template prepopulates the `project` keyword. Area, person, and resource captures do not inject structural keywords automatically, and there is no Denote-backed meeting capture template. -The people roster capture template writes to `~/org/areas/people/roster.org` and records name, abbrev trigger, aliases, role, engagement, team, manager, email, and location. `C-c n f` opens the roster lookup prompt, and `C-c n r` opens the roster file directly. `M-x ss/people-report-by-engagement`, `M-x ss/people-report-by-role`, and `M-x ss/people-report-by-manager` generate filtered roster views. +Denote captures still prompt for title, keywords, and subdirectory placement where appropriate, but folder placement does most of the classification work. The project capture template prepopulates the `project` keyword. Area and resource captures do not inject structural keywords automatically, and there is no Denote-backed meeting capture template. +The people rolodex is intentionally outside `org-capture`: `M-x ss/people-add` writes a compact card directly into `~/org/people.org`, keeping the rolodex as a fast reference file instead of another capture sink. ### Note creation and linking @@ -135,10 +138,14 @@ Denote handles long-lived notes. The main bindings are: - `C-c n n` to open or create a Denote note. - `C-c n l` to insert a Denote link. - `C-c n M` to open the central MOC note. -- `C-c n f` to search the people roster. +- `C-c n f` to find a person card. +- `C-c n i` to insert a canonical person name. +- `C-c n I` to insert a compact person summary. - `C-c n m` to create a PARA subdirectory from the minibuffer before capturing into it. - `C-c n d` to open today's daily note. -- `C-c n r` to open the roster file. +- `C-c n o` to restore the rolodex overview. +- `C-c n p` to open `people.org`. +- `C-c n P` to add a new person card. Keyword prompts and directory placement are part of the workflow, not an afterthought. The config is set up so structure is created first, then capture writes into it, with folder placement carrying most of the durable type information. @@ -279,309 +279,284 @@ annotations. * Name shortcuts -The name workflow uses fixed abbrev shortcuts for deterministic one-shot -expansions and a structured people roster for searchable metadata. Abbrev -mutates the buffer immediately, while Corfu-backed CAPF completion only -proposes candidates and annotations. The roster file holds the name, role, -engagement, and other lookup fields used by manager-facing searches and -reports. +The people workflow is a lightweight rolodex backed by a single =~/org/people.org= +file. Each top-level heading is a compact card with properties for lookup, +completion, reports, and abbrevs. Abbrev remains the fast path for names you +type all the time, while CAPF plus Corfu remains the discovery path. The +machine-facing layer only reads heading text and properties; the =Context= and +=TODOs= sections stay human-facing notes. #+begin_src emacs-lisp - (defconst ss/name-dictionary-file - (expand-file-name "name-dictionary.el" user-emacs-directory) - "Persistent source of truth for name abbrevs and CAPF candidates.") - - (defvar ss/name-dictionary-entries nil - "Persistent name entries used by abbrev and CAPF.") - - (when (file-exists-p ss/name-dictionary-file) - (load ss/name-dictionary-file nil t)) - (require 'seq) (require 'subr-x) + (require 'marginalia nil t) - (defun ss/name-dictionary--entry-name (entry) - "Return the canonical name in ENTRY." - (plist-get entry :name)) - - (defun ss/name-dictionary--entry-abbrev (entry) - "Return the abbrev trigger in ENTRY." - (plist-get entry :abbrev)) - - (defun ss/name-dictionary--entry-aliases (entry) - "Return alias candidates in ENTRY." - (plist-get entry :aliases)) - - (defun ss/name-dictionary-default-abbrev (name) - "Suggest a short trigger for NAME." - (let* ((parts (split-string (string-trim name) "[[:space:]]+" t)) - (first (downcase (substring (car parts) 0 (min 2 (length (car parts)))))) - (last (downcase (substring (car (last parts)) 0 1)))) - (if (> (length parts) 1) - (concat ";" first last) - (concat ";" first)))) + (defconst ss/people-file + (expand-file-name "people.org" "~/org/") + "Single source of truth for the lightweight people rolodex.") - (defun ss/name-dictionary-read-aliases (prompt) - "Read PROMPT and return a cleaned alias list." - (let ((aliases (mapcar #'string-trim (split-string (read-string prompt) "," t)))) - (seq-filter (lambda (string) (not (string-empty-p string))) aliases))) + (defconst ss/people-engagement-values + '("permanent" "contractor" "other") + "Allowed engagement values for people cards.") - (defvar ss/people-roster--cache nil - "Cached roster entries loaded from `ss/people-roster-file'.") + (defconst ss/people-relationship-values + '("direct-report" "stakeholder" "peer" "skip" "other") + "Allowed relationship values for people cards.") - (defvar ss/people-roster--cache-mtime nil - "Modification time of the cached roster entries.") + (defvar ss/people--cache nil + "Cached rolodex entries loaded from `ss/people-file'.") - (defun ss/people-roster--org-property-line (key value) - "Return an Org property line for KEY and VALUE, or the empty string." - (if (and value (not (string-empty-p value))) - (format ":%s: %s\n" key value) - "")) + (defvar ss/people--cache-mtime nil + "Modification time of the cached rolodex entries.") - (defun ss/people-roster--entry-name (entry) + (defun ss/people--entry-name (entry) "Return the canonical name in ENTRY." (plist-get entry :name)) - (defun ss/people-roster--entry-abbrev (entry) + (defun ss/people--entry-abbrev (entry) "Return the abbrev trigger in ENTRY." (plist-get entry :abbrev)) - (defun ss/people-roster--entry-aliases (entry) + (defun ss/people--entry-aliases (entry) "Return alias variants in ENTRY." (plist-get entry :aliases)) - (defun ss/people-roster--entry-role (entry) + (defun ss/people--entry-role (entry) "Return the role in ENTRY." (plist-get entry :role)) - (defun ss/people-roster--entry-engagement (entry) - "Return the engagement type in ENTRY." - (plist-get entry :engagement)) + (defun ss/people--entry-location (entry) + "Return the location in ENTRY." + (plist-get entry :location)) - (defun ss/people-roster--entry-team (entry) - "Return the team in ENTRY." - (plist-get entry :team)) + (defun ss/people--entry-engagement (entry) + "Return the engagement in ENTRY." + (plist-get entry :engagement)) - (defun ss/people-roster--entry-employee-time (entry) - "Return the raw employee time in ENTRY." - (plist-get entry :employee-time)) + (defun ss/people--entry-relationship (entry) + "Return the relationship in ENTRY." + (plist-get entry :relationship)) - (defun ss/people-roster--entry-manager (entry) - "Return the manager in ENTRY." - (plist-get entry :manager)) + (defun ss/people--entry-current-focus (entry) + "Return the current focus in ENTRY." + (plist-get entry :current-focus)) - (defun ss/people-roster--entry-email (entry) - "Return the email address in ENTRY." - (plist-get entry :email)) + (defun ss/people--entry-team (entry) + "Return the team in ENTRY." + (plist-get entry :team)) - (defun ss/people-roster--entry-location (entry) - "Return the location in ENTRY." - (plist-get entry :location)) + (defun ss/people-default-abbrev (name) + "Suggest a short abbrev trigger for NAME." + (let* ((parts (split-string (string-trim name) "[[:space:]]+" t)) + (first (downcase (substring (car parts) 0 (min 2 (length (car parts)))))) + (last (downcase (substring (car (last parts)) 0 1)))) + (if (> (length parts) 1) + (concat ";" first last) + (concat ";" first)))) - (defun ss/people-roster--split-values (value) - "Split VALUE on commas or semicolons and trim each item." + (defun ss/people--split-values (value) + "Split VALUE on commas and trim each item." (when (and value (not (string-empty-p value))) (seq-filter (lambda (string) (not (string-empty-p string))) - (mapcar #'string-trim (split-string value "[,;]" t))))) + (mapcar #'string-trim (split-string value "," t))))) - (defun ss/people-roster--entry-summary (entry) - "Return a one-line summary for ENTRY." + (defun ss/people--summary (entry) + "Return the compact one-line summary for ENTRY." (string-join (seq-filter (lambda (string) (and string (not (string-empty-p string)))) - (list (ss/people-roster--entry-role entry) - (ss/people-roster--entry-engagement entry) - (ss/people-roster--entry-team entry) - (ss/people-roster--entry-manager entry))) + (list (ss/people--entry-role entry) + (ss/people--entry-location entry) + (ss/people--entry-engagement entry) + (ss/people--entry-current-focus entry))) " | ")) - (defun ss/people-roster-entry-display (entry) - "Return a searchable display string for ENTRY." - (let ((name (ss/people-roster--entry-name entry)) - (summary (ss/people-roster--entry-summary entry))) + (defun ss/people--display (entry) + "Return the compact display string for ENTRY." + (let ((summary (ss/people--summary entry))) (if (string-empty-p summary) - name - (format "%s | %s" name summary)))) + (ss/people--entry-name entry) + (format "%s %s" (ss/people--entry-name entry) summary)))) + + (defun ss/people--property-line (key value) + "Return an Org property line for KEY and VALUE." + (if (and value (not (string-empty-p value))) + (format ":%s: %s\n" key value) + "")) - (defun ss/people-roster--entry-by-name (name) - "Return the roster entry matching NAME or one of its aliases." + (defun ss/people--ensure-file () + "Create `ss/people-file' when it is missing." + (make-directory (file-name-directory ss/people-file) t) + (unless (file-exists-p ss/people-file) + (with-temp-file ss/people-file + (insert "#+title: People\n\n"))) + ss/people-file) + + (defun ss/people-entries () + "Return top-level people cards from `ss/people-file'." + (let* ((file (ss/people--ensure-file)) + (attributes (file-attributes file)) + (mtime (file-attribute-modification-time attributes))) + (unless (and ss/people--cache + (equal mtime ss/people--cache-mtime)) + (setq ss/people--cache + (with-temp-buffer + (insert-file-contents file) + (org-mode) + (goto-char (point-min)) + (let (entries) + (org-element-map (org-element-parse-buffer) 'headline + (lambda (headline) + (when (= 1 (org-element-property :level headline)) + (goto-char (org-element-property :begin headline)) + (push (list :name (org-element-property :raw-value headline) + :abbrev (org-entry-get nil "ABBREV") + :aliases (ss/people--split-values + (org-entry-get nil "ALIASES")) + :role (org-entry-get nil "ROLE") + :location (org-entry-get nil "LOCATION") + :engagement (org-entry-get nil "ENGAGEMENT") + :relationship (org-entry-get nil "RELATIONSHIP") + :current-focus (org-entry-get nil "CURRENT_FOCUS") + :team (org-entry-get nil "TEAM")) + entries)))) + (sort entries + (lambda (left right) + (string< (ss/people--entry-name left) + (ss/people--entry-name right))))) + ss/people--cache-mtime mtime)) + ss/people--cache)) + + (defun ss/people-reload () + "Reload the people cache and refresh prose buffers." + (interactive) + (setq ss/people--cache nil + ss/people--cache-mtime nil) + (ss/people-refresh-buffers) + (message "Reloaded people rolodex")) + + (defun ss/people--entry-by-name (name) + "Return the people entry matching canonical NAME." (seq-find (lambda (entry) - (or (string= name (ss/people-roster--entry-name entry)) - (member name (ss/people-roster--entry-aliases entry)))) - (ss/people-roster-entries))) - - (defun ss/people-roster--ensure-file () - "Create the roster file when it is missing." - (make-directory (file-name-directory ss/people-roster-file) t) - (unless (file-exists-p ss/people-roster-file) - (with-temp-file ss/people-roster-file - (insert "#+title: People roster\n\n"))) - ss/people-roster-file) - - (defun ss/people-roster-entries () - "Return the structured roster entries from `ss/people-roster-file'." - (let* ((attributes (and (file-exists-p ss/people-roster-file) - (file-attributes ss/people-roster-file))) - (mtime (and attributes (file-attribute-modification-time attributes)))) - (unless (and ss/people-roster--cache - (equal mtime ss/people-roster--cache-mtime)) - (setq ss/people-roster--cache - (when (file-exists-p ss/people-roster-file) - (with-temp-buffer - (insert-file-contents ss/people-roster-file) - (org-mode) - (goto-char (point-min)) - (let (entries) - (org-element-map (org-element-parse-buffer) 'headline - (lambda (headline) - (goto-char (org-element-property :begin headline)) - (let ((name (or (org-entry-get nil "NAME") - (org-element-property :raw-value headline)))) - (push (list :name name - :abbrev (org-entry-get nil "ABBREV") - :aliases (ss/people-roster--split-values - (org-entry-get nil "ALIASES")) - :role (org-entry-get nil "ROLE") - :employee-time (org-entry-get nil "EMPLOYEE-TIME") - :engagement (org-entry-get nil "ENGAGEMENT") - :team (org-entry-get nil "TEAM") - :manager (org-entry-get nil "MANAGER") - :email (org-entry-get nil "EMAIL") - :location (org-entry-get nil "LOCATION")) - entries)))) - (nreverse entries)))) - ss/people-roster--cache-mtime mtime)) - ss/people-roster--cache)) - - (defun ss/people-roster-reload () - "Reload the roster cache and refresh prose buffers." + (string= name (ss/people--entry-name entry))) + (ss/people-entries))) + + (defun ss/people--search-keys (entry) + "Return canonical and alias search keys for ENTRY." + (cons (ss/people--entry-name entry) + (ss/people--entry-aliases entry))) + + (defun ss/people--match-p (query entry) + "Return non-nil when QUERY matches ENTRY name or aliases." + (let* ((parts (split-string (downcase (string-trim query)) "[[:space:]]+" t)) + (keys (mapcar #'downcase (ss/people--search-keys entry)))) + (seq-every-p + (lambda (part) + (seq-some (lambda (key) + (string-match-p (regexp-quote part) key)) + keys)) + parts))) + + (defun ss/people--matching-entries (query) + "Return entries whose canonical name or aliases match QUERY." + (let ((entries (ss/people-entries))) + (if (string-empty-p (string-trim query)) + entries + (seq-filter (lambda (entry) + (ss/people--match-p query entry)) + entries)))) + + (defun ss/people--completion-table (string pred action) + "Complete canonical people names while matching aliases via STRING." + (if (eq action 'metadata) + '(metadata (category . ss-person)) + (complete-with-action + action + (mapcar #'ss/people--entry-name (ss/people--matching-entries string)) + string + pred))) + + (defun ss/people-marginalia-annotator (candidate) + "Return a Marginalia annotation for person CANDIDATE." + (when-let ((entry (ss/people--entry-by-name candidate))) + (concat " " (ss/people--summary entry)))) + + (with-eval-after-load 'marginalia + (add-to-list 'marginalia-annotator-registry + '(ss-person ss/people-marginalia-annotator builtin none))) + + (defun ss/people-select-entry (&optional prompt) + "Select a person entry using PROMPT." + (let ((completion-extra-properties '(:annotation-function ss/people-marginalia-annotator))) + (ss/people--entry-by-name + (completing-read (or prompt "Person: ") + #'ss/people--completion-table + nil + t)))) + + (defun ss/people-overview () + "Show `ss/people-file' in overview mode." (interactive) - (setq ss/people-roster--cache nil - ss/people-roster--cache-mtime nil) - (ss/name-dictionary-refresh-buffers) - (message "Reloaded people roster")) - - (defun ss/people-roster-canonical-names () - "Return the canonical names from the roster." - (mapcar #'ss/people-roster--entry-name (ss/people-roster-entries))) - - (defun ss/people-roster-completion-candidates () - "Return roster names and aliases for completion." - (delete-dups - (apply #'append - (mapcar (lambda (entry) - (cons (ss/people-roster--entry-name entry) - (ss/people-roster--entry-aliases entry))) - (ss/people-roster-entries))))) - - (defun ss/people-roster-entry-display-candidates () - "Return searchable completion candidates for the roster." - (mapcar (lambda (entry) - (cons (ss/people-roster-entry-display entry) entry)) - (ss/people-roster-entries))) - - (defun ss/people-roster-select-entry (&optional prompt) - "Select a roster ENTRY using PROMPT." - (let* ((candidates (ss/people-roster-entry-display-candidates)) - (choice (completing-read (or prompt "Person: ") candidates nil t))) - (or (cdr (assoc choice candidates)) - (user-error "No roster entry selected")))) - - (defun ss/people-roster-open () - "Open the roster file." + (unless (and buffer-file-name + (string= (file-truename buffer-file-name) + (file-truename ss/people-file))) + (find-file (ss/people--ensure-file))) + (widen) + (goto-char (point-min)) + (org-overview) + (org-cycle-hide-drawers 'all)) + + (defun ss/people-open () + "Open the rolodex in overview mode." (interactive) - (find-file (ss/people-roster--ensure-file))) + (ss/people-overview)) - (defun ss/people-roster-track-buffer () - "Refresh roster caches when the roster file is saved." + (defun ss/people--track-buffer () + "Refresh rolodex caches when `ss/people-file' is saved." (when (and buffer-file-name - (string= buffer-file-name ss/people-roster-file)) - (add-hook 'after-save-hook #'ss/people-roster-reload nil t))) + (string= (file-truename buffer-file-name) + (file-truename ss/people-file))) + (add-hook 'after-save-hook #'ss/people-reload nil t))) - (defun ss/people-roster-open-entry (entry) - "Open the roster file, jump to ENTRY, and narrow to its subtree." - (find-file (ss/people-roster--ensure-file)) + (defun ss/people--open-entry (entry) + "Open people.org and narrow to ENTRY." + (find-file (ss/people--ensure-file)) (widen) (let ((position (org-find-exact-headline-in-buffer - (ss/people-roster--entry-name entry)))) + (ss/people--entry-name entry)))) (unless position - (user-error "No roster heading for %s" (ss/people-roster--entry-name entry))) + (user-error "No people card for %s" (ss/people--entry-name entry))) (goto-char position)) (org-narrow-to-subtree) - (org-show-subtree) - (goto-char (point-max))) + (org-fold-show-subtree) + (org-show-entry) + (goto-char (point-min))) (defun ss/people-find () - "Find and open a roster entry." + "Find a person and open that card." (interactive) - (ss/people-roster-open-entry - (ss/people-roster-select-entry "Find person: "))) + (ss/people--open-entry + (or (ss/people-select-entry "Find person: ") + (user-error "No person selected")))) - (defun ss/people-roster-insert-summary () - "Insert a compact roster summary at point." + (defun ss/people-insert-name () + "Insert a canonical person name at point." (interactive) - (let ((entry (ss/people-roster-select-entry "Insert person: "))) - (insert (ss/people-roster-entry-display entry)))) - - (defvar ss/people-roster--capture-name nil - "Most recent roster name captured through `ss/people-roster-capture-name'.") + (let ((entry (or (ss/people-select-entry "Insert person name: ") + (user-error "No person selected")))) + (insert (ss/people--entry-name entry)))) - (defun ss/people-roster-read-string (prompt &optional default) - "Read PROMPT and trim the result." - (string-trim (read-string prompt nil nil default))) + (defun ss/people-insert-summary () + "Insert a compact person summary at point." + (interactive) + (let ((entry (or (ss/people-select-entry "Insert person summary: ") + (user-error "No person selected")))) + (insert (ss/people--display entry)))) - (defun ss/people-roster-capture-name () - "Read the canonical roster name." - (or ss/people-roster--capture-name - (setq ss/people-roster--capture-name - (ss/people-roster-read-string "Full name: ")))) - - (defun ss/people-roster-capture-begin () - "Reset cached roster capture state." - (setq ss/people-roster--capture-name nil) - "") - - (defun ss/people-roster-capture-abbrev () - "Read the roster abbrev trigger." - (let ((name (or ss/people-roster--capture-name - (ss/people-roster-capture-name)))) - (ss/people-roster-read-string - "Abbrev trigger: " - (ss/name-dictionary-default-abbrev name)))) - - (defun ss/people-roster-capture-aliases () - "Read optional alias variants for a roster entry." - (ss/people-roster-read-string "Aliases (comma-separated, optional): ")) - - (defun ss/people-roster-capture-role () - "Read the role for a roster entry." - (ss/people-roster-read-string "Role: ")) - - (defun ss/people-roster-capture-engagement () - "Read the engagement type for a roster entry." - (completing-read "Engagement: " '("permanent" "sow" "other") nil t nil nil - "permanent")) - - (defun ss/people-roster-capture-team () - "Read the team for a roster entry." - (ss/people-roster-read-string "Team: ")) - - (defun ss/people-roster-capture-manager () - "Read the manager for a roster entry." - (ss/people-roster-read-string "Manager: " "You")) - - (defun ss/people-roster-capture-email () - "Read the email address for a roster entry." - (ss/people-roster-read-string "Email: ")) - - (defun ss/people-roster-capture-location () - "Read the location for a roster entry." - (ss/people-roster-read-string "Location: ")) - - (defun ss/people-roster-report-buffer (title group-fn) - "Render a grouped roster report into a dedicated buffer." + (defun ss/people--report-buffer (title group-fn) + "Render a grouped rolodex report titled TITLE using GROUP-FN." (let ((groups (sort (seq-group-by (lambda (entry) @@ -589,227 +564,169 @@ reports. (if (string-empty-p (or value "")) "(none)" value))) - (ss/people-roster-entries)) + (ss/people-entries)) (lambda (left right) (string< (car left) (car right)))))) - (with-current-buffer (get-buffer-create "*People Roster*") + (with-current-buffer (get-buffer-create "*People Report*") (let ((inhibit-read-only t)) (erase-buffer) (org-mode) (insert "#+title: " title "\n\n") (dolist (group groups) (insert "* " (car group) "\n") - (dolist (entry (sort (copy-sequence (cdr group)) - (lambda (left right) - (string< (ss/people-roster--entry-name left) - (ss/people-roster--entry-name right))))) - (insert "- " (ss/people-roster-entry-display entry) "\n"))) + (dolist (entry (cdr group)) + (insert "- " (ss/people--display entry) "\n"))) (goto-char (point-min)) + (read-only-mode 1) (view-mode 1)) (pop-to-buffer (current-buffer))))) + (defun ss/people-report-by-relationship () + "Show people grouped by relationship." + (interactive) + (ss/people--report-buffer + "People by relationship" + #'ss/people--entry-relationship)) + (defun ss/people-report-by-engagement () - "Show roster entries grouped by engagement." + "Show people grouped by engagement." (interactive) - (ss/people-roster-report-buffer + (ss/people--report-buffer "People by engagement" - #'ss/people-roster--entry-engagement)) + #'ss/people--entry-engagement)) (defun ss/people-report-by-role () - "Show roster entries grouped by role." + "Show people grouped by role." (interactive) - (ss/people-roster-report-buffer + (ss/people--report-buffer "People by role" - #'ss/people-roster--entry-role)) + #'ss/people--entry-role)) - (defun ss/people-report-by-manager () - "Show roster entries grouped by manager." + (defun ss/people-report-by-location () + "Show people grouped by location." (interactive) - (ss/people-roster-report-buffer - "People by manager" - #'ss/people-roster--entry-manager)) - - (defun ss/name-dictionary-canonical-names () - "Return the canonical names from the dictionary and roster." - (delete-dups - (append (mapcar #'ss/name-dictionary--entry-name ss/name-dictionary-entries) - (ss/people-roster-canonical-names)))) - - (defun ss/name-dictionary-candidates () - "Return all CAPF candidates from the dictionary." - (delete-dups - (append - (apply #'append - (mapcar (lambda (entry) - (cons (ss/name-dictionary--entry-name entry) - (ss/name-dictionary--entry-aliases entry))) - ss/name-dictionary-entries)) - (ss/people-roster-completion-candidates)))) - - (defun ss/name-dictionary-entry-by-name (name) - "Return the legacy dictionary entry matching NAME or an alias." - (seq-find - (lambda (entry) - (or (string= name (ss/name-dictionary--entry-name entry)) - (member name (ss/name-dictionary--entry-aliases entry)))) - ss/name-dictionary-entries)) + (ss/people--report-buffer + "People by location" + #'ss/people--entry-location)) - (defun ss/name-entry-by-name (name) - "Return the matching name entry from the roster or legacy dictionary." - (or (ss/people-roster--entry-by-name name) - (ss/name-dictionary-entry-by-name name))) + (defun ss/people-read-string (prompt &optional default) + "Read PROMPT and trim the result." + (string-trim (read-string prompt nil nil default))) - (defun ss/name-dictionary-install-abbrevs () - "Install name abbrevs into the current buffer." + (defun ss/people-read-required-string (prompt &optional default) + "Read PROMPT and require a non-empty result." + (let ((value (ss/people-read-string prompt default))) + (if (string-empty-p value) + (user-error "%s is required" (string-remove-suffix ": " prompt)) + value))) + + (defun ss/people-read-optional-string (prompt) + "Read PROMPT and return nil when the answer is empty." + (let ((value (ss/people-read-string prompt))) + (unless (string-empty-p value) + value))) + + (defun ss/people-add () + "Add a new compact person card to `ss/people-file'." + (interactive) + (let* ((name (ss/people-read-required-string "Full name: ")) + (abbrev (ss/people-read-string "Abbrev: " (ss/people-default-abbrev name))) + (aliases (ss/people-read-string "Aliases (comma-separated, optional): ")) + (role (ss/people-read-required-string "Role: ")) + (location (ss/people-read-required-string "Location: ")) + (engagement (completing-read "Engagement: " + ss/people-engagement-values nil t nil nil + "permanent")) + (relationship (completing-read "Relationship: " + ss/people-relationship-values nil t)) + (current-focus (ss/people-read-required-string "Current focus: ")) + (team (ss/people-read-optional-string "Team (optional): "))) + (when (ss/people--entry-by-name name) + (user-error "A person card for %s already exists" name)) + (when (string-empty-p abbrev) + (setq abbrev (ss/people-default-abbrev name))) + (find-file (ss/people--ensure-file)) + (widen) + (goto-char (point-max)) + (unless (bolp) + (insert "\n")) + (unless (looking-back "\n\n" nil) + (insert "\n")) + (insert "* " name "\n" + ":PROPERTIES:\n" + (ss/people--property-line "ABBREV" abbrev) + (ss/people--property-line "ALIASES" aliases) + (ss/people--property-line "ROLE" role) + (ss/people--property-line "LOCATION" location) + (ss/people--property-line "ENGAGEMENT" engagement) + (ss/people--property-line "RELATIONSHIP" relationship) + (ss/people--property-line "CURRENT_FOCUS" current-focus) + (ss/people--property-line "TEAM" team) + ":END:\n\n" + "** Context\n\n" + "** TODOs\n") + (save-buffer) + (ss/people-reload) + (ss/people--open-entry (ss/people--entry-by-name name)))) + + (defun ss/people-install-abbrevs () + "Install people abbrevs into the current buffer." (setq-local local-abbrev-table (copy-abbrev-table local-abbrev-table)) - (dolist (entry (append ss/name-dictionary-entries (ss/people-roster-entries))) - (when-let ((name (or (ss/name-dictionary--entry-name entry) - (ss/people-roster--entry-name entry)))) - (let ((abbrev (or (ss/name-dictionary--entry-abbrev entry) - (ss/people-roster--entry-abbrev entry)))) - (define-abbrev local-abbrev-table - (if (or (null abbrev) (string-empty-p abbrev)) - (ss/name-dictionary-default-abbrev name) - abbrev) - name))))) - - (defun ss/name-dictionary-refresh-buffers () - "Refresh name abbrevs in every prose buffer." + (dolist (entry (ss/people-entries)) + (let ((name (ss/people--entry-name entry)) + (abbrev (ss/people--entry-abbrev entry))) + (define-abbrev local-abbrev-table + (if (or (null abbrev) (string-empty-p abbrev)) + (ss/people-default-abbrev name) + abbrev) + name)))) + + (defun ss/people-refresh-buffers () + "Refresh people abbrevs in every prose buffer." (dolist (buffer (buffer-list)) (with-current-buffer buffer (when (and (bound-and-true-p abbrev-mode) (derived-mode-p 'text-mode 'org-mode)) - (ss/name-dictionary-install-abbrevs))))) - - (defun ss/name-dictionary-save () - "Write the name dictionary file." - (let ((print-length nil) - (print-level nil)) - (with-temp-file ss/name-dictionary-file - (insert ";; -*- lexical-binding: t; -*-\n") - (insert ";; Persistent name entries used by abbrev and CAPF.\n\n") - (insert "(setq ss/name-dictionary-entries\n '") - (insert (pp-to-string ss/name-dictionary-entries)) - (insert ")\n")))) - - (defun ss/name-dictionary-reload () - "Reload the name dictionary file and refresh prose buffers." - (interactive) - (when (file-exists-p ss/name-dictionary-file) - (load ss/name-dictionary-file nil t)) - (ss/people-roster-reload) - (message "Reloaded name dictionary")) - - (defun ss/name-dictionary--upsert (entry) - "Insert or replace ENTRY in `ss/name-dictionary-entries'." - (setq ss/name-dictionary-entries - (sort - (cons entry - (seq-remove (lambda (existing) - (or (string= (ss/name-dictionary--entry-name existing) - (ss/name-dictionary--entry-name entry)) - (string= (ss/name-dictionary--entry-abbrev existing) - (ss/name-dictionary--entry-abbrev entry)))) - ss/name-dictionary-entries)) - (lambda (left right) - (string< (ss/name-dictionary--entry-name left) - (ss/name-dictionary--entry-name right)))))) - - (defun ss/name-dictionary--remove (name) - "Remove NAME from `ss/name-dictionary-entries'." - (setq ss/name-dictionary-entries - (seq-remove (lambda (entry) - (string= (ss/name-dictionary--entry-name entry) name)) - ss/name-dictionary-entries))) - - (defun ss/name-dictionary--save-entry (name abbrev aliases) - "Persist a name entry and refresh prose buffers." - (let ((entry (list :name name :abbrev abbrev))) - (when aliases - (setq entry (append entry (list :aliases aliases)))) - (ss/name-dictionary--upsert entry) - (ss/name-dictionary-save) - (ss/name-dictionary-refresh-buffers) - (message "Added name: %s" name))) - - (defun ss/name-dictionary-add-name (name abbrev aliases) - "Add a canonical NAME, ABBREV trigger, and optional ALIASES." - (interactive - (let* ((name (read-string "Full name: ")) - (abbrev (string-trim - (read-string "Abbrev trigger: " - (ss/name-dictionary-default-abbrev name)))) - (aliases (ss/name-dictionary-read-aliases - "Aliases (comma-separated, optional): "))) - (list name abbrev aliases))) - (when (string-empty-p abbrev) - (setq abbrev (ss/name-dictionary-default-abbrev name))) - (ss/name-dictionary--save-entry name abbrev aliases)) - - (defun ss/name-dictionary-add-name-from-region (beg end abbrev aliases) - "Add the active region as a name entry." - (interactive - (if (use-region-p) - (let* ((name (string-trim - (buffer-substring-no-properties - (region-beginning) (region-end)))) - (abbrev (string-trim - (read-string "Abbrev trigger: " - (ss/name-dictionary-default-abbrev name)))) - (aliases (ss/name-dictionary-read-aliases - "Aliases (comma-separated, optional): "))) - (list (region-beginning) (region-end) abbrev aliases)) - (user-error "Select a name first"))) - (let ((name (string-trim - (buffer-substring-no-properties beg end)))) - (when (string-empty-p abbrev) - (setq abbrev (ss/name-dictionary-default-abbrev name))) - (ss/name-dictionary--save-entry name abbrev aliases))) - - (defun ss/name-dictionary-remove-name (name) - "Remove NAME from the persistent dictionary." - (interactive - (list (completing-read "Remove name: " - (mapcar #'ss/name-dictionary--entry-name - ss/name-dictionary-entries) - nil t))) - (ss/name-dictionary--remove name) - (ss/name-dictionary-save) - (ss/name-dictionary-refresh-buffers) - (message "Removed name: %s" name)) - - (defun ss/name-dictionary-open () - "Open the persistent name dictionary." - (interactive) - (find-file ss/name-dictionary-file)) + (ss/people-install-abbrevs))))) - (defun ss/name-capf () - "Return a name completion candidate set at a word boundary." + (defun ss/people-capf () + "Return canonical people completions at a word boundary." (let ((end (point))) (save-excursion (skip-syntax-backward "w_") - (let ((beg (point)) - (candidates (ss/name-dictionary-candidates))) - (when (and (< beg end) candidates) - (list beg end candidates + (let ((beg (point))) + (when (< beg end) + (list beg end #'ss/people--completion-table :exclusive 'no :annotation-function (lambda (candidate) - (when-let ((entry (ss/name-entry-by-name candidate))) - (let ((summary - (or (ss/people-roster--entry-summary entry) - ""))) - (when (not (string-empty-p summary)) - (concat " " summary))))))))))) - - (defun ss/enable-name-capf () - "Add `ss/name-capf' once in prose buffers." - (unless (memq #'ss/name-capf completion-at-point-functions) - (add-hook 'completion-at-point-functions #'ss/name-capf nil t))) + (when-let ((entry (ss/people--entry-by-name candidate))) + (concat " " (ss/people--summary entry)))) + :company-docsig + (lambda (candidate) + (when-let ((entry (ss/people--entry-by-name candidate))) + (ss/people--summary entry))))))))) + + (defun ss/enable-people-capf () + "Add `ss/people-capf' once in prose buffers." + (unless (memq #'ss/people-capf completion-at-point-functions) + (add-hook 'completion-at-point-functions #'ss/people-capf nil t))) + + (defun ss/people--maybe-overview-buffer () + "Reset people.org to overview when visiting it directly." + (when (and buffer-file-name + (string= (file-truename buffer-file-name) + (file-truename ss/people-file))) + (widen) + (goto-char (point-min)) + (org-overview) + (org-cycle-hide-drawers 'all)))) (dolist (hook '(text-mode-hook org-mode-hook)) - (add-hook hook #'ss/enable-name-capf)) - (add-hook 'find-file-hook #'ss/people-roster-track-buffer) + (add-hook hook #'ss/enable-people-capf) + (add-hook hook #'ss/people-install-abbrevs)) + (add-hook 'find-file-hook #'ss/people--track-buffer) + (add-hook 'find-file-hook #'ss/people--maybe-overview-buffer) #+end_src * Notes workflow @@ -848,13 +765,6 @@ directly during startup rather than creating it on demand. (defconst ss/org-areas-directory (expand-file-name "areas/" ss/org-directory) "Directory for area notes.") - (defconst ss/org-people-directory (expand-file-name "areas/people/" ss/org-directory) - "Directory for people notes.") - - (defconst ss/people-roster-file - (expand-file-name "areas/people/roster.org" ss/org-directory) - "Structured roster of people and role metadata.") - (defconst ss/org-resources-directory (expand-file-name "resources/" ss/org-directory) "Directory for resource notes.") @@ -869,7 +779,6 @@ directly during startup rather than creating it on demand. ss/org-daily-directory ss/org-projects-directory ss/org-areas-directory - ss/org-people-directory ss/org-resources-directory ss/org-archives-directory) "Directories that make up the note-taking workflow.") @@ -882,7 +791,6 @@ directly during startup rather than creating it on demand. (defconst ss/org-subdirectory-roots `(("projects" . ,ss/org-projects-directory) ("areas" . ,ss/org-areas-directory) - ("people" . ,ss/org-people-directory) ("resources" . ,ss/org-resources-directory)) "Capture roots offered when creating note subdirectories.") @@ -982,9 +890,13 @@ directly during startup rather than creating it on demand. ("C-c c" . org-capture) ("C-c n M" . ss/open-moc) ("C-c n f" . ss/people-find) + ("C-c n i" . ss/people-insert-name) + ("C-c n I" . ss/people-insert-summary) ("C-c n m" . ss/create-note-subdirectory) ("C-c n d" . ss/open-todays-note) - ("C-c n r" . ss/people-roster-open)) + ("C-c n o" . ss/people-overview) + ("C-c n p" . ss/people-open) + ("C-c n P" . ss/people-add)) :config (setq org-directory ss/org-directory org-hide-emphasis-markers t) @@ -997,7 +909,8 @@ directly during startup rather than creating it on demand. (advice-add 'org-agenda :before #'ss/refresh-org-agenda-files) (mapc (lambda (directory) (make-directory directory t)) - ss/org-note-directories)) + ss/org-note-directories) + (ss/people--ensure-file)) #+end_src ** Capture entry points @@ -1007,8 +920,9 @@ notes and meetings land under =Notes=. Denote capture uses Denote's own Org integration so note identity, metadata, and directories stay under Denote's control rather than custom code. The convenience templates keep the familiar entry points, but only project capture injects a structural keyword by default. -People have two paths: =nP= creates a Denote note for narrative context, while -=nR= writes a structured roster entry with role and engagement metadata. +The people rolodex lives outside =org-capture=: adding a person uses the +dedicated =ss/people-add= command so =~/org/people.org= stays a compact, +structured card file rather than turning into another capture target. #+begin_src emacs-lisp (use-package org-capture @@ -1056,23 +970,6 @@ People have two paths: =nP= creates a Denote note for narrative context, while :immediate-finish nil :kill-buffer t :jump-to-captured t) - ("nP" "Person" plain - (file denote-last-path) - (function - (lambda () - (ss/denote-capture-in-directory - ss/org-people-directory nil :title :keywords :subdirectory))) - :no-save t - :immediate-finish nil - :kill-buffer t - :jump-to-captured t) - ("nR" "Roster" entry - (file ,#'ss/people-roster--ensure-file) - "%(ss/people-roster-capture-begin)* %(ss/people-roster-capture-name)\n:PROPERTIES:\n:NAME: %(ss/people-roster-capture-name)\n:ABBREV: %(ss/people-roster-capture-abbrev)\n:ALIASES: %(ss/people-roster-capture-aliases)\n:ROLE: %(ss/people-roster-capture-role)\n:ENGAGEMENT: %(ss/people-roster-capture-engagement)\n:TEAM: %(ss/people-roster-capture-team)\n:MANAGER: %(ss/people-roster-capture-manager)\n:EMAIL: %(ss/people-roster-capture-email)\n:LOCATION: %(ss/people-roster-capture-location)\n:END:\n%?" - :no-save t - :immediate-finish nil - :kill-buffer t - :jump-to-captured t) ("nr" "Resource" plain (file denote-last-path) (function @@ -1083,7 +980,6 @@ People have two paths: =nP= creates a Denote note for narrative context, while :immediate-finish nil :kill-buffer t :jump-to-captured t)))) - (add-hook 'org-capture-after-finalize-hook #'ss/people-roster-reload) #+end_src ** Denote diff --git a/docs/plans/2026-04-07-people-rolodex-implementation.md b/docs/plans/2026-04-07-people-rolodex-implementation.md new file mode 100644 index 0000000..16578f8 --- /dev/null +++ b/docs/plans/2026-04-07-people-rolodex-implementation.md @@ -0,0 +1,239 @@ +# People Rolodex Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the old roster and name-dictionary setup with a lightweight `~/org/people.org` rolodex that powers lookup, completion, reports, and dynamic people abbrevs from one Org file. + +**Architecture:** Keep a single cached parser for top-level headings in `~/org/people.org`, treat each heading as one person card, and build all lookup and insertion commands from that cache. Use explicit Org commands for overview/card mode, a small CAPF for Corfu, Marginalia annotations for minibuffer completion, dynamic buffer-local abbrevs sourced from the parsed cards, and a dedicated interactive add-person command that writes a compact subtree directly into `people.org`. + +**Tech Stack:** Emacs Lisp, Org mode, Vertico, Orderless, Marginalia, Corfu, CAPF, abbrev + +--- + +### Task 1: Replace the old name dictionary and roster model in `config.org` + +**Files:** +- Modify: `config.org` + +**Step 1: Write the failing test** + +Define the expected end state as code-level checks while editing: + +- there is no reference to `name-dictionary.el` +- there is no reference to `ss/people-roster-file` +- there is no capture template that writes a roster entry +- the rolodex source file points to `~/org/people.org` + +**Step 2: Run test to verify it fails** + +Run: `rg -n "name-dictionary|people-roster|roster.org" config.org README.md` + +Expected: FAIL by finding the legacy model still present. + +**Step 3: Write minimal implementation** + +In the “Name shortcuts” section: + +- rewrite the prose to describe the lightweight rolodex +- remove the legacy dictionary variables and functions +- remove the old roster parsing code +- add a single `ss/people-file` constant consumer model +- add entry parsing helpers for: + - canonical name + - abbrev + - aliases + - role + - location + - engagement + - relationship + - current focus + - team +- cache entries by `file-attribute-modification-time` +- parse only level-1 headlines + +**Step 4: Run test to verify it passes** + +Run: `rg -n "name-dictionary|people-roster|roster.org" config.org` + +Expected: PASS with no remaining legacy implementation references. + +**Step 5: Commit** + +Do not commit yet. Verification and README updates are still pending. + +### Task 2: Implement rolodex overview, card view, lookup, insertion, reports, and add-person flow + +**Files:** +- Modify: `config.org` + +**Step 1: Write the failing test** + +List the required commands before coding: + +- `ss/people-open` +- `ss/people-find` +- `ss/people-overview` +- `ss/people-insert-name` +- `ss/people-insert-summary` +- `ss/people-report-by-relationship` +- `ss/people-report-by-engagement` +- `ss/people-report-by-role` +- `ss/people-report-by-location` +- `ss/people-add` + +Also require: + +- aliases match but insert canonical names +- reports group under Org headings in a read-only buffer +- opening `people.org` always restores overview mode + +**Step 2: Run test to verify it fails** + +Run: `rg -n "defun ss/(people-open|people-find|people-overview|people-insert-name|people-insert-summary|people-report-by-relationship|people-report-by-engagement|people-report-by-role|people-report-by-location|people-add)" config.org` + +Expected: FAIL because some or all required commands do not exist yet. + +**Step 3: Write minimal implementation** + +Add: + +- file creation helper for `~/org/people.org` +- overview-mode helper that widens, jumps to start, and shows top-level headings only +- entry-open helper that widens, jumps, narrows to subtree, and reveals subtree +- minibuffer completion metadata and Marginalia annotator +- CAPF that returns canonical names and single-line annotations +- insertion helpers for canonical name and compact summary +- grouped report renderer +- interactive prompts for add-person, including default abbrev generation and constrained choices for engagement/relationship +- direct Org insertion for a compact starter card with `Context` and `TODOs` sections + +**Step 4: Run test to verify it passes** + +Run: `rg -n "defun ss/(people-open|people-find|people-overview|people-insert-name|people-insert-summary|people-report-by-relationship|people-report-by-engagement|people-report-by-role|people-report-by-location|people-add)" config.org` + +Expected: PASS with all commands present. + +**Step 5: Commit** + +Do not commit yet. Generated files and docs still need verification. + +### Task 3: Rewire Org directories, keybindings, and capture flow to the new people model + +**Files:** +- Modify: `config.org` + +**Step 1: Write the failing test** + +Define the required workflow changes: + +- `ss/org-people-directory` should no longer point to `areas/people/` +- there should be a `ss/people-file` under `~/org/` +- keybindings should point at the new rolodex commands +- `org-capture` should no longer include the old roster template + +**Step 2: Run test to verify it fails** + +Run: `rg -n "areas/people|nR|people-roster|C-c n r" config.org` + +Expected: FAIL by showing the old path and capture/binding behavior. + +**Step 3: Write minimal implementation** + +- update Org path constants so people notes are no longer treated as a PARA subdirectory +- make `people.org` part of the created note roots where appropriate +- keep Denote people-note capture removed or re-scoped so the rolodex remains distinct from narrative notes +- bind the new commands to sensible keys, including a way to open `people.org` and add/find people + +**Step 4: Run test to verify it passes** + +Run: `rg -n "areas/people|nR|people-roster" config.org` + +Expected: PASS with the roster model removed. + +**Step 5: Commit** + +Do not commit yet. Documentation and generated output still need verification. + +### Task 4: Update the README to reflect the new rolodex end state + +**Files:** +- Modify: `README.md` + +**Step 1: Write the failing test** + +Identify stale statements to remove: + +- references to `name-dictionary.el` +- references to `~/org/areas/people/roster.org` +- manager/email/employee-time roster fields +- capture docs for the old roster template + +Add the new documented state: + +- `~/org/people.org` is the only source of truth +- commands, keybindings, reports, and overview/card behavior +- people-specific abbrevs come dynamically from `people.org` + +**Step 2: Run test to verify it fails** + +Run: `rg -n "name-dictionary|roster.org|manager|employee time|email" README.md` + +Expected: FAIL because the README still documents the old roster. + +**Step 3: Write minimal implementation** + +Edit the README sections for: + +- note layout +- completion setup +- abbrevs +- capture flow +- note creation and linking + +Make the current workflow truthful and concise. + +**Step 4: Run test to verify it passes** + +Run: `rg -n "name-dictionary|roster.org|employee time" README.md` + +Expected: PASS with stale docs removed. + +**Step 5: Commit** + +Do not commit yet. Final verification still needs to run. + +### Task 5: Tangle and verify the generated configuration + +**Files:** +- Modify indirectly via tangling: `init.el` + +**Step 1: Write the failing test** + +Require two checks: + +- `config.org` tangles without Lisp errors +- generated `init.el` loads under batch Emacs + +**Step 2: Run test to verify it fails** + +Run: `emacs --batch -Q --eval '(progn (require (quote ob-tangle)) (org-babel-tangle-file "config.org"))'` + +Expected: If there are syntax or tangle issues, this fails and guides fixes. + +**Step 3: Write minimal implementation** + +Fix any syntax, quoting, or load-order issues reported by the tangle or load checks. + +**Step 4: Run test to verify it passes** + +Run: `emacs --batch -Q --eval '(progn (require (quote ob-tangle)) (org-babel-tangle-file "config.org"))'` + +Expected: `init.el` and `early-init.el` are regenerated successfully. + +Run: `emacs --batch -Q --load ./init.el` + +Expected: Emacs exits successfully after loading the generated main config. + +**Step 5: Commit** + +Stop after verification and summarize the verified unit to the user before any commit. diff --git a/name-dictionary.el b/name-dictionary.el deleted file mode 100644 index a9bb86f..0000000 --- a/name-dictionary.el +++ /dev/null @@ -1,25 +0,0 @@ -;; -*- lexical-binding: t; -*- -;; Persistent name shortcuts used by abbrev and CAPF compatibility. - -(setq ss/name-dictionary-entries - '((:name "Ajay Shirke" :abbrev ";ajs") - (:name "Akash Ali" :abbrev ";aka") - (:name "Anant Sharma" :abbrev ";ans") - (:name "Anish Kapoor" :abbrev ";ank") - (:name "Ashish Pawar" :abbrev ";asp") - (:name "Atilla Gul" :abbrev ";atg") - (:name "Harjeet Singh" :abbrev ";has") - (:name "Ilayaraja Selvaraju" :abbrev ";ils") - (:name "Jaganmohanrao Peddada" :abbrev ";jap") - (:name "Karthik Seelam" :abbrev ";kas") - (:name "Kashif Hussain" :abbrev ";kah") - (:name "Kenny Xu" :abbrev ";kex") - (:name "Krishnaraj Muralidharan" :abbrev ";krm") - (:name "Manmohan Verma" :abbrev ";mav") - (:name "Mudit Sharma" :abbrev ";mus") - (:name "Munesh Wali" :abbrev ";muw") - (:name "Naresh Kumar Patro" :abbrev ";nap") - (:name "Ramesh Sugandh Mallela" :abbrev ";ram") - (:name "Shailesh Borse" :abbrev ";shb") - (:name "Vinay Deo" :abbrev ";vid") - (:name "Vishnu Kaarthi Thangadurai" :abbrev ";vit"))) |
