# 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` /
`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
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.
`plan/` is gitignored (local-only).
+2 -3
View File
@@ -1,6 +1,7 @@
# 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:** 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
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) |
| 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. 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) |
| 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) |
@@ -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) |
| 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) |
| 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) |
| 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) |
| 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) |
@@ -75,6 +73,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| 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-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 |
+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);