Merge upstream/main into pr/raw-erf

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrew Frahn
2026-10-14 18:48:38 +11:00
co-authored by Claude Opus 5.5
148 changed files with 6649 additions and 662 deletions
+6 -1
View File
@@ -135,6 +135,8 @@ mod tests {
gpu vs cpu, loupe 1920×1280 typical: max 1 LSB, mean 0.0001 LSB
loupe draft 1152×768 cold: cpu: 379.5 ms wall, 139.5 ms cpu | gpu: 19.9 ms wall, 17.2 ms cpu
export JPEG encode: 63.9 ms wall, 278.0 ms cpu
window 3072×2304 at (3328, 2048) of 9984×6656
window pan (new window, warm src): cpu: 912.0 ms wall, 2210.5 ms cpu | gpu: 37.5 ms wall, 8.7 ms cpu
";
#[test]
@@ -143,7 +145,10 @@ export JPEG encode: 63.9 ms wall, 278.0 ms cpu
assert_eq!(m["loupe draft 1152×768 cold/cpu/wall"], 379.5);
assert_eq!(m["loupe draft 1152×768 cold/gpu/cpu"], 17.2);
assert_eq!(m["export JPEG encode/cpu/cpu"], 278.0);
assert_eq!(m.len(), 6, "{m:?}");
// (a comma in the name is no column separator)
assert_eq!(m["window pan (new window, warm src)/cpu/cpu"], 2210.5);
assert_eq!(m["window pan (new window, warm src)/gpu/wall"], 37.5);
assert_eq!(m.len(), 10, "{m:?}");
}
#[test]
+20
View File
@@ -524,6 +524,26 @@ const RAW_SAMPLES: &[(&str, &str, &str)] = &[
"https://raw.pixls.us/getfile.php/6587/nice/Canon%20-%20EOS%20R8%20-%203:2.CR3",
"df33cf394573645ce03dea1b2e9f0b5cc2b7734e9e3391ee7114151414cf2812",
),
(
"cr3-canon-rp-raw.cr3",
"https://raw.pixls.us/getfile.php/4674/nice/Canon%20-%20EOS%20RP%20-%203:2.CR3",
"dcbd0f6f9929de2e2331b0fd10a9d01e7cfd04c5a5e4038456de1647d714cdf2",
),
(
"cr3-canon-rp-craw.cr3",
"https://raw.pixls.us/getfile.php/4672/nice/Canon%20-%20EOS%20RP%20-%203:2.CR3",
"bd3270610593d37ff8a399908bb9406a4e32f9151d55bf19ac1cf8f10d787b46",
),
(
"cr3-canon-rp-raw-crop.cr3",
"https://raw.pixls.us/getfile.php/4673/nice/Canon%20-%20EOS%20RP%20-%203:2.CR3",
"7d4ba45dc9264d598336133593c677b09774bcdf13b602ee770e72415a652a16",
),
(
"cr3-canon-rp-craw-crop.cr3",
"https://raw.pixls.us/getfile.php/4671/nice/Canon%20-%20EOS%20RP%20-%203:2.CR3",
"4a859ab137f792aedaf3416b0aad1497ebaab02052a49453902e226699154aa7",
),
// EOS R5 Mark II, full frame and 1.6x crop mode: odd-height (2825- / 1787-row) Bayer planes.
(
"cr3-canon-r5m2-raw.cr3",
+64 -4
View File
@@ -6,11 +6,14 @@
//! inside the file). A trailing `*` matches an id prefix. Row ids must be unique and statuses valid.
//! - **Summary**: per section done / partial / missing / out of scope and the share of P0 and P1 rows done;
//! `--write` refreshes the table between the `parity:summary` markers in the document.
//! - **Gaps** (in `ci`): the ranked table of `docs/gaps.md` lists each gap number once. Two
//! branches that both edit an entry leave both copies after a merge, and nothing else notices.
use std::collections::{BTreeSet, HashSet};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::Path;
pub const DOC: &str = "docs/parity-checklist.md";
pub const GAPS: &str = "docs/gaps.md";
const BEGIN: &str = "<!-- parity:summary -->";
const END: &str = "<!-- /parity:summary -->";
@@ -268,6 +271,33 @@ pub fn with_summary(md: &str, table: &str) -> Option<String> {
Some(format!("{}\n{}{}", &md[..b], table, &md[e..]))
}
/// Gap numbers listed more than once in the `## Ranked` table of `docs/gaps.md` (a row's first
/// cell is its number, `B` marking a beta blocker: `| 10 B | …`).
pub fn check_gaps(md: &str) -> Vec<String> {
let mut first: HashMap<u32, usize> = HashMap::new();
let mut problems = Vec::new();
let mut ranked = false;
for (i, line) in md.lines().enumerate() {
if let Some(heading) = line.strip_prefix("## ") {
ranked = heading.trim() == "Ranked";
continue;
}
let Some(cell) = line.strip_prefix('|').and_then(|r| r.split('|').next()) else { continue };
let cell = cell.trim();
let Ok(n) = cell.strip_suffix(" B").unwrap_or(cell).trim().parse::<u32>() else { continue };
if !ranked {
continue;
}
match first.get(&n) {
Some(at) => problems.push(format!("{GAPS}:{}: gap {n} is listed again (first at line {at}): keep one row per gap", i + 1)),
None => {
first.insert(n, i + 1);
}
}
}
problems
}
fn registry(what: &str) -> Result<BTreeSet<String>, String> {
let out = crate::cargo()
.args(["run", "-q", "-p", "lightcraft-cli", "--", what, "--json"])
@@ -288,10 +318,12 @@ pub fn run(root: &Path, write: bool) -> Result<(), String> {
let mut commands = registry("commands")?;
commands.extend(ui_command_ids(&menus));
let controls = registry("controls")?;
let problems = check(&doc, &commands, &controls, |p| {
let mut problems = check(&doc, &commands, &controls, |p| {
let full = root.join(p);
if full.is_dir() { Some(0) } else { std::fs::read_to_string(full).ok().map(|s| s.lines().count()) }
});
let gaps = std::fs::read_to_string(root.join(GAPS)).map_err(|e| format!("{GAPS}: {e}"))?;
problems.extend(check_gaps(&gaps));
let table = summary_table(&doc);
println!("Lightroom parity ({DOC}): {} rows, {} references\n", doc.rows.len(), doc.refs.len());
@@ -319,14 +351,14 @@ pub fn run(root: &Path, write: bool) -> Result<(), String> {
}
}
if problems.is_empty() {
println!("\nOK: every command, control and path referenced in {DOC} exists.");
println!("\nOK: every command, control and path referenced in {DOC} exists; {GAPS} lists each gap once.");
Ok(())
} else {
println!();
for p in &problems {
println!(" - {p}");
}
Err(format!("{} stale reference(s) in {DOC}", problems.len()))
Err(format!("{} problem(s) in {DOC} / {GAPS}", problems.len()))
}
}
@@ -388,6 +420,34 @@ old
assert!(d.errors.iter().any(|e| e.contains("KEY-SHORT")));
}
#[test]
fn gaps_are_listed_once() {
let md = "\
# Gaps
## Ranked
| # | Gap | Hours |
|---:|---|---:|
| 1 B | one | 3 |
| 2 | two | 4 |
| 2 | two, as the other branch left it | 5 |
| 10 B | ten | 6 |
| 10 B | ten again | 6 |
## Closed recently
| # | Gap |
|---|---|
| 1 | an old one, numbered as it was |
";
let p = check_gaps(md);
assert_eq!(p.len(), 2, "{p:?}");
assert!(p[0].contains(":9: gap 2 is listed again (first at line 8)"), "{p:?}");
assert!(p[1].contains(":11: gap 10 is listed again (first at line 10)"), "{p:?}");
assert!(check_gaps("## Ranked\n\n| 1 | a |\n| 2 B | b |\n").is_empty());
}
#[test]
fn collects_references_everywhere() {
let d = parse(SAMPLE);