# Main Process Architecture (`src/main`) This is the canonical reference for how `src/main/` is organized: what each top-level directory is for, the rules that keep them from sprawling, and how they depend on each other. It is the main-process peer of [Renderer Architecture](./renderer-architecture.md) and [Shared Layer Architecture](./shared-layer-architecture.md); the cross-process picture (process model, monorepo tree) lives in [Architecture Overview](./architecture-overview.md). The top level is a **closed, locked set of principled categories**, not an open list of modules. Each top-level directory holds **one kind of thing** and earns its place for a distinct reason. **The set is locked** — a new capability is always routed into an existing category by its nature, never given a new top-level directory (§4). ## 1. The Closed Top-Level Set Exactly these, each with a single charter: | Dir | Category | Why it earns a top-level home | |---|---|---| | `core` | **App runtime** | Business-agnostic infrastructure concerned only with running the app. The test: lift `core/` onto a different Electron app, add other business code, and you have a different application. One kind of thing — the app substrate: lifecycle / DI container, path registry, logger, window manager, scheduler & jobs, preboot, diagnostics. | | `ipc` | **Cross-process boundary** | Electron's defining inter-process mechanism — special and important enough to stand alone. Unified as **IpcApi** (schema + router + handler): the single typed boundary between main and renderer. | | `data` | **Data layer** | The general business-data store — a first-class data layer, hence independent. Holds DbService / CacheService / PreferenceService / DataApiService / BootConfig, DB schemas, and the v1→v2 migrators (which by design read domain data — throwaway migration code). Detailed in [Data System Reference](./data/README.md). | | `ai` | **Core domain** | Cherry Studio *is* an AI client, so AI earns its own top-level home: everything tied to the AI essence lives here (providers, middleware, MCP, agents, stream manager). Mirrors `@shared/ai`. | | `features` | **Domain modules** | Business domains, one directory each. A complex domain bundles its own related services / utils / etc. under `features//`. | | `services` | **Business services** | Business feature services. A simple service is a single file; a larger one is organized into its own subdirectory. | | `utils` | **Stateless helpers** | Cross-domain stateless, domain-agnostic functions with no single owner. "Stateless" is the bar, not "pure": a helper may reach infra through the ambient `@application` / `@logger` (§3); it just owns no state and performs no outward side effects (§2). | | `i18n` | **Main-process localization** | Main's own locale catalog (`locales/` human + `translate/` machine) and its `t()` / `getI18n()` resolver. A deliberate, governed expansion of the closed set (§4), mirroring `src/renderer/i18n/` so each process owns an independent catalog; the `utils/i18n/` alternative was rejected for that cross-process symmetry. | Entry files: `main.ts` (process entry — runs preboot, then `application.bootstrap()`; a named file, since `index` is reserved for barrels per [Naming §6.4](./naming-conventions.md)) and `ipc.ts` (legacy IPC registration, being retired into `ipc/`). Naming follows [Naming Conventions §4.9](./naming-conventions.md): `core` / `data` / `ai` / `ipc` / `i18n` are singular namespaces; `features` / `services` / `utils` are plural buckets. ```text src/main/ ├── main.ts # process entry: preboot → application.bootstrap() ├── ipc.ts # legacy IPC registration (being retired into ipc/) ├── core/ # business-agnostic app runtime (lifecycle/DI, paths, logger, window, scheduler/job, preboot) ├── ipc/ # IpcApi — the typed main↔renderer boundary ├── data/ # the data layer (DB/Cache/Preference/DataApi/BootConfig, schemas, migration) ├── ai/ # the AI subsystem — the product's core domain ├── features/ # business domains, one dir each (each bundles its own services/utils) ├── services/ # business feature services (single file, or a subdirectory) └── utils/ # cross-domain stateless helpers ``` ## 2. `features` vs `services` (Placement) `services/` and `features/` are the same kind of thing — business logic — at two sizes. The split follows the cross-process rule in [Naming Conventions §4.10](./naming-conventions.md): - **Promotion, not default — and in steps.** A small, self-contained service starts as a single file at the bucket root — `services/Service.ts` (a stateful `Service`/`Manager` class is `PascalCase` matching its class name, [Naming §5.2](./naming-conventions.md); a **generic** helper is `utils/.ts` — a **topic-specific** one stays inline until the subdirectory step below). When one file can no longer hold it, grow it **in place** into a `camelCase` topic subdirectory first — `services//` holding `Service.ts` plus its helpers — **not** straight into a feature. Mind the shape: the **directory is the topic name and carries no `Service` suffix** ([Naming §4.5](./naming-conventions.md)); only the class file keeps the suffix (e.g. `services/webSearch/WebSearchService.ts`). It earns a `features//` home only once it grows into a **large, multi-file domain** bundling its own services, utils, and helpers (knowledge, apiGateway, fileProcessing). Do not pre-create a subdirectory or a feature for an anticipated module. - **`ai/` is not an ordinary feature.** It is the product's core domain and has its own top-level home (§1); it is foundational, not one domain among many. - **Route by role** ([Naming Conventions §5.2](./naming-conventions.md)): a stateful class owning long-lived resources or persistent side effects → a lifecycle `Service` ([Lifecycle Reference](./lifecycle/README.md)); a stateless module → `utils/` by default, promoted to `services/` only by **outward side effects** or a forced upward dependency (§5.2 routing table); a large domain → `features//`. **Reads never promote** — a helper touching infra for queries is still a helper. ### 2.1 Subdirectories and Barrels A single `.ts` file is the default; promote a topic to a subdirectory only when it actually owns multiple files. Barrels follow [Naming §6.4](./naming-conventions.md) (the cross-process authority), applied here to `services/` and `utils/`: - **The bucket roots `services/` and `utils/` have no `index.ts`.** A bucket is a category, not a module — import the specific file or topic, never the whole bucket. - **A `services//` subdirectory has exactly one `index.ts`** as its public API (explicit named exports, no `export *`); its other files stay private behind it. - **A complex `utils//` subdirectory likewise has one `index.ts`.** - **Why:** each topic is then imported through a single public entry — exactly like a one-file module — so its internal files stay private and consumers never deep-import. For `utils/`, where file and directory share the topic name, the specifier (`@main/utils/`) is even unchanged when a file grows into a folder. (A `features//` is the same single-entry idea one tier up: consumers import the domain through its one public entry, not its internal files.) ## 3. Dependency Direction The charters imply a direction; dependencies flow toward the business-agnostic foundation: - **Foundation** — `core/` and `utils/` carry no business knowledge; nothing business sits below them. - **Data layer** — `data/` is the storage layer above the foundation. - **Business** — `ai/`, `features/`, and `services/` are the business tier; they depend **down** on `data/`, `core/`, and `utils/`. - **`ai/` is foundational within the business tier**: `features/` and `services/` may depend on it; it must not import a feature. - **Feature domains are mutually isolated**: a `features//` must not import a **sibling** feature — share through `services/`, `ai/`, `data/`, or `@shared`. - **`ipc/` is the boundary adapter**: it resolves services through the DI container (`application.get`) rather than importing domain modules directly. - **No renderer imports**: `src/main` and `src/preload` must not import renderer code. Cross-process types live in `@shared`, main-only types stay in `src/main` — see [Shared Layer Architecture](./shared-layer-architecture.md) for placement. Enforced by an ESLint `no-restricted-imports` rule banning `@renderer` in `src/main` + `src/preload`; §7 tracks the one remaining exception. Two dependencies cut across **every** directory and are **not** layering edges — they are ambient infrastructure access: `@logger` (logging) and `@application` (the DI container / service locator). A raw import scan shows almost everything "depending on `core`" only because of these two; the rules above concern direct module imports between domains. There is no automated enforcement of the **internal** direction edges above yet (unlike the `import/no-restricted-paths` zones proposed for the renderer in [Renderer Architecture §5](./renderer-architecture.md)); direction is held by convention and review. The **external** main↔renderer boundary (no renderer imports) *is* enforced — see the rule above. ## 4. Closed Top-Level Governance > **The top-level set is closed and locked — treat adding a new directory under `src/main/` as off the table.** This is [Naming Conventions §4.8](./naming-conventions.md) (top-level closed by default) at its strict end: §4.8 admits a new top-level directory only on proven *necessity* (no existing category can host the files) and *completeness*, and main's categories already span the space — so a new capability is routed into an existing category, never given its own directory. The one deliberate expansion is `i18n/` (§1), added so the main process owns a locale catalog symmetric with `src/renderer/i18n/`; it is a governed exception with a recorded rationale, not a loosening of the rule. The [renderer (§6)](./renderer-architecture.md) and [`@shared` (§2)](./shared-layer-architecture.md) top levels are held to the same governance. A new capability **never** earns a new top-level directory; route it by nature: | The capability is… | Home | |---|---| | tied to the AI essence | `ai/` | | business data / storage | `data/` | | an IPC route | `ipc/` (IpcApi) | | business-agnostic app-runtime infra | `core/` | | a business service | `services/` — or `features//` if it is a large, multi-file domain | | pure, domain-agnostic logic | `utils/` | ## 5. Anti-Patterns - Business code (anything specific to what Cherry Studio *does*) placed in `core/` — `core/` must stay app-runtime-only. - A `features//` importing a **sibling** feature (cross-domain coupling). - `ai/` importing `features/` (the core domain depending up on a feature). - Opening a new top-level directory for a single capability (§4). - Scattering business data through ad-hoc storage instead of the `data/` subsystems, or imperative commands through ad-hoc channels instead of `ipc/` (IpcApi). ## 6. Subsystem References Per-subsystem depth lives in dedicated docs; this page owns only the directory layout. Do not duplicate subsystem detail here. | Subsystem | Location | Reference | |---|---|---| | Service lifecycle (IoC, phased bootstrap) | `core/lifecycle/`, `core/application/` | [Lifecycle Reference](./lifecycle/README.md) | | Startup phases (preboot / bootstrap / running) | `core/preboot/`, `core/application/` | [core/README](../../src/main/core/README.md) | | Window manager | `core/window/` | [Window Manager Reference](./window-manager/README.md) | | Scheduler & jobs | `core/scheduler/`, `core/job/` | [Job & Scheduler Reference](./job-and-scheduler/README.md) | | Path registry | `core/paths/` | [paths/README](../../src/main/core/paths/README.md) | | Data systems (DB/Cache/Preference/DataApi/BootConfig) | `data/` | [Data System Reference](./data/README.md) | | IPC (IpcApi) | `ipc/` | [IPC Reference](./ipc/README.md) | | AI subsystem | `ai/` | [AI Reference](./ai/README.md) | ## 7. Current Deviations (target vs current) This page describes the **target**. Where current code does not yet match it, the gap is tracked below — **this pass changes no code**. Only structural deviations are listed (the closed top-level set §4, bucket barrels §2.1, placement §2); a per-file naming-suffix audit (§5.2) is out of scope here. | Area | Current | Target | |---|---|---| | `utils/index.ts` | a bucket-root `index.ts` holds loose helpers (`debounce`, `makeSureDirExists`, `toAsarUnpackedPath`, …) — the junk-drawer root barrel §2.1 forbids | split into named topic files (`utils/.ts`); `@shared` has already done exactly this — see [Shared Layer Architecture §6](./shared-layer-architecture.md) | | legacy `ipc.ts` | v1 IPC registration at the process root, coexisting with IpcApi | domains migrate into `ipc/` (IpcApi) incrementally until `ipc.ts` is retired (§1) | By-design edges are **not** deviations and are deliberately omitted: the `data/migration/v2/` migrators reading domain data (§1) and `@logger` / `@application` ambient access from any tier (§3). ## 8. Related - [Architecture Overview](./architecture-overview.md) — process model, data flow, monorepo tree (the cross-process parent of this doc). - [Renderer Architecture](./renderer-architecture.md) / [Shared Layer Architecture](./shared-layer-architecture.md) — the peer per-process directory references. - [Naming Conventions](./naming-conventions.md) — §4.8 closed top-level, §4.9 singular/plural, §4.10 feature vs type-bucket, §5.2 route-by-shape.