# Service Authoring Guide

TeXise keeps service wiring explicit and small. Services should be easy to
inspect, easy to replace in tests, and difficult to accidentally couple across
workspace layers.

## Where Things Go

- Slot IDs live in the service maps, not inside the service implementation.
  The core service map is in
  `packages/core/initialize-services/src/service-setup.ts`; composition roots
  supply only platform slots through `createServiceSetup`, attaching required
  platform configs with `slot(Class, () => config)`.
- Contracts live at the lowest layer that owns the concept. Shared contracts
  belong in `packages/shared/types`; core service APIs belong in
  `packages/core/service-core`; platform implementations belong in
  `packages/platform/service-platform`.
- Implementations stay in their owning package and are exported through that
  package root. Consumers should import from package roots such as
  `@learnitive/service-core` or `@learnitive/service-platform`, not private
  `src` paths.
- Browser wiring belongs in `@learnitive/initialize-services`. It chooses the
  browser implementations, supplies configs, instantiates the container, and
  exposes the typed app services to React.
- Test wiring belongs in `@learnitive/test-utils`. It should mirror production
  wiring while replacing browser-only platform services with memory or fake
  implementations.

## Dependencies

Declare service-to-service dependencies with a static `deps` field:

```ts
export class NavigationService extends BaseService {
  static deps = ['router'] as const;

  constructor(
    context: BaseServiceContext,
    dependencies: { router: BaseRouterService },
  ) {
    super('NavigationService', context, dependencies);
  }
}
```

`static deps` is the constructor contract for services. Always declare it
`as const`; a widened `string[]` is rejected by the map validation below. The
container uses it to build the dependency graph, instantiate services in
dependency order, and report startup diagnostics. Service map validation
(`defineServiceMap`, applied by `createServiceSetup` to the platform map and
locked over the core graph by a compile-time assertion) type-checks that
every declared dependency exists in the map, that constructor dependency keys
match `static deps`, that the registered dependency instance is assignable
to the constructor's dependency type, and that a service whose constructor
requires a config is registered via `slot()` rather than bare. A validation
failure surfaces as the offending map entry not being assignable to `never`;
check that slot's `static deps`, constructor dependency keys, `as const`, and
missing `slot()` config first. If a service reads another service, that
relationship should be visible in `static deps`; do not hide it in mutable
module state, late lookups, or callbacks passed only to work around the graph.

Services should not call the container. They receive their declared
dependencies and config through the constructor, then use their service context
for shared app facilities such as logging, abort handling, and the store. This
keeps the container in composition roots and keeps lower layers oblivious to who
uses them.

## Wiring

Production setup and test setup share one composition builder:
`createServiceSetup` in `@learnitive/initialize-services/setup` (a
platform-free subpath, safe to import from test code). The builder owns the
core service slots and every core config; the map is the complete composition
— there is no post-construction mutation phase (`setConfig`/`use` do not
exist). A composition root supplies only what genuinely differs per
environment:

- `platformServices`: the environment's platform slots, with each required
  config attached at declaration via `slot(Class, () => config)` (router
  strategy, error sink, Native FS root-handle resolution, storage `onChange`).
  A config-requiring class registered bare fails to type-check, and the map
  must satisfy `PlatformRequirements` — the router/database/syncDatabase
  contracts core services depend on.
- `fileStorageSlots`: which of those slots provide file storage.
- the keyboard shortcut target and the theme manager.
- `coreConfigOverrides` (optional, meant for tests): per-slot decorators over
  the canonical core configs, e.g. tuning `userActivityService` cooldowns
  while keeping the production wiring as the base.

The browser root is `@learnitive/initialize-services`; the test root is
`createTestEnvironment` in `@learnitive/test-utils`. Both call the builder, so
adding a core service means updating `coreServiceMap` (and the
`CoreServiceSlotId` union in `@learnitive/types` plus the `CoreServices`
aggregate, which the compiler enforces) — test wiring follows automatically.

General rules:

- Use the same slot IDs for equivalent services.
- Declare configs with their slot via `slot(Class, () => config)`; the factory
  runs lazily at `instantiate()`.
- Keep browser-specific choices in the production composition root.
- Keep memory, fake, or test-only replacements in `@learnitive/test-utils`.
- Mount services through the returned setup helper rather than from individual
  service constructors.

A test that creates the real container is usually better than mocking a
service that participates in lifecycle, persistence, routing, commands, or
editor state.

## Guardrails

`pnpm custom-validation` is the place for lightweight service architecture
checks. It currently enforces the workspace dependency graph, service package
layer order, and service-specific import boundaries:

- Service consumers must not import private `@learnitive/service-core/src/...`,
  `@learnitive/service-platform/src/...`, or
  `@learnitive/initialize-services/src/...` paths.
- `@learnitive/poor-mans-di` is only imported by the DI package itself, the base
  service adapter, the browser composition root, and the test composition
  helper.

Broader checks should only be added when they catch real drift. For example,
checking every `static deps` relationship with text patterns would be fragile:
some platform services have no dependencies, and the map validation plus the
container already fail missing or mismatched dependencies before release. Prefer
a focused unit or integration test when a dependency contract needs more
precision.
