feat(mod-loader): own the load-order contract (mods.lst), drop DMF prepend - #14
Merged
Conversation
…epend
Decouple Enginseer's mod loader from DMF: it now reads its own load-order file (mods.lst, authored by Magos Modificus) instead of the community mod_load_order.txt, and no longer injects DMF as the first entry. The order file is authoritative for both what loads and the order; the loader is framework-agnostic. Magos lists dmf first so DMF's init() still defines new_mod/get_mod before user mods run.
mod_manager.lua: read mods.lst (Mods.file.read_content_to_table("mods", "lst")); delete the table.insert(order, 1, "dmf") prepend; drop the now-dead enabled field from the _mods entry shape (DMF reads only id/name/handle). Missing/empty mods.lst -> empty _mods -> nothing loads (graceful via the existing or {}).
Tests rewritten to the new contract: the loader loads exactly the listed mods in order (no injection); tests needing DMF list it explicitly; tests using order={} for a DMF-only bootstrap now expect nothing to load. Added a dedicated missing/empty mods.lst -> load-nothing test. Header/source comments in init/lifecycle/file updated.
97/97 mod-loader Lua tests pass (was 96; +1 new).
Bring every in-scope doc in line with the Enginseer load-order contract change: the loader reads mods.lst (authored by Magos Modificus) authoritatively and injects nothing — DMF is first because Magos lists it first, not because the loader prepends it. Missing/empty mods.lst means load nothing. Updated: docs/architecture/MOD_LOADER-DMF.md (core model, two-roots, SCAN description, DMF load sequence, deferred-bootstrap step), docs/architecture/ENGINSEER.md (loader bullet, staging-dirs prose, env-var table, out-of-scope), enginseer/README.md (two-roots mod-root contents), README.md (where-mods-go layout + prose), AGENTS.md (directory-structure commentary). Out of scope and intentionally untouched: C shell comments (trampoline.h, dllmain.c) and docs/poc + docs/reference (frozen / community-framework reference) — flagged for the lead.
Comment-only: three shell comments describing the mod root contents still referenced mod_load_order after the Enginseer load-order contract rename. Updated to mods.lst so the comments match the code in this PR. No logic changed.
The 'missing/empty mods.lst' test only drove the missing-file path (read_content_to_table returns false). Split into two honest cases: missing file (returns false) and empty file (file exists, returns {}). Both collapse through the same `or {}` line into an empty order, so both yield empty _mods, no load attempted, _state="done". Reviewer noted this isn't a real coverage gap (same code path), but the test now matches its titles.
Collapse the triple space after 'flag).' on the staging-dirs line to a single space (artifact introduced by the earlier load-order-contract edit).
ModifAmorphic
added a commit
that referenced
this pull request
Jun 29, 2026
## What Adds `docs/architecture/MAGOS-MODIFICUS.md` — the canonical target architecture for the upcoming Magos Modificus (the mod-manager app) build, plus a small repo-wide cleanup of stale `mod-manager/` → `magos-modificus/` references. Doc-only; no code. ## Why Magos Modificus is the second of the project's two components, sitting on top of the Enginseer runtime (built). This doc is the source of truth the build decomposes from — it locks the architecture decisions captured during the kickoff, so the implementation work has a stable contract to build against. Builds on the merged #14 (the `mods.lst` load-order contract). ## Decisions locked in this doc - **Tech:** C# / .NET 10 (LTS) + Avalonia 11; `Microsoft.Extensions.*` DI + logging; SOLID/DRY; libraries namespaced by domain. - **Layout:** flat project folders under `magos-modificus/` (`ui/`, the domain libraries, `launcher/`, `config/`). - **Enginseer contract consumed:** `magos_launcher.exe` subprocess (flag > env > default); Magos authors the mod root + `mods.lst` (regenerated per launch; enable/disable by omission); fire-and-forget launch in v1 (no live control — awaits a future Enginseer IPC contract). - **Profiles + DMF:** DMF is a normal, optional mod (not required) with exactly two exceptions — a new-profile "add latest DMF?" prompt (sourced from GitHub Releases so it needs no Nexus key), and no loader-side auto-place (Magos writes it first via dependency resolution). - **Mod sources:** Nexus Mods primary; GitHub Releases + local as alternatives. - **Shared mod storage (in v1):** shared-first allocation, diverge-when-needed, resolved by version-policy intent (pinned vs auto-update) — not after-the-fact dedup. Built in from v1 so the storage model is uniform. - **Launch:** Windows trivial; **Linux = native Magos + Proton-at-launch** — Magos owns Steam/compatdata/Proton discovery + escape hatch (fail-fast with a UI prompt) + sets `STEAM_COMPAT_DATA_PATH` and invokes Proton; **Enginseer is unchanged on Linux** (no Linux helper, no new flag). - **v1 scope cut:** in/out enumerated (out = Enginseer live-control, dependency-view, conflict detection). ## Files - `docs/architecture/MAGOS-MODIFICUS.md` (new, ~270 lines). - `docs/architecture/README.md` — index: links the new doc; fixed `mod-manager/` → `magos-modificus/`. - `AGENTS.md`, root `README.md`, `magos-modificus/README.md` — stale `mod-manager` → `magos-modificus` references cleaned up. ## Notes - Doc-only — no build/test impact. - 6 commits, rebased on current `main` (the #14 merge). - For review: the load-bearing sections are **Shared mod storage** (the shared-first model + 4-case resolution table), **Launch → Linux** (the discovery + `STEAM_COMPAT_DATA_PATH` design), and **v1 scope**.
ModifAmorphic
added a commit
that referenced
this pull request
Jun 29, 2026
…#16) ## What Phase 0 of the Magos Modificus build — the .NET 10 + Avalonia 12 foundation all later phases build on. Project structure + DI + structured logging + a global config schema/loader + a bare UI window + CI. Library projects are **stubs** (interfaces + DI registration only); real implementations come in Phases 1+. Builds on the merged architecture doc (#15) and the Enginseer load-order contract (#14, also merged). ## What's in it - **Solution + 8 projects** under `magos-modificus/` (flat layout, per the arch doc): `ui/` (Avalonia 12 exe + DI composition root), `general/` (logging bootstrap, config loader, DI extensions), `config/` (`MagosConfig` schema + defaults), and stub libraries `profiles/`, `integrations/`, `steam/`, `enginseer-client/`, `launcher/`. Plus an xUnit test project. - **DI** (`Microsoft.Extensions.DependencyInjection`): each library exposes `Add<Library>()`; the composition root wires them all; resolution is verified at startup (5 domain services resolve — real, not vacuous). - **Structured logging** (`Microsoft.Extensions.Logging` + Serilog console + file), honoring the config's level + log file. - **Global config** (`MagosConfig`): Logging (level/file), ProfilesBaseFolder, SharedModsFolder, EnginseerRuntimeDir — platform-appropriate defaults; loaded from JSON; first-run-safe (parent-dir guard + regression test). - **CI** (`.github/workflows/magos-build.yml`): Win + Linux matrix, path-filtered to `magos-modificus/**`; existing mingw/msvc (Enginseer) workflows untouched. - **Docs**: dev-facing `magos-modificus/README.md` + `AGENTS.md` directory-structure update + arch-doc status synced. ## Verification trail - **coder** bootstrapped .NET 10 locally (so every version resolved via NuGet, not memory); build 0/0, 7/7 tests, headless Xvfb run shows config-load + DI-wired + log-file-written; caught + fixed a real first-run `DirectoryNotFoundException` during the smoke run. - **qa → PASS** — all 10 acceptance criteria met with file:line evidence; version spot-check (Avalonia 12.0.5, CommunityToolkit.Mvvm 8.4.2, Serilog 4.3.1, xunit 2.9.3, M.E.* 10.0.9 — all latest stable); Avalonia.Diagnostics omission verified sound (its 11.3.18 targets 11.x → would mismatch 12.x core). - **code-review → APPROVE WITH NITS** — caught one stale pin qa missed (`coverlet.collector` 6.0.4 → bumped to 10.0.1), plus the arch-doc status staleness and trivial cleanups; all folded into this PR. - **CI**: run #28344634825 green on Win + Linux; latest push (review fixes) running as #28345590519 (csproj version bump + field/element removal + doc line — zero-risk against the green build). ## One open decision for the reviewer The `enginseer-client` library's namespace is **`Magos.EnginseerClient`**; all seven sibling libraries are `Magos.Modificus.*`. Code review flagged the inconsistency and suggested `Magos.Modificus.EnginseerClient` for consistency. It's defensible either way (the distinct namespace reads as "Magos's client for Enginseer"; the folder `enginseer-client/` already diverges from siblings). Left as-is for a deliberate ruling — trivial to rename if preferred. ## Known follow-ups (acceptable for Phase 0; later phases) - Avalonia.Diagnostics — re-add when a 12.x version ships (currently 11.x lags core). - Malformed-JSON config test; auto-create of profile/shared dirs; log rotation; `--warnaserror` / coverage gate in CI; stub-`Add<Library>()` registration unit tests.
ModifAmorphic
added a commit
that referenced
this pull request
Jul 1, 2026
## What
Phase 1 Profiles library for Magos Modificus — the profile data model,
persistence, `mods.lst` generation, 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 + the `mods.lst`
contract in `docs/architecture/MOD_LOADER-DMF.md`.
## What's in it
- **Data model:** `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), and
`PrepareModRoot(id) → string` (the `--mod-path` seam — ensures the mod
root + writes `mods.lst`).
- **Persistence:** `<ProfilesBaseFolder>/<guid>/{profile.json, mods/}`;
auto-creates dirs on first run (closes the Phase-0 gap).
- **`mods.lst`:** enabled mods in `Order`, 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).
- **41 tests** (black-box, via the `AddProfiles()` DI path; persistence
proven across service instances).
## Verification trail
- **coder** — implemented to spec; 49 tests pass (41 Profiles + 8
General); ~98% line coverage on `ProfileService`; no new NuGet deps
(in-box `System.Text.Json`); versions all latest-stable.
- **qa → PASS** — all 7 acceptance criteria met; all 7 deviations sound
+ tested; **`mods.lst` no-BOM independently byte-level-verified + the
rationale confirmed against the Lua loader code path** (`file.lua` `%s`
trim doesn't strip a BOM → it would prefix the first mod name); version
pins latest-stable (coverlet 10.0.1 — Phase-0 miss resolved).
- **code-review → APPROVE WITH NITS** — independently traced the
loader's `mods.lst` consumption 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); `ListProfiles` sorted by
Name; storage-agnostic `RemoveMod` doc; `tests/` doc listing updated.
- **CI** — gates Win + Linux on this PR (the workflow is PR-gated
against main); Linux build/test green locally (49/49).
## Phase 2 storage-swap seam (by design)
`IProfileService` is shaped so Phase 2 swaps storage (per-profile dirs →
shared-first + staging) **without changing the interface**:
`PrepareModRoot(id) → string` abstracts "give me the `--mod-path`"; all
storage paths stay private to `ProfileService`; `ModListEntry` will grow
fields (version policy, source) but stays immutable. Phase 2 watch-items
already flagged for that rewrite: atomic `profile.json` writes,
concurrency, mod-name case-canonicalization for the Windows FS.
## Notes
- Doc-only label updates included (README/AGENTS reflect Profiles
implemented; other libraries remain stub until their Phase 1 tracks
land).
- Builds on the merged Phase 0 scaffold (#16) + the `mods.lst`
load-order contract (#14).
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
Decouples Enginseer's mod loader from DMF by giving Enginseer its own load-order file. The loader becomes framework-agnostic: it loads exactly what's listed, in order.
Why
Historically
mod_load_order.txtwas consumed by DML (the community Darktide-Mod-Loader); Enginseer'smod_loader/replaced DML. DMF never read it — only the loader did. The loader nonetheless carried one DMF-specific assumption: it auto-prependeddmfas the first entry (table.insert(order, 1, "dmf")).This PR removes that coupling. The file is renamed
mod_load_order.txt→mods.lst(authored by the future Magos Modificus app; Enginseer's loader is the sole consumer). Magos writes the complete order — DMF included, first — and the loader loads what's listed, no injection. DMF-first becomes a Magos policy (dependency resolution), not a loader invariant.Changes
mod_manager.lua: readsmods.lstviaread_content_to_table("mods", "lst"); deletes thedmfauto-prepend; removes the deadenabledfield from the_modsentry shape (set but never read; DMF reads onlyid/name/handle/_state/_settings.developer_mode); comments updated to the new contract.order = {}→ "nothing loads"; added dedicated missing-and-emptymods.lstcases (98/98).ENGINSEER.md,MOD_LOADER-DMF.md,enginseer/README.md, rootREADME.md,AGENTS.md, loader module headers, and shell comments (trampoline.h,dllmain.c) all updated. Left untouched:docs/reference/*references tomod_load_order.txt(those describe the community DML toolchain — accurate historical fact)._adapt_dmf_io), Rustdiscovery/, Cshell/logic,launcher/, CI.Behavior change
Missing/empty
mods.lst→ no mods load (graceful, no crash). Previously a missing file still loaded DMF via the prepend. Manual users must now listdmffirst inmods.lst(documented in README) — until Magos Modificus ships and authors the file automatically.Verification
make mod-loader-test98/98;make test(40/40 C, 10/10 Rust, 98/98 Lua);make build;make check; clippy clean. All 7 acceptance criteria met; targeted cases validated (order preservation, DMF-not-first respected, bad-mod isolation, missing/empty paths, blank/comment-line handling).Follow-ups (not in this PR)
mods.lst).mods/dir: the gitignored localmods/still hasmod_load_order.txt; rename it tomods.lstlocally before any live smoke test (out of PR scope — gitignored).mods.lstper profile).