# DarkRoom — Outstanding work **Status:** Living document · first written 2026-08-29 **Companion to:** [requirements.md §7](requirements.md), [technical-debt.md](technical-debt.md), [traceability.md](traceability.md) What is specified and not built, and for each cluster whether that is a decision, a dependency, or a gap nobody has looked at. This document exists because [traceability.md](traceability.md) cannot tell those apart. It reports one number — the share of requirements carrying a `TRACES` tag — and a missing tag means either "nobody has built this" or "somebody built it and did not say so". Both read the same way in the summary table, which makes that figure pessimistic *and* uninformative at once: it understates what works while hiding which of the remainder matters. Eight requirements gained a tag on this branch because the code already satisfied them and nobody had said so. Everything below is the other kind. It is also not a plan. [requirements.md §7](requirements.md) records what was deferred deliberately and needs no argument; this records what is still nominally in scope, so that the distance between the register and the binary is visible rather than something a reader has to reconstruct from a percentage. Where the honest answer is "this requirement should be amended rather than met", it says so — an unbuilt requirement that nobody intends to build is worse than a deferred one, because it keeps costing attention. **Several entries were struck between 2026-08-29 and 2026-08-30**, as two waves of work landed: burst grouping (FR-CULL-5), Flatpak packaging (FR-PLAT-LIN-3), FR-CULL-3 in full, Android memory pressure and lost-root recovery (FR-PLAT-AND-5, FR-PLAT-AND-2), image intents (FR-PLAT-AND-6), and the job runner (FR-PLAT-AND-4's Rust half). What remains of each is recorded where it appears rather than deleted, because a requirement that is *half* met is the one most likely to be reported as closed. --- ## 1. Plugins — 21 requirements, and a contradiction to resolve before any of them **Untagged:** FR-PLG-1, -1a, -2a, -2b, -2c, -3, -3a, -4, -4a, -5, -5a, -5b, -5c, -6, -6a, -7, -8, -9, -10, -11, -12. No plugin host exists. No crate loads anything at runtime: there is no manifest reader, no WASM or Lua engine, no registry, no signature check, no install path, no capability grant, no per-plugin failure ledger. `declared/mod.rs` says as much in its own documentation — the operation format is "not a plugin directory read at startup". **Two of §3.10's requirements are met, and they are the interesting two.** FR-PLG-2 and FR-PLG-2d — the declarative node format — are built and tagged: `core/dr-pipeline/ops/*.yaml` compiled by `build.rs`, with the restricted expression grammar in `declared/expr.rs` and a parity test asserting a declared operation and a hand-written one produce identical output. [code-health.md §3](code-health.md) calls it "a working plugin system that happens to resolve at build time", and that is exactly right. What is missing is not the format; it is everything that would let somebody who is not in this repository use it. **The contradiction.** [requirements.md §7](requirements.md) lists `| Plugin API | — |` among the things deferred for v1 — a bare row, where most deferrals carry a justifying note. §3.10 then spends roughly 280 lines and 23 requirement IDs specifying that same Plugin API in detail. Both statements are in the register of record, and the traceability denominator counts the second one: 21 IDs, 12% of all 179 defined requirements, worth about twelve points of coverage on their own — and nearly a third of everything the matrix reports as uncovered. A reader looking at the coverage figure has no way to know that, or that the subsystem behind it is one the same document says is not in this version. **And D16 is open.** [Decision D16](requirements.md) — plugin licensing — records that GPLv3 answers the derivative-work question differently for each of §3.10's three plugin forms, and that this "must be answered *before* an ecosystem exists, not after", because a term introduced later cannot be applied to plugins already written. D16 explicitly does not block FR-PLG-2; it blocks publishing a third-party format as stable. **What would resolve this:** an edit to `requirements.md`, not code. Either §7 drops the row, or §3.10 is marked deferred with the two built requirements carved out. Until one of those happens the coverage figure is measuring a decision that has already been taken, and taking it again every time somebody reads the matrix. --- ## 2. Culling — the stated differentiator, half built [D11](requirements.md) names culling "the core differentiator". FR-CULL-1, -2, -3, -4 and -8 through -12 are built. Three are not. **FR-CULL-3 — Raw-truth overlays. Built, all three bullets.** Focus peaking is `core/dr-gpu/src/focus.rs` and `ui/dr-ui/src/peaking.rs`; the raw histogram and the raw clipping indicators are `core/dr-gpu/src/raw_histogram.rs` and the second reading of the panel in `histogram.slint`. Worth recording, because it is the thing this entry previously got wrong and the next reader will have to check again. The display histogram — `dr-gpu/src/histogram.rs`, `ui/dr-ui/src/histogram.rs` — is **not** this requirement and never was: it is tagged FR-DSP-7, it reads `AdjustPass`'s 8-bit output, it counts clipping as `r == 255`, and so it describes the frame the display is about to show, after the whole develop chain. FR-CULL-3 asks for the *sensor data*, on the explicit grounds that a rendered image "systematically lies about what is recoverable in the raw". The two now sit in one panel behind a chip row, which is the arrangement that keeps them from being mistaken for each other: they answer different questions and both are true. What the raw reduction cannot answer is written down rather than left to be discovered — [architecture.md §5.5](architecture.md) records why it reduces over the demosaiced texture instead of the CFA samples §5.5 originally specified, and what that costs in what it can say. **FR-CULL-5 — Burst and near-duplicate grouping. Built.** `core/dr-catalog/src/bursts.rs`: frames join a burst when they are adjacent in time *and* look like the frame before them, compared adjacent-pair-only in one ordered walk. Signatures are 64-bit difference hashes taken from the thumbnails `dr-thumbs` already holds, on a background pass after the thumbnail sweep — nothing at import, nothing at query time. Nothing scores or rejects a frame: the representative is the earliest, a fact about the clock, and a new burst arrives *open* so the pass never takes a row off the screen. Two threads left hanging. `core/dr-face/src/calibrate.rs` still says "since FR-CULL-5 already groups bursts, positives are bootstrapped from bursts" while in fact bootstrapping from confirmed labels — that comment was a forward reference and is now simply wrong, rather than premature. And `dr_catalog::bursts::choose_representative` is written and tested but bound to no gesture, so today the only override is expanding the burst. `core/dr-catalog/src/dedup.rs` remains a different thing: re-import detection under FR-CAT-11, matching a file against one already catalogued, not two photographs against each other. **FR-CULL-6 — Compare and survey.** Absent. No side-by-side view, no synchronised zoom or pan. This is the one of the four with no adjacent machinery at all, and it is also the one that most directly distinguishes culling from browsing. **FR-CULL-7 — Culling on tablet.** Absent, and blocked by the three above rather than independent of them: there is no separate tablet culling surface to build until there is something to put on it. --- ## 3. FR-DEV-3g — AI denoise Promoted into v1 by [D11](requirements.md), and named there as the precondition for deferring AI masking — the argument being that one learned stage earns the runtime that a second could then reuse. Only classical noise reduction exists: `ops/noise_reduction.rs`, a bilateral filter in two arrangements, exact for luminance and separable for chroma. It is good, and it is not this. `models/` holds two face models and nothing else; `core/dr-segment/models/` holds a YOLO segmentation model for subject masks. There is no denoise model, no learned demosaic, and no inference path that is not face or segmentation. The obstacle is not the pipeline. It is that [D13](requirements.md) — model licensing — is still open for the models that already ship, and adding a third learned stage adds a third licence to answer for. Building the runtime before that is settled means owning the same problem in one more place. --- ## 4. The render path — FR-DSP-2, FR-DSP-4, NFR-RES-2 **FR-DSP-2 — Tiled computation. Unbuilt, and under challenge.** [architecture.md §6.2](architecture.md) calls for tiling "from day one" on the grounds that retrofitting it is a rewrite. It was not built, and the evidence has since moved. `core/dr-gpu/tests/frame_budget.rs` carries the argument in its own header: one fused dispatch over a viewport-sized target is comfortably inside the frame budget, and "if that stops being true, the recommendation to strike tiled computation from the interactive path stops being supported, and this test is what says so." [technical-debt.md TD-4](technical-debt.md) reaches the same place from the other direction — a tiled convolution at clarity's radius reads nearly twice the taps that an untiled one does, so the stage that looks most like it wants a tile cache is the stage that would be hurt most by one. What exists is the declaration and not the mechanism: `DetailPass::radius` is documented as the halo a tile would have to be grown by, with a test that pins it, and there is no scheduler to read it. That is deliberate plumbing, not an oversight. **So the open question here is not "when is tiling built" but "is FR-DSP-2 still a requirement".** Two measurements say it costs more than it saves on the interactive path. Neither says anything about the export path or about a device under memory pressure, which is where the case for it actually lives — and that is spike S6, which has not run. **FR-DSP-4 — Progressive refinement.** Unbuilt. FR-DSP-1's proxy rendering and TD-4's quarter-resolution base are adjacent and are not it: both are fixed choices about what resolution to compute at, where FR-DSP-4 asks for a first frame that is deliberately cheap and a second that replaces it. Nothing tracks a "this frame is provisional" state. **NFR-RES-2 — Images larger than GPU memory.** No answer, and §4.3 knows it: the requirement text itself asks the reader to "decide explicitly" how ARCH §6.4 and NFR-RES-2 are reconciled. There is no headroom budget, no allocation-failure fallback, and no spill. Spike S6 — a tiled pipeline on a mid-range Android device with an image larger than available GPU memory — is the one that would settle both this and FR-DSP-2, and there is no evidence it has run. --- ## 5. Android beyond running, and Flatpak The Android app is not a stub — it builds an APK, runs the whole application, unpacks bundled face models, and has been measured on a tablet ([faces.md §12.1](faces.md), [technical-debt.md TD-1](technical-debt.md)). What is missing is the platform contract around it. **FR-PLAT-AND-1 is untagged, and what it was tagged for was intent rather than code.** The requirement demands that library access be obtained *exclusively* through the Storage Access Framework. There is no SAF code: no `ACTION_OPEN_DOCUMENT_TREE`, no `takePersistableUriPermission`, no `DocumentsContract`. Its two tags rested on a `SourceRef::Document` variant constructed only inside `#[cfg(test)]` — `LocalStorage::open` refuses it, and the test that proves so is named `a_reference_of_the_wrong_kind_is_refused_rather_than_guessed_at` — and on `dr_plat::imports_supported`, which *returns false on Android* and whose own documentation says it "stops being false when a SAF implementation lands". The second tag documented the absence of the thing it was counted as evidence for. Both have been removed; this is the "plumbing a future feature would use" case [CONTRIBUTING.md](../CONTRIBUTING.md) and [code-health.md CH-4](code-health.md) both warn about. Android reaches a library through a Nextcloud account or a folder, over paths, like the desktop. That has a consequence for the rest of the cluster: **FR-PLAT-AND-2** — detecting the loss of a granted tree permission and marking images offline rather than deleting rows — cannot be built until there is a permission to lose. It is listed here as unbuilt, but it is blocked, not skipped. **FR-PLAT-AND-4 — half built.** The runner is done (`core/dr-catalog/src/runner.rs`): the queue that `jobs.rs` always had is now claimed from, completed, failed and recovered after a crash, which is FR-PLAT-AND-3's resumability as much as this requirement's. What is missing is the platform half — a foreground `Service`, `FOREGROUND_SERVICE` and `POST_NOTIFICATIONS` in the manifest, and a stated Doze behaviour. The build step that blocked it is no longer a blocker: the APK now compiles its own Java. Note also that **no handler is registered**, deliberately. The only enqueue site reachable in the shipping app produces remote thumbnail jobs already served by the async grid worker, and `walk::scan_root` — which holds the other two enqueue sites — has no caller outside an example. Wiring the sweep to claim from the queue is the honest next step and is an async rewrite of `library.rs`. **FR-PLAT-AND-5 — built.** A tiered eviction registry drives GPU caches, then proxies, then thumbnails, from `MainEvent::LowMemory` and `MainEvent::Stop`. **FR-PLAT-AND-6 — built, with one half unwired.** VIEW, SEND and SEND_MULTIPLE filters, the launch Intent read over JNI, and an `ExportProvider` rooted at `getFilesDir()` rather than AndroidX's `FileProvider`. The outbound share has no caller in `ui/` yet. **None of the runtime behaviour has been exercised on a device** — the tests read the manifest and the Java through `include_str!`, which catches a deleted filter but not a class loader that cannot find the class. **FR-PLAT-LIN-3 — packaged, not satisfied.** There is a Flatpak manifest now, granting no filesystem permission of any kind, plus AppStream metainfo and `docs/distribution.md`. The requirement is still not met, and cannot be met by packaging: a folder library is chosen by typing an absolute path, nothing in the tree calls the FileChooser portal, and inside the sandbox `$HOME` holds only `.var/app/...`. `dr_plat::volumes()` reads `/proc/self/mountinfo`, so a card mounted on the host is invisible to a sandboxed process as well. The fix is an `ashpd` directory picker beside `LocalStorage::grant`, not a change to the manifest. No Flatpak has been built here — `flatpak-builder` is not installed — so the permission set is reasoned, not observed. **NFR-COMPAT-2 — distribution channels. Stated, which is all this requirement asks.** The paragraph above cites [distribution.md](distribution.md) and it is the same document that answers this: §1 names five channels and their state — Arch source package and Flatpak in tree, AppImage a v1 channel whose recipe is not written, F-Droid a v1 channel not yet submitted, and Play explicitly **not** v1. The requirement is to *state* the channels, and they are stated, including the deferral. What remains is the coupling the requirement points at rather than the statement it demands. §4.8 observes that publishing on Play is what turns SAF from a preference into a constraint, and distribution.md §6 argues the coupling runs the other way for this project — F-Droid asks nothing that ARCH §6.9 does not already require. Spike S11, the Play permissions dry-run, has not run, and until it does that argument is reasoned rather than confirmed. Two of the five channels also exist as decisions rather than as recipes, and no Flatpak has been built here at all. Related, NFR-COMPAT-1's baseline is real but scattered — API 28/36 live in the Android Dockerfile and are checked in CI against the built ELF, which is good — while the items the requirement singles out are missing: whether `shaderFloat16` and 16-bit storage are required (the one it flags as jeopardising R1), minimum RAM, minimum desktop Mesa, and a named reference device from a second GPU vendor. **NFR-OPS-2 and NFR-OPS-4.** Crash reporting is a `log::error!` panic hook on Android and nothing at all on desktop: no local crash record, no backtrace capture, no upload path and therefore no opt-in gate to guard it. Update and first run are undefined; the concrete reason NFR-OPS-4 gives — that D2 pins rawler at a non-SemVer alpha whose camera-support fixes users will need — is unaddressed, and there is no update mechanism of any kind. --- ## 6. Accessibility and internationalisation — the hard half is done and the easy half is not **NFR-A11Y-1 — Localisation.** `@tr(` appears **zero** times across 14,482 lines of Slint. That number overstates the problem, because the part that is genuinely architectural was got right: `LocalizedKey` keeps display strings out of `core/` entirely, every operation publishes a key rather than a label, and `labels::resolve` is the single point where a key becomes text. What that single point does, however, is a hardcoded English `match` in Rust source — so changing a translation requires a recompile, which is the one thing the requirement explicitly forbids. There is no message catalogue in any format, no locale-resolution rule, and no decision recorded about RTL. The work left is therefore smaller than it looks and entirely mechanical: a catalogue format, a load path behind `resolve`, and `@tr(` around the Slint literals. The design it needs already exists. **NFR-A11Y-2 — Accessibility.** `accessible-*` appears five times in the whole interface, all five on one control — the parameter slider in `adjust.slint` — and nothing is set from the Rust side at all. Everything else in eighteen Slint files is unnamed to AT-SPI and TalkBack. The requirement's own caveat, that Slint's Android accessibility needs verifying, is spike S13, which has not run. **NFR-A11Y-3 — Colour-independent status.** Built where a control exists, and now tagged: the clipping readout pairs a marker that appears or disappears with a figure in words, the rating strip is a solid star against an outline in an achromatic palette, the pick/reject mark is a tick against a cross, and the focus-peaking colour chips say "Red" and "Cyan" rather than showing swatches. Each of those already carried the reasoning in a comment naming this requirement and simply had no `TRACES` line. Two caveats, because the tag now says more than the evidence does. **Only the clipping clause has a test** — `a_clipping_figure_distinguishes_none_from_nearly_none`, which pins `<0.1%` apart from `0%` so the figure cannot contradict the lit marker beside it. The three Slint components are inspected-and-argued, not asserted, and nothing would fail if a future edit made a star differ only in tint. **And the requirement's first named example has no interface at all**: catalog colour labels are a nullable `label INTEGER` column on the versions table and are set and shown nowhere, so the clause about them is untestable rather than satisfied. That clause closes when the label UI is built, not before, and it should be built with a shape from the outset — which is the same argument as below, for doing this alongside NFR-A11Y-2 rather than after it. --- ## 7. Catalog and sync **FR-CAT-14 — Migration import.** Reading ratings, labels, keywords and collections out of a Lightroom `.lrcat` or a darktable `library.db`. Unbuilt. The destination is not: keywords, collections, ratings and the cross-device merge rules are all built and tested, and `keywords.rs` already anticipates the arrival ("an import from Lightroom can bring in…"). What is missing is only the two source adapters — which is a comparatively contained piece of work for a requirement that decides whether somebody can try this software on a library they already have. **FR-NC-11 — Initial catalog build.** Using WebDAV `SEARCH` (RFC 5323) against `/remote.php/dav/`, filtered by mimetype and paginated, in preference to walking folders with PROPFIND. Unbuilt: no `SEARCH` request is issued anywhere. The PROPFIND walk this exists to replace is fully built and well optimised — ETag pruning under FR-NC-4 turns an unchanged 50k library into one request — so the gap is narrower than it reads. It is the *first* build against a large remote library that pays, and that is the moment a new user meets. **FR-CAT-13 — XMP interoperability, untagged and not met.** Read and write standard XMP sidecars. Its single tag sat on `keywords.rs`, which stores keywords in the catalog and mentions `dc:subject` in a comment about what a keyword's text is *for*; no XMP is parsed or written anywhere in the tree, and `dr-export`'s metadata module says so about its own half ("neither is read by `dr-decode` today"). The tag has been removed — `dr-preset-xmp` is not the counter-example it looks like, being a reader of Lightroom *presets* under FR-DEV-6, which is a different file and a different purpose. Listed here rather than silently, because a tag makes a gap invisible and this one is load-bearing for interoperating with the editors FR-CAT-14 imports from. --- ## 8. The performance targets are half-verified, and the half that is left is the hard one §8 and §4.1 both require the same thing in the same words: an automated benchmark suite against a synthetic 50k catalog, run per commit, where **"a regression beyond a stated tolerance is a build failure, not a notification."** For most of this project's life it did not exist — no `benches/`, no criterion, no synthetic catalog, and three CI workflows that between them measured nothing. **It exists now, for everything that does not need a frame.** [`tools/bench`](../tools/bench) builds a deterministic 50,000-row catalog over a pool of a dozen real files, measures against it, and fails the build on a violated budget or a drift past tolerance; [`.gitea/workflows/benchmark.yml`](../.gitea/workflows/benchmark.yml) runs it on every push, and [benchmarks.md](benchmarks.md) is the account of what it does and does not cover. **NFR-P1** and **NFR-P3** are now genuinely gated, and R2's "catalog opens in under 2s" clause with them. Three qualifications, all of them stated in the harness itself rather than only here: - **The numbers have not been recorded yet.** Every `recorded` field in [bench-baseline.json](bench-baseline.json) is `null`, deliberately: a fabricated baseline is worse than none. Until `dr-bench record --reference` is run on the reference desktop and committed, the budget gate works and the regression gate does not. - **NFR-P7 and NFR-P8 are half-measured and are not tagged.** The export row covers the encode half of the chain and no GPU render, so it can fail the requirement and cannot pass it. The memory row covers a process holding the catalog and nothing else — no toolkit, no adapter — so it is the catalog layer's share of the 500 MB rather than the figure NFR-P8 is about. Neither carries a `TRACES:` tag, which is the point. - **NFR-P8 needs a decision, not more code.** How much of its 500 MB belongs below the UI is unstated, and until somebody says, the metric can record but not judge. [benchmarks.md](benchmarks.md) also answers the question §4.1 raises about GPU memory — RSS cannot see device-local allocations at all — and recommends restating the requirement as two figures. **What is left is the frame-timing half, and it is the hard one.** NFR-P2, -P4, -P5, -P6, -P9, -P10, -P11, -P12, -P13, -P14 and -P15 all need a probe inside a running Slint application, a GPU adapter, or both. `dr-gpu/examples/frame_budget` is a real instrument for the GPU part and its results are committed in [frame-budget.md](frame-budget.md) with the machine and profile named — but it is run by hand, and the guard version in CI skips itself where there is no adapter, which is the normal case on a runner. So the claim to take from this section is now narrower than it was, and still true: **a scroll that dropped to 30 fps tomorrow would reach a user before it reached CI.** --- ## 9. Two core requirements that cannot be closed as written **R1 — Cross-platform output within a bounded tolerance.** §2 states that the threshold "must be fixed before spike S9", because S9 both validates R1 and calibrates what tolerance is achievable. The threshold was never fixed and S9 has not run, so R1 currently has no acceptance criterion at all — there is nothing a test could assert. The matrix used to report R1 as *covered*, and what covered it was two string literals: fixtures inside the traceability tool's own unit tests, which the tool scans along with everything else, because a fixture demonstrating tag extraction was indistinguishable from a tag. The extractor now asks where the tag sits — a tag is the first word of a comment, not a string appearing anywhere on a line — and R1 is untagged again, which is the honest reading while it has no acceptance criterion to tag anything against. NFR-OPS-1 was covered by tags that were real rather than fixtures, which is the worse case of the two: one on `compute_coverage` and one on the gesture extractor, both on the traceability tool. A coverage calculation and a documentation generator are not diagnostics under any reading, and the requirement asks for a rotating, size-capped on-disk log in the XDG state directory, credential redaction, and a consented diagnostics bundle. None of that exists — logging goes to stderr and logcat — so both tags have been removed and NFR-OPS-1 is untagged. It is the case [CONTRIBUTING.md](../CONTRIBUTING.md) warns about in its own words: a tag proves a tag exists. **R2 — Efficient display of huge RAW libraries.** Its acceptance criterion contains "*(figure TBD)*" — the scroll velocity below which no cell may render as a placeholder — and asks for a stated prefetch margin and cache-hit rate. No figure is stated anywhere in the tree, neither quantity is measured, and [TD-2](technical-debt.md) and [TD-3](technical-debt.md) both describe the thumbnail path falling short of it in ways that were measured. R2 was deliberately left untagged on this branch for that reason: the machinery is substantial and the criterion is unmet and partly undefined. Both belong with §8 above. A requirement whose threshold was never chosen and a target nothing measures fail in the same way — not by being wrong, but by being unfalsifiable. --- ## 10. Spikes §9 defines fourteen validation spikes and says of three of them: "S1, S2 and S10 are the three that can invalidate the architecture." Only **S1** (Slint + wgpu zero-copy on Linux) and **S14** (the face pipeline on a real library) have recorded results. S14's are the best evidence of any spike — a dedicated document, a measured pass over an 18,143-face library, a named device and a reproducible command — though D13's licensing half remains open. **S6, S9, S10, S11 and S13 show no evidence of having run at all.** Each is referenced only from the requirement text that asks for it: | Spike | Would settle | Blocked on | |---|---|---| | S6 | FR-DSP-2, NFR-RES-2 — tiling and images larger than GPU memory | Nothing; needs a device and a large image | | S9 | R1's tolerance threshold, and therefore R1 | Nothing; the threshold is defined *by* running it | | S10 | Whether SAF at 10k files meets NFR-P1/P3 | §5 — there is no SAF code to measure | | S11 | NFR-COMPAT-2, and whether Play makes SAF binding | Nothing | | S13 | NFR-A11Y-2 on Android | §6 — there is almost nothing to test with | S2, S3, S4, S5, S7, S8 and S12 are also unrun, several with acknowledgements in the code that say so (`dr-sync/src/upload.rs` on S8, `dr-sync-nextcloud/src/lib.rs` on S3). S2 is one of the three architecture-invalidating spikes and needs Adreno and Mali hardware, which the manifest notes no emulator represents. The pattern is worth stating rather than leaving to be inferred: the spikes that ran are the ones whose subject was being built anyway. The ones that did not are the ones that would have said whether something *should* be built — which is the opposite of the order §9 asks for. --- ## 11. D12, which governs all of the above [Decision D12 — scope versus pace](requirements.md) is still **OPEN**, and says: > The calibration selected an ambitious feature set — full tablet editing, full ingest, culling as a > differentiator, complete GPU masking, AI denoise, Fuji-first colour, deep sync, sidecar durability > — against a stated pace of evenings and weekends, indefinitely. > > **Those are not compatible as stated.** Sections 1 through 10 are what that incompatibility looks like eleven versions later, and they land almost exactly where D12 predicted: tablet editing carries SAF at unproven scale, background execution limits and two GPU vendors to validate (§5), and every one of those is unbuilt or unrun. The parts that *were* built — the develop pipeline, sync, faces, the catalog — are the parts that did not need a decision first. D12 is not resolved by choosing to work faster. It is resolved by moving requirements across the line into §7, which costs nothing but the admission, and which this document is intended to make easy: every cluster above is a candidate, and each says what it would take to build and what it would cost to drop. Resolving D12 sets D3 and [architecture.md §10](architecture.md)'s Phase 2.