League Akari Shard Development
Use this skill for any League Akari shard work: creating a new shard, adding a feature to an existing shard, splitting a large shard, normalizing names, or reviewing shard organization.
The goal is stable shard architecture: clear responsibilities, boring public contracts, platform-safe side effects, and minimal behavior drift.
Enforcement Scope
These rules are hard requirements for any newly created shard and for any feature migration into a shard. Do not treat them as optional style guidance.
- Existing shards do not need retroactive cleanup just because they predate this skill.
- When creating a new shard, moving a feature into another shard, or consolidating several features under one shard, follow Create Mode / Refactor Mode structure from the start.
- Do not dump migrated feature logic directly into
index.ts, even if the old shard was small. index.ts should stay an entrypoint for DI, settings registration, state sync, controller construction, lifecycle orchestration, and thin compatibility methods.
- For a local change inside an old shard that is not already organized this way, ask the user whether to:
- make a narrow in-place change; or
- first reorganize the touched area according to this skill, then apply the change.
- If the user explicitly requests a fast or narrow fix, keep the edit local but do not make the old structure worse. Add a controller/context split only when the requested change itself creates or migrates a functional module boundary.
Core Rules
- Preserve public contracts unless the user explicitly asks to change them:
@Shard(...) id
- renderer shard id
- bootstrap registration identity
- IPC namespace, call names, and event names
- settings keys
- propSync keys
- renderer store data shape
- persisted data shape
- For refactors, do not split files under 500 lines unless the user explicitly asks or there is a correctness reason.
- For new shards, do not start with a giant
index.ts. Add structure only where it has a real boundary.
- Do not split a coherent flow just because it is long. A clear state-sync pipeline can stay together. If a shared abstraction accumulates many feature-specific flags, hooks, or exceptions, prefer feature-owned paths that make the business behavior explicit.
- Do not create tiny
*-controller.ts, *-executor.ts, or helper files merely because two branches
have different conceptual responsibilities. When the code is still small and tightly owned by one
feature, keep the distinction as clearly named private methods in the existing module. Extract a
new file only when it owns enough behavior, lifecycle, state, platform guarding, or tests to be
worth the extra navigation.
- Prefer mechanical extraction before behavior changes.
- For persisted state or settings that are still changing during active development, manually inspect and align local DB/state instead of adding config migrations immediately. Add migrations when the shape is stable enough for production compatibility.
- Before adding defensive checks, inspect the authoritative type, schema, or data adapter. Handle documented nullability and failure states, but avoid guards for states the contract does not allow.
- Use full descriptive names:
remoteConfig, not rc
leagueClient, not lc
savedPlayer, not sp
settingFactory, not setting
logger, not log
ipc, not ipcMain
- In League Akari shard classes, private fields and methods use a leading
_ prefix.
Keep injected constructor properties private and underscored unless they are intentionally public.
- Keep platform-specific side effects behind explicit platform guard helpers.
- Renderer VNode-heavy modules should be
.tsx; avoid large h(...) chains.
- Use project loggers, not
console.
- Never revert unrelated dirty files.
Initial Workflow
- Check the worktree:
git status --short
- For refactor target discovery, use a Node-based scan instead of OS-specific shell utilities:
node -e "const fs=require('node:fs');const path=require('node:path');const roots=['src/main/shards','src/renderer-shared/shards','src/renderer'];const out=[];function walk(dir){if(!fs.existsSync(dir))return;for(const ent of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,ent.name);if(ent.isDirectory())walk(p);else if(ent.isFile()&&ent.name==='index.ts'&&p.split(path.sep).includes('shards'))out.push(p)}}roots.forEach(walk);out.map(f=>[fs.readFileSync(f,'utf8').split(/\\r?\\n/).length,f]).sort((a,b)=>b[0]-a[0]).forEach(([n,f])=>console.log(String(n).padStart(5),f))"
Read enough code to identify:
- shard ids and public calls
- injected dependencies
- settings/state persistence
- propSync/store shape
- IPC registration
- lifecycle hooks
- reactions or subscriptions that must be owned by a controller
- side effects
- platform assumptions
- pure helpers worth testing
Decide whether the task touches a new/migrated feature or an old shard local edit:
- New shard or feature migration: follow the hard structure rules without asking.
- Old shard local edit: if the user did not specify architecture scope, ask whether to change in place or reorganize the touched area first.
Choose one of two modes:
- Create mode: design the minimal shard shape before coding.
- Refactor mode: preserve behavior first, then split by actual responsibilities.
Create Mode
When creating a new main shard:
- Pick a stable shard id. Main-facing ids normally end with
-main.
- Create
src/main/shards/<name>/index.ts.
- Add
state.ts only if the shard owns observable state/settings.
- Add
context.ts if more than one internal module needs the same dependencies.
- Add
ipc-handlers.ts if renderer calls into it.
- Register the shard in
src/main/bootstrap/index.ts.
- If renderer-facing, create the renderer shard/store under
src/renderer-shared/shards/<name>/.
- Use
SettingFactoryMain for persisted settings and MobxUtilsMain.propSync(...) for synced state.
- Keep side effects behind clearly named methods or controller/executor modules.
- Add focused tests for pure helpers, platform guards, config migration, or race-prone executors.
Minimal new shard shape:
- Main
index.ts: @Shard, injected shards, logger/settings setup, applyToState(), propSync(...), lifecycle, and thin public methods.
- Renderer
index.ts: @Shard, @Dep(...) injections, Pinia/MobX sync, and thin IPC wrapper methods.
- Keep feature logic out of the entry file once it grows beyond one obvious responsibility.
Refactor Mode
After splitting, index.ts should usually contain only:
@Shard(...) class and compatibility constants
- dependency injection
- settings registration
- state initialization and propSync
- construction of controllers/loaders/handlers
- lifecycle orchestration
- thin public methods that existing callers use
Move feature logic into internal modules. Pass a typed context object rather than long constructor parameter lists.
Good entry file after extraction:
@Shard(SelfUpdateMain.id)
export class SelfUpdateMain implements IAkariShardInitDispose {
static id = SELF_UPDATE_MAIN_NAMESPACE
public readonly settings = new SelfUpdateSettings()
public readonly state = new SelfUpdateState()
private readonly context: SelfUpdateMainContext
private readonly executor: SelfUpdateExecutor
private readonly ipcHandlers: SelfUpdateIpcHandlers
constructor(/* injected shards */) {
this.context = {
namespace: SelfUpdateMain.id,
settings: this.settings,
state: this.state /* ... */
}
this.executor = new SelfUpdateExecutor(this.context)
this.ipcHandlers = new SelfUpdateIpcHandlers(this.context, this.executor)
}
async onInit() {
await this.setupState()
this.ipcHandlers.register()
}
}
Bad extraction:
private _rc: RemoteConfigMain
private _ipcMain: AkariIpcMain
private _doEverything() { /* still owns IPC, watchers, fetching, cache, side effects */ }
Allowed Shard Modules
Use a deliberately small set of module shapes for shard feature organization. For newly created
shards and migrated feature code, choose from this list when a file owns shard lifecycle, IPC,
state/store wiring, feature coordination, data loading, command-like side effects, platform guards,
or renderer UI tied to the shard.
index.ts: the shard entrypoint. Owns @Shard, DI, settings registration, state sync, lifecycle
orchestration, internal module construction, and thin compatibility methods.
context.ts: shared internal dependencies, namespace/id constants, settings keys, loading
priorities, and shard-local result types. Add it only when more than one internal module needs the
same dependencies or constants.
state.ts: MobX state/settings classes.
store.ts: Pinia store for renderer shards.
ipc-handlers.ts: IPC onCall / onEvent registration and thin delegation.
platform.ts: pure platform guard helpers.
*-controller.ts: feature flow coordination. Use this for lifecycle, reactions, watchers,
subscriptions, routing, registries, configuration collections, derived UI options, settings
resolution, and any glue code that does not fit loader or executor.
*-loader.ts: data loading and refresh. Use this for remote/local fetches, cache reads/writes,
queue tags, reload methods, and normalizing loaded data before handing it to a controller.
*-executor.ts: one imperative operation that may fail, be canceled, or need a structured result.
Use this for process launches, filesystem writes, downloads, apply/uninstall actions, and other
command-like side effects. Do not extract a separate executor for a few lines of implementation
that are only called by one existing executor; prefer a private method unless the extracted
operation has meaningful independent size, ownership, or focused tests.
*-component.tsx or a descriptive .vue file: renderer component colocated with a shard. Do not
use comp.tsx.
*-notification.tsx, *-modal.tsx, *-dialogs.tsx: renderer notification/modal/dialog modules
that compose VNodes or JSX.
*.test.ts: focused tests for pure helpers, guards, races, migrations, and normalization.
Utility modules are outside the shard-organization suffix rules. If a file is pure support code and
does not express a shard feature boundary, name it naturally for what it contains. Examples include
domain constants, schemas, mapping tables, parsers, formatters, tiny calculation helpers, and
test-only builders. These files must not own IPC, lifecycle, timers, reactions, IO, mutable state, or
feature coordination. If they start owning one of those responsibilities, move the behavior into one
of the fixed shard module shapes above.
Project-specific exceptions:
state.ts and store.ts are fixed names for MobX state/settings and Pinia stores.
- TypeORM entities keep their entity names under
storage/entities/.
- Storage upgrades and config migrations keep versioned names such as
version-15.ts and from-1-4-3.ts.
league-client/lc-state/ may use LCU endpoint domain names such as champ-select.ts and gameflow.ts
because that directory is a coherent state-sync pipeline.
Context Pattern
Use context when multiple internal modules need shared dependencies.
export const SELF_UPDATE_MAIN_NAMESPACE = 'self-update-main'
export const PLATFORM_UNSUPPORTED_REASON = 'platform-unsupported'
export interface SelfUpdateMainContext {
namespace: string
settings: SelfUpdateSettings
state: SelfUpdateState
logger: AkariLogger
appCommon: AppCommonMain
ipc: AkariIpcMain
mobxUtils: MobxUtilsMain
remoteConfig: RemoteConfigMain
httpClient: AxiosInstance
}
Keep original static class constants as aliases if existing code may reference them.
Naming Rules
Use role-based, fully spelled names:
matchHistoryLoader
playerDataLoader
additionalInfoController
sideEffectsController
settingsController
logger
ipc
Choose names in this order:
- First decide whether a new file is warranted. A small, single-owner branch inside an existing
executor/controller should usually become a private method, not a new module.
- If it is the shard entry, use
index.ts.
- If it owns MobX state/settings or a Pinia store, use
state.ts or store.ts.
- If it only registers IPC, use
ipc-handlers.ts.
- If it only contains platform predicates, use
platform.ts.
- If it loads, refreshes, caches, or normalizes data, use
*-loader.ts.
- If it performs one command-like side effect, use
*-executor.ts.
- If it coordinates a feature flow, owns reactions/subscriptions, routes by domain keys, manages
registrations, resolves settings into runtime choices, or prepares derived UI options, use
*-controller.ts.
- If it renders notification/modal/dialog/component UI, use the renderer UI suffixes above.
- If it is pure utility/support code and does not express shard feature organization, use any clear
descriptive file name.
When two role names seem plausible, choose the narrower allowed role first: loader for data
loading, executor for imperative commands, then controller for coordination. For example, a
module that "watches state and starts a reload" is a controller; the reload implementation itself
can live in a loader. A module that "resolves settings and launches a process" is usually a
controller plus an executor.
Do not rename public contracts only for style.
ongoing-game-main is a contract, not a naming cleanup target.
Renderer Rules
Renderer shard entries follow the same orchestration rules.
- Keep composables (
useDialog, useNotification, useTranslation, Pinia stores) inside setup functions or functions called from setupInAppScope.addSetupFn(...), not at module top level.
- Render-heavy notification/modal modules should be
.tsx.
- Reactive setup without JSX should live in a controller module.
- Colocated renderer components should use
*-component.tsx or a descriptive .vue filename.
Do not add vague files like comp.tsx.
- Preserve store fields and modal state shape.
Good TSX:
export function registerUpdateDownloadFailedNotification(
context: SimpleNotificationsRendererContext
) {
const notification = useNotification()
const { t } = useTranslation(undefined, {
keyPrefix: 'simple-notifications-renderer.updateDownloadFailed'
})
context.ipc.onEventVue(SelfUpdateRenderer.id, 'error-download-update', (error) => {
const item = notification.warning({
title: () => t('title'),
content: () => (
<div>
{t('content', { error: error.message })}
<div class="flex justify-end gap-2">
<NButton size="tiny" => item.destroy()}>
{t('negativeText')}
</NButton>
</div>
</div>
)
})
})
}
Avoid:
content: () => h('div', [h('span', text), h(NButton, { onClick }, () => label)])
For Vue model/update events in TSX, preserve exact event props with object spread:
<UpdateModal
{...{
show: store.showNewReleaseModal,
'onUpdate:show': (value: boolean) => (store.showNewReleaseModal = value),
onStartDownload: () => selfUpdate.startUpdate()
}}
/>
Platform Guards
When behavior is platform-specific, add pure guard helpers and use them at every side-effect boundary.
export function shouldRunSelfUpdateLifecycle(platform: NodeJS.Platform = process.platform) {
return platform === 'win32'
}
export function shouldDownloadUpdateArchive(platform: NodeJS.Platform = process.platform) {
return shouldRunSelfUpdateLifecycle(platform)
}
Guard:
- lifecycle start
- IPC handlers
- filesystem writes
- process spawning
- download/unpack/apply
- uninstall
- Windows APIs such as JumpList, WMI, registry,
.exe updater
In tests, do not hardcode Unix paths like /tmp. Use os.tmpdir() and path.join(...).
IPC Rules
Keep IPC handlers thin and return real controller results.
this.context.ipc.onCall(this.context.namespace, 'startUpdate', async () => {
if (!shouldRunSelfUpdateLifecycle()) {
return { result: 'failed', reason: PLATFORM_UNSUPPORTED_REASON }
}
const release = this.context.remoteConfig.state.latestRelease
if (!release?.isNew) return { result: 'no-op' }
return await this.executor.start(release)
})
Avoid swallowing results:
await this.executor.start(release)
return { result: 'ok' }
Logging
Use LoggerFactoryMain / LoggerRenderer.
warn: unexpected but recoverable.
error: current flow cannot continue.
- Do not log auth secrets, Riot/LCU tokens, or credential-bearing command lines.
- Good log boundaries: lifecycle start/end, IPC registration/call, remote fetch start/end, connection state transitions, queue start/end, cancel/abort.
Testing And Verification
Add focused tests for:
- pure mapping/extraction helpers
- platform guards
- cancellation/race-prone executors
- config migrations
- data normalization edge cases
Prefer cross-platform commands. Avoid examples that depend on GNU tools, Bash-only syntax, or macOS-only paths unless the task itself is platform-specific.
Normal verification:
yarn prettier --write <changed-files>
yarn typecheck:node
yarn typecheck:web
yarn test
git diff --check
If the repo supports a single aggregate command, yarn typecheck is also acceptable. Report exact commands that were actually run.
Examples
- New renderer-facing shard: create the main
index.ts, add state.ts only when it owns
observable state/settings, register settings with SettingFactoryMain, sync with
MobxUtilsMain.propSync(...), register the shard in bootstrap, and add the renderer shard/store
only when renderer access is required.
- Large feature split: keep the original shard id, IPC names, settings keys, propSync shape, and
renderer store shape unchanged. Extract
context.ts, loaders, controllers, executors, and
ipc-handlers.ts only around actual responsibilities.
- Coherent flow: keep related steps such as ban and pick together in one controller when they form
one business flow. Split unrelated bench, trade, config, or local-message behavior elsewhere.
- Declarative remote config: put resource declarations in a pure data file such as
cached-resources.ts, put common sync orchestration in cached-sync-controller.ts, and keep
special release or announcement behavior in dedicated controllers.
- Windows-only updater: add
platform.ts, guard lifecycle, IPC, download, apply, and uninstall
side-effect boundaries, and put download/apply work in executors that return structured results.
- Renderer notification center: keep the renderer shard entry as orchestration, use
.tsx for
VNode-heavy notification/modal/dialog modules, and keep composables inside setup functions or
setup-registered functions.
- Long coherent state sync: do not split
league-client/lc-state/index.ts just because it is long.
Preserve initial fetch, websocket event updates, disconnect cleanup, and MobX reaction coupling
unless the user explicitly asks for a split or the endpoint domains become hard to maintain.
Final Checklist
- New shard is registered and renderer-facing pieces are wired.
- Refactored entry file is orchestration, not a hidden giant.
- Each moved module has one responsibility.
- Names are full and descriptive.
- Public ids, IPC names, settings keys, propSync keys, and store shapes are unchanged unless requested.
- Platform-specific side effects are guarded.
- Renderer JSX lives in
.tsx.
- Commands and tests do not assume macOS-only paths.
- No unrelated dirty files were reverted or reformatted.
- Verification commands were run and reported.
1---2name: league-akari-shard-development3description: Use when creating, extending, refactoring, splitting, or reviewing League Akari main or renderer shards, including shard file organization, controller/loader/executor/handler boundaries, naming conventions, renderer TSX usage, platform guards, and public contract compatibility.4---56# League Akari Shard Development78Use this skill for any League Akari shard work: creating a new shard, adding a feature to an existing shard, splitting a large shard, normalizing names, or reviewing shard organization.910The goal is stable shard architecture: clear responsibilities, boring public contracts, platform-safe side effects, and minimal behavior drift.1112## Enforcement Scope1314These rules are hard requirements for any newly created shard and for any feature migration into a shard. Do not treat them as optional style guidance.1516- Existing shards do not need retroactive cleanup just because they predate this skill.17- When creating a new shard, moving a feature into another shard, or consolidating several features under one shard, follow Create Mode / Refactor Mode structure from the start.18- Do not dump migrated feature logic directly into `index.ts`, even if the old shard was small. `index.ts` should stay an entrypoint for DI, settings registration, state sync, controller construction, lifecycle orchestration, and thin compatibility methods.19- For a local change inside an old shard that is not already organized this way, ask the user whether to:20 - make a narrow in-place change; or21 - first reorganize the touched area according to this skill, then apply the change.22- If the user explicitly requests a fast or narrow fix, keep the edit local but do not make the old structure worse. Add a controller/context split only when the requested change itself creates or migrates a functional module boundary.2324## Core Rules2526- Preserve public contracts unless the user explicitly asks to change them:27 - `@Shard(...)` id28 - renderer shard id29 - bootstrap registration identity30 - IPC namespace, call names, and event names31 - settings keys32 - propSync keys33 - renderer store data shape34 - persisted data shape35- For refactors, do not split files under 500 lines unless the user explicitly asks or there is a correctness reason.36- For new shards, do not start with a giant `index.ts`. Add structure only where it has a real boundary.37- Do not split a coherent flow just because it is long. A clear state-sync pipeline can stay together. If a shared abstraction accumulates many feature-specific flags, hooks, or exceptions, prefer feature-owned paths that make the business behavior explicit.38- Do not create tiny `*-controller.ts`, `*-executor.ts`, or helper files merely because two branches39 have different conceptual responsibilities. When the code is still small and tightly owned by one40 feature, keep the distinction as clearly named private methods in the existing module. Extract a41 new file only when it owns enough behavior, lifecycle, state, platform guarding, or tests to be42 worth the extra navigation.43- Prefer mechanical extraction before behavior changes.44- For persisted state or settings that are still changing during active development, manually inspect and align local DB/state instead of adding config migrations immediately. Add migrations when the shape is stable enough for production compatibility.45- Before adding defensive checks, inspect the authoritative type, schema, or data adapter. Handle documented nullability and failure states, but avoid guards for states the contract does not allow.46- Use full descriptive names:47 - `remoteConfig`, not `rc`48 - `leagueClient`, not `lc`49 - `savedPlayer`, not `sp`50 - `settingFactory`, not `setting`51 - `logger`, not `log`52 - `ipc`, not `ipcMain`53- In League Akari shard classes, private fields and methods use a leading `_` prefix.54 Keep injected constructor properties private and underscored unless they are intentionally public.55- Keep platform-specific side effects behind explicit platform guard helpers.56- Renderer VNode-heavy modules should be `.tsx`; avoid large `h(...)` chains.57- Use project loggers, not `console`.58- Never revert unrelated dirty files.5960## Initial Workflow61621. Check the worktree:6364```bash65git status --short66```67682. For refactor target discovery, use a Node-based scan instead of OS-specific shell utilities:6970```bash71node -e "const fs=require('node:fs');const path=require('node:path');const roots=['src/main/shards','src/renderer-shared/shards','src/renderer'];const out=[];function walk(dir){if(!fs.existsSync(dir))return;for(const ent of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,ent.name);if(ent.isDirectory())walk(p);else if(ent.isFile()&&ent.name==='index.ts'&&p.split(path.sep).includes('shards'))out.push(p)}}roots.forEach(walk);out.map(f=>[fs.readFileSync(f,'utf8').split(/\\r?\\n/).length,f]).sort((a,b)=>b[0]-a[0]).forEach(([n,f])=>console.log(String(n).padStart(5),f))"72```73743. Read enough code to identify:75 - shard ids and public calls76 - injected dependencies77 - settings/state persistence78 - propSync/store shape79 - IPC registration80 - lifecycle hooks81 - reactions or subscriptions that must be owned by a controller82 - side effects83 - platform assumptions84 - pure helpers worth testing85864. Decide whether the task touches a new/migrated feature or an old shard local edit:87 - New shard or feature migration: follow the hard structure rules without asking.88 - Old shard local edit: if the user did not specify architecture scope, ask whether to change in place or reorganize the touched area first.89905. Choose one of two modes:91 - **Create mode**: design the minimal shard shape before coding.92 - **Refactor mode**: preserve behavior first, then split by actual responsibilities.9394## Create Mode9596When creating a new main shard:97981. Pick a stable shard id. Main-facing ids normally end with `-main`.992. Create `src/main/shards/<name>/index.ts`.1003. Add `state.ts` only if the shard owns observable state/settings.1014. Add `context.ts` if more than one internal module needs the same dependencies.1025. Add `ipc-handlers.ts` if renderer calls into it.1036. Register the shard in `src/main/bootstrap/index.ts`.1047. If renderer-facing, create the renderer shard/store under `src/renderer-shared/shards/<name>/`.1058. Use `SettingFactoryMain` for persisted settings and `MobxUtilsMain.propSync(...)` for synced state.1069. Keep side effects behind clearly named methods or controller/executor modules.10710. Add focused tests for pure helpers, platform guards, config migration, or race-prone executors.108109Minimal new shard shape:110111- Main `index.ts`: `@Shard`, injected shards, logger/settings setup, `applyToState()`, `propSync(...)`, lifecycle, and thin public methods.112- Renderer `index.ts`: `@Shard`, `@Dep(...)` injections, Pinia/MobX sync, and thin IPC wrapper methods.113- Keep feature logic out of the entry file once it grows beyond one obvious responsibility.114115## Refactor Mode116117After splitting, `index.ts` should usually contain only:118119- `@Shard(...)` class and compatibility constants120- dependency injection121- settings registration122- state initialization and propSync123- construction of controllers/loaders/handlers124- lifecycle orchestration125- thin public methods that existing callers use126127Move feature logic into internal modules. Pass a typed context object rather than long constructor parameter lists.128129Good entry file after extraction:130131```ts132@Shard(SelfUpdateMain.id)133export class SelfUpdateMain implements IAkariShardInitDispose {134 static id = SELF_UPDATE_MAIN_NAMESPACE135136 public readonly settings = new SelfUpdateSettings()137 public readonly state = new SelfUpdateState()138139 private readonly context: SelfUpdateMainContext140 private readonly executor: SelfUpdateExecutor141 private readonly ipcHandlers: SelfUpdateIpcHandlers142143 constructor(/* injected shards */) {144 this.context = {145 namespace: SelfUpdateMain.id,146 settings: this.settings,147 state: this.state /* ... */148 }149 this.executor = new SelfUpdateExecutor(this.context)150 this.ipcHandlers = new SelfUpdateIpcHandlers(this.context, this.executor)151 }152153 async onInit() {154 await this.setupState()155 this.ipcHandlers.register()156 }157}158```159160Bad extraction:161162```ts163private _rc: RemoteConfigMain164private _ipcMain: AkariIpcMain165private _doEverything() { /* still owns IPC, watchers, fetching, cache, side effects */ }166```167168## Allowed Shard Modules169170Use a deliberately small set of module shapes for shard feature organization. For newly created171shards and migrated feature code, choose from this list when a file owns shard lifecycle, IPC,172state/store wiring, feature coordination, data loading, command-like side effects, platform guards,173or renderer UI tied to the shard.174175- `index.ts`: the shard entrypoint. Owns `@Shard`, DI, settings registration, state sync, lifecycle176 orchestration, internal module construction, and thin compatibility methods.177- `context.ts`: shared internal dependencies, namespace/id constants, settings keys, loading178 priorities, and shard-local result types. Add it only when more than one internal module needs the179 same dependencies or constants.180- `state.ts`: MobX state/settings classes.181- `store.ts`: Pinia store for renderer shards.182- `ipc-handlers.ts`: IPC `onCall` / `onEvent` registration and thin delegation.183- `platform.ts`: pure platform guard helpers.184- `*-controller.ts`: feature flow coordination. Use this for lifecycle, reactions, watchers,185 subscriptions, routing, registries, configuration collections, derived UI options, settings186 resolution, and any glue code that does not fit `loader` or `executor`.187- `*-loader.ts`: data loading and refresh. Use this for remote/local fetches, cache reads/writes,188 queue tags, reload methods, and normalizing loaded data before handing it to a controller.189- `*-executor.ts`: one imperative operation that may fail, be canceled, or need a structured result.190 Use this for process launches, filesystem writes, downloads, apply/uninstall actions, and other191 command-like side effects. Do not extract a separate executor for a few lines of implementation192 that are only called by one existing executor; prefer a private method unless the extracted193 operation has meaningful independent size, ownership, or focused tests.194- `*-component.tsx` or a descriptive `.vue` file: renderer component colocated with a shard. Do not195 use `comp.tsx`.196- `*-notification.tsx`, `*-modal.tsx`, `*-dialogs.tsx`: renderer notification/modal/dialog modules197 that compose VNodes or JSX.198- `*.test.ts`: focused tests for pure helpers, guards, races, migrations, and normalization.199200Utility modules are outside the shard-organization suffix rules. If a file is pure support code and201does not express a shard feature boundary, name it naturally for what it contains. Examples include202domain constants, schemas, mapping tables, parsers, formatters, tiny calculation helpers, and203test-only builders. These files must not own IPC, lifecycle, timers, reactions, IO, mutable state, or204feature coordination. If they start owning one of those responsibilities, move the behavior into one205of the fixed shard module shapes above.206207Project-specific exceptions:208209- `state.ts` and `store.ts` are fixed names for MobX state/settings and Pinia stores.210- TypeORM entities keep their entity names under `storage/entities/`.211- Storage upgrades and config migrations keep versioned names such as `version-15.ts` and `from-1-4-3.ts`.212- `league-client/lc-state/` may use LCU endpoint domain names such as `champ-select.ts` and `gameflow.ts`213 because that directory is a coherent state-sync pipeline.214215## Context Pattern216217Use context when multiple internal modules need shared dependencies.218219```ts220export const SELF_UPDATE_MAIN_NAMESPACE = 'self-update-main'221export const PLATFORM_UNSUPPORTED_REASON = 'platform-unsupported'222223export interface SelfUpdateMainContext {224 namespace: string225 settings: SelfUpdateSettings226 state: SelfUpdateState227 logger: AkariLogger228 appCommon: AppCommonMain229 ipc: AkariIpcMain230 mobxUtils: MobxUtilsMain231 remoteConfig: RemoteConfigMain232 httpClient: AxiosInstance233}234```235236Keep original static class constants as aliases if existing code may reference them.237238## Naming Rules239240Use role-based, fully spelled names:241242- `matchHistoryLoader`243- `playerDataLoader`244- `additionalInfoController`245- `sideEffectsController`246- `settingsController`247- `logger`248- `ipc`249250Choose names in this order:2512520. First decide whether a new file is warranted. A small, single-owner branch inside an existing253 executor/controller should usually become a private method, not a new module.2541. If it is the shard entry, use `index.ts`.2552. If it owns MobX state/settings or a Pinia store, use `state.ts` or `store.ts`.2563. If it only registers IPC, use `ipc-handlers.ts`.2574. If it only contains platform predicates, use `platform.ts`.2585. If it loads, refreshes, caches, or normalizes data, use `*-loader.ts`.2596. If it performs one command-like side effect, use `*-executor.ts`.2607. If it coordinates a feature flow, owns reactions/subscriptions, routes by domain keys, manages261 registrations, resolves settings into runtime choices, or prepares derived UI options, use262 `*-controller.ts`.2638. If it renders notification/modal/dialog/component UI, use the renderer UI suffixes above.2649. If it is pure utility/support code and does not express shard feature organization, use any clear265 descriptive file name.266267When two role names seem plausible, choose the narrower allowed role first: `loader` for data268loading, `executor` for imperative commands, then `controller` for coordination. For example, a269module that "watches state and starts a reload" is a controller; the reload implementation itself270can live in a loader. A module that "resolves settings and launches a process" is usually a271controller plus an executor.272273Do not rename public contracts only for style.274275`ongoing-game-main` is a contract, not a naming cleanup target.276277## Renderer Rules278279Renderer shard entries follow the same orchestration rules.280281- Keep composables (`useDialog`, `useNotification`, `useTranslation`, Pinia stores) inside setup functions or functions called from `setupInAppScope.addSetupFn(...)`, not at module top level.282- Render-heavy notification/modal modules should be `.tsx`.283- Reactive setup without JSX should live in a controller module.284- Colocated renderer components should use `*-component.tsx` or a descriptive `.vue` filename.285 Do not add vague files like `comp.tsx`.286- Preserve store fields and modal state shape.287288Good TSX:289290```tsx291export function registerUpdateDownloadFailedNotification(292 context: SimpleNotificationsRendererContext293) {294 const notification = useNotification()295 const { t } = useTranslation(undefined, {296 keyPrefix: 'simple-notifications-renderer.updateDownloadFailed'297 })298299 context.ipc.onEventVue(SelfUpdateRenderer.id, 'error-download-update', (error) => {300 const item = notification.warning({301 title: () => t('title'),302 content: () => (303 <div>304 {t('content', { error: error.message })}305 <div class="flex justify-end gap-2">306 <NButton size="tiny" onClick={() => item.destroy()}>307 {t('negativeText')}308 </NButton>309 </div>310 </div>311 )312 })313 })314}315```316317Avoid:318319```ts320content: () => h('div', [h('span', text), h(NButton, { onClick }, () => label)])321```322323For Vue model/update events in TSX, preserve exact event props with object spread:324325```tsx326<UpdateModal327 {...{328 show: store.showNewReleaseModal,329 'onUpdate:show': (value: boolean) => (store.showNewReleaseModal = value),330 onStartDownload: () => selfUpdate.startUpdate()331 }}332/>333```334335## Platform Guards336337When behavior is platform-specific, add pure guard helpers and use them at every side-effect boundary.338339```ts340export function shouldRunSelfUpdateLifecycle(platform: NodeJS.Platform = process.platform) {341 return platform === 'win32'342}343344export function shouldDownloadUpdateArchive(platform: NodeJS.Platform = process.platform) {345 return shouldRunSelfUpdateLifecycle(platform)346}347```348349Guard:350351- lifecycle start352- IPC handlers353- filesystem writes354- process spawning355- download/unpack/apply356- uninstall357- Windows APIs such as JumpList, WMI, registry, `.exe` updater358359In tests, do not hardcode Unix paths like `/tmp`. Use `os.tmpdir()` and `path.join(...)`.360361## IPC Rules362363Keep IPC handlers thin and return real controller results.364365```ts366this.context.ipc.onCall(this.context.namespace, 'startUpdate', async () => {367 if (!shouldRunSelfUpdateLifecycle()) {368 return { result: 'failed', reason: PLATFORM_UNSUPPORTED_REASON }369 }370371 const release = this.context.remoteConfig.state.latestRelease372 if (!release?.isNew) return { result: 'no-op' }373374 return await this.executor.start(release)375})376```377378Avoid swallowing results:379380```ts381await this.executor.start(release)382return { result: 'ok' }383```384385## Logging386387Use `LoggerFactoryMain` / `LoggerRenderer`.388389- `warn`: unexpected but recoverable.390- `error`: current flow cannot continue.391- Do not log auth secrets, Riot/LCU tokens, or credential-bearing command lines.392- Good log boundaries: lifecycle start/end, IPC registration/call, remote fetch start/end, connection state transitions, queue start/end, cancel/abort.393394## Testing And Verification395396Add focused tests for:397398- pure mapping/extraction helpers399- platform guards400- cancellation/race-prone executors401- config migrations402- data normalization edge cases403404Prefer cross-platform commands. Avoid examples that depend on GNU tools, Bash-only syntax, or macOS-only paths unless the task itself is platform-specific.405406Normal verification:407408```bash409yarn prettier --write <changed-files>410yarn typecheck:node411yarn typecheck:web412yarn test413git diff --check414```415416If the repo supports a single aggregate command, `yarn typecheck` is also acceptable. Report exact commands that were actually run.417418## Examples419420- New renderer-facing shard: create the main `index.ts`, add `state.ts` only when it owns421 observable state/settings, register settings with `SettingFactoryMain`, sync with422 `MobxUtilsMain.propSync(...)`, register the shard in bootstrap, and add the renderer shard/store423 only when renderer access is required.424- Large feature split: keep the original shard id, IPC names, settings keys, propSync shape, and425 renderer store shape unchanged. Extract `context.ts`, loaders, controllers, executors, and426 `ipc-handlers.ts` only around actual responsibilities.427- Coherent flow: keep related steps such as ban and pick together in one controller when they form428 one business flow. Split unrelated bench, trade, config, or local-message behavior elsewhere.429- Declarative remote config: put resource declarations in a pure data file such as430 `cached-resources.ts`, put common sync orchestration in `cached-sync-controller.ts`, and keep431 special release or announcement behavior in dedicated controllers.432- Windows-only updater: add `platform.ts`, guard lifecycle, IPC, download, apply, and uninstall433 side-effect boundaries, and put download/apply work in executors that return structured results.434- Renderer notification center: keep the renderer shard entry as orchestration, use `.tsx` for435 VNode-heavy notification/modal/dialog modules, and keep composables inside setup functions or436 setup-registered functions.437- Long coherent state sync: do not split `league-client/lc-state/index.ts` just because it is long.438 Preserve initial fetch, websocket event updates, disconnect cleanup, and MobX reaction coupling439 unless the user explicitly asks for a split or the endpoint domains become hard to maintain.440441## Final Checklist442443- New shard is registered and renderer-facing pieces are wired.444- Refactored entry file is orchestration, not a hidden giant.445- Each moved module has one responsibility.446- Names are full and descriptive.447- Public ids, IPC names, settings keys, propSync keys, and store shapes are unchanged unless requested.448- Platform-specific side effects are guarded.449- Renderer JSX lives in `.tsx`.450- Commands and tests do not assume macOS-only paths.451- No unrelated dirty files were reverted or reformatted.452- Verification commands were run and reported.