Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
3b2a578
Add check.yml: typecheck, lint, test on PRs and main across 3 OSes
sevenns Sep 11, 2026
7fb0dd2
Guard path.posix in platform layer with a source-as-text test; fix 6 …
sevenns Sep 11, 2026
5bdd68d
Lint: warn on process.platform outside the platform layer (allowlist)
sevenns Sep 11, 2026
19709c0
Add file-size ratchet test over src/** and the four screen factories
sevenns Sep 11, 2026
22238e6
Docs: Prettier is not a CI gate; Tooling section names check.yml
sevenns Sep 11, 2026
f952cc6
Fix Windows runner: CRLF-safe ratchet span, separator-agnostic compat…
sevenns Sep 11, 2026
49bdcd8
Merge pull request #46 from sevenns/chore/arch-audit-phase-0
sevenns Sep 11, 2026
e6cb1d5
README: Settings section, Logs pointer and project structure match th…
sevenns Sep 11, 2026
18f7a5c
CLAUDE.md: DI reference, renderer state wording, mechanical guards se…
sevenns Sep 11, 2026
070b279
Fix stale comments: HOLD_DELAY_MS, types.ts header, locateSteam, Sett…
sevenns Sep 11, 2026
10cad8b
Replace references to external plan/audit documents with self-contain…
sevenns Sep 11, 2026
c9a4505
Merge pull request #48 from sevenns/chore/arch-audit-phase-1
sevenns Sep 11, 2026
a24c719
Extract SettingsService: settings:* IPC moves out of UpdaterService
sevenns Sep 11, 2026
8bc4947
Controller seams: dep slices, ProcessControl, characterization test
sevenns Sep 11, 2026
311e01c
Extract uninstaller.win32.ts from the controller, with unit tests
sevenns Sep 11, 2026
4e23587
Extract BrowsePresenter (what the window shows) from the controller
sevenns Sep 11, 2026
c0e1c00
Merge sequences into runSequence; ipc.ts becomes game-controller.ts
sevenns Sep 11, 2026
8177bed
Split file-picker-service and game-move-transaction out of game-config
sevenns Sep 11, 2026
f843526
Move the last process.platform checks onto Platform; rule set to error
sevenns Sep 11, 2026
59b4f2a
Fold ENOENT, manifest-parse, describe and send duplicates; warn on reads
sevenns Sep 11, 2026
aec5bc6
shared/types.ts: manifest model to main, drop ThemeMode, section banners
sevenns Sep 11, 2026
4e17cc6
Format the modules added in this branch; ratchet entry for types.ts
sevenns Sep 11, 2026
bb12a19
Controller test: wait on journal markers instead of a fixed yield count
sevenns Sep 11, 2026
d5b20f8
Controller test: exit() opens the wait gate even when requested befor…
sevenns Sep 11, 2026
baf2b87
Controller test: the fake installer writes the exe after the pre-clean
sevenns Sep 11, 2026
62ac5a6
Merge pull request #49 from sevenns/chore/arch-audit-phase-2
sevenns Sep 15, 2026
8846021
Customize tests: horizontal navigation, pane entry and inert-row skip…
sevenns Sep 15, 2026
1e7e8f5
Move TextEntrySurface and FilePickerSurface to nav-surface.ts
sevenns Sep 15, 2026
4eddced
Move the boot reveal and the busy-phase chatter out of app.ts
sevenns Sep 15, 2026
b2e8925
ControlsApi seam over window.api, controls-deps.ts, first controls DO…
sevenns Sep 15, 2026
3ae0baf
list-screen-core, marquee.ts, shared pressFlash and hover guard for t…
sevenns Sep 15, 2026
a629dbd
Customize: the column menu stack moves to menu-stack.ts
sevenns Sep 15, 2026
6691950
Customize: the artwork lightbox and thumbnails move to asset-lightbox.ts
sevenns Sep 15, 2026
9473ad7
Customize: debounced validation and the issue split move to manifest-…
sevenns Sep 15, 2026
dd7d123
controls: the idle guard and the keyboard controller move to idle.ts …
sevenns Sep 15, 2026
0d84778
controls: the popup column moves to popups.ts (+ confirm.ts, details-…
sevenns Sep 15, 2026
c09b962
i18n: ru is the full record, dead keys dropped, .js on every renderer…
sevenns Sep 15, 2026
be92ba1
Customize: the field writers are pure functions of game-settings-mode…
sevenns Sep 15, 2026
163bf86
Customize: the list rows' editor moves to list-editor.ts
sevenns Sep 15, 2026
995ad3b
Customize: the Find-online flow moves to online-flow.ts
sevenns Sep 15, 2026
92bbd9d
Customize: the column entries, status notes and artwork signature are…
sevenns Sep 15, 2026
d5314f1
Merge pull request #50 from sevenns/chore/arch-audit-phase-3
sevenns Sep 15, 2026
95f4ba6
Fix six clipped stars in the bundled wallpaper
sevenns Sep 15, 2026
ab27f5d
Merge pull request #51 from sevenns/fix/wallpaper-star-artifacts
sevenns Sep 15, 2026
9d719b8
Escape the NUL in issueKey and strip a BOM in parseManifestItems
sevenns Sep 15, 2026
97b9833
Move removeWithRetry and describePickRejection out of win32/picker mo…
sevenns Sep 15, 2026
80a0b1b
Cover steam, in-body failures and IPC double registration in tests
sevenns Sep 15, 2026
c7233e9
Drop the dead electron cache step and build on every PR
sevenns Sep 15, 2026
180fd6f
Refresh stale comments, README labels and i18n headers after the split
sevenns Sep 15, 2026
f151216
Bump version to 0.9.0-alpha.1
sevenns Sep 16, 2026
cc024c1
Settings: write the last slider step a fast burst held back
sevenns Sep 16, 2026
af7602d
Metadata: log every refused source; name a rejected SteamGridDB key
sevenns Sep 16, 2026
d5183bd
Manifest: a missing required field says so instead of quoting zod
sevenns Sep 16, 2026
6aa2860
Settings store: parse the file once per change, not once per read
sevenns Sep 16, 2026
1f8783b
Vitest logs to a temp dir; split manifest-issues, trailing-throttle
sevenns Sep 16, 2026
24b3309
Focus ring skips the hidden Play of a game with nothing to start
sevenns Sep 16, 2026
9594d94
Customize: an existing game's id no longer follows a title rename
sevenns Sep 16, 2026
b6baa5d
Customize: an omitted number steps from the launcher's default
sevenns Sep 16, 2026
a81fe97
Library: a PC-library rename is not a colliding card
sevenns Sep 16, 2026
014e806
CLAUDE.md: name the test logger setup
sevenns Sep 16, 2026
bc4a70a
Bump version to 0.9.0-alpha.2
sevenns Sep 16, 2026
24c6296
Hero: key the palette cache by image, not by game and slot
sevenns Sep 16, 2026
1e3f3e2
Find online: a gallery waits from empty on a filter or section change
sevenns Sep 16, 2026
b4c9124
Merge pull request #52 from sevenns/fix/qa-run-findings
sevenns Sep 16, 2026
4fc006b
Hero: a replayed payload keeps the rotated-to image
sevenns Oct 3, 2026
ab10859
Bump version to 0.9.0-alpha.3
sevenns Oct 3, 2026
7b9e727
Reload: a removed game hands over to the row's first, not the file's
sevenns Oct 3, 2026
5d88644
Bump version to 0.9.0-alpha.4
sevenns Oct 3, 2026
ce6dd03
Select: a Steam download in flight keeps the state on its game
sevenns Oct 5, 2026
bb9e1db
Bump version to 0.9.0-alpha.5
sevenns Oct 5, 2026
eea6380
Select: a Steam download no longer pins the selection to its game
sevenns Oct 5, 2026
244a5dd
Add the per-game activity type and its registry
sevenns Oct 5, 2026
3a4f7cc
Steam: track downloads and removals per game as activities
sevenns Oct 5, 2026
af6a9a6
Test: tolerate the late ready of the startup Steam scan
sevenns Oct 5, 2026
615ddb4
Bump version to 0.9.0-alpha.6
sevenns Oct 5, 2026
a7ed906
Details: Force close only on the running game's own screen
sevenns Oct 5, 2026
bfdbad5
Steam: show a finished pre-load instead of a paused 100%
sevenns Oct 5, 2026
bc0c091
Bump version to 0.9.0-alpha.7
sevenns Oct 5, 2026
0dfbabd
Merge pull request #53 from sevenns/feature/per-game-activity
sevenns Oct 5, 2026
2bd9ad1
Linux: serialize winetricks provisioning across games
sevenns Oct 5, 2026
3171fe6
Add GameJobs: background copy installs, uninstalls, prefix cleanups
sevenns Oct 5, 2026
1bdffc8
Run copy installs, uninstalls and prefix cleanups as per-game jobs
sevenns Oct 5, 2026
189befc
Quit, shutdown and reboot ask first while jobs run
sevenns Oct 5, 2026
7be2d84
Run card installers as background jobs with session-aware queues
sevenns Oct 5, 2026
3b00b67
Act on games by id: install or remove beside a running game
sevenns Oct 5, 2026
04d0ee5
AppState is the session only: drop installing and uninstalling
sevenns Oct 5, 2026
fb88a3e
Docs: background installs, the session vs activity models
sevenns Oct 5, 2026
b021dfa
Bump version to 0.9.0-alpha.8
sevenns Oct 5, 2026
6d67668
Updater: install silently and relaunch instead of showing the NSIS wi…
sevenns Oct 6, 2026
9b977bb
Bump version to 0.9.0-alpha.9
sevenns Oct 6, 2026
6c12745
Bump version to 0.9.0-alpha.10
sevenns Oct 6, 2026
4613b8c
Merge release/v0.9.0 into feature/per-game-activity
sevenns Oct 6, 2026
3d636d9
Bump version to 0.9.0-alpha.11
sevenns Oct 6, 2026
db17157
Merge pull request #55 from sevenns/feature/per-game-activity
sevenns Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
- name: Compile TypeScript + assets
run: npm run build

# Fetch the umu-launcher zipapp into resources/umu (extraResources) for the Proton launcher (Р1).
# Fetch the umu-launcher zipapp into resources/umu (extraResources) for the Proton launcher.
- name: Fetch umu-launcher
run: npm run build:umu

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Build macOS

# Build the macOS (Apple Silicon) distributable in the cloud, mirroring build-linux.yml. The runner is
# macos-14 — an arm64 image — because the build is arm64-only (Д7): Intel Macs get no artifact, and a
# macos-14 — an arm64 image — because the build is arm64-only: Intel Macs get no artifact, and a
# universal build would additionally need a universal native `drivelist`.
#
# The dmg is NOT signed with a Developer ID and NOT notarized (Д6 — no Apple Developer account), so it
# The dmg is NOT signed with a Developer ID and NOT notarized (no Apple Developer account), so it
# does NOT self-update: the macOS build reports the updater as unsupported and the user re-downloads by
# hand. See README (macOS section) for the Gatekeeper steps a first launch needs.
#
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Check

# Gate every pull request and every push to main on typecheck + lint + tests, on all three OSes. The
# build-*.yml workflows run the same steps, but only on a v* tag or by hand — so between releases nothing
# checked main, and rule violations (a bare `path.join` in a *.linux.ts module, which only fails on the
# Windows runner) reached main and sat there until the next release build went red.
#
# Windows is not optional here: it is the only runner where the `path.posix` rule (CLAUDE.md, platform
# layer) fails loudly.

on:
pull_request:
push:
branches:
- main

# A new push to the same PR supersedes the run in flight — three OSes × ~5 minutes is too much to spend
# on a commit nobody will look at.
concurrency:
group: check-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
# Pinned to windows-2022 for the same reason as build-windows.yml: windows-latest ships Visual
# Studio 18, which the bundled node-gyp cannot recognize, so the native drivelist build fails
# during `npm ci`.
- windows-2022
- macos-latest

steps:
- name: Checkout
uses: actions/checkout@v5

# Node 22.12+ is required by electron@43 and @electron/rebuild@4 (their `engines`). `cache: npm`
# keeps ~/.npm between runs.
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'npm'

# Deliberately WITHOUT --ignore-scripts: the postinstall (electron-builder install-app-deps) builds
# the native drivelist, and the game-config-* / game-move-transaction tests reach it through
# game-config.ts → drive-watcher.ts.
- name: Install dependencies
run: npm ci

- name: Type check
run: npm run typecheck

- name: Lint
run: npm run lint

- name: Unit tests
run: npm test

# typecheck proves the types; the esbuild bundle of the renderer and the tsc emit of main are what
# ships, and until now they were only built on a release tag.
- name: Build
run: npm run build
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ release/
# umu-launcher zipapp is fetched at build time (scripts/fetch-umu.mjs), never committed.
resources/umu/

# Build artifacts must never live next to their .ts sources in src/ (see audit C3).
# Build artifacts must never live next to their .ts sources in src/: a stale .js beside its .ts shadows
# the source for every tool that resolves extensionless imports.
src/**/*.js
src/**/*.js.map
103 changes: 85 additions & 18 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ mandate to rewrite what already works.

## Layers (do not blur)

- **main** owns all game logic (fs, registry, process control, FFI). **renderer** is stateless UI.
- **main** owns all game logic (fs, registry, process control, FFI). **renderer** holds no domain state:
UI state (a manifest draft, a menu stack, the boot sequence) lives inside the screen factories, and
everything about games, cards and settings is pushed from main via the `on*` subscriptions.
- They talk **only over IPC**. The renderer never touches fs/registry; main never touches the DOM.
- Preload bridges are typed and sandboxed (`contextIsolation: true`, `sandbox: true`).
- A **pure** function BOTH sides must compute identically (no fs/electron either way) lives in
Expand All @@ -30,6 +32,29 @@ mandate to rewrite what already works.
browser, so a `node:*` import there breaks the build, not just the convention. See
`src/shared/asset-move-names.ts` (move-to-card asset names, computed identically in main and renderer).

## Two state models: the session and per-game activities

- **`AppState` is the game session only** (`idle | ready | configuringProton | syncing-in | launching |
running | syncing-out | error`): at most one game is launched, played and synced at a time. Its locks
live in `GameSequences` (`game-sequences.ts`).
- **Everything that runs in the background is a per-game activity** (`shared/activity.ts`,
`ActivityMap` keyed by game id), held by `ActivityRegistry` (`activity-registry.ts`) and pushed in full
on `activity:update` (seeded by `activity:request`). Writers: `SteamActivityWatch`
(`steam-activity-watch.ts`, Steam downloads / updates / removals of every available Steam game) and
`GameJobs` (`game-jobs.ts`, copy installs, installer runs, uninstalls, prefix cleanups, with their own
queues and session rules in the pure `startVerdict`).
- **A game never has both.** A game with an activity is not launched; the session's game gets no
activity (Steam updating it before launch must not hide "Launching...").
- **Actions are addressed by game id** (`action:launch`, `action:uninstall`, `action:cancel-job`).
`GameActions` (`game-actions.ts`) resolves the target with the pure `resolveActionTarget`
(`action-target.ts`): with the session busy, another game may be installed or removed, never launched.
- **The renderer judges the game on screen by itself**: `screenActions` in `state-view.ts` decides Play,
Install, Uninstall, Cancel and Force close from the session, the browse model and that game's activity.
`#app[data-phase]` stays global; `data-busy`, `data-activity` and `data-play-locked` describe the game
on screen.
- **A managed exit goes through `QuitGate`** (`quit-gate.ts`): with jobs running, Quit / Shutdown / Reboot
ask first (`quit:confirm` / `quit:confirm-reply`, a native dialog when the window does not answer).

## Error-handling convention

Pick per situation, matching the existing patterns:
Expand All @@ -48,11 +73,11 @@ Pick per situation, matching the existing patterns:

## Adding a new service

Follow the **interface-DI** shape of `StatsService` / `UpdaterService` (dependencies passed via a
typed `…Deps` interface), not the bare-primitive-constructor or free-function styles that predate it.
Interface-DI is the most testable: it lets a unit test inject fakes without electron/fs. Bootstrap the
service in `main.ts`; wire IPC through `GameController` (or the service's own `init()`, the way
`GameConfigService` and `MetadataService` register their channels).
Follow the **interface-DI** shape of `UpdaterService` / `NotificationsService` / `MetadataService`
(dependencies passed via a typed `…Deps` interface), not the bare-primitive-constructor or free-function
styles that predate it. Interface-DI is the most testable: it lets a unit test inject fakes without
electron/fs. Bootstrap the service in `main.ts`; wire IPC through `GameController` (or the service's own
`init()`, the way `GameConfigService` and `MetadataService` register their channels).

## Adding a new IPC channel

Expand All @@ -63,7 +88,10 @@ The channel literal lives in **one** source of truth and is bridged with compile
3. Add the literal to `src/preload/preload.ts`'s `CHANNELS` map. It is `satisfies typeof IPC`, so a
wrong value, a typo'd key AND a forgotten channel are all compile errors — there is one window and one
preload, so the map has to be complete.
4. Wire the handler in `ipc.ts` (main) and consume it in the renderer.
4. Wire the handler in the owning service's `init()` (`settings-service.ts`, `game-config.ts`,
`file-picker-service.ts`, `metadata/service.ts`, … — `game-controller.ts` only for the card-session
channels) and consume it in the renderer. `test/ipc-channels.test.ts` also reads those `init()`s as
text and fails when a channel is registered twice: Electron refuses the second `handle` at runtime.

The `test/ipc-channels.test.ts` suite guards the same invariant from the outside (it reads the preload
sources as text) and would still catch it if a second window — and a second preload, back to
Expand Down Expand Up @@ -99,7 +127,8 @@ build does not self-update. **All OS-specific behaviour lives behind the `Platfo

- Add the capability to an interface in `platform/types.ts` (the bundle is `ProcessMonitor`,
`SteamLocator`, `SteamShortcuts`, `GameProcessLauncher`, `SavePathResolver`, `PowerBackend`,
`RemovableMounter`, `resolveInstallDir`).
`RemovableMounter`, `resolveInstallDir`). The file is types only and imports nothing from the
implementations — `GameProcess`, `PowerAction` and `InstallDirResolver` live there for that reason.
- Implement it in **all three** of `platform/win32.ts`, `platform/linux.ts` and `platform/darwin.ts`
(linux Proton helpers live in `platform/*.linux.ts` / `umu.ts`; the macOS ones in `platform/*.darwin.ts`).
`createPlatform(process.platform)` selects the bundle once at bootstrap in an explicit three-way branch
Expand Down Expand Up @@ -133,14 +162,19 @@ build does not self-update. **All OS-specific behaviour lives behind the `Platfo
it), a wrong separator does not fail loudly — it produces a wrong value.
Quick check before pushing:
`grep -rn "path\.\(join\|dirname\|basename\|resolve\)(" src/main/platform/*.{linux,darwin}.ts`
— and `test/platform-posix-paths.test.ts` is that grep with teeth (see "Mechanical guards").

## Tests

- Runner: **vitest** (`npm test`). Tests live in `test/`, run in plain Node with **no electron**
(`test/stubs/electron.ts` is aliased for the `electron` import — see `vitest.config.ts`).
- Testable = **pure / electron-free** modules. Modules that evaluate koffi FFI at import
(`game-launcher.ts`) are not importable in Node — extract pure logic into a util (as `launch-args.ts`
was) and test that.
(`test/stubs/electron.ts` is aliased for the `electron` import — see `vitest.config.ts`). `test/setup.ts`
points the file logger at a temp dir first, so a `log.warn` a test trips never lands in your real
launcher log (logger.ts would otherwise fall back to userData, as it does for the GUI).
- Testable = **pure / electron-free** modules. The koffi-bound win32 modules (`game-launcher.ts`,
`registry.ts`, `window-finder.ts`) DO import under vitest on every OS — the prebuilt addon loads and the
DLLs bind lazily — but their FFI branches cannot be exercised without Windows, so test them through
the pure logic split out beside them (as `launch-args.ts` was) and hand the process waits in through
a seam (`ProcessControl` in `test/game-controller.test.ts`).
- Prefer covering the risky, data-touching functions: manifest validation/anti-traversal, stats merge,
save-sync retry, argument quoting.
- **DOM tests of the renderer's screen controllers live in `test/renderer/**`** and run under
Expand All @@ -152,6 +186,9 @@ build does not self-update. **All OS-specific behaviour lives behind the `Platfo
picker / online picker surfaces), and the translator is the real `createTranslator('en')`. Input is the
`NavSurface` primitives called directly — no gamepad polling; the hover/veil branches are reachable
through `hoverOver()` (they all sit behind the `mouse-asleep` class the fixture starts with).
**The `mouse-asleep` class on `<html>` is a contract**: `idle.ts` is its only writer, every surface's
hover branch reads it with `classList.contains`, and the fixture starts with it on — do not replace it
with a per-screen `isAsleep()` dep.
Four rules that bite:
- **The screens that fetch their own data open ASYNCHRONOUSLY** — `filePicker.open()` awaits `listDir`,
`gameSettings.open(id)` awaits the manifest read, so assert after `await flushAsync()`. `SettingsScreen`
Expand All @@ -161,9 +198,14 @@ build does not self-update. **All OS-specific behaviour lives behind the `Platfo
- **Load the fixture per test** (`beforeEach`), and create the controller after it: no controller removes
its listeners, so a fixture shared across a file collects one live instance per test on the same nodes.
- **`app.ts` stays out** — it touches `window.api` at module scope.
Covered so far: `screen-sidebar`, `osk`, `file-picker`, `settings-screen`, `game-settings-screen`. Still
uncovered and next in line for the same base: `controls.ts`, `online-picker.ts`, `library-screen.ts`,
`carousel.ts`. Anything needing real layout (`scrollHeight`, canvas) is still a manual check on the Deck.
Covered so far: `screen-sidebar`, `osk`, `file-picker`, `settings-screen`, `game-settings-screen`,
`controls` (its seams live in `controls-deps.ts`; input is a `keydown` on `window`, since the six
primitives are not on its public surface), `hero` (with `computePalette` mocked — the canvas decode is
the one thing happy-dom cannot do — so a test can decide WHEN a palette lands relative to a swap).
`online-picker` (its stateless nodes and captions live in `online-picker-view.ts`; the artwork api is
answered by hand, so a test decides when a page lands relative to a filter or section change). Still
uncovered and next in line for the same base: `library-screen.ts`, `carousel.ts`. Anything needing real layout (`scrollHeight`,
canvas) is still a manual check on the Deck.
Upgrade note: `environmentMatchGlobs` is deprecated in vitest 3 and GONE in vitest 4 — an upgrade must
move `test/renderer/**` to `test.projects` (or a per-file `@vitest-environment` docblock) or the suites
will quietly run in Node again and fail on `document is not defined`.
Expand All @@ -174,13 +216,38 @@ build does not self-update. **All OS-specific behaviour lives behind the `Platfo
"fix" such a failure by rewriting the expectation with `path.join`: that makes the test assert whatever
the code does and stops testing anything at all.

## Tooling (all run in CI before build)
## Mechanical guards

Rules that live only in this file get broken between releases, so the ones that matter are enforced by
tests and lint, in the same source-as-text style as `test/daemon-imports.test.ts`:

- **`path.posix` in the platform layer** — `test/platform-posix-paths.test.ts` reads every module under
`src/main/platform/` except `win32.ts` / `*.win32.ts` and fails on a bare `path.join` / `dirname` /
`basename` / `resolve` / `relative` / `normalize` / `isAbsolute` / `path.sep`.
- **`process.platform` outside `platform/`** — ESLint `no-restricted-syntax` over `src/main/**`
(`eslint.config.mjs`), with an explicit allowlist and a reason per file (bootstrap / electron-UI glue,
electron-updater environment detection, the win32-only FFI modules' self-guards). It is an `error`: a
behavioural branch belongs on a `Platform` interface, implemented for all three OSes. A per-line
`eslint-disable` is not the way out: the repo has none.
- **File and factory size ratchet** — `test/file-size-ratchet.test.ts` keeps a baseline of raw `wc -l`
per `src/**` file over 1000 lines and per screen factory (`createControls`, `createGameSettingsScreen`,
…). Nothing may grow past its entry; a file that shrank by more than 50 lines must have its entry
lowered; a file not in the map may not exceed 1000. When you grow one, split it instead.

All three run on every PR and every push to `main` on Windows, Linux and macOS
(`.github/workflows/check.yml`), so a rule violation no longer waits for the next release build to surface.

## Tooling

The first three run in CI on every PR and push to `main` (`check.yml`, followed by `npm run build`) and
again before every release build. Prettier is deliberately NOT a gate.

- `npm run typecheck` — strict `tsc`, no `any`. Covers `test/` as well as `src/`.
- `npm run lint` — ESLint with type-aware rules (`no-non-null-assertion` — the "no `!`" rule, which
`tsc` cannot express — plus `no-floating-promises`, `no-misused-promises`,
`strict-boolean-expressions`), over `src` and `test`. Tests switch off `require-await` and
`unbound-method` (both only ever fire on test doubles) and allow a `_`-prefixed unused parameter.
- `npm test` — vitest.
- `npm run format` / `format:check` — Prettier (available for new code; the existing hand-aligned
files are intentionally not mass-reformatted).
- `npm run format` / `format:check` — Prettier, available for new code and not run in CI: the existing
hand-aligned files are intentionally not mass-reformatted, so `format:check` is red on most of the repo
by design. Format the file you are adding; do not reformat the one you are touching.
Loading
Loading