aboutsummaryrefslogtreecommitdiff
path: root/docs/design/init-load-graph.org
blob: 3db2fe8547d6f36f1901a402a8b59cdba37c6d2a (plain)
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
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 + term-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.