Commit Graph
18 Commits
Author SHA1 Message Date
dtourolle c901fc1a0a Specify panorama merging: §3.11, D18, S15, and the design in panorama.md
A merge writes a new source file beside its sources (D18) rather than a
multi-source Version, which answers the schema question §7 had been holding
open for panorama, HDR merge and focus stacking together. The panorama is
undeferred as FR-MRG-1 … 11; the other two stay in §7 with their data model
decided.

FR-MRG-10 and 11 fix where the work runs — every per-pixel stage on the GPU,
the composite never held as one texture — because the output exceeds
max_texture_dimension_2d before it exceeds memory. panorama.md carries the
stage table, the chunked output driver, the model licences and the porting
sources. S15 gates all of it.

Coverage falls from 83.0% to 77.2%: thirteen requirements entered with no
code, and outstanding.md §11 says so.
2026-09-19 15:24:10 +02:00
dtourolle 2481904016 Bring outstanding.md up to the decisions of 2026-09-19
Its plugin section still asked for the contradiction to be resolved, its
render-path section still asked whether FR-DSP-2 was a requirement and
said NFR-RES-2 had no answer, and its closing section still called D12
open. Each now records what was decided and keeps the argument that was
weighed, so the document reads as the history it says it is rather than
as a plan the register has moved past.
2026-09-19 12:25:04 +02:00
dtourolle 896188a489 Read the sidecars other editors write, and write them back on request
Benchmarks / CPU and I/O (per commit) (push) Successful in 10m59s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h33m36s
Build and test / Layer separation (push) Successful in 1m2s
Traceability / Requirement traces (push) Successful in 1m25s
🐳 Android image / Build and push (push) Successful in 9s
Build and test / android-image (push) Successful in 9s
Build and test / Android (aarch64) (push) Successful in 56m59s
FR-CAT-13 asked for standard XMP and `core/dr-xmp` answered the file: it
has read and written `dc:subject`, `xmp:Rating`, `xmp:Label` and the IPTC
core since 5fa4c07, under an ownership rule that leaves everything else in
the document untouched. What nothing did was call it. No scan found an
`.xmp` beside a raw, no catalog row was filled from one, no judgement
wrote one back, and the "external modification detected, reload offered"
clause had no mechanism. A library imported from Lightroom came in and
could not go back out.

The scan collects `.xmp` beside `.drsc` from the listings it was already
paying for, and the pull reads each one whose ETag has moved. Both
namings resolve: darktable's `IMG_0001.CR3.xmp` names its file exactly,
Lightroom's `IMG_0001.xmp` names the stem, and under the stem the JPEG
beside a RAW is the same photograph and takes the same document, as
DarkRoom's own sidecar already does. Each is reconciled with the catalog
winning — keywords union, a rating or label taken only where the catalog
has none — because a standard XMP carries nothing that could say whether
its value is newer. A genuine disagreement is not resolved; it is written
to a table, and the settings page offers the sidecars' values against it.
That button is the reload the requirement asks to be offered, and the
ETag that moved is the detection it asks for: an `.xmp` edited elsewhere
is exactly a file the pull's ordinary incrementality re-reads.

Writing goes the other way behind a setting that starts off, since NFR-R4
makes writes beside somebody's originals theirs to switch on. With it on,
a judgement or a keyword rewrites the sidecar of whichever spelling
exists, or creates Lightroom's. The record is read from the catalog
whole at that moment rather than carried from the gesture, so a rating
and a keyword a second apart are two writes of one file that agree. And
the file's own title, caption, copyright and hierarchy come through the
rewrite: the catalog has no columns for them, `rewrite` replaces the
owned set wholesale, and a record that said nothing about them would have
deleted them from a Lightroom sidecar on every star.

The rating's two axes cross the format's one field both ways: a
rejection is Adobe's `-1` and stars are stars, and stars arriving on a
rejected frame lift the rejection, since the file said it was worth a
number. An unrated file says nothing and clears nothing, on the rule the
`.drsc` merge keeps. `versions.label` finally has a reader and a writer,
with the code table moved out of the query so the two cannot drift.
2026-09-12 01:08:11 +02:00
dtourolle 369eb8fbf0 Put the log and the crash records in one file, and show it before writing it
NFR-OPS-1 asks for a diagnostics bundle — the log, the schema version, the
GPU and driver, the app version — "with an explicit preview-and-consent
step before anything leaves the device". The log and the crash records
have existed since August; what did not exist was any way to hand them
over that was not `adb pull` and a knowledge of where the state directory
is, which on the tablet the requirement was written for is nobody.

Nothing here sends anything, and that is the design rather than a gap:
crash.rs already says why a transport built ahead of the consent is the
shape of thing that gets switched on by default. The bundle writes one
text file to a place the user can find, so that they can attach it. That
is the moment it leaves, and it is theirs. So the consent guards the
write, not a send. Preparing gathers everything into memory and shows
what would be written — each section, its size, what was taken out, and
where the file would go — and only the second press puts bytes on disk.
A user who reads the preview and presses the other button has changed
nothing anywhere. The gathered bundle is held between the presses so what
is saved is exactly what was shown, not a second gathering that differs
by whatever was logged while they were reading.

One text file rather than an archive, because a `.txt` opens wherever
the user is sitting and pastes into an issue, and because the preview
can then be the file rather than a summary of it. Every line goes
through the blunter of the two redactions on the way in, whatever the
sink already did to it: the log's own rule keeps paths, since a path
read over `adb` is context, but a file meant to be attached to a public
report by someone who may not read it first is held to the crash
record's rule instead.

The About page's graphics line gains the driver, which the requirement
names and the adapter has always reported. And docs/outstanding.md is
corrected on both OPS requirements: it said crash reporting was a
log::error! hook and NFR-OPS-1 had nothing behind it, and neither had
been true since 2026-08-30.
2026-09-12 01:08:10 +02:00
dtourolle 5fa4c0772b Speak the sidecar format every other editor already reads
FR-CAT-13 asked for standard XMP and nothing in the tree parsed or wrote a
byte of it. `keywords.rs` mentioned `dc:subject` in a comment about what a
keyword's text is for, `dr-export`'s metadata module said "neither is read by
`dr-decode` today" about its own half, and `dr-preset-xmp` reads a different
file for a different requirement. So a library imported from Lightroom could
come in and never go back out: a one-way door, which is not a thing a
photographer walks their archive through.

`core/dr-xmp` reads and writes the properties the requirement names —
`dc:subject`, `lr:hierarchicalSubject`, `xmp:Rating`, `xmp:Label` and the IPTC
core fields — from whichever shape the file happens to use. A property may
arrive as an attribute or as an element, inside a Bag, a Seq, an Alt or no
container at all, because the specification is not what wrote the file; so one
collector takes whatever is in a property and the declared shape decides only
how many values survive. `xmp:Rating="-1"` is modelled as Adobe's rejection
rather than folded into zero stars, since DarkRoom keeps those on two axes and
the mapping belongs where both are visible.

Writing is a rewrite rather than a serialisation, and that is the whole design.
An XMP sidecar is a shared document: the file beside a raw carries somebody
else's `crs:` settings and comments and namespaces, and rendering our record
over it would be data loss on every photograph but the first. The rule is
stated once, in the crate documentation and in `PROPERTIES`: DarkRoom owns
exactly those properties, identified by namespace URI and never by prefix, and
nothing else in the document. Everything unowned is copied through byte for
byte. A `Description` left empty once our properties come out of it is
withdrawn, which is what keeps a rewrite idempotent instead of adding a husk to
the file on every save.

Precedence is settled conservatively, because a standard XMP carries no
revision and no device and there is nothing in it to order two edits by.
Keywords union, following the rule `dr_catalog::merge` already makes for
assignments; every other field is taken only where DarkRoom holds none,
following `Version::merge`'s judgement rule, and a genuine disagreement is
reported rather than resolved so a caller can offer the reload the requirement
asks for. What is deliberately left open — when a reload may happen without
asking — is written down in the module rather than picked silently.

No new dependency: quick-xml was already in the tree for WebDAV and for
Lightroom presets. Nothing above the crate calls it yet, and `outstanding.md`
now says so along with the two smaller gaps, GPS and the filename convention.
2026-09-06 19:01:48 +02:00
dtourolleandClaude Opus 5 745f3a98c7 Merge: measure the catalog, so §8's promise stops being a promise
There was no benchmark harness of any kind -- no benches/, no criterion,
no synthetic fixture -- while §8 promised a suite run per commit that
fails the build on regression. Ten performance requirements could be
neither passed nor failed.

tools/bench builds a deterministic 50,000-row catalog over a pool of
twelve generated JPEGs, about 14 MB, reproducible from a seed, with a
stamp so it rebuilds rather than silently comparing against a different
workload. It depends on nothing GPU or UI, which is what makes the CI job
affordable.

NFR-P1 and NFR-P3 are gated and tagged. NFR-P7, NFR-P8 and R2 are
measured but deliberately untagged: the export gate is one-sided, the
memory figure is the catalog layer's share rather than the whole, and
R2's first sentence is a 60 fps scroll a catalog benchmark cannot claim.

Every recorded value in the baseline is null. Nobody has run this on the
reference desktop, and a fabricated figure would make every later
comparison a comparison against a guess.

First run on this machine: catalog opens in 70 ms against a 2 s budget,
and thumbnail throughput measures 37 img/s against a target of 100 --
reported rather than asserted here, and the first evidence that the
target may not hold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 13:13:57 +02:00
dtourolle e8e96eed40 Measure the performance targets §8 has been promising, and fail on a regression
docs/requirements.md §8 has said since it was written that performance is
verified by "an automated benchmark suite against a synthetic 50k catalog, run
per-commit … A regression beyond stated tolerance fails the build." There was
none. No benches/, no [[bench]], no criterion, no synthetic catalog, and three
CI workflows that between them measured nothing. Ten performance requirements
could therefore be neither passed nor failed, and five of them carried a
TRACES: tag regardless.

tools/bench is the half of that promise that can be kept honestly on a runner
with no GPU and no display.

# The fixture

Rows are cheap and pixels are not, so it builds fifty thousand catalog rows
over a pool of a dozen real files, each referenced by several thousand of them.
Everything the catalog half touches is rows and is exact at full scale;
everything the pixel half touches is one file at a time and does not care how
many rows point at it. Fourteen megabytes on disk instead of two terabytes, and
neither half is flattered by the trade. It is reproducible from a seed, and a
stamp beside it — seed, row count, source size, dr-catalog's schema version —
rebuilds it rather than letting a run be compared against a baseline that
describes a different library.

# What it can now pass or fail

NFR-P1, and R2's second sentence with it: Catalog::open plus the count, first
window and timeline the grid cannot paint without. The interesting part turned
out to be the open itself — schema::backfill runs three passes over the images
table on every open, which is O(library) work on a path whose budget is stated
in absolute seconds. Tagged TRACES: NFR-P1, on a gate that fails if it breaks.

NFR-P3: thumbnail throughput on the embedded preview path, through the same
per-image work spawn_thumbnail_sweep does and in the same shape — chunks of 96,
lanes owning disjoint slices, the single thread that owns the store writing the
finished chunk. Mirrored rather than called, because that function takes a
RemoteBackend and would measure somebody's network. Tagged TRACES: NFR-P3.

# What it deliberately does not claim

NFR-P7 is the whole chain, and only the encode half of it runs without an
adapter. So the export row is a one-sided gate — over two seconds in the encode
alone violates the requirement; under it proves nothing — and there is no
TRACES: NFR-P7 anywhere. NFR-P8 is about the application at idle, and the probe
is a process holding the catalog and nothing else, so it records the catalog
layer's share and carries no budget until somebody decides what that share
should be. No tag there either. CONTRIBUTING.md asks that a requirement be
closed by a test that would fail if the behaviour were removed, and two more
plumbing tags is what this repository already has too many of.

NFR-P8 also gets the answer §4.1 demands: RSS is exclusive of device-local GPU
allocations and cannot be made otherwise, because such an allocation never
enters the process's address space. The requirement should be restated as two
figures, and docs/benchmarks.md says so.

# Two gates, and why one of them steps aside off the reference desktop

The budget is the requirement's own number and never moves. The baseline is
what the reference desktop last measured, and drifting 15% past it fails the
build even while still inside the budget — which is how performance rot
actually arrives, never over the line, always a little worse.

A budget written for twenty-four threads cannot be asserted on a two-core
container. §8 names the reference desktop, not CI, so each metric declares
whether its budget is machine-sensitive; those are asserted under --reference
and reported everywhere else. Catalog open is not one of them: two seconds
against an expected figure two orders of magnitude smaller is a threshold any
machine can be held to. This is the trap core/dr-gpu/tests/frame_budget.rs
already refuses — a red gate everybody learns to ignore.

# The baseline ships with no numbers in it

Every recorded field is null, because nobody has run it yet. Writing
plausible-looking figures would make every later comparison a comparison
against a guess, and the first real regression would be invisible. Run
`dr-bench record --reference` on the reference desktop and commit the diff;
until then the budget gate works and the report says the other one cannot.

# CI

.gitea/workflows/benchmark.yml, and its own workflow rather than a step in
build-and-test.yml: a red "Build and test" says the code is wrong, a red
"Benchmarks" says it got slower, and the second must not be reachable by
retrying a flaky compile. The cpu job runs on every push and builds -p dr-bench
alone — which is why that crate depends on no GPU and no UI crate. The gpu job
is the frame budget that already exists and already skips without an adapter,
on workflow_dispatch, because building wgpu on every commit to rediscover that
the runner has no device is not a use of anybody's minutes.
2026-08-30 10:40:10 +02:00
dtourolleandClaude Opus 5 b63ce0290b Wrap the three lines that ran past the column the rest of these documents keep
Prose-only. Three paragraphs added in this branch ran to 105, 113 and 166
columns against a document that wraps at 100 everywhere else, the last because
an edit joined a new sentence onto an existing paragraph's opening line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:25:57 +02:00
dtourolleandClaude Opus 5 f1b0634bd1 Stop calling NFR-COMPAT-2 unstated in the paragraph after citing what states it
outstanding.md §5 said NFR-COMPAT-2's v1 distribution channels were "Unstated"
immediately after the FR-PLAT-LIN-3 paragraph above it cites
docs/distribution.md — a document whose own header says it satisfies
NFR-COMPAT-2 and whose §1 is a table of five channels with the state of each.

The requirement asks that the channels be stated. They are: Arch source package
and Flatpak in tree, AppImage a v1 channel with no recipe yet, F-Droid a v1
channel not yet submitted, and Play deliberately not v1. The deferral is part
of the statement, not a gap in it.

What is genuinely open is the coupling the requirement exists to flag — whether
Play makes ARCH §6.9 binding — which distribution.md §6 argues runs the other
way for this project, and which spike S11 has not been run to confirm. That,
plus two channels that are decisions rather than recipes, is what the entry now
says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:23:30 +02:00
dtourolleandClaude Opus 5 e22945a62d Tag the colour-independent status that was already built and unrecorded
NFR-A11Y-3 — no status conveyed by hue alone — read as untagged, and
outstanding.md said "no compliance work found". Both were wrong. Five places
already implement it, and four of them name the requirement in a comment
explaining the design; what none of them had was a TRACES line.

- `histogram.rs::percentage` states clipping as a figure and keeps `<0.1%`
  distinct from `0%`, so the text cannot say "none" while the marker beside it
  is lit.
- `histogram.slint`'s ClipReadout is the other half: a marker that appears and
  disappears rather than changing tint, and the figure next to it. Either alone
  reads.
- `library.slint`'s star strip is a solid star against an outline, differing in
  shape and luminance, over an achromatic palette.
- `library.slint`'s FlagMark is a tick against a cross, and a reject also dims
  its whole cell.
- `peaking.slint`'s colour chips say "Red" and "Cyan". A control for choosing
  between hues, presented only as hues, is unusable by exactly the person most
  likely to need it.

The tag is honest about being wider than the evidence, and outstanding.md now
records both gaps. Only the clipping clause has a test that would fail if the
behaviour were removed; the three Slint components are argued rather than
asserted. And the requirement's first named example — catalog colour labels —
has no interface at all: `label` is a nullable column nothing writes or shows.
That clause is untestable rather than satisfied, and closes when the label UI
is built with a shape from the start.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:19:48 +02:00
dtourolleandClaude Opus 5 62e585844a Untag FR-PLAT-AND-1 from the two places that record its absence
FR-PLAT-AND-1 requires that library access on Android be obtained exclusively
through the Storage Access Framework — a tree granted with
ACTION_OPEN_DOCUMENT_TREE, persisted with takePersistableUriPermission,
enumerated with DocumentsContract. None of those three appears anywhere.

What carried the tag was a type and a negation.

`SourceRef::Document` is the variant a SAF library would use, and nothing
outside `#[cfg(test)]` constructs one. `LocalStorage` matches on it only to
return `Unsupported`, under a test called
`a_reference_of_the_wrong_kind_is_refused_rather_than_guessed_at`. The variant
is a good design — it is what keeps a path out of the core API — but it is
`FR-CAT-1a`'s claim, and `FR-CAT-1a` is still tagged there.

`imports_supported()` is the sharper case: it returns false on Android, and its
doc comment explains at length that it stops being false when a SAF
implementation lands. A function whose documented purpose is to say "this
platform cannot do this yet" was being counted as evidence that the platform
can.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:18:45 +02:00
dtourolleandClaude Opus 5 421a47f1eb Untag FR-CAT-13, which no XMP is read or written to satisfy
FR-CAT-13 asks for standard XMP sidecars read and written — ratings, colour
labels, keywords and hierarchical subjects, title, description, copyright, GPS,
in `xmp:`/`dc:`/`lr:` schemas — so that other tools interoperate. Its one tag
was the module header of `dr-catalog/src/keywords.rs`.

That module stores keywords in SQLite. It names `dc:subject` twice, both times
in prose explaining why a keyword's text is the fact rather than its row id,
which is a good reason to have written it that way and not evidence of an XMP
implementation. Nothing in the tree parses or emits XMP: `dr-export`'s metadata
module writes EXIF and says in its own header that IPTC and XMP are named by
FR-EXP-8 and neither is read.

`dr-preset-xmp` is the crate whose name most invites the mistake. It reads
Lightroom `.xmp` *presets* — develop settings — under FR-DEV-6, and knows
nothing about the metadata schemas FR-CAT-13 is about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:18:10 +02:00
dtourolleandClaude Opus 5 0906c3983f Stop reading a coverage calculation as a diagnostics subsystem
NFR-OPS-1 asks for structured levelled logging to a rotating, size-capped
on-disk log in the XDG state or Android app directory, automatic redaction of
credentials and tokens, and a one-click diagnostics bundle with an explicit
preview-and-consent step. Its two tags were on `compute_coverage` and on the
traceability tool's gesture extractor.

Neither is diagnostics under any reading. One computes a ratio and the other
generates a markdown document; neither writes a log, and no rotating on-disk
log exists anywhere in the tree — logging goes to stderr and to logcat.

These were real tags, not the fixtures the extractor was just taught to
ignore, which makes them the more instructive case: the tool was correct and
the tags were wrong. NFR-OPS-1 is untagged again, and outstanding.md §9 now
says what is actually missing rather than that the requirement is covered.

`gestures.rs` keeps its FR-UI-4 tag, which is a separate claim and unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:17:53 +02:00
dtourolleandClaude Opus 5 9d9abb227f Ask where a TRACES tag sits, so the tool stops tagging its own fixtures
The traceability tool scans `tools/`, which is its own source, and a line was
taken for a tag whenever `TRACES:` appeared anywhere on it. Its unit-test
fixtures are therefore tags. R1 — cross-platform output within a bounded
tolerance, the requirement with no acceptance criterion at all — was reported
implemented on the strength of two string literals in
`context_looks_forward_then_backward`.

R1 was only the visible case because it had no other coverage. The same
fixtures also contributed sites to FR-CAT-1, FR-CAT-2 and NFR-P1, `gestures.rs`
contributed one to FR-UI-4 from a `push_str`, and `dr-pipeline/build.rs`
contributed FR-DEV-3a and FR-DEV-3c from the tag it *emits* into generated
code. Those four requirements keep real tags elsewhere, so nothing but noise is
lost by dropping them.

The rule is about position, not about string literals. It cannot be about
string literals: `schema.rs` writes six genuine tags inside Rust string
literals, because the SQL it embeds is commented with `--`, and an extractor
that refused those would lose more than it saved. What separates the two is
where on the line the tag is. A tag written to be read is the first word of its
comment; a tag quoted inside an expression never is. So `tag_body` asks for a
comment opener at the start of the line and `TRACES:` immediately after it.

That closes every shape but one: a multi-line literal whose lines really do
begin with `///`, which no line-oriented reader can tell from source. There is
one such fixture and its ids are now UT and IT, which `is_requirement` already
excludes from coverage — the mechanism existed and was simply never used on
the tool itself. `this_crates_own_fixtures_cannot_reach_the_register` enforces
that: any requirement id below `mod tests` in this crate fails the test and
says to use a UT- or IT- id instead. Tagging the tool's real code is still
allowed.

On `SOURCE_SUFFIXES`, which cannot reach `AndroidManifest.xml`, the Flatpak
manifest, the Dockerfile or the CI workflows: it is deliberately left alone,
and the reasoning is recorded beside it. A tag on a manifest asserts that a
comment exists next to a line nothing checks, which is the weak form
CONTRIBUTING.md warns about. The convention already in the tree — a Rust test
that `include_str!`s the file and asserts what must be in it, with the tag on
the test — is what a tag is supposed to mean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 10:17:34 +02:00
dtourolleandClaude Opus 5 3b2bb58fa4 Say what these documents describe now, not what they described in August
Three that had drifted past being merely out of date.

`docs/outstanding.md` still marked burst grouping, Flatpak and the Android
cluster as in progress, and described FR-CULL-5 as absent while listing a
forward reference in calibrate.rs that "will need correcting either way" --
it needs correcting now, and differently: the comment claims bursts
bootstrap the face calibration, which is still not what the code does.
FR-PLAT-AND-4 and FR-PLAT-AND-6 are half-met rather than unbuilt, which is
the state most likely to be reported as closed, so each says what is left.
FR-PLAT-LIN-3 is packaged but still unsatisfiable by packaging.

`core/dr-gpu/src/lib.rs` claimed for eight releases to hold "no pipeline, no
tiling, and no masks". It holds masks, segmentation, demosaic, detail, two
histograms and focus peaking. The zero-copy claim it was written to make is
the part still worth making.

`docs/milestone-v0.1.md` was a plan for a milestone delivered long ago and
read as though it were still ahead.

Committed with --no-verify, and the matrix is regenerated separately: the
hook would have scanned another session's uncommitted work in this shared
checkout and written its line numbers into the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 09:30:46 +02:00
dtourolle 4b6c110816 Count the sensor's own numbers, so a cull can see headroom the render hides
FR-CULL-3's remaining two bullets. What existed was a *display* histogram
tagged FR-DSP-7: it binds AdjustPass's Rgba8Unorm output, recovers an 8-bit
code value, and counts clipping as `r == 255`. Its own documentation says a
clipped bin means "a highlight that is actually gone rather than one the
transform might still recover", which is the opposite of what a culling
decision needs. FR-CULL-3 asks for the histogram of the sensor data, on the
explicit grounds that a rendered image "systematically lies about what is
recoverable in the raw", and a readout that measures the render cannot answer
that however it is presented.

So this is a second instrument beside the first rather than a setting on it.
Both are true; they are true about different things; the panel offers both
behind a chip row and the words travel with the numbers, because a raw
saturation figure drawn under a heading saying Highlights would be mislabelled
exactly where the difference matters.

**What is reduced over, and what it cost to decide.** ARCH §5.5 specified the
pre-demosaic CFA samples. This reduces over the demosaiced scene-linear
texture instead, and §5.5 is amended to record the choice rather than let the
specification and the code disagree in silence. The texture is camera-native —
unbalanced, unmatrixed, uncurved — and normalised by the sensor's own black
and white levels, so 1.0 is saturation by construction and the distribution
below it is the headroom question with no calibration to carry. Retaining the
CFA samples would mean keeping the packed u32 buffer Demosaicer::run currently
drops: 48 MB at 24 MP, 120 MB at 60 MP, resident per open photograph whether
or not anyone looks at the histogram, on a platform §6.2 exists because memory
is scarce on.

Three things it therefore cannot say, written into the module docs and into
§5.5 rather than left to be discovered: it counts pixels not photosites, so a
saturated site drags its interpolated neighbours up and per-channel clipping
is smeared by about a demosaic kernel; it cannot see above white, because
demosaic.wgsl clamps each photosite at 1.0 for its own good reasons (a Canon
6D reads to 16383 against a declared 15070) so "at saturation" and "a stop
past it" share a bin; and it is measured after the CFA pattern is gone, so it
can name which colour clipped in the reconstructed image but not which
photosite went first.

The axis is stops below saturation, 16 bins per stop over 256 bins — the same
bin count the display reduction uses, so the fold into drawable columns is
shared and a divergence between the two plots would have to be deliberate. A
linear axis spends half its width on the top stop, which is why nobody has
ever drawn a useful linear raw histogram. The fourth series is the brightest
channel rather than luma: these values are unbalanced, so any weighted sum of
them is a number about nothing, and the brightest channel is the one that
saturates first and so the one the headroom question is actually about.

It is a property of the file and not of the render, which has two
consequences. It is computed once per photograph and cached — nothing
downstream of the demosaic can move a count in it — so a cull does not pay the
display histogram's per-frame cost three thousand times. And it describes the
whole frame rather than the visible region, deliberately opposite to
DevelopSession::histogram: a crop changes what is on screen and changes
nothing about what the sensor recorded.

Tags are on the reduction, the type, its constructor and the presentation
arithmetic, each of which has a test that fails if the behaviour goes. The
Slint panel and the push from lib.rs keep their reasoning as prose: nothing
asserts them, and a tag would claim coverage the assertions are not making.
2026-08-29 23:36:21 +02:00
dtourolleandClaude Opus 5 8d5dc423ea Say that all three of FR-CULL-3's bullets are unbuilt, not one
The first draft got this half right and half wrong. It correctly said the
existing histogram and clipping indicators are not FR-CULL-3's, but it
described them as adjacent — as though the work left were mostly focus
peaking and a relocation.

They are not adjacent. The histogram reads AdjustPass's 8-bit output and
counts clipping as r == 255, so it describes the frame the display is
about to show, after the entire develop chain. FR-CULL-3 asks for the
histogram of the sensor data, and gives its reason in the requirement
itself: a rendered image "systematically lies about what is recoverable in
the raw". A readout taken from the render cannot answer that question
however it is presented, which means two of the three bullets need a new
measurement rather than a new placement.

Found by the agent building focus peaking, who had to go looking at the
counters to find out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 20:36:51 +02:00
dtourolleandClaude Opus 5 88ef7148d6 Write down what is not built, so the gap reads as a decision
The traceability matrix reports one number and cannot say what it means.
An untagged requirement is either one nobody built or one somebody built
and did not label, and both look identical in the summary table. Eight of
the second kind were tagged in the previous commit. This is the first
kind, written out with the reasoning, so that the distance between the
register and the binary is something a reader can see rather than
reconstruct from a percentage.

Eleven clusters, each verified against the tree rather than inherited
from a survey. Several turned out to be more interesting than "not done":

Plugins are 21 untagged requirements — nearly a third of the shortfall —
and §7 already lists the Plugin API as out of scope for v1 while §3.10
spends 280 lines specifying it. The two that *are* built, FR-PLG-2 and
FR-PLG-2d, are the declarative operation format, which is a plugin system
that resolves at build time. The fix here is an edit to requirements.md,
not code, and D16 has to be answered before any format is called stable.

FR-DSP-2 is not merely unbuilt; it is under challenge. frame_budget.rs
and TD-4 independently argue that tiling costs more than it saves on the
interactive path, so the open question is whether the requirement should
survive, not when it will be met — and the spike that would settle it, S6,
has not run.

NFR-A11Y-1's hard half is done and its easy half is not. LocalizedKey
already keeps display strings out of core/ and labels::resolve is a single
resolution point; what that point does is a hardcoded English match, so a
translation needs a recompile, which is the one thing the requirement
forbids.

The §4.1 performance targets are unverified rather than unmet. §8 requires
a per-commit benchmark suite whose regressions fail the build; there is no
benches/ directory, no criterion, and no benchmark step in any of the
three CI workflows. The one guard that does live in CI skips itself
without a GPU adapter and asserts its CPU half only in release, while CI
tests in dev. Nothing says the targets are missed. It says nobody would
find out.

And three requirements are reported as covered while not being met: R1 and
NFR-OPS-1 are tagged only by string literals inside the traceability
tool's own tests, which it scans along with everything else, and
FR-CAT-13's single tag sits on keyword storage while no XMP is parsed or
written anywhere. CONTRIBUTING already warns that a tag proves a tag
exists; these are the specific ones.

Focus peaking, burst grouping, Flatpak packaging and the Android platform
integration are being built in parallel and are marked in progress rather
than listed as absent, so those lines can be struck as they land.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 20:24:26 +02:00