1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
#+TITLE: Utility Helper Inventory
#+AUTHOR: Craig Jennings
#+DATE: 2026-05-10
* Status
Living inventory. Phase 1 of [[file:utility-consolidation.org][utility-consolidation.org]]. Records the current state of helpers identified in the spec's Candidate Extraction Table plus any new candidates discovered during module walkthroughs. Decisions become concrete tasks in =todo.org= for Phase 2+.
* Scope
Phase 1 inventories the candidates from the spec's Candidate Extraction Table. The spec explicitly does not require listing every private helper across the codebase before Phase 2 can start -- new candidates surface and are recorded as they show up.
* Methodology
For each helper:
- Read the function definition for arguments and body.
- Note dependencies (=require= statements, calls to other modules, built-ins).
- Note side effects (process, file I/O, network, message log, none).
- Search the tree for callers (modules and tests).
- Locate the test file(s) covering the helper.
- Decide: =Migrate= / =Leave= / =Defer=, plus a rationale.
Caller counts in the inventory below reflect grep results from 2026-05-10. The columns "Tests" and "Callers" record what actually references the symbol; a test that loads a defining module without naming the symbol does not count.
* Inventory
** Executable Discovery
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/mail--executable-or-warn= | =mail-config.el:66= | private | =executable-find=, =display-warning= | =display-warning= | =system-lib.el= / =cj/executable-find-or-warn= | 0 (used inline within =mail-config.el=) | none | High | Migrate | Mail-specific name hides a generally useful pattern. Mail, language tools, and dirvish wallpaper/file-manager commands all need "find this program or surface a clear missing-dep warning." |
| =cj/executable-exists-p= | =system-lib.el:13= | public | =executable-find= | none (returns path) | =system-lib.el= / =cj/executable-available-p= | =custom-buffer-file.el= (1) | =test-system-lib-executable-exists-p.el= | Medium | Migrate | Current name claims a predicate but returns a path. Rename to =-available-p= and return =t=/=nil=, keep one-cycle alias. |
| direct =(executable-find ...)= without warning | =prog-c.el=, =prog-go.el=, =prog-python.el=, =prog-shell.el=, =dirvish-config.el=, =browser-config.el=, =mail-config.el= | n/a | =executable-find= | sometimes silent nil | =cj/executable-find-or-warn= or =cj/executable-available-p= | many | n/a | High | Migrate selectively | User-invoked features should warn on missing executables; package =:if= silent checks should stay silent. |
** Shell Argument Quoting
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/--f6-shell-safe-argument-regexp= | =dev-fkeys.el:337= (defconst) | private | none | none | =system-lib.el= / =cj/shell-safe-argument-regexp= | 1 (defining file) | none | High | Migrate | Internal regex paired with the readable-quote function. Move with its consumer. |
| =cj/--f6-shell-quote-argument= | =dev-fkeys.el:340= | private | =shell-quote-argument=, the regexp above | none | =system-lib.el= / =cj/shell-quote-argument-readable= | 1 (defining file) | none | High | Migrate | Generates compile/test commands where readable paths matter for log inspection. F6 dispatch is one of several future callers (see =mail-config=, =dirvish-config=). |
| direct =shell-quote-argument= in command strings | =prog-c.el=, =prog-python.el=, =prog-shell.el=, =mail-config.el=, =dirvish-config.el=, =vc-config.el=, =elfeed-config.el=, =system-utils.el= | n/a | =shell-quote-argument= | none | case-by-case | many | n/a | Medium | Defer | Audit each call site after =cj/shell-quote-argument-readable= lands; some are security-sensitive and should keep =shell-quote-argument= as-is. |
** Process Runner
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/--coverage-git-string= | =coverage-core.el:200= | private | =process-file=, =with-temp-buffer=, =user-error= | runs git via =process-file= | =system-lib.el= or new =cj-process.el= / =cj/process-output-or-error= | 2 (within =coverage-core.el=) | covered indirectly by coverage tests | High | Migrate | Generic "run program with argv -> stdout, raise user-error with status+output on non-zero" pattern. Future callers: reconcile-open-repos, vc-config, mail integrations. |
| =cj/--coverage-git-string= (wrapper) | =coverage-core.el= | private | the generic above | runs git | =system-lib.el= or =cj-process.el= / =cj/git-output-or-error= | 2 | n/a | High | Migrate | Thin wrapper around the generic runner with =git= as the program. |
| =cj/--coverage-git-merge-base= | =coverage-core.el:213= | private | =cj/--coverage-git-string= | runs git | =coverage-core.el= (stay) | 1 (within file) | covered by coverage tests | Low | Leave | Coverage-specific semantics. May call the generic runner once it exists, but stays in coverage-core. |
| =cj/--coverage-git-diff= | =coverage-core.el:221= | private | =cj/--coverage-git-string= | runs git | =coverage-core.el= (stay) | 1 (within file) | covered by coverage tests | Low | Leave | Coverage-specific =--unified=0= policy. |
** File / Path Helpers
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/--file-from-context= | =system-utils.el:58= | private | =dired-get-filename=, =buffer-file-name= | none (reads buffer state) | =system-lib.el= / =cj/file-from-context= | 1 (within =system-utils.el=) | =test-system-utils--file-from-context.el= | High | Migrate | Useful for any "current Dired entry or current buffer's file" command. Tests already exist; rename + re-home is straightforward. |
| =cj/test--file-in-directory-p= | =test-runner.el:160= | private | =file-in-directory-p= (built-in) | none | built-in | 1 (within file) | none | Medium | Migrate (delete) | Wraps the built-in for no apparent reason. Replace caller with =file-in-directory-p= directly. |
| =cj/test--assert-inside-base= | =testutil-general.el:41= | test-only | =file-in-directory-p= | signals error | =system-lib.el= / =cj/path-assert-in-directory= | 2 (within file) | =testutil-general.el= itself | Medium | Defer | Useful pattern for destructive commands, but currently test-only. Extract when a production caller appears (e.g. =safe-recursive-delete=). |
| =cj/test--safe-base-dir-p= | =testutil-general.el:30= | test-only | path predicates | none | =system-lib.el= / =cj/safe-recursive-delete-root-p= | 1 (within file) | =testutil-general.el= itself | Medium | Defer | Policy-heavy ("is this dir safe to recursively delete from"). Should be explicit and well-tested before any production use. |
** External Open
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/--open-with-is-launcher-p= | =system-utils.el:70= | private | =string-match-p= | none | =external-open.el= / =cj/external-open-launcher-p= | 1 (within =system-utils.el=) | =test-system-utils--open-with-is-launcher-p.el= | Medium | Defer | Move when external-open consumers (dirvish, mail) align on a single owner; not blocking. |
| =cj/identify-external-open-command= | =system-utils.el:103= | public | =executable-find=, system-type | none | =external-open.el= / =cj/external-open-command= | 1 (within file) | =test-system-utils-identify-external-open-command.el= | Medium | Migrate | External-open should own command-string resolution; host-environment stays predicate-only. |
| duplicated OS-open command selection | =system-utils.el=, =dirvish-config.el=, =external-open.el= | n/a | =executable-find=, system-type | external process | =external-open.el= / =cj/external-open-command= | many | several | Medium | Migrate | One source of truth for =xdg-open=, =open=, =start=. The dirvish refactor I just shipped extracts =cj/--file-manager-program-for=; that helper merges naturally into =cj/external-open-command= once external-open is the owner. |
** Org-Safe Text Helpers
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =calendar-sync--sanitize-org-body= | =calendar-sync.el:297= | private | =replace-regexp-in-string= | none | =cj-org-text.el= or =system-lib.el= / =cj/org-sanitize-body-text= | 1 (within file) | =test-calendar-sync--sanitize-org-body.el= | High | Migrate | Already tested. Useful for webclipper, AI conversations, mail-to-org capture. Pure string work; no Org dependency. |
| =calendar-sync--sanitize-org-property-value= | =calendar-sync.el:308= | private | =replace-regexp-in-string= | none | =cj-org-text.el= or =system-lib.el= / =cj/org-sanitize-property-value= | 1 (within file) | =test-calendar-sync--sanitize-org-body.el= | High | Migrate | Strips characters that would break Org property syntax. Pure string. |
| =calendar-sync--sanitize-org-heading= | =calendar-sync.el:317= | private | =replace-regexp-in-string= | none | =cj-org-text.el= or =system-lib.el= / =cj/org-sanitize-heading= | 1 (within file) | =test-calendar-sync--sanitize-org-body.el= | High | Migrate | Protects outline structure from external text. Pure string. |
| =calendar-sync--strip-html= | =calendar-sync.el:271= | private | =replace-regexp-in-string= | none | =system-lib.el= or =cj-text.el= / =cj/text-strip-html= | 1 (within file) | =test-calendar-sync--strip-html.el= | Medium | Defer | Useful beyond calendar, but the regex-only approach is intentionally simple; document its limits before promoting. |
| =calendar-sync--clean-text= | =calendar-sync.el:291= | private | the two helpers above | none | =system-lib.el= or =cj-text.el= / =cj/text-clean-external= | 1 (within file) | =test-calendar-sync--clean-text.el= | Medium | Defer | Combines ICS unescape + HTML strip; too calendar-specific without splitting. |
| =calendar-sync--unescape-ics-text= | =calendar-sync.el:258= | private | =replace-regexp-in-string= | none | =calendar-sync.el= (stay) | 1 (within file) | =test-calendar-sync--unescape-ics-text.el= | Low | Leave | ICS-specific. Not a general utility. |
** Cache Abstraction
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/modeline-vc-cache-*= helpers (key/get/put/clear/valid-p) | =modeline-config.el:108-140= | private | buffer-local vars | mutates buffer-local state | =cj-cache.el= / =cj/cache-valid-p=, =cj/cache-get=, =cj/cache-put=, =cj/cache-clear= | 1 (within file) | =test-modeline-config-vc-cache.el= | Medium | Defer | Good pattern, but variable-local cache shape differs from the agenda/refile caches. Needs design before extraction. Spec calls out a Phase 5 design addendum at =docs/design/cache-helper-design.org=. |
| agenda/refile cache vars and build flags | =org-agenda-config.el=, =org-refile-config.el= | n/a | timers, file scans | scans filesystem, sets vars | =cj-cache.el= / =cj/cache-value-or-rebuild= | 2 | none | Medium | Defer | TTL/build/invalidate lifecycle; higher risk than the modeline cache. Same Phase 5 work. |
** Logging / Warnings
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/log-silently= | =system-lib.el:20= | public | =message= with =inhibit-message= | writes to *Messages* without echo-area flash | =system-lib.el= / =cj/message-log-only= | 10 (=elfeed-config=, =media-utils=, =org-agenda-config-debug=, =quick-video-capture=, =wrap-up=) | several test files reference it | Low | Defer | Rename is clearer but low-value. Only do when touching system-lib for a higher-priority change. |
| direct =display-warning= boilerplate | =mail-config.el= | n/a | =display-warning= | warning entry | =system-lib.el= / =cj/display-warning-once= or =cj/warn-once= | 1+ | none | Low | Defer | Single caller today. Add only after a second caller appears or once-only behavior becomes a real need. |
** Macros / Debug Helpers
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =with-timer= | =config-utilities.el:50= (defmacro) | public | =current-time=, =message= | times forms, messages elapsed | =config-utilities.el= (stay for now) | 0 in production; 1 test | =test-config-utilities--with-timer.el= | Low | Defer | Debug-oriented today. Promote only after a production caller appears. |
| =cj/--benchmark-method= | =config-utilities.el:64= | private | =benchmark-call= | runs forms, messages timing | =config-utilities.el= (stay) | 0 outside file | =test-config-utilities--benchmark-method.el= | Low | Leave | Debug command helper. Not general architecture. |
| =cj/--delete-compiled-files-in-dir= | =config-utilities.el:123= | private | =directory-files-recursively=, =delete-file= | deletes files | =config-utilities.el= (stay until safe second caller) | 0 outside file | =test-config-utilities--delete-compiled-files-in-dir.el= | Low | Defer | Destructive. A second caller plus a path-safety contract should land before promoting. |
** Theme File I/O
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/theme-read-file-contents= | =ui-theme.el:66= | public | =insert-file-contents= | reads file | =system-lib.el= / =cj/read-file-string= | 0 outside file | =test-ui-theme-persistence.el= | Low | Defer | One production caller. Built-in =insert-file-contents= wrappers are small; keep theme-specific until reused. |
| =cj/theme-write-file-contents= | =ui-theme.el:75= | public | =write-region= | writes file | =system-lib.el= / =cj/write-file-string= | 0 outside file | =test-ui-theme-persistence.el= | Low | Defer | Same as above. |
** String / Modeline Helpers
| Symbol | File | Vis | Deps | Side effects | Proposed home / name | Callers (modules) | Tests | Pri | Decision | Rationale |
|--------+------+-----+------+--------------+----------------------+-------------------+-------+-----+----------+-----------|
| =cj/modeline-string-cut-middle= | =modeline-config.el:59= | public | =substring=, length math | none | =system-lib.el= or =cj-text.el= / =cj/string-truncate-middle= | 1 (within file) | =test-modeline-config-string-cut-middle.el= | Low | Defer | Single production caller. Good candidate when completion/headings/report buffers need ellipsis-in-middle truncation. |
* Decisions Summary
| Action | Count | Examples |
|--------+-------+----------|
| Migrate | 11 | =cj/--file-from-context=, three calendar-sync sanitizers, =cj/executable-find-or-warn=, =cj/shell-quote-argument-readable=, process runner pair, =cj/external-open-command= |
| Leave | 3 | =cj/--coverage-git-merge-base=, =cj/--coverage-git-diff=, =calendar-sync--unescape-ics-text= |
| Defer | 13 | cache helpers (need design addendum), test-only helpers awaiting production caller, low-value renames, theme/string/HTML extractions awaiting second caller |
* Concrete Next Actions
These become =todo.org= entries (or update existing ones) as Phase 2 starts.
** Phase 2 candidates (already in spec's recommended order)
1. *Extract* =cj/executable-find-or-warn= into =system-lib.el=. Move =cj/mail--executable-or-warn= and rename. Migrate user-invoked features in =mail-config.el=, =prog-*.el=, =dirvish-config.el=, =browser-config.el= as appropriate.
2. *Rename* =cj/executable-exists-p= -> =cj/executable-available-p=, return boolean, keep one-cycle alias.
3. *Extract* =cj/shell-quote-argument-readable= and the paired regexp into =system-lib.el=. Move =cj/--f6-shell-quote-argument= and the constant.
4. *Extract* =cj/process-output-or-error= (generic argv -> stdout / user-error) and =cj/git-output-or-error= (thin wrapper) into =system-lib.el= or =cj-process.el=. Migrate =cj/--coverage-git-string= callers.
5. *Extract* =cj/file-from-context= into =system-lib.el=.
6. *Replace* =cj/test--file-in-directory-p= caller with built-in =file-in-directory-p=, then delete the wrapper.
** Phase 3 candidates (Org-safe text)
7. *Extract* =cj/org-sanitize-heading=, =cj/org-sanitize-property-value=, =cj/org-sanitize-body-text= into =cj-org-text.el= (new) or =system-lib.el=. Migrate =org-webclipper.el= and =ai-conversations.el= as second consumers; mail capture if applicable.
** Phase 4 candidates (External-open consolidation)
8. *Move* =cj/identify-external-open-command= to =external-open.el= as =cj/external-open-command=. Consolidate the duplicated OS-open dispatch from =system-utils.el=, =dirvish-config.el=, and (already-shipped) =cj/--file-manager-program-for= into one source of truth.
** Deferred (track in =todo.org= but no commit yet)
- Cache abstraction (modeline + agenda/refile) -- needs Phase 5 design addendum at =docs/design/cache-helper-design.org=.
- =cj/--open-with-is-launcher-p= -- move when external-open ownership is finalized.
- =cj/log-silently= rename -- low value; do during incidental =system-lib= work.
- HTML/text helpers (=strip-html=, =clean-text=) -- defer until a second consumer.
- Theme file I/O wrappers -- defer until a second consumer.
- =cj/string-truncate-middle= -- defer until a second consumer.
- =cj/path-assert-in-directory= and =cj/safe-recursive-delete-root-p= -- defer until a production caller justifies promotion.
- =with-timer=, =cj/--delete-compiled-files-in-dir=, =cj/display-warning-once= -- defer until a clear second caller.
* Discoveries Worth Recording
- =cj/--file-manager-program-for= already exists in =vterm-config.el= (post-split: =modules/vterm-config.el=) -- wait, the dirvish refactor put it in =modules/dirvish-config.el=. It's the new form of the OS-dispatch consolidation. The =cj/external-open-command= work in Phase 4 should fold this helper in rather than re-deriving it.
- =cj/log-silently= has 10 production callers, more than the spec's table suggested. The rename's churn cost is real; defer is the right call.
- The three calendar sanitizers (=org-body=, =org-property-value=, =org-heading=) all share one test file (=test-calendar-sync--sanitize-org-body.el=). When moved, the tests should also move and split per helper for clarity.
|