aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* fix(font-config): theme-aware browser labels and daemon-safe emoji fontsetCraig Jennings2026-05-252-12/+46
| | | | | | Two font-config robustness fixes. The font-browser (cj/display-available-fonts) hardcoded a "Light Blue" foreground for each family label, which goes nearly unreadable on a light theme. I switched it to font-lock-keyword-face so the label follows the theme's contrast, keeping it bold. The emoji-fontset cond ran once at module load behind (env-gui-p). In daemon mode there's no GUI frame at load, so env-gui-p is nil and the fontset never gets set — a later emacsclient -c GUI frame then has no emoji font. I wrapped it in cj/setup-emoji-fontset (GUI-guarded, idempotent) and, mirroring how the fontaine preset is already applied, run it from server-after-make-frame-hook in daemon mode and directly otherwise. The daemon TTY-then-GUI path can't be exercised in batch, so I left a manual-test entry for it.
* docs(todo): tag babel-confirm, health-check, module-ownership, and strapdown ↵Craig Jennings2026-05-251-4/+4
| | | | tasks :discuss:
* docs(todo): close mousetrap keymap cache and formatter argv tasksCraig Jennings2026-05-251-19/+23
|
* refactor(prog): run JSON/YAML/webdev formatters via argv, not a shellCraig Jennings2026-05-256-47/+213
| | | | | | | | cj/json-format-buffer, cj/yaml-format-buffer, and cj/webdev-format-buffer ran their formatters through shell-command-on-region, which goes via a shell. I moved each to call-process-region with an explicit program and argv list, so a filename or buffer content can't be word-split or read as shell syntax. The webdev path dropped its shell-quote-argument dance once the filename became a plain argv element. Point preservation is unchanged. One deliberate improvement, and it's tested: shell-command-on-region with replace replaced the buffer with the formatter's error text on a non-zero exit. The new per-formatter helper captures output to a temp buffer, checks the exit code, replaces only on success, and otherwise raises a user-error carrying stderr — so a failed format leaves the buffer alone. I kept a small format-region helper in each of the three modules rather than one shared helper. They have no common module to live in short of system-lib, and coupling three unrelated domain modules through it wasn't worth saving sixteen lines.
* perf(mousetrap): cache built keymaps per profileCraig Jennings2026-05-252-2/+116
| | | | | | mouse-trap--build-keymap ran on every major-mode hook and rebuilt the whole keymap (~8 prefixes by ~30 events) from scratch each time, so rapid mode-switching paid that cost over and over. I moved the build into mouse-trap--build-keymap-1 and cache its result in mouse-trap--keymap-cache, keyed on the profile name plus its allowed-categories list. The same profile reuses the cached keymap, and editing a profile's categories changes the key so it rebuilds. Sharing one keymap object across buffers is safe here: the map only binds disallowed events to ignore and is never mutated after it's built. Added mouse-trap--clear-keymap-cache to force a fresh build after editing profiles by hand.
* docs(todo): log external-tool guards across Org export/publishing modulesCraig Jennings2026-05-251-12/+19
|
* fix(org): guard external-tool assumptions in export and publishing commandsCraig Jennings2026-05-259-2/+130
| | | | | | | | | | | Four export/publishing commands shelled out to external tools without checking they exist, so a missing tool surfaced as an opaque process error — or, for reveal.js, a silently broken presentation. I added a command-time guard to each that names the tool and what's needed: - zathura, in the pandoc PDF export-and-open command - the hugo binary and the platform file-manager opener, in hugo-config - the local reveal.js checkout (run scripts/setup-reveal.sh), shared by the reveal export and preview commands - pandoc, in the web-clip protocol handler The checks run only when the command runs, so startup stays quiet. Each guard has a test asserting the user-error fires when the tool is absent, and the existing happy-path tests now stub the lookups so they exercise the real path rather than tripping the new guard.
* docs(todo): log prog-lisp smoke coverage and the prog-general/prog-training ↵Craig Jennings2026-05-251-12/+16
| | | | dispositions
* test(prog-lisp): cover the elisp and Common Lisp mode-setup functionsCraig Jennings2026-05-251-0/+66
| | | | | | prog-lisp.el had no tests. I added four for the config it owns directly: cj/elisp-setup and cj/common-lisp-setup are each registered on their mode hook and set the buffer-local conventions this config picks (4-space, no tabs, fill-column 120 for elisp; 2-space, fill-column 100 for Common Lisp). The module loads with use-package stubbed to a no-op, so no packages load, ensure, or download during the run. That keeps it batch-safe and independent of which Lisp packages happen to be installed.
* docs(todo): log mu4e org-contacts completion coverage under hardeningCraig Jennings2026-05-251-9/+11
|
* test(mu4e-org-contacts): cover the completion-at-point and TAB dispatch logicCraig Jennings2026-05-251-0/+135
| | | | | | mu4e-org-contacts-integration.el had no tests. I added ten characterization tests for the completion glue. The capf only fires inside a header field of a compose buffer, so I check it both ways (wrong mode, wrong field) plus the bounds and table it returns when contacts exist and the empty-contacts case. TAB dispatches three ways depending on context, so each branch gets a test: completion-at-point in a header, org-cycle in the org-msg body, indent elsewhere. Comma completion and the direct-insert path round it out. The header predicate, the mode actions, and cj/get-all-contact-emails are stubbed, so the run stays headless with no mu4e or org-contacts dependency.
* docs(todo): log font-config smoke coverage; route popper smoke test to its ↵Craig Jennings2026-05-251-8/+20
| | | | decision task
* test(font-config): smoke-cover the install check and daemon-frame applierCraig Jennings2026-05-251-0/+74
| | | | | | font-config.el had no direct tests. I added four: cj/font-installed-p returns t or nil depending on find-font, and cj/apply-font-settings-to-frame is a no-op on a non-GUI frame and applies the preset exactly once per frame, so reopening a daemon frame doesn't restack it. find-font, env-gui-p, and fontaine-set-preset are stubbed so the run stays headless. The module :demand's fontaine and all-the-icons, so a skip-unless on those packages keeps a bare checkout green while the tests still run wherever the fonts are installed.
* docs(todo): log system-defaults settings smoke tests under hardeningCraig Jennings2026-05-251-14/+13
|
* test(system-defaults): cover custom-file, backups, and GC-hook registrationCraig Jennings2026-05-253-49/+142
| | | | | | system-defaults.el had no coverage for its settings, only its functions (in test-system-defaults-functions.el) and the vc-follow-symlinks default. I added three settings smoke tests: custom-file is redirected to a temp trashbin rather than the repo, backups land under user-emacs-directory/backups, and the minibuffer GC hooks are actually wired onto the minibuffer hooks. I pulled the sandbox loader the vc-follow-symlinks test had inline into tests/testutil-system-defaults.el so both files share one copy. The backups test clears cj/backup-directory before loading — it's a defvar, so once an earlier test loads the module it keeps that first sandbox path and won't recompute, which made the assertion fail until I forced the recompute.
* feat(ui-theme): default the theme fallback to bundled dupreCraig Jennings2026-05-252-3/+16
| | | | | | The fallback kicks in when persist/emacs-theme is missing — a fresh machine, or one that's never saved a theme. It was modus-vivendi, which ships with Emacs but has none of the dimming colors this config chooses, so an unconfigured machine looked and dimmed differently from a configured one. I hit exactly that on a second box this week. dupre is bundled in themes/ and carries those colors, and it loads wherever this config does, so it's the better default. I added a regression test asserting the default is dupre; its loadability is already covered by test-dupre-theme.el. The docstring no longer claims the fallback must be a built-in theme, since dupre isn't one.
* docs(todo): close the latex-config WIP taskCraig Jennings2026-05-251-1/+4
| | | | I investigated the inherited "WIP need to fix" comment. The module compiles and works, company-auctex is tracked under the corfu migration, and the one real defect (non-idempotent PDF-viewer selection) is fixed in b007a9b8. Closed with a note on what I found.
* fix(latex): make PDF-viewer selection idempotentCraig Jennings2026-05-252-5/+92
| | | | | | cj/--latex-select-pdf-viewer runs on every LaTeX-mode buffer and was blindly pushing an (output-pdf VIEWER) entry onto TeX-view-program-selection, so each LaTeX buffer opened in a session stacked another duplicate. The head still won, so the viewer worked, but the list grew and the docstring's idempotency claim was false. I drop any existing output-pdf entry before consing the chosen viewer, which also makes "wins over any default" actually true. Added a test file (the module had none) covering selection, preference order, the PDF-Tools fallback, idempotency, and default override, with executable-find mocked so the run doesn't depend on which viewers are installed.
* docs(todo): close the byte-compile-paths task and drop the prog-shell taskCraig Jennings2026-05-251-8/+3
| | | | The byte-compile load-paths task shipped as the make compile-file target, so I closed it DONE with a note on what landed and where the parallel hook fix went. I dropped the prog-shell config-home task — it isn't worth tracking as its own item.
* build: add compile-file for single-file byte-compile with the project load pathCraig Jennings2026-05-251-2/+17
| | | | | | Bare emacs -Q --batch byte-compile-file fails on local compile-time requires (undead-buffers for a module, dupre-palette for a theme file) because nothing is on the load path, even though init and the test harness load fine. I added a compile-file target that compiles one file with the project load path (modules, themes, tests, plus package-initialize), and put themes on make compile's path too. Bare emacs -Q stays unsupported by design; compile-file is the documented command, and it guards against a missing FILE. Verified modules/dashboard-config.el and themes/dupre-faces.el both compile through it.
* chore: sync bundled claude rules and git hooksCraig Jennings2026-05-2510-8/+568
| | | | Routine sync of the .claude/rules and git hooks distributed with the language bundle. Adds the cross-project, emacs, interaction, todo-format, triggers, and working-files rules; refreshes the elisp and elisp-testing rules, the elisp validation hook, and the pre-commit hook.
* docs(todo): close shipped tasks and refile completed sub-tasksCraig Jennings2026-05-251-19/+44
| | | | | | I marked the tasks that shipped as DONE: the ai-vterm third-split fix, projectile open-todo in the other window, the elfeed-config byte-compile-safe tests, the org TODO-keyword color theming, and the manual cj/org-finalize-task verification. The load-graph spec and the elfeed-config header annotation are level-4 sub-tasks, so they become dated event-log entries rather than DONE keywords. I also reformatted a few headings, bumped the dashboard over-scroll task to [#D], and filed a task to make the standalone byte-compile load paths match module dependencies.
* feat(dashboard): render the butterfly banner with a transparency maskCraig Jennings2026-05-253-4/+10
| | | | | | The dashboard banner showed the butterfly PNG without its transparency, because the image descriptor was built before the mask props were set. I moved the banner setup (dashboard-startup-banner, dashboard-banner-logo-title) ahead of dashboard-setup-startup-hook and added dashboard-image-extra-props with :mask heuristic, so the mask is in place when the startup hook builds the buffer. If a *dashboard* buffer already exists I refresh it, so the change shows without a restart. I also re-encoded the PNG smaller and kept the previous encoding as M-x_butterfly.png.bak.
* feat(auto-dim): dim vterm windows by blending terminal colorsCraig Jennings2026-05-252-0/+283
| | | | | | | | Window dimming via face-remap never reached vterm. The terminal resolves its own colors per cell while redrawing, so it bypasses the remapped faces, and agent and shell windows stayed bright when they lost focus. I advise vterm--get-color to blend each looked-up color toward the auto-dim faces whenever every window showing the buffer is dimmed. The foreground and background blend amounts are separate defcustoms (foreground stays more legible, background fades harder). After a dim-state change I force a full vterm repaint by briefly nudging the terminal size, because vterm only repaints the rows libvterm marked dirty. A post-command hook and a select-window advice cover the windmove and Shift-arrow focus paths that window-selection-change-functions misses. Tests cover the dimmed-buffer predicate, the color blend, the selection-change scheduling, and the auto-dim-before-repaint ordering.
* fix(theme): register dupre faces so org status colors are themedCraig Jennings2026-05-254-15/+114
| | | | | | | | The dupre theme defined its own faces (dupre-accent, the headings, and the org status faces) only through custom-theme-set-faces, never defface. That leaves them unregistered, so they render through :inherit but silently fail when applied directly as a text property. org-todo-keyword-faces and org-priority-faces apply faces that way, so the org keyword and priority colors never showed as dupre tones. I added a defface registration block to dupre-faces.el for all of dupre's own faces, so they're real faces. The theme still sets their colors. Then I pointed org-todo-keyword-faces and org-priority-faces (in org-config.el) at named dupre-org-* faces, each the closest palette color to its former hard-coded name, and gave each a dimmed variant that auto-dim-config.el swaps in for unfocused windows. A keyword in a dimmed window now shows a darker shade of its own color rather than flat gray or full brightness. A regression test asserts dupre's faces stay registered, since that was the latent bug behind all of this.
* feat(auto-dim): dim non-selected windows via auto-dim-other-buffersCraig Jennings2026-05-256-4/+111
| | | | | | | | I added auto-dim-config, a module that loads my local auto-dim-other-buffers fork and dims windows that don't have focus so the selected window stands out. A non-selected window drops to a pure-black background with faded gray text. The dimmed faces live in the dupre theme (themes/dupre-faces.el) so they track theme switches, and the module remaps default, the font-lock faces, and org-block onto them so syntax-highlighted code fades too rather than staying lit. Fringe is left out because dimming it forces a full-frame refresh that flickers on this non-pgtk build. dim-on-focus-out is nil, so tabbing to a browser or terminal on Hyprland doesn't dim the whole frame. vterm and agent windows don't dim either, because the terminal paints its own per-cell colors past the face remap. I'm keeping that, since the agent's output stays readable while I work in code on the other side. The module loads after the theme, carries a load-graph header, joins the header-contract allowlist, and the inventory moves to 103 of 103 classified.
* docs(load-graph): classify elfeed-config, the last init moduleCraig Jennings2026-05-253-21/+19
| | | | | | elfeed-config was the only init module without a load-graph header. It was deferred because annotating the header triggers a byte-compile, which broke its tests. With that test rewritten to use real structs, I added the header (Layer 4, optional, currently eager but a command-loaded deferral candidate, runtime requires user-constants, system-lib, media-utils), added elfeed-config to the header-contract allowlist, and moved it from the inventory's deferred and pending sections into the Batch 8 table. That brings the inventory to 102 of 102 modules classified, completing the Phase 1 classification pass.
* test(elfeed-config): use real structs so tests survive byte-compilationCraig Jennings2026-05-251-26/+49
| | | | | | | | The cj/elfeed-process-entries tests faked entries as bare symbols and stubbed the elfeed-entry-link accessor, which only works while elfeed-config loads as interpreted source. Byte-compiled, the cl-defstruct accessor inlines to an elfeed-entry-p check plus an aref, so the stub is bypassed and the inlined check rejects the fake entry. Three tests failed the moment a .elc existed. I rewrote the five process-entries tests to build genuine elfeed-entry structs with elfeed-entry--create, calling package-initialize so the installed elfeed lands on the load-path, and guarded them with skip-unless for an environment that lacks the package. The elfeed-search UI boundary is still stubbed. The four extract-stream-url tests are unchanged. This unblocks annotating elfeed-config with its load-graph header, which triggers the byte-compile that surfaced the problem.
* fix(prog-general): open the project todo in the other window when splitCraig Jennings2026-05-252-1/+73
| | | | | | C-c p t (cj/open-project-root-todo) called find-file, which always opened todo.org in the selected window, replacing whatever I was looking at. Now it opens in the other window when the frame is split and in the current window when it isn't, through a small cj/--find-file-respecting-split helper. The helper is a top-level defun rather than buried in the projectile :config block so it can be unit-tested without loading projectile. I left cj/project-switch-actions alone. Opening the todo on a project switch is a different trigger and not what this fixes.
* fix(ai-vterm): reuse the frame's half instead of splitting a thirdCraig Jennings2026-05-255-268/+480
| | | | | | F9 split a third window into a frame that was already divided in two, wedging the agent into the middle or a skinny extra column instead of taking the half it should occupy. The display rule only knew how to reuse a window already showing an agent or to split a fresh one. With a plain two-pane layout it fell through to the split and added a window. I added a display action, cj/--ai-vterm-reuse-edge-window, that reuses the window already forming the target half (the right column on a desktop, the bottom row on a laptop), found by a new cj/window-at-edge helper. It records the displaced buffer with display-buffer-record-window, so toggling off restores that buffer through the native quit-restore-window. The slot's buffer swaps between the agent and whatever it displaced, and no window is created or deleted. The split path still handles a single-window frame or a layout split on the other axis, and the lone fullscreen agent keeps its bury-and-restore-in-place behavior.
* refactor(load-graph): route C-; registration through the keymap APICraig Jennings2026-05-2425-42/+34
| | | | | | Migrated all 31 cj/custom-keymap registration sites across 24 modules from direct (keymap-set cj/custom-keymap ...) calls to cj/register-prefix-map and cj/register-command. Consumers no longer reference cj/custom-keymap directly, so keybindings.el is the sole owner of the C-; prefix and modules reach it only through the API (each already requires keybindings from Phase 2). Behavior-preserving: I dumped every C-; binding before and after the migration and they're identical: 279 bindings, each resolving to the same command. The which-key label blocks are untouched, since they use string key descriptions and never assumed the keymap existed. I byte-compiled all 24 files (no new free-variable warnings, because the cj/custom-keymap references are gone), and make test, validate-modules, and an init load all pass.
* feat(keybindings): add cj/custom-keymap registration APICraig Jennings2026-05-243-1/+72
| | | | | | Phase 3 of the load-graph project. cj/register-prefix-map and cj/register-command bind a prefix map or command under the C-; prefix and register the which-key label once which-key loads. Feature modules will route their registration through these instead of mutating cj/custom-keymap directly, so keybindings.el stays the sole owner of the prefix and modules stop assuming the keymap already exists at load. Adds test-init-keymap-registration.el covering prefix-map and command resolution, the optional label, and invalid-key rejection. No modules are migrated yet; that follows in batches.
* refactor(load-graph): make hidden module dependencies explicitCraig Jennings2026-05-2410-89/+59
| | | | | | | | | | | | | Phase 2 of the load-graph project. I fixed the seven hidden dependencies the classification surfaced, so each module declares what it uses instead of relying on init order. - system-defaults now requires host-environment and user-constants at runtime. They were eval-when-compile only, but env-bsd-p and user-home-dir are read at load, so the compiled module couldn't load standalone. - custom-buffer-file, dev-fkeys, calendar-sync, and video-audio-recording require keybindings and drop their (when (boundp 'cj/custom-keymap) ...) shims. The shim silently dropped the C-; binding when the module loaded before keybindings. The explicit require makes the dependency real. - flycheck-config and mail-config require keybindings for their cj/custom-keymap bindings (a use-package :map and a direct keymap-set). - Removed a dead eval-when-compile (defvar cj/custom-keymap) in transcription-config; nothing there used the variable. No init.el load-order change. keybindings and the foundation modules already load before these, so the requires are no-ops at startup and only fix standalone and test loading. I verified each fix with a fresh emacs --batch (require 'X), then swept all modules standalone: every one loads or fails only with a clear missing-package message. Full make test, make validate-modules, and an init smoke all pass. Module headers and the inventory's hidden-dependency section are updated to mark the seven resolved.
* docs(todo): close module-classification task, split out elfeed-configCraig Jennings2026-05-241-16/+4
| | | | Rewrote the classify-modules child as a dated log entry now that 101 of 102 modules carry load-graph headers. Pulled the lone deferral — annotating elfeed-config — into its own task, blocked on the elfeed test-byte-compile fix.
* docs(load-graph): classify remaining domain and optional modulesload-graph-classify-endCraig Jennings2026-05-2421-30/+249
| | | | | | Final classification batch: the last 19 modules — linear-config, local-repository, lorem-optimum, mail-config, markdown-config, music-config, pdf-config, quick-video-capture, reconcile-open-repos, restclient-config, slack-config, system-commands, telega-config, tramp-config, transcription-config, video-audio-recording, vterm-config, weather-config, wrap-up. I annotated each header, added a Batch 9 table to the inventory, and extended the validation allowlist. 101 of 102 modules are now classified; only elfeed-config remains, deferred on its test fix. Two more hidden dependencies turned up. video-audio-recording uses the boundp shim for its C-; r binding, and mail-config registers C-; e directly without requiring keybindings, so it errors standalone rather than degrading. Both recorded for Phase 2.
* docs(load-graph): classify domain, integration, and optional modulesCraig Jennings2026-05-2420-33/+240
| | | | | | | | | | Eighth classification batch: 17 domain/integration/optional modules — ai-config, ai-vterm, browser-config, calendar-sync, calibredb-epub-config, chrono-tools, dirvish-config, dwim-shell-config, erc-config, eshell-config, eww-config, flyspell-and-abbrev, games-config, gloss-config, httpd-config, jumper, latex-config. I annotated each header, added a Batch 8 table to the inventory, and extended the validation allowlist. 82 of 102 modules are now classified. Almost all are eager only by init order and become command/hook/mode-loaded. calendar-sync stays eager when its .local.el is present. One new hidden dependency: calendar-sync guards its C-; g registration with a boundp shim and doesn't require keybindings, so the binding drops standalone. I deferred elfeed-config rather than annotate it. Its header edit triggers byte-compilation, and the existing tests only pass when the module loads as interpreted source — the compiled cj/elfeed-process-entries inlines an elfeed struct accessor the stubs can't intercept, and the batch test environment has no elfeed package to build real structs. It needs its tests rewritten first, recorded in the inventory and a new todo task. Also made the header allowlist scoping test durable: it used games-config (now classified) as its unclassified example; switched to a sentinel name plus a duplicate-entry guard.
* docs(load-graph): classify Org modulesCraig Jennings2026-05-2415-17/+172
| | | | | | Seventh classification batch: the thirteen Org modules — config, agenda, babel, capture, contacts, drill, export, noter, refile, reveal, roam, webclipper, hugo. I annotated each header, added a Batch 7 table to the inventory, and extended the validation allowlist. 65 of 102 modules are now classified. The daily workflows (config, agenda, capture, refile, roam) keep their eager reason per the spec's Phase 6 target. Babel and contacts move to after-load; export, reveal, drill, noter, webclipper, and hugo become command-loaded. The agenda and refile idle-timer caches are recorded as the side effects the spec already tracks for cache-lifecycle work. No new hidden dependencies.
* docs(load-graph): classify programming modulesCraig Jennings2026-05-2412-13/+137
| | | | | | Sixth classification batch: prog-general plus the language modules — prog-c, prog-go, prog-lisp, prog-python, prog-webdev, prog-json, prog-yaml, prog-shell, prog-training. I annotated each header, added a Batch 6 table to the inventory, and extended the validation allowlist. 52 of 102 modules are now classified. prog-general owns the shared defaults and tree-sitter/LSP policy and stays eager. The language modules are eager only by init order and should load by major mode, so they're tagged Phase 6 deferral candidates. prog-shell's after-save executable hook is the one side effect worth scoping. No new hidden dependencies.
* docs(load-graph): classify dev, diff, help, lint, and VC modulesCraig Jennings2026-05-2411-15/+126
| | | | | | Fifth classification batch: the development-workflow entry points and package config — coverage-core, coverage-elisp, dev-fkeys, diff-config, help-config, help-utils, flycheck-config, test-runner, vc-config. I annotated each header, added a Batch 5 table to the inventory, and extended the validation allowlist. 42 of 102 modules are now classified. Two more hidden dependencies turned up, both about cj/custom-keymap. dev-fkeys repeats the custom-buffer-file boundp shim for its C-; P binding. flycheck-config binds (:map cj/custom-keymap ...) through use-package without requiring keybindings, so it fails to load standalone. Both recorded for the Phase 2 dependency pass.
* docs(load-graph): classify UI and core-UX modulesCraig Jennings2026-05-2412-19/+133
| | | | | | Fourth classification batch: the modules that shape the first interactive frame — ui-config, ui-theme, ui-navigation, font-config, selection-framework, modeline-config, mousetrap-mode, popper-config, dashboard-config, nerd-icons-config. I annotated each header, added a Batch 4 table to the inventory, and extended the validation allowlist. 33 of 102 modules are now classified. These mostly stay eager: each has a real first-frame reason (theme, font, modeline, completion stack, landing page). No new hidden dependencies. popper-config carries the spec's open question about its enabled/disabled state, noted for the deferral phase.
* docs(load-graph): classify core libraries and command modulesCraig Jennings2026-05-249-10/+92
| | | | | | Third classification batch: the remaining core and library command modules from init.el's early block — external-open, media-utils, auth-config, keyboard-macros, system-utils, text-config, undead-buffers. I annotated each with the load-graph header contract, added a Batch 3 table to the inventory, and extended the validation allowlist. 23 of 102 modules are now classified. No new hidden dependencies in this batch. auth-config stays eager because other modules need credentials early; the command libraries (external-open, media-utils, keyboard-macros) are eager only by init order and flagged as Phase 4 deferral candidates.
* test: extend header allowlist to text/editing modulesCraig Jennings2026-05-241-2/+13
| | | | I added the nine custom-* modules to the classified allowlist so the header-validation test enforces the contract on them too. 16 of 102 modules are now covered.
* docs(load-graph): classify text/editing command modulesCraig Jennings2026-05-2410-14/+130
| | | | | | Second classification batch: the nine custom-* text/editing command helpers (case, comments, datetime, buffer-file, line-paragraph, misc, ordering, text-enclose, whitespace). I annotated each with the load-graph header contract and added a Batch 2 table to the inventory. They're all Layer 2, eager only to register a C-; submap at load, with no necessary eager reason, so all are Phase 3/4 deferral candidates. The inventory records a second hidden dependency for Phase 2: custom-buffer-file guards its C-; b registration with (when (boundp 'cj/custom-keymap) ...) and declares the keymap only via eval-when-compile, so the binding silently drops when the module loads without keybindings.
* docs(init): retire stale module comments and track follow-upsCraig Jennings2026-05-242-4/+10
| | | | Three init.el requires carried vague comments: latex-config "WIP need to fix", prog-shell "combine elsewhere", and a "Modules In Test" banner. I replaced them with descriptive comments and moved the real follow-up work into todo.org tasks: investigate the latex-config state, and decide whether prog-shell config folds into prog-general. I also marked the module-classification task DOING.
* test: enforce load-graph headers on classified modulesCraig Jennings2026-05-241-0/+111
| | | | I added test-init-module-headers.el, which checks that every classified module declares the seven required header lines and names any that are missing. The classified set is an explicit allowlist that grows one batch at a time. Parity with the init.el require set is the Phase 1 exit criterion. The cases cover the happy path, a single missing line, the eager-reason conditional, and allowlist scoping.
* docs(load-graph): seed module inventory and annotate foundation headersCraig Jennings2026-05-248-2/+258
| | | | | | I started the init.el load-graph classification with the foundation batch. I added docs/design/module-inventory.org as the living per-module inventory and annotated the seven foundation modules (system-lib, user-constants, host-environment, system-defaults, keyboard-compat, keybindings, config-utilities) with the load-graph header contract: layer, category, load shape, eager reason, top-level side effects, runtime requires, and direct-test-load safety. I changed no load order, so init.el keeps its current eager order. The inventory records one hidden dependency for Phase 2: system-defaults uses host-environment and user-constants symbols at load while declaring them eval-when-compile, so the compiled module cannot load standalone.
* test: require host-environment in system-defaults testsCraig Jennings2026-05-241-3/+7
| | | | system-defaults reads `env-bsd-p` (host-environment) and `user-home-dir` (user-constants) at load, but the module declares both only via eval-when-compile. Loading the compiled module in isolation leaves `env-bsd-p` void, so the test failed whenever it ran outside a full init. I added the host-environment require alongside the existing user-constants require so the unit loads standalone. The production fix (promoting those eval-when-compile requires to a runtime require) is Phase 2 work, recorded in the module inventory.
* docs(todo): close coverage backlog after assessing the sub-60% clusterload-graph-classify-startCraig Jennings2026-05-241-19/+13
| | | | Read each remaining sub-60% module to separate real untested logic from interactive/config glue. Filled the three genuine gaps (markdown-html, media-utils select-media-player, elfeed helpers — committed separately). The rest — flyspell, dashboard, ai-quick-ask — already have their pure logic tested with Normal/Boundary/Error; their low percentages come entirely from interactive commands. prog-general, restclient, vc-config, quick-video-capture are config/interactive-only with no pure logic to cover. Backlog cleared: every module's testable logic is covered; residual low % is code the testing rules say not to chase.
* test: cover markdown-html filter and media-player selectorCraig Jennings2026-05-242-0/+53
| | | | Two real-logic gaps from the refreshed coverage backlog. cj/markdown-html (markdown-config) is the impatient-mode filter that wraps a source buffer's text in the strapdown HTML shell — tested for normal content and an empty buffer. cj/select-media-player (media-utils) was the one untested function there — tested that choosing an available player updates cj/default-media-player and that a non-matching selection leaves it unchanged. Both mock at the boundary (completing-read, the source buffer).
* docs(todo): refresh stale coverage backlog against a clean runCraig Jennings2026-05-241-318/+18
| | | | The per-module percentages in the coverage audit were stale — this session's test-writing covered most of the listed modules (prog-python 0%->100%, hugo-config 17.7%->91.7%, undead-buffers 5.7%->85.7%, and selection-framework / keyboard-compat / system-utils / system-defaults / ui-navigation / prog-go now ~100%). Re-measured against a clean make-coverage run and replaced the ~75 stale entries with current numbers for only the modules genuinely below ~60%. Modules in the 60-80% band are adequately covered and dropped. vc-config and quick-video-capture are flagged config/interactive-only (their pure logic is tested; the uncovered lines are use-package/interactive glue), so they're recorded as no-action rather than chased.