From 8aed4c0e9fb521f1097dca9810b2787785aa4df9 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 16:29:12 +0200 Subject: [PATCH 1/7] chore(flow): changelog.d/ fragments + gitignore pmat db-shm/wal (Refs #5761) FLOW-009 RH-1/RH-2/RH-4: one changelog.d/.md per PR, schema-checked, aggregated into CHANGELOG [Unreleased] at release. Roadmap fragments stay docs/roadmaps/entries/ (already gated by roadmap-fragment-parity). No new CI step. Agent: aprender-w5018 Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 2 + changelog.d/README.md | 25 ++++++++ scripts/changelog_fragments.sh | 110 +++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 changelog.d/README.md create mode 100755 scripts/changelog_fragments.sh diff --git a/.gitignore b/.gitignore index b524a2aec6..2073487e82 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,5 @@ docs/audits/quorum-*.json.lanes/ # PVL-001 EV-8a (#4202): `pv discharge run`'s full log. The tracked summary is its sibling # crates/aprender-contracts-staging/discharge-summary.json, outside the tree it hashes. crates/aprender-contracts-staging/lean/discharge.json +.pmat/context.db-shm +.pmat/context.db-wal diff --git a/changelog.d/README.md b/changelog.d/README.md new file mode 100644 index 0000000000..13f901ce4f --- /dev/null +++ b/changelog.d/README.md @@ -0,0 +1,25 @@ +# changelog.d/ + +One file per ticket: `changelog.d/.md` (e.g. `PMAT-1234.md`, `GH-5761.md`). A PR writes its +fragment and never edits `CHANGELOG.md`; the aggregate is generated at release, so N PRs never +contend on one file (FLOW-009 RH-1/RH-2, paiml/aprender#5761). + +Fragment shape (schema only, checked per PR): + +``` +### Fixed + +- fix(scope): what changed (Refs #N) +``` + +Exactly one `###
` heading (Added, Changed, Fixed, Performance, Security, Removed) followed +by one or more `- ` bullets. + +``` +bash scripts/changelog_fragments.sh check # schema of every fragment +bash scripts/changelog_fragments.sh aggregate # release: fold into CHANGELOG.md [Unreleased], delete fragments +bash scripts/changelog_fragments.sh --self-test # case table +``` + +Roadmap fragments are `docs/roadmaps/entries/.yaml` (apr-roadmap-fragments-v1); +`roadmap.yaml` is their generated aggregate. diff --git a/scripts/changelog_fragments.sh b/scripts/changelog_fragments.sh new file mode 100755 index 0000000000..bc5cb3c0b5 --- /dev/null +++ b/scripts/changelog_fragments.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# changelog_fragments.sh -- changelog.d/.md fragments (FLOW-009 RH-1/RH-2, #5761). +# +# check [DIR] schema of every fragment; exit 1 on a bad one +# aggregate [DIR] [CHANGELOG] fold fragments into CHANGELOG [Unreleased], then delete them +# --self-test plant each falsifier; must PASS/FAIL as named +set -euo pipefail + +SECTIONS="Added Changed Fixed Performance Security Removed" + +# One defect per line: "FILE: reason". Empty output = valid. +check_dir() { + local dir=$1 f name + for f in "$dir"/*.md; do + [ -e "$f" ] || continue + name=$(basename "$f") + [ "$name" = README.md ] && continue + if ! [[ "$name" =~ ^[A-Za-z]+-[0-9]+\.md$ ]]; then + echo "$name: file name must be .md (e.g. PMAT-1234.md)" + continue + fi + awk -v name="$name" -v secs="$SECTIONS" ' + BEGIN { n = split(secs, s, " "); for (i = 1; i <= n; i++) ok[s[i]] = 1 } + /^[[:space:]]*$/ { next } + /^### / { h++; sec = substr($0, 5); if (!(sec in ok)) printf "%s: unknown section \"%s\"\n", name, sec; if (b) printf "%s: heading after bullets\n", name; next } + /^- / { b++; if (!h) printf "%s: bullet before the ### heading\n", name; next } + { printf "%s: line is neither a heading nor a \"- \" bullet: %s\n", name, $0 } + END { if (h != 1) printf "%s: needs exactly one ### heading (found %d)\n", name, h; if (!b) printf "%s: needs at least one bullet\n", name } + ' "$f" + done +} + +aggregate() { + local dir=$1 log=$2 tmp sec f body + [ -f "$log" ] || { echo "no $log" >&2; return 1; } + local bad + bad=$(check_dir "$dir") + [ -z "$bad" ] || { printf '%s\n' "$bad" >&2; return 1; } + tmp=$(mktemp) + body="" + for sec in $SECTIONS; do + local lines="" + for f in "$dir"/*.md; do + [ -e "$f" ] || continue + [ "$(basename "$f")" = README.md ] && continue + if grep -qx "### $sec" "$f"; then + lines="$lines$(grep '^- ' "$f")"$'\n' + fi + done + [ -z "$lines" ] || body="$body### $sec"$'\n\n'"$lines"$'\n' + done + [ -n "$body" ] || { echo "no fragments" >&2; rm -f "${tmp:?}"; return 0; } + awk -v body="$body" ' + { print } + /^## \[Unreleased\]/ && !done { print ""; printf "%s", body; done = 1 } + ' "$log" >"$tmp" + mv "$tmp" "$log" + for f in "$dir"/*.md; do + [ "$(basename "$f")" = README.md ] || rm -f -- "${f:?}" + done +} + +self_test() { + local d rc=0 + d=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf -- '${d:?}'" RETURN + expect() { # name want(PASS|FAIL) content filename [needle] + mkdir -p "${d:?}/$1" + printf '%s\n' "$3" >"$d/$1/${4:-T-1.md}" + local out got=PASS + out=$(check_dir "$d/$1") + [ -z "$out" ] || got=FAIL + if [ "$got" != "$2" ] || { [ -n "${5:-}" ] && [[ "$out" != *"$5"* ]]; }; then + echo "SELF-TEST FAIL: $1 wanted $2 ${5:-}, got $got: $out" >&2 + rc=1 + fi + } + expect ok PASS $'### Fixed\n\n- fix(x): y (#1)' + expect bad-name FAIL $'### Fixed\n\n- a' notes.md 'file name must be' + expect no-heading FAIL '- a' T-1.md 'needs exactly one ### heading' + expect bad-section FAIL $'### Misc\n\n- a' T-1.md 'unknown section' + expect no-bullet FAIL '### Fixed' T-1.md 'at least one bullet' + expect prose FAIL $'### Fixed\n\nsome prose' T-1.md 'neither a heading' + expect two-headings FAIL $'### Fixed\n- a\n### Added\n- b' T-1.md 'needs exactly one' + # aggregate: both fragments land under [Unreleased], in section order, and the files go. + mkdir -p "$d/agg" + printf '# Changelog\n\n## [Unreleased]\n\n## [1.0.0]\n' >"$d/CHANGELOG.md" + printf '### Fixed\n\n- fix a (#1)\n' >"$d/agg/PMAT-1.md" + printf '### Added\n\n- add b (#2)\n' >"$d/agg/PMAT-2.md" + aggregate "$d/agg" "$d/CHANGELOG.md" + local want got + want=$(printf '# Changelog\n\n## [Unreleased]\n\n### Added\n\n- add b (#2)\n\n### Fixed\n\n- fix a (#1)\n\n\n## [1.0.0]') + got=$(cat "$d/CHANGELOG.md") + [ "$got" = "$want" ] || { echo "SELF-TEST FAIL: aggregate wrote: $got" >&2; rc=1; } + [ ! -e "$d/agg/PMAT-1.md" ] || { echo "SELF-TEST FAIL: fragment not deleted" >&2; rc=1; } + [ "$rc" -eq 0 ] && echo "changelog_fragments self-test: 8 cases ok" + return "$rc" +} + +cmd=${1:-} +case "$cmd" in + --self-test) self_test ;; + check) + out=$(check_dir "${2:-changelog.d}") + if [ -n "$out" ]; then printf '%s\n' "$out" >&2; exit 1; fi + ;; + aggregate) aggregate "${2:-changelog.d}" "${3:-CHANGELOG.md}" ;; + *) echo "usage: $0 check [DIR] | aggregate [DIR] [CHANGELOG] | --self-test" >&2; exit 2 ;; +esac From d7f4f9435c03f783e69fd1646492b31f22db7b6b Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 08:38:30 +0200 Subject: [PATCH 2/7] test(contracts-cli): cover pv coverage report rendering (Refs #5621) Co-Authored-By: Claude Sonnet 5.5 --- ...tracts-cli-coverage-report-render-5621.cmd | 1 + .../bin-aprender-contracts-cli--pv-v1.yaml | 6 ++ .../tests/coverage_report_render.rs | 86 +++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 ci/explicit-test-commands.d/813-aprender-contracts-cli-coverage-report-render-5621.cmd create mode 100644 crates/aprender-contracts-cli/tests/coverage_report_render.rs diff --git a/ci/explicit-test-commands.d/813-aprender-contracts-cli-coverage-report-render-5621.cmd b/ci/explicit-test-commands.d/813-aprender-contracts-cli-coverage-report-render-5621.cmd new file mode 100644 index 0000000000..7c56846aa0 --- /dev/null +++ b/ci/explicit-test-commands.d/813-aprender-contracts-cli-coverage-report-render-5621.cmd @@ -0,0 +1 @@ +cargo test -p aprender-contracts-cli --test coverage_report_render diff --git a/contracts/bin-aprender-contracts-cli--pv-v1.yaml b/contracts/bin-aprender-contracts-cli--pv-v1.yaml index cbd66a2dac..fd9c6b2ce5 100644 --- a/contracts/bin-aprender-contracts-cli--pv-v1.yaml +++ b/contracts/bin-aprender-contracts-cli--pv-v1.yaml @@ -168,3 +168,9 @@ falsification_tests: test: 'scripts/check_surface_audit_bins.sh (aprender#4476)' if_fails: 'a pv command ships unaudited (RED 2026-09-26: 11 of 49 paths had no row; GREEN 2026-09-27: 49 of 49)' mutation: 'delete every pv row; must turn RED naming pv' + - id: FALSIFY-BIN-PV-011 + rule: '`coverage` prints per-contract rows, totals and the overall percentage; binding totals only with --binding (#5621)' + prediction: 'without --binding no Binding lines print; with it, implemented/partial/missing totals and impl=N/M rows appear' + test: 'cargo test -p aprender-contracts-cli --test coverage_report_render' + if_fails: 'the coverage report renderer drifts without a test noticing' + mutation: 'print binding_partial in the Binding implemented line; coverage_with_binding_adds_binding_totals must turn RED (MEASURED 2026-10-19)' diff --git a/crates/aprender-contracts-cli/tests/coverage_report_render.rs b/crates/aprender-contracts-cli/tests/coverage_report_render.rs new file mode 100644 index 0000000000..ca84af7533 --- /dev/null +++ b/crates/aprender-contracts-cli/tests/coverage_report_render.rs @@ -0,0 +1,86 @@ +//! `pv coverage` report rendering (#5621), driven through +//! the binary on a two-contract corpus. + +#![allow(clippy::disallowed_methods)] + +use std::path::Path; +use std::process::{Command, Output}; + +fn corpus() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("tempdir"); + let fx = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); + std::fs::copy( + fx.join("relu-kernel-v1.yaml"), + dir.path().join("relu-kernel-v1.yaml"), + ) + .expect("copy relu"); + let dot = std::fs::read_to_string(fx.join("dot-kernel-v1.yaml")).expect("read dot"); + let dot = dot.replacen( + "metadata:\n", + "metadata:\n depends_on: [\"relu-kernel-v1\"]\n", + 1, + ); + std::fs::write(dir.path().join("dot-kernel-v1.yaml"), dot).expect("write dot"); + dir +} + +fn pv(args: &[&str], dir: &Path) -> Output { + Command::new(env!("CARGO_BIN_EXE_pv")) + .arg(args[0]) + .arg(dir) + .args(&args[1..]) + .output() + .expect("run pv") +} + +fn stdout(o: &Output) -> String { + assert!( + o.status.success(), + "pv failed: {}", + String::from_utf8_lossy(&o.stderr) + ); + String::from_utf8_lossy(&o.stdout).into_owned() +} + +#[test] +fn coverage_report_prints_per_contract_rows_totals_and_percentage() { + let d = corpus(); + let out = stdout(&pv(&["coverage"], d.path())); + assert!( + out.starts_with("Obligation Coverage Report\n=========================="), + "{out}" + ); + assert!( + out.contains("dot-kernel-v1") && out.contains("relu-kernel-v1"), + "{out}" + ); + assert!(out.contains("Contracts: 2"), "{out}"); + assert!(out.contains("Overall obligation coverage: "), "{out}"); + assert!( + out.contains("Falsification tests:") && out.contains("Kani harnesses:"), + "{out}" + ); + assert!( + !out.contains("Binding implemented"), + "binding lines only with --binding: {out}" + ); +} + +#[test] +fn coverage_with_binding_adds_binding_totals() { + let d = corpus(); + let b = d.path().join("binding.yaml"); + std::fs::write( + &b, + "version: 1.0.0\ntarget_crate: x\nbindings:\n- contract: dot-kernel-v1.yaml\n equation: dot\n \ + module_path: x::d\n function: d\n signature: 'fn d()'\n status: implemented\n", + ) + .expect("binding"); + let out = stdout(&pv( + &["coverage", "--binding", b.to_str().expect("utf8")], + d.path(), + )); + assert!(out.contains("Binding implemented: 1"), "{out}"); + assert!(out.contains("Binding partial: 0"), "{out}"); + assert!(out.contains("impl=1/1"), "{out}"); +} From ae9c69d22740689050ccfe158e315afe64237e57 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 19:21:38 +0200 Subject: [PATCH 3/7] test(test-lib): cover collect_partials_cdp row filtering and final marking via partials_from_raw (Refs #5625) Co-Authored-By: Claude Sonnet 5.5 --- crates/aprender-test-lib/src/lib.rs | 3 + crates/aprender-test-lib/src/validators.rs | 46 ++++++++-------- .../src/validators_partials_5625_tests.rs | 55 +++++++++++++++++++ 3 files changed, 81 insertions(+), 23 deletions(-) create mode 100644 crates/aprender-test-lib/src/validators_partials_5625_tests.rs diff --git a/crates/aprender-test-lib/src/lib.rs b/crates/aprender-test-lib/src/lib.rs index 9f85f5b8f0..a3335174fe 100644 --- a/crates/aprender-test-lib/src/lib.rs +++ b/crates/aprender-test-lib/src/lib.rs @@ -1560,3 +1560,6 @@ mod tests { } } } + +#[cfg(all(test, feature = "browser"))] +mod validators_partials_5625_tests; diff --git a/crates/aprender-test-lib/src/validators.rs b/crates/aprender-test-lib/src/validators.rs index 544ecbad46..10250b2731 100644 --- a/crates/aprender-test-lib/src/validators.rs +++ b/crates/aprender-test-lib/src/validators.rs @@ -1043,6 +1043,28 @@ pub struct StreamingValidationResult { pub total_frames: usize, } +/// Turn the raw `[timestamp, text]` rows of `window.__probar_partials` into +/// results; malformed rows are dropped and the last survivor is marked final. +#[cfg(feature = "browser")] +pub(crate) fn partials_from_raw(rows: Vec>) -> Vec { + let mut partials: Vec = rows + .into_iter() + .filter_map(|arr| { + let ts = arr.first()?.as_f64()?; + let text = arr.get(1)?.as_str()?.to_string(); + Some(PartialResult { + timestamp_ms: ts, + text, + is_final: false, + }) + }) + .collect(); + if let Some(last) = partials.last_mut() { + last.is_final = true; + } + partials +} + // CDP-based streaming UX tracking (PROBAR-SPEC-011) #[cfg(feature = "browser")] impl StreamingUxValidator { @@ -1313,29 +1335,7 @@ impl StreamingUxValidator { .into_value() .map_err(|_| StreamingValidationError::EndedInError)?; - let partials: Vec = result - .into_iter() - .filter_map(|arr| { - let ts = arr.first()?.as_f64()?; - let text = arr.get(1)?.as_str()?.to_string(); - Some(PartialResult { - timestamp_ms: ts, - text, - is_final: false, - }) - }) - .collect(); - - // Mark the last one as final if present - if partials.is_empty() { - return Ok(partials); - } - - let mut result = partials; - if let Some(last) = result.last_mut() { - last.is_final = true; - } - Ok(result) + Ok(partials_from_raw(result)) } /// Assert state sequence occurred in order diff --git a/crates/aprender-test-lib/src/validators_partials_5625_tests.rs b/crates/aprender-test-lib/src/validators_partials_5625_tests.rs new file mode 100644 index 0000000000..ee40748dc5 --- /dev/null +++ b/crates/aprender-test-lib/src/validators_partials_5625_tests.rs @@ -0,0 +1,55 @@ +//! Coverage for the post-processing inside `collect_partials_cdp` (#5625). +//! +//! The CDP call itself needs a live chromium; the row filtering and the +//! "last row is final" marking are the branches, and they live in +//! `partials_from_raw`. + +use crate::validators::partials_from_raw; +use serde_json::json; + +#[test] +fn empty_input_yields_no_partials() { + assert!(partials_from_raw(vec![]).is_empty()); +} + +#[test] +fn only_the_last_row_is_final() { + let out = partials_from_raw(vec![ + vec![json!(10.0), json!("he")], + vec![json!(20.0), json!("hel")], + vec![json!(30.0), json!("hello")], + ]); + let finals: Vec = out.iter().map(|p| p.is_final).collect(); + assert_eq!(finals, vec![false, false, true]); + assert_eq!(out[2].text, "hello"); + assert!((out[1].timestamp_ms - 20.0).abs() < f64::EPSILON); +} + +#[test] +fn single_row_is_final() { + let out = partials_from_raw(vec![vec![json!(1.5), json!("a")]]); + assert_eq!(out.len(), 1); + assert!(out[0].is_final); +} + +#[test] +fn malformed_rows_are_dropped_and_finality_goes_to_last_survivor() { + let out = partials_from_raw(vec![ + vec![json!(1.0), json!("ok")], + vec![], // no timestamp + vec![json!("x"), json!("bad")], // non-numeric timestamp + vec![json!(2.0)], // no text + vec![json!(3.0), json!(7)], // non-string text + vec![json!(4.0), json!("last")], + vec![json!(5.0), json!(null)], // trailing malformed row + ]); + let texts: Vec<&str> = out.iter().map(|p| p.text.as_str()).collect(); + assert_eq!(texts, vec!["ok", "last"]); + assert!(!out[0].is_final); + assert!(out[1].is_final); +} + +#[test] +fn all_malformed_yields_empty_not_final() { + assert!(partials_from_raw(vec![vec![json!(null), json!(null)], vec![]]).is_empty()); +} From 9f780273d43de3879e485779b90c24cf60fb4055 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 19:22:46 +0200 Subject: [PATCH 4/7] test(ptx-debug): cover generate_fkr_tests failed/bug/summary output (Refs #5624) Co-Authored-By: Claude Sonnet 5.5 --- ...nder-ptx-debug--aprender-ptx-debug-v1.yaml | 6 + crates/aprender-ptx-debug/src/bugs/mod.rs | 2 + .../src/output/fkr_generator.rs | 115 +++++++++++++++++- 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/contracts/bin-aprender-ptx-debug--aprender-ptx-debug-v1.yaml b/contracts/bin-aprender-ptx-debug--aprender-ptx-debug-v1.yaml index db490f1c31..8e0f9e4916 100644 --- a/contracts/bin-aprender-ptx-debug--aprender-ptx-debug-v1.yaml +++ b/contracts/bin-aprender-ptx-debug--aprender-ptx-debug-v1.yaml @@ -126,3 +126,9 @@ falsification_tests: test: 'cargo test -p aprender-ptx-debug --test cli_surface_gate unusable_input_is_rejected' if_fails: 'MEASURED 2026-09-27 (aprender-01): before the fix `analyze` on an empty file printed "Score: 97.1/100" with F001–F003 failed and exited 0; with the fix reverted this test is RED; fixed (a module with no .version is refused), GREEN' mutation: 'make a rejection path return Ok(()) or unwrap the read; must turn RED on that row' + - id: FALSIFY-BIN-APRENDER-PTX-DEBUG-007 + rule: 'gen-fkr emits one test per failed falsification result and per bug, named from the normalised module' + prediction: 'generate_fkr_tests emits fkr__ only for Fail results, fkr_bug__ per bug, and fkr_summary_ with score and confidence; "My-Kernel" yields idents my_kernel but keeps My-Kernel in the header' + test: 'cargo test -p aprender-ptx-debug --lib fkr_generator' + if_fails: 'a generated probar suite would miss a failed check, test a passing one, or carry an ident that does not compile' + mutation: 'flip result.is_fail(), drop the replace of - with _, or drop the *100 on confidence in fkr_generator.rs; fkr_generator tests must turn RED (all three shown RED 2026-10-19)' diff --git a/crates/aprender-ptx-debug/src/bugs/mod.rs b/crates/aprender-ptx-debug/src/bugs/mod.rs index 01025c1242..a952077f76 100644 --- a/crates/aprender-ptx-debug/src/bugs/mod.rs +++ b/crates/aprender-ptx-debug/src/bugs/mod.rs @@ -2,6 +2,8 @@ mod registry; +#[cfg(test)] +pub(crate) use registry::Bug; pub use registry::{BugClass, BugPattern, BugRegistry}; /// Severity levels for bugs diff --git a/crates/aprender-ptx-debug/src/output/fkr_generator.rs b/crates/aprender-ptx-debug/src/output/fkr_generator.rs index 9eeeb7c773..6bbbcaa117 100644 --- a/crates/aprender-ptx-debug/src/output/fkr_generator.rs +++ b/crates/aprender-ptx-debug/src/output/fkr_generator.rs @@ -98,9 +98,9 @@ fn fkr_summary_{module}() {{ #[cfg(test)] mod tests { use super::*; - use crate::bugs::BugRegistry; - use crate::falsification::FalsificationRegistry; - use crate::parser::Parser; + use crate::bugs::{Bug, BugClass, BugPattern, BugRegistry}; + use crate::falsification::{Category, FalsificationRegistry, FalsificationReport, TestResult}; + use crate::parser::{Parser, SourceLocation}; #[test] fn test_fkr_generation() { @@ -123,4 +123,113 @@ mod tests { assert!(fkr.contains("test_kernel")); assert!(fkr.contains("fkr_summary_")); } + + fn report_with( + results: Vec<(String, Category, String, TestResult)>, + score: f64, + confidence: f64, + ) -> FalsificationReport { + FalsificationReport { + results, + score, + earned_points: 0, + total_points: 0, + confidence, + } + } + + fn fail(id: &str, desc: &str) -> (String, Category, String, TestResult) { + let result = TestResult::Fail { + evidence: "e".to_string(), + location: None, + }; + ( + id.to_string(), + Category::BarrierSafety, + desc.to_string(), + result, + ) + } + + fn sample_bug(class: BugClass) -> Bug { + Bug { + class, + location: SourceLocation { + line: 7, + column: 3, + file: None, + }, + pattern: BugPattern::Other("p".to_string()), + message: "msg-xyz".to_string(), + } + } + + #[test] + fn only_failed_results_get_a_test() { + let results = vec![ + fail("F036", "barrier missing"), + ( + "F037".to_string(), + Category::BarrierSafety, + "ok".to_string(), + TestResult::Pass, + ), + ( + "F038".to_string(), + Category::BarrierSafety, + "na".to_string(), + TestResult::NotApplicable, + ), + ]; + let result = AnalysisResult::new("k", report_with(results, 80.0, 0.5), BugRegistry::new()); + let fkr = generate_fkr_tests(&result); + assert!(fkr.contains("fn fkr_f036_k()")); + assert!(fkr.contains("/// FKR test for F036: barrier missing")); + assert!(fkr.contains("/// Category: BarrierSafety")); + assert!(!fkr.contains("fkr_f037")); + assert!(!fkr.contains("fkr_f038")); + } + + #[test] + fn each_bug_gets_a_test_with_severity_and_mitigation() { + let mut bugs = BugRegistry::new(); + bugs.add(sample_bug(BugClass::MissingBarrierSync)); + let result = AnalysisResult::new("k", report_with(vec![], 80.0, 0.5), bugs); + let fkr = generate_fkr_tests(&result); + assert!(fkr.contains("fn fkr_bug_missingbarriersync_k()")); + assert!(fkr.contains("/// Severity: HIGH")); + assert!(fkr.contains("// Issue: msg-xyz")); + assert!(fkr.contains(BugClass::MissingBarrierSync.mitigation())); + } + + #[test] + fn module_name_is_normalised_for_idents_but_not_the_header() { + let results = vec![fail("F036", "d")]; + let result = AnalysisResult::new( + "My-Kernel", + report_with(results, 80.0, 0.5), + BugRegistry::new(), + ); + let fkr = generate_fkr_tests(&result); + assert!(fkr.contains("//! Auto-generated PTX FKR tests for My-Kernel")); + assert!(fkr.contains("fn fkr_f036_my_kernel()")); + assert!(fkr.contains("fn fkr_summary_my_kernel()")); + } + + #[test] + fn summary_reports_score_confidence_and_critical_flag() { + let ok = AnalysisResult::new("k", report_with(vec![], 85.0, 0.5), BugRegistry::new()); + let fkr = generate_fkr_tests(&ok); + assert!(fkr.contains("// Falsification Score: 85.0/100")); + assert!(fkr.contains("// Confidence: 50.0%")); + assert!(fkr.contains("// Critical bugs absent: true")); + assert!(fkr.contains("assert!(85.0 >= 70.0")); + + let bad = AnalysisResult::new( + "k", + report_with(vec![fail("F082", "critical")], 85.0, 0.5), + BugRegistry::new(), + ); + assert!(generate_fkr_tests(&bad).contains("// Critical bugs absent: false")); + } } From 6eeed7dcc589de56d46c6bb1409fa09dce49c27d Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 19:44:53 +0200 Subject: [PATCH 5/7] batch(manual-b2): eject #5761 (adds changelog_fragments.sh, an unwired guard: check_guards_are_wired 3 -> 4) Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 2 - changelog.d/README.md | 25 -------- scripts/changelog_fragments.sh | 110 --------------------------------- 3 files changed, 137 deletions(-) delete mode 100644 changelog.d/README.md delete mode 100755 scripts/changelog_fragments.sh diff --git a/.gitignore b/.gitignore index 2073487e82..b524a2aec6 100644 --- a/.gitignore +++ b/.gitignore @@ -140,5 +140,3 @@ docs/audits/quorum-*.json.lanes/ # PVL-001 EV-8a (#4202): `pv discharge run`'s full log. The tracked summary is its sibling # crates/aprender-contracts-staging/discharge-summary.json, outside the tree it hashes. crates/aprender-contracts-staging/lean/discharge.json -.pmat/context.db-shm -.pmat/context.db-wal diff --git a/changelog.d/README.md b/changelog.d/README.md deleted file mode 100644 index 13f901ce4f..0000000000 --- a/changelog.d/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# changelog.d/ - -One file per ticket: `changelog.d/.md` (e.g. `PMAT-1234.md`, `GH-5761.md`). A PR writes its -fragment and never edits `CHANGELOG.md`; the aggregate is generated at release, so N PRs never -contend on one file (FLOW-009 RH-1/RH-2, paiml/aprender#5761). - -Fragment shape (schema only, checked per PR): - -``` -### Fixed - -- fix(scope): what changed (Refs #N) -``` - -Exactly one `###
` heading (Added, Changed, Fixed, Performance, Security, Removed) followed -by one or more `- ` bullets. - -``` -bash scripts/changelog_fragments.sh check # schema of every fragment -bash scripts/changelog_fragments.sh aggregate # release: fold into CHANGELOG.md [Unreleased], delete fragments -bash scripts/changelog_fragments.sh --self-test # case table -``` - -Roadmap fragments are `docs/roadmaps/entries/.yaml` (apr-roadmap-fragments-v1); -`roadmap.yaml` is their generated aggregate. diff --git a/scripts/changelog_fragments.sh b/scripts/changelog_fragments.sh deleted file mode 100755 index bc5cb3c0b5..0000000000 --- a/scripts/changelog_fragments.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash -# changelog_fragments.sh -- changelog.d/.md fragments (FLOW-009 RH-1/RH-2, #5761). -# -# check [DIR] schema of every fragment; exit 1 on a bad one -# aggregate [DIR] [CHANGELOG] fold fragments into CHANGELOG [Unreleased], then delete them -# --self-test plant each falsifier; must PASS/FAIL as named -set -euo pipefail - -SECTIONS="Added Changed Fixed Performance Security Removed" - -# One defect per line: "FILE: reason". Empty output = valid. -check_dir() { - local dir=$1 f name - for f in "$dir"/*.md; do - [ -e "$f" ] || continue - name=$(basename "$f") - [ "$name" = README.md ] && continue - if ! [[ "$name" =~ ^[A-Za-z]+-[0-9]+\.md$ ]]; then - echo "$name: file name must be .md (e.g. PMAT-1234.md)" - continue - fi - awk -v name="$name" -v secs="$SECTIONS" ' - BEGIN { n = split(secs, s, " "); for (i = 1; i <= n; i++) ok[s[i]] = 1 } - /^[[:space:]]*$/ { next } - /^### / { h++; sec = substr($0, 5); if (!(sec in ok)) printf "%s: unknown section \"%s\"\n", name, sec; if (b) printf "%s: heading after bullets\n", name; next } - /^- / { b++; if (!h) printf "%s: bullet before the ### heading\n", name; next } - { printf "%s: line is neither a heading nor a \"- \" bullet: %s\n", name, $0 } - END { if (h != 1) printf "%s: needs exactly one ### heading (found %d)\n", name, h; if (!b) printf "%s: needs at least one bullet\n", name } - ' "$f" - done -} - -aggregate() { - local dir=$1 log=$2 tmp sec f body - [ -f "$log" ] || { echo "no $log" >&2; return 1; } - local bad - bad=$(check_dir "$dir") - [ -z "$bad" ] || { printf '%s\n' "$bad" >&2; return 1; } - tmp=$(mktemp) - body="" - for sec in $SECTIONS; do - local lines="" - for f in "$dir"/*.md; do - [ -e "$f" ] || continue - [ "$(basename "$f")" = README.md ] && continue - if grep -qx "### $sec" "$f"; then - lines="$lines$(grep '^- ' "$f")"$'\n' - fi - done - [ -z "$lines" ] || body="$body### $sec"$'\n\n'"$lines"$'\n' - done - [ -n "$body" ] || { echo "no fragments" >&2; rm -f "${tmp:?}"; return 0; } - awk -v body="$body" ' - { print } - /^## \[Unreleased\]/ && !done { print ""; printf "%s", body; done = 1 } - ' "$log" >"$tmp" - mv "$tmp" "$log" - for f in "$dir"/*.md; do - [ "$(basename "$f")" = README.md ] || rm -f -- "${f:?}" - done -} - -self_test() { - local d rc=0 - d=$(mktemp -d) - # shellcheck disable=SC2064 - trap "rm -rf -- '${d:?}'" RETURN - expect() { # name want(PASS|FAIL) content filename [needle] - mkdir -p "${d:?}/$1" - printf '%s\n' "$3" >"$d/$1/${4:-T-1.md}" - local out got=PASS - out=$(check_dir "$d/$1") - [ -z "$out" ] || got=FAIL - if [ "$got" != "$2" ] || { [ -n "${5:-}" ] && [[ "$out" != *"$5"* ]]; }; then - echo "SELF-TEST FAIL: $1 wanted $2 ${5:-}, got $got: $out" >&2 - rc=1 - fi - } - expect ok PASS $'### Fixed\n\n- fix(x): y (#1)' - expect bad-name FAIL $'### Fixed\n\n- a' notes.md 'file name must be' - expect no-heading FAIL '- a' T-1.md 'needs exactly one ### heading' - expect bad-section FAIL $'### Misc\n\n- a' T-1.md 'unknown section' - expect no-bullet FAIL '### Fixed' T-1.md 'at least one bullet' - expect prose FAIL $'### Fixed\n\nsome prose' T-1.md 'neither a heading' - expect two-headings FAIL $'### Fixed\n- a\n### Added\n- b' T-1.md 'needs exactly one' - # aggregate: both fragments land under [Unreleased], in section order, and the files go. - mkdir -p "$d/agg" - printf '# Changelog\n\n## [Unreleased]\n\n## [1.0.0]\n' >"$d/CHANGELOG.md" - printf '### Fixed\n\n- fix a (#1)\n' >"$d/agg/PMAT-1.md" - printf '### Added\n\n- add b (#2)\n' >"$d/agg/PMAT-2.md" - aggregate "$d/agg" "$d/CHANGELOG.md" - local want got - want=$(printf '# Changelog\n\n## [Unreleased]\n\n### Added\n\n- add b (#2)\n\n### Fixed\n\n- fix a (#1)\n\n\n## [1.0.0]') - got=$(cat "$d/CHANGELOG.md") - [ "$got" = "$want" ] || { echo "SELF-TEST FAIL: aggregate wrote: $got" >&2; rc=1; } - [ ! -e "$d/agg/PMAT-1.md" ] || { echo "SELF-TEST FAIL: fragment not deleted" >&2; rc=1; } - [ "$rc" -eq 0 ] && echo "changelog_fragments self-test: 8 cases ok" - return "$rc" -} - -cmd=${1:-} -case "$cmd" in - --self-test) self_test ;; - check) - out=$(check_dir "${2:-changelog.d}") - if [ -n "$out" ]; then printf '%s\n' "$out" >&2; exit 1; fi - ;; - aggregate) aggregate "${2:-changelog.d}" "${3:-CHANGELOG.md}" ;; - *) echo "usage: $0 check [DIR] | aggregate [DIR] [CHANGELOG] | --self-test" >&2; exit 2 ;; -esac From 9a5d783d2557b0104528ac2d609c3b6c271112ee Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 20:14:05 +0200 Subject: [PATCH 6/7] chore(ci): register coverage_report_render in tree_reader_tests (fold #5621 added the target) Co-Authored-By: Claude Sonnet 5.5 --- scripts/tree_reader_tests.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/tree_reader_tests.txt b/scripts/tree_reader_tests.txt index 5315c8d0da..e97a5e952b 100644 --- a/scripts/tree_reader_tests.txt +++ b/scripts/tree_reader_tests.txt @@ -98,6 +98,7 @@ aprender-contracts-cli --lib commands::obligations aprender-contracts-cli --lib commands::verify_pipeline aprender-contracts-cli --lib contract_walk aprender-contracts-cli --test cli_integration +aprender-contracts-cli --test coverage_report_render aprender-contracts-cli --test ev11_lint_ratchets aprender-contracts-cli --test ground_truth aprender-contracts-cli --test ont2b_sigma_gate From d179e13ba315c857ad37e3bbbe0f92113bbf0f3a Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 21:13:05 +0200 Subject: [PATCH 7/7] fix(contracts): number FALSIFY-BIN-PV-006 in sequence (fold #5621 skipped 006-010; data-integrity ceiling 445 > 444) Co-Authored-By: Claude Sonnet 5.5 --- contracts/bin-aprender-contracts-cli--pv-v1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/bin-aprender-contracts-cli--pv-v1.yaml b/contracts/bin-aprender-contracts-cli--pv-v1.yaml index fd9c6b2ce5..c4358221b9 100644 --- a/contracts/bin-aprender-contracts-cli--pv-v1.yaml +++ b/contracts/bin-aprender-contracts-cli--pv-v1.yaml @@ -168,7 +168,7 @@ falsification_tests: test: 'scripts/check_surface_audit_bins.sh (aprender#4476)' if_fails: 'a pv command ships unaudited (RED 2026-09-26: 11 of 49 paths had no row; GREEN 2026-09-27: 49 of 49)' mutation: 'delete every pv row; must turn RED naming pv' - - id: FALSIFY-BIN-PV-011 + - id: FALSIFY-BIN-PV-006 rule: '`coverage` prints per-contract rows, totals and the overall percentage; binding totals only with --binding (#5621)' prediction: 'without --binding no Binding lines print; with it, implemented/partial/missing totals and impl=N/M rows appear' test: 'cargo test -p aprender-contracts-cli --test coverage_report_render'