feat(magos-modificus): implement Phase 1 Profiles library - #17
Merged
Conversation
Replaces the Phase-0 stub with the minimal Profiles library: the data model (Profile, ProfileSummary, ModListEntry), IProfileService, and a filesystem-backed ProfileService. Each profile persists under <ProfilesBaseFolder>/<guid>/ as profile.json + a mods/ mod root; PrepareModRoot writes mods.lst (enabled mods in Order, UTF-8 no BOM, trailing newline) and returns the --mod-path. IProfileService is shaped for a Phase 2 storage swap (per-profile dirs -> shared-first + staging) without interface change: PrepareModRoot abstracts the --mod-path and no storage detail leaks through. Storage paths are private to ProfileService. Registered as a singleton via AddProfiles (stateless over the FS; MagosConfig is itself a singleton). No new NuGet deps — JSON uses the in-BCL System.Text.Json (the csproj only adds the config ProjectReference).
40 tests across CRUD round-trip, mod-list management, PrepareModRoot / mods.lst generation (enabled-only, Order, disabled omitted, empty file, UTF-8 no BOM, faithful duplicate handling, idempotency), first-run dir creation, and the unknown-id / missing-mod-dir edges. Tests resolve IProfileService through the real AddProfiles DI path against a per-test temp ProfilesBaseFolder (black-box against the interface). ProfileService.cs at ~98% line / 90% branch coverage; the remaining gap is a defensive IO-error catch that needs a filesystem mock to hit. Test deps mirror General.Tests (versions verified latest-stable on NuGet).
AGENTS.md, README.md, and magos-modificus/README.md still labelled the Profiles library a 'stub' after Phase 1 landed — stale per the repo's doc-currency rule. Soften those references to show Profiles is implemented; the other four libraries (integrations, steam, enginseer-client, launcher) remain 'stub' until their Phase 1 tracks land. No code changes.
…tate) ModListEntry.Enabled and .Order had public setters, so a consumer could mutate an entry returned from GetModList (silently non-persisting — the service always read-fresh/mutate-fresh/write). That contradicted the Profile.Mods doc claim of immutability. Convert ModListEntry to a sealed record with init-only properties; SetModEnabled/SetModOrder now rebuild the changed entries via 'with' expressions, and AddMod rebuilds via Append. RemoveMod already filtered (no entry mutation), unchanged. Tighten Profile.Mods + ModListEntry doc comments to state the now-true immutability. Record (vs class-with-init) keeps the rebuilds one-line as Phase 2 adds version/source fields.
…c RemoveMod doc
ListProfiles returned filesystem-enumeration order (unspecified). Sort
by Name with StringComparer.Ordinal (stable) so the UI profile picker
gets a predictable order; add a test asserting the sort (also pinning
Ordinal vs OrdinalIgnoreCase behavior).
Reword RemoveMod's doc to be storage-agnostic ('the mod's local files,
if any') instead of naming the per-profile mod root, so the doc holds
under Phase 2's shared-first storage (where the dir may be a shared
reference that must not be deleted). Signature unchanged.
AGENTS.md and magos-modificus/README.md listed only Magos.Modificus.General.Tests/ under tests/; add Magos.Modificus.Profiles.Tests/ to both, matching the existing listing style. No other library status touched.
ModifAmorphic
added a commit
that referenced
this pull request
Jul 1, 2026
## What
Phase 1 Steam library for Magos Modificus — discover everything needed
to launch Darktide modded (Steam install, Darktide install, compatdata,
Proton version) + an escape hatch (report missing pieces for the UI to
prompt) + game-running detection. Enginseer-client (Phase 1 capstone)
consumes the discoveries to set the Proton env vars + invoke the
launcher; the UI (Phase 3) consumes the discovery result + game-running
state. **Steam discovers; Enginseer-client acts.**
Spec: `_local/phase1-steam-spec.md` (approved). Contract:
`docs/architecture/MAGOS-MODIFICUS.md` (Launch → Linux).
## What's in it
- **`ISteamService.Discover() → DiscoveryResult`** +
**`IsGameRunning()`**. `DiscoveryResult` is a flat record of nullables
(`SteamInstallPath`, `DarktideGameBinaryPath`, `CompatdataPath`,
`ProtonBinaryPath`, `ProtonVersion`) + `Status`
(Complete/Partial/Failed) + `Warnings` — the null fields drive the
escape-hatch prompt.
- **Linux discovery:** Steam install (default `~/.local/share/Steam` →
Flatpak fallback), libraries via `libraryfolders.vdf`, Darktide install,
compatdata (`steamapps/compatdata/1361210/`), Proton (`Proton -
Experimental` → highest-versioned real Proton → `compatibilitytools.d/`
→ null/escape-hatch).
- **Windows discovery:** registry `HKCU\Software\Valve\Steam\SteamPath`
→ default; same VDF/Darktide search; no compatdata/Proton (native).
- **Testability seams:** `SteamDiscoveryOptions` (injectable roots +
`DiscoveryPlatform`), `ISteamRegistryReader`, `IProcessLookup` — Windows
logic runs on Linux CI; game-running is mockable.
- **Arch-doc fix folded in:** Launch → Linux now states Magos sets
**both** `STEAM_COMPAT_DATA_PATH` and `STEAM_COMPAT_CLIENT_INSTALL_PATH`
(the live-validated finding — the working invocation set both).
- **38 tests** (synthetic-layout discovery, VDF parsing, Proton
selection, Flatpak, Windows-via-abstraction, game-running, escape-hatch
fidelity).
## Notable bug caught during impl
The coder caught a subtle real bug: a literal "highest-versioned `Proton
X.Y` in `steamapps/common`" would parse the **Darktide game dir itself**
(`Warhammer 40,000 DARKTIDE`) as "Proton 40.0" and pick it over every
real Proton. Fixed by requiring an actual `proton` script in the dir
(the defining trait of a Proton install) + a regression test
(`Darktide_game_dir_is_not_mistaken_for_a_proton_build`). qa
independently verified the bug + the fix.
## Verification trail
- **coder** — implemented to spec; 87 tests pass (38 Steam + 49
existing); ~90%+ coverage on the testable surface; **no new NuGet deps**
(initially added `Microsoft.Win32.Registry`, removed it as
NU1510-redundant — the type is framework-provided on net10.0,
`OperatingSystem.IsWindows()`-gated, no-ops on Linux).
- **qa → PASS** — all 8 acceptance criteria met; all 4 deviations sound;
**independently verified the Darktide-Proton bug is real + the fix
covers it**; no `Microsoft.Win32.Registry` dep; all package pins
latest-stable.
- **code-review → APPROVE WITH NITS (merge as-is)** — confirmed the
Proton-script gate robust against other `steamapps/common` tooling, the
version parser correct (hand-traced), the VDF parser sound, the
escape-hatch honest, the Windows registry path sound, the testability
seams clean. All findings are optional nits; reviewer recommends merging
as-is + tracking follow-ups.
- **CI** — gates Win + Linux on this PR; Linux build/test green locally
(87/87).
## Tracked follow-ups (not blockers)
- **`IsGameRunning` Linux/Proton accuracy** —
`Process.GetProcessesByName("Darktide")` may false-negative under
Proton; **fix before Phase 3 UI consumes it** (a false "not running"
could permit a double-launch). Informational only for Phase 1.
- Legacy pre-2019 `LibraryFolders` VDF format not parsed (graceful
fallback; doc note warranted).
- Windows library dedup is `Ordinal` (registry lowercase/forward-slash
vs VDF backslash) → Steam root double-counted (harmless redundant probe
+ misleading count).
- `Failed` status carries empty `Warnings` (diagnostic in log only) —
Phase 3 UI would want a human-readable warning.
- Flatpak + `compatibilitytools.d` edge (escape-hatch-covered; proper
fix = discover Flatpak's own compat-tools dir).
## Notes
- Builds on merged #16 (scaffold) + #17 (Profiles). Steam is independent
of Profiles (parallel Phase 1 track).
- Enginseer-client (next track) consumes: `SteamInstallPath` +
`CompatdataPath` → env vars; `ProtonBinaryPath` → `proton run`;
`DarktideGameBinaryPath` → `--game-binary` (Z:\-translated).
ModifAmorphic
added a commit
that referenced
this pull request
Jul 2, 2026
…de (#20) ## What Phase 1 Enginseer-client — the launch façade (the **Phase 1 capstone**). `IEnginseerLaunchService.Launch(profileId)` resolves the profile + discovery internally, then invokes `magos_launcher.exe`: **Windows directly**, **Linux via `proton run`** with both `STEAM_COMPAT_*` env vars + `Z:\`-translated paths. Returns `LaunchResult` (`Launched` / `DiscoveryIncomplete` + missing fields / `Error`). Consumes Profiles (`PrepareModRoot` → `--mod-path`) + Steam (`DiscoveryResult` → env vars + proton + game-binary). **The launch smoke test is a USER-machine validation** (no Darktide/Windows/Proton in CI). The PR provides a **CLI smoke harness** (`dotnet run -- discover/list/launch`) for the user to run the real launch on their Win + Linux boxes — the underlying path was already live-validated manually. Spec: `_local/phase1-enginseer-client-spec.md` (approved). ## What's in it - **`IEnginseerLaunchService.Launch(Guid profileId) → LaunchResult`** — resolves profile (`IProfileService.PrepareModRoot`) + discovery (`ISteamService.Discover`) internally; never throws. - **Windows path:** `Process.Start(launcher, args)` directly — no Proton, no translation. Args: `--game-binary`, `--mod-path` (from `PrepareModRoot`), `--log-file`. - **Linux path:** sets `STEAM_COMPAT_DATA_PATH` + `STEAM_COMPAT_CLIENT_INSTALL_PATH` from `DiscoveryResult`; `Z:\`-translates `--game-binary` + `--mod-path` + `--log-file`; invokes `<ProtonBinaryPath> run <launcher.exe> <args>`. - **`DiscoveryIncomplete`:** derived from the null required fields (≡ Steam's `Status != Complete` by construction — verified equivalent per-platform) + returned with the missing field names (`nameof`-based) for the Phase 3 UI's escape-hatch prompt. - **`IProcessLauncher` seam** — mockable; real `ProcessLauncher` uses `ProcessStartInfo.ArgumentList` (argv-correct, no shell — paths-with-spaces safe, no injection surface). - **CLI smoke harness** — dual-purpose test project (`dotnet test` = xUnit; `dotnet run -- discover/list/launch` = real-composition harness); README + instructions embedded. - **28 tests** (Windows/Linux arg assembly, `Z:\` translation, both env vars, `proton run` invocation, `DiscoveryIncomplete` per-platform, profile integration, error cases, DI). ## Verification trail - **coder** — implemented to spec (resumed cleanly after a session-stall abort); 144 tests pass (28 EnginseerClient + 116 existing); no new NuGet deps; all pins latest-stable. Caught + the lead folded two real fixes (below). - **qa → PASS** — all 10 acceptance criteria met; all 5 deviations sound; **the smoke harness executes** (`dotnet run -- discover` → real composition → DiscoveryIncomplete/exit 2 in this env, correct); `DiscoveryIncomplete`↔`Status` equivalence verified; no new deps. - **code-review → REQUEST CHANGES on one should-fix (now fixed):** the `--log-level` vocabulary mismatch — Magos forwarded its Serilog level name to the shell, but the shell only recognizes `error`/`warn`/`info`/`debug`/`trace` → `Warning` silently became `info` (more noise), etc. **Fixed by omitting `--log-level` entirely** (rely on the shell's `info` default; decouple the two logs — they serve different purposes; consistent with the `--steam-app-id` deviation). The reviewer verified everything else sound (the `Status` equivalence, `Z:\`, `proton run`, both env vars, the `IProcessLauncher` seam, the smoke-harness design). Fixing this was the condition to flip to APPROVE. - **CI** — gates Win + Linux on this PR; Linux build/test green locally (144/144). ## Tracked follow-ups (not blockers) - `#2` dead `DarktideSteamAppId` constant — documented placeholder for a future `--steam-app-id` config-override field. - `#3` generic `Error` message on process-start failure — the specific exception lands in the log, not `LaunchResult.Message`. Thread it out when Phase 3 UI consumes `Error` + wants to show why. - `--steam-app-id` + a dedicated shell-log-level config field — add if/when a knob is needed (Phase 1 relies on the launcher's defaults). ## After this merges Phase 1 (core domain libraries) is **complete** — Profiles + Steam + Integrations + Enginseer-client all in main. → **Phase 2: shared-mod storage** (the version-policy allocation model + staging). The `IEnginseerLaunchService` interface is designed so Phase 3 (UI) consumes `Launch` + handles `DiscoveryIncomplete` (escape-hatch prompt) cleanly, with a future `Launch(profileId, DiscoveryResult)` overload for cached discovery. Builds on merged #16 (scaffold) + #17 (Profiles) + #18 (Steam) + #19 (Integrations).
ModifAmorphic
added a commit
that referenced
this pull request
Jul 2, 2026
## What
Phase 2 shared-mod storage — the shared-first allocation + symlink
staging model from the arch doc. A profile uses the global shared copy
of a mod when version policies are compatible; a profile-local
`diverged/` copy when they diverge. `PrepareModRoot` now **stages**
(symlinks shared/diverged mod dirs into a `staged/` dir + writes
`mods.lst`), replacing Phase 1's per-profile `mods/` dir. **Symlinks,
not copies** (download once, store once — copying defeats the purpose).
Built on Profiles (Phase 1); `IProfileService` contract preserved
(signature unchanged; impl swapped).
Spec: `_local/phase2-shared-mod-storage-spec.md` (approved). Phase plan:
`_local/v1-phase-plan.md`.
## What's in it
- **New `Magos.Modificus.SharedMods` project** — `ModVersionPolicy`
(`Pinned(version)` / `Latest`, JSON-polymorphic via `$kind`),
`AllocationResolver` (the 4-case share/diverge rule, by policy intent
not current version), `SharedModEntry`, `ISharedModStore`
(manifest-backed list/get/add-upsert/remove).
- **`IProfileService` evolution** — `ModListEntry.Policy` added
(additive, default Latest); `AddMod(.., policy)` + `SetModPolicy(..)`;
`PrepareModRoot` signature unchanged (impl → symlink staging).
- **Staging** — for each enabled mod, resolve share/diverge; symlink
shared → `<SharedModsFolder>/<mod>` or diverged →
`<profile>/diverged/<mod>` into `<profile>/staged/`; write `mods.lst`
from successfully-staged mods; regenerate each launch. Missing
`diverged/` copy (Phase 4 pending) → skip + warn (no crash).
- **Divergence transitions** — `SetModPolicy` share→diverge (metadata;
Phase 4 acquires the local copy), diverge→share (drops `diverged/`).
- **On-disk layout:** `<SharedModsFolder>/{shared-manifest.json,
<mod>/}` + `<ProfilesBaseFolder>/<guid>/{profile.json, diverged/<mod>/,
staged/<symlinks + mods.lst>}`.
- **185 tests** (22 new SharedMods + 59 Profiles [updated + new
StagingTests] + 104 existing).
## Verification trail
- **coder** — implemented to spec; caught + handled a real **data-safety
risk**: clearing `staged/` for regeneration could, with a naive
`Directory.Delete(recursive)`, follow a directory symlink into the
shared store + nuke shared files. Fixed with symlink-aware cleanup
(deletes the link, never follows it) + a dedicated test. 185 tests, no
new deps.
- **qa → PASS** — all 8 acceptance criteria met; all 6 deviations sound;
**the data-safety claim rigorously verified** (`ClearStagedDir` checks
`ReparsePoint` before `Directory`; the test exercises a real dir-symlink
into the shared store + asserts shared files survive); allocation
resolution (4 cases + policy-intent); symlink staging (no copies);
divergence transitions; manifest persistence.
- **code-review → APPROVE WITH NITS** — **the data-safety invariant
independently verified** (the reviewer wrote a throwaway .NET 10 probe:
dir-symlink → `File.Delete` → target survived; dangling symlinks handled
via lstat semantics); `PrepareModRoot` signature byte-identical; Phase 1
backward-compat confirmed. Should-fix (Phase 4 `Version` equality quirk)
+ one nit folded in (see below).
- **CI** — gates Win + Linux on this PR; Linux build/test green locally
(185/185).
## Folded in from review
- **`// TODO(phase4)`** near the pinned-version comparison in
`AllocationResolver` — `Version` equality is component-count-sensitive
(`Version(1,0) != Version(1,0,0)`); dormant in Phase 2 but a Phase 4
trap when acquisition parses user pins / release tags. Marker so the
seam isn't lost.
- **`TryAddSingleton` for `ISharedModStore`** — consistency with the
`SymlinkCreator` seam; lets Phase 3 UI tests mock the store without
`AddProfiles()` clobbering it.
## Tracked forward-looking nits (not blockers; Phase 3/4)
- `ClearStagedDir` external-tamper defense-in-depth (assert `staged`
itself isn't a symlink before enumerating — only reachable via external
tampering, not the service's operations).
- `$kind` forward-compat: an unknown future policy variant deserialized
by an older build throws (SharedModStore catches it → empty;
ProfileService.ReadProfileFile doesn't → that profile unreadable on
older builds). Acceptable for single-dev rollout; revisit if
multi-version coexistence matters.
- `diverge→share` drops `diverged/` unconditionally (by spec) — Phase 3
UI should confirm when a populated `diverged/` copy is about to be
dropped.
## After this merges
Phase 2 complete → **Phase 3 (UI build-out)**: profile management UI,
mod-list UI (surfacing the policy model), Launch button wired to
Enginseer-client. This is when the deferred launch smoke test (create a
profile → launch) becomes runnable + when the app becomes user-usable.
Builds on merged #16 (scaffold) + #17–#21 (Phase 1).
This was referenced Jul 8, 2026
ModifAmorphic
added a commit
that referenced
this pull request
Jul 8, 2026
🤖 I have created a release *beep* *boop* --- ## 0.1.0 (2026-07-08) ### Features * **component-a:** Hybrid Rust+C discovery + shell + launcher ([#1](#1)) ([491e5d1](491e5d1)) * **magos-modificus:** implement Phase 1 Enginseer-client launch façade ([#20](#20)) ([7950ed1](7950ed1)) * **magos-modificus:** implement Phase 1 Integrations (GitHub Releases client) ([#19](#19)) ([781d65c](781d65c)) * **magos-modificus:** implement Phase 1 Profiles library ([#17](#17)) ([f355ceb](f355ceb)) * **magos-modificus:** implement Phase 1 Steam discovery library ([#18](#18)) ([8f6ec00](8f6ec00)) * **magos-modificus:** implement Phase 2 shared-first mod storage ([#22](#22)) ([cf2af80](cf2af80)) * **magos-modificus:** Phase 3 Track B mod-list, import, source model ([#29](#29)) ([5075cee](5075cee)) * **magos-modificus:** Phase 3 Track C launch + Settings + escape-hatch + base-folder mod loading ([#32](#32)) ([c595700](c595700)) * **magos-modificus:** Phase 4 Stage 1 nxm scheme handler + IPC ([#34](#34)) ([0017529](0017529)) * **magos-modificus:** Phase 4 Stage 2 Nexus auth + Integrations dialog ([#35](#35)) ([0790a8e](0790a8e)) * **magos-modificus:** Phase 4 Stage 3 Nexus mod acquisition ([#36](#36)) ([d01105f](d01105f)) * **magos-modificus:** Phase 4 Stage 4 Nexus update-check service ([#39](#39)) ([1749e76](1749e76)) * **magos-modificus:** Phase 4 Stage 5 mod-list update badges + per-mod update ([#43](#43)) ([423e146](423e146)) * **magos-modificus:** Phase 4 Stage 6 DMF new-profile/auth prompt ([#44](#44)) ([994b4f8](994b4f8)) * **magos-modificus:** scaffold .NET 10 + Avalonia 12 app + libraries ([#16](#16)) ([d1fac91](d1fac91)) * **mod-loader:** own the load-order contract (mods.lst), drop DMF prepend ([#14](#14)) ([1ccb891](1ccb891)) * **release:** add Curator release pipeline ([#49](#49)) ([01517e4](01517e4)) * **runtime:** engine-context proven — trampoline, Enginseer v1, launcher fail-fast ([#4](#4)) ([4565ba8](4565ba8)) * **runtime:** Enginseer v2 — mod loader + launcher config + logging ([#5](#5)) ([1e65b3f](1e65b3f)) * **runtime:** package Enginseer with the runtime; relocate build files to runtime/ ([#6](#6)) ([7221bdb](7221bdb)) * **ui:** Phase 3 Track A — app shell + profile management ([#27](#27)) ([f7f8250](f7f8250)) * **ui:** Phase 3 Track D — Preferences + i18n + custom title bars + icon ([#28](#28)) ([c921650](c921650)) ### Bug Fixes * **enginseer:** DMF integration fixes — IO re-root, load timing, destroy ([#7](#7)) ([401759c](401759c)) * **magos-modificus:** multi-format archive import (zip + 7z + rar) ([#41](#41)) ([ee4f5c6](ee4f5c6)) * **magos-modificus:** search all Steam libraries for the compatdata prefix ([#21](#21)) ([895fa2b](895fa2b)) * **steam:** detect running Darktide via /proc argv[0] under Proton ([#23](#23)) ([c5f38c4](c5f38c4)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: ModifAmorphic <86930443+ModifAmorphic@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Phase 1 Profiles library for Magos Modificus — the profile data model, persistence,
mods.lstgeneration, and config integration. The foundation Enginseer-client (Phase 1's capstone) and the UI (Phase 3) build on. Minimal scope: no shared-mod storage, no version policy, no auto-sort, no dependency resolution, no mod download (those are Phase 2 / Integrations / later).Spec:
docs/architecture/MAGOS-MODIFICUS.md→ Profiles + themods.lstcontract indocs/architecture/MOD_LOADER-DMF.md.What's in it
Profile(Id/Name/CreatedAt/Mods),ProfileSummary(Id/Name),ModListEntry(Name/Enabled/Order — immutable record, init-only).IProfileService: CRUD (create/get/list/rename/delete), mod-list management (GetModList/SetModOrder/SetModEnabled/AddMod/RemoveMod), andPrepareModRoot(id) → string(the--mod-pathseam — ensures the mod root + writesmods.lst).<ProfilesBaseFolder>/<guid>/{profile.json, mods/}; auto-creates dirs on first run (closes the Phase-0 gap).mods.lst: enabled mods inOrder, one per line, UTF-8 without BOM (correctness-critical — a BOM prefixes the first mod name in the Lua loader), trailing newline; empty/all-disabled → 0-byte file; faithful to stored order (no auto-sort, no DMF-first enforcement — that's a higher layer).AddProfiles()DI path; persistence proven across service instances).Verification trail
ProfileService; no new NuGet deps (in-boxSystem.Text.Json); versions all latest-stable.mods.lstno-BOM independently byte-level-verified + the rationale confirmed against the Lua loader code path (file.lua%strim doesn't strip a BOM → it would prefix the first mod name); version pins latest-stable (coverlet 10.0.1 — Phase-0 miss resolved).mods.lstconsumption path (confirmed contract faithfulness); confirmed the Phase 2 storage-swap seam holds (no path/shared-vs-local leakage through the interface or data types); all deviations sound. All nits folded in:ModListEntry→ immutable record (was a mutable class with a misleading immutability doc claim);ListProfilessorted by Name; storage-agnosticRemoveModdoc;tests/doc listing updated.Phase 2 storage-swap seam (by design)
IProfileServiceis shaped so Phase 2 swaps storage (per-profile dirs → shared-first + staging) without changing the interface:PrepareModRoot(id) → stringabstracts "give me the--mod-path"; all storage paths stay private toProfileService;ModListEntrywill grow fields (version policy, source) but stays immutable. Phase 2 watch-items already flagged for that rewrite: atomicprofile.jsonwrites, concurrency, mod-name case-canonicalization for the Windows FS.Notes
mods.lstload-order contract (feat(mod-loader): own the load-order contract (mods.lst), drop DMF prepend #14).