# DarkRoom — Code health and the cost of a contribution **Status:** Audit · 2026-08-27 **Companion to:** [architecture.md](architecture.md), [technical-debt.md](technical-debt.md), [view-composition.md](view-composition.md) What it costs to add something to this codebase, measured rather than estimated, and the work that would lower the price. [technical-debt.md](technical-debt.md) records compromises that were *chosen* — each one has a reason that outlived the person who took it. This document records the opposite: friction nobody chose, which accumulated because no single commit was responsible for it. The distinction matters when deciding what to touch. A TD entry is load-bearing until its "done when" is met; an entry here is not defending anything. It is also not a bug list. Everything below compiles, passes 2,042 tests and ships. --- ## 1. What was measured Every figure in this document is reproducible from a clean checkout. Worktrees under `.claude/` and build output under `target/` are excluded from all counts — including them roughly triples the line totals and was the first thing to get wrong. ```bash # Lines of Rust per crate for d in core/* ui/* platform/* apps/* tools/*; do [ -d "$d/src" ] && echo "$(find $d/src -name '*.rs' -exec cat {} + | wc -l) $d" done | sort -rn # unwrap() in production code only — split each file at its #[cfg(test)] marker # (a naive grep counts ~1,500 and tells you nothing) # Distinct window properties written per UI module for f in ui/dr-ui/src/*.rs; do echo "$(grep -oP '\b(w|window|win|ui)\.\Kset_[a-z0-9_]+(?=\()' "$f" | sort -u | wc -l) $f" done | sort -rn ``` | Measure | Value | |---|---| | Rust across 19 crates | ~112,000 lines; ~78,000 after comments and blanks | | Comment density | 28% overall, 20–34% per crate | | Test functions | 2,042, plus 21 integration test files | | `.unwrap()` in production code | **3** — one in `dr-gpu`, two in `dr-ingest` | | `.unwrap()` in test code | ~1,500, which is where it belongs | | `unsafe` blocks | 9 — three of them the face scan's SIMD kernels (faces.md §9) | | `TRACES` tags / orphan tags | 793 / 0 | | Resolved dependencies | 826 | | Largest function | `dr-ui::run` — 1,855 lines | | `AppWindow` members | 282 properties + 190 callbacks | CI gates on `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`, a release build, and an Android cross-check. The traceability matrix is regenerated and compared, with a pre-commit hook that keeps it in step. --- ## 2. The seams, graded Six things a contributor might plausibly want to add. Each grade was checked against the tree. | Feature | What you touch | Cost | |---|---|---| | A develop operation
*split toning, channel mixer* | One file in `core/dr-pipeline/ops/`. Nothing else. | **Trivial** | | A RAW format | A `Format` variant and magic-byte recognition. `dr-decode` is a generic TIFF walker carrying only 3 format-specific branches. | **Easy** | | A neighbourhood operation
*dehaze, a sharpener* | Rust in `dr-pipeline/src/ops/` implementing `Operation` + `DetailStage`, plus a stub YAML declaring `rust:` and `order:`. | **Moderate** | | A parameter widget
*a colour wheel* | A `WidgetKind` variant, `develop::supported()`, the panel model, a Slint component. | **Moderate** | | A second sync backend
*S3, WebDAV, a local folder* | `RemoteBackend` is the easy half. Seven UI files construct `NextcloudBackend` directly and ten signatures take it concretely — see [CH-2](#ch-2). | **Hard** | | Anything with its own UI | `app.slint`'s root component, a ~1,000-line `wire()`, and `run()` at 1,855 lines — see [CH-1](#ch-1). | **Hard** | The top half of that table is the good half, and it is very good. The bottom half is one problem wearing two hats: **`dr-ui` has no seams, so every UI feature lands in the same three files.** --- ## 3. What is load-bearing, and must not be "tidied" Listed before the findings deliberately. A remediation document that only enumerates problems invites someone to fix something that was right. **The operation declaration format.** `ops/*.yaml` + `build.rs` is a working plugin system that happens to resolve at build time — see [display-and-extension.md §6](display-and-extension.md). Its deliberate smallness is the point: the expression grammar is restricted so a declaration cannot become a second, worse place to write code. Do not "improve" it by letting a node name arbitrary Rust. **No operation is named in `ui/`.** Verified: all fifteen built-in op ids grepped across every `.rs` and `.slint` file in `ui/` yield exactly one hit, a localisation test in `labels.rs:353`. This is what makes "a new operation is one file" true rather than aspirational, and it is the single property most likely to be destroyed by a well-meaning special case in the panel. See [CH-5](#ch-5). **Unimplemented widgets degrade rather than break.** `develop::supported()` lists every `WidgetKind` explicitly instead of using a wildcard, so a new kind added to the core surfaces as a compile error rather than as silence, and a widget nothing draws falls back to sliders with the edit still working (ARCH §4.3a). **The mpsc-plus-timer worker shape.** Slint's event loop must never block (NFR-P9). Threading is not what any finding below proposes changing. **`sync_rows` mutating rows in place.** Replacing the model breaks slider dragging. --- ## CH-1 — `dr-ui` has no view layer, and the cost is compounding **Where:** `ui/dr-ui/src/lib.rs:836`, `library_ui.rs:4374`, `collections_ui.rs:1457`, `ui/dr-ui/ui/app.slint` ### What it is Every UI feature lands in the same three places: the Slint root component, one of the `wire()` functions, and `run()`. | Function | Lines | |---|---| | `lib.rs::run` | 1,855 | | `library_ui::wire` | 998 | | `collections_ui::wire` | 964 | | `identity_ui::wire` | 478 | | `masks_ui::wire` | 357 | | `settings_ui::wire` | 317 | Above them sits one `AppWindow` carrying 282 properties and 190 callbacks, with exactly one Slint global in the whole `ui/` directory — and it is not exported. Cross-view state therefore has nowhere to live except the root component, and every interaction is a callback registered inside a `wire`. The core crates are healthy by the same measure: their largest functions are `compose_full` at 437 lines and `build.rs::emit_node` at 558, both of which earn their length. This is specific to `dr-ui`. ### Why it matters more than it did **[view-composition.md](view-composition.md) already diagnosed this and specified the fix**, in three independently landable stages, on 2026-08-09. None of the three has landed. In the eighteen days since, the numbers that document itself used have moved: | Its measure | 2026-08-09 | 2026-08-27 | |---|---|---| | `run()` | 500 lines | **1,855** | | `library_ui` distinct window properties | 24 | **54** | | `lib.rs` distinct window properties | 23 | **52** | | `collections_ui` | 9 | 11 | | `launch_ui` | 16 | 16 | | `set_show_*` call sites | 7 | 9 | | View-state booleans on `AppWindow` | 2 | **5** | Four modules it did not list now write window properties too: `settings_ui` (39), `import_ui` (24), `identity_ui` (18), `masks_ui` (16). The prediction it made has also come true literally. It described `app.slint` compensating for two mutually exclusive booleans with `if !root.show-launch && root.show-library` chains. There are now five such booleans — `show-launch`, `show-library`, `show-identity`, `show-settings`, `show-import` — and the chains at `app.slint:1405`, `:1445` and `:1654` are five-term conjunctions. Each new view multiplies the conjunctions rather than adding to them. None of this is difficult work. It is simply work that every feature must now do, in files every other feature is also editing — which is why two contributors working in parallel conflict by construction, and why a newcomer must read a 1,855-line startup sequence with real ordering constraints before safely inserting a line into it. ### What to do Execute [view-composition.md](view-composition.md) as written. Its analysis holds and its staging is right; stage 1 alone removes the nullable-callback knot and the duplicated post-load sequence, and is worth landing whether or not stages 2 and 3 follow. One thing to add to it, because it was not in scope there: the `wire()` functions. They are already sectioned internally by comment, so lifting each section into `fn wire_ratings(window, ctl)`, `fn wire_keywords(...)` and so on is mechanical, checked entirely by the compiler, and can go one section per commit. It gives a feature a *function* to own rather than a region of one, which is what removes the conflict, and it does not wait on stage 1. Do the extraction behind new features rather than as a big-bang refactor. The pile grows either way; the question is only whether each new feature adds to it or subtracts. **Done when:** no function in `dr-ui` exceeds 300 lines, a new view registers itself instead of adding a boolean to `AppWindow`, and `active-view` has replaced the boolean set so both-true is unrepresentable. --- ## CH-2 — `RemoteBackend` is an abstraction nothing above `dr-sync` uses **Where:** `core/dr-sync/src/lib.rs:46`; seven files in `ui/dr-ui/src` ### What it is The trait is carefully built. Capability negotiation decides the sync strategy; range reads are documented as a hint rather than a guarantee so correctness holds either way; chunked upload is deliberately kept internal so one server's protocol cannot leak into the interface. Every choice is explained where it is made. And then `trash.rs`, `import.rs`, `export.rs`, `derived_sync.rs`, `launch_ui.rs`, `library.rs` and `settings_ui.rs` each construct `NextcloudBackend` directly — 34 references — and ten functions take `&NextcloudBackend` rather than `&dyn RemoteBackend`. Exactly **two** sites in the tree take the trait object, both inside `dr-sync` itself. ### Why it matters The abstraction currently buys nothing it was designed for. Worse, it reads as though it does: a contributor who wants a WebDAV or local-folder backend will find a well-documented trait, implement it correctly, and only then discover that nothing above `dr-sync` can be handed the result. There is no defence of this in the tree, which is what makes it an entry here rather than in `technical-debt.md`. It is what a single-backend application looks like when the second backend has not yet been attempted. ### What to do Change the ten signatures to `&dyn RemoteBackend` and construct the backend once, behind something the UI does not name — the same discipline `develop.rs` already applies to operations. The trait is already correct, so this is a mechanical change, and it is much cheaper now than during a second backend when it would be entangled with that backend's own problems. Worth doing even if no second backend is ever written: it makes the sync layer testable against a fake, which today it is not. **Done when:** `NextcloudBackend` is named in at most one file in `ui/`, and a stub backend can be substituted in a test without touching the UI. **Done**, with one half deliberately left. `ui/dr-ui/src/remote.rs` is now the only file in the interface that names a connector; the ten worker functions take `&dyn RemoteBackend` and will accept a stub. Every method the UI ever called on a backend — `get`, `put`, `list`, `delete`, `create_dir`, `move_to` — was already on the trait, so nothing had to be added to it. What remains is **credentials**. `AppCredentials` is an app password obtained through Login Flow v2, which is a Nextcloud protocol rather than a general notion of how one authenticates to a remote, and seven files still name it. Abstracting it needs a decision about what an account *is* across backends — an OAuth token, a bucket key pair and an app password have no useful common shape — and making that decision before a second backend exists would produce a confident wrong answer. It is a design problem rather than a mechanical one, and it should wait for the backend that forces it. --- ## CH-3 — There is no path in for a contributor who is not already here **Where:** repository root ### What it is No `CONTRIBUTING.md`. No `rust-toolchain.toml`, though CI pins 1.92.0 exactly. No issue or PR templates. The documentation that exists is excellent — 7,990 lines across 14 files, including 177 numbered requirements — and all of it is written for someone who has already decided to work on this. Nothing tells a newcomer which document to read first, that `core/dr-pipeline/ops/README.md` is the door with the lowest bar, or that a clone without `git-lfs` needs one command before the build succeeds. That last point is handled well in the code: `dr-segment`'s build script detects an LFS pointer file and fails with an instruction rather than embedding 130 bytes and dying at inference time. It is simply not written anywhere a first-time cloner would look. ### Why it matters It is the cheapest item in this document and it gates every other contribution. A person who cannot get a first build is not going to reach the parts that are good. ### What to do Write `CONTRIBUTING.md` and point the first door at the operation format. "Add a develop operation" is a genuinely one-file contribution with declared tests that run under `cargo test` — the best first experience this codebase can offer, and it happens to teach the architecture's central idea on the way through. Then state the three things that are currently folklore: `git lfs` is a prerequisite, the first build resolves 826 crates and takes a while (saying so stops it reading as a hang), and the toolchain is 1.92.0. Add `rust-toolchain.toml` so that last one is enforced rather than documented — a contributor on an older stable currently gets confusing type errors instead of a version message. **Done when:** someone who has never seen the repository can clone it, build it, and land a new `ops/*.yaml` node without asking a question. --- ## CH-4 — Coverage is counted by tagging, not by behaviour **Where:** `docs/traceability.md`, `tools/traceability` ### What it is Not a new finding — [display-and-extension.md §7](display-and-extension.md) states it plainly, and `traceability.md` itself says coverage is the intersection of tagged and defined IDs. The tooling is genuinely good: 732 tags, zero orphans, denominators parsed from `requirements.md` at run time rather than hardcoded, regenerated in CI and guarded by a pre-commit hook. What it cannot do is check that the code under a tag does the thing. `FR-DEV-8` is currently tagged against instance-buffer plumbing a future spot-removal operation *would* use; `FR-DEV-7` against a history row for a frontend that does not exist. Both read as covered. ### Why it matters The 55.4% figure is an overstatement of unknown size, and the risk is that it is used as a planning input. It is recorded here so that the number keeps its asterisk when read outside the document that already qualified it. ### What to do Nothing structural — the honest framing already exists in two places. Adopt display-and-extension.md's rule going forward: **close a requirement with a test that would fail if the behaviour were removed**, and let the percentage move slowly and mean something. **Done when:** the rule is stated in `CONTRIBUTING.md` alongside the tag syntax, so it reaches someone adding their first tag. --- ## CH-5 — The best invariant in the codebase is unprotected **Where:** `ui/dr-ui/src`, `ui/dr-ui/ui` ### What it is "No code in `ui/` names an operation" (FR-DEV-3a) is what makes the whole declarative pipeline pay off, and it is currently maintained by discipline alone. Nothing fails if someone special-cases `exposure` in the panel to fix a layout problem at five in the evening. ### Why it matters It is one grep, it would take an hour, and it protects the property this audit rates highest. The failure mode is silent and cumulative: the first special case is defensible, and by the fifth the panel names half the chain and "a new operation is one file" has quietly stopped being true. ### What to do A test that greps `ui/dr-ui/src` and `ui/dr-ui/ui` for every id in `ops/*.yaml` and fails on a hit, with the current `labels.rs` localisation test as its one allowed exception. It belongs in CI beside the traceability check, which is the existing precedent for a structural gate. **Done when:** adding `window.set_exposure_slider(...)` to the panel fails CI with a message naming FR-DEV-3a. --- ## 4. Order of work Ordered by value per hour rather than by size. | | Item | Effort | Why first | |---|---|---|---| | 1 | [CH-3](#ch-3) — `CONTRIBUTING.md` + `rust-toolchain.toml` | Half a day | Gates everything else; unblocks the trivial seam that already works | | 2 | [CH-5](#ch-5) — CI gate on operation names in `ui/` | An hour | Protects the property everything else in the pipeline rests on | | 3 | [CH-2](#ch-2) — make `RemoteBackend` load-bearing | 1–2 days | Mechanical now, entangled later; also makes sync testable | | 4 | [CH-1](#ch-1) — split the `wire` functions | Incremental | No behaviour change, compiler-checked, one section per commit | | 5 | [CH-1](#ch-1) — [view-composition.md](view-composition.md) stages 1–3 | Sustained | Largest and most invasive; every deferred month adds to the pile | Items 1, 2 and 3 are independent of each other and of the rest. Item 4 does not wait on item 5. --- ## 5. What this document does not claim It measures structure, discipline and coupling. It does **not** assess runtime correctness, GPU shader behaviour, security posture, or whether any tagged requirement is actually implemented — [CH-4](#ch-4) is precisely the observation that the last of those is unmeasured. Line counts and function sizes are proxies. `run()` being 1,855 lines is a real problem because every feature must edit it, not because 1,855 is a bad number; `build.rs::emit_node` at 558 lines is not a problem at all. Where a figure appears above, the sentence around it says which of the two it is. The audit was first measured against `origin/master` at `d4a34ef`, then re-measured after `android-bundled-face-models` merged in: `run()` moved from 1,810 lines to 1,855, and the whole- library face sweep added its fetching path to `library.rs`. The seam grades are unchanged by that merge — the work went into the seams that already existed rather than cutting new ones, which is itself the pressure CH-1 describes.