d0ebc9f571b5ffa915b9e89bf5ce8dcb391fcffc
584
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
465f5a7ffa |
Keep the log after the session that produced it
Everything this application knew about a failure went to stderr on the desktop and to logcat on Android, and both are gone the moment the terminal closes or the ring buffer wraps. That is fine when the person debugging is sitting at the machine. It is useless for the case NFR-OPS-1 actually describes, and the one Android makes normal: somebody reproduces a bug on a tablet, and then sends us a file. A large amount of Android behaviour has never run on a device — image intents read over JNI, an ExportProvider, a class loaded through the activity's class loader, memory-pressure eviction, lost-root recovery — and the single most likely failure of the lot, the activity's loader not resolving our classes from android_main's thread, produces one line that scrolls past. That line is now in a file, with the thread that emitted it named beside it. dr_plat::state answers "where does this platform keep state for this app": $XDG_STATE_HOME/darkroom on Linux, and on Android whatever the entry point declares. Separate from configuration and from the catalog for the reason XDG separates them — state is the thing nobody backs up and the user may delete without consequence. dr_plat::diagnostics is the sink. Two files of 4 MiB, so the worst case is a number rather than a discovery on a full phone; one line per write with no BufWriter anywhere, because on Android processes are killed rather than ended and a buffered log loses exactly the line it was kept for; and redaction applied at the sink rather than at the call sites, since a rule every author has to remember is not a rule. It tees the platform's own logger rather than replacing it, so logcat is unchanged — losing that while debugging would have made this a downgrade. Android logs to external_data_path, not internal. Both are app-private and both survive backgrounding; what separates them is that /data/data/<pkg>/files needs run-as against a debuggable build to read and /sdcard/Android/data/<pkg>/files is a plain adb pull from any build. A log nobody can retrieve is not a diagnostic. The consequence is that anyone holding the tablet can read it, which is why the redaction is where it is, and why configuration stays on internal_data_path. What is redacted is what NFR-SEC-2 and NFR-OPS-1 name: credentials and tokens, found by the keyword that nearly always sits next to them, plus the two forms that carry one with no keyword at all — an Authorization scheme and a URL's userinfo. What is deliberately not redacted is filesystem paths and the names of the user's photographs. They are in neither requirement's list, and "failed to decode <redacted>" is not a diagnostic; the preview-and-consent step NFR-OPS-1 asks for governs those better than scrubbing would, because it lets the user look. The over-redaction failure is tested as carefully as the under-redaction one. A scrubber that eats "using basic sRGB as the fallback" makes a log useless without ever being caught. |
||
|
|
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. |
||
|
|
1ff52102b6 |
Drop the anchor bookkeeping the double tap took with it
`previous_anchor` existed for one gesture: a double tap in selection mode took the range from where selecting began, and both taps had already moved the anchor onto the cell being tapped, so the origin the user meant had to be remembered separately. That gesture is gone — "Select to…" says what it is about to do instead of hiding a forty-image range behind a thing a hand does by accident — and what is left is a field that four places write, `PressUndo` carries, `cancel_press` restores, and nothing at all reads. `apply_press` is `select_row`'s only call now that there is no anchor to remember, so the wrapper goes with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8848bbcff3 |
Stop drawing a hover ring on a device that cannot hover
A cell drew a 1px ring while the pointer was over it, in the same colour as the 2px ring that means "selected". On a tablet there is no pointer, and `has-hover` is not the harmless no-op that implies: Slint raises it on a touch press and lowers it on the `Exit` that normally follows the release — but the release that ends a pinch carries no `Exit` at all, and neither does a finger lifting while a second one is still down. So resizing the thumbnails, which is a pinch, left a ring around whichever cell each finger had come down on. The grid then showed boxes around photographs that were not selected, a pixel thinner than the ones that were, with nothing to tell them apart. Hover chrome has no meaning once there has been a finger, so it is not drawn: the same `touched` latch the rating strip already keys off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
35954dfa1d |
Write a panic down where it can still be read, with nothing in it that names the user
NFR-OPS-2 is two sentences — local crash capture always, upload only on explicit opt-in — and what existed was one `log::error!` in the Android entry point and nothing at all on desktop. So a panic on desktop went to stderr and died with the terminal, and a panic on Android went to a logcat ring buffer that is gone long before anyone reports anything. What the user saw either way was a job that stopped or a control that went dead, with nothing to send. That matters more here than it would in most applications, because NFR-ARCH-4 says no worker error may panic the process and the code is written that way: errors are typed and attached to the image or job they belong to. A panic is therefore by construction a bug — an invariant this codebase believed and got wrong — and it was the one class of failure with no trace. `dr_plat::crash` writes a record to the XDG state directory: version, time, os, arch, thread, panic location, message, backtrace. In dr-plat rather than in either entry point because "where does this platform let an application keep state" is a platform question, and Android's answer is neither XDG nor `temp_dir` — `set_state_dir` takes it from `internal_data_path`, the same place `dr_sync::account::set_data_dir` gets its answer. The hook resolves the directory when it fires rather than when it is installed, which is what lets it go in before everything else and cover the startup it would otherwise miss. **The content rule is the substance of this, not the plumbing.** NFR-SEC-2 forbids credentials in logs and plain files; the same reasoning applies with more force to what this application is actually about, because a user's library is private and so is its shape. `/home/anna/Photos/2019 Divorce/` says something about a person, and a crash record is exactly the file someone attaches to a bug report while trying to be helpful. So `redact` runs over the message *and* the backtrace, and is deliberately blunt: anything containing a slash goes, `content://` and `primary:DCIM/...` included, since SAF names a library just as precisely as a path does; anything beside a word like `password` goes; a long opaque run with letters and digits in it goes, which is the shape of an app password nobody labelled. The one exception is a `.rs` path, which keeps its basename — a backtrace with no filenames is close to useless and `library.rs:1270` says nothing about anybody. Over-redaction costs legibility. Under-redaction costs a user something they cannot take back. Those are not comparable, so the boundary is not the place to be clever. NFR-SEC-5 — face data never in a crash report, under any configuration — is met structurally rather than by filtering: this module reads no catalog, opens no image, touches no account. A record is assembled from the panic hook's own arguments and `std::env::consts`, and there is no code path from here to an embedding. The message length cap is the backstop for a payload some other module formatted something large into. stderr is the one surface that still sees the message unredacted, deliberately: the previous hook is chained rather than replaced, so a developer watching a terminal does not lose the panic because the application started writing files. It is ephemeral, local, and never attached to a report. **No upload path, and not half of one** — no endpoint, no queue, no "send this later" flag. Opt-in upload needs a server to receive it and a consent flow stating what leaves the device (NFR-SEC-4, and the preview-and-consent step NFR-OPS-1 requires of the diagnostics bundle). Neither exists, and a transport built ahead of its consent is the shape of thing that later gets switched on by default. Leaves NFR-OPS-1 cheaper by three things it will want unchanged: `state_dir` (the log belongs at `state_dir()/log` beside `crash/`, so the diagnostics bundle has one directory to collect), `redact` (NFR-OPS-1's "automatic redaction of credentials and tokens" is this function), and `prune` (a size-capped rotation is this, counting bytes instead of files). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8eeb9ba0f6 |
Offer the backup, and then the rebuild, when the index turns out to be damaged
NFR-R6 asks for an integrity check at startup and two offers behind it, and none of it existed. `PRAGMA integrity_check` appeared nowhere in the tree, `Catalog::open` was `open` → `configure` → `migrate` → `backfill` and nothing else, and corruption therefore surfaced as whatever rusqlite error the first unlucky query happened to produce — "database disk image is malformed" attached to a thumbnail refresh, elided into a 34px banner, over an empty grid saying "No images found · Check the library folder". Two messages that disagreed, and no way forward but deleting catalog.sqlite by hand. The property that makes the second offer real was already here and load- bearing: the catalog is an index, not a source of truth, rebuildable from sources plus sidecars (invariant §5.2.4, cited by schema.rs, trash.rs and lib.rs). And sync.rs already knew how to take a coherent snapshot of a WAL database. What was missing was the check, the type, and the conversation. Four pieces: **The type.** `CatalogError::Corrupt`, and — the part that makes it worth having — a hand-written `From<rusqlite::Error>` that classifies rather than wraps. `SQLITE_CORRUPT` and `SQLITE_NOTADB` become `Corrupt` wherever they arise, so a background job that trips over the damage first reports the same thing the startup check would have. `SQLITE_IOERR` and `SQLITE_BUSY` deliberately do not: a dropped network mount is a different problem, and telling someone to rebuild their index would be a wrong answer delivered confidently. **The check.** `Catalog::open_verified`, `quick_check` before the open rather than after, because opening runs migrations and a damaged catalog with an intact header would otherwise have structure rewritten on top of structure that is already wrong. Bound to `open_verified` and not to `open`: the check reads every page, which is affordable once at startup where a user can answer a question, and not affordable on the dozens of opens a session's background tasks make. **The backup.** NFR-R2's second clause, taken between `configure` and `migrate` in `Catalog::open`. A migration is the one routine operation that rewrites table structure, so it is the likeliest way this file becomes unreadable, and it is the last moment the pre-migration state exists to be copied. Three generations, through SQLite's backup API after a TRUNCATE checkpoint — never `fs::copy`, which on a WAL database backs up a state older than the catalog and possibly torn. A failure to take the copy is logged, not raised: a full disk must not be what makes a library unopenable. **The conversation.** The first line of the dialogue is that the photographs and the edits are safe, before the diagnosis, because that is the question the user is actually asking. Then the two offers, which are *not* interchangeable and are not presented as if they were: a restore keeps collections, and a rebuild cannot, because a manual collection is a set of images assembled by hand and nothing in the filesystem records it (docs/catalog.md §8.1). The labels say so, and the rebuild does not take the affirmative styling while a restore is on the table. One thing that is a fix rather than a feature: `show_catalog_now` now gates the scan. `Catalog::open` succeeds on a file whose header survived, so the scan that used to start immediately afterwards would write folder ETags and image rows into damaged pages in the seconds while the user was still reading the question — turning a file that had a backup into one where the backup is the only copy left. Restore also deletes the damaged catalog's `-wal` and `-shm`. That step is easy to leave out and fatal to leave out: a journal belonging to the old file, sitting beside the new one under the same name, is replayed into it on the next open. That is not a restore, it is a fresh corruption with the evidence gone. Tested by corrupting a fixture catalog — 500 images and a collection, then every page past the second overwritten — and driving both branches. The restore is asserted on the collection, because a collection is precisely what distinguishes the two paths; the rebuild on the damaged file being kept and the next open producing an empty catalog at the current schema. Plus the `SQLITE_NOTADB` presentation, a damaged backup being refused rather than installed, and a v1 catalog whose pre-migration backup comes back reading v1 rather than v11. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
258aae71d6 |
Regenerate the matrix without the tool's own fixtures
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
677b047235 |
Merge: stop the matrix counting the tool's own test fixtures as coverage
A tag was any line containing the string, so the traceability tool scanned tools/ -- its own source -- and its test fixtures became coverage. R1 and NFR-OPS-1 were tagged on nothing but that. Four requirements had sites that were never implementations: the same fixtures also fed FR-CAT-1, FR-CAT-2 and NFR-P1, gestures.rs fed FR-UI-4 from a push_str, and build.rs fed FR-DEV-3a and FR-DEV-3c from the tag it emits into generated code. A tag is now a comment whose first word is TRACES:. Excluding cfg(test) was rejected because tags on tests are this project's recommended practice, and keying on string literals was impossible because schema.rs carries six genuine tags inside Rust strings -- the SQL it embeds is commented with --. Four tags removed as unearned: FR-CAT-13 (no XMP is parsed or written anywhere), FR-PLAT-AND-1 (SourceRef::Document is constructed only in test modules, and the second tag sat on imports_supported, which documents the absence). Two added as earned: NFR-A11Y-3, which was built and untagged, and FR-PLG-8's preservation clause. Four requirements amended rather than built, each with its reasoning: R5's tiling clauses struck, NFR-P11 naming the state that must survive, FR-RAW-2's mechanism reworded to bytes-in, FR-EXP-1's AVIF and JPEG XL stated as post-v1. Coverage falls 122 to 120 of 179, and means more than it did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
53b04dc561 |
Let the accessibility test see a callback that takes an argument
`sets()` recognised `name:` and `name =>` and not `name(arg) =>`, which is how Slint writes a callback handler with a parameter. There is exactly one of those among the properties asserted — `accessible-action-set-value`, the action a screen reader uses to type a number into a slider — so the test reported `SliderTrack` as missing the action it declares four lines above. A false alarm rather than a false pass, and the less dangerous of the two. It is still worth fixing rather than dropping the assertion: set-value is the action that makes a slider reachable without dragging, which is most of what the actions were added for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bddd30fba2 |
Measure the chrome's contrast, and say why font scaling is not a multiplier
NFR-A11Y-2 has three clauses and this branch closes one of them. The other two — WCAG AA contrast on non-canvas UI, and platform font scaling honoured without clipping — are now measured and reasoned about rather than left as two sentences in the requirements register that nobody had checked. Contrast is measured, not estimated. Every ink against every surface it is drawn on, by the WCAG 2 formula, and the result is narrower and more specific than "the palette is dark": `ink` and `warn-ink` pass everywhere, the inverted cases on the near-white fills are the best-contrasting text in the application at 18.6, and exactly two tokens fail. `ink-faint` reaches 4.5:1 on no surface at all — 3.97 at its best — and it is the ink for every caption, every section name and every count. `rule` reaches 3:1 on none either, and it is the border of every button, field and panel, so an unfilled secondary button is a 1.4:1 outline on a 1.2:1 ground. Neither is an oversight, which is why they belong here rather than in a bug list. They fall out of the palette's own argument: a bright surround biases how a photograph is judged and hue is banned outright, so all the signalling is luminance and the luminance is deliberately spent on the image. "Text you are meant to skip" and "text everyone can read" are in real tension. The fix is a re-derived ink scale and a stronger rule, both of which change how the application looks beside a photograph — a screenshot and an opinion, not a patch, and not something to do blind. Font scaling is the more interesting entry because the obvious fix is wrong. It is not a multiplier on the type sizes: the layout rests on constants that are not derived from them — control-height, touch-target, rail-entry-height, panel-width, and a dozen fixed heights written at their call sites — and scaling the type alone clips against every one, silently, because Slint elides rather than errors. The colour mixer is the sharpest case, thirty-six controls in a 360px column sized so a track and a swatch and a readout share a line. So the entry sets out the four pieces in dependency order, and notes that the first is nearly built already: `live-style` makes `build.rs` emit `in-out` theme tokens that Rust writes at startup, which is exactly the mechanism a scale factor needs. Both entries carry the falsifiable condition this document asks for, and the contrast table is the baseline a later measurement compares against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
46706fe622 |
Let the tool rail be pressed, not only read
The rail is the develop view's primary navigation and reached the platform as four pieces of static text. The labels got through — a `Text` announces itself — so a screen reader could read "Photo, Crop, Local, Repair" and had no way to learn that any of them could be pressed, or which one was currently held. The one control that decides what a click on the photograph does was a caption. Each entry now declares itself a checkable button carrying the tool's own word, with the held state reported rather than left to the fill. Checkable is unconditional here, unlike `Button`'s: a rail entry is always a held-or-not state, so an unheld one should say "not pressed" rather than pass for an ordinary button. The action repeats the click handler's expression rather than calling it. A `TouchArea`'s `clicked` is raised by the pointer and cannot be raised from a binding, so the alternative is a function wrapping two lines — and the duplicated ternary sits four lines below the original where the two cannot drift out of sight of each other. Worth recording for the next control like this: `accessible-*` on an element inside a `for` does work, despite the accessibility pass skipping repeated elements. `process_repeater_components` runs first and moves the bindings into a real component whose root is not repeated; what the later pass skips is the empty placeholder left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
80298403f7 |
Name the launch screen's entries, and let its folder rows be pressed
A heading above a text field names it on screen and names nothing to the platform: Slint associates the two only if something says so, and nothing did. So the four entries a user signs in through — server, username, app password, folder — reached AT-SPI as unnamed boxes with a separate piece of static text floating above each. The word is now written twice, deliberately, and the second copy is the one attached to the control being typed into. FolderRow is the more consequential half. It is a Rectangle with a TouchArea over it, which is a button to the user and a decorated box to the platform — its label got through, because the `Value` inside is a Text, while the fact that the row could be entered at all did not. The folder picker was therefore readable and not navigable, which for the screen that chooses where the whole library lives is the difference between using the application and not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
7409cb7767 |
Make the launch screen translatable, and say how the rest follows
`@tr(` appeared zero times in 14,482 lines of markup. Every string the user
reads was a literal, so NFR-A11Y-1 was not partly done or done badly — there
was nothing to extract and nothing a translator could have been given.
The mechanism turns out to cost almost nothing, and the reason is worth
stating because it decides the order of the work: a string written `@tr("Sign
in")` is already correct in a build with no translation at all. Slint's
`translate()` formats the original and hands it back when neither delivery
path is active, so a converted string and a literal are the same string until
someone writes a `.po`. That means the interface can be converted a screen at
a time rather than in one 14,000-line commit that nobody can review, and every
intermediate state is shippable.
So the delivery half is wired and left inert. `build.rs` asks for bundled
translations only once `lang/<lang>/LC_MESSAGES/dr-ui.po` exists — the first
catalogue anyone commits turns it on with no build-system change, and until
then a checkout with no `lang/` builds exactly as it did. Bundling rather than
the `gettext` feature because of Android: under ARCH §6.9's storage model
there is no path a `.mo` could sit at that the app can reach, and no C library
to link it against. The extraction command and the one flag that must not be
passed to it are recorded in the module docs.
The launch screen is converted whole: 32 calls covering every heading, button,
caption and placeholder. It goes first because it is the screen a user cannot
get past — an unreadable preferences page can be ignored, an unreadable sign-in
cannot. Four kinds of literal are deliberately left alone and the file says
which: the product name, example values whose shape is the message, `..`, and
the path separator.
Two things this leaves open, recorded rather than papered over. The headings
carry their own capitals, because `PanelHeading` draws what it is handed — so
a translator supplies "SERVEUR", not "Serveur", and styling in the string is a
real cost now paid rather than a surprise later. And `@tr()` is markup only:
the operation and parameter labels NFR-A11Y-1 names explicitly resolve in
`labels.rs`, in Rust, because the core may not depend on a localisation
library — those need a second mechanism, and it is not built.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
6ec6c5cbd6 |
Count the tags this rule would have cost, rather than guessing at it
The module doc said an extractor keyed on string literals would "lose six genuine tags to save two false ones". The six is right — `schema.rs` carries that many `-- TRACES:` lines inside Rust literals — but the two undercounted the false ones, which were R1 twice, FR-CAT-1 twice, FR-CAT-2, NFR-P1, one in `gestures.rs` and two emitted by `dr-pipeline/build.rs`. The comparison it was drawing does not need a number on that side to hold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
8865743db6 |
Split FR-EXP-1, so its tag stops standing for a deferral as well
FR-EXP-1 listed four things in one sentence: JPEG, PNG, TIFF at both depths, and AVIF or JPEG XL. Three of them are built, encode, embed an ICC profile, and are covered by tests that walk every offered format. The fourth is a deliberate deferral — `export` returns `FormatUnsupported` for AVIF and JPEG XL, and `the_formats_without_an_encoder_say_so` pins that behaviour in place. Fused, the requirement could only be tagged dishonestly or not at all, and not at all is worse: it would delete the register's record of the part that is finished, which is most of it. So the v1 scope is now JPEG, PNG and TIFF with the configurability clause, and AVIF and JPEG XL are stated as post-v1 with the condition that already holds — either may appear in the settings page before its encoder does, provided choosing it fails with a typed error naming the format rather than producing a file. That is what `every_offered_format_either_encodes_or_explains_itself` exists to guarantee, and it is why a format cannot be added to the picker and quietly reach an encoder that does not handle it. No intent is dropped. AVIF and JPEG XL remain wanted; they are now scheduled rather than silently outstanding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8e4befa727 |
Say what each control is, so a screen reader can use one
The whole interface carried five accessible-* declarations in 14,482 lines of markup, all of them on a single row of the colour mixer, and nothing said so. Every other control — every button, every tick-box, every chip, and the slider the develop panel builds thirty-six of for the mixer alone — reached AT-SPI and TalkBack as an unnamed rectangle. NFR-A11Y-2 is not a polish item for a user in that position; it is whether the application can be used at all. The annotations go on the shared components rather than on the screens, which is the same argument widgets.slint was written to make one layer down: a control named where it is used is a control unnamed everywhere it is used next. Twelve components now declare a role, a name and — where the control does something — the action assistive technology invokes to do it. Three screens were touched, and only where the component could not know the answer. SliderTrack is the one that mattered most and the one that could not be fixed from inside itself. It is handed four numbers and knows nothing about what they mean, so it takes a `label` and a formatted `readout` and every wrapper passes down what it was already drawing. A test asserts that every instantiation does, because a track added without one announces "slider, 0.35" and looks perfectly correct in a screenshot. It also gains increment, decrement and set-value. A slider that can only be dragged is a slider a pointer is a modifier for, which is the objection ui-navigation D-N2 makes about hover-only affordances with the argument run one step further; these three are what a screen reader drives a slider with, and they commit as well as change — one nudge is a whole gesture, so a caller that persists on `committed` must hear about it. Two decisions worth recording because the obvious alternative is wrong: `active` on Button and IconButton is deliberately not announced from the component. It says a toggle is on and says nothing about whether a control that is *off* is a toggle at all, so announcing it would report every button in the application as an unpressed toggle. The four call sites that mean a toggle say so themselves, which Slint permits because the role is inherited. SwatchSlider's row-level role is removed rather than kept. It was the one control that had a label, and now that the track underneath it has one too the two would nest — a slider inside a slider, the outer holding the value and the inner holding the actions that can change it. The row stands down and hands the same strings to the control that owns the gesture. The test reads the markup the way darkroom-android's manifest test reads its XML: there is no accessibility tree without a window, so what it defends is the failure that actually happens — a role or a name lost in a refactor, which compiles, renders identically, and is invisible to everyone not using a screen reader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e0da93c59a |
Reword FR-RAW-2's mechanism to the one that serves its purpose
FR-RAW-2 required RAW decoding to sit behind "a trait taking a SourceRef, not a filesystem path". The purpose is met — `dr_decode::decode` takes `&[u8]` and the crate has no path-based entry point anywhere — and the mechanism is not, because a decoder taking a SourceRef would be a worse design than the one built. A SourceRef is opaque. The only thing that turns one into bytes is `Storage`, which lives in platform/dr-plat, so a decoder taking a SourceRef must take a Storage with it: retry, permission loss and remote fetching move inside the decoder, and the decoder becomes constructible only where a Storage exists. Bytes in, image out, is narrower and more portable — the decoder cannot know where its input came from, which is the property the requirement wants. The Nextcloud case is the one a byte-oriented API looks like it should lose, and it is the clearest illustration that it does not. `dr_decode::HEADER_BYTES` declares how much of a file the decoder needs in order to read metadata, and `import.rs` fetches exactly that range through `Storage::read_range` before calling `dr_decode::metadata`. The decoder states a requirement; the storage layer satisfies it. A decoder holding its own SourceRef would have had to carry the range policy itself. The trait half of the clause is left standing and unmet. There is one decoder reached through free functions, so "a second implementation may be added without changing callers" is still outstanding work rather than a description. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
11e515a295 |
Say which state NFR-P11 forbids losing, since one kind is dropped on purpose
NFR-P11's criterion was "no dropped frames, no state loss" across a layout class transition. Read literally, the interface already fails it, and fails it by design: `apply_layout_class` clears the user's panel open/closed choices whenever the class changes, and `PanelChoices` documents why — a choice made in landscape answers a different question from the one portrait asks, and carrying it across leaves a 232 px sidebar on a screen with no room for it. A requirement that the code deliberately contradicts is worse than no requirement, because the next person to read it either "fixes" the behaviour or learns to discount the register. So the criterion now names what must survive — the open image and version, the selection, scroll position, the in-progress edit and its undo history, the current mode — and states the exemption with the reasoning attached. Panel disclosure is a default re-derived per class, not state; the user's disagreement with it is remembered within the class where it was expressed. The intent is unchanged: a resize must not cost the photographer anything they did. It is now possible to write a test for that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
99f9b5b7c5 |
Strike R5's tiling clauses, which the frame budget argues against
R5 asks that the application work on downscaled proxies for display, and its criterion stated that in two parts: the display pipeline operates at viewport resolution, and only visible tiles are computed with panning recomputing only newly exposed ones. The first is built and pixel-equality tested. The second is not, and should not be. This is the same correction FR-DSP-2 already received, applied to the requirement that FR-DSP-2's tiling clause traces back to. frame-budget.md measured the case tiling exists for: recomputing the whole 4K viewport costs 4.5 ms of a 16 ms budget, so a perfect tile cache could save at most 4.5 ms in exchange for a cache keyed by (VersionId, tile, zoom, graph_hash_prefix) that must stay correct across every parameter change in the graph. For the one stage that does exceed the budget it is worse than useless. That stage is a convolution, and a tiled convolution reads a halo per tile: at the 52 px radius measured at 4K, 256 px tiles read (256+104)² taps instead of 256². The intent is not removed — a display path that does work proportional to the source image is still forbidden, and that is what the remaining clause says. What is removed is a mechanism written in as though it were the only way to get there, and which measurement says is the wrong one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
99563b33f7 |
Record the one clause of FR-PLG-8 that is built, and say what it is short of
FR-PLG-8 reads as unbuilt, and most of it is: no aggregated missing-plugin notice, no catalog-wide view, no export gate, no mark on an image whose operation is unavailable. But its opening sentence is a claim about existing behaviour — "the sidecar already preserves lines it does not understand verbatim and writes them back untouched" — and it goes on to say that property "is now load-bearing and shall be treated as such". Two tests treat it as such, and neither was tagged. `sidecar.rs` keeps an unknown operation across a parse-and-write, and keeps it out of the edit graph so that preserving it is safe rather than merely tidy. Both would fail if the verbatim path were removed, which is what CONTRIBUTING.md asks a tag to mean. The tag sits on the two tests rather than on the module, so the matrix points at the clause that is closed rather than at the requirement as a whole. It is still short of the requirement's own acceptance criterion, and the tag comment says so: FR-PLG-8 asks that a sidecar written with a plugin, opened and saved without it, be byte-identical to the original, and the test asserts `contains`. Both halves exist separately — `writing_the_same_state_twice_is_byte_identical` proves byte identity for content this build understands — and nothing joins them into the single claim the requirement makes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
126beaf7ed |
Tell javac the Java sources are UTF-8
The APK stopped building the moment the tree gained Java with prose in it: 55 errors, every one "unmappable character (0x94)", every one from a comment. The code was fine. javac reads sources in the *platform* encoding, and the build container sets no locale, so that is US-ASCII. Every curly quote and em dash in a doc comment is then unrepresentable. The repository is UTF-8 throughout, so this says so rather than asking one file's prose to be typed in ASCII to suit a default nobody chose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
79b7f54e04 |
Tell javac the Java sources are UTF-8
The APK stopped building the moment the tree gained Java with prose in it: 55 errors, every one "unmappable character (0x94)", every one from a comment. The code was fine. javac reads sources in the *platform* encoding, and the build container sets no locale, so that is US-ASCII. Every curly quote and em dash in a doc comment is then unrepresentable. The repository is UTF-8 throughout, so this says so rather than asking one file's prose to be typed in ASCII to suit a default nobody chose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9a2b39b8e5 |
Add the ADE20K scene model beside the instance one
`models/LICENCE.md` recorded, on 2026-08-21, that no YOLO model trained on ADE20K existed in usable form — the stuff classes photography cares about, sky and vegetation and water, had no model to come from. Re-checked 2026-08-30: Ultralytics now ships a `semantic` task with ADE20K checkpoints, so `models/scene/` holds `yolo26s-sem-ade20k`. This is an addition, not a replacement. A semantic model labels every pixel but merges same-class pixels into one region, so it cannot tell three people apart — which is exactly what clicking a subject needs, and exactly what `segment/`'s COCO instance model already does. The scene tab grades per category and does not care that instances are merged. Keeping both is the point. ## The export is truncated, deliberately Ultralytics ends the graph with `Resize -> ArgMax -> Cast` and hands back a `[1, 640, 640]` u8 label map. The script cuts that tail and exposes the classifier's `[1, 150, 80, 80]` f32 logits instead, for two reasons. Cost: the Resize materialises 150 x 640 x 640 x f32, 246 MB, and ArgMax then reduces across the channel axis, striding 409,600 elements per comparison. On one loaded machine the full graph ran ~1160 ms against ~500 ms truncated — roughly four fifths of the time spent on work the application discards. Those numbers were measured under contention and are upper bounds, but the ratio is structural. Softness: ArgMax destroys the per-class scores, and the scene tab needs them. Softmax over the 150 channels, summed within each photographic category, yields per-category weights summing to 1 at every pixel. Feathering a partition of unity cannot double-grade a boundary, whereas feathering hard labels outward from two adjacent categories paints both grades into the overlap and haloes every horizon. The discarded upsample was never information: the graph's true spatial resolution is the 80x80 logit grid, and the application can resample from that itself. The tail is matched by op type and asserted before cutting, so an upstream graph change fails loudly in the exporter rather than quietly shipping a differently-shaped model. Nothing reads these weights yet — the decode path, the category descriptor grouping 150 classes into ~8 photographic ones, and the scene tab are still to come. At 24 MB this model also wants the runtime-asset treatment `models/face/` already gets on Android rather than `include_bytes!`; embedding it would put ~35 MB of weights in the binary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2e09906a08 |
Keep the export venv off tmpfs
`mktemp -d` lands in `/tmp`, which on this and most current Linux
distributions is a tmpfs — memory, not disk, sized at half of RAM. The
venv this script builds installs torch into it, several gigabytes, and
the failure mode is not subtle:
error: Failed to install: torch-2.13.0-...whl
Caused by: No space left on device (os error 28)
on a machine with 102 GB free on the filesystem holding `/var/tmp`. The
quieter version of the same bug is worse: when it does fit, it evicts
whatever the user had in page cache to make room.
`${TMPDIR:-/var/tmp}` respects an explicit TMPDIR and otherwise picks the
disk-backed directory, which is what a multi-gigabyte throwaway wants.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e26f71d15d |
Gather every model under one tree at the repository root
The weights were in two places: face detection and recognition in `models/face/`, segmentation in `core/dr-segment/models/`. Nothing was wrong with either path, but between them there was nowhere to look to answer "how much model does this application carry", and that number is about to start growing. So the crate-local copy moves up beside the other. `models/` now holds `face/` and `segment/`, and a `du -sh` of one directory is the whole answer. No content changes: the .onnx and its vocabulary are byte-identical, and `LICENCE.md` moves up a level to cover the tree rather than one crate. The LFS pattern in `.gitattributes` is `*.onnx` and already matched both locations, so only its comment needed the new path. `include_bytes!` is relative to the source file and `build.rs` runs with the crate root as its working directory, which is why the two paths climb a different number of levels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ef07e6ca3e |
Give the canvas tools a rail of their own, and the column one width
Build and test / Desktop (Linux) (push) Failing after 1h14m38s
Build and test / Layer separation (push) Successful in 48s
🐳 Android image / Build and push (push) Successful in 16m30s
Build and test / android-image (push) Successful in 16m31s
Traceability / Requirement traces (push) Successful in 1m47s
Build and test / Android (aarch64) (push) Successful in 1h0m21s
Crop, Local and Repair were chips at the head of the develop column, sharing a row with the adjustment groups and told apart from them by the shape of their highlight. Three things followed from that, and only the last is cosmetic: the column closes, so the way out of a mode went away with the way in — hence the duplicate "Done Cropping" over the canvas; the chips are generated from the operation set, so the widest thing in the sidebar was a row nobody had chosen the contents of; and a mode and a filter are different kinds of state wearing one control. They are a fixed 60px rail down the left now, generated from a single table in toolrail.slint. A tool is one row of it plus a drawing plus a ViewMode variant; nothing in app.slint is touched to add one. What is left of the strip is the group filters, so it is GroupStrip. The column stops measuring itself. Every panel published a content-width and declared it as min-width, and the column took the largest — which spent the photograph's pixels on whatever happened to be widest, and moved the image sideways when switching tools swapped one set of panels for another. It is panel-width now, one number in style.yaml. That number is 360 and it is measured, not picked: the contents report a minimum of 344 in every mode, and they do not compress below it because a Text that does not elide reports the same minimum as preferred. 320 was tried and sliced Paste down the middle. The Flickable's viewport is floored at the layout's minimum rather than its preferred width for the same reason — content that is never told how much room it has cannot adapt to having less. Removing the eight content-width declarations repairs three comments an earlier edit had spliced sentences into. The raw histogram's note on keeping its hint short is rewritten rather than dropped: an over-long hint no longer widens the column, it pushes the column's minimum past the width it has and clips the panel, which makes that constraint sharper rather than obsolete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9e519eb8a6 |
Make the thin ring the only mark a selected photograph carries
Two treatments said "selected" and neither said it well. A selected cell got a 2px border and a lifted fill. The border is drawn on the outside of a cell whose content sits 6px in, so it ate into the thumbnail: selecting appeared to nudge the photograph. Inside it, a second thin ring marked the anchor — the end a shift-click measures from — and that ring was the clearest thing on the cell, so it read as *the* selection to everyone who had not written it. Worse, the ring outlived what it described. An anchor survives a deselection, so a thin box sat around the last photograph touched with nothing selected at all, indistinguishable from a cell that had stayed behind. That is the one thing a selection cue must never be: ambiguous about whether something is selected. So the ring is now what it already looked like. One mark, drawn inside the cell and 4px clear of its edge, so it never touches the thumbnail and never changes a dimension — selecting adds ink and moves nothing. Two pixels rather than one, because it is now carrying the whole cue across forty cells at arm's length against a thumbnail of any brightness. The outer border is hover alone. The anchor keeps no mark, and loses nothing it was earning: the bar says "Tap the last photograph" while a range is armed, which answers the question the ring existed to answer. The ordinal still lives in the controller and still decides where a range extends from; what is gone is the claim that the user needs to see it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b120ce48ec |
Float the selection bar over the grid instead of above it
Selecting the first photograph inserted a 40px row into the view's vertical flow, so every cell in the grid moved down by it. The act of selecting shifted the thing being selected out from under the finger — and a second tap aimed at the neighbour landed on the row below it, which is the worst possible response to a gesture whose whole job is to say "this one". The bar is a floating one now, at the foot of the view. Nothing above it is re-laid out, so selecting changes what is drawn and never where. The grid's viewport grows by the same 40px while the bar is there rather than the Flickable shrinking, which is what keeps that change invisible too: every cell stays exactly where it was and there is simply further to scroll, so the last row can be brought clear of the bar instead of being trapped under it. It also swallows presses that land on it. A bar floating over the grid is a bar a thumb can reach for and miss into. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ea88216008 |
Follow the raw histogram's line numbers after the module doc grew
Five tags in dr-gpu moved by three lines when lib.rs's module doc was corrected. Coverage is unchanged at 68.2%; only the anchors moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
49787414fb |
Regenerate the matrix after taking master in
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f41edc03ff |
Merge master into wave-2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> # Conflicts: # docs/traceability.md |
||
|
|
091306f736 |
Gate the gesture vocabulary the way the matrix is gated
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Desktop (Linux) (push) Failing after 1h16m50s
Build and test / Layer separation (push) Successful in 39s
Traceability / Requirement traces (push) Successful in 50s
Build and test / Android (aarch64) (push) Successful in 22m27s
Three holes, all found by the gate catching itself out. **Prose that mentions the tag was read as a tag.** `dr-ui`'s module list carries a comment saying where the generated table comes from, and it names `GESTURE:` in passing; the scan extracted that sentence fragment as a gesture with no place and no way to perform it. A tag must now *open* its comment. A line that merely mentions it is describing the mechanism, not declaring a member of it, and position is the only thing that tells the two apart — which also makes the string-literal guard fall out for free rather than being a special case. **Neither artefact was regenerated on commit.** They cite line numbers, so they go stale on anything that moves a line — the sheet commit made the document wrong about every gesture in `library.slint` without touching a single one. The pre-commit hook that already keeps the matrix in step now keeps these too, and unlike the matrix it *fails* rather than shrugging when the scan does: a matrix that will not build leaves a stale one in place, where a malformed gesture block means a user about to be told the wrong thing. **CI did not check them at all.** It does now, blocking. The matrix is read; the gesture table is *shown to somebody using the application*, and a stale one tells them to perform a gesture that no longer exists — from which they will conclude the application is broken rather than the page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1cd5ab6815 |
Put the gesture reference in the application
The document the previous commit generates is for somebody reading the repository. The person who needs it most is holding a tablet, has just discovered that a hold does something, and has nowhere to ask what else does. So the same scan writes a table the application draws: a "Gestures" button beside Settings, a sheet with the same scrim and dismissal as the ones that file and name, and every gesture grouped by where it applies with its touch, pointer and keyboard routes side by side. Not the `why` — that is the argument for the design and belongs in the document; on a phone-sized card it would bury the one line the sheet was opened to read. The sheet's file knows nothing about what a gesture is. It draws the rows it is handed, and the rows come from the generated table, because a help screen with its text typed into it is a second description of one behaviour — and the second description is always the one that goes stale. The commit before this deleted a gesture; a hand-kept sheet would still be describing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
04203b4a82 |
Regenerate the matrix after taking master in
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4b212089d2 |
Merge master into wave-2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> # Conflicts: # docs/traceability.md |
||
|
|
964e72bca2 |
Merge: the formatter's pass over the raw histogram
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e38b730aa6 |
Reflow what rustfmt wanted in the raw histogram
The author could not run cargo, so this is the formatter's first pass over the new module and its presentation half. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
12812452e8 |
Regenerate the matrix over the second wave
67.0% to 68.2% (122/179). FR-PLAT-AND-4 and FR-PLAT-AND-6 are the two that moved; FR-CULL-3 was already tagged by the peaking half and now has the reduction its other two bullets asked for behind the same tag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
df90dd95a2 |
Merge: a histogram that reads the sensor, beside the one that reads the frame
FR-CULL-3's other two bullets. What existed was a display histogram tagged FR-DSP-7, counting AdjustPass's 8-bit output with r == 255 clipping counters -- it says a highlight is gone precisely where this requirement needs it to say the highlight is recoverable. The new reduction runs over the demosaiced scene-linear texture on a stops-below-saturation axis: camera-native, unbalanced, unmatrixed, uncurved, normalised by the sensor's own black and white levels, so 1.0 is saturation by construction. Four series, and the fourth is the brightest channel rather than luma, because a weighted sum of unbalanced values is a number about nothing. Cached per photograph, not per frame: nothing downstream of the demosaic can move a count. Both readings are legitimate and answer different questions, so the panel offers a choice rather than replacing one with the other. ARCH 5.5 is amended to match. It specified a pre-demosaic reduction; retaining the CFA samples costs 48 MB at 24 MP and 120 MB at 60 MP resident on every photograph opened, whether or not anyone looks at the histogram, on the platform ARCH 6.2 exists for. The spec now records two reductions, why the more complete one was not worth its cost, and what the cheaper one cannot answer: it counts pixels not photosites, it cannot see above white, and it is measured after the CFA pattern is gone. Verified: clippy -D warnings clean, 98 dr-gpu tests, 556 dr-ui tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
31a3580f9d |
Extract the gesture vocabulary from the code that implements it
Every gesture the application has was documented in the comment beside the `TouchArea` that implements it. Excellent comments, and unreachable by anyone not reading the source — which is the FR-UI-4 failure in a different costume: a gesture nobody can find is a feature only its author knows about. Writing them out again in a hand-kept help page is the failure this avoids. Two descriptions of one gesture drift, and it is always the prose that drifts: the code is exercised every time somebody uses the application and the page is exercised never. A help screen confidently describing a double tap the grid stopped honouring last week is worse than no help screen — and the grid did stop honouring one, in the commit before this. So the comment beside the implementation stays the only copy, and a `GESTURE:` block beside it is scanned into two artefacts: `docs/gestures.md` for a reader, and a Rust table for the application to draw a help sheet from. Both committed, both gated, so neither can quietly stop describing the code. It lives in the traceability crate because it is the same operation on the same input — walk the tree, pull structured tags out of comments, render, fail if the committed artefact has moved. Only the vocabulary is new. It scans `ui` and `apps` alone: a gesture needs an interface to be performed on, and excluding `tools` is also what stops the scanner extracting its own worked examples as broken gestures. Fifteen gestures so far, across the library grid and the People screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
20c368d3fc |
Give each touch gesture one meaning, and give tap-to-open back
I broke opening a photograph. The dwell added in "Tell a tap on a photograph from a hand going past" required a finger to stay down 120 ms, and a deliberate tap is routinely quicker than that — so the grid stopped opening anything. Duration was the wrong discriminator: a tap and a brush are the same length. **Travel is what separates them, and a graze is by definition a moving contact.** A press now records where it landed and the release compares: within 12px it is a tap, beyond that the hand was going somewhere else. No dwell, so no deliberate tap can be refused, and the rule is the same for a finger and a mouse — one rule instead of two, and the `touch` argument the dwell needed goes away with it. Two real conflicts went with it, because a gesture set that overlaps itself is unlearnable however each half is documented. **A drag was also a hold.** Grabbing a cell and moving inside 450 ms left the hold timer armed underneath the drag, so it fired mid-gesture and put the grid into selection mode nobody asked for — the drag finished into a mode that changed what every later tap meant. Starting a drag now cancels it, exactly as a pinch already did. **A double tap was also a range.** In selection mode two taps on one cell selected everything back to where selecting began: no visible state, no warning, from a thing a hand does by accident. "Select to…" does that job and announces itself first, so the double tap is gone and two taps are now two toggles that land where they started. `extend_to_row` went with it — a second range implementation that only the double tap reached, where every other range goes through `apply_press`. The resulting vocabulary, one meaning each: tap opens, tap-and-slide does nothing, hold starts selecting, drag files, two fingers resize, and while selecting a tap only ever toggles. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c57fd8ec5a |
Merge: compile our own Java into the APK, and answer an image intent
The APK carried no Java of its own -- the only dex in it was Slint's -- which blocked FR-PLAT-AND-4's foreground service and FR-PLAT-AND-6's outbound half alike. assemble-apk.sh now compiles everything under android/java against android.jar and has d8 merge it with Slint's dex into one classes.dex. With no .java in the tree the step is skipped and the APK is byte-for-byte what it was. -source 8 -target 8 -bootclasspath android.jar is load-bearing: from -target 9 javac rejects -bootclasspath, platform classes then come from the JDK instead of android.jar, and the build stays green while the device raises NoClassDefFoundError. FR-PLAT-AND-6 with it: VIEW, SEND and SEND_MULTIPLE filters, the launch Intent read over JNI before Slint is given the app, and a hand-written ExportProvider rooted at getFilesDir() rather than AndroidX FileProvider, which would have meant Gradle. singleTask, because the new filters let another app launch the activity while it runs and the default mode would start a second android_main, Slint backend and wgpu device in one process. Classes load through the activity's loader; FindClass on android_main's thread sees only the system loader. The share half has no caller yet and is deliberately untagged. The five tests read the manifest and the Java through include_str!, so they fail if a filter goes, if the activity stops being singleTask, if the provider becomes exported, or if the authority and the class drift apart. Verified: javac, d8 and a real dex merge run against the host SDK; aapt2 link over the manifest; clippy -D warnings clean; 5 tests pass. Not verified: anything needing a device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |