diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | abbrev_defs | 2 | ||||
| -rw-r--r-- | config.org | 145 | ||||
| -rw-r--r-- | docs/plans/2026-04-07-people-roster-design.md | 151 | ||||
| -rw-r--r-- | name-dictionary.el | 44 |
6 files changed, 196 insertions, 163 deletions
@@ -6,3 +6,5 @@ !/config.org !/abbrev_defs !/name-dictionary.el +!/docs/ +!/docs/plans/ @@ -56,17 +56,20 @@ The minibuffer stack is intentionally small: - `marginalia` adds annotations. - `corfu` handles in-buffer completion popups for text and Org buffers. -Name entry uses two separate paths: +Name entry uses fixed abbrevs plus the roster: - `abbrev` provides deterministic one-shot shortcuts for fixed name expansions. -- a small CAPF feeds Corfu a fixed list of name variants from `name-dictionary.el`. -- `M-x ss/name-dictionary-add-name` and `M-x ss/name-dictionary-remove-name` update that file and refresh the current prose buffers. +- 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. ### 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 name shortcut list lives in `name-dictionary.el` so it can be managed from Emacs with a single source of truth. +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. ### Babel tangle process @@ -118,10 +121,12 @@ This means the agenda reflects the current project and area files at runtime ins - daily notes - daily meetings - Denote-backed captures for generic notes, projects, areas, people, and resources +- structured roster captures for manager-facing people data 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. ### Note creation and linking @@ -130,8 +135,10 @@ 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 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. 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. diff --git a/abbrev_defs b/abbrev_defs index 055c8c8..64bee34 100644 --- a/abbrev_defs +++ b/abbrev_defs @@ -1 +1 @@ -;; Persistent abbrev definitions written by Emacs. +;;-*-coding: utf-8;-*- @@ -332,128 +332,12 @@ reports. (defvar ss/people-roster--cache-mtime nil "Modification time of the cached roster entries.") - (defun ss/people-roster--split-values (value) - "Split VALUE on commas or semicolons 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))))) - - (defun ss/people-roster--normalize-engagement (employee-time) - "Map the CSV EMPLOYEE-TIME field to a roster engagement category." - (cond ((or (null employee-time) (string-empty-p employee-time)) nil) - ((string-match-p "\\`Perm\\'" employee-time) "permanent") - ((string-match-p "\\`NCS India\\'" employee-time) "permanent") - ((string-match-p "\\`SOW" employee-time) "sow") - (t (downcase employee-time)))) - - (defun ss/people-roster--csv-header-key (header) - "Convert a CSV HEADER to a plist keyword." - (intern - (concat ":" - (replace-regexp-in-string - "[^[:alnum:]]+" "-" - (downcase (string-trim header)))))) - - (defun ss/people-roster--csv-row-values (line) - "Split a CSV LINE into trimmed values." - (mapcar #'string-trim - (split-string (string-trim-right line "\r") "," nil))) - - (defun ss/people-roster--csv-row-plist (headers values) - "Zip HEADERS and VALUES into a plist." - (let ((plist nil)) - (while headers - (setq plist (plist-put plist (pop headers) (or (pop values) "")))) - plist)) - - (defun ss/people-roster--csv-rows () - "Return the rows from `ss/people-roster-source-file'." - (when (file-exists-p ss/people-roster-source-file) - (with-temp-buffer - (insert-file-contents ss/people-roster-source-file) - (let* ((lines (split-string (buffer-string) "\n" t)) - (headers (mapcar #'ss/people-roster--csv-header-key - (ss/people-roster--csv-row-values (car lines))))) - (mapcar (lambda (line) - (ss/people-roster--csv-row-plist - headers - (ss/people-roster--csv-row-values line))) - (cdr lines)))))) - - (defun ss/people-roster--csv-row->entry (row) - "Convert CSV ROW to a roster entry plist." - (let ((name (string-trim (or (plist-get row :name) ""))) - (role (string-trim (or (plist-get row :role) - (plist-get row :title) - ""))) - (employee-time (string-trim (or (plist-get row :employee-time) ""))) - (team (string-trim (or (plist-get row :team) ""))) - (abbrev (string-trim (or (plist-get row :abbrev) ""))) - (aliases (ss/people-roster--split-values (plist-get row :aliases))) - (manager (string-trim (or (plist-get row :manager) ""))) - (email (string-trim (or (plist-get row :email) ""))) - (location (string-trim (or (plist-get row :location) ""))) - (engagement (string-trim - (or (plist-get row :engagement) - (ss/people-roster--normalize-engagement employee-time) - "")))) - (list :name name - :abbrev (if (string-empty-p abbrev) - (ss/name-dictionary-default-abbrev name) - abbrev) - :aliases aliases - :role role - :employee-time employee-time - :engagement engagement - :team team - :manager manager - :email email - :location location))) - (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) "")) - (defun ss/people-roster--csv-entry-to-org (entry) - "Render ENTRY as an Org headline with properties." - (let ((name (ss/people-roster--entry-name entry)) - (abbrev (ss/people-roster--entry-abbrev entry)) - (aliases (ss/people-roster--entry-aliases entry)) - (role (ss/people-roster--entry-role entry)) - (employee-time (plist-get entry :employee-time)) - (engagement (ss/people-roster--entry-engagement entry)) - (team (ss/people-roster--entry-team entry)) - (manager (ss/people-roster--entry-manager entry)) - (email (ss/people-roster--entry-email entry)) - (location (ss/people-roster--entry-location entry))) - (concat - "* " name "\n" - ":PROPERTIES:\n" - (ss/people-roster--org-property-line "NAME" name) - (ss/people-roster--org-property-line "ABBREV" abbrev) - (ss/people-roster--org-property-line "ALIASES" (mapconcat #'identity aliases "; ")) - (ss/people-roster--org-property-line "EMPLOYEE-TIME" employee-time) - (ss/people-roster--org-property-line "ROLE" role) - (ss/people-roster--org-property-line "ENGAGEMENT" engagement) - (ss/people-roster--org-property-line "TEAM" team) - (ss/people-roster--org-property-line "MANAGER" manager) - (ss/people-roster--org-property-line "EMAIL" email) - (ss/people-roster--org-property-line "LOCATION" location) - ":END:\n\n"))) - - (defun ss/people-roster--seed-from-csv () - "Write the roster file from the CSV seed data." - (let ((entries (mapcar #'ss/people-roster--csv-row->entry - (ss/people-roster--csv-rows)))) - (with-temp-file ss/people-roster-file - (insert "#+title: People roster\n\n") - (dolist (entry entries) - (insert (ss/people-roster--csv-entry-to-org entry)))) - entries)) - (defun ss/people-roster--entry-name (entry) "Return the canonical name in ENTRY." (plist-get entry :name)) @@ -494,6 +378,13 @@ reports. "Return the location in ENTRY." (plist-get entry :location)) + (defun ss/people-roster--split-values (value) + "Split VALUE on commas or semicolons 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))))) + (defun ss/people-roster--entry-summary (entry) "Return a one-line summary for ENTRY." (string-join @@ -525,24 +416,10 @@ reports. "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) - (if (file-exists-p ss/people-roster-source-file) - (ss/people-roster--seed-from-csv) - (with-temp-file ss/people-roster-file - (insert "#+title: People roster\n\n")))) + (with-temp-file ss/people-roster-file + (insert "#+title: People roster\n\n"))) ss/people-roster-file) - (defun ss/people-roster-seed-from-csv (&optional overwrite) - "Seed the roster file from the CSV source. -With OVERWRITE, rebuild the roster even when it already exists." - (interactive "P") - (when (and (file-exists-p ss/people-roster-file) (not overwrite)) - (user-error "Roster already exists; use C-u M-x ss/people-roster-seed-from-csv to rebuild it")) - (unless (file-exists-p ss/people-roster-source-file) - (user-error "Missing seed CSV: %s" ss/people-roster-source-file)) - (ss/people-roster--seed-from-csv) - (ss/people-roster-reload) - (message "Seeded people roster from CSV")) - (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) @@ -975,10 +852,6 @@ directly during startup rather than creating it on demand. (expand-file-name "areas/people/roster.org" ss/org-directory) "Structured roster of people and role metadata.") - (defconst ss/people-roster-source-file - (expand-file-name "people.csv" user-emacs-directory) - "CSV seed data for the roster.") - (defconst ss/org-resources-directory (expand-file-name "resources/" ss/org-directory) "Directory for resource notes.") diff --git a/docs/plans/2026-04-07-people-roster-design.md b/docs/plans/2026-04-07-people-roster-design.md new file mode 100644 index 0000000..f68b140 --- /dev/null +++ b/docs/plans/2026-04-07-people-roster-design.md @@ -0,0 +1,151 @@ +# People Roster Design + +## Goal + +Turn the managed-people list into a small, searchable database inside Emacs. +The setup should make it easy to: + +- look up a person quickly by name +- see role and engagement type at a glance +- filter the roster by manager, role, or employment type +- keep notes and follow-ups close to the record +- avoid duplicating the same information across multiple files + +## Recommendation + +Use a single Org file as the source of truth and layer completion plus search on top of it. + +Recommended stack: + +- `org-mode` for storage +- `consult` for fast lookup +- `corfu` or `completion-at-point` for inline name completion when typing inside notes +- `embark` for actions on a selected person +- `org-ql` for filtered views and reports + +This keeps the data editable in plain text while still making it queryable and useful as a working tool. + +## Data Model + +Store one person per Org heading in `~/org/areas/people/roster.org`. + +Suggested properties: + +- `NAME` +- `ROLE` +- `ENGAGEMENT` with values such as `permanent`, `sow`, or `other` +- `TEAM` +- `MANAGER` +- `EMAIL` +- `LOCATION` +- `NOTES` + +Example: + +```org +* Ajay Shirke +:PROPERTIES: +:NAME: Ajay Shirke +:ROLE: Engineering Manager +:ENGAGEMENT: permanent +:TEAM: Platform +:MANAGER: You +:EMAIL: ajay@example.com +:END: + +Current context, follow-ups, or other notes go here. +``` + +## Lookup Workflow + +Add a command that prompts for a person and opens their entry. + +Typical flow: + +1. `M-x ss/people-find` +2. type a name, role, or partial string +3. preview matching entries +4. open the selected person + +The lookup command should display useful summary text in the completion UI, such as: + +- full name +- role +- engagement type +- team + +That makes the list useful even before opening the record. + +## Inline Completion + +Use Corfu only for in-buffer completion when typing names in notes, tasks, or meeting logs. + +The CAPF should: + +- trigger on word boundaries +- offer canonical names from the roster +- annotate candidates with role and engagement type +- stay read-only and avoid changing the buffer + +This is a narrow use of completion: it helps insert a person’s name without turning the completion layer into the database itself. + +## Actions + +Use Embark-style actions on a selected person so lookup is not a dead end. + +Useful actions: + +- open the person’s Org entry +- copy a short summary +- insert the name into the current buffer +- insert a formatted roster card +- jump to related notes or follow-ups + +This makes completion a launcher for common manager tasks. + +## Reports + +Use `org-ql` or equivalent Org searches for views that answer manager questions. + +Useful reports: + +- all `permanent` people +- all `sow` people +- people grouped by role +- people grouped by manager +- people with notes older than a threshold + +These reports should be generated from the same Org file rather than maintained separately. + +## File Layout + +Planned files: + +- `config.org` + - adds the lookup commands, completion functions, and report helpers +- `~/org/areas/people/roster.org` + - stores the roster entries + +Optional later additions, only if needed: + +- `~/org/areas/people/notes/` + - supporting notes for long-lived context + +## Trade-offs + +Why this approach: + +- one source of truth +- plain-text editing +- easy search and filtering +- works well with existing Org habits + +What it avoids: + +- a separate database format to learn +- duplicated JSON or Lisp data files +- maintaining one file for lookup and another for reporting + +## Next Step + +Implement the roster helpers in `config.org`, then add the first few people entries to `~/org/areas/people/roster.org`. diff --git a/name-dictionary.el b/name-dictionary.el index ccad42a..a9bb86f 100644 --- a/name-dictionary.el +++ b/name-dictionary.el @@ -1,25 +1,25 @@ ;; -*- lexical-binding: t; -*- -;; Persistent name entries used by abbrev and CAPF. +;; 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"))) + '((: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"))) |
