Put the developer docs under docs/dev and index the folder for users first
docs/ had 26 developer documents flat beside the manual, and the two audiences are very differently sized: most readers want the manual and the gesture reference, a few want the register, the designs and the measurements. The manual and gestures.md stay at the top; everything for someone changing the code moves to docs/dev/, and the two documents that name their own successors — the v0.1 milestone and the UI-refinement plan — go to docs/dev/archive/ rather than being deleted, since both are still cited. docs/README.md is the index, users first. Every reference follows: code comments, Cargo manifests, the workflows, the pre-commit hook, the bench and traceability tools (which locate the repo root by docs/dev/requirements.md now), packaging, the Docker READMEs, CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level deeper and is regenerated. Links out of the moved documents into the tree gain a level; a link checker over every Markdown file finds none broken.
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
# 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 — post-v1 since 2026-09-19
|
||||
|
||||
> **Resolved, in the register.** The contradiction below was settled on 2026-09-19 the way the
|
||||
> last paragraph of this section asked: §3.10 is marked `(post-v1)` clause by clause, §7's row
|
||||
> says so with a reason, D16 defers with it, and the traceability tool lists deferred clauses in
|
||||
> their own table instead of counting them. Coverage went from 72.2% of 194 to 80.6% of 170 on
|
||||
> that edit alone. What follows is kept as the record of what was decided and why; nothing in it
|
||||
> is owed a tag.
|
||||
|
||||
|
||||
**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" — and on 2026-09-19 the answer was: as written, until S6 runs.** FR-DSP-2 now carries a status note saying exactly that, and R5's note no longer claims it was rewritten.
|
||||
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.** Half answered. NFR-R8's "decide explicitly" was
|
||||
decided on 2026-09-19: there is no CPU render pipeline, the degraded mode is the viewer on
|
||||
embedded previews with develop withheld, and NFR-RES-2 no longer promises a fallback render. What
|
||||
remains unbuilt is the memory half: there is no headroom budget, no allocation-failure staging,
|
||||
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 is met, and NFR-OPS-4 is not.** Crash reporting is `platform/dr-plat/src/crash.rs`: a
|
||||
panic on either platform writes a local record with a redacted message and backtrace, ten are kept,
|
||||
and there is deliberately no upload path — the requirement's "upload only on explicit opt-in" is
|
||||
satisfied by there being nothing to opt into, and the module says why a transport built ahead of the
|
||||
consent is the wrong order. (This paragraph said the opposite until 2026-09-12; the record had landed
|
||||
on 2026-08-30 and the paragraph had not been read against 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, wired on 2026-09-12.** `core/dr-xmp` reads and writes
|
||||
standard XMP sidecars: `dc:subject` and `lr:hierarchicalSubject`, `xmp:Rating` and `xmp:Label`, and
|
||||
the IPTC core fields, in both the attribute and the element form and whatever RDF container a file
|
||||
happened to use. It states the ownership rule in one place — DarkRoom owns the properties in
|
||||
`PROPERTIES` and nothing else in the document, identified by namespace URI rather than by prefix —
|
||||
and enforces it by rewriting a packet event by event rather than serialising over it, so another
|
||||
application's `crs:` settings, comments and processing instructions survive a write byte for byte.
|
||||
|
||||
**The wiring is `ui/dr-ui/src/xmp_sync.rs`.** The scan collects `.xmp` beside `.drsc` from the
|
||||
listings it was already making; the pull reads each one whose ETag has moved and reconciles it
|
||||
against the catalog with the catalog winning — keywords union, and a rating, label or caption taken
|
||||
only where the catalog holds none. Both naming conventions resolve: `IMG_0001.CR3.xmp` names its
|
||||
file, `IMG_0001.xmp` the stem, and the JPEG beside a RAW is the same photograph. A genuine
|
||||
disagreement is written to `xmp_conflicts` and the settings page offers "Take the sidecars' values",
|
||||
which is the reload the requirement asks for; the detection it asks for is the ETag that moved. The
|
||||
write in the other direction is behind a setting that starts off (NFR-R4): a judgement or a keyword
|
||||
then also rewrites the sidecar beside the original, keeping the file's own caption, copyright and
|
||||
hierarchy, which the catalog has no columns for and would otherwise have deleted.
|
||||
|
||||
**What remains.** GPS is not carried — `exif:GPSLatitude` is a format of its own and `dr-decode`
|
||||
produces no location for it to carry yet. Title, description and copyright are read and reconciled
|
||||
but the catalog has nowhere to put them, so they pass through a rewrite rather than being editable.
|
||||
An XMP write made offline is not queued: the catalog and the `.drsc` are authoritative, and the next
|
||||
judgement online writes the file whole again.
|
||||
|
||||
`dr-preset-xmp` remains what it always was and is still not the counter-example it looks like: a
|
||||
reader of Lightroom *presets* under FR-DEV-6, a different file for a different purpose.
|
||||
|
||||
---
|
||||
|
||||
## 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, so both
|
||||
tags were removed. It is the case [CONTRIBUTING.md](../../CONTRIBUTING.md) warns about in its own words:
|
||||
a tag proves a tag exists. The requirement has since been built where it says: the rotating,
|
||||
size-capped log and its redaction in `platform/dr-plat/src/diagnostics.rs` (2026-08-30), and the
|
||||
bundle in `diagnostics/bundle.rs` (2026-09-12) — the log, the crash records, the version, the schema
|
||||
and the GPU as one text file, shown in full in Settings before a second press writes it, and sent
|
||||
nowhere by either press.
|
||||
|
||||
**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. Merging — specified 2026-09-19, nothing built
|
||||
|
||||
§3.11 was written on 2026-09-19 under D18, undeferring the panorama from §7 and leaving HDR merge
|
||||
and focus stacking there with their data model decided. Eleven `FR-MRG` clauses and two `NFR-MRG`
|
||||
figures entered the register at once with no code behind any of them, which is why the coverage
|
||||
figure fell from 83.0% to 77.2% on the same day — a specification, not a regression.
|
||||
|
||||
[panorama.md](panorama.md) is the design, and its §10 is the order of work. Nothing starts before
|
||||
**S15**: whether rawler reads back a linear DNG the application writes, whether XFeat loads under
|
||||
tract at a fixed shape, whether the working-space texture can be tapped where FR-MRG-2 needs it,
|
||||
and what a chunked blend of a 100 MP composite costs on the tablet. The first two are a day each
|
||||
and either can change the design, which is the reason they come first.
|
||||
|
||||
## 12. D12, which governed all of the above
|
||||
|
||||
> **Decided 2026-09-19, by events.** The scope stands as calibrated, v1 has no date, and `(post-v1)`
|
||||
> in §7 is the one way a clause leaves the count — used for the plugin API and nothing else. The
|
||||
> argument below is kept because it is what the decision weighed; its prediction about tablet
|
||||
> editing was right, and the cluster was built anyway.
|
||||
|
||||
[Decision D12 — scope versus pace](requirements.md) said, while it was open:
|
||||
|
||||
> 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 was not resolved by choosing to work faster, and in the end not by moving clusters into §7
|
||||
either, except the one: plugins. Every other cluster above stays in scope, and each still says what
|
||||
it would take to build. That is the list. D3 is delivered, and
|
||||
[architecture.md §11](architecture.md)'s build order is what followed.
|
||||
Reference in New Issue
Block a user