From aa4787d1c449655273ef0c89776c7943c5af9a6a Mon Sep 17 00:00:00 2001 From: SessionLedger Bot Date: Sat, 8 Aug 2026 02:00:04 -0700 Subject: [PATCH 1/4] fix: tighten per-CodeRabbit review + harden rootless-matrix policy scoping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #428 + #429 carrying the CodeRabbit review items that landed after #429's merge. Rebased onto origin/main (which now carries both PRs) to keep the deltas minimal. 1) crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs — redesign the session strategy around per-message Option timestamps (instead of session-wide) so the detect_unfinished.find_map(|m| m.ts_ms).rev() projection is exercised naturally. Add a 7th property (unfinished_items_last_activity_matches_session_max_ts) that asserts the projected last_activity_ms matches the session's reverse-walk max known ts_ms. Tighten orders_known_timestamps_descending to retain all items (including None) and verify no Some(ts) ever appears after a None (None is the 'unknown last activity' sentinel and must sort last). 2) tests/alloc_profile.rs — narrow non-Windows fallback to NotFound. Hard-panic on any other io::ErrorKind (permission, broken pipe, etc.) since those signal a misconfigured test environment rather than the portable-pwsh-missing case. 3) scripts/rootless-nonet-check.ps1 — throw when the rootless-nonet-policy block is absent (instead of treating a failed match as success). The block is a documented C04 L40 anchor; absence is a drift, not an acceptable state. 4) scripts/rootless-matrix-check.ps1 — same throw-on-absent fix plus the '^ rootless-matrix-policy:.*?continue-on-error:\s*true' regex was bleeding into the next security job's 'continue-on- error: true' (security starts on the line right after the matrix policy job). Replaced with [regex]::Match + a proper terminator ('(?=^ [A-Za-z][\w-]*:\s|\z)') so the check scopes to just the policy block. Mirrors the fix already applied to rootless-nonet-check.ps1 in #429. 5) .github/workflows/ci.yml — pin actions/checkout to the immutable 3d3c42e5 SHA + persist-credentials: false for both policy jobs (rootless-nonet-policy, rootless-matrix-policy). Brings the policy jobs in line with the rest of the repo's checked-in workflows. --- .github/workflows/ci.yml | 8 +- .../tests/properties_viewer_unfinished_tab.rs | 96 ++++++++++++++----- scripts/rootless-matrix-check.ps1 | 9 +- scripts/rootless-nonet-check.ps1 | 5 +- tests/alloc_profile.rs | 15 +-- 5 files changed, 100 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84a29744..6d1686ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -200,7 +200,9 @@ jobs: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false - name: rootless / no-net SelfCheck shell: pwsh run: ./scripts/rootless-nonet-check.ps1 -SelfCheck @@ -250,7 +252,9 @@ jobs: name: rootless-only matrix policy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false - name: assert rootless-only OCI runner matrix scaffold anchors shell: pwsh run: ./scripts/rootless-matrix-check.ps1 -SelfCheck diff --git a/crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs b/crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs index e457c260..de38fbe6 100644 --- a/crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs +++ b/crates/sl-viewer/tests/properties_viewer_unfinished_tab.rs @@ -21,7 +21,7 @@ use proptest::prelude::*; use session_ledger::domain::session::{Corpus, Message, Role, Session}; -use session_ledger::domain::worklog::{UnfinishedReason, UnfinishedWorkItem}; +use session_ledger::domain::worklog::UnfinishedReason; use sl_viewer::unfinished_tab::{reason_label, unfinished_items}; // ── strategies ───────────────────────────────────────────────────────────── @@ -30,19 +30,18 @@ fn session_strategy() -> impl Strategy { ( // session_id — non-empty, identifier-shaped. "[a-zA-Z0-9_-]{1,16}", - // 0..6 messages; mix of roles + content. Bounded so the - // `detect_unfinished` projection runs cheaply. + // 0..6 messages; each message has its own independent + // `Option` ts_ms so the `detect_unfinished` projection's + // `find_map(|m| m.ts_ms).rev()` contract is exercised naturally + // (per-message timestamps, not a session-wide value). prop::collection::vec( - (0u8..5, "[ -~]{1,40}"), + (0u8..5, "[ -~]{1,40}", prop::option::of(0i64..1_000_000_000_000)), 0..6, ), - // last_activity_ms — Some(i64) or None. None is the "unknown - // last activity" sentinel the worklog projector uses. - prop::option::of(0i64..1_000_000_000_000), ) - .prop_map(|(session_id, messages, ts_ms)| { + .prop_map(|(session_id, messages)| { let mut session = Session::new(format!("sess-{session_id}"), Corpus::Forge); - for (role_idx, content) in messages { + for (role_idx, content, ts_ms) in messages { let role = match role_idx % 5 { 0 => Role::User, 1 => Role::Assistant, @@ -109,26 +108,46 @@ proptest! { } /// Property: `unfinished_items` orders known timestamps descending. - /// Two items with the same `last_activity_ms` may appear in any order - /// (we don't constrain the tiebreak here; see next property). + /// Two invariants: + /// (a) within the sliding window of items with a known timestamp, + /// timestamps are non-increasing; + /// (b) once an item with `last_activity_ms == None` appears, no + /// later item may carry a known timestamp (None is the + /// "unknown last activity" sentinel and always sorts last). #[test] fn unfinished_items_orders_known_timestamps_descending( sessions in prop::collection::vec(session_strategy(), 1..10), ) { let items = unfinished_items(&sessions); - // Filter to items with a known timestamp so the descending - // invariant applies cleanly. - let with_ts: Vec<&UnfinishedWorkItem> = - items.iter().filter(|i| i.last_activity_ms.is_some()).collect(); - - for window in with_ts.windows(2) { - let prev = window[0].last_activity_ms.expect("filtered Some"); - let next = window[1].last_activity_ms.expect("filtered Some"); - prop_assert!( - prev >= next, - "known timestamps must be descending: {prev} came before {next}", - ); + // (a) descending among items with a known timestamp. + for window in items.windows(2) { + let prev = &window[0]; + let next = &window[1]; + match (prev.last_activity_ms, next.last_activity_ms) { + (Some(a), Some(b)) => { + prop_assert!( + a >= b, + "known timestamps must be non-increasing: {a} came before {b}", + ); + } + _ => {} + } + } + + // (b) no Some(ts) appears after a None. + let mut seen_none = false; + for item in &items { + if seen_none { + prop_assert!( + item.last_activity_ms.is_none(), + "Some(ts) found after None: {:?} appeared after a None item", + item.last_activity_ms, + ); + } + if item.last_activity_ms.is_none() { + seen_none = true; + } } } @@ -177,4 +196,35 @@ proptest! { "appending sessions must not lose items: base={base_items}, combined={combined_items}", ); } + + /// Property: for each projected item, `last_activity_ms` equals the + /// maximum known `ts_ms` over the *session's* messages, or `None` + /// if none of the session's messages carried a timestamp. This pins + /// the per-message → projected-Item reduction explicitly (the unit + /// tests in `domain/worklog.rs` cover specific values; this property + /// pins the projection over many shapes). + #[test] + fn unfinished_items_last_activity_matches_session_max_ts( + sessions in prop::collection::vec(session_strategy(), 0..8), + ) { + let items = unfinished_items(&sessions); + + for item in &items { + // Reconstruct the source session by id. + let session = sessions + .iter() + .find(|s| s.id == item.session_id) + .expect("projected item must reference an input session"); + + let expected_last_activity_ms = + session.messages.iter().rev().find_map(|m| m.ts_ms); + + prop_assert_eq!( + item.last_activity_ms, + expected_last_activity_ms, + "session {}: projected last_activity_ms must equal session max known ts_ms", + session.id, + ); + } + } } diff --git a/scripts/rootless-matrix-check.ps1 b/scripts/rootless-matrix-check.ps1 index a427b912..02e8206c 100644 --- a/scripts/rootless-matrix-check.ps1 +++ b/scripts/rootless-matrix-check.ps1 @@ -149,7 +149,14 @@ if ($securityWf -match '(?ms)^ rootless-matrix:(.*?)(?=^ [a-z][a-z0-9-]*:)') { } [void](Write-Check -Label "security.yml rootless-matrix job is blocking when present" -Ok $true) -if ($ciWf -match '(?ms)^ rootless-matrix-policy:.*?continue-on-error:\s*true') { +if (-not ($ciWf -match '(?ms)^ rootless-matrix-policy:')) { + throw "ci.yml must define a rootless-matrix-policy job block (C04 L40 cross-reference anchor)." +} +$matrixPolicyBlockMatch = [regex]::Match( + $ciWf, + '(?ms)^ rootless-matrix-policy:.*?(?=^ [A-Za-z][\w-]*:\s|\z)' +) +if ($matrixPolicyBlockMatch.Success -and $matrixPolicyBlockMatch.Value -match 'continue-on-error:\s*true') { throw "ci.yml rootless-matrix-policy job must be blocking (no continue-on-error)." } [void](Write-Check -Label "ci.yml rootless-matrix-policy job is blocking when present" -Ok $true) diff --git a/scripts/rootless-nonet-check.ps1 b/scripts/rootless-nonet-check.ps1 index be461f0c..ea93ee40 100644 --- a/scripts/rootless-nonet-check.ps1 +++ b/scripts/rootless-nonet-check.ps1 @@ -139,7 +139,10 @@ $policyBlockMatch = [regex]::Match( $ciWf, '(?ms)^ rootless-nonet-policy:.*?(?=^ [A-Za-z][\w-]*:\s|\z)' ) -if ($policyBlockMatch.Success -and $policyBlockMatch.Value -match 'continue-on-error:\s*true') { +if (-not $policyBlockMatch.Success) { + throw "ci.yml must define a rootless-nonet-policy job block (C04 L40 cross-reference anchor)." +} +if ($policyBlockMatch.Value -match 'continue-on-error:\s*true') { throw "ci.yml rootless-nonet-policy job must be blocking (no continue-on-error)." } [void](Write-Check -Label "ci.yml rootless-nonet-policy job is blocking when present" -Ok $true) diff --git a/tests/alloc_profile.rs b/tests/alloc_profile.rs index 1787096c..64fd7caf 100644 --- a/tests/alloc_profile.rs +++ b/tests/alloc_profile.rs @@ -82,14 +82,15 @@ fn alloc_profile_script_self_check_parses_args_and_ceilings() { assert!(stdout.contains("Profiler: dhat"), "expected profiler echo, got:\n{stdout}"); } Err(error) => { - // Windows can't fall back to a portable load + print, so the - // spawn failure is unrecoverable there. Other targets run the - // portable fallback below. The clippy `panic_in_if_then` lint - // requires the if-then to have an else branch — fold the - // fallback into `else` so the panic sits on the windows-only path. + // On non-Windows targets, a `NotFound` error means pwsh isn't + // installed — fall back to the portable SelfCheck. Any other + // I/O error (permission, broken pipe, etc.) is treated as a + // hard failure since it likely means the test environment is + // misconfigured (e.g., spawn denial, missing CWD). Windows has + // no portable fallback path, so any spawn failure is fatal. if cfg!(target_os = "windows") { panic!("failed to spawn pwsh for self-check: {error}"); - } else { + } else if error.kind() == std::io::ErrorKind::NotFound { let (max_bytes, total_blocks) = load_profile(); println!( "pwsh unavailable; running portable alloc-profile SelfCheck fallback.\nSelf-check passed\nMax bytes ceiling: {max_bytes}\nTotal blocks ceiling: {total_blocks}\nProfiler: dhat" @@ -102,6 +103,8 @@ fn alloc_profile_script_self_check_parses_args_and_ceilings() { total_blocks >= 1_000, "total_blocks ceiling should stay generous (got {total_blocks})" ); + } else { + panic!("failed to spawn pwsh for self-check (non-NotFound): {error}"); } } } From 6abd0391ed4e505f0960ceb845b99bd8e5abb31b Mon Sep 17 00:00:00 2001 From: SessionLedger Bot Date: Sat, 8 Aug 2026 02:15:28 -0700 Subject: [PATCH 2/4] fix(viewer): use WebExportProvider::default_subdir instead of hardcoded literals The `WebExportProvider::default_subdir` method has been a dead_code warning since it was added (sl-viewer help unit tests compile the library with RUSTFLAGS=-D warnings, so the lint fails the PR gate). Drive the `defaults` array in `web_export_roots_with_env` from `default_subdir` instead of repeating the literal strings, which both removes the dead_code error and keeps the canonical name table in one place. `default_subdir` becomes `pub` so the function is reachable from outside the impl block via the method path used in `defaults`. Pre-existing on main; surfaced when running `cargo test cli_help` under `-D warnings` on the viewer-unfinished-tab-fixes branch. --- crates/sl-viewer/src/web_exports.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/sl-viewer/src/web_exports.rs b/crates/sl-viewer/src/web_exports.rs index 507f9840..b2b9d407 100644 --- a/crates/sl-viewer/src/web_exports.rs +++ b/crates/sl-viewer/src/web_exports.rs @@ -45,7 +45,7 @@ impl WebExportProvider { } /// The default subdirectory under `~/Downloads` for this provider's exports. - fn default_subdir(self) -> &'static str { + pub fn default_subdir(self) -> &'static str { match self { WebExportProvider::ChatGpt => "ChatGPT", WebExportProvider::Claude => "Claude", @@ -69,9 +69,18 @@ pub fn web_export_roots_with_env( }; let defaults = [ - (WebExportProvider::ChatGpt, home.join("Downloads").join("ChatGPT")), - (WebExportProvider::Claude, home.join("Downloads").join("Claude")), - (WebExportProvider::Gemini, home.join("Downloads").join("Gemini")), + ( + WebExportProvider::ChatGpt, + home.join("Downloads").join(WebExportProvider::ChatGpt.default_subdir()), + ), + ( + WebExportProvider::Claude, + home.join("Downloads").join(WebExportProvider::Claude.default_subdir()), + ), + ( + WebExportProvider::Gemini, + home.join("Downloads").join(WebExportProvider::Gemini.default_subdir()), + ), ]; if !explicit_list.is_empty() { From fa243faa78fc97589ef10862f65bf579a8bdaf53 Mon Sep 17 00:00:00 2001 From: SessionLedger Bot Date: Sat, 8 Aug 2026 02:22:40 -0700 Subject: [PATCH 3/4] fix(ci): align hermetic.yml reusable workflow pin with documented SHA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `scripts/reusable-provenance-check.ps1 -SelfCheck` enforces that every caller workflow pins the reusable hermetic build workflow to the SHA documented in `docs/ops/reusable-hermetic-pin.{md,json}` (currently `ec8916547e5678f72fe6894509249f9b23367b80`). `hermetic.yml` was pinned to `a8db485c046f9efab8ee51f25edb8f2458c95694` instead — the documented and the in-file pins drifted. Update the in-file pin to the documented SHA so the C06 L53 anchor holds. Pre-existing on main; surfaced when running `hermetic · reusable workflow provenance (soft)` on the viewer-unfinished-tab-fixes branch. --- .github/workflows/hermetic.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/hermetic.yml b/.github/workflows/hermetic.yml index 1d2f19fa..93dd6d7f 100644 --- a/.github/workflows/hermetic.yml +++ b/.github/workflows/hermetic.yml @@ -69,9 +69,9 @@ jobs: run: ./scripts/reusable-provenance-check.ps1 -SelfCheck # Reusable workflow — job-level call, pinned to SessionLedger commit SHA. - # Pin: ec8916547e5678f72fe6894509249f9b23367b80 (see docs/ops/reusable-hermetic-pin.md) + # Pin: ec8916547e5678f72fe6894509249f9b23367b80 (see docs/ops/reusable-hermetic-pin.md) sl-daemon-offline-container: name: sl-daemon · repository builder image offline build - uses: KooshaPari/SessionLedger/.github/workflows/reusable-hermetic-build.yml@a8db485c046f9efab8ee51f25edb8f2458c95694 + uses: KooshaPari/SessionLedger/.github/workflows/reusable-hermetic-build.yml@ec8916547e5678f72fe6894509249f9b23367b80 with: - builder_image_digest: sha256:16381cf25d89fd5dc8a904ff4a7b8d4660a856ed9738b8a7e879d816439ce2a5 + builder_image_digest: sha256:16381cf25d89fd5dc8a904ff4a7b8d4660a856ed9738b8a7e879d816439ce2a5 From f726e1c9ab116f3e3c37aafd438d17beadfe44b6 Mon Sep 17 00:00:00 2001 From: SessionLedger Bot Date: Sat, 8 Aug 2026 02:23:06 -0700 Subject: [PATCH 4/4] chore(changelog): WBS-6.2 #432 CI drift follow-ups --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 611d3edc..d975c8b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/); versioning is [SemVer]( - CI drift cleanups (WBS-6.2 #428): `scripts/fuzz-cadence-check.ps1` re-points the "PR smoke stays short" anchor from `ci.yml` (10 s budget) to `fuzz-blocking.yml` (30 s budget) since the PR smoke was consolidated there. `scripts/rootless-nonet-check.ps1` + `.github/workflows/ci.yml` restore the documented `rootless-nonet-policy` cross-reference smoke job, with the script's regex tightened so `continue-on-error` detection can't bleed across jobs. `tests/alloc_profile.rs` + `tests/replay_breadth.rs` clear `clippy::panic_in_if_then` / `clippy::unnecessary_trailing_comma` under `--all-targets --all-features`. +- CI drift cleanups (WBS-6.2 #432 follow-up): `scripts/rootless-matrix-check.ps1` `^ rootless-matrix-policy:.*?continue-on-error:\s*true` regex was bleeding across jobs into the next `security:` job's `continue-on-error: true`; replaced with `[regex]::Match` + a proper terminator (`(?=^ [A-Za-z][\w-]*:\s|\z)`) so the check scopes to just the policy block. Same script now throws when the policy block is absent (was treating a failed match as success). `.github/workflows/ci.yml` pins `actions/checkout` to the immutable `3d3c42e5` SHA + `persist-credentials: false` for both policy jobs. `.github/workflows/hermetic.yml` aligns its reusable-workflow pin to the documented `ec891654` SHA (was `a8db485` — drift between pin doc + caller). `crates/sl-viewer/src/web_exports.rs` uses `WebExportProvider::default_subdir` to populate the `defaults` array in `web_export_roots_with_env` instead of repeating literal strings, fixing a `dead_code` warning that broke `cargo test cli_help` under `-D warnings`. + - Wave-44 plan landed: `WAVE44_SCOPE.md` + `docs/ops/WAVE44_PERT.md` enumerate 6 close-out lanes (3 machine, 3 human-gated) for the 6 unpaid residuals from Wave-43 (396/402 → 402/402 target). Theme: stack-stability closure + i18n migration + eval coverage + supply-chain signing. - Wave-44 reaudit (Wave-44-D): `audit/SCORECARD.md` refresh at commit `13c974f7` (machine-w44-reaudit); `docs/ops/TRACEABILITY.json` overall_audit wave=Wave-44 commit=13c974f7 (conservative hold at 396/402); `docs/ops/GAP_QA_MATRIX.md` C00 + C08 + PLAN-W8-B rows reflect Wave-44 closure (#368 W44-B6 corpus / #372 W44-B1 loom / #373 PERT correction). 2 of 3 machine lanes shipped 2026-07-24; remaining 6 raw pts across C04 L36 / C08 L76 / C11 L110.