Invariants: Code structure¶
How the code is put together: background tasks, aggregates and their verbs, Units of Work and the seams they must not wrap, injected clocks, module size, service independence, import direction on both sides, and suppressions. How an entry reads and binds, and the other areas: the register's index.
-
create_task,ensure_futureandasyncio.Task(...)are called underbackend/only inlib/background_tasks.py, whoseBackgroundTasksholds each task until it ends —spawnfor a task nobody awaits (logged as a warning when it fails),start_awaitedfor one its starter awaits itself — checkAn owner with a place in
Application.shutdowncancels and awaits its tasks there; a taskgatherorshieldmakes from a coroutine its caller awaits on the spot is outside the rule, and so arehost/,_vendor/andnative/scripts/check_background_tasks.py. The event loop keeps only a weak reference to a task, so a task nothing else holds can be collected before it ends and its work is lost without a word; and the exception of a task nobody awaits reaches asyncio's own log only when the task is collected, or is dropped by agather(..., return_exceptions=True).BackgroundTasksholds every task it starts until the task ends.spawnis for a task nobody awaits: it logs one that ends with an exception as a warning under the name it was started with (a cancellation is not logged).start_awaitedis for a task its starter awaits itself and whose failure the starter handles there, so it logs nothing. An owner with a place inApplication.shutdowncancels and awaits what it still holds there; which owners have one, and what happens to the tasks of those that do not, is Backend Architecture's shutdown paragraph. A service that needs a handle on its task keeps the one the helper answers. A taskasyncio.gatherorasyncio.shieldmakes from a coroutine its caller awaits on the spot is the other kind, and outside the rule.backend/host/is outside it too: it uses the standard library only, and its call tasks end with their connection;_vendor/andnative/are copies. The check is an AST call-name scan with no pragma: it fails on a call namedcreate_taskorensure_futurein any shape, onasyncio.Task(...), and on a bareTask(...)in a module that importsTaskfromasyncio; what it does not see is listed in its docstring - Aggregate state mutated only via verb-named methods: outside them no code assigns, deletes orsetattrs an aggregate's field, or mutates one of its list, dict or set fields in place — check + prompt-only —scripts/check_aggregate_field_assignment.py, by field name: the fields the class body of every@cosmic_aggregateclass indomain/annotates, written on any receiver but bareselfanywhere inbackend/but_vendor/andnative/; a value that only shares a field name takes# pragma: no aggregate-check. An aggregate's verb-named methods are where it keeps its invariants, so a field written from outside them leaves a state no method would have produced, and the repository persists it as it finds it. The check cannot know what a receiver holds, so it matches by field name rather than by variable or type: a variable list misses every aggregate held under a new name, and reading each receiver's type from annotations would re-implement the type checker with less recall. Matching by name means no aggregate escapes for being held under another name, at the price of a pragma on a value that only shares a field name (e.g. the ctypes struct inadapters/gavel_native.py). Bareselfis exempt in every class, because an aggregate's own methods write through it and other classes' own fields share the names freely.tests/is not scanned: tests and fakes build state by writing fields on purpose. Unseen by it: a container field, or a value inside one, bound to another name, handed to a helper, or reached through a call,getattr,ororand((state.files or {})[k] = v); a container mutated through one of its dunder methods (state.files.__setitem__); asetattrwhose name is not a literal; a field the class body does not annotate, an aggregate outsidedomain/, and the in-place mutation of a container field typed by an alias or an abstract type; a store through bareselfin any class; a subclass of an aggregate, or an aggregate bound toself, writing through bareself;tests/andscripts/; a file it cannot read or parse, which it skips without a word. This list is the one home of what the check misses; the script's docstring and Database Design point here. Prompt-only: those shapes, and the repository's column writers, which are outside the check because they write a column in SQL and assign no field, and so are no field assignment to the check. Most of them are their column's only write path because the sync's upsert never touches that column (their docstrings inservices/protocols/repositories.py): the aggregate's verb sets the value and the writer persists that one column —RomRepository'sset_selected_disc,set_chosen_sgdb_id,set_artwork_checked_atandset_shortcut_appliedeach persist one column the aggregate's own verb has just set, andset_emulator_overridethe two (emulator_overrideandemulator_override_source)Rom.pin_emulator_overridehas just set, so the sync upsert never touches them. Two more change aggregate state with no verb:RomRepository.clear_all_shortcut_applied(Force Full Sync, the reset theshortcut_appliedentry (library sync) sanctions) andPlatformSyncStateRepository.revoke_skip(by design,domain/platform_sync_state.py's docstring). The writes the entry names as having no verb change the stored aggregate without its invariants being asked at all, which is why each is named, with what sanctions it where something does. Detail: Database Design → AST field-assignment check - No UoW-opening seam (ActiveCoreResolver, PlatformSystems, uow_factory) is called while a UoW is open on the same path — check —scripts/check_uow_seam_nesting.py, first seam family (the first of the two rules that script carries, over one shared matcher; the file-I/O rule below is a different hazard with its own seam list and its own failure message, and neither entry is evidence about the other). A seam reached as a bound method, through a helper, a local alias or a nesteddef/lambda, inside a UoW opened through a factory attribute not ending inuow_factory, a__call__-only seam, and a seam whose implementation later grows a UoW open slip past it - No file-I/O seam is called while a UoW is open — a Unit of Work wraps database reads and writes, never file or server I/O — check — (GLOSSARY.md → Unit of Work, ADR-0006).scripts/check_uow_seam_nesting.py, second seam family (IO_SEAM_METHODS). The list is the seams this checker can see and has been told about, never an inventory of the I/O seams that exist:DiscResolver.enumerate_discs/.resolve_for_install(a recursive walk of the ROM's install directory), the twoCoreInfoProviderreads —get_active_coreandget_emulator_options— which are answered by the vendored resolver's live read of ES-DE's catalogue (every call from the panel opens it again; only a run's one reading of the sources keeps a system's answer for the run, and the answer carries the resolver's lookup of each entry's emulator on the machine),StartCommandReader.start_command(the same catalogue read, plus the files the command reads in and the distribution's own start;StartHandover.hand_over, which writes the start's file, is unlisted, because its name is alsoArtworkSlotHandover.hand_over, a domain verb called inside a UoW),SystemSupportedExtensionsFn/SystemKnownFn(two more questions to the same catalogue, asked afresh from the panel and once per reading within a run),SteamConfigStore.read_shortcut_exesand.read_shortcut_launch_options(each parses Steam's wholeshortcuts.vdf, for the one-time shortcut relocation and for a launch-options confirm that Steam's reports did not settle. Listing them changes nothing at their call sites: each service reaches its reading throughrun_in_executoras a bound method, which is this checker's documented blind spot, so the entries are a statement of the rule rather than an enforcement of it. They are also not the store's only real I/O —grid_dir()is called fromservices/artwork/(seven sites),services/steamgrid/(service.pyand_slots.py),services/shortcut_removal.pyandservices/library/reporter.py,set_steam_input_config()fromservices/settings.py,services/shortcut_removal.pyandservices/library/reporter.py,write_shortcut_icon()fromservices/steamgrid/service.py, andensure_grid_file()fromservices/shortcut_artwork.py— those are unlisted, and their being unlisted is a gap, not a judgement),.read_shortcut_icons(the same whole-file parse, for the shortcut artwork job's worklist, called plainly, so its entry is enforced),.grid_file_names(lists Steam's grid directory to answer which artwork slots a shortcut has),.read_grid_file(reads one grid file whole, for the ARTWORK tab to hash a slot's image; reached as a bound method, so listed rather than enforced),.grid_file_stamps(stats the files filling the ARTWORK tab's slots, for each thumbnail's signature), the shortcut artwork job's reading of Steam's renderer memory before each batch (_renderer_above_ceiling, withLibraryService.renderer_above_ceilingbeside it: a/procread, and at or aboveGC_SKIP_BELOW_KBa garbage collection over Steam's debugger, bounded at about 5 s, and a second read),FirmwarePlatformResolver(reads what one system's emulators want WITH content verification: it opens each candidate in a declared folder and reads it the way the emulator does — 59-173 ms per system on the reference machine at emu-atlas 0.22) and its whole-machine siblingFirmwareResolver, the save answer —resolve_save_answerand the saves package's ownsave_answerwrapper, a full read of the machine per ROM (the first ask about a core also runs that core's probe) — the savestate question put to the same catalogue entry (resolve_savestate_location) and the seam's detection question (installation_detected, which detects the emulator sources afresh and reads the answering one's health), the two path resolvers —MigrationFileStore.realpath(one walk per stored RetroDECK-home marker, a directory that may sit on the SD card the marker is pending a migration away from) andResolvedPathFn(the same walk, but on both sides of a comparison, so a call site costs what the rows it checks cost, not what it checks them against) — and everyRetroDeckFoldersquestion:download_folder,rom_root,rom_folders,bios_folder,saves_rootandmove_roots, each a fresh reading of the emulator sources and a question to the resolver, which reads RetroDECK's settings file and ES-DE's, and arealpathof the answer — together with the services' own wrappers around them (_rom_folder,_rom_folders,_resolve_target,_platform_dir,_saves_root), which are what the services call; the save backup's two moves,SaveBackup.back_up_save_file(a creation and a listing of.romm-backup, a rename, and the removal of copies past its retention) and.back_up_claimed_save_file(a creation and a listing of that folder, and a rename), with_back_up_save, the adoption renamer's binding of the first; the save backup's check of that folder before a first move,SaveBackup.prepare_save_backup_folder(a creation and a listing of that folder); and every save-transfer step (fetch_server_copy,hash_server_copy,download_save_file,upload_save_file,acknowledge_server_head,adopt_server_head,record_server_head: a RomM request, a temporary file, or a measure of the local file). The firmware timing is the only entry a cost was measured for; every other one is listed from reading its implementation. One other real I/O seam was weighed and kept out — the reason is in the script's docstring, and it is not an exemption; nor is it an inventory of what else touches the disk. "It's only a read" is the reasoning this rule exists to refuse:SqliteUnitOfWork.__enter__issuesBEGIN IMMEDIATE, so even a read-only UoW takes the write lock. The database is in WAL, so readers are unaffected — but every other writer waits on the lock for up tobusy_timeout=5000and fails withSQLITE_BUSYif it is still held then, andFakeUnitOfWorkshares no connection, so no unit test notices. Six call sites had drifted across the rule before anything looked (#1779), for the reason the check exists: nothing at a call site reveals that an injected seam touches the disk. The rule and the gate come from reading code — no measurement of how long any of those transactions actually held the lock exists, and nothing here should be read as one. What the check sees is the deadlock rule's matcher unchanged — an attribute call naming a listed seam, lexically inside awith <...>uow_factory()block in the same function scope — so it inherits every blind spot of that half: a seam behind a helper one level down, an alias to a local, a factory attribute whose name does not end inuow_factory, a nesteddef/lambda(which resets the scope by design), a seam passed as a bound method (run_in_executor(None, self._disc_resolver.enumerate_discs, install)— an attribute, not a call, andrun_in_executoris exactly howdisc.pyandcores.pyreach their_iobodies; the same shapecheck_read_only_module.pyrecords for its own gate), and the hand-maintained list itself, which cannot notice a seam whose implementation grows a file read later. Matching only attribute calls is deliberate: the puredomain.disc_selection.enumerate_discsshares a name with the seam and does no I/O — it is safe because its call site imports it bare, not because of the name. The call-shaped blind spot is shared with the deadlock rule and only this family closes it: for each__call__-only seam the list carries the attribute it is bound to — by convention rather than by construction, and only while such a name means one thing, which is exactly what keeps_list_filesout. Which entries are call-shaped, and which of those also carry a twin under their implementation's own method name, is in the script's module docstring. The deadlock rule's own call-shaped seams stay open. One# pragma: no uow-checkcovers both families — it suppresses the line, and no seam is in both lists, so where a line does name two seams it silences both - Services never call clocks / sleep / uuid / random directly (inject the Protocol) — check —scripts/check_cosmic_call_bans.sh- No module inservices/,bootstrap/,adapters/,domain/,host/,lib/ormodels/crosses the ~1000-LOC decomposition threshold — check —scripts/check_module_size.py(no module is grandfathered any more: every one that predated the gate has dropped back under the threshold and left itsALLOWLIST, to which nothing is ever added.main.py,_vendor/,tests/,scripts/andfrontend/src/are out of scope, each for a reason recorded atSCOPE_DIRS) - Service-independence contract list stays complete — check —scripts/check_service_independence_contract.py- Layer import direction (services ↛ adapters, adapters ↛ services, …) — check —.importlinter(lint-imports) - Frontend direction:frontend/src/utils/andfrontend/src/api/never import either surface (frontend/src/bigpicture/,frontend/src/desktop/) orfrontend/src/shared/;shared/never imports either surface; the two surfaces never import each other — checkOnly
frontend/src/index.tsxand its test importfrontend/src/qam/orfrontend/src/boot/, and neither of those two imports the other;qam/never imports either surface;frontend/src/types/imports nothing else fromfrontend/src/; and nofrontend/src/module takes part in an import cyclefrontend/eslint.config.js(import-x/no-restricted-paths,import-x/no-cycle). The surface pair is a peer rule, not a layer rule: the two share data and logic and almost nothing visual, so anything that turns out to belong to both moves DOWN, never sideways — UI intoshared/, which both surfaces may import and which imports neither, and the rest intoapi/,utils/ortypes/.qam/, Tender's Quick Access entry, sits below the surfaces: it mounts the panel through the factoryindex.tsxhands it, and may importshared/like either surface — the panel's error boundary is there.boot/sits besideindex.tsxinstead, and may import a surface, because its start-up check has to see every Steam lookup the panel relies on, a surface's included —boot/steamModules.tsreadsAppDetailsRoutefrombigpicture/patches/installGamePagePatch.ts. Theqam/andboot/zones name every directory underfrontend/src/but their own, and every file directly in it butindex.tsxand its test, by glob, so a directory or a module added there is covered without a change to them. These rules go inert rather than loud when misconfigured: until the config names.ts/.tsxfor the plugin to read,no-cyclefinds no cycle among the frontend's modules (the comment atimport-x/extensionsinfrontend/eslint.config.jssays how).frontend/src/eslintBoundaries.test.tslints known-bad fixtures through the real config and fails if any of the rules stops reporting — a greenpnpm lintalone proves nothing. For the cycle rule a type-only import is not an edge (erased at runtime), which is why theapi/backend.ts⇄utils/cachedGameDetailStore.tsback-reference is not a cycle; a zone reports animport typelike any other import - No bare# type: ignore/ blanket suppressions — check —scripts/check_no_bare_ignores.sh