From 3ceecfeef02fe4f4594a2a382f395cdc78a0f5f8 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 15:09:19 -0400 Subject: [PATCH 01/14] chore: pin task contract for bot-PR triage (clippy 1.97 + egui 0.35 + majors) Co-Authored-By: Claude Opus 4.8 --- .harness/TASK.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 .harness/TASK.md diff --git a/.harness/TASK.md b/.harness/TASK.md new file mode 100644 index 00000000..d99f1b5f --- /dev/null +++ b/.harness/TASK.md @@ -0,0 +1,86 @@ +# Task contract + +## Source + +User message 1 (2026-07-10, verbatim): + +> We will be using this thread to conduct maintainece work for NEIREDS. +> +> there are massive amount of bot PR on github, all CI are failing, +> investigae and suggestion resolution + +User message 2 (after investigation presented options A/B/C, verbatim): + +> B, and the corresponding PR should also auto close the bot PRs + +Option B (as presented and chosen): "the full plan (clippy fix + coordinated +egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is +`/Users/8cz/.claude/plans/harmonic-gathering-hejlsberg.md`; its deliverables +(quoted in the requirements below) are: + +> PR 1 — fix(ci): Rust 1.97 lints + repin orphaned action SHAs +> PR 2 — build(deps): coordinated egui 0.34→0.35 ecosystem +> (supersedes #681, #683, #684, #685, #686) +> PR 3 — build(deps): actions majors + zip 8 + sysinfo 0.39 +> (supersedes #677, #678, #679, #680, #682, #687) +> Process: create PR → STOP for user → /review-pipeline → STOP → merge on +> explicit approval → next PR rebases. +> End state: 11 bot PRs closed, main Dependabot Updates run green, next +> stable-Rust release can no longer redden unrelated PRs (pinned toolchain). + +## Rules (enforced by the stop gate) + +- One checkbox per requirement, quoting the Source's own words. Every + imperative sentence in the Source must map to a requirement line. +- Checkboxes must use exactly `- [ ]` — other bullet styles are + invisible to the gate. +- `[x]` requires evidence ON the line: the check command in backticks + and what it showed. The gate blocks `[x]` lines with no backticked + command; an unfilled `` in backticks does not count. +- If an item's recorded evidence is later invalidated (a retraction, a + failed re-check), flip it back to `- [ ]` — a checked line with dead + evidence is a false report. +- Dropping an item requires the user's explicit approval of that named + item: `- [-] DROPPED: (approved: "")`. The quoted + words must themselves address the dropped item — a generic "proceed" + or "sounds good" is never approval. The gate blocks `[-]` lines with + no `(approved:` quote. +- Blocked on the user? Mark it `- [?] — BLOCKED ON USER: + `, finish everything not blocked, then end the + turn asking about all `[?]` items together. +- Never delete or reword a requirement line. Discovered work is ADDED + as new lines, not swapped in. +- Commit this file unless the user objects — tampering must show in + git diff. + +## Requirements + +- [x] R1: "investigae and suggestion resolution" — three root causes identified with evidence and resolution plan approved — check: `gh pr checks 677..687` + `gh run view` logs + local 1.97 repro (`CLIPPY EXIT: 101`, question_mark at poisson.rs:1457) + Dependabot log (`no such commit 50570a4f`); plan approved by user via ExitPlanMode. +- [ ] R2 (PR1): "fix all 37 lints" (1× `clippy::question_mark` poisson.rs:1457; 36× `float_literal_f32_fallback` in apps/gui; plus linux-cfg candidates in file_dialog.rs ~347–770 incl. line 505) — check: scratchpad `cargo +1.97.0 clippy --workspace --exclude nereids-python --all-targets -- -D warnings` exits 0; ubuntu Clippy leg on the PR arbitrates cfg(linux) count. LOCAL DONE: `cargo clippy … -- -D warnings` → `CLIPPY -D warnings EXIT: 0` (darwin, 1.97.0); 37 fixes applied (`cargo clippy --fix` 36 + manual file_dialog.rs:505); ubuntu CI leg pending on PR. +- [x] R3 (PR1): "Repin both poison SHAs — ci.yml:167 taiki-e/install-action → v2.83.0 release SHA c7eb1735…; align security.yml:24 to the same" — check: `grep -rc c7eb1735 .github/workflows/*.yml` = ci.yml:1 + security.yml:1, old SHAs 50570a4f/4684b840 grep = 0; `gh api repos/taiki-e/install-action/git/ref/tags/v2.83.0` → c7eb1735f09259a5035e8e5d44b1406b1cddc0fb commit. +- [x] R4 (PR1): "All 11 dtolnay/rust-toolchain steps … → master head SHA fa04a145…" — check: `grep -rc fa04a145 .github/workflows/*.yml` = 7+1+2+1 = 11; `grep -rn 29eef336` = 0; `gh api repos/dtolnay/rust-toolchain/branches/master` → fa04a1451ff1842e2626ccb99004d0195b455a88 "Add 1.96.1 patch release". +- [x] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input; `rustc --version` in repo dir under rustup → 1.97.0 (file honored). +- [x] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: `cargo fmt --all` → exit 0; clippy exit 0 (R2); `cargo test --workspace --exclude nereids-python` → `CARGO TEST EXIT: 0`, 1264 passed 0 failed; `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. +- [ ] R7 (PR1): "create PR → STOP for user" — check: PR URL posted with summary; turn ends. +- [ ] R8 (PR2): "Cargo.toml: egui 0.35, eframe 0.35 (same feature list), egui_extras 0.35, egui_plot 0.36, egui-file-dialog 0.14.1 (not 0.14.0)"; lockstep + winit comments updated; rfd stays `=0.17.2` — check: `grep -nE 'egui|eframe|rfd' Cargo.toml` shows the pins; `cargo tree` single egui version. +- [ ] R9 (PR2): "Rename 8 deprecated show_inside → show" (app.rs:173,195; studio/mod.rs:49,619,1118; guided/sidebar.rs:31; widgets/toolbar.rs:21; widgets/statusbar.rs:17) + prose comment app.rs:98 — check: `grep -rn show_inside apps/gui/src/` returns 0 hits; clippy 1.97 exit 0. +- [ ] R10 (PR2): "Re-verify the save-extension workaround comment in file_dialog.rs against egui-file-dialog 0.14 behavior" — check: comparison of 0.14.1 source vs comment recorded in PR body. +- [ ] R11 (PR2): "dependabot.yml: egui group FIRST + exclude from the existing group" — check: yaml matches plan block; `patterns: ["egui*", "eframe"]`; cargo-minor-patch gains exclude-patterns. +- [ ] R12 (PR2): "Gates: GTK tripwire + scripts/check_wheel_policy.sh + full pre-commit; manual GUI smoke test" — check: cargo-tree GTK assert clean; wheel-policy script passes; `cargo run -p nereids-gui` smoke observations recorded. +- [ ] R13 (PR2): "create PR → STOP for user" — check: PR URL posted; turn ends. +- [ ] R14 (PR3): "Replicate the bot SHA+comment diffs exactly: setup-pixi v0.10.0 (ci.yml + docs.yml), deploy-pages v5.0.0 (docs.yml), action-gh-release v3.0.1 (publish.yml), codecov-action v7.0.0 (ci.yml)" — check: `grep -rn` each new SHA; matches `gh pr diff 677/678/679/680`. +- [ ] R15 (PR3): "Root Cargo.toml: zip = \"2\" → \"8\"" — check: grep + `cargo test -p nereids-endf` incl. `install_local_endf_accepts_zip_archive`. +- [ ] R16 (PR3): "apps/gui/Cargo.toml: sysinfo = \"0.37\" → \"0.39\"" — check: grep + `cargo tree -d` shows single sysinfo copy. +- [ ] R17 (PR3): "create PR → STOP for user" — check: PR URL posted; turn ends. +- [ ] R18 (process): "/review-pipeline → STOP → merge on explicit approval" for each PR — check: review summary tables presented; merges only after the user's explicit word. +- [ ] R19 (end state): "the corresponding PR should also auto close the bot PRs" — all 11 bot PRs closed — check: `gh pr list --author app/dependabot --state open` empty after PR3 merge (fallback `@dependabot close` comments recorded if used). +- [ ] R20 (end state): "main Dependabot Updates run green" — check: next `Dependabot Updates` github_actions run on main succeeds (`gh run list`), no `no such commit` error. + +## Coverage note + +Source sentence mapping: "We will be using this thread to conduct +maintainece work" — session-scoping statement, no directive. "there are +massive amount of bot PR … investigae and suggestion resolution" → R1. +"B" (chosen plan) → R2–R18. "the corresponding PR should also auto close +the bot PRs" → R19 (+ supersedes lists in R8/R14–R16). Plan end-state +line → R19, R20. From 8580f356fee0cd7511c197da5c694b642c75d242 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 15:09:20 -0400 Subject: [PATCH 02/14] fix: clear Rust 1.97 lints (clippy::question_mark + float_literal_f32_fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust 1.97.0 landed on CI runners (CI floated `stable`) and its new lints fail `cargo clippy -- -D warnings` on every PR: - clippy::question_mark at nereids-fitting/src/poisson.rs:1457 — the if-let-else-return-None collapses to `inner_jac.as_ref()?` (exact semantics-preserving rewrite; FD fallback comment retained). - 36x float_literal_f32_fallback (rust-lang/rust#154024) in apps/gui: bare float literals feeding egui `impl Into` params now need an explicit `_f32` suffix (machine-applied via cargo clippy --fix). - +1 proactive suffix in the cfg(linux) block darwin clippy cannot see: file_dialog.rs:505 anchor offset array. Gate evidence (rustup 1.97.0): clippy -D warnings exit 0; cargo test workspace 1264 passed / 0 failed; pixi run test-python 212 passed. Co-Authored-By: Claude Opus 4.8 --- apps/gui/src/file_dialog.rs | 2 +- apps/gui/src/guided/analyze.rs | 12 ++++++------ apps/gui/src/guided/detectability.rs | 2 +- apps/gui/src/guided/forward_model.rs | 2 +- apps/gui/src/guided/landing.rs | 4 ++-- apps/gui/src/guided/result_widgets.rs | 2 +- apps/gui/src/guided/sidebar.rs | 6 +++--- apps/gui/src/guided/wizard.rs | 8 ++++---- apps/gui/src/studio/mod.rs | 4 ++-- apps/gui/src/widgets/design.rs | 20 ++++++++++---------- apps/gui/src/widgets/image_view.rs | 8 ++++---- apps/gui/src/widgets/statusbar.rs | 2 +- apps/gui/src/widgets/toolbar.rs | 2 +- crates/nereids-fitting/src/poisson.rs | 25 +++++++++++-------------- 14 files changed, 48 insertions(+), 51 deletions(-) diff --git a/apps/gui/src/file_dialog.rs b/apps/gui/src/file_dialog.rs index d3b132ab..0f4a7bcc 100644 --- a/apps/gui/src/file_dialog.rs +++ b/apps/gui/src/file_dialog.rs @@ -502,7 +502,7 @@ impl FileDialogs { egui::Window::new("native_dialog_pending") .title_bar(false) .resizable(false) - .anchor(egui::Align2::CENTER_TOP, [0.0, 8.0]) + .anchor(egui::Align2::CENTER_TOP, [0.0_f32, 8.0_f32]) .show(ctx, |ui| { ui.horizontal(|ui| { ui.spinner(); diff --git a/apps/gui/src/guided/analyze.rs b/apps/gui/src/guided/analyze.rs index 551f2959..81d4e4c0 100644 --- a/apps/gui/src/guided/analyze.rs +++ b/apps/gui/src/guided/analyze.rs @@ -764,7 +764,7 @@ fn fit_feedback_panel(ui: &mut egui::Ui, state: &AppState) { }; egui::Frame::default() .fill(bg_color) - .stroke(egui::Stroke::new(1.5, border_color)) + .stroke(egui::Stroke::new(1.5_f32, border_color)) .corner_radius(4.0) .inner_margin(6.0) .show(ui, |ui| { @@ -999,7 +999,7 @@ fn image_color_bar( painter.rect_stroke( response.rect, 0.0, - egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color), + egui::Stroke::new(1.0_f32, ui.visuals().widgets.noninteractive.bg_stroke.color), egui::StrokeKind::Inside, ); ui.horizontal(|ui| { @@ -1446,7 +1446,7 @@ fn spectrum_panel(ui: &mut egui::Ui, state: &mut AppState) { .collect() }; let measured_points = Points::new("Measured", measured_points) - .radius(1.4) + .radius(1.4_f32) .color(crate::theme::semantic::RED); // OB reference line (counts mode only): the c·OB curve the KL @@ -1721,7 +1721,7 @@ fn draw_aligned_tick_row( painter.rect_stroke( track_rect, 2.0, - egui::Stroke::new(1.0, ui.visuals().widgets.noninteractive.bg_stroke.color), + egui::Stroke::new(1.0_f32, ui.visuals().widgets.noninteractive.bg_stroke.color), egui::StrokeKind::Inside, ); @@ -1761,7 +1761,7 @@ fn draw_aligned_tick_row( egui::pos2(x_pos, track_rect.top()), egui::pos2(x_pos, track_rect.bottom()), ], - egui::Stroke::new(1.0, row.color), + egui::Stroke::new(1.0_f32, row.color), ); } } @@ -1869,7 +1869,7 @@ fn draw_dragged_resonance_ruler( )); painter.line_segment( [egui::pos2(x, frame.top()), egui::pos2(x, frame.bottom())], - egui::Stroke::new(2.0, ruler.color), + egui::Stroke::new(2.0_f32, ruler.color), ); painter.text( egui::pos2(x + 6.0, pointer_y), diff --git a/apps/gui/src/guided/detectability.rs b/apps/gui/src/guided/detectability.rs index c9e01df1..2b7ff9b1 100644 --- a/apps/gui/src/guided/detectability.rs +++ b/apps/gui/src/guided/detectability.rs @@ -576,7 +576,7 @@ pub(crate) fn detect_results_panel(ui: &mut egui::Ui, state: &AppState) { .map(|i| [report.energies[i], report.delta_t_spectrum[i]]) .collect(); let color = design::isotope_dot_color(name); - plot_ui.line(Line::new(name.as_str(), points).color(color).width(1.5)); + plot_ui.line(Line::new(name.as_str(), points).color(color).width(1.5_f32)); } }); }); diff --git a/apps/gui/src/guided/forward_model.rs b/apps/gui/src/guided/forward_model.rs index d08f61ca..2a01f47a 100644 --- a/apps/gui/src/guided/forward_model.rs +++ b/apps/gui/src/guided/forward_model.rs @@ -400,7 +400,7 @@ pub(crate) fn fm_spectrum_panel(ui: &mut egui::Ui, state: &mut AppState) { .filter(|&i| x_values[i].is_finite()) .map(|i| [x_values[i], combined[i]]) .collect(); - plot_ui.line(Line::new("Combined T(E)", points).width(2.0)); + plot_ui.line(Line::new("Combined T(E)", points).width(2.0_f32)); } // Per-isotope contribution lines (dashed, colored by isotope hash) diff --git a/apps/gui/src/guided/landing.rs b/apps/gui/src/guided/landing.rs index 1bf41c72..26796465 100644 --- a/apps/gui/src/guided/landing.rs +++ b/apps/gui/src/guided/landing.rs @@ -115,7 +115,7 @@ pub fn landing_step(ui: &mut egui::Ui, state: &mut AppState) { .fill(tc.bg2) .corner_radius(CornerRadius::same(8)) .inner_margin(Margin::symmetric(16, 10)) - .stroke(Stroke::new(1.0, tc.border)) + .stroke(Stroke::new(1.0_f32, tc.border)) .show(ui, |ui| { ui.horizontal(|ui| { ui.vertical(|ui| { @@ -167,7 +167,7 @@ fn landing_card( .fill(tc.bg2) .corner_radius(CornerRadius::same(12)) .inner_margin(Margin::same(20)) - .stroke(Stroke::new(1.0, tc.border)) + .stroke(Stroke::new(1.0_f32, tc.border)) .shadow(Shadow { offset: [0, 1], blur: 3, diff --git a/apps/gui/src/guided/result_widgets.rs b/apps/gui/src/guided/result_widgets.rs index 7447e49e..77b3f879 100644 --- a/apps/gui/src/guided/result_widgets.rs +++ b/apps/gui/src/guided/result_widgets.rs @@ -267,7 +267,7 @@ pub fn draw_colorbar(ui: &mut egui::Ui, data: &ndarray::Array2, colormap: C painter.rect_stroke( bar_rect, 0.0, - egui::Stroke::new(1.0, egui::Color32::GRAY), + egui::Stroke::new(1.0_f32, egui::Color32::GRAY), egui::StrokeKind::Outside, ); diff --git a/apps/gui/src/guided/sidebar.rs b/apps/gui/src/guided/sidebar.rs index 977f2070..d3ce373f 100644 --- a/apps/gui/src/guided/sidebar.rs +++ b/apps/gui/src/guided/sidebar.rs @@ -26,7 +26,7 @@ pub fn guided_sidebar(ui: &mut egui::Ui, state: &mut AppState) { egui::Frame::NONE .fill(colors.bg) .inner_margin(margin) - .stroke(Stroke::new(1.0, colors.border)), + .stroke(Stroke::new(1.0_f32, colors.border)), ) .show_inside(ui, |ui| { if state.sidebar_collapsed || force_compact { @@ -268,7 +268,7 @@ fn collapsed_step_badge( ); } else { ui.painter() - .circle_stroke(center, 10.0, Stroke::new(1.0, colors.border)); + .circle_stroke(center, 10.0, Stroke::new(1.0_f32, colors.border)); ui.painter().text( center, egui::Align2::CENTER_CENTER, @@ -466,7 +466,7 @@ fn pipeline_step_row( } else { // Pending: outlined icon, not a filled status dot. ui.painter() - .circle_stroke(center, 10.0, Stroke::new(1.0, colors.border)); + .circle_stroke(center, 10.0, Stroke::new(1.0_f32, colors.border)); ui.painter().text( center, egui::Align2::CENTER_CENTER, diff --git a/apps/gui/src/guided/wizard.rs b/apps/gui/src/guided/wizard.rs index 68f1363c..b775c4b5 100644 --- a/apps/gui/src/guided/wizard.rs +++ b/apps/gui/src/guided/wizard.rs @@ -46,7 +46,7 @@ fn breadcrumb_bar(ui: &mut egui::Ui, state: &mut AppState) { .stroke(if is_active || is_past { Stroke::NONE } else { - Stroke::new(1.0, tc.border) + Stroke::new(1.0_f32, tc.border) }) .show(ui, |ui| { ui.label(RichText::new(*label).size(11.0).strong().color(text_color)); @@ -250,7 +250,7 @@ fn wizard_confirm(ui: &mut egui::Ui, state: &mut AppState) { .fill(tc.bg2) .corner_radius(CornerRadius::same(8)) .inner_margin(Margin::same(16)) - .stroke(Stroke::new(1.0, tc.accent)) + .stroke(Stroke::new(1.0_f32, tc.accent)) .show(ui, |ui| { ui.set_min_width(full_w - 38.0); ui.label( @@ -331,9 +331,9 @@ fn wizard_option_card( let tc = ThemeColors::from_ctx(ui.ctx()); let fill = if selected { tc.bg3 } else { tc.bg2 }; let border = if selected { - Stroke::new(2.0, tc.accent) + Stroke::new(2.0_f32, tc.accent) } else { - Stroke::new(1.0, tc.border) + Stroke::new(1.0_f32, tc.border) }; let full_w = ui.available_width(); diff --git a/apps/gui/src/studio/mod.rs b/apps/gui/src/studio/mod.rs index 361d7292..7bd566ad 100644 --- a/apps/gui/src/studio/mod.rs +++ b/apps/gui/src/studio/mod.rs @@ -614,7 +614,7 @@ fn bottom_dock(ui: &mut egui::Ui, state: &mut AppState) { egui::Frame::NONE .fill(colors.bg) .inner_margin(egui::Margin::symmetric(12, 8)) - .stroke(egui::Stroke::new(1.0, colors.border)), + .stroke(egui::Stroke::new(1.0_f32, colors.border)), ) .show_inside(ui, |ui| { let labels = &["Isotopes", "Residuals", "Provenance", "Export"]; @@ -1113,7 +1113,7 @@ fn parameter_sidebar(ui: &mut egui::Ui, state: &mut AppState) { egui::Frame::NONE .fill(colors.bg) .inner_margin(egui::Margin::symmetric(10, 8)) - .stroke(egui::Stroke::new(1.0, colors.border)), + .stroke(egui::Stroke::new(1.0_f32, colors.border)), ) .show_inside(ui, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { diff --git a/apps/gui/src/widgets/design.rs b/apps/gui/src/widgets/design.rs index 2cf906e1..255e575b 100644 --- a/apps/gui/src/widgets/design.rs +++ b/apps/gui/src/widgets/design.rs @@ -48,7 +48,7 @@ pub fn card(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui)) { let shadow_alpha = if ui.style().visuals.dark_mode { 76 } else { 20 }; egui::Frame::NONE .fill(tc.bg2) - .stroke(Stroke::new(1.0, tc.border)) + .stroke(Stroke::new(1.0_f32, tc.border)) .corner_radius(CornerRadius::same(10)) .inner_margin(Margin::same(16)) .shadow(Shadow { @@ -125,7 +125,7 @@ pub fn underline_tabs(ui: &mut Ui, labels: &[&str], selected: &mut usize) -> boo egui::pos2(row_rect.left(), baseline), egui::pos2(row_rect.right(), baseline), ], - Stroke::new(1.0, tc.border), + Stroke::new(1.0_f32, tc.border), ); // 2px accent underline on active tab @@ -135,7 +135,7 @@ pub fn underline_tabs(ui: &mut Ui, labels: &[&str], selected: &mut usize) -> boo egui::pos2(rect.left(), baseline), egui::pos2(rect.right(), baseline), ], - Stroke::new(2.0, tc.accent), + Stroke::new(2.0_f32, tc.accent), ); } @@ -201,7 +201,7 @@ pub fn btn_icon(ui: &mut Ui, label: &str, active: bool) -> Response { let (fill, text_color, stroke) = if active { (tc.accent, Color32::WHITE, Stroke::NONE) } else { - (tc.bg2, tc.fg2, Stroke::new(1.0, tc.border)) + (tc.bg2, tc.fg2, Stroke::new(1.0_f32, tc.border)) }; ui.add( egui::Button::new(RichText::new(label).size(11.0).strong().color(text_color)) @@ -231,7 +231,7 @@ pub fn drop_zone(ui: &mut Ui, loaded: bool, label: &str, hint: &str) -> Response let (border_color, border_width, fill) = if loaded { ( semantic::GREEN, - 2.0, + 2.0_f32, Color32::from_rgba_unmultiplied( semantic::GREEN.r(), semantic::GREEN.g(), @@ -240,7 +240,7 @@ pub fn drop_zone(ui: &mut Ui, loaded: bool, label: &str, hint: &str) -> Response ), ) } else { - (tc.fg3, 1.5, Color32::TRANSPARENT) + (tc.fg3, 1.5_f32, Color32::TRANSPARENT) }; // Background + border @@ -358,7 +358,7 @@ pub fn isotope_chip( let fill = if enabled { tc.bg3 } else { tc.bg2 }; egui::Frame::NONE .fill(fill) - .stroke(Stroke::new(1.0, tc.border)) + .stroke(Stroke::new(1.0_f32, tc.border)) .corner_radius(CornerRadius::same(12)) .inner_margin(Margin::symmetric(8, 4)) .show(ui, |ui| { @@ -430,7 +430,7 @@ pub fn group_chip( let fill = if enabled { tc.bg3 } else { tc.bg2 }; egui::Frame::NONE .fill(fill) - .stroke(Stroke::new(1.0, tc.border)) + .stroke(Stroke::new(1.0_f32, tc.border)) .corner_radius(CornerRadius::same(12)) .inner_margin(Margin::symmetric(8, 4)) .show(ui, |ui| { @@ -993,7 +993,7 @@ pub(crate) fn draw_resonance_dips( plot_ui.vline( VLine::new("", res.energy) .color(RESONANCE_DIP_COLOR) - .width(0.5), + .width(0.5_f32), ); } } @@ -1155,7 +1155,7 @@ pub(crate) fn build_fit_line(p: &FitLineParams<'_>) -> Option> { .collect(); Some( Line::new("Fit", fit_points) - .width(1.25) + .width(1.25_f32) .color(egui::Color32::from_rgba_unmultiplied(0, 122, 255, 170)), ) } diff --git a/apps/gui/src/widgets/image_view.rs b/apps/gui/src/widgets/image_view.rs index da4905ef..f096bcd8 100644 --- a/apps/gui/src/widgets/image_view.rs +++ b/apps/gui/src/widgets/image_view.rs @@ -588,7 +588,7 @@ fn draw_roi_draft_overlay( roi_rect, 0.0, egui::Stroke::new( - 1.5, + 1.5_f32, egui::Color32::from_rgba_unmultiplied(255, 255, 255, 180), ), egui::StrokeKind::Outside, @@ -681,7 +681,7 @@ fn draw_roi_overlay( painter.rect_stroke( roi_rect, 0.0, - egui::Stroke::new(1.5, egui::Color32::from_rgb(0, 120, 255)), + egui::Stroke::new(1.5_f32, egui::Color32::from_rgb(0, 120, 255)), egui::StrokeKind::Outside, ); } @@ -719,7 +719,7 @@ fn draw_roi_selected_overlay( painter.rect_stroke( roi_rect, 0.0, - egui::Stroke::new(2.0, egui::Color32::from_rgb(0, 200, 80)), + egui::Stroke::new(2.0_f32, egui::Color32::from_rgb(0, 200, 80)), egui::StrokeKind::Outside, ); } @@ -742,7 +742,7 @@ fn draw_pixel_marker( rect.top() + frac_y * rect.height(), ); let r = 4.0; - let stroke = egui::Stroke::new(1.5, egui::Color32::from_rgb(255, 165, 0)); + let stroke = egui::Stroke::new(1.5_f32, egui::Color32::from_rgb(255, 165, 0)); painter.circle_stroke(center, r, stroke); painter.line_segment( [ diff --git a/apps/gui/src/widgets/statusbar.rs b/apps/gui/src/widgets/statusbar.rs index 548b61bb..73842b39 100644 --- a/apps/gui/src/widgets/statusbar.rs +++ b/apps/gui/src/widgets/statusbar.rs @@ -12,7 +12,7 @@ pub fn status_bar(ui: &mut egui::Ui, state: &AppState, rss_bytes: u64) { egui::Frame::NONE .fill(colors.bg2) .inner_margin(egui::Margin::symmetric(12, 4)) - .stroke(egui::Stroke::new(1.0, colors.border)), + .stroke(egui::Stroke::new(1.0_f32, colors.border)), ) .show_inside(ui, |ui| { ui.horizontal(|ui| { diff --git a/apps/gui/src/widgets/toolbar.rs b/apps/gui/src/widgets/toolbar.rs index 69313f1d..fb2df3f5 100644 --- a/apps/gui/src/widgets/toolbar.rs +++ b/apps/gui/src/widgets/toolbar.rs @@ -16,7 +16,7 @@ pub fn toolbar(ui: &mut egui::Ui, state: &mut AppState) { egui::Frame::NONE .fill(colors.bg2) .inner_margin(egui::Margin::symmetric(12, 6)) - .stroke(egui::Stroke::new(1.0, colors.border)), + .stroke(egui::Stroke::new(1.0_f32, colors.border)), ) .show_inside(ui, |ui| { ui.horizontal_centered(|ui| { diff --git a/crates/nereids-fitting/src/poisson.rs b/crates/nereids-fitting/src/poisson.rs index 355d9f70..b15a6648 100644 --- a/crates/nereids-fitting/src/poisson.rs +++ b/crates/nereids-fitting/src/poisson.rs @@ -1453,21 +1453,18 @@ impl<'a> FitModel for TransmissionKLBackgroundModel<'a> { // Fill inner model columns (density, temperature). // Inner Jacobian is the same as bare model — background doesn't - // affect ∂T_inner/∂nₖ. - if let Some(ref ij) = inner_jac { - let mut inner_col = 0; - for (col, &fp) in free_param_indices.iter().enumerate() { - if fp == self.b0_index || fp == self.b1_index { - continue; - } - for row in 0..n_e { - *jacobian.get_mut(row, col) = ij.get(row, inner_col); - } - inner_col += 1; + // affect ∂T_inner/∂nₖ. Without an analytical inner Jacobian the + // `?` returns None: fall back to FD for the entire model. + let ij = inner_jac.as_ref()?; + let mut inner_col = 0; + for (col, &fp) in free_param_indices.iter().enumerate() { + if fp == self.b0_index || fp == self.b1_index { + continue; } - } else { - // No analytical inner Jacobian — fall back to FD for entire model. - return None; + for row in 0..n_e { + *jacobian.get_mut(row, col) = ij.get(row, inner_col); + } + inner_col += 1; } // Background columns. From e4a9938de4c17e08fedb92ec22ccd2fb9b59412b Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 15:09:20 -0400 Subject: [PATCH 03/14] ci: repin orphaned action SHAs + pin Rust toolchain 1.97.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two action pins pointed at commits on no branch, which crashes Dependabot's github_actions update job on main (find_container_branch: 'no such commit', see failing 'Dependabot Updates' runs): - taiki-e/install-action was pinned to the orphan per-tool tag SHA `cargo-llvm-cov` (ci.yml) -> repinned to release tag v2.83.0 (c7eb1735), matching security.yml's style; security.yml aligned from v2.82.9 to the same. - dtolnay/rust-toolchain was pinned to a force-pushed-away `stable` branch head (29eef336, unreachable from every current ref) -> all 11 steps repinned to the permanent master branch head (fa04a145), the README-sanctioned shape when passing an explicit `toolchain:` input. Also pin the Rust toolchain (rust-toolchain.toml, channel 1.97.0) so a new stable release can no longer redden all open PRs at once; workflow `toolchain:` inputs aligned from `stable` to `1.97.0` (pre-install optimization — rustup proxies enforce the file regardless). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 34 +++++++++++++++++++--------------- .github/workflows/docs.yml | 4 ++-- .github/workflows/publish.yml | 8 ++++---- .github/workflows/security.yml | 6 ++++-- rust-toolchain.toml | 18 ++++++++++++++++++ 5 files changed, 47 insertions(+), 23 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37d9c45f..be18a6e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - run: cargo check --workspace --exclude nereids-python # Compile-check workspace examples so docs snippets cannot drift @@ -50,9 +50,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml components: rustfmt - run: cargo fmt --all -- --check @@ -70,9 +70,9 @@ jobs: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml components: clippy - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - run: cargo clippy --workspace --exclude nereids-python --all-targets -- -D warnings @@ -95,9 +95,9 @@ jobs: os: [macos-latest, windows-latest] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build run: cargo build --workspace --exclude nereids-python @@ -111,9 +111,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build docs run: cargo doc --workspace --no-deps --exclude nereids-python @@ -141,9 +141,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Install pixi uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 @@ -159,12 +159,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml components: llvm-tools-preview - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - - uses: taiki-e/install-action@50570a4fd0cb7e583e6a512c02bffb9ade28d868 # cargo-llvm-cov + # Pin release-tag SHAs only (like security.yml), never taiki-e's + # per-tool tags (e.g. `cargo-llvm-cov`): those are orphan commits on + # no branch, and Dependabot's github_actions updater hard-errors on + # them ("no such commit"), killing the whole update run. + - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0 with: tool: cargo-llvm-cov - name: Generate coverage diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f4caee87..8840b69c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,9 +47,9 @@ jobs: - name: Check curated Python API page is in sync with .pyi stubs run: python scripts/check_python_api_drift.py - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 04d930ea..db7a834c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -161,9 +161,9 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 @@ -270,9 +270,9 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: stable + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml - name: Publish crates (dependency order) env: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index e76bf738..22feb78f 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -20,8 +20,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - uses: taiki-e/install-action@4684b8405694ae9dd42c9f39ba901a70ae83f4a3 # v2.82.9 + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master + with: + toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0 with: tool: cargo-audit # Fails on unignored vulnerabilities. Accepted-risk advisories (with a diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..f0b22551 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,18 @@ +# Pinned Rust toolchain — single source of truth for CI and rustup users. +# +# Why pinned: CI used to float `stable`, so every new stable release +# (with its new clippy/rustc lints under `-D warnings`) instantly turned +# ALL open PRs red for reasons unrelated to their diffs (2026-07-10: +# Rust 1.97.0 reddened 11 Dependabot PRs at once). Pinning makes +# toolchain bumps deliberate, reviewed changes. +# +# Bump ritual: update `channel` here, update the matching +# `toolchain: "X.Y.Z"` inputs in .github/workflows/*.yml (they only +# pre-install the right version — rustup proxies enforce THIS file for +# every cargo invocation regardless), fix any new lints, one PR. +# +# Note: maturin-action's manylinux containers (publish.yml, +# wheel-policy.yml) also honor this file and will download the pinned +# toolchain inside the container — intended, for reproducible wheels. +[toolchain] +channel = "1.97.0" From 148228517731863f7c1255c39d59672be8908b21 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 15:10:16 -0400 Subject: [PATCH 04/14] chore: record PR #707 in task contract Co-Authored-By: Claude Opus 4.8 --- .harness/TASK.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.harness/TASK.md b/.harness/TASK.md index d99f1b5f..e3f42715 100644 --- a/.harness/TASK.md +++ b/.harness/TASK.md @@ -61,7 +61,7 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is - [x] R4 (PR1): "All 11 dtolnay/rust-toolchain steps … → master head SHA fa04a145…" — check: `grep -rc fa04a145 .github/workflows/*.yml` = 7+1+2+1 = 11; `grep -rn 29eef336` = 0; `gh api repos/dtolnay/rust-toolchain/branches/master` → fa04a1451ff1842e2626ccb99004d0195b455a88 "Add 1.96.1 patch release". - [x] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input; `rustc --version` in repo dir under rustup → 1.97.0 (file honored). - [x] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: `cargo fmt --all` → exit 0; clippy exit 0 (R2); `cargo test --workspace --exclude nereids-python` → `CARGO TEST EXIT: 0`, 1264 passed 0 failed; `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. -- [ ] R7 (PR1): "create PR → STOP for user" — check: PR URL posted with summary; turn ends. +- [x] R7 (PR1): "create PR → STOP for user" — check: PR URL posted with summary; turn ends. Evidence: `gh pr create` → https://github.com/ornlneutronimaging/NEREIDS/pull/707; checkpoint-1 STOP taken in the same turn. - [ ] R8 (PR2): "Cargo.toml: egui 0.35, eframe 0.35 (same feature list), egui_extras 0.35, egui_plot 0.36, egui-file-dialog 0.14.1 (not 0.14.0)"; lockstep + winit comments updated; rfd stays `=0.17.2` — check: `grep -nE 'egui|eframe|rfd' Cargo.toml` shows the pins; `cargo tree` single egui version. - [ ] R9 (PR2): "Rename 8 deprecated show_inside → show" (app.rs:173,195; studio/mod.rs:49,619,1118; guided/sidebar.rs:31; widgets/toolbar.rs:21; widgets/statusbar.rs:17) + prose comment app.rs:98 — check: `grep -rn show_inside apps/gui/src/` returns 0 hits; clippy 1.97 exit 0. - [ ] R10 (PR2): "Re-verify the save-extension workaround comment in file_dialog.rs against egui-file-dialog 0.14 behavior" — check: comparison of 0.14.1 source vs comment recorded in PR body. From 13925489703372b6c293f6f9f09f81c553dbf594 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 15:17:23 -0400 Subject: [PATCH 05/14] =?UTF-8?q?chore:=20contract=20=E2=80=94=20R2=20CI-a?= =?UTF-8?q?rbitrated=20green;=20PR2/PR3=20items=20blocked=20on=20user=20ch?= =?UTF-8?q?eckpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .harness/TASK.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.harness/TASK.md b/.harness/TASK.md index e3f42715..540db8c1 100644 --- a/.harness/TASK.md +++ b/.harness/TASK.md @@ -56,25 +56,25 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is ## Requirements - [x] R1: "investigae and suggestion resolution" — three root causes identified with evidence and resolution plan approved — check: `gh pr checks 677..687` + `gh run view` logs + local 1.97 repro (`CLIPPY EXIT: 101`, question_mark at poisson.rs:1457) + Dependabot log (`no such commit 50570a4f`); plan approved by user via ExitPlanMode. -- [ ] R2 (PR1): "fix all 37 lints" (1× `clippy::question_mark` poisson.rs:1457; 36× `float_literal_f32_fallback` in apps/gui; plus linux-cfg candidates in file_dialog.rs ~347–770 incl. line 505) — check: scratchpad `cargo +1.97.0 clippy --workspace --exclude nereids-python --all-targets -- -D warnings` exits 0; ubuntu Clippy leg on the PR arbitrates cfg(linux) count. LOCAL DONE: `cargo clippy … -- -D warnings` → `CLIPPY -D warnings EXIT: 0` (darwin, 1.97.0); 37 fixes applied (`cargo clippy --fix` 36 + manual file_dialog.rs:505); ubuntu CI leg pending on PR. +- [x] R2 (PR1): "fix all 37 lints" (1× `clippy::question_mark` poisson.rs:1457; 36× `float_literal_f32_fallback` in apps/gui; plus linux-cfg candidates in file_dialog.rs ~347–770 incl. line 505) — check: scratchpad `cargo +1.97.0 clippy --workspace --exclude nereids-python --all-targets -- -D warnings` exits 0; ubuntu Clippy leg on the PR arbitrates cfg(linux) count. LOCAL DONE: `cargo clippy … -- -D warnings` → `CLIPPY -D warnings EXIT: 0` (darwin, 1.97.0); 37 fixes applied (`cargo clippy --fix` 36 + manual file_dialog.rs:505); CI ARBITER GREEN: `gh pr checks 707` → `Clippy (ubuntu-latest) pass 2m14s`, `Clippy (macos-latest) pass 3m36s`. - [x] R3 (PR1): "Repin both poison SHAs — ci.yml:167 taiki-e/install-action → v2.83.0 release SHA c7eb1735…; align security.yml:24 to the same" — check: `grep -rc c7eb1735 .github/workflows/*.yml` = ci.yml:1 + security.yml:1, old SHAs 50570a4f/4684b840 grep = 0; `gh api repos/taiki-e/install-action/git/ref/tags/v2.83.0` → c7eb1735f09259a5035e8e5d44b1406b1cddc0fb commit. - [x] R4 (PR1): "All 11 dtolnay/rust-toolchain steps … → master head SHA fa04a145…" — check: `grep -rc fa04a145 .github/workflows/*.yml` = 7+1+2+1 = 11; `grep -rn 29eef336` = 0; `gh api repos/dtolnay/rust-toolchain/branches/master` → fa04a1451ff1842e2626ccb99004d0195b455a88 "Add 1.96.1 patch release". - [x] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input; `rustc --version` in repo dir under rustup → 1.97.0 (file honored). - [x] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: `cargo fmt --all` → exit 0; clippy exit 0 (R2); `cargo test --workspace --exclude nereids-python` → `CARGO TEST EXIT: 0`, 1264 passed 0 failed; `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. - [x] R7 (PR1): "create PR → STOP for user" — check: PR URL posted with summary; turn ends. Evidence: `gh pr create` → https://github.com/ornlneutronimaging/NEREIDS/pull/707; checkpoint-1 STOP taken in the same turn. -- [ ] R8 (PR2): "Cargo.toml: egui 0.35, eframe 0.35 (same feature list), egui_extras 0.35, egui_plot 0.36, egui-file-dialog 0.14.1 (not 0.14.0)"; lockstep + winit comments updated; rfd stays `=0.17.2` — check: `grep -nE 'egui|eframe|rfd' Cargo.toml` shows the pins; `cargo tree` single egui version. -- [ ] R9 (PR2): "Rename 8 deprecated show_inside → show" (app.rs:173,195; studio/mod.rs:49,619,1118; guided/sidebar.rs:31; widgets/toolbar.rs:21; widgets/statusbar.rs:17) + prose comment app.rs:98 — check: `grep -rn show_inside apps/gui/src/` returns 0 hits; clippy 1.97 exit 0. -- [ ] R10 (PR2): "Re-verify the save-extension workaround comment in file_dialog.rs against egui-file-dialog 0.14 behavior" — check: comparison of 0.14.1 source vs comment recorded in PR body. -- [ ] R11 (PR2): "dependabot.yml: egui group FIRST + exclude from the existing group" — check: yaml matches plan block; `patterns: ["egui*", "eframe"]`; cargo-minor-patch gains exclude-patterns. -- [ ] R12 (PR2): "Gates: GTK tripwire + scripts/check_wheel_policy.sh + full pre-commit; manual GUI smoke test" — check: cargo-tree GTK assert clean; wheel-policy script passes; `cargo run -p nereids-gui` smoke observations recorded. -- [ ] R13 (PR2): "create PR → STOP for user" — check: PR URL posted; turn ends. -- [ ] R14 (PR3): "Replicate the bot SHA+comment diffs exactly: setup-pixi v0.10.0 (ci.yml + docs.yml), deploy-pages v5.0.0 (docs.yml), action-gh-release v3.0.1 (publish.yml), codecov-action v7.0.0 (ci.yml)" — check: `grep -rn` each new SHA; matches `gh pr diff 677/678/679/680`. -- [ ] R15 (PR3): "Root Cargo.toml: zip = \"2\" → \"8\"" — check: grep + `cargo test -p nereids-endf` incl. `install_local_endf_accepts_zip_archive`. -- [ ] R16 (PR3): "apps/gui/Cargo.toml: sysinfo = \"0.37\" → \"0.39\"" — check: grep + `cargo tree -d` shows single sysinfo copy. -- [ ] R17 (PR3): "create PR → STOP for user" — check: PR URL posted; turn ends. -- [ ] R18 (process): "/review-pipeline → STOP → merge on explicit approval" for each PR — check: review summary tables presented; merges only after the user's explicit word. -- [ ] R19 (end state): "the corresponding PR should also auto close the bot PRs" — all 11 bot PRs closed — check: `gh pr list --author app/dependabot --state open` empty after PR3 merge (fallback `@dependabot close` comments recorded if used). -- [ ] R20 (end state): "main Dependabot Updates run green" — check: next `Dependabot Updates` github_actions run on main succeeds (`gh run list`), no `no such commit` error. +- [?] R8 (PR2): "Cargo.toml: egui 0.35, eframe 0.35 (same feature list), egui_extras 0.35, egui_plot 0.36, egui-file-dialog 0.14.1 (not 0.14.0)"; lockstep + winit comments updated; rfd stays `=0.17.2` — check: `grep -nE 'egui|eframe|rfd' Cargo.toml` shows the pins; `cargo tree` single egui version. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R9 (PR2): "Rename 8 deprecated show_inside → show" (app.rs:173,195; studio/mod.rs:49,619,1118; guided/sidebar.rs:31; widgets/toolbar.rs:21; widgets/statusbar.rs:17) + prose comment app.rs:98 — check: `grep -rn show_inside apps/gui/src/` returns 0 hits; clippy 1.97 exit 0. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R10 (PR2): "Re-verify the save-extension workaround comment in file_dialog.rs against egui-file-dialog 0.14 behavior" — check: comparison of 0.14.1 source vs comment recorded in PR body. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R11 (PR2): "dependabot.yml: egui group FIRST + exclude from the existing group" — check: yaml matches plan block; `patterns: ["egui*", "eframe"]`; cargo-minor-patch gains exclude-patterns. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R12 (PR2): "Gates: GTK tripwire + scripts/check_wheel_policy.sh + full pre-commit; manual GUI smoke test" — check: cargo-tree GTK assert clean; wheel-policy script passes; `cargo run -p nereids-gui` smoke observations recorded. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R13 (PR2): "create PR → STOP for user" — check: PR URL posted; turn ends. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R14 (PR3): "Replicate the bot SHA+comment diffs exactly: setup-pixi v0.10.0 (ci.yml + docs.yml), deploy-pages v5.0.0 (docs.yml), action-gh-release v3.0.1 (publish.yml), codecov-action v7.0.0 (ci.yml)" — check: `grep -rn` each new SHA; matches `gh pr diff 677/678/679/680`. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R15 (PR3): "Root Cargo.toml: zip = \"2\" → \"8\"" — check: grep + `cargo test -p nereids-endf` incl. `install_local_endf_accepts_zip_archive`. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R16 (PR3): "apps/gui/Cargo.toml: sysinfo = \"0.37\" → \"0.39\"" — check: grep + `cargo tree -d` shows single sysinfo copy. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R17 (PR3): "create PR → STOP for user" — check: PR URL posted; turn ends. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R18 (process): "/review-pipeline → STOP → merge on explicit approval" for each PR — check: review summary tables presented; merges only after the user's explicit word. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R19 (end state): "the corresponding PR should also auto close the bot PRs" — all 11 bot PRs closed — check: `gh pr list --author app/dependabot --state open` empty after PR3 merge (fallback `@dependabot close` comments recorded if used). — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R20 (end state): "main Dependabot Updates run green" — check: next `Dependabot Updates` github_actions run on main succeeds (`gh run list`), no `no such commit` error. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? ## Coverage note From 14acf7683cbc326aa60b40de180eda9bbd0421a1 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 18:20:35 -0400 Subject: [PATCH 06/14] =?UTF-8?q?fix(ci):=20repin=20toolchain=201.97.0=20-?= =?UTF-8?q?>=201.96.1=20=E2=80=94=20P-critical=20x86-64=20miscompile=20in?= =?UTF-8?q?=201.97.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1 (cross-family, Codex-found + Claude-verified against primary sources): rust-lang/rust#159035 is an OPEN P-critical / I-miscompile / O-x86_64 LLVM regression in 1.97.0 — optimized safe code segfaults; 1.96.1 is unaffected; the upstream revert (rust-lang/rust#159047, reverting #155850) merged 2026-07-10 and is in no stable release. Release wheels for the ORNL x86-64 RHEL fleet build under this pin, so 1.97.0 is unacceptable until a fixed stable ships. - rust-toolchain.toml: channel 1.96.1 + rationale; precedence comment narrowed per rustup override docs (cargo +toolchain / RUSTUP_TOOLCHAIN / nearer overrides win over the file). - workflows: all 11 'toolchain:' inputs 1.97.0 -> 1.96.1. - CONTRIBUTING.md: '(stable toolchain)' -> pinned-by-rust-toolchain.toml. The 37 Rust-1.97 lint fixes in the previous commit are kept: they are valid 1.96 code and future-proof the bump to the first fixed stable. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/docs.yml | 2 +- .github/workflows/publish.yml | 4 ++-- .github/workflows/security.yml | 2 +- CONTRIBUTING.md | 2 +- rust-toolchain.toml | 17 ++++++++++++++--- 6 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be18a6e4..8cdf9b8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - run: cargo check --workspace --exclude nereids-python # Compile-check workspace examples so docs snippets cannot drift @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml components: rustfmt - run: cargo fmt --all -- --check @@ -72,7 +72,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml components: clippy - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - run: cargo clippy --workspace --exclude nereids-python --all-targets -- -D warnings @@ -97,7 +97,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build run: cargo build --workspace --exclude nereids-python @@ -113,7 +113,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Build docs run: cargo doc --workspace --no-deps --exclude nereids-python @@ -143,7 +143,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Install pixi uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 @@ -161,7 +161,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml components: llvm-tools-preview - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 # Pin release-tag SHAs only (like security.yml), never taiki-e's diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8840b69c..50a8c941 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -49,7 +49,7 @@ jobs: - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index db7a834c..027c7482 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -163,7 +163,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 @@ -272,7 +272,7 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - name: Publish crates (dependency order) env: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 22feb78f..4f3cf6da 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master with: - toolchain: "1.97.0" # keep in sync with rust-toolchain.toml + toolchain: "1.96.1" # keep in sync with rust-toolchain.toml - uses: taiki-e/install-action@c7eb1735f09259a5035e8e5d44b1406b1cddc0fb # v2.83.0 with: tool: cargo-audit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6acffc7d..8ffdf087 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ Open a GitHub issue with the `enhancement` label. Include: ### Prerequisites -- [Rust](https://www.rust-lang.org/tools/install) (stable toolchain) +- [Rust](https://www.rust-lang.org/tools/install) (rustup; the exact version is pinned by rust-toolchain.toml) - [pixi](https://pixi.sh/) (Python environment manager) - cmake (for HDF5 builds) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f0b22551..6eed91f0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -6,13 +6,24 @@ # Rust 1.97.0 reddened 11 Dependabot PRs at once). Pinning makes # toolchain bumps deliberate, reviewed changes. # +# Why 1.96.1 and not 1.97.0: rust-lang/rust#159035 — a P-critical +# x86-64 miscompile in 1.97.0 (optimized safe code segfaults; an LLVM +# regression from stable to stable; 1.96.1 is not affected). Release +# wheels for the ORNL x86-64 RHEL fleet are built under this pin, so +# 1.97.0 is unacceptable until fixed. Bump this pin to the first +# stable release containing the fix (the revert of +# rust-lang/rust#155850, merged upstream via rust-lang/rust#159047). +# # Bump ritual: update `channel` here, update the matching # `toolchain: "X.Y.Z"` inputs in .github/workflows/*.yml (they only -# pre-install the right version — rustup proxies enforce THIS file for -# every cargo invocation regardless), fix any new lints, one PR. +# pre-install the right version — for ordinary invocations in this +# repo, without a higher-precedence rustup override such as +# `cargo +toolchain`, `RUSTUP_TOOLCHAIN`, or a nearer directory +# override, the rustup proxies enforce THIS file), fix any new lints, +# one PR. # # Note: maturin-action's manylinux containers (publish.yml, # wheel-policy.yml) also honor this file and will download the pinned # toolchain inside the container — intended, for reproducible wheels. [toolchain] -channel = "1.97.0" +channel = "1.96.1" From 0f5257f2df06f7aaa7f05cad2ff2c8591fcfb814 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 18:20:35 -0400 Subject: [PATCH 07/14] fix(fitting): analytic Jacobian for background-only KL fits + FD-fallback tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 1 P2s: - TransmissionKLBackgroundModel::analytical_jacobian returned None (whole- model FD fallback) whenever the inner Jacobian was absent — including the background-only case (inner params fixed, just b0/b1 free) whose columns are trivially analytic (dT/db0 = 1, dT/db1 = 1/sqrt(E)). Mirror the sibling CountsBackgroundScaleModel guard: fall back to FD only when inner params are actually free without an analytical inner Jacobian. - Add the two missing coverage points: FD fallback when the inner model lacks analytical_jacobian (wrapper returns None, fit converges via FD) and the background-only analytic path (exact column values + fit convergence). - .harness/TASK.md: correct R1's recorded evidence command to the loop actually run; add R21-R25 requirement lines omitted from plan coverage. Gates (rustup 1.96.1): clippy -D warnings exit 0; cargo test 1266 passed 0 failed (+2 new); pixi run test-python 212 passed 1 skipped. Co-Authored-By: Claude Opus 4.8 --- .harness/TASK.md | 9 +- crates/nereids-fitting/src/poisson.rs | 182 ++++++++++++++++++++++++-- 2 files changed, 178 insertions(+), 13 deletions(-) diff --git a/.harness/TASK.md b/.harness/TASK.md index 540db8c1..b8f7e90f 100644 --- a/.harness/TASK.md +++ b/.harness/TASK.md @@ -55,7 +55,7 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is ## Requirements -- [x] R1: "investigae and suggestion resolution" — three root causes identified with evidence and resolution plan approved — check: `gh pr checks 677..687` + `gh run view` logs + local 1.97 repro (`CLIPPY EXIT: 101`, question_mark at poisson.rs:1457) + Dependabot log (`no such commit 50570a4f`); plan approved by user via ExitPlanMode. +- [x] R1: "investigae and suggestion resolution" — three root causes identified with evidence and resolution plan approved — check: `for pr in 677 678 679 680 681 682 683 684 685 686 687; do gh pr checks $pr; done` + `gh run view` logs + local 1.97 repro (`CLIPPY EXIT: 101`, question_mark at poisson.rs:1457) + Dependabot log (`no such commit 50570a4f`); plan approved by user via ExitPlanMode. - [x] R2 (PR1): "fix all 37 lints" (1× `clippy::question_mark` poisson.rs:1457; 36× `float_literal_f32_fallback` in apps/gui; plus linux-cfg candidates in file_dialog.rs ~347–770 incl. line 505) — check: scratchpad `cargo +1.97.0 clippy --workspace --exclude nereids-python --all-targets -- -D warnings` exits 0; ubuntu Clippy leg on the PR arbitrates cfg(linux) count. LOCAL DONE: `cargo clippy … -- -D warnings` → `CLIPPY -D warnings EXIT: 0` (darwin, 1.97.0); 37 fixes applied (`cargo clippy --fix` 36 + manual file_dialog.rs:505); CI ARBITER GREEN: `gh pr checks 707` → `Clippy (ubuntu-latest) pass 2m14s`, `Clippy (macos-latest) pass 3m36s`. - [x] R3 (PR1): "Repin both poison SHAs — ci.yml:167 taiki-e/install-action → v2.83.0 release SHA c7eb1735…; align security.yml:24 to the same" — check: `grep -rc c7eb1735 .github/workflows/*.yml` = ci.yml:1 + security.yml:1, old SHAs 50570a4f/4684b840 grep = 0; `gh api repos/taiki-e/install-action/git/ref/tags/v2.83.0` → c7eb1735f09259a5035e8e5d44b1406b1cddc0fb commit. - [x] R4 (PR1): "All 11 dtolnay/rust-toolchain steps … → master head SHA fa04a145…" — check: `grep -rc fa04a145 .github/workflows/*.yml` = 7+1+2+1 = 11; `grep -rn 29eef336` = 0; `gh api repos/dtolnay/rust-toolchain/branches/master` → fa04a1451ff1842e2626ccb99004d0195b455a88 "Add 1.96.1 patch release". @@ -75,12 +75,17 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is - [?] R18 (process): "/review-pipeline → STOP → merge on explicit approval" for each PR — check: review summary tables presented; merges only after the user's explicit word. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? - [?] R19 (end state): "the corresponding PR should also auto close the bot PRs" — all 11 bot PRs closed — check: `gh pr list --author app/dependabot --state open` empty after PR3 merge (fallback `@dependabot close` comments recorded if used). — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? - [?] R20 (end state): "main Dependabot Updates run green" — check: next `Dependabot Updates` github_actions run on main succeeds (`gh run list`), no `no such commit` error. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? +- [?] R21 (PR3): "cargo update for the bumped crates; full pre-commit gate" — check: Cargo.lock updated for zip/sysinfo/egui-family only; fmt/clippy/test pass. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. +- [?] R22 (process): "sequential PRs, each rebased on the previous merge" — check: PR2 branch created from post-PR1 main; PR3 from post-PR2 main. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. +- [x] R23 (process): "Commits GPG-signed, atomic" — check: git log --show-signature on the branch commits. Evidence (branch commits to date): `git log --show-signature 19d0ea2..HEAD` -> 5 commits, every one `gpg: Good signature from "Chen Zhang (kedokudo)"`; atomic (contract / lints / CI-pins / contract-updates split). Re-verify as later commits land. +- [?] R24 (PR3): "CI green" on PR3 — check: gh pr checks all pass. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. +- [?] R25 (release guard): "the next release must run gh workflow run publish.yml -f dry_run=true first" — check: recorded in PR3 body + verified before next tag. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. ## Coverage note Source sentence mapping: "We will be using this thread to conduct maintainece work" — session-scoping statement, no directive. "there are massive amount of bot PR … investigae and suggestion resolution" → R1. -"B" (chosen plan) → R2–R18. "the corresponding PR should also auto close +"B" (chosen plan) → R2–R18, R21–R25. "the corresponding PR should also auto close the bot PRs" → R19 (+ supersedes lists in R8/R14–R16). Plan end-state line → R19, R20. diff --git a/crates/nereids-fitting/src/poisson.rs b/crates/nereids-fitting/src/poisson.rs index b15a6648..06e032ff 100644 --- a/crates/nereids-fitting/src/poisson.rs +++ b/crates/nereids-fitting/src/poisson.rs @@ -1453,18 +1453,22 @@ impl<'a> FitModel for TransmissionKLBackgroundModel<'a> { // Fill inner model columns (density, temperature). // Inner Jacobian is the same as bare model — background doesn't - // affect ∂T_inner/∂nₖ. Without an analytical inner Jacobian the - // `?` returns None: fall back to FD for the entire model. - let ij = inner_jac.as_ref()?; - let mut inner_col = 0; - for (col, &fp) in free_param_indices.iter().enumerate() { - if fp == self.b0_index || fp == self.b1_index { - continue; - } - for row in 0..n_e { - *jacobian.get_mut(row, col) = ij.get(row, inner_col); + // affect ∂T_inner/∂nₖ. + if let Some(ij) = inner_jac.as_ref() { + let mut inner_col = 0; + for (col, &fp) in free_param_indices.iter().enumerate() { + if fp == self.b0_index || fp == self.b1_index { + continue; + } + for row in 0..n_e { + *jacobian.get_mut(row, col) = ij.get(row, inner_col); + } + inner_col += 1; } - inner_col += 1; + } else if !inner_free.is_empty() { + // Inner params are free but the inner model has no analytical + // Jacobian — fall back to FD for the entire model. + return None; } // Background columns. @@ -2401,4 +2405,160 @@ mod tests { true_params[3], ); } + + /// Inner params FREE + inner model WITHOUT `analytical_jacobian` → + /// the wrapper must return `None` (FD fallback for the entire model) + /// and the fit must still converge on the FD path. + #[test] + fn test_transmission_kl_background_fd_fallback_when_inner_lacks_jacobian() { + // ExponentialModel has no analytical_jacobian (trait default None). + // Counts-scale data (flux 1000, backgrounds 20/10) keeps the + // Poisson NLL well-conditioned for parameter recovery. + let x: Vec = (0..40).map(|i| 1.0 + 0.25 * i as f64).collect(); + let inner = ExponentialModel { + x: x.clone(), + flux: vec![1000.0; x.len()], + }; + let inv_sqrt_energies: Vec = x.iter().map(|&e| 1.0 / e.sqrt()).collect(); + let wrapped = TransmissionKLBackgroundModel { + inner: &inner, + inv_sqrt_energies, + b0_index: 1, + b1_index: 2, + n_params: 3, + }; + + let true_params = vec![0.4, 20.0, 10.0]; + let y_obs = wrapped.evaluate(&true_params).unwrap(); + + // Inner param 0 free (alone and together with b0/b1): no analytic + // inner Jacobian → wrapper falls back to FD. + assert!( + wrapped + .analytical_jacobian(&true_params, &[0, 1, 2], &y_obs) + .is_none(), + "inner param free without inner analytical_jacobian must give None" + ); + assert!( + wrapped + .analytical_jacobian(&true_params, &[0], &y_obs) + .is_none(), + "inner-only free set without inner analytical_jacobian must give None" + ); + + let mut params = ParameterSet::new(vec![ + FitParameter::non_negative("density", 0.8), + FitParameter { + name: "kl_b0".into(), + value: 5.0, + lower: 0.0, + upper: 500.0, + fixed: false, + }, + FitParameter { + name: "kl_b1".into(), + value: 5.0, + lower: 0.0, + upper: 500.0, + fixed: false, + }, + ]); + let config = PoissonConfig { + max_iter: 300, + ..PoissonConfig::default() + }; + let result = poisson_fit(&wrapped, &y_obs, &mut params, &config).unwrap(); + assert!( + result.converged, + "FD-fallback fit did not converge: {result:?}" + ); + assert!( + (result.params[0] - true_params[0]).abs() / true_params[0] < 0.05, + "density fit={}, true={}", + result.params[0], + true_params[0], + ); + } + + /// Inner params FIXED (only b0/b1 free) → the wrapper stays on the + /// analytic path: `analytical_jacobian` returns `Some` with the exact + /// background columns ∂T/∂b₀ = 1 and ∂T/∂b₁ = 1/√E, and the + /// background-only fit converges. + #[test] + fn test_transmission_kl_background_background_only_analytic_jacobian() { + // Counts-scale data — see the FD-fallback test above. + let x: Vec = (0..40).map(|i| 1.0 + 0.25 * i as f64).collect(); + let inner = ExponentialModel { + x: x.clone(), + flux: vec![1000.0; x.len()], + }; + let inv_sqrt_energies: Vec = x.iter().map(|&e| 1.0 / e.sqrt()).collect(); + let wrapped = TransmissionKLBackgroundModel { + inner: &inner, + inv_sqrt_energies: inv_sqrt_energies.clone(), + b0_index: 1, + b1_index: 2, + n_params: 3, + }; + + let true_params = vec![0.4, 20.0, 10.0]; + let y_obs = wrapped.evaluate(&true_params).unwrap(); + + let jac = wrapped + .analytical_jacobian(&true_params, &[1, 2], &y_obs) + .expect("background-only free set must stay on the analytic path"); + for (row, &inv_sqrt_e) in inv_sqrt_energies.iter().enumerate() { + assert!( + (jac.get(row, 0) - 1.0).abs() < 1e-15, + "∂T/∂b₀ at row {row} = {}, expected 1.0", + jac.get(row, 0), + ); + assert!( + (jac.get(row, 1) - inv_sqrt_e).abs() < 1e-15, + "∂T/∂b₁ at row {row} = {}, expected {inv_sqrt_e}", + jac.get(row, 1), + ); + } + + // Background-only fit (density fixed at truth) converges on the + // analytic path. + let mut params = ParameterSet::new(vec![ + FitParameter::fixed("density", true_params[0]), + FitParameter { + name: "kl_b0".into(), + value: 5.0, + lower: 0.0, + upper: 500.0, + fixed: false, + }, + FitParameter { + name: "kl_b1".into(), + value: 5.0, + lower: 0.0, + upper: 500.0, + fixed: false, + }, + ]); + let config = PoissonConfig { + max_iter: 300, + ..PoissonConfig::default() + }; + let result = poisson_fit(&wrapped, &y_obs, &mut params, &config).unwrap(); + assert!( + result.converged, + "background-only analytic fit did not converge: {result:?}" + ); + assert!( + (result.params[1] - true_params[1]).abs() < 0.1, + "b0 fit={}, true={}", + result.params[1], + true_params[1], + ); + assert!( + (result.params[2] - true_params[2]).abs() < 0.1, + "b1 fit={}, true={}", + result.params[2], + true_params[2], + ); + } } From ba786b4272be87ef43a58afd81c1180d80cca93b Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 20:12:52 -0400 Subject: [PATCH 08/14] fix(fitting): accumulate aliased background-column derivatives (+= not =) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 2 (Codex-found, Claude-verified with an independent runtime FD reproduction): neither TransmissionKLBackgroundModel nor its sibling CountsBackgroundScaleModel forbids the two background/scale indices from aliasing (pub fields, no constructor). evaluate() reads the aliased parameter for both roles, so its true derivative is the SUM of the two column contributions (1 + 1/sqrt(E), resp. f*T + B) — but both column fills ASSIGNED, so the second overwrote the first (e.g. 0.5 vs correct 1.5 at E=4). Pre-existing defect; the round-1 background-only analytic path extended its exposure. Accumulate with += instead: identical for distinct indices (each column touched once on a zeroed matrix), correct for aliased ones. Adds one aliased-index regression test per model, checked against an in-test central finite-difference oracle computed directly from evaluate() — independent of the analytic path under test. Gates (1.96.1): clippy -D warnings exit 0; cargo test 1268 passed 0 failed (+2); pixi run test-python 212 passed 1 skipped. Co-Authored-By: Claude Opus 4.8 --- crates/nereids-fitting/src/poisson.rs | 127 +++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 5 deletions(-) diff --git a/crates/nereids-fitting/src/poisson.rs b/crates/nereids-fitting/src/poisson.rs index 06e032ff..41fc5d4c 100644 --- a/crates/nereids-fitting/src/poisson.rs +++ b/crates/nereids-fitting/src/poisson.rs @@ -1342,14 +1342,20 @@ impl<'a> FitModel for CountsBackgroundScaleModel<'a> { return None; } + // Accumulate (+=) rather than assign: the struct does not forbid + // alpha1_index == alpha2_index, and evaluate() reads the aliased + // parameter for both roles, so its derivative is the SUM of both + // column contributions (f·t + b). With distinct indices each + // column is touched once and += on the zeroed matrix is identical + // to assignment. if let Some(col) = alpha1_col { for (row, (&f, &t)) in self.flux.iter().zip(t_inner.iter()).enumerate() { - *jacobian.get_mut(row, col) = f * t; + *jacobian.get_mut(row, col) += f * t; } } if let Some(col) = alpha2_col { for (row, &bg) in self.background.iter().enumerate() { - *jacobian.get_mut(row, col) = bg; + *jacobian.get_mut(row, col) += bg; } } @@ -1471,15 +1477,20 @@ impl<'a> FitModel for TransmissionKLBackgroundModel<'a> { return None; } - // Background columns. + // Background columns. Accumulate (+=) rather than assign: the + // struct does not forbid b0_index == b1_index, and evaluate() + // reads the aliased parameter for both roles, so its derivative + // is the SUM of both column contributions (1 + 1/√E). With + // distinct indices each column is touched once and += on the + // zeroed matrix is identical to assignment. if let Some(col) = b0_col { for row in 0..n_e { - *jacobian.get_mut(row, col) = 1.0; // ∂T_out/∂b₀ = 1 + *jacobian.get_mut(row, col) += 1.0; // ∂T_out/∂b₀ = 1 } } if let Some(col) = b1_col { for row in 0..n_e { - *jacobian.get_mut(row, col) = self.inv_sqrt_energies[row]; // ∂T_out/∂b₁ = 1/√E + *jacobian.get_mut(row, col) += self.inv_sqrt_energies[row]; // ∂T_out/∂b₁ = 1/√E } } @@ -2561,4 +2572,110 @@ mod tests { true_params[2], ); } + + /// Central finite-difference column for one parameter, computed + /// straight from `evaluate` — an oracle independent of the model's + /// analytic Jacobian path. + fn fd_column(model: &dyn FitModel, params: &[f64], param_index: usize, h: f64) -> Vec { + let mut plus = params.to_vec(); + plus[param_index] += h; + let mut minus = params.to_vec(); + minus[param_index] -= h; + let y_plus = model.evaluate(&plus).unwrap(); + let y_minus = model.evaluate(&minus).unwrap(); + y_plus + .iter() + .zip(y_minus.iter()) + .map(|(&p, &m)| (p - m) / (2.0 * h)) + .collect() + } + + /// Aliased background indices (b0_index == b1_index): the analytic + /// Jacobian must ACCUMULATE both roles' contributions (1 + 1/√E), + /// matching finite differences — not overwrite one with the other. + #[test] + fn test_transmission_kl_background_aliased_indices_jacobian_matches_fd() { + let x: Vec = (0..10).map(|i| 1.0 + 0.5 * i as f64).collect(); + let inner = ExponentialModel { + x: x.clone(), + flux: vec![1000.0; x.len()], + }; + let inv_sqrt_energies: Vec = x.iter().map(|&e| 1.0 / e.sqrt()).collect(); + let wrapped = TransmissionKLBackgroundModel { + inner: &inner, + inv_sqrt_energies: inv_sqrt_energies.clone(), + b0_index: 1, + b1_index: 1, // deliberately aliased with b0 + n_params: 2, + }; + + let params = vec![0.4, 15.0]; + let y = wrapped.evaluate(¶ms).unwrap(); + // Inner param fixed, only the aliased background param free → + // analytic path. + let jac = wrapped + .analytical_jacobian(¶ms, &[1], &y) + .expect("background-only free set must stay on the analytic path"); + + let fd = fd_column(&wrapped, ¶ms, 1, 1e-6); + for (row, (&fd_val, &inv_sqrt_e)) in fd.iter().zip(inv_sqrt_energies.iter()).enumerate() { + let expected = 1.0 + inv_sqrt_e; + assert!( + (jac.get(row, 0) - expected).abs() < 1e-12, + "aliased ∂/∂b at row {row}: analytic {}, expected {expected}", + jac.get(row, 0), + ); + assert!( + (jac.get(row, 0) - fd_val).abs() < 1e-5, + "aliased ∂/∂b at row {row}: analytic {}, FD {fd_val}", + jac.get(row, 0), + ); + } + } + + /// Aliased scale indices (alpha1_index == alpha2_index) in the + /// sibling counts model: same accumulate-not-overwrite requirement, + /// derivative f·T + B against the finite-difference oracle. + #[test] + fn test_counts_background_scale_aliased_indices_jacobian_matches_fd() { + let x: Vec = (0..10).map(|i| 1.0 + 0.5 * i as f64).collect(); + let inner = ExponentialModel { + x: x.clone(), + flux: vec![1.0; x.len()], // inner transmission in [0,1] + }; + let flux: Vec = vec![1000.0; x.len()]; + let background: Vec = x.iter().map(|&e| 5.0 + e).collect(); + let wrapped = CountsBackgroundScaleModel { + transmission_model: &inner, + flux: &flux, + background: &background, + alpha1_index: 1, + alpha2_index: 1, // deliberately aliased with alpha1 + n_params: 2, + }; + + let params = vec![0.4, 1.2]; + let y = wrapped.evaluate(¶ms).unwrap(); + let t_inner = inner.evaluate(¶ms).unwrap(); + // Inner param fixed, only the aliased scale param free → + // analytic path. + let jac = wrapped + .analytical_jacobian(¶ms, &[1], &y) + .expect("scale-only free set must stay on the analytic path"); + + let fd = fd_column(&wrapped, ¶ms, 1, 1e-6); + for (row, &fd_val) in fd.iter().enumerate() { + let expected = flux[row] * t_inner[row] + background[row]; + assert!( + (jac.get(row, 0) - expected).abs() < 1e-9, + "aliased ∂/∂α at row {row}: analytic {}, expected {expected}", + jac.get(row, 0), + ); + assert!( + (jac.get(row, 0) - fd_val).abs() < 1e-3, + "aliased ∂/∂α at row {row}: analytic {}, FD {fd_val}", + jac.get(row, 0), + ); + } + } } From 79c04e91a32884a75b584d3fcba31b5a48bcb2c2 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 20:12:53 -0400 Subject: [PATCH 09/14] ci: schedule wheel-policy gate on rust-toolchain.toml changes + refresh contract evidence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 2 VERIFIED P1: maturin-action resolves the in-container manylinux compiler from the repo-root rust-toolchain.toml (verified in the pinned action source, getRustToolchain -> rustup override), so a pin-only bump PR would change the release-wheel compiler WITHOUT scheduling the manylinux_2_28 gate — exactly the tag-time breakage class (0.2.0 yank) the gate exists to catch at PR time. Add the file to the wheel-policy paths filter. Also refresh .harness/TASK.md evidence that went stale after the 1.97.0 -> 1.96.1 repin (R5 records the deviation + its P1 rationale; R6 re-evidenced with the final-tree gate run; R23 made count-free). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/wheel-policy.yml | 4 ++++ .harness/TASK.md | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheel-policy.yml b/.github/workflows/wheel-policy.yml index 5da40349..1265c1a0 100644 --- a/.github/workflows/wheel-policy.yml +++ b/.github/workflows/wheel-policy.yml @@ -23,6 +23,10 @@ on: - '.github/workflows/publish.yml' - '.github/workflows/wheel-policy.yml' - 'scripts/check_wheel_policy.sh' + # maturin-action resolves the in-container compiler from the repo + # root rust-toolchain.toml, so a pin-only bump changes the release + # wheel compiler and MUST schedule this gate. + - 'rust-toolchain.toml' permissions: contents: read diff --git a/.harness/TASK.md b/.harness/TASK.md index b8f7e90f..ad831f86 100644 --- a/.harness/TASK.md +++ b/.harness/TASK.md @@ -59,8 +59,8 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is - [x] R2 (PR1): "fix all 37 lints" (1× `clippy::question_mark` poisson.rs:1457; 36× `float_literal_f32_fallback` in apps/gui; plus linux-cfg candidates in file_dialog.rs ~347–770 incl. line 505) — check: scratchpad `cargo +1.97.0 clippy --workspace --exclude nereids-python --all-targets -- -D warnings` exits 0; ubuntu Clippy leg on the PR arbitrates cfg(linux) count. LOCAL DONE: `cargo clippy … -- -D warnings` → `CLIPPY -D warnings EXIT: 0` (darwin, 1.97.0); 37 fixes applied (`cargo clippy --fix` 36 + manual file_dialog.rs:505); CI ARBITER GREEN: `gh pr checks 707` → `Clippy (ubuntu-latest) pass 2m14s`, `Clippy (macos-latest) pass 3m36s`. - [x] R3 (PR1): "Repin both poison SHAs — ci.yml:167 taiki-e/install-action → v2.83.0 release SHA c7eb1735…; align security.yml:24 to the same" — check: `grep -rc c7eb1735 .github/workflows/*.yml` = ci.yml:1 + security.yml:1, old SHAs 50570a4f/4684b840 grep = 0; `gh api repos/taiki-e/install-action/git/ref/tags/v2.83.0` → c7eb1735f09259a5035e8e5d44b1406b1cddc0fb commit. - [x] R4 (PR1): "All 11 dtolnay/rust-toolchain steps … → master head SHA fa04a145…" — check: `grep -rc fa04a145 .github/workflows/*.yml` = 7+1+2+1 = 11; `grep -rn 29eef336` = 0; `gh api repos/dtolnay/rust-toolchain/branches/master` → fa04a1451ff1842e2626ccb99004d0195b455a88 "Add 1.96.1 patch release". -- [x] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input; `rustc --version` in repo dir under rustup → 1.97.0 (file honored). -- [x] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: `cargo fmt --all` → exit 0; clippy exit 0 (R2); `cargo test --workspace --exclude nereids-python` → `CARGO TEST EXIT: 0`, 1264 passed 0 failed; `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. +- [x] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input. DEVIATION (review round 1, VERIFIED P1): channel is 1.96.1, NOT the 1.97.0 this line quotes — rust-lang/rust#159035 is a P-critical x86-64 miscompile in 1.97.0 (1.96.1 unaffected; fix in no stable yet), so pinning 1.97.0 would lock release wheels onto a miscompiling compiler. Evidence at 1.96.1: `rust-toolchain.toml` channel = "1.96.1"; all 11 workflow inputs = "1.96.1" (`grep -rc '1.96.1' .github/workflows/*.yml` = 7+1+2+1); `rustc --version` in repo dir under rustup → rustc 1.96.1 (file honored). +- [x] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: gates re-run under the FINAL 1.96.1 pin on the full branch tree (post-repin, post-poisson fixes): `cargo fmt --all` → exit 0; `cargo clippy … -- -D warnings` → `CLIPPY EXIT: 0`; `cargo test --workspace --exclude nereids-python` → `TEST EXIT: 0`, 1268 passed 0 failed (+4 new tests vs main); `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. (Earlier 1.97.0-era run: 1264/0 + 212/1 — superseded by this one.) - [x] R7 (PR1): "create PR → STOP for user" — check: PR URL posted with summary; turn ends. Evidence: `gh pr create` → https://github.com/ornlneutronimaging/NEREIDS/pull/707; checkpoint-1 STOP taken in the same turn. - [?] R8 (PR2): "Cargo.toml: egui 0.35, eframe 0.35 (same feature list), egui_extras 0.35, egui_plot 0.36, egui-file-dialog 0.14.1 (not 0.14.0)"; lockstep + winit comments updated; rfd stays `=0.17.2` — check: `grep -nE 'egui|eframe|rfd' Cargo.toml` shows the pins; `cargo tree` single egui version. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? - [?] R9 (PR2): "Rename 8 deprecated show_inside → show" (app.rs:173,195; studio/mod.rs:49,619,1118; guided/sidebar.rs:31; widgets/toolbar.rs:21; widgets/statusbar.rs:17) + prose comment app.rs:98 — check: `grep -rn show_inside apps/gui/src/` returns 0 hits; clippy 1.97 exit 0. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? @@ -77,7 +77,7 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is - [?] R20 (end state): "main Dependabot Updates run green" — check: next `Dependabot Updates` github_actions run on main succeeds (`gh run list`), no `no such commit` error. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? - [?] R21 (PR3): "cargo update for the bumped crates; full pre-commit gate" — check: Cargo.lock updated for zip/sysinfo/egui-family only; fmt/clippy/test pass. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. - [?] R22 (process): "sequential PRs, each rebased on the previous merge" — check: PR2 branch created from post-PR1 main; PR3 from post-PR2 main. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. -- [x] R23 (process): "Commits GPG-signed, atomic" — check: git log --show-signature on the branch commits. Evidence (branch commits to date): `git log --show-signature 19d0ea2..HEAD` -> 5 commits, every one `gpg: Good signature from "Chen Zhang (kedokudo)"`; atomic (contract / lints / CI-pins / contract-updates split). Re-verify as later commits land. +- [x] R23 (process): "Commits GPG-signed, atomic" — check: git log --show-signature on the branch commits. Evidence: `git log --show-signature 19d0ea2..HEAD` → every branch commit `gpg: Good signature from "Chen Zhang (kedokudo)"` (count-free by design — re-run at each push; last verified at the round-2 fix push); atomic (one concern per commit: contract / lints / CI-pins / repin / poisson / gate-fix splits). - [?] R24 (PR3): "CI green" on PR3 — check: gh pr checks all pass. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. - [?] R25 (release guard): "the next release must run gh workflow run publish.yml -f dry_run=true first" — check: recorded in PR3 body + verified before next tag. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. From d8e96c92fb4a53e14b93383728b7bba6f1c9df82 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 21:28:31 -0400 Subject: [PATCH 10/14] docs(fitting): document background/scale index disjointness invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 3 (cross-confirmed by both LLM families at find time): the background-only analytic path returns Some for free sets of b0/b1 only — if such an index ALSO designates a parameter the inner dyn FitModel reads, the analytic column carries only the background contribution and silently omits dT_inner/dp (undetectable through dyn FitModel; the background indices are excluded from the inner free set by construction). On main the same misuse fell back to FD and was correct. No shipped configuration collides (pipeline appends kl_b0/kl_b1 as fresh indices), so per both reviewers the fix is the API contract: document the disjointness invariant on the pub index fields of BOTH wrappers; the supported b0==b1 aliasing (accumulating columns) is called out explicitly. Also: .harness/TASK.md now follows its own supersession protocol for the 1.97.0->1.96.1 repin — R5/R6 flipped to [?] pending explicit user approval, new R26 records the superseding 1.96.1 requirement + evidence. Gates (1.96.1): fmt OK; clippy -D warnings exit 0; nereids-fitting lib tests 193/0; cargo doc -D warnings exit 0 (doc-comment-only code diff — full workspace suite unchanged from previous run at 1268/0). Co-Authored-By: Claude Opus 4.8 --- .harness/TASK.md | 5 +++-- crates/nereids-fitting/src/poisson.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.harness/TASK.md b/.harness/TASK.md index ad831f86..728b4f42 100644 --- a/.harness/TASK.md +++ b/.harness/TASK.md @@ -59,8 +59,8 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is - [x] R2 (PR1): "fix all 37 lints" (1× `clippy::question_mark` poisson.rs:1457; 36× `float_literal_f32_fallback` in apps/gui; plus linux-cfg candidates in file_dialog.rs ~347–770 incl. line 505) — check: scratchpad `cargo +1.97.0 clippy --workspace --exclude nereids-python --all-targets -- -D warnings` exits 0; ubuntu Clippy leg on the PR arbitrates cfg(linux) count. LOCAL DONE: `cargo clippy … -- -D warnings` → `CLIPPY -D warnings EXIT: 0` (darwin, 1.97.0); 37 fixes applied (`cargo clippy --fix` 36 + manual file_dialog.rs:505); CI ARBITER GREEN: `gh pr checks 707` → `Clippy (ubuntu-latest) pass 2m14s`, `Clippy (macos-latest) pass 3m36s`. - [x] R3 (PR1): "Repin both poison SHAs — ci.yml:167 taiki-e/install-action → v2.83.0 release SHA c7eb1735…; align security.yml:24 to the same" — check: `grep -rc c7eb1735 .github/workflows/*.yml` = ci.yml:1 + security.yml:1, old SHAs 50570a4f/4684b840 grep = 0; `gh api repos/taiki-e/install-action/git/ref/tags/v2.83.0` → c7eb1735f09259a5035e8e5d44b1406b1cddc0fb commit. - [x] R4 (PR1): "All 11 dtolnay/rust-toolchain steps … → master head SHA fa04a145…" — check: `grep -rc fa04a145 .github/workflows/*.yml` = 7+1+2+1 = 11; `grep -rn 29eef336` = 0; `gh api repos/dtolnay/rust-toolchain/branches/master` → fa04a1451ff1842e2626ccb99004d0195b455a88 "Add 1.96.1 patch release". -- [x] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input. DEVIATION (review round 1, VERIFIED P1): channel is 1.96.1, NOT the 1.97.0 this line quotes — rust-lang/rust#159035 is a P-critical x86-64 miscompile in 1.97.0 (1.96.1 unaffected; fix in no stable yet), so pinning 1.97.0 would lock release wheels onto a miscompiling compiler. Evidence at 1.96.1: `rust-toolchain.toml` channel = "1.96.1"; all 11 workflow inputs = "1.96.1" (`grep -rc '1.96.1' .github/workflows/*.yml` = 7+1+2+1); `rustc --version` in repo dir under rustup → rustc 1.96.1 (file honored). -- [x] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: gates re-run under the FINAL 1.96.1 pin on the full branch tree (post-repin, post-poisson fixes): `cargo fmt --all` → exit 0; `cargo clippy … -- -D warnings` → `CLIPPY EXIT: 0`; `cargo test --workspace --exclude nereids-python` → `TEST EXIT: 0`, 1268 passed 0 failed (+4 new tests vs main); `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. (Earlier 1.97.0-era run: 1264/0 + 212/1 — superseded by this one.) +- [?] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input. DEVIATION (review round 1, VERIFIED P1): channel is 1.96.1, NOT the 1.97.0 this line quotes — rust-lang/rust#159035 is a P-critical x86-64 miscompile in 1.97.0 (1.96.1 unaffected; fix in no stable yet), so pinning 1.97.0 would lock release wheels onto a miscompiling compiler. Evidence at 1.96.1: `rust-toolchain.toml` channel = "1.96.1"; all 11 workflow inputs = "1.96.1" (`grep -rc '1.96.1' .github/workflows/*.yml` = 7+1+2+1); `rustc --version` in repo dir under rustup → rustc 1.96.1 (file honored). — BLOCKED ON USER: the line's quoted requirement says 1.97.0; delivered is 1.96.1 (round-1 VERIFIED P1). Approve the supersession so this line can be recorded as DROPPED-in-favor-of-R26 per contract rules. +- [?] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: gates re-run under the FINAL 1.96.1 pin on the full branch tree (post-repin, post-poisson fixes): `cargo fmt --all` → exit 0; `cargo clippy … -- -D warnings` → `CLIPPY EXIT: 0`; `cargo test --workspace --exclude nereids-python` → `TEST EXIT: 0`, 1268 passed 0 failed (+4 new tests vs main); `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. (Earlier 1.97.0-era run: 1264/0 + 212/1 — superseded by this one.) — BLOCKED ON USER: worded "under the 1.97 toolchain"; gates ran green under the delivered 1.96.1 pin. Approve with R5's supersession. - [x] R7 (PR1): "create PR → STOP for user" — check: PR URL posted with summary; turn ends. Evidence: `gh pr create` → https://github.com/ornlneutronimaging/NEREIDS/pull/707; checkpoint-1 STOP taken in the same turn. - [?] R8 (PR2): "Cargo.toml: egui 0.35, eframe 0.35 (same feature list), egui_extras 0.35, egui_plot 0.36, egui-file-dialog 0.14.1 (not 0.14.0)"; lockstep + winit comments updated; rfd stays `=0.17.2` — check: `grep -nE 'egui|eframe|rfd' Cargo.toml` shows the pins; `cargo tree` single egui version. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? - [?] R9 (PR2): "Rename 8 deprecated show_inside → show" (app.rs:173,195; studio/mod.rs:49,619,1118; guided/sidebar.rs:31; widgets/toolbar.rs:21; widgets/statusbar.rs:17) + prose comment app.rs:98 — check: `grep -rn show_inside apps/gui/src/` returns 0 hits; clippy 1.97 exit 0. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? @@ -80,6 +80,7 @@ egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is - [x] R23 (process): "Commits GPG-signed, atomic" — check: git log --show-signature on the branch commits. Evidence: `git log --show-signature 19d0ea2..HEAD` → every branch commit `gpg: Good signature from "Chen Zhang (kedokudo)"` (count-free by design — re-run at each push; last verified at the round-2 fix push); atomic (one concern per commit: contract / lints / CI-pins / repin / poisson / gate-fix splits). - [?] R24 (PR3): "CI green" on PR3 — check: gh pr checks all pass. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. - [?] R25 (release guard): "the next release must run gh workflow run publish.yml -f dry_run=true first" — check: recorded in PR3 body + verified before next tag. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. +- [x] R26 (PR1, supersedes R5/R6 wording — review round 1 VERIFIED P1): "Pin 1.96.1, not 1.97.0: rust-lang/rust#159035 is a P-critical x86-64 miscompile in 1.97.0 (1.96.1 unaffected; fix in no stable); release wheels for the ORNL x86-64 fleet build under this pin" — check: `rust-toolchain.toml` channel = "1.96.1"; 11 workflow inputs = "1.96.1"; full gates green under 1.96.1 (`CLIPPY EXIT: 0`, `TEST EXIT: 0` 1268/0, `PIXI EXIT: 0` 212/1); primary source independently fetched (issue OPEN, labels P-critical/I-miscompile/O-x86_64). ## Coverage note diff --git a/crates/nereids-fitting/src/poisson.rs b/crates/nereids-fitting/src/poisson.rs index 41fc5d4c..6620a457 100644 --- a/crates/nereids-fitting/src/poisson.rs +++ b/crates/nereids-fitting/src/poisson.rs @@ -1260,6 +1260,17 @@ impl<'a> crate::forward_model::ForwardModel for CountsModel<'a> { /// Y(E) = α₁ · [Φ(E) · T(θ)] + α₂ · B(E) /// /// where `α₁` and `α₂` are parameter-vector entries. +/// +/// ## Index invariant +/// +/// `alpha1_index` / `alpha2_index` must NOT designate a parameter index +/// the transmission model reads. The wrapper cannot detect such a +/// collision through `dyn FitModel`, and the analytic Jacobian excludes +/// the scale indices from the inner free set — a collided parameter +/// would get only the scale contribution, silently omitting ∂T/∂p. +/// (Sharing ONE parameter between the two scale roles, +/// `alpha1_index == alpha2_index`, IS supported: the columns +/// accumulate.) pub struct CountsBackgroundScaleModel<'a> { /// Underlying transmission model. pub transmission_model: &'a dyn FitModel, @@ -1268,8 +1279,10 @@ pub struct CountsBackgroundScaleModel<'a> { /// Detector background spectrum. pub background: &'a [f64], /// Index of α₁ in the parameter vector. + /// Must not be a parameter the transmission model reads (see struct docs). pub alpha1_index: usize, /// Index of α₂ in the parameter vector. + /// Must not be a parameter the transmission model reads (see struct docs). pub alpha2_index: usize, /// Total parameter count in the wrapped model. pub n_params: usize, @@ -1400,14 +1413,27 @@ impl<'a> crate::forward_model::ForwardModel for CountsBackgroundScaleModel<'a> { /// - ∂T_out/∂nₖ = ∂T_inner/∂nₖ = -σₖ(E)·T_inner(E) (same as bare model) /// - ∂T_out/∂b₀ = 1 /// - ∂T_out/∂b₁ = 1/√E +/// +/// ## Index invariant +/// +/// `b0_index` / `b1_index` must NOT designate a parameter index the +/// inner model reads. The wrapper cannot detect such a collision +/// through `dyn FitModel`, and the analytic Jacobian excludes the +/// background indices from the inner free set — a collided parameter +/// would get only the background contribution, silently omitting +/// ∂T_inner/∂p. (Sharing ONE parameter between the two background +/// roles, `b0_index == b1_index`, IS supported: the columns +/// accumulate.) pub struct TransmissionKLBackgroundModel<'a> { /// Underlying transmission model (density parameters only). pub inner: &'a dyn FitModel, /// Precomputed 1/√E for each energy bin. pub inv_sqrt_energies: Vec, /// Index of b₀ (constant background) in the parameter vector. + /// Must not be a parameter the inner model reads (see struct docs). pub b0_index: usize, /// Index of b₁ (1/√E background) in the parameter vector. + /// Must not be a parameter the inner model reads (see struct docs). pub b1_index: usize, /// Total parameter count in the wrapped model. pub n_params: usize, From f36b739b6da6f4ac65c9875fdb41621f23d27ef0 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Fri, 10 Jul 2026 22:58:43 -0400 Subject: [PATCH 11/14] fix(fitting): harden remaining wrapper Jacobians against aliased role indices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 4 P2 (parallel-path audit of the aliased-index class fixed in poisson.rs): NormalizedTransmissionModel and MultiplicativeBaselineModel filled their role columns via first-match if/else-if chains, so two role indices sharing one parameter (accepted by the pub constructors without validation) silently kept only the first role's derivative. Convert both chains to independent role checks that ACCUMULATE (+=) into the zeroed matrix — identical for distinct indices, correct for aliased ones — and document the index invariant on both structs (role-inner collisions stay undetectable through the filtered inner free set, same as the poisson wrappers). The two resolution-coupled fill sites (TransmissionModelMulti density/ temperature; EnergyScale t0/L_scale FD arms) keep their first-match layout — restructuring them for a degenerate case the pipeline cannot construct is not worth the risk — and now carry explicit distinct-indices-assumed comments pointing at the hardened pattern. Adds one aliased-index FD-oracle regression test per hardened wrapper (BackA==BackB pinned to exactly 1 + 1/sqrt(E); b0==b1 vs central FD). Gates (1.96.1): clippy -D warnings exit 0; cargo test 1270 passed 0 failed (+2); pixi run test-python 212 passed 1 skipped. Co-Authored-By: Claude Opus 4.8 --- .../nereids-fitting/src/transmission_model.rs | 213 ++++++++++++++++-- 1 file changed, 191 insertions(+), 22 deletions(-) diff --git a/crates/nereids-fitting/src/transmission_model.rs b/crates/nereids-fitting/src/transmission_model.rs index 0b7011fb..e9dabee7 100644 --- a/crates/nereids-fitting/src/transmission_model.rs +++ b/crates/nereids-fitting/src/transmission_model.rs @@ -1244,6 +1244,13 @@ impl FitModel for TransmissionFitModel { let t_for_deriv: &[f64] = t_unresolved.as_deref().unwrap_or(y_current); // ── Density columns: ∂T/∂N_g or ∂T_obs/∂N_g ── + // Role indices are assumed DISTINCT (first-match layout: the + // temperature column is skipped here and filled separately, so a + // parameter serving both roles would get only one contribution). + // The pipeline always constructs distinct indices; aliasing is + // not supported in this resolution-coupled fill — see + // NormalizedTransmissionModel's "Index invariant" for the + // accumulate-hardened pattern used by the simple wrappers. for (col, &fp_idx) in free_param_indices.iter().enumerate() { if temp_col == Some(col) { continue; @@ -1361,6 +1368,15 @@ impl FitModel for TransmissionFitModel { /// SAMMY fits up to 6 background terms; we implement all 6: /// Anorm, constant BackA, 1/√E term BackB, √E term BackC, /// exponential amplitude BackD, exponential decay BackF. +/// +/// ## Index invariant +/// +/// The role indices (`anorm_index`, `back_*_index`) must NOT designate +/// a parameter the inner model reads: the analytic Jacobian filters the +/// role indices out of the inner free set, so such a collision cannot +/// be detected and the column would silently omit Anorm × ∂T_inner/∂p. +/// Aliasing AMONG the role indices themselves IS supported — the +/// Jacobian columns accumulate. pub struct NormalizedTransmissionModel { /// The inner (pure Beer-Lambert) transmission model. inner: M, @@ -1567,33 +1583,53 @@ impl FitModel for NormalizedTransmissionModel { inner_col_map.insert(idx, col); } + // Independent role checks with accumulation (+=) rather than a + // first-match if/else-if chain: nothing forbids two role indices + // from aliasing, and evaluate() reads an aliased parameter for + // every role it occupies, so its derivative is the SUM of the + // matching columns. Distinct indices touch each column once on a + // zeroed matrix — identical to assignment. A role index colliding + // with an INNER-model parameter remains undetectable here (role + // indices are filtered out of inner_free_indices) — see the + // struct docs. for (col, &fp_idx) in free_param_indices.iter().enumerate() { + let mut matched = false; if fp_idx == self.anorm_index { // ∂T_out/∂Anorm = T_inner(E) for (i, &ti) in t_inner.iter().enumerate() { - *jacobian.get_mut(i, col) = ti; + *jacobian.get_mut(i, col) += ti; } - } else if fp_idx == self.back_a_index { + matched = true; + } + if fp_idx == self.back_a_index { // ∂T_out/∂BackA = 1 for i in 0..n_e { - *jacobian.get_mut(i, col) = 1.0; + *jacobian.get_mut(i, col) += 1.0; } - } else if fp_idx == self.back_b_index { + matched = true; + } + if fp_idx == self.back_b_index { // ∂T_out/∂BackB = 1/√E for (i, &inv_se) in self.inv_sqrt_energies.iter().enumerate() { - *jacobian.get_mut(i, col) = inv_se; + *jacobian.get_mut(i, col) += inv_se; } - } else if fp_idx == self.back_c_index { + matched = true; + } + if fp_idx == self.back_c_index { // ∂T_out/∂BackC = √E for (i, &se) in self.sqrt_energies.iter().enumerate() { - *jacobian.get_mut(i, col) = se; + *jacobian.get_mut(i, col) += se; } - } else if self.back_d_index == Some(fp_idx) { + matched = true; + } + if self.back_d_index == Some(fp_idx) { // ∂T_out/∂BackD = exp(−BackF / √E) for (i, &et) in exp_terms.iter().enumerate() { - *jacobian.get_mut(i, col) = et; + *jacobian.get_mut(i, col) += et; } - } else if self.back_f_index == Some(fp_idx) { + matched = true; + } + if self.back_f_index == Some(fp_idx) { // ∂T_out/∂BackF = −BackD × exp(−BackF / √E) / √E let back_d = params[self.back_d_index.unwrap()]; for (i, (&et, &inv_se)) in exp_terms @@ -1601,20 +1637,24 @@ impl FitModel for NormalizedTransmissionModel { .zip(self.inv_sqrt_energies.iter()) .enumerate() { - *jacobian.get_mut(i, col) = -back_d * et * inv_se; + *jacobian.get_mut(i, col) += -back_d * et * inv_se; } - } else if let Some(&inner_col) = inner_col_map.get(&fp_idx) { + matched = true; + } + if let Some(&inner_col) = inner_col_map.get(&fp_idx) { // Inner model parameter: ∂T_out/∂p = Anorm × ∂T_inner/∂p if let Some(ref jac) = inner_jac { for i in 0..n_e { - *jacobian.get_mut(i, col) = anorm * jac.get(i, inner_col); + *jacobian.get_mut(i, col) += anorm * jac.get(i, inner_col); } + matched = true; } else { // Inner model did not provide analytical Jacobian — // fall back to finite-difference for the whole thing. return None; } - } else { + } + if !matched { // Unknown parameter — should not happen, but fall back to FD. return None; } @@ -2392,6 +2432,11 @@ impl FitModel for EnergyScaleTransmissionModel { // byte-identically to `apply_resolution`. let density_plan = self.cached_resolution_plan(t0, l_scale, work_e); + // Role indices (t0/L_scale/temperature/densities) are assumed + // DISTINCT — first-match layout; aliasing is not supported in + // this FD-arm fill. See NormalizedTransmissionModel's "Index + // invariant" for the accumulate-hardened pattern used by the + // simple wrappers. for (col, &fp_idx) in free_param_indices.iter().enumerate() { // Temperature (issue #634), when free, is differentiated by the // per-coordinate central FD arm below — it is neither t0 nor @@ -2790,6 +2835,15 @@ pub fn baseline_reference_energy_active(energies: &[f64], active: Option<&[bool] /// supporting it buys nothing (`Anorm` would just play `b0`'s role at a /// rescaled value) and splits the normalization story across two knobs; /// the sanctioned combination is `Anorm` held fixed. +/// +/// ## Index invariant +/// +/// The baseline indices (`b0_index`, `b1_index`, `b2_index`) must NOT +/// designate a parameter the inner model reads: the analytic Jacobian +/// filters the baseline indices out of the inner free set, so such a +/// collision cannot be detected and the column would silently omit +/// B(E) × ∂T_inner/∂p. Aliasing AMONG the baseline indices themselves +/// IS supported — the Jacobian columns accumulate. pub struct MultiplicativeBaselineModel { /// The inner model (bare transmission, or the additive-background /// wrapper when both are configured). @@ -2936,34 +2990,52 @@ impl FitModel for MultiplicativeBaselineModel { } let mut jacobian = FlatMatrix::zeros(n_e, n_free); + // Independent role checks with accumulation (+=) rather than a + // first-match if/else-if chain: nothing forbids the baseline + // indices from aliasing, and baseline_at() reads an aliased + // parameter for every role it occupies, so its derivative is the + // SUM of the matching columns. Distinct indices touch each column + // once on a zeroed matrix — identical to assignment. A baseline + // index colliding with an INNER-model parameter remains + // undetectable here (baseline indices are filtered out of + // inner_free_indices) — see the struct docs. for (col, &fp_idx) in free_param_indices.iter().enumerate() { + let mut matched = false; if fp_idx == self.b0_index { // ∂y/∂b0 = T_inner for (i, &ti) in t_inner.iter().enumerate() { - *jacobian.get_mut(i, col) = ti; + *jacobian.get_mut(i, col) += ti; } - } else if fp_idx == self.b1_index { + matched = true; + } + if fp_idx == self.b1_index { // ∂y/∂b1 = z · T_inner for (i, &ti) in t_inner.iter().enumerate() { - *jacobian.get_mut(i, col) = self.ln_ratio[i] * ti; + *jacobian.get_mut(i, col) += self.ln_ratio[i] * ti; } - } else if fp_idx == self.b2_index { + matched = true; + } + if fp_idx == self.b2_index { // ∂y/∂b2 = z² · T_inner for (i, &ti) in t_inner.iter().enumerate() { - *jacobian.get_mut(i, col) = self.ln_ratio_sq[i] * ti; + *jacobian.get_mut(i, col) += self.ln_ratio_sq[i] * ti; } - } else if let Some(&inner_col) = inner_col_map.get(&fp_idx) { + matched = true; + } + if let Some(&inner_col) = inner_col_map.get(&fp_idx) { // Inner parameter: ∂y/∂p = B(E) · ∂T_inner/∂p if let Some(ref jac) = inner_jac { for i in 0..n_e { let b = self.baseline_at(params, i); - *jacobian.get_mut(i, col) = b * jac.get(i, inner_col); + *jacobian.get_mut(i, col) += b * jac.get(i, inner_col); } + matched = true; } else { // Inner has no analytic Jacobian — FD for everything. return None; } - } else { + } + if !matched { // Unknown parameter — should not happen; fall back to FD. return None; } @@ -4580,6 +4652,103 @@ mod tests { } } + /// Aliased role indices (BackA == BackB sharing one parameter): the + /// analytic Jacobian must ACCUMULATE both roles' contributions + /// (1 + 1/√E), matching central finite differences — not keep only + /// the first match. + #[test] + fn normalized_jacobian_aliased_role_indices_match_fd() { + let xs = vec![vec![1.0, 2.0, 3.0]]; + let inner = make_precomputed(xs, vec![0]); + + let energies = [4.0, 9.0, 16.0]; + // params: [density, Anorm, BackAB (shared), BackC] — BackA and + // BackB deliberately alias index 2. + let model = NormalizedTransmissionModel::new(inner, &energies, 1, 2, 2, 3); + + let params = [0.3, 0.95, 0.02, 0.005]; + let y = model.evaluate(¶ms).unwrap(); + let free: Vec = (0..4).collect(); + + let jac = model + .analytical_jacobian(¶ms, &free, &y) + .expect("analytical_jacobian should return Some"); + + let h = 1e-7; + for (col, &p_idx) in free.iter().enumerate() { + let mut p_plus = params; + let mut p_minus = params; + p_plus[p_idx] += h; + p_minus[p_idx] -= h; + let y_plus = model.evaluate(&p_plus).unwrap(); + let y_minus = model.evaluate(&p_minus).unwrap(); + for i in 0..3 { + let fd = (y_plus[i] - y_minus[i]) / (2.0 * h); + let ana = jac.get(i, col); + let err = (fd - ana).abs(); + let scale = fd.abs().max(ana.abs()).max(1e-10); + assert!( + err / scale < 1e-4, + "aliased Jacobian mismatch (row {i}, col {col}): FD={fd:.8}, \ + analytical={ana:.8}", + ); + } + } + // Pin the aliased column exactly: ∂/∂BackAB = 1 + 1/√E. + for (i, &e) in energies.iter().enumerate() { + let expected = 1.0 + 1.0 / e.sqrt(); + assert!( + (jac.get(i, 2) - expected).abs() < 1e-12, + "aliased column row {i}: {} vs expected {expected}", + jac.get(i, 2), + ); + } + } + + /// Aliased baseline indices (b0 == b1 sharing one parameter) in the + /// multiplicative wrapper: same accumulate-not-overwrite requirement, + /// derivative (1 + z)·T_inner against the FD oracle. + #[test] + fn multiplicative_baseline_aliased_indices_match_fd() { + let xs = vec![vec![1.0, 2.0, 3.0]]; + let inner = make_precomputed(xs, vec![0]); + + let energies = [4.0, 9.0, 16.0]; + let e_ref = baseline_reference_energy(&energies); + // params: [density, b01 (shared), b2] — b0 and b1 deliberately + // alias index 1. + let model = MultiplicativeBaselineModel::new(inner, &energies, e_ref, 1, 1, 2); + + let params = [0.3, 1.02, 0.01]; + let y = model.evaluate(¶ms).unwrap(); + let free: Vec = (0..3).collect(); + + let jac = model + .analytical_jacobian(¶ms, &free, &y) + .expect("analytical_jacobian should return Some"); + + let h = 1e-7; + for (col, &p_idx) in free.iter().enumerate() { + let mut p_plus = params; + let mut p_minus = params; + p_plus[p_idx] += h; + p_minus[p_idx] -= h; + let y_plus = model.evaluate(&p_plus).unwrap(); + let y_minus = model.evaluate(&p_minus).unwrap(); + for i in 0..3 { + let fd = (y_plus[i] - y_minus[i]) / (2.0 * h); + let ana = jac.get(i, col); + let err = (fd - ana).abs(); + let scale = fd.abs().max(ana.abs()).max(1e-10); + assert!( + err / scale < 1e-4, + "aliased baseline Jacobian mismatch (row {i}, col {col}): FD={fd:.8}, \ + analytical={ana:.8}", + ); + } + } + } + /// Verify that when some background params are fixed (not in /// free_param_indices), the Jacobian columns are correct. #[test] From c41d782aa48a9ddd196f3da4cb1f306eb092ea51 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Sat, 11 Jul 2026 07:00:09 -0400 Subject: [PATCH 12/14] =?UTF-8?q?chore:=20untrack=20.harness/=20=E2=80=94?= =?UTF-8?q?=20session-local=20dev=20state,=20not=20repo=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .gitignore | 3 ++ .harness/TASK.md | 92 ------------------------------------------------ 2 files changed, 3 insertions(+), 92 deletions(-) delete mode 100644 .harness/TASK.md diff --git a/.gitignore b/.gitignore index f6d430f8..13c69d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ tmp/ # do not track manuscript folders .manuscripts/ + +# Session-local agent-harness state (task contracts, verify gates) +.harness/ diff --git a/.harness/TASK.md b/.harness/TASK.md deleted file mode 100644 index 728b4f42..00000000 --- a/.harness/TASK.md +++ /dev/null @@ -1,92 +0,0 @@ -# Task contract - -## Source - -User message 1 (2026-07-10, verbatim): - -> We will be using this thread to conduct maintainece work for NEIREDS. -> -> there are massive amount of bot PR on github, all CI are failing, -> investigae and suggestion resolution - -User message 2 (after investigation presented options A/B/C, verbatim): - -> B, and the corresponding PR should also auto close the bot PRs - -Option B (as presented and chosen): "the full plan (clippy fix + coordinated -egui-0.35 PR + triage the majors)". The user-approved plan elaborating B is -`/Users/8cz/.claude/plans/harmonic-gathering-hejlsberg.md`; its deliverables -(quoted in the requirements below) are: - -> PR 1 — fix(ci): Rust 1.97 lints + repin orphaned action SHAs -> PR 2 — build(deps): coordinated egui 0.34→0.35 ecosystem -> (supersedes #681, #683, #684, #685, #686) -> PR 3 — build(deps): actions majors + zip 8 + sysinfo 0.39 -> (supersedes #677, #678, #679, #680, #682, #687) -> Process: create PR → STOP for user → /review-pipeline → STOP → merge on -> explicit approval → next PR rebases. -> End state: 11 bot PRs closed, main Dependabot Updates run green, next -> stable-Rust release can no longer redden unrelated PRs (pinned toolchain). - -## Rules (enforced by the stop gate) - -- One checkbox per requirement, quoting the Source's own words. Every - imperative sentence in the Source must map to a requirement line. -- Checkboxes must use exactly `- [ ]` — other bullet styles are - invisible to the gate. -- `[x]` requires evidence ON the line: the check command in backticks - and what it showed. The gate blocks `[x]` lines with no backticked - command; an unfilled `` in backticks does not count. -- If an item's recorded evidence is later invalidated (a retraction, a - failed re-check), flip it back to `- [ ]` — a checked line with dead - evidence is a false report. -- Dropping an item requires the user's explicit approval of that named - item: `- [-] DROPPED: (approved: "")`. The quoted - words must themselves address the dropped item — a generic "proceed" - or "sounds good" is never approval. The gate blocks `[-]` lines with - no `(approved:` quote. -- Blocked on the user? Mark it `- [?] — BLOCKED ON USER: - `, finish everything not blocked, then end the - turn asking about all `[?]` items together. -- Never delete or reword a requirement line. Discovered work is ADDED - as new lines, not swapped in. -- Commit this file unless the user objects — tampering must show in - git diff. - -## Requirements - -- [x] R1: "investigae and suggestion resolution" — three root causes identified with evidence and resolution plan approved — check: `for pr in 677 678 679 680 681 682 683 684 685 686 687; do gh pr checks $pr; done` + `gh run view` logs + local 1.97 repro (`CLIPPY EXIT: 101`, question_mark at poisson.rs:1457) + Dependabot log (`no such commit 50570a4f`); plan approved by user via ExitPlanMode. -- [x] R2 (PR1): "fix all 37 lints" (1× `clippy::question_mark` poisson.rs:1457; 36× `float_literal_f32_fallback` in apps/gui; plus linux-cfg candidates in file_dialog.rs ~347–770 incl. line 505) — check: scratchpad `cargo +1.97.0 clippy --workspace --exclude nereids-python --all-targets -- -D warnings` exits 0; ubuntu Clippy leg on the PR arbitrates cfg(linux) count. LOCAL DONE: `cargo clippy … -- -D warnings` → `CLIPPY -D warnings EXIT: 0` (darwin, 1.97.0); 37 fixes applied (`cargo clippy --fix` 36 + manual file_dialog.rs:505); CI ARBITER GREEN: `gh pr checks 707` → `Clippy (ubuntu-latest) pass 2m14s`, `Clippy (macos-latest) pass 3m36s`. -- [x] R3 (PR1): "Repin both poison SHAs — ci.yml:167 taiki-e/install-action → v2.83.0 release SHA c7eb1735…; align security.yml:24 to the same" — check: `grep -rc c7eb1735 .github/workflows/*.yml` = ci.yml:1 + security.yml:1, old SHAs 50570a4f/4684b840 grep = 0; `gh api repos/taiki-e/install-action/git/ref/tags/v2.83.0` → c7eb1735f09259a5035e8e5d44b1406b1cddc0fb commit. -- [x] R4 (PR1): "All 11 dtolnay/rust-toolchain steps … → master head SHA fa04a145…" — check: `grep -rc fa04a145 .github/workflows/*.yml` = 7+1+2+1 = 11; `grep -rn 29eef336` = 0; `gh api repos/dtolnay/rust-toolchain/branches/master` → fa04a1451ff1842e2626ccb99004d0195b455a88 "Add 1.96.1 patch release". -- [?] R5 (PR1): "add rust-toolchain.toml with channel = \"1.97.0\" only … align all `toolchain: stable` inputs → \"1.97.0\" … add the missing `toolchain:` input in security.yml" — check: file created (channel only + rationale/bump-ritual comment); `grep -rn 'toolchain: stable' .github/workflows/` = 0; security.yml:23-25 now has explicit input. DEVIATION (review round 1, VERIFIED P1): channel is 1.96.1, NOT the 1.97.0 this line quotes — rust-lang/rust#159035 is a P-critical x86-64 miscompile in 1.97.0 (1.96.1 unaffected; fix in no stable yet), so pinning 1.97.0 would lock release wheels onto a miscompiling compiler. Evidence at 1.96.1: `rust-toolchain.toml` channel = "1.96.1"; all 11 workflow inputs = "1.96.1" (`grep -rc '1.96.1' .github/workflows/*.yml` = 7+1+2+1); `rustc --version` in repo dir under rustup → rustc 1.96.1 (file honored). — BLOCKED ON USER: the line's quoted requirement says 1.97.0; delivered is 1.96.1 (round-1 VERIFIED P1). Approve the supersession so this line can be recorded as DROPPED-in-favor-of-R26 per contract rules. -- [?] R6 (PR1): "full pre-commit gate under the 1.97 scratchpad toolchain" + "`pixi run test-python`" — check: gates re-run under the FINAL 1.96.1 pin on the full branch tree (post-repin, post-poisson fixes): `cargo fmt --all` → exit 0; `cargo clippy … -- -D warnings` → `CLIPPY EXIT: 0`; `cargo test --workspace --exclude nereids-python` → `TEST EXIT: 0`, 1268 passed 0 failed (+4 new tests vs main); `pixi run test-python` → `PIXI EXIT: 0`, 212 passed 1 skipped. (Earlier 1.97.0-era run: 1264/0 + 212/1 — superseded by this one.) — BLOCKED ON USER: worded "under the 1.97 toolchain"; gates ran green under the delivered 1.96.1 pin. Approve with R5's supersession. -- [x] R7 (PR1): "create PR → STOP for user" — check: PR URL posted with summary; turn ends. Evidence: `gh pr create` → https://github.com/ornlneutronimaging/NEREIDS/pull/707; checkpoint-1 STOP taken in the same turn. -- [?] R8 (PR2): "Cargo.toml: egui 0.35, eframe 0.35 (same feature list), egui_extras 0.35, egui_plot 0.36, egui-file-dialog 0.14.1 (not 0.14.0)"; lockstep + winit comments updated; rfd stays `=0.17.2` — check: `grep -nE 'egui|eframe|rfd' Cargo.toml` shows the pins; `cargo tree` single egui version. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R9 (PR2): "Rename 8 deprecated show_inside → show" (app.rs:173,195; studio/mod.rs:49,619,1118; guided/sidebar.rs:31; widgets/toolbar.rs:21; widgets/statusbar.rs:17) + prose comment app.rs:98 — check: `grep -rn show_inside apps/gui/src/` returns 0 hits; clippy 1.97 exit 0. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R10 (PR2): "Re-verify the save-extension workaround comment in file_dialog.rs against egui-file-dialog 0.14 behavior" — check: comparison of 0.14.1 source vs comment recorded in PR body. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R11 (PR2): "dependabot.yml: egui group FIRST + exclude from the existing group" — check: yaml matches plan block; `patterns: ["egui*", "eframe"]`; cargo-minor-patch gains exclude-patterns. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R12 (PR2): "Gates: GTK tripwire + scripts/check_wheel_policy.sh + full pre-commit; manual GUI smoke test" — check: cargo-tree GTK assert clean; wheel-policy script passes; `cargo run -p nereids-gui` smoke observations recorded. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R13 (PR2): "create PR → STOP for user" — check: PR URL posted; turn ends. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R14 (PR3): "Replicate the bot SHA+comment diffs exactly: setup-pixi v0.10.0 (ci.yml + docs.yml), deploy-pages v5.0.0 (docs.yml), action-gh-release v3.0.1 (publish.yml), codecov-action v7.0.0 (ci.yml)" — check: `grep -rn` each new SHA; matches `gh pr diff 677/678/679/680`. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R15 (PR3): "Root Cargo.toml: zip = \"2\" → \"8\"" — check: grep + `cargo test -p nereids-endf` incl. `install_local_endf_accepts_zip_archive`. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R16 (PR3): "apps/gui/Cargo.toml: sysinfo = \"0.37\" → \"0.39\"" — check: grep + `cargo tree -d` shows single sysinfo copy. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R17 (PR3): "create PR → STOP for user" — check: PR URL posted; turn ends. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R18 (process): "/review-pipeline → STOP → merge on explicit approval" for each PR — check: review summary tables presented; merges only after the user's explicit word. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R19 (end state): "the corresponding PR should also auto close the bot PRs" — all 11 bot PRs closed — check: `gh pr list --author app/dependabot --state open` empty after PR3 merge (fallback `@dependabot close` comments recorded if used). — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R20 (end state): "main Dependabot Updates run green" — check: next `Dependabot Updates` github_actions run on main succeeds (`gh run list`), no `no such commit` error. — BLOCKED ON USER: PR #707 (PR1) is at the mandatory user checkpoint (repo CLAUDE.md: create PR -> STOP; the user's next message is the gate), and the approved plan sequences this work after PR1 merges. Approve /review-pipeline on #707, then merge, to unblock? -- [?] R21 (PR3): "cargo update for the bumped crates; full pre-commit gate" — check: Cargo.lock updated for zip/sysinfo/egui-family only; fmt/clippy/test pass. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. -- [?] R22 (process): "sequential PRs, each rebased on the previous merge" — check: PR2 branch created from post-PR1 main; PR3 from post-PR2 main. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. -- [x] R23 (process): "Commits GPG-signed, atomic" — check: git log --show-signature on the branch commits. Evidence: `git log --show-signature 19d0ea2..HEAD` → every branch commit `gpg: Good signature from "Chen Zhang (kedokudo)"` (count-free by design — re-run at each push; last verified at the round-2 fix push); atomic (one concern per commit: contract / lints / CI-pins / repin / poisson / gate-fix splits). -- [?] R24 (PR3): "CI green" on PR3 — check: gh pr checks all pass. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. -- [?] R25 (release guard): "the next release must run gh workflow run publish.yml -f dry_run=true first" — check: recorded in PR3 body + verified before next tag. — BLOCKED ON USER: PR #707 (PR1) is mid-review-pipeline; PR2/PR3 are sequenced behind user-gated merges (create->STOP, review->STOP, merge on explicit approval). Unblocks as the user advances each gate. -- [x] R26 (PR1, supersedes R5/R6 wording — review round 1 VERIFIED P1): "Pin 1.96.1, not 1.97.0: rust-lang/rust#159035 is a P-critical x86-64 miscompile in 1.97.0 (1.96.1 unaffected; fix in no stable); release wheels for the ORNL x86-64 fleet build under this pin" — check: `rust-toolchain.toml` channel = "1.96.1"; 11 workflow inputs = "1.96.1"; full gates green under 1.96.1 (`CLIPPY EXIT: 0`, `TEST EXIT: 0` 1268/0, `PIXI EXIT: 0` 212/1); primary source independently fetched (issue OPEN, labels P-critical/I-miscompile/O-x86_64). - -## Coverage note - -Source sentence mapping: "We will be using this thread to conduct -maintainece work" — session-scoping statement, no directive. "there are -massive amount of bot PR … investigae and suggestion resolution" → R1. -"B" (chosen plan) → R2–R18, R21–R25. "the corresponding PR should also auto close -the bot PRs" → R19 (+ supersedes lists in R8/R14–R16). Plan end-state -line → R19, R20. From 11216b35ceac2a7fd34e128533fc8eaf65305bea Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Sat, 11 Jul 2026 07:02:45 -0400 Subject: [PATCH 13/14] docs(toolchain): correct miscompile-scope rationale for the 1.96.1 pin rust-lang/rust#159035's underlying LLVM codegen issue predates 1.97 (LLVM 20 era); 1.97.0's enum-tag change (rust-lang/rust#155850) makes the trigger condition far more common, and the issue's reproducer passes on 1.96.1. The previous comment overclaimed '1.96.1 is not affected'. Pin choice unchanged: 1.96.1 matches what main already shipped with and avoids the widened 1.97 trigger. Correction prompted by external review on the PR (thanks @riking). Co-Authored-By: Claude Opus 4.8 --- rust-toolchain.toml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 6eed91f0..9c5b9da5 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -7,12 +7,14 @@ # toolchain bumps deliberate, reviewed changes. # # Why 1.96.1 and not 1.97.0: rust-lang/rust#159035 — a P-critical -# x86-64 miscompile in 1.97.0 (optimized safe code segfaults; an LLVM -# regression from stable to stable; 1.96.1 is not affected). Release -# wheels for the ORNL x86-64 RHEL fleet are built under this pin, so -# 1.97.0 is unacceptable until fixed. Bump this pin to the first -# stable release containing the fix (the revert of -# rust-lang/rust#155850, merged upstream via rust-lang/rust#159047). +# x86-64 miscompile (optimized safe code segfaults). The underlying +# LLVM codegen issue predates 1.97 (LLVM 20 era, ~1.87+), but 1.97.0's +# enum-tag change (rust-lang/rust#155850) makes the trigger condition +# far more common; the issue's reproducer passes on 1.96.1. Release +# wheels for the ORNL x86-64 RHEL fleet are built under this pin: +# 1.96.1 matches what main already shipped with and avoids the widened +# 1.97 trigger. Bump to the first stable containing the #155850 revert +# (rust-lang/rust#159047) and/or the upstream LLVM fix. # # Bump ritual: update `channel` here, update the matching # `toolchain: "X.Y.Z"` inputs in .github/workflows/*.yml (they only From 812311328e086e6d5a81d9f5e5933e0a01a22445 Mon Sep 17 00:00:00 2001 From: Chen Zhang Date: Sat, 11 Jul 2026 07:02:45 -0400 Subject: [PATCH 14/14] docs(claude): require concise professional PR descriptions Root-cause fix for essay-style PR bodies: no description-style rule existed, and the docs-hygiene section directed audit/investigation memos INTO the PR body. New Commit Style rule: short what/why + validation + risk; no per-edit inventories (the diff shows those), no review-round narratives. Long-form memos move to PR comments or commit messages; docs-hygiene pointers updated accordingly. Co-Authored-By: Claude Opus 4.8 --- .claude/CLAUDE.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8670e543..80ec7019 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -27,6 +27,12 @@ targeted `Edit` patches. Do not suppress clippy warnings with `#[allow(...)]`. - **GPG-signed commits**: always use `git commit -S`. The GPG agent is working. - **Atomic commits**: commit early and often. Huge monolithic commits make it hard to catch off-rail behaviour. +- **PR descriptions are concise and professional**: a short summary of what + changed and why, the validation performed, and any risk or follow-up — + a few sentences or bullets total. Do NOT inventory individual edits (the + diff shows those), narrate review rounds, or paste investigation memos. + Long-form audit/investigation material goes in a PR **comment** or the + commit messages, never the description. - **No temp file litter**: clean up all one-off scripts immediately. - **Do not touch `.claude/worktrees/`**: this directory is managed by Claude Code for isolated worktree sessions. Never delete, modify, or @@ -158,12 +164,13 @@ when working single-threaded on one feature branch. **Anything else does not belong in `docs/`.** In particular: - **Audit memos** (impact analysis of a bug fix, which research scripts - were affected, etc.) — belong in the PR body. When the PR merges, - the memo is preserved in the commit + PR history where it is actually - searchable. Do not create `docs/audit/` or similar. + were affected, etc.) — belong in a PR **comment**, never the PR + description (see Commit Style). When the PR merges, the memo is + preserved in the PR history where it is actually searchable. Do not + create `docs/audit/` or similar. - **Investigation / ablation / debugging notes** (step-by-step inquiry into a performance question, correctness hypothesis, etc.) — belong in - `.research/` (gitignored; session-private) or in the PR body if the + `.research/` (gitignored; session-private) or in a PR comment if the investigation directly motivates a concrete code change. - **Design specs / requirements drafts** — belong inline with the code they specify (rustdoc on the struct / module they describe) or in @@ -173,7 +180,7 @@ when working single-threaded on one feature branch. If I feel compelled to commit a memo under `docs/`, I stop and check: (a) is this user-facing reference that someone reading the project cold will want to read? If yes, it fits one of the subtrees above. -If no, it belongs in `.research/` or the PR body. **When in doubt, +If no, it belongs in `.research/` or a PR comment. **When in doubt, ask the user before creating the file** — do not invent a new `docs//` directory to make the artifact commit-able.