Docs: drop the Last updated / Change header line that every PR conflicted on (#3790)

* Docs: drop the Last updated / Change header line that every PR conflicted on

* xtask parity: the header's Changes field holds only the revision-history link; name the kept fields

* xtask parity: reject a field after the header's Changes link
This commit is contained in:
FredApps
2026-11-05 13:54:05 +01:00
committed by GitHub
parent d58b4d6723
commit eab130ecee
16 changed files with 73 additions and 17 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ LightCraft is a clean-room, open-source, pure-Rust photo library + non-destructi
4. **Pick work** from [`docs/gaps.md`](docs/gaps.md) (ranked by user impact; `B` marks beta blockers). The row-by-row
tracker is [`docs/parity-checklist.md`](docs/parity-checklist.md) (one row per feature, menu item and shortcut).
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); `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and
`docs/target-app-parity.md` when a listed gap closes (add a row to each doc's revision history; the header line keeps only `Last reviewed`, `Target` and the `Changes` link, never a `Last updated` date or a change note, which every PR would edit and conflict on); `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.
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.
+1 -1
View File
@@ -14,7 +14,7 @@ LightCraft is a clean-room, open-source, pure-Rust photo library + non-destructi
4. **Pick work** from [`docs/gaps.md`](docs/gaps.md) (ranked by user impact; `B` marks beta blockers). The row-by-row
tracker is [`docs/parity-checklist.md`](docs/parity-checklist.md) (one row per feature, menu item and shortcut).
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); `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and
`docs/target-app-parity.md` when a listed gap closes (add a row to each doc's revision history; the header line keeps only `Last reviewed`, `Target` and the `Changes` link, never a `Last updated` date or a change note, which every PR would edit and conflict on); `cargo xtask parity` (part of `ci`) checks that every `cmd:`/`ctl:` id and
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.
+1 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~14 points and ~340–600 h away
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (Canon CRW decoded clean-room, 6 bodies verified, 557 models verified; gap 9: CI runs the tests on Linux x64 since #2667; macOS, Windows and the GPU equivalence tests still don't; the lens profile database priority links its tracking issue #329; Print: Japanese photo and postcard paper sizes — L判, 2L判, KG判, はがき, 往復はがき, 六切, 四切, 名刺; Minolta MRW decoded, 6 bodies verified; RAW: 85% of Adobe's models decode, 39% verified after the camera-coverage sweep #1327; German catalog 91% → 97%: interface strings and What's New lines translated) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 · **Changes:** [revision history](#revision-history)
One-page summary of how close LightCraft is to Adobe Lightroom. Agents: read this, then pick work
from [docs/gaps.md](docs/gaps.md). The full assessment, method and evidence are in
+1 -1
View File
@@ -1,6 +1,6 @@
# Architecture
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (`cargo xtask ci`: one integration-test binary per crate, doctests apart, one WASM check) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 · **Changes:** [revision history](#revision-history)
How LightCraft is built today. The longer design document (`plan/architecture.md`) is local-only
(`plan/` is gitignored); this page is the committed, current summary. Sizes are Rust lines counted
+1 -1
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (LR-PROF-IMPORT: the user's own `.dcp` camera profiles import; Render to DNG as an export format) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 · **Changes:** [revision history](#revision-history)
Every format Lightroom reads or writes, with our support, fidelity and tests. Camera raw formats are
deep enough to have their own document: [raw-parity.md](raw-parity.md).
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
# Hardware parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (monitor profile from Windows Color System on Windows); previously major (created from the code, gpu-pipeline.md and the parity checklist) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 · **Changes:** [revision history](#revision-history)
What Lightroom does with the machine's hardware, per platform, and what LightCraft does. Sources:
[gpu-pipeline.md](gpu-pipeline.md), [display-profiles.md](display-profiles.md), [denoise.md](denoise.md),
+1 -1
View File
@@ -1,6 +1,6 @@
# Calibrated Contrast and Blacks
> **Last updated:** 2026-10-13 · **Change:** minor (preserve exact evaluation for luminance mixes)
> **Changes:** [revision history](#revision-history)
Process V4 calibrates global Contrast and Blacks on the camera-look path: a raw with
`SourceInfo::camera_tone`. Contrast acts per channel. Blacks blends a per-channel result with
+1 -1
View File
@@ -1,6 +1,6 @@
# Localization parity
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-24 · **Change:** minor (German: interface strings and formats messages reviewed, terms fixed in localization-de.md; German form of address: *du*; Indonesian and Finnish interface catalogs added; Italian: every interface string translated, native-speaker corrections; Vietnamese shipped; previously minor: de, es, fr, ja, uk catalogs filled; re-measured; Italian and Georgian interface catalogs added) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-13 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 · **Changes:** [revision history](#revision-history)
How far each interface language is from `full`. How to add or maintain a language is in
[localization.md](localization.md); per-language notes are in `localization-<code>.md`
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
# Process versions
> **Last updated:** 2026-10-12 · **Change:** minor (calibrated Contrast and Blacks)
> **Changes:** [revision history](#revision-history)
Every photo's develop settings record the **rendering process** they are interpreted by
(`DevelopSettings::process`, a `ProcessVersion` in `crates/develop/src/settings.rs`). Lightroom calls
+1 -1
View File
@@ -1,6 +1,6 @@
# RAW parity: cameras, formats, demosaic, colour and lens profiles
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05 · **Change:** minor (LR-PROF-IMPORT: the user's own `.dcp` camera profiles import, checked on a real D750 profile; Canon CRW decoder shared via craft-libs `craft-crw`; Canon EOS D60: chart-measured colour matrices, white balance in Kelvin; Canon CRW merged with MRW: 557 verified; CRW variant rows split by verification status; Sony ILCE-6700 in-file distortion; Minolta MRW decoded, 6 bodies verified; lossy JPEG XL DNG tiles with a non-linear declared transfer curve decode; Canon ColorData Kelvin; X-Trans CMOS 4 bodies take the X-T3's spectral matrices; AHD no longer turns flat noise into a maze pattern) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7)
> **Last reviewed:** 2026-10-10 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7) · **Changes:** [revision history](#revision-history)
Raw processing is the deepest single dimension of Lightroom parity: a photographer whose camera
does not decode, or decodes with the wrong colour, cannot switch. This document measures it model by
+1 -1
View File
@@ -1,6 +1,6 @@
# Roadmap: milestones, current focus and what's next
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** major (LR-PROF-IMPORT: the user's own `.dcp` camera profiles import; gap 9: roadmap item 8 no longer lists Linux, where CI runs the tests since #2667, and lists the GPU tests; created; milestones moved here from ROADMAP.md, focus re-ranked after the 2026-10-10 re-measure) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-10 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 · **Changes:** [revision history](#revision-history)
Forward-looking plan. The summary with the headline numbers is [ROADMAP.md](../ROADMAP.md); the
ranked shortfalls are [gaps.md](gaps.md); the numbers come from [target-app-parity.md](target-app-parity.md).
+1 -1
View File
@@ -1,6 +1,6 @@
# Target-app parity: LightCraft vs Adobe Lightroom
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (AI Denoise works out of the box, numbers unchanged; Canon CRW decoder shared via craft-libs `craft-crw`; Canon CRW decoded clean-room, 6 bodies verified; gap 9: CI runs the tests on Linux x64 since #2667; macOS, Windows and the GPU equivalence tests still don't; Minolta MRW decoded, 6 bodies verified; RAW: 85% of Adobe's models decode, 39% verified after the camera-coverage sweep #1327; Platforms: the Windows x64 installer verified on Windows 11) · **Target:** Adobe Lightroom Classic 15.5 (August 2026) and Lightroom 9.6 (desktop)
> **Last reviewed:** 2026-10-10 · **Target:** Adobe Lightroom Classic 15.5 (August 2026) and Lightroom 9.6 (desktop) · **Changes:** [revision history](#revision-history)
The authoritative assessment of how close LightCraft is to Lightroom. The row-by-row checklist is
[parity-checklist.md](parity-checklist.md); the ranked work list is [gaps.md](gaps.md); deep areas
+1 -1
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (KEYC-METADATA: ⌘⌥⇧K Set Keyword Shortcut and ⇧K; KEYC-METADATA: ⌘⇧K Edit Keywords; KEYC-METADATA: ⌘⌥K Enable Painting; KEYC-METADATA: ⌘⌥⇧C / ⌘⌥⇧V Copy / Paste Metadata; KEYC-RATING: ⌘L Enable Filters; context menus: a right-click elsewhere on the same widget moves its open menu instead of flashing it, issue #1877; Reference Escape, catalog-local pins and deleted-photo handling verified on Windows; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; earlier: zoom row soft proofing sharp at 1:1, canvas tools by keyboard, focus-order test, ~50 unnamed hand-drawn widgets named; KEYC-METADATA: ⌥0 / ⌥⇧0 next / previous keyword set; KEYC-RATING: ⌘L Enable Filters; context menus: a right-click elsewhere on the same widget moves its open menu instead of flashing it, issue #1877; Reference Escape, catalog-local pins and deleted-photo handling verified on Windows; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; earlier: zoom row soft proofing sharp at 1:1, canvas tools by keyboard, focus-order test, ~50 unnamed hand-drawn widgets named; KEYC-METADATA: ⌘K Add Keywords; KEYC-RATING: ⌘L Enable Filters; context menus: a right-click elsewhere on the same widget moves its open menu instead of flashing it, issue #1877; Reference Escape, catalog-local pins and deleted-photo handling verified on Windows; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; earlier: zoom row soft proofing sharp at 1:1, canvas tools by keyboard, focus-order test, ~50 unnamed hand-drawn widgets named) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
> **Last reviewed:** 2026-10-11 · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 · **Changes:** [revision history](#revision-history)
How LightCraft's interaction compares with Lightroom's: views, panels, tools and handles, modifier
keys, shortcuts, zoom, context menus and feel. Rows for individual menu items and shortcuts live in
+56
View File
@@ -322,6 +322,45 @@ pub fn check_gaps(md: &str) -> Vec<String> {
problems
}
/// The header's `**Changes:**` field: only this link.
const CHANGES_LINK: &str = "**Changes:** [revision history](#revision-history)";
/// A doc whose header (the lines before its first `## ` heading) carries a `Last updated` date or a
/// change note (a `Change` field, or a `Changes` field holding more than [`CHANGES_LINK`], or followed by another field): every
/// PR that touches the doc would edit that one line and conflict with every other open PR. Changes
/// go in the doc's `## Revision history` table instead.
pub fn check_header(path: &str, md: &str) -> Option<String> {
let (i, _) = md.lines().enumerate().take_while(|(_, l)| !l.starts_with("## ")).find(|(_, l)| {
l.contains("**Last updated:**") || l.contains("**Change:**") || {
// The `Changes` field must be exactly the link, and the line's last field: text after
// it (`… · 2026-11-05: gap closed`) is a change note too.
let fields: Vec<&str> = l.split(" · ").map(|f| f.trim_start_matches("> ").trim()).collect();
fields.iter().enumerate().any(|(i, f)| f.starts_with("**Changes:**") && (*f != CHANGES_LINK || i + 1 < fields.len()))
}
})?;
Some(format!(
"{path}:{}: the header has a `Last updated` date or a change note: keep `{CHANGES_LINK}` there and add a row to the doc's revision history instead",
i + 1
))
}
fn check_headers(root: &Path) -> Result<Vec<String>, String> {
let mut files: Vec<String> = std::fs::read_dir(root.join("docs"))
.map_err(|e| format!("docs: {e}"))?
.filter_map(|e| e.ok()?.file_name().into_string().ok())
.filter(|n| n.ends_with(".md"))
.map(|n| format!("docs/{n}"))
.collect();
files.sort();
files.push("ROADMAP.md".into());
let mut problems = Vec::new();
for f in files {
let md = std::fs::read_to_string(root.join(&f)).map_err(|e| format!("{f}: {e}"))?;
problems.extend(check_header(&f, &md));
}
Ok(problems)
}
fn registry(what: &str) -> Result<BTreeSet<String>, String> {
let out = crate::cargo()
.args(["run", "-q", "-p", "lightcraft-cli", "--", what, "--json"])
@@ -348,6 +387,7 @@ pub fn run(root: &Path, write: bool) -> Result<(), String> {
});
let gaps = std::fs::read_to_string(root.join(GAPS)).map_err(|e| format!("{GAPS}: {e}"))?;
problems.extend(check_gaps(&gaps));
problems.extend(check_headers(root)?);
let table = summary_table(&doc);
println!("Lightroom parity ({DOC}): {} rows, {} references\n", doc.rows.len(), doc.refs.len());
@@ -472,6 +512,22 @@ old
assert!(check_gaps("## Ranked\n\n| 1 | a |\n| 2 B | b |\n").is_empty());
}
#[test]
fn header_without_dates_or_change_notes() {
let ok = "# Doc\n\n> **Last reviewed:** 2026-10-10 · **Changes:** [revision history](#revision-history)\n\n## A\n\n**Change:** in the body is fine\n";
assert_eq!(check_header("docs/a.md", ok), None);
let dated = "# Doc\n\n> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-05\n\n## A\n";
assert!(check_header("docs/a.md", dated).is_some_and(|p| p.starts_with("docs/a.md:3: ")));
let noted = "# Doc\n> **Change:** minor (x)\n";
assert!(check_header("docs/b.md", noted).is_some_and(|p| p.starts_with("docs/b.md:2: ")));
let in_link = "# Doc\n> **Changes:** [revision history](#revision-history) (2026-11-05: x)\n";
assert!(check_header("docs/c.md", in_link).is_some_and(|p| p.starts_with("docs/c.md:2: ")));
let trailing = "# Doc\n> **Changes:** [revision history](#revision-history) · 2026-11-05: gap closed\n";
assert!(check_header("docs/e.md", trailing).is_some_and(|p| p.starts_with("docs/e.md:2: ")));
let instead = "# Doc\n> **Last reviewed:** 2026-10-10 · **Changes:** minor (x)\n";
assert!(check_header("docs/d.md", instead).is_some());
}
#[test]
fn collects_references_everywhere() {
let d = parse(SAMPLE);