diff --git a/CLAUDE.md b/CLAUDE.md index 5bf9403c94..b6c91b1fe6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ PdfCraft is a clean-room, open-source, Rust-native PDF application targeting Ado - **Layering.** Nothing below L7 depends on egui/winit/eframe/rfd (`plan/architecture.md` §3). `cos`/`filters`/`crypt`/`arlington` stay standalone. - **Page input.** Page modules read `PagePointer` (`crates/ui-egui/src/page_pointer.rs`), never egui's raw pointer state (`hover_pos`, `interact_pos`, `latest_pos`, `press_origin`, button reads); `page_pointer::tests::page_modules_read_the_pointer_only_through_page_pointer` enforces it. - **Page cursor.** Page modules claim the cursor for what is under the pointer (`view.cursor.claim(Layer::…, icon)`, `crates/ui-egui/src/canvas/page_cursor.rs`), never set it (`set_cursor_icon`, `cursor_icon =`, `on_hover_cursor`); `document_area` sets it once, from the claim on the highest layer the active tool reaches. `page_pointer::tests::page_modules_claim_the_cursor_and_never_set_it` enforces it. -- **Page presses.** A page module decides what of its own is under a point in one hit test, which returns a `Hit` (owner, layer, cursor, tip; `crates/ui-egui/src/canvas/page_router.rs`) and gives its cursor, its tip and its press. It acts on a press only when the router says the press is its own (`view.router.owner(page)`), never by testing the press point again or by asking another module whether it wants it. Escape cancels any press the pages hold (`cancel_on_escape`), and that press is ignored until the button is up. +- **Page presses.** A page module decides what of its own is under a point in one hit test, which returns a `Hit` (owner, layer, cursor, tip; `crates/ui-egui/src/canvas/page_router.rs`) and gives its cursor, its tip and its press. It acts on a press only when the router says the press is its own (`view.router.owner(page)`), never by testing the press point again or by asking another module whether it wants it. Escape cancels any press the pages hold (`cancel_on_escape`), and that press is ignored until the button is up. `page_modules_take_presses_from_the_router_without_repeating_hit_tests` guards the centralized hit calls and captured-owner handlers; runtime routing tests cover gesture behavior. - **Fidelity.** The PDF object graph is the model. Preserve unknown data. Saves are incremental unless a full rewrite is required. Never silently drop or repair data without recording it. - **Never panic.** Every PDF, script, MCP call, settings file and keystroke is untrusted input, and none of it may crash the app or lose the user's work. This outranks feature work. Return `Result` (the crate's error enum) and propagate with `?`, or fall back leniently and record it. In non-test code: - No `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` unless it is provably infallible, with a comment saying why. diff --git a/crates/ui-egui/src/canvas/page_pointer/tests.rs b/crates/ui-egui/src/canvas/page_pointer/tests.rs index b405832ab0..124e370207 100644 --- a/crates/ui-egui/src/canvas/page_pointer/tests.rs +++ b/crates/ui-egui/src/canvas/page_pointer/tests.rs @@ -361,3 +361,84 @@ fn page_modules_claim_the_cursor_and_never_set_it() { ); assert!(problems.is_empty(), "{}", problems.join("\n")); } + +/// Hit producers are called only by the central routing table, not again by an input handler. +const PAGE_HIT_CALLS: [&str; 8] = [ + "prepare::hit(", + "forms_ui::hit(", + "comments::hit(", + "edit_text_ui::hit(", + "edit_text_ui::picture_hit(", + "edit_text_ui::picture_handle_hit(", + "object_ui::hit(", + "content_ui::hit(", +]; + +fn routed_press_handler(code: &str) -> bool { + let code = code.lines().filter(|line| !line.trim_start().starts_with("//")).collect::>().join("\n"); + code.contains("router.owner(") + && !PAGE_HIT_CALLS.iter().any(|call| code.contains(call)) + && !uses(&code, "hit(") + && !uses(&code, "body_hit(") + && !uses(&code, "handle_hit(") +} + +/// Source-level guard for the Page presses rule, alongside the raw-pointer and cursor guards. +/// Runtime routing, capture lifetime and Escape behavior have separate behavioral tests. +#[test] +fn page_modules_take_presses_from_the_router_without_repeating_hit_tests() { + let mut problems = misuses( + &PAGE_HIT_CALLS, + &[("canvas.rs", "at", "the central PageHits table")], + "repeats a module hit test outside PageHits::at; use the captured router owner", + "no longer collects module hits", + ); + problems.extend(misuses( + &["router.press("], + &[("canvas.rs", "page_tool_input", "the central press capture")], + "captures a page press outside the central routing path", + "no longer captures page presses", + )); + let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + for (file, function) in [ + ("tools/forms_ui.rs", "page_input"), + ("tools/prepare.rs", "page_input"), + ("tools/content_ui.rs", "page_input"), + ("tools/edit_text_ui.rs", "page_input"), + ("tools/edit_text_ui/image.rs", "image_input"), + ("tools/object_ui.rs", "input"), + ("tools/comments/input.rs", "select_input"), + ] { + let source = std::fs::read_to_string(src.join(file)).expect("page input module"); + let body = source + .lines() + .skip_while(|line| declares(line) != Some(function)) + .take_while(|line| *line != "}") + .collect::>() + .join("\n"); + // Forms reuse their hit test for hover text and to require release over the captured + // widget. Neither chooses the press owner. Pin both calls to the current pointer `p`, + // not the press origin, and reject an additional call or a changed argument. + let body = if file == "tools/forms_ui.rs" { + let call = "hit(xf, info, page, form, allowed, p)"; + assert_eq!(body.matches(call).count(), 2, "only the existing hover and release checks are exempt"); + body.replace(call, "hover_or_release_check") + } else { + body + }; + if !routed_press_handler(&body) { + problems.push(format!("src/{file}: {function} must consume the captured router owner without repeating a hit test")); + } + } + assert!(problems.is_empty(), "{}", problems.join("\n")); +} + +#[test] +fn the_page_press_guard_rejects_missing_owners_and_second_hit_tests() { + assert!(routed_press_handler("let owner = view.router.owner(page);")); + assert!(!routed_press_handler("// view.router.owner(page)\nlet owner = None;")); + assert!(!routed_press_handler("let owner = view.router.owner(page);\nlet again = cx.hit(origin);")); + for call in PAGE_HIT_CALLS { + assert!(!routed_press_handler(&format!("let owner = view.router.owner(page);\n{call}origin);")), "{call}"); + } +} diff --git a/docs/integration-20261030.md b/docs/integration-20261030.md index f1d14c5c69..92b5e07127 100644 --- a/docs/integration-20261030.md +++ b/docs/integration-20261030.md @@ -2,7 +2,7 @@ Base: 1d1cbc4ec92012767f5b48c92b451e15eb401c42 -Validation: pending combined GitHub CI. No fresh manual GUI pass has been performed. +Validation: combined GitHub CI passed on bdf56209ce6a22980ef488eec92a55073f5a4e18 (Linux and Windows, run 41038860617). No fresh manual GUI pass has been performed. The subsequent architectural-guard additions require a fresh run. All included source heads are preserved as merge parents, retaining original authorship. No upstream PR was merged. Conflicts were resolved after the user requested inclusion despite merge conflicts. ## Included @@ -41,3 +41,43 @@ Excluded held PRs #4648, #2877 and #3762 were not included. Source reviews and individual hosted CI do not establish combined-build safety; the integration gate must pass. Integration-specific changes: preserve hard/soft wrap metadata together with per-row alignment; share CCITT/JBIG2 export paths with the 32 MiB globals cap retained; keep downsampling and RGB ownership guards with JBIG2 encoding; retain all translation keys and both sets of regression tests. + +## Per-PR regression evidence + +Evidence: [Linux job 124006360624](https://github.com/FredApps/pdfcraft/actions/runs/41038860617/job/124006360624) on bdf56209ce6a22980ef488eec92a55073f5a4e18: 5,627 passed, 15 skipped. The Windows leg of the same run also passed. + +For each source-head diff above, the added `#[test]` function names were matched against PASS/SKIP entries in the combined Linux log. Every listed source regression was found passing, none skipped or missing. Counts overlap because stacked PRs carry their prerequisites; they must not be summed as distinct tests. This records source regression coverage, not manual feature acceptance. + +- #4304: 11/11 added source regression names passed; 0 missing, 0 skipped. +- #4320: 21/21 added source regression names passed; 0 missing, 0 skipped. +- #4325: 18/18 added source regression names passed; 0 missing, 0 skipped. +- #4335: 19/19 added source regression names passed; 0 missing, 0 skipped. +- #4384: 39/39 added source regression names passed; 0 missing, 0 skipped. +- #4404: 50/50 added source regression names passed; 0 missing, 0 skipped. +- #4805: 36/36 added source regression names passed; 0 missing, 0 skipped. +- #5300: 18/18 added source regression names passed; 0 missing, 0 skipped. +- #4719: 6/6 added source regression names passed; 0 missing, 0 skipped. +- #4541: 5/5 added source regression names passed; 0 missing, 0 skipped. +- #2815: 35/35 added source regression names passed; 0 missing, 0 skipped. +- #4620: 1/1 added source regression names passed; 0 missing, 0 skipped. +- #2529: 35/35 added source regression names passed; 0 missing, 0 skipped. +- #3789: 5/5 added source regression names passed; 0 missing, 0 skipped. +- #3779: 4/4 added source regression names passed; 0 missing, 0 skipped. +- #2960: 1/1 added source regression names passed; 0 missing, 0 skipped. +- #5208: 2/2 added source regression names passed; 0 missing, 0 skipped. +- #4596: 1/1 added source regression names passed; 0 missing, 0 skipped. +- #3636: 3/3 added source regression names passed; 0 missing, 0 skipped. +- #4494: 4/4 added source regression names passed; 0 missing, 0 skipped. +- #4013: 4/4 added source regression names passed; 0 missing, 0 skipped. +- #4180: 54/54 added source regression names passed; 0 missing, 0 skipped. +- #4179: 20/20 added source regression names passed; 0 missing, 0 skipped. +- #5134: the source diff adds no test function. Combined gates passed; no new manual scrollbar acceptance is claimed. +- #2133: 1/1 added source regression names passed; 0 missing, 0 skipped. +- #2635: 6/6 added source regression names passed; 0 missing, 0 skipped. +- #1952: 6/6 added source regression names passed; 0 missing, 0 skipped. + +The conflict-specific tests `wrapped_rows_keep_alignment_separate_from_hard_break_markers` and `black_and_white_images_become_jbig2` (including the CCITT-to-JBIG2 conversion/render assertions) passed in that same run. Source tests for alignment/history, paragraph boundaries, field calculations and button export values remain present and passed. + +## Protect dependency check + +The integration base main commit 1d1cbc4ec92012767f5b48c92b451e15eb401c42 already locks registry `getrandom` 0.4.3 (checksum 300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099). #4013 makes it a direct engine dependency rather than introducing an unrecorded package. The combined dependency-policy and asset gates passed. The fail-closed randomness regression also passed.