# Conflicts:
#	docs/gaps.md
This commit is contained in:
Brandon Thomas committed 2026-10-14 01:39:50 -04:00
commit 694ce5b42b
3 files changed
+68 -8

No files matched your search

+2 -1
View File
@@ -16,7 +16,8 @@ LightCraft is a clean-room, open-source, pure-Rust photo library + non-destructi
When you land a feature, update its row(s) and the gap entry in the same commit, and the numbers in `ROADMAP.md` / When you land a feature, update its row(s) and the gap entry in the same commit, and the numbers in `ROADMAP.md` /
`docs/target-app-parity.md` when a listed gap closes (bump each doc's *Last updated* line and revision history, per `docs/target-app-parity.md` when a listed gap closes (bump each doc's *Last updated* line and revision history, per
craftrules `standards/progress-docs.md`); `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and craftrules `standards/progress-docs.md`); `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and
path the tracker cites still exists, and `cargo xtask parity --write` refreshes its summary. path the tracker cites still exists and that `docs/gaps.md` lists each gap once, and `cargo xtask parity --write`
refreshes its summary.
5. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's. 5. Follow the autonomous operation protocol (`plan/execution-plan.md` §7). Don't stop to ask unless §7 lists the decision as the user's.
`plan/` is gitignored (local-only). `plan/` is gitignored (local-only).
+2 -3
View File
@@ -1,6 +1,7 @@
# Gaps: where LightCraft falls short of Lightroom # Gaps: where LightCraft falls short of Lightroom
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (gap 8: Nikon Z 6II / Z f in-file distortion; gap 10: Leica Q3 verified, 236 models; CR3 entry names the EOS R7 C-RAW fix and the refused CRX variants; CI and Windows / Linux entries note the 0.6.0 riscv64 build; camera colour: tone curve from black-and-white camera JPEGs; Compare and Reference get sharp zoom windows; gap 4 shrinks; gap 14: the HDR switch is read from crs XMP; gaps 2 and 10: EOS R5 Mark II CR3 verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 > **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** minor (gap 8: Nikon Z 6II / Z f in-file distortion; gap 10: Leica Q3 verified, 236 models; CR3 entry names the EOS R7 C-RAW fix and the refused CRX variants; CI and Windows / Linux entries note the 0.6.0 riscv64 build; camera colour: tone curve from black-and-white camera JPEGs; Compare and Reference get sharp zoom windows; gap 4 shrinks; gap 14: the HDR switch is read from crs XMP; gaps 2 and 10: EOS R5 Mark II CR3 verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** patch (gaps 10, 22 and 23 listed once again: merges had left two copies of each; `cargo xtask parity` now fails on a repeated gap number; gap 10: Leica Q3 verified, 236 models; CR3 entry names the EOS R7 C-RAW fix and the refused CRX variants; CI and Windows / Linux entries note the 0.6.0 riscv64 build; camera colour: tone curve from black-and-white camera JPEGs; Compare and Reference get sharp zoom windows; gap 4 shrinks; gap 14: the HDR switch is read from crs XMP; gaps 2 and 10: EOS R5 Mark II CR3 verified) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and
how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is
@@ -25,7 +26,6 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A distortion and Nikon Z 6II / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) | | 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A distortion and Nikon Z 6II / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) |
| 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) | | 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) |
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12, Leica 14 (the Q3 by a recorded check of 38 private DNGs). Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) | | 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12, Leica 14 (the Q3 by a recorded check of 38 private DNGs). Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) |
| 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) | | 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) |
| 12 | **Content-aware / generative remove, detect distractions, reflections removal** | REM rows 75%; patch synthesis missing | cleanup work goes back to Photoshop | 25–45 (+ generative model) | [target-app-parity](target-app-parity.md#ai) | | 12 | **Content-aware / generative remove, detect distractions, reflections removal** | REM rows 75%; patch synthesis missing | cleanup work goes back to Photoshop | 25–45 (+ generative model) | [target-app-parity](target-app-parity.md#ai) |
| 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) | | 13 | **Lightroom catalog migration is one-way and approximate**: unsupported Adobe profiles / AI settings archived, history kept as data | lightroom-catalog-import.md | switching users lose some looks | 8–12 | [file-format-parity](file-format-parity.md) |
@@ -37,10 +37,8 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
| 19 | **Plug-in ecosystem**: no Lightroom SDK equivalent (export / publish plug-ins, metadata plug-ins) | ⬜ | pro workflows built on plug-ins (LR/Enfuse, Jeffrey's exporters, Negative Lab Pro) | 40–80 + owner decision | [target-app-parity](target-app-parity.md#by-dimension) | | 19 | **Plug-in ecosystem**: no Lightroom SDK equivalent (export / publish plug-ins, metadata plug-ins) | ⬜ | pro workflows built on plug-ins (LR/Enfuse, Jeffrey's exporters, Negative Lab Pro) | 40–80 + owner decision | [target-app-parity](target-app-parity.md#by-dimension) |
| 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, MRW, ERF, KDC, DCR, MEF, GPR, old FinePix | raw-parity.md | small user share each | 45–85 | [raw-parity](raw-parity.md) | | 20 | **Long-tail raw formats**: compressed SRW, ARQ pixel shift, IIQ, Hasselblad compression, X3F, CRW, MRW, ERF, KDC, DCR, MEF, GPR, old FinePix | raw-parity.md | small user share each | 45–85 | [raw-parity](raw-parity.md) |
| 21 | **Localization**: 9 of 15 shipped languages partial (74–99% of catalog messages), lower-layer errors English; Hindi, Arabic, Indonesian, Korean, Vietnamese absent; no RTL or complex shaping | measured from catalogs | non-English users see mixed English | 55–90 (shipped to full); 145–255 (five new) | [localization-parity](localization-parity.md) | | 21 | **Localization**: 9 of 15 shipped languages partial (74–99% of catalog messages), lower-layer errors English; Hindi, Arabic, Indonesian, Korean, Vietnamese absent; no RTL or complex shaping | measured from catalogs | non-English users see mixed English | 55–90 (shipped to full); 145–255 (five new) | [localization-parity](localization-parity.md) |
| 22 | **Pen pressure**, OS monitor profiles, follow-window colour | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) |
| 23 | **Accessibility**: AccessKit tree audited by a test (every control named, sliders with range); left: canvas handles, spot and mask placement by keyboard, a session with a real screen reader | LR-BEHAV-ACCESS 🟡 | blind and low-vision users | 4–8 | [ui-parity](ui-parity.md) | | 23 | **Accessibility**: AccessKit tree audited by a test (every control named, sliders with range); left: canvas handles, spot and mask placement by keyboard, a session with a real screen reader | LR-BEHAV-ACCESS 🟡 | blind and low-vision users | 4–8 | [ui-parity](ui-parity.md) |
| 22 | **Pen pressure**, OS monitor profiles on Windows (Linux reads colord's, macOS the screen's), follow-window colour on Linux / Windows | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) | | 22 | **Pen pressure**, OS monitor profiles on Windows (Linux reads colord's, macOS the screen's), follow-window colour on Linux / Windows | hardware-parity.md | brush users with tablets; multi-monitor setups | 11–22 | [hardware-parity](hardware-parity.md) |
| 23 | **Accessibility** audit with screen readers; canvas tools keyboard-reachable | LR-BEHAV-ACCESS 🟡 | blind and low-vision users | 10–20 | [ui-parity](ui-parity.md) |
| 24 | **Brush live preview and masking feel** (#517, #518); Feather / Edge mask refinement (15.5) | issues | masking feels slower than Lightroom | 8–12 | [ui-parity](ui-parity.md) | | 24 | **Brush live preview and masking feel** (#517, #518); Feather / Edge mask refinement (15.5) | issues | masking feels slower than Lightroom | 8–12 | [ui-parity](ui-parity.md) |
| 25 | **File writes**: lossy DNG; AVIF import; Render to DNG as a catalog command (the export format landed) (PSD export landed with Edit in PhotoCraft, #953) | file-format-parity.md | interchange edge cases | 19–34 | [file-format-parity](file-format-parity.md) | | 25 | **File writes**: lossy DNG; AVIF import; Render to DNG as a catalog command (the export format landed) (PSD export landed with Edit in PhotoCraft, #953) | file-format-parity.md | interchange edge cases | 19–34 | [file-format-parity](file-format-parity.md) |
| 26 | **Performance at scale**: 85k-photo library opens in 1.7–4.7 s; 100k grid test pending; colour NR at half resolution | ROADMAP log, gpu-pipeline.md | large-catalog users | 15–30 | [target-app-parity](target-app-parity.md#by-dimension) | | 26 | **Performance at scale**: 85k-photo library opens in 1.7–4.7 s; 100k grid test pending; colour NR at half resolution | ROADMAP log, gpu-pipeline.md | large-catalog users | 15–30 | [target-app-parity](target-app-parity.md#by-dimension) |
@@ -75,6 +73,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| Date | Change | Summary | | Date | Change | Summary |
|---|---|---| |---|---|---|
| 2026-10-13 | patch | Gaps 10, 22 and 23 each had two rows (two branches edited them; the merges kept both): the older copies are removed, and `cargo xtask parity` (in `ci`) now fails when a gap number is listed twice |
| 2026-10-11 | minor | Gap 10 shrinks: Leica Q3 verified by a recorded check of 38 private DNGs (236 models verified; Leica 14); gap not closed | | 2026-10-11 | minor | Gap 10 shrinks: Leica Q3 verified by a recorded check of 38 private DNGs (236 models verified; Leica 14); gap not closed |
| 2026-10-12 | minor | Camera-look deep shadows follow the JPEG toe (#1307); camera calibration remains partial | | 2026-10-12 | minor | Camera-look deep shadows follow the JPEG toe (#1307); camera calibration remains partial |
| 2026-10-12 | minor | Gap 23 (accessibility): the AccessKit tree audit and its fixes landed; what is left is canvas placement by keyboard and a real screen-reader session; 10–20 h → 4–8 h; gap not closed | | 2026-10-12 | minor | Gap 23 (accessibility): the AccessKit tree audit and its fixes landed; what is left is canvas placement by keyboard and a real screen-reader session; 10–20 h → 4–8 h; gap not closed |
+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. //! 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; //! - **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. //! `--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; use std::path::Path;
pub const DOC: &str = "docs/parity-checklist.md"; pub const DOC: &str = "docs/parity-checklist.md";
pub const GAPS: &str = "docs/gaps.md";
const BEGIN: &str = "<!-- parity:summary -->"; const BEGIN: &str = "<!-- parity:summary -->";
const END: &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..])) 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> { fn registry(what: &str) -> Result<BTreeSet<String>, String> {
let out = crate::cargo() let out = crate::cargo()
.args(["run", "-q", "-p", "lightcraft-cli", "--", what, "--json"]) .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")?; let mut commands = registry("commands")?;
commands.extend(ui_command_ids(&menus)); commands.extend(ui_command_ids(&menus));
let controls = registry("controls")?; let controls = registry("controls")?;
let problems = check(&doc, &commands, &controls, |p| { let mut problems = check(&doc, &commands, &controls, |p| {
let full = root.join(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()) } 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); let table = summary_table(&doc);
println!("Lightroom parity ({DOC}): {} rows, {} references\n", doc.rows.len(), doc.refs.len()); 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() { 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(()) Ok(())
} else { } else {
println!(); println!();
for p in &problems { for p in &problems {
println!(" - {p}"); 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"))); 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] #[test]
fn collects_references_everywhere() { fn collects_references_everywhere() {
let d = parse(SAMPLE); let d = parse(SAMPLE);