aboutsummaryrefslogtreecommitdiff
path: root/docs/design/init-load-graph.org
diff options
context:
space:
mode:
Diffstat (limited to 'docs/design/init-load-graph.org')
-rw-r--r--docs/design/init-load-graph.org829
1 files changed, 829 insertions, 0 deletions
diff --git a/docs/design/init-load-graph.org b/docs/design/init-load-graph.org
new file mode 100644
index 00000000..d4a68f47
--- /dev/null
+++ b/docs/design/init-load-graph.org
@@ -0,0 +1,829 @@
+#+TITLE: Design: Untangle the init.el Load Graph
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-05-04
+
+* Status
+
+Draft. Specification only. No load-order implementation is part of this design
+document.
+
+* Problem
+
+=init.el= is currently both the startup script and the dependency graph. It
+eagerly requires almost every module in a fixed order, so many modules work
+because some earlier require happened to define a variable, keymap, path
+constant, hook owner, package, or helper function.
+
+That creates four practical problems:
+
+- Standalone module loading is unreliable. A module may byte-compile but fail at
+ runtime unless enough of =init.el= was loaded first.
+- Startup has unnecessary work. Optional workflows, heavy packages, timers,
+ network-facing integrations, and media tools load even when not used.
+- Side effects are hard to audit. Keybindings, timers, global hooks, server
+ setup, package configuration, and command definitions are mixed together.
+- Test boundaries are blurry. Tests often need to simulate init order instead of
+ loading the unit under test directly.
+
+The target is not "lazy load everything." The target is an explicit, testable
+load graph where eager startup is a small documented set, optional workflows
+load from commands/hooks/autoloads, and module dependencies are declared by the
+modules that use them.
+
+* Goals
+
+- Make module ownership obvious: libraries, keymap ownership, package
+ configuration, commands, and startup side effects should be distinguishable.
+- Make dependencies explicit with ordinary =require=, =autoload=, or documented
+ hook/package boundaries.
+- Reduce eager startup load without breaking existing keybindings or daily
+ workflows.
+- Keep the migration incremental and reversible. Each batch should be small
+ enough to test and inspect.
+- Preserve interactive behavior for configured workflows, including calendar
+ sync, Org capture/agenda, mail, F-keys, and media commands.
+- Improve testability: modules should either load directly or fail with a clear
+ missing external package/config message.
+
+* Non-Goals
+
+- Rewriting the whole configuration into one framework or literate init.
+- Removing =use-package=. This design assumes package config modules continue to
+ use it where appropriate.
+- Eliminating all top-level forms. Some top-level configuration is appropriate,
+ especially for foundational Emacs settings and hook registration.
+- Solving package bootstrap in =early-init.el=. That is tracked by the separate
+ "Move package bootstrap out of =early-init.el= where possible" project.
+- Rotating calendar feed URLs or designing secret storage beyond the local
+ calendar config path already introduced. Token rotation remains a separate
+ security task.
+- Consolidating all scattered utility helpers. Utility consolidation is a
+ sibling project because it changes helper ownership, tests, and call sites
+ without necessarily changing startup load order.
+
+* Principles
+
+** Eager Requires Are Allowed Only With A Reason
+
+An eager require in =init.el= should satisfy one of these conditions:
+
+- It establishes basic Emacs behavior needed for the rest of startup.
+- It defines shared constants or helpers used by many eager modules.
+- It owns the global key prefix/keymap registration system.
+- It configures core UI behavior that should be visible in the first frame.
+- It starts a user-approved startup service that cannot be triggered lazily.
+
+Everything else should be a candidate for autoload, hook-based loading,
+=with-eval-after-load=, or a command wrapper.
+
+** Modules Declare What They Use
+
+If a module calls a function or reads a variable at runtime, it should not rely
+on init order unless that dependency is an explicit startup contract.
+
+Preferred dependency forms:
+
+- Runtime dependency: =(require 'module)=.
+- Optional runtime dependency: =(require 'module nil t)= with a clear degraded
+ behavior.
+- Macro/compile-time dependency: =(eval-when-compile (require 'module))=.
+- Command-only dependency: =(autoload 'command "module" nil t)= or a lazy
+ command wrapper.
+- Package-bound dependency: =use-package :after=, =:hook=, =:commands=, or
+ =with-eval-after-load=.
+
+Avoid test-only shims in production modules such as "define this keymap if it
+does not exist." Tests should provide stubs or load the real owner.
+
+** Utility Extraction Should Stay Small And Evidence-Based
+
+Some hidden dependencies exist because generic helpers live in feature modules
+where they were first needed. Moving those helpers into =system-lib= can make
+dependencies clearer, but utility extraction should not become part of every
+load-order change by default.
+
+Extract a helper only when:
+
+- at least two callers need substantially the same behavior,
+- the helper can stay dependency-light enough for foundation startup,
+- tests can move with the helper,
+- the extraction is atomic and easy to review.
+
+Avoid building a broad utility suite speculatively. Prefer one helper, one
+tested extraction, one commit.
+
+** Keymaps Have Owners
+
+=keybindings.el= should own global prefixes, especially =cj/custom-keymap= and
+the =C-;= prefix. Feature modules may define local maps or command maps, but
+registration into global prefixes should go through a small convention/helper so
+load order is not a hidden dependency.
+
+** Side Effects Are Named And Isolated
+
+Side effects include:
+
+- starting timers,
+- starting processes,
+- calling network-facing sync/fetch commands,
+- setting global keybindings,
+- mutating global hooks,
+- opening files/buffers,
+- enabling global modes,
+- loading large packages solely for optional commands.
+
+Each side effect should have one of:
+
+- a documented eager reason,
+- an interactive command,
+- a hook/package boundary,
+- a noninteractive/batch guard,
+- a test that proves the side effect does not happen in the wrong context.
+
+* Target Architecture
+
+** Layer 0: Early Startup
+
+Owned by =early-init.el=. Should remain limited to startup mechanics that must
+happen before package/UI initialization.
+
+Examples:
+
+- package archive/bootstrap policy,
+- native-comp/cache startup knobs that must be early,
+- disabling expensive default UI before first frame.
+
+This design does not refactor =early-init.el= except to avoid adding new load
+graph responsibilities to it.
+
+** Layer 1: Foundation
+
+Small eager set required before most other modules can safely load.
+
+Expected contents:
+
+- =system-lib=
+- =user-constants=
+- =host-environment=
+- =system-defaults=
+- =keyboard-compat=
+- =keybindings=
+- maybe =config-utilities=, if debug helpers are intentionally eager
+
+Foundation modules should be able to load in batch mode without package,
+network, timer, or UI-package side effects.
+
+Adding a new Layer 1 module requires a coordinated update to the
+=system-lib.el= dependency budget in [[file:utility-consolidation.org][utility-consolidation.org]].
+
+Topic libraries introduced by the utility project join Layer 1 only when their
+first consumer is foundation-eager. Otherwise they are Layer 2 and loaded by an
+explicit =require= from their eager consumers. Add each new topic library to the
+module category table before migrating its first consumer.
+
+** Layer 2: Core UX
+
+Eager or near-eager modules that shape the first interactive session.
+
+Expected contents:
+
+- basic text/editing defaults,
+- core UI frame/theme/font/modeline behavior,
+- selection/completion framework,
+- F-key development entry points,
+- VC/test/coverage command entry points.
+
+Core UX modules may configure packages, but heavy features should still use
+=:commands=, =:hook=, or =:defer= where practical.
+
+** Layer 3: Domain Workflows
+
+Org, programming, mail, browser, media, AI, and integration modules. These
+should generally load through hooks, commands, package =:after= clauses, or
+workflow-specific entry commands.
+
+Examples:
+
+- Org capture/agenda can remain eager if the user's daily workflow needs it,
+ but exporters and optional extensions can be deferred.
+- Language modules should load from mode hooks or file associations, not because
+ every startup might edit every language.
+- Mail/media/AI/rest tools should register commands eagerly if needed, then load
+ heavy packages only on use.
+
+** Layer 4: Optional And Experimental
+
+Entertainment, modules in test, diagnostics, and rarely used tools. These should
+not be required by default unless the user explicitly chooses that behavior.
+
+Examples:
+
+- =games-config=
+- =music-config=
+- =lorem-optimum=
+- =gloss-config=
+- optional IRC/Slack/feed/media modules when not in active use
+
+* Module Categories
+
+This is a first-pass classification to guide implementation. It is not an
+architectural truth table; each module should be confirmed while refactoring.
+
+Category key:
+
+- =F= foundation or shared library/config.
+- =C= core eager UX.
+- =P= package configuration that should usually be hook/command/package loaded.
+- =D= domain workflow that may have a user-visible eager reason.
+- =S= startup side-effect or timer/process owner.
+- =O= optional, entertainment, experimental, or rarely used.
+- =L= pure-ish library/command helpers that should be easy to load directly.
+
+| Module | Category | Expected final load shape | Notes |
+|--------+----------+---------------------------+-------|
+| =early-init= | F | early | Layer 0; see Non-Goals. |
+| =system-lib= | F/L | eager | Low-level helpers. Keep side-effect free. |
+| =cj-process= | F/L | TBD per first consumer | Topic library placeholder; see utility-consolidation Group 3. |
+| =cj-org-text= | F/L | TBD per first consumer | Topic library placeholder; see utility-consolidation Group 6. |
+| =cj-cache= | F/L | TBD per first consumer | Topic library placeholder; see utility-consolidation Group 7. |
+| =user-constants= | F | eager, then split | Split pure path constants from directory creation/failure behavior. |
+| =host-environment= | F/L | eager | Predicate helpers. |
+| =system-defaults= | F/S | eager | Owns global Emacs defaults, server/recentf/minibuffer hooks. |
+| =keyboard-compat= | F/S | eager | Terminal/GUI keyboard setup hooks. |
+| =keybindings= | F/C | eager | Owner of =cj/custom-keymap= and global prefixes. |
+| =config-utilities= | C/O | eager or command-loaded | Debug keymap may be eager; heavy org parsing commands can lazy require. |
+| =custom-case= | L/C | autoload commands + key registration | Text command helper. |
+| =custom-comments= | L/C | autoload commands + key registration | Text command helper. |
+| =custom-datetime= | L/C | autoload commands + key registration | Text command helper. |
+| =custom-buffer-file= | L/C | eager only if remaps required | Has file/process helpers and keymap registration. |
+| =custom-line-paragraph= | L/C | autoload commands + key registration | Requires =expand-region= at command boundary if possible. |
+| =custom-misc= | L/C | autoload commands + key registration | Misc commands. |
+| =custom-ordering= | L/C | autoload commands + key registration | Text command helper. |
+| =custom-text-enclose= | L/C | autoload commands + key registration | Text command helper. |
+| =custom-whitespace= | L/C | autoload commands + key registration | Text command helper. |
+| =external-open= | L/D | autoload commands | Runtime requires environment/process helpers explicitly. |
+| =media-utils= | D | command-loaded | Downloads/players should run only by command. |
+| =auth-config= | F/D | eager or package-after | Auth setup may be core; GPG commands should remain commands. |
+| =keyboard-macros= | C | eager or keymap-only | Lightweight command/key owner. |
+| =system-utils= | L/C | eager or command-loaded | Timers/process monitor utilities. |
+| =text-config= | C/P | eager hooks | General text defaults and package config. |
+| =undead-buffers= | C | eager if remaps desired | Global kill-buffer remaps. |
+| =browser-config= | D/P | command/package-loaded | Browser workflow. |
+| =coverage-core= | C/L | eager command entry | F7 entry point and backend registry. |
+| =coverage-elisp= | C/P | eager after core | Backend registration; keep cheap. |
+| =dev-fkeys= | C | eager | F4/F6 command entry points. |
+| =ui-config= | C/S | eager | Cursor/UI defaults; post-command hook should be documented. |
+| =ui-theme= | C | eager + explicit startup call | Theme load stays explicit in init. |
+| =ui-navigation= | C/P | eager | Window keybindings and winner/buffer-move config. |
+| =font-config= | C/P/S | eager or first-frame | Font hooks/font installation checks need guards. |
+| =selection-framework= | C/P | eager | Completion stack; likely core UX. |
+| =modeline-config= | C/S | eager | Mode line and VC cache hooks. |
+| =mousetrap-mode= | C | eager if global behavior desired | Prevents accidental mouse edits. |
+| =popper-config= | C/P | eager if enabled, else remove/defer | Existing disabled-state question remains. |
+| =chrono-tools= | D/P | command-loaded | Calendar/timer commands; sound path dependency explicit. |
+| =diff-config= | C/P | eager or package-loaded | Diff/merge UX. |
+| =erc-config= | O/D/P | command-loaded | IRC should not be startup load by default. |
+| =slack-config= | O/D/P | command-loaded | Slack package/auth and which-key registration should be after-load. |
+| =eshell-vterm-config= | D/P | command/hook-loaded | Shell/terminal packages. |
+| =help-utils= | L/D | autoload commands | Search/help commands. |
+| =help-config= | C/P | eager or after help | Info/man/help config. |
+| =tramp-config= | D/P | package-loaded | Remote shell configuration. |
+| =calibredb-epub-config= | O/D/P | command-loaded | Ebook workflow. |
+| =dashboard-config= | C/S | eager only if startup dashboard desired | Opens/initializes landing page behavior. |
+| =dirvish-config= | D/P | command/hook-loaded | File manager; runtime constants explicit. |
+| =dwim-shell-config= | D/P | command-loaded | Shell commands from Dired/Dirvish. |
+| =elfeed-config= | O/D/P | command-loaded | Feed reader/podcast workflow. |
+| =eww-config= | D/P | command-loaded | Web browsing helpers. |
+| =flyspell-and-abbrev= | C/P | hooks | Text-mode spelling/abbrev. |
+| =httpd-config= | O/D/P | command-loaded | Local web server. |
+| =latex-config= | D/P | hook-loaded | Existing WIP comment should become tasks or be removed. |
+| =mail-config= | D/P | command-loaded or eager by choice | Heavy mu4e/org-msg; daily workflow may justify eager command registration. |
+| =markdown-config= | D/P | mode-loaded | Markdown package config. |
+| =pdf-config= | D/P | file/mode-loaded | Heavy PDF packages should load on PDF open. |
+| =quick-video-capture= | O/D/S | command/protocol-loaded | Top-level timers should be removed or guarded. |
+| =video-audio-recording= | O/D/S | command-loaded | External process/device probing only on command. |
+| =transcription-config= | O/D/P | command-loaded | Auth/process workflow. |
+| =weather-config= | O/D/P | command-loaded | Optional command. |
+| =prog-general= | C/P/S | eager or hooks | Projectile, treesit policy, LSP ownership concerns. |
+| =test-runner= | C/L | eager command entry | Test keymap and project-scoped state. |
+| =vc-config= | C/P | eager command entry | Magit/git keymap; clone command hardening separate. |
+| =flycheck-config= | C/P | hooks | General linting. |
+| =prog-training= | O/D/P | command-loaded | Exercism/Leetcode optional. |
+| =prog-c= | D/P | mode-loaded | C hooks and compile command. |
+| =prog-go= | D/P | mode-loaded | Go hooks/LSP. |
+| =prog-lisp= | D/P | mode-loaded | Lisp package config. |
+| =prog-lsp= | C/P | package policy owner | Should consolidate generic LSP policy. |
+| =prog-shell= | D/P/S | mode-loaded | after-save executable hook should be opt-in or scoped. |
+| =prog-python= | D/P | mode-loaded | Python hooks/LSP. |
+| =prog-webdev= | D/P | mode-loaded | Webdev modes/LSP. |
+| =prog-json= | D/P | mode-loaded | JSON formatting/mode config. |
+| =prog-yaml= | D/P | mode-loaded | YAML formatting/mode config. |
+| =org-config= | C/D/P | eager | Core Org behavior likely eager. |
+| =org-agenda-config= | D/S | eager by workflow, timers guarded | Agenda cache lifecycle project tracks cleanup. |
+| =org-babel-config= | D/P | after Org | Babel languages package config. |
+| =org-capture-config= | D/P | eager if capture hot path | Protocol/capture templates. |
+| =org-contacts-config= | D/P | after Org/mail | Contacts workflow. |
+| =org-drill-config= | O/D/P | command-loaded | Optional drill workflow. |
+| =org-export-config= | D/P | command-loaded | Export packages/processes. |
+| =hugo-config= | D/P | command-loaded | Blog workflow. |
+| =org-reveal-config= | O/D/P | command-loaded | Presentation workflow. |
+| =org-refile-config= | D/S | eager by workflow, timers guarded | Refile cache lifecycle project tracks cleanup. |
+| =org-roam-config= | D/P/S | eager by workflow | Capture/finalize hooks, db. |
+| =org-webclipper= | O/D/P | protocol/command-loaded | Global temp state cleanup tracked separately. |
+| =org-noter-config= | O/D/P | command-loaded | PDF notes workflow. |
+| =ai-config= | D/P | command-loaded | GPTel commands; avoid loading all AI tooling at startup. |
+| =ai-conversations= | D/L/S | after gptel | Autosave hook and persistence path need coverage. |
+| =restclient-config= | D/P | command-loaded | API exploration. |
+| =calendar-sync= | D/S | eager only if configured, batch safe | Private config path and noninteractive guard exist. |
+| =reconcile-open-repos= | D/S | command-loaded | Repo scanning/reconciliation should not run at startup. |
+| =local-repository= | O/D/P | command-loaded | Local package mirror workflow. |
+| =music-config= | O/D/P/S | command-loaded | EMMS/keymap optional, hooks only after EMMS. |
+| =games-config= | O | command-loaded | Optional. |
+| =lorem-optimum= | O/L | command-loaded | Module in test. |
+| =jumper= | O/L | command-loaded | Navigation helper. |
+| =system-commands= | D/S | command-loaded | High-impact commands; defensive work tracked separately. |
+| =gloss-config= | O/D/P | command-loaded | Glossary workflow. |
+| =wrap-up= | S | eager if desired | End-of-startup buffer bury timer. |
+| =ledger-config= | O/D/P | mode-loaded | Not currently required by init. |
+| =mu4e-org-contacts-integration= | D/L | after mu4e/org-contacts | Loaded by mail workflow. |
+| =mu4e-org-contacts-setup= | D/L | after mu4e/org-contacts | Setup helper. |
+| =org-agenda-config-debug= | O/L | command/debug-loaded | Debug helper. |
+| =show-kill-ring= | O/L | command-loaded | Not currently required by init. |
+
+* Module File Header Standard
+
+Each module should eventually declare its load-graph contract in its own
+commentary header. The category table above is the seed view; module headers
+are the contributor-facing contract that travels with the code.
+
+Required header lines, after =;;; Commentary:=:
+
+1. =;; Layer: <0|1|2|3|4> (<layer name>).=
+2. =;; Category: <F|C|P|D|S|O|L>=.
+3. =;; Load shape: <eager|hook|mode|command|after-load>=.
+4. =;; Eager reason:= one-line justification when load shape is =eager=,
+ omitted otherwise.
+5. =;; Top-level side effects:= timer, process, hook, package, network,
+ buffer mutation, file write, or =none=.
+6. =;; Runtime requires:= explicit runtime module/package list.
+7. =;; Direct test load: <yes|conditional|no>=, with a brief reason when not
+ =yes=.
+
+Optional:
+
+- =;; See also:= references to tests and design docs.
+
+Worked example:
+
+#+begin_src emacs-lisp
+;;; calendar-sync.el --- One-way calendar synchronization to Org -*- lexical-binding: t; -*-
+;;
+;;; Commentary:
+;;
+;; Layer: 3 (Domain Workflow).
+;; Category: D/S.
+;; Load shape: eager only when calendar-sync.local.el configures calendars.
+;; Eager reason: daily-driver workflow; user expects calendars synced at first
+;; session. Top-level startup is guarded so batch/test loads do not start
+;; timers or network fetches.
+;; Top-level side effects: timer, network fetch, file writes to calendar Org
+;; files. Guarded by noninteractive/config checks.
+;; Runtime requires: user-constants, seq, subr-x.
+;; Direct test load: yes (batch-safe; private config is optional).
+;;
+;; See also: docs/design/init-load-graph.org, tests/test-calendar-sync.el.
+;;
+;;; Code:
+#+end_src
+
+Phase 1 should annotate every module required by =init.el= with this header.
+Later validation can assert that every required module declares the seven
+required lines.
+
+* Proposed Load Shape
+
+Migration commits should use conventional commit prefixes consistently:
+
+- =refactor:= for behavior-preserving load-order, dependency, keymap, and lazy
+ loading migrations.
+- =feat:= only when adding a new user-visible capability.
+- =test:= for test-only follow-up work.
+- =docs:= for spec, inventory, design updates, and module-header annotations,
+ even when those annotations touch =modules/*.el= files.
+
+Default deferral mechanism:
+
+- Prefer =use-package :commands= for command-driven deferrals.
+- Prefer =use-package :mode= when loading is file-extension or major-mode
+ driven.
+- Prefer =use-package :hook= when the consumer is a mode-hook function.
+- Use explicit =(autoload 'command "module" nil t)= only when the command is
+ not naturally owned by a =use-package= form.
+
+** Phase 1: Inventory And Contracts
+
+Do not change load order yet.
+
+1. Keep the current eager =init.el= order.
+2. Create/maintain =docs/design/module-inventory.org= as a living inventory
+ with:
+ - module name,
+ - category,
+ - eager/deferred target,
+ - known runtime dependencies,
+ - top-level side effects,
+ - tests that cover standalone load or command behavior.
+3. Annotate every module required by =init.el= with the module header standard.
+4. Convert vague comments in =init.el= into tasks or remove them:
+ - =latex-config= "WIP need to fix",
+ - =prog-shell= "combine elsewhere",
+ - "Modules In Test" section.
+5. Add lightweight standalone-load smoke tests for the lowest-level modules.
+
+Inventory rules:
+
+- The module table in this spec seeds the inventory.
+- =docs/design/module-inventory.org= is the living per-module truth after Phase
+ 1 starts.
+- Every module required by =init.el= must be represented before Phase 2 starts.
+- Discoveries during later phases update the inventory.
+- This inventory is independent from the helper inventory owned by
+ [[file:utility-consolidation.org][utility-consolidation.org]].
+
+Exit criteria:
+
+- Every module required by =init.el= has a category and target load shape.
+- Every eager survivor has a documented reason.
+- The inventory identifies top-level timers/process/network-ish side effects.
+- Every module required by =init.el= has the required load-graph header lines.
+
+** Phase 2: Explicit Dependencies
+
+Still do not significantly change startup behavior.
+
+1. For each module batch, load it directly in batch mode.
+2. Fix hidden dependencies by adding real =require=, =autoload=, or package
+ boundaries.
+3. Remove production shims that only exist because tests load modules in an
+ incomplete environment.
+4. If a keymap dependency is hidden, document it and make the dependency
+ explicit with =require= or =autoload=. Do not refactor into the registration
+ convention until Phase 3. When the hidden dependency is on
+ =cj/custom-keymap= itself, add =(require 'keybindings)= to the consuming
+ module; Phase 3 replaces these direct dependencies with the registration
+ API.
+5. When a hidden dependency is really a duplicated generic helper, either:
+ - hand the extraction to the utility-consolidation sibling project when it
+ is in scope there, or
+ - leave it in place and record it under that project.
+
+Suggested order:
+
+- Foundation and libraries.
+- Text/editing command modules.
+- UI modules.
+- Programming modules.
+- Org modules.
+- Optional integrations.
+
+Exit criteria:
+
+- Direct module load either succeeds or fails with a clear missing external
+ package/config message.
+- =make test-file FILE=test-all-comp-errors.el= passes.
+- New tests cover any helper extracted while fixing dependencies.
+- Helper extraction remains dependency-light and does not pull heavy packages
+ into foundation startup.
+
+** Phase 3: Keymap Registration Boundary
+
+Introduce a small keymap registration API before deferring many feature modules.
+
+Possible API:
+
+#+begin_src emacs-lisp
+(defun cj/register-prefix-map (key map label)
+ "Register MAP under KEY in `cj/custom-keymap' with LABEL for which-key."
+ ...)
+
+(defun cj/register-command (key command label)
+ "Register COMMAND under KEY in `cj/custom-keymap' with LABEL for which-key."
+ ...)
+#+end_src
+
+Design rules:
+
+- =keybindings.el= owns =cj/custom-keymap= and the global =C-;= binding.
+- Feature modules may define maps and commands without mutating global keys
+ directly.
+- Which-key labels must be registered after which-key loads.
+- Tests can assert key resolution without loading every feature package.
+
+Exit criteria:
+
+- Modules no longer need to assume =cj/custom-keymap= exists at top level
+ except through the registration API.
+- Existing =C-;= bindings continue to resolve.
+- Which-key labels for documented prefixes remain available.
+
+** Phase 4: Defer Low-Risk Optional Modules
+
+Start with modules that are unlikely to affect first-frame startup.
+
+Candidate batch:
+
+- =games-config=
+- =music-config=
+- =weather-config=
+- =gloss-config=
+- =lorem-optimum=
+- =jumper=
+- =httpd-config=
+- =prog-training=
+
+For each module:
+
+1. Keep its user-facing command/key available via the default deferral mechanism
+ above.
+2. Move package loading into =use-package :commands=, =:hook=, =:mode=, or an
+ explicit autoload/wrapper only when the default does not fit.
+3. Run targeted tests and an interactive smoke check.
+
+Exit criteria:
+
+- Startup no longer requires the module eagerly.
+- User command still works from a fresh Emacs session.
+- Module-specific tests pass.
+
+** Phase 5: Defer Heavy Domain Modules
+
+Candidate batch:
+
+- =pdf-config=
+- =calibredb-epub-config=
+- =video-audio-recording=
+- =transcription-config=
+- =mail-config=
+- =ai-config=
+- =restclient-config=
+- =elfeed-config=
+- =erc-config=
+- =slack-config=
+
+These need more care because they often combine package setup, auth, keymaps,
+processes, hooks, and user workflows.
+
+Exit criteria for each:
+
+- Commands are discoverable before package load.
+- Package load happens through the default deferral mechanism: command, hook,
+ mode, or explicit startup opt-in.
+- Auth and private config are not read until necessary unless the user opts in.
+- Batch/test startup does not start network/process work.
+
+Private config opt-in follows the =calendar-sync.local.el= precedent: a module
+reads =<module-name>.local.el= when readable, the file is gitignored, and the
+module degrades cleanly when the file is missing. Token rotation is a separate
+security task; this convention is about config presence, not secret protection.
+
+** Phase 6: Revisit Org And Programming Eagerness
+
+Org and programming modules are daily-use, so the goal is not blindly deferring
+everything.
+
+Programming target:
+
+- Keep generic programming defaults and F-key command entry points available.
+- Load language-specific modules by major mode.
+- Consolidate generic LSP policy under =prog-lsp=.
+ - Move to =prog-lsp=: global LSP toggles such as =lsp-idle-delay=,
+ =lsp-log-io=, =lsp-enable-folding=, =lsp-enable-snippet=,
+ =lsp-headerline-breadcrumb-enable=, and file-watch ignore lists.
+ - Keep per-language: server client settings such as
+ =lsp-clients-clangd-args= and =lsp-pyright-*=, plus language-mode hook
+ wiring.
+- Tree-sitter grammar auto-install is always on; the project policy is global
+ allow. =treesit-auto-install= is =t= without per-language conditionals.
+
+Org target:
+
+- Keep these daily first-session workflows eager: =org-config=,
+ =org-agenda-config=, =org-capture-config=, =org-refile-config=,
+ =calendar-sync= when local config is present, and =org-roam-config=.
+- Defer exporters, reveal, drill, noter, webclipper, and optional publishing
+ pieces behind commands/hooks.
+- Normalize agenda/refile cache lifecycle before changing timer behavior. This
+ is behavioral normalization within the load-graph project; the shared
+ =cj-cache.el= extraction is owned by utility-consolidation Phase 5 and may
+ follow.
+
+The =prog-lsp= consolidation and tree-sitter policy decisions are owned by this
+load-graph project. Utility consolidation owns reusable helper extraction, not
+programming policy.
+
+Exit criteria:
+
+- Common daily Org/programming workflows work from a fresh session.
+- Optional exporters/languages load when used.
+- Timers are guarded in batch/test contexts.
+
+* Adjacent Project: Utility Consolidation
+
+The review of this spec identified a related but distinct architectural
+problem: helper functions are scattered across feature modules, sometimes with
+duplicated behavior. This matters to the load graph because modules can become
+coupled to whichever feature file happened to define a useful helper first.
+
+This should be tracked as a sibling project, not folded into the load-graph
+project. The load-graph project asks "when and why does this module load?" The
+utility consolidation project asks "which module should own this reusable
+behavior?" Those questions overlap, but their changes have different risk and
+rollback shapes.
+
+This sibling project can run beside Phase 2. When explicit-dependency work finds
+a generic duplicated helper, the sibling project owns the extraction commit when
+the helper is in scope for that project. See
+[[file:utility-consolidation.org][utility-consolidation.org]] for candidate
+helpers, naming rules, dependency budgets, migration phases, and test policy.
+
+* Testing Strategy
+
+** Static/Batch Tests
+
+Add or extend tests for:
+
+- Direct module load smoke tests for modules in each batch.
+- Header validation: every module required by =init.el= declares the seven
+ required load-graph header lines.
+ - Test file: =tests/test-init-module-headers.el=.
+ - Assertion shape: inspect every module required by =init.el=, read its
+ commentary header, and fail with the missing line names for any absent
+ required header line.
+- Keymap registration: prefix maps and commands resolve without requiring the
+ feature implementation package.
+- No startup timers/processes in batch for side-effect modules.
+- =init.el= startup smoke in batch, where possible.
+- Byte/native compile smoke via existing =test-all-comp-errors.el=.
+
+Test files for this project use =test-init-<feature>.el=, for example
+=test-init-module-headers.el= and =test-init-keymap-registration.el=. This keeps
+load-graph validation tests distinct from per-module unit tests.
+
+Header validation runs directly against module files. It does not depend on the
+final =docs/design/module-inventory.org= format, which remains a Phase 1
+authoring decision.
+
+** Automated Smoke Checks
+
+Automate every smoke item that can run in batch:
+
+- Important keybindings resolve to the intended command symbols, including
+ =C-;= prefixes and F4/F6/F7 entry points.
+- Org capture and agenda command entry points load or produce expected
+ batch-safe guidance.
+- Calendar sync status reports configured/no-config state without starting
+ timers or network fetches in batch.
+- Optional commands touched in the batch autoload and resolve.
+- Non-graphical interactive flows use =execute-kbd-macro= or
+ =with-simulated-input= where practical.
+
+These checks should run under =make test= for every migration commit.
+
+** Manual Smoke Checks
+
+Each migration batch should be followed by an interactive restart and checklist:
+
+- First frame appears with expected theme/font/modeline.
+- =C-;= prefix appears and key descriptions are present.
+- Magit opens.
+- Mail command opens or gives expected package/config guidance.
+- Refile target lookup works in an interactive session.
+- Any optional command changed in the batch runs end to end.
+- If daemon mode is part of normal use, run the visual checklist once via
+ regular =emacs= and once via =emacsclient= against a running daemon.
+
+** Performance Checks
+
+Before and after major batches:
+
+- Record =emacs-init-time=.
+- Record a startup profile baseline and diff, preferably with =benchmark-init=
+ if enabled for the phase.
+- =benchmark-init= is installed via package.el. The activation block in
+ =early-init.el= is commented; uncomment it locally during phases that need
+ profiling and do not commit the activation. Profile output goes to
+ =.profile/=, which should stay gitignored.
+- Suggested workflow:
+ - =make profile-baseline= records =emacs-init-time= and a startup profile to
+ =.profile/baseline.txt=.
+ - =make profile-diff= records the current run and compares it to the phase
+ baseline.
+- Keep a simple note of eagerly loaded feature count from
+ =cj/info-loaded-features= or equivalent.
+
+Performance is a supporting signal. Correctness and explicit dependencies are
+the primary acceptance criteria. Startup regressions larger than roughly 50 ms
+against the phase baseline should be investigated and explained; after several
+stable baseline runs, this can become a stricter gate.
+
+* Acceptance Criteria
+
+The project is complete when:
+
+- =init.el= contains only documented eager requires and explicit startup calls.
+- Optional modules no longer load merely because Emacs started.
+- Each module required by =init.el= has a category and eager/deferred rationale.
+- Modules that remain eager have no hidden dependencies on arbitrary earlier
+ init order.
+- Global key registration has a central owner/convention.
+- Top-level timers/process/network work is either removed, guarded, or
+ documented as intentional.
+- Full =make test= passes.
+- Byte/native compile smoke passes.
+- Interactive startup checklist passes.
+
+* Risks And Mitigations
+
+** Risk: Breaking muscle-memory keybindings
+
+Mitigation:
+
+- Change key registration mechanics before changing bindings.
+- Add keymap resolution tests for important prefixes.
+- Keep a per-batch manual keybinding checklist.
+
+** Risk: Lazy-loaded packages miss early hook setup
+
+Mitigation:
+
+- Prefer =use-package :hook= and =:mode= over ad hoc lazy command bodies for mode
+ packages.
+- Add tests that inspect hook contents where possible.
+- Smoke-test opening representative files.
+
+** Risk: Daily workflows silently stop starting
+
+Mitigation:
+
+- Distinguish "safe default" from "local opt-in" for workflows like calendar
+ sync.
+- Use ignored/local config files for private eager opt-ins.
+- Report missing config clearly.
+
+** Risk: Batch tests differ from interactive startup
+
+Mitigation:
+
+- Guard timers/process/network work with =noninteractive= only when that is the
+ intended distinction.
+- Add at least one interactive checklist per migration batch.
+
+** Risk: Refactor becomes too broad
+
+Mitigation:
+
+- One batch, one module family.
+- Do not mix dependency fixes, keybinding redesign, and package lazy-loading in
+ the same commit unless tightly coupled.
+- Keep rollback easy by preserving user-facing commands and using wrappers.
+
+* Implementation Backlog
+
+The project in =todo.org= should remain the source of task state. This design
+supports these implementation tickets:
+
+1. Classify modules by role and startup requirement.
+2. Add explicit module dependencies before changing load order.
+3. Centralize custom keymap registration.
+4. Defer low-risk optional modules.
+5. Defer heavy document/media/integration modules.
+6. Revisit programming module LSP/tree-sitter ownership.
+7. Revisit Org module cache/timer and optional extension loading.
+8. Retire or rewrite stale =init.el= comments.
+9. Create a sibling utility consolidation project with an inventory pass and
+ first helper extractions.
+
+* Open Questions
+
+- Should =config-utilities= remain eager because debug commands are useful
+ during startup work, or should it become command-loaded after this project?
+- Should local/private opt-ins share one file, or should modules keep
+ workflow-specific local files such as =calendar-sync.local.el=?
+- Should the module inventory become machine-readable for validation, or is an
+ org table enough? Decide during Phase 1 based on inventory authoring
+ experience.
+- Should =init.el= ultimately become declarative sections plus an explicit
+ startup contract list?
+
+* Next Steps
+
+1. Use this document as the reference for the =Classify modules by role and
+ startup requirement= task.
+2. Build the first inventory directly from the module table above, correcting
+ category guesses while inspecting each file.
+3. Do not defer a module until its direct runtime dependencies are explicit.
+4. Implement keymap registration before deferring feature modules that currently
+ mutate =cj/custom-keymap= at top level.
+5. Create the sibling utility consolidation project before Phase 2 work begins,
+ so duplicated helpers found during dependency cleanup have a clear place to
+ land.