diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bad79aa5..56fe776ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 - name: Validate tracked files are not ignored shell: bash @@ -80,6 +82,12 @@ jobs: fi echo "No tracked files match .gitignore rules." + - name: Validate newly introduced commit subjects + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: python3 scripts/validate_release_commit_subjects.py "$BASE_SHA" "$HEAD_SHA" + python-contract: runs-on: ubuntu-latest steps: @@ -90,10 +98,14 @@ jobs: python-version: "3.11" - name: Install pytest - run: python -m pip install --upgrade pip pytest + run: python -m pip install --upgrade pip pytest PyYAML - - name: Validate Python provenance stubs and documentation - run: python -m pytest -q python/tests/test_relation_provenance_contract.py + - name: Validate Python provenance, documentation, and release contracts + run: >- + python -m pytest -q + python/tests/test_relation_provenance_contract.py + python/tests/test_docs_contract.py + python/tests/test_release_changelog_generation.py # The CAVIAR and maritime example suites on CPU (review follow-up: # previously no workflow ran any of them; the maritime suites were diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 52d96f612..21b39b86b 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -98,6 +98,7 @@ jobs: python3 \ python3-dev \ python3-pip + python3 -m pip install --no-cache-dir tomli==2.2.1 - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -158,6 +159,22 @@ jobs: if: github.event_name == 'workflow_dispatch' run: bash scripts/preflight_release_publish.sh + - name: Prepare unpublished changelog regeneration + if: github.event_name != 'workflow_dispatch' + run: | + set -euo pipefail + python3 scripts/prepare_release_changelog.py + + unexpected=() + while IFS= read -r entry; do + [[ "$entry" == " M CHANGELOG.md" ]] || unexpected+=("$entry") + done < <(git status --porcelain=v1 --untracked-files=all) + if ((${#unexpected[@]} != 0)); then + echo "Changelog preparation changed unexpected repository paths:" >&2 + printf ' %s\n' "${unexpected[@]}" >&2 + exit 1 + fi + - name: Run release-plz id: release run: | @@ -209,6 +226,7 @@ jobs: --git-token "$GITHUB_TOKEN" \ --repo-url "https://github.com/${GITHUB_REPOSITORY}" \ --config release-plz.toml \ + --allow-dirty \ -o json) echo "release_pr_output: $release_pr_output" diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bd6ad56c..de9dd2ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,322 +3,196 @@ All notable changes to this project are documented in this file. ## [Unreleased] +## xlog-cli [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-cli-v0.11.0...xlog-cli-v0.12.0) - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-cli-v0.11.0...xlog-cli-v0.12.0) - 2026-08-06 ### Added -- *(logic)* warn when imported-module pragmas are ignored ([#189](https://github.com/BrainyBlaze/xlog/pull/189)) +- *(logic)* warn when imported-module pragmas are ignored (#189) ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) -- *(cli)* evaluate imported rules in probabilistic programs ([#193](https://github.com/BrainyBlaze/xlog/pull/193)) +- *(cli)* align explain with normalized execution (#225) +- *(runtime)* preserve epistemic constraint diagnostics (#213) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) +- *(cli)* evaluate imported rules in probabilistic programs (#193) - *(cli)* drop the epistemic-evidence tests that need an untracked fixture ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- realign the public documentation with v0.12.0 (#203) +- *(deps)* bump arrow from 53.4.1 to 59.2.0 (#207) +- align runtime contracts and release status (#191) +- truth-align public claims and complete discovery metadata (#176) +## xlog-gpu [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-gpu-v0.11.0...xlog-gpu-v0.12.0) - 2026-08-06 ### Added -- *(logic)* warn when imported-module pragmas are ignored ([#189](https://github.com/BrainyBlaze/xlog/pull/189)) -- *(python)* [**breaking**] make n-ary provenance native and delta-safe ([#188](https://github.com/BrainyBlaze/xlog/pull/188)) +- *(pyxlog)* condition exact queries on accepted epistemic evidence (#201) +- *(logic)* warn when imported-module pragmas are ignored (#189) +- *(python)* [**breaking**] make n-ary provenance native and delta-safe (#188) ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) +- *(cli)* align explain with normalized execution (#225) +- *(runtime)* preserve epistemic constraint diagnostics (#213) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- *(logic)* share ground-term encoding (#231) +- realign the public documentation with v0.12.0 (#203) +- align runtime contracts and release status (#191) +- truth-align public claims and complete discovery metadata (#176) +## xlog-prob [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-prob-v0.11.0...xlog-prob-v0.12.0) - 2026-08-06 ### Added -- *(pyxlog)* expose exact log-evidence and the CNF variable to fact mapping ([#180](https://github.com/BrainyBlaze/xlog/pull/180)) +- *(pyxlog)* expose exact log-evidence and the CNF variable to fact mapping (#180) ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) -- *(cli)* evaluate imported rules in probabilistic programs ([#193](https://github.com/BrainyBlaze/xlog/pull/193)) +- *(prob)* [**breaking**] remove unreachable exact GPU compiler (#233) +- *(prob)* align circuit log-sum-exp semantics across CPU and GPU (#230) +- *(cuda)* [**breaking**] remove unselectable dense D4 frontier (#236) +- *(prob)* [**breaking**] remove unused CSR CNF module (#235) +- *(cli)* align explain with normalized execution (#225) +- *(prob)* restore G91 evidence GPU coverage (#200) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) +- *(cli)* evaluate imported rules in probabilistic programs (#193) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) +- realign the public documentation with v0.12.0 (#203) +- align runtime contracts and release status (#191) - *(prob)* make MC benches measure real inference and fail loudly on evaluate errors -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- truth-align public claims and complete discovery metadata (#176) +## xlog-solve [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-solve-v0.11.0...xlog-solve-v0.12.0) - 2026-08-06 ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) +- realign the public documentation with v0.12.0 (#203) +- align runtime contracts and release status (#191) - serialize GPU-state-sharing test binaries - *(solve)* align production-gate message asserts with the host-to-device wording -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- truth-align public claims and complete discovery metadata (#176) +## xlog-runtime [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-runtime-v0.11.0...xlog-runtime-v0.12.0) - 2026-08-06 ### Added -- *(python)* [**breaking**] make n-ary provenance native and delta-safe ([#188](https://github.com/BrainyBlaze/xlog/pull/188)) -- feat!(runtime): mark the output stats structs non_exhaustive +- *(python)* [**breaking**] make n-ary provenance native and delta-safe (#188) +- *(runtime)* [**breaking**] mark the output stats structs non_exhaustive +- *(runtime)* expose epistemic uncertainty over accepted world views ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) +- *(runtime)* [**breaking**] remove dormant epistemic export (#228) +- *(cli)* align explain with normalized execution (#225) +- *(runtime)* preserve epistemic constraint diagnostics (#213) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- *(runtime)* batch same-head rule unions into one multiway pass per head ([#183](https://github.com/BrainyBlaze/xlog/pull/183)) -- apply rustfmt to the TIER-2 epistemic sources -- Merge TIER-2 epistemic possible-source probes into main -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- realign the public documentation with v0.12.0 (#203) +- align runtime contracts and release status (#191) +- *(runtime)* batch same-head rule unions into one multiway pass per head (#183) +- format accepted-world-view epistemic exports +- truth-align public claims and complete discovery metadata (#176) +## xlog-logic [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-logic-v0.11.0...xlog-logic-v0.12.0) - 2026-08-06 ### Added -- *(logic)* warn when imported-module pragmas are ignored ([#189](https://github.com/BrainyBlaze/xlog/pull/189)) +- *(logic)* warn when imported-module pragmas are ignored (#189) +- *(runtime)* expose epistemic uncertainty over accepted world views ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) -- *(cli)* evaluate imported rules in probabilistic programs ([#193](https://github.com/BrainyBlaze/xlog/pull/193)) +- *(logic)* [**breaking**] remove dormant type and name scaffolding (#234) +- *(cli)* align explain with normalized execution (#225) +- *(runtime)* preserve epistemic constraint diagnostics (#213) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) +- *(cli)* evaluate imported rules in probabilistic programs (#193) - *(logic)* reject cross-predicate schema mismatches at compilation ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- apply rustfmt to the TIER-2 epistemic sources -- Merge TIER-2 epistemic possible-source probes into main -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- *(logic)* share ground-term encoding (#231) +- align strict Clippy with current stable (#237) +- realign the public documentation with v0.12.0 (#203) +- align runtime contracts and release status (#191) +- format accepted-world-view epistemic exports +- truth-align public claims and complete discovery metadata (#176) +## xlog-stats [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-stats-v0.11.0...xlog-stats-v0.12.0) - 2026-08-06 ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- realign the public documentation with v0.12.0 (#203) +- align runtime contracts and release status (#191) +- truth-align public claims and complete discovery metadata (#176) +## xlog-cuda [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-cuda-v0.11.0...xlog-cuda-v0.12.0) - 2026-08-06 ### Added -- *(python)* [**breaking**] make n-ary provenance native and delta-safe ([#188](https://github.com/BrainyBlaze/xlog/pull/188)) +- *(induce)* bounded exact n-ary rule induction — enumeration, reference scorer, CUDA kernel, device-resident engine (#240) +- *(python)* [**breaking**] make n-ary provenance native and delta-safe (#188) ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) +- *(prob)* align circuit log-sum-exp semantics across CPU and GPU (#230) +- *(cuda)* remove dormant RawCudaView recorder contract (#229) +- *(cuda)* [**breaking**] remove unselectable dense D4 frontier (#236) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- *(runtime)* batch same-head rule unions into one multiway pass per head ([#183](https://github.com/BrainyBlaze/xlog/pull/183)) +- align strict Clippy with current stable (#237) +- realign the public documentation with v0.12.0 (#203) +- *(deps)* bump arrow from 53.4.1 to 59.2.0 (#207) +- align runtime contracts and release status (#191) +- *(runtime)* batch same-head rule unions into one multiway pass per head (#183) - *(cuda)* retain freed mempool blocks so drop-and-reuse tests observe reuse on large-VRAM GPUs -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- truth-align public claims and complete discovery metadata (#176) +## xlog-ir [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-ir-v0.11.0...xlog-ir-v0.12.0) - 2026-08-06 ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) +- realign the public documentation with v0.12.0 (#203) +- align runtime contracts and release status (#191) +- truth-align public claims and complete discovery metadata (#176) +## xlog-core [0.12.0] - 2026-08-16 -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-core-v0.11.0...xlog-core-v0.12.0) - 2026-08-06 ### Fixed -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) +- *(runtime)* preserve epistemic constraint diagnostics (#213) +- *(logic)* classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195) ### Other -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-cli-v0.11.0...xlog-cli-v0.12.0) - 2026-08-06 - -### Added - -- *(logic)* warn when imported-module pragmas are ignored ([#189](https://github.com/BrainyBlaze/xlog/pull/189)) - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) -- *(cli)* evaluate imported rules in probabilistic programs ([#193](https://github.com/BrainyBlaze/xlog/pull/193)) -- *(cli)* drop the epistemic-evidence tests that need an untracked fixture - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-gpu-v0.11.0...xlog-gpu-v0.12.0) - 2026-08-06 - -### Added - -- *(logic)* warn when imported-module pragmas are ignored ([#189](https://github.com/BrainyBlaze/xlog/pull/189)) -- *(python)* [**breaking**] make n-ary provenance native and delta-safe ([#188](https://github.com/BrainyBlaze/xlog/pull/188)) - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-prob-v0.11.0...xlog-prob-v0.12.0) - 2026-08-06 - -### Added - -- *(pyxlog)* expose exact log-evidence and the CNF variable to fact mapping ([#180](https://github.com/BrainyBlaze/xlog/pull/180)) - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) -- *(cli)* evaluate imported rules in probabilistic programs ([#193](https://github.com/BrainyBlaze/xlog/pull/193)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- *(prob)* make MC benches measure real inference and fail loudly on evaluate errors -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-solve-v0.11.0...xlog-solve-v0.12.0) - 2026-08-06 - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- serialize GPU-state-sharing test binaries -- *(solve)* align production-gate message asserts with the host-to-device wording -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-runtime-v0.11.0...xlog-runtime-v0.12.0) - 2026-08-06 - -### Added - -- *(python)* [**breaking**] make n-ary provenance native and delta-safe ([#188](https://github.com/BrainyBlaze/xlog/pull/188)) -- feat!(runtime): mark the output stats structs non_exhaustive - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- *(runtime)* batch same-head rule unions into one multiway pass per head ([#183](https://github.com/BrainyBlaze/xlog/pull/183)) -- apply rustfmt to the TIER-2 epistemic sources -- Merge TIER-2 epistemic possible-source probes into main -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-logic-v0.11.0...xlog-logic-v0.12.0) - 2026-08-06 - -### Added - -- *(logic)* warn when imported-module pragmas are ignored ([#189](https://github.com/BrainyBlaze/xlog/pull/189)) - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) -- *(cli)* evaluate imported rules in probabilistic programs ([#193](https://github.com/BrainyBlaze/xlog/pull/193)) -- *(logic)* reject cross-predicate schema mismatches at compilation - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- apply rustfmt to the TIER-2 epistemic sources -- Merge TIER-2 epistemic possible-source probes into main -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-stats-v0.11.0...xlog-stats-v0.12.0) - 2026-08-06 - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-cuda-v0.11.0...xlog-cuda-v0.12.0) - 2026-08-06 - -### Added - -- *(python)* [**breaking**] make n-ary provenance native and delta-safe ([#188](https://github.com/BrainyBlaze/xlog/pull/188)) - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- *(runtime)* batch same-head rule unions into one multiway pass per head ([#183](https://github.com/BrainyBlaze/xlog/pull/183)) -- *(cuda)* retain freed mempool blocks so drop-and-reuse tests observe reuse on large-VRAM GPUs -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-ir-v0.11.0...xlog-ir-v0.12.0) - 2026-08-06 - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -## [0.12.0](https://github.com/BrainyBlaze/xlog/compare/xlog-core-v0.11.0...xlog-core-v0.12.0) - 2026-08-06 - -### Fixed - -- *(logic)* preserve predicate unions and recursive epistemic semantics ([#195](https://github.com/BrainyBlaze/xlog/pull/195)) - -### Other - -- align runtime contracts and release status ([#191](https://github.com/BrainyBlaze/xlog/pull/191)) -- truth-align public claims and complete discovery metadata ([#176](https://github.com/BrainyBlaze/xlog/pull/176)) - -### Changed - -- Epistemic execution classifies modal dependencies before single-pass planning. - Acyclic programs use Generate-Propagate-Test; positive FAEEL cycles run to an - ordinary founded least fixpoint; supported positive exact-tuple - Gelfond-1991 possibility cycles run to a greatest compatible tuple fixpoint; - and supported cycles through negation use the GPU-backed WFS plan. The - Gelfond-1991 route computes a GPU upper bound, then reevaluates against frozen - relation snapshots until the concrete tuples converge, so predicate-level - recursion cannot manufacture support across disjoint tuple domains. -- An unseeded FAEEL modal cycle, including `p() :- possible p().`, executes to an - empty founded extension instead of returning an unsupported-construct error. - +- realign the public documentation with v0.12.0 (#203) +- *(deps)* bump arrow from 53.4.1 to 59.2.0 (#207) +- align runtime contracts and release status (#191) +- truth-align public claims and complete discovery metadata (#176) ## [0.11.0](https://github.com/BrainyBlaze/xlog/compare/xlog-cli-v0.10.0...xlog-cli-v0.11.0) - 2026-07-29 ### Added diff --git a/docs/architecture/release-process.mdx b/docs/architecture/release-process.mdx index 932be0a45..2aef961cc 100644 --- a/docs/architecture/release-process.mdx +++ b/docs/architecture/release-process.mdx @@ -62,6 +62,36 @@ Workspace packages that are not published to crates.io: `pyxlog` ships as a Python package on PyPI, not as a crates.io Rust crate. +## Release Automation Ownership + +XLOG versions the publishable crates and the Python package together, so release +worthiness is a whole-workspace release-worthiness gate. The +`.github/workflows/release-plz.yml` owns this gate: on a push to `main`, it looks +for release-worthy Conventional Commits since the latest CLI tag before creating +or updating the rolling release pull request. +`release-plz.toml` does not decide release worthiness; it describes the shared +version group, publication targets, generated changelog, release pull request, +and tag formats after the workflow has admitted the workspace release. + +The workspace has a single authoritative Git tag, owned by `xlog-cli` and named +`xlog-cli-v{{ version }}`. The shared changelog therefore uses package-labelled +headings for all generated crate sections, while only the `xlog-cli` heading has +a compare link. The preparation step removes unpublished sections for the +current workspace version before regeneration, including sections written with +the earlier unlabelled heading format. This keeps repeated automation runs +convergent without changing sections once the authoritative tag exists. +Release-plz attributes commits from the packaged file set for each crate, so a +commit that changes a shared packaged file can appear in several package +sections. Repeated appearances reflect package attribution; regeneration does +not append another copy of an existing current-version section. + +A push to `main` can prepare a release pull request, but it cannot publish a +release. Publication is a manual `workflow_dispatch` from the reviewed release +commit. The operator must set `confirm_gpu_validation` and provide the GPU host, +commit, and evidence after the complete release validation command has passed. +Only that manual path invokes registry publication and creates the CLI tag; the +Python wheel and GitHub release assets then use the same tag. + ## CUDA Artifact Model XLOG runs GPU kernels, and those compiled kernels are not checked into the tree. diff --git a/python/tests/test_docs_contract.py b/python/tests/test_docs_contract.py index 571aa7510..3fc61dc4d 100644 --- a/python/tests/test_docs_contract.py +++ b/python/tests/test_docs_contract.py @@ -256,3 +256,17 @@ def compile(source: str, device: int = 0) -> CompiledLogicProgram: ... ) assert "## LogicProgram" in result.stdout assert "compile(source: str, device: int = 0)" in result.stdout + + +def test_release_process_documents_automation_ownership_and_single_tag_policy() -> None: + page = read("docs/architecture/release-process.mdx") + + assert "whole-workspace release-worthiness gate" in page + assert "`.github/workflows/release-plz.yml` owns this gate" in page + assert "`release-plz.toml` does not decide release worthiness" in page + assert "single authoritative Git tag" in page + assert "`xlog-cli-v{{ version }}`" in page + assert "package-labelled" in page + assert "manual `workflow_dispatch`" in page + assert "confirm_gpu_validation" in page + assert "docs-internal" not in page diff --git a/python/tests/test_release_changelog_generation.py b/python/tests/test_release_changelog_generation.py new file mode 100644 index 000000000..c2ac0db3f --- /dev/null +++ b/python/tests/test_release_changelog_generation.py @@ -0,0 +1,385 @@ +from __future__ import annotations + +import importlib.util +import re +import subprocess +import sys +from pathlib import Path +from types import ModuleType + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.10 local fallback + import tomli as tomllib + + +ROOT = Path(__file__).resolve().parents[2] + +EPISTEMIC_RELEASE_BEHAVIORS = ( + "Generate-Propagate-Test for acyclic programs", + "founded least fixpoints for positive FAEEL cycles", + "greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles", + "GPU-backed WFS for supported cycles through negation", + "GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge", + "disjoint tuple domains cannot manufacture support", + "empty founded extension for unseeded FAEEL cycles", +) + + +def _load_script(name: str) -> ModuleType: + path = ROOT / "scripts" / name + assert path.exists(), f"missing release helper: {path}" + spec = importlib.util.spec_from_file_location(path.stem, path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _legacy_release_section( + package: str, + version: str, + *, + previous: str = "0.11.0", + date: str = "2026-08-16", + entry: str = "- generated entry", +) -> str: + return ( + f"## [{version}](https://example.invalid/org/repo/compare/" + f"{package}-v{previous}...{package}-v{version}) - {date}\n\n" + f"### Fixed\n\n{entry}\n\n" + ) + + +def _package_release_section( + package: str, + version: str, + *, + linked: bool = False, + previous: str = "0.11.0", + date: str = "2026-08-16", + entry: str = "- generated entry", +) -> str: + link = "" + if linked: + link = ( + "(https://example.invalid/org/repo/compare/" + f"{package}-v{previous}...{package}-v{version})" + ) + return f"## {package} [{version}]{link} - {date}\n\n### Fixed\n\n{entry}\n\n" + + +def test_unpublished_cleanup_accepts_legacy_and_package_labelled_sections() -> None: + helper = _load_script("prepare_release_changelog.py") + current = "0.12.0" + changelog = ( + "# Changelog\n\n## [Unreleased]\n\n" + + _legacy_release_section("xlog-core", current, entry="- legacy core") + + _package_release_section("xlog-cli", current, linked=True, entry="- new cli") + + _package_release_section("xlog-core", current, entry="- new core") + + _legacy_release_section("xlog-cli", "0.11.0", previous="0.10.0") + ) + + updated, removed = helper.remove_unpublished_release_sections( + changelog, + version=current, + packages={"xlog-cli", "xlog-core"}, + release_tag_exists=False, + ) + + assert removed == 3 + assert "legacy core" not in updated + assert "new cli" not in updated + assert "new core" not in updated + assert "xlog-cli-v0.10.0...xlog-cli-v0.11.0" in updated + + second, removed_again = helper.remove_unpublished_release_sections( + updated, + version=current, + packages={"xlog-cli", "xlog-core"}, + release_tag_exists=False, + ) + assert second == updated + assert removed_again == 0 + + +def test_package_labelled_parser_rejects_cross_package_compare_links() -> None: + helper = _load_script("prepare_release_changelog.py") + mismatched = ( + "## xlog-core [0.12.0](https://example.invalid/org/repo/compare/" + "xlog-cli-v0.11.0...xlog-cli-v0.12.0) - 2026-08-16\n" + ) + + assert helper.release_sections(mismatched) == [] + + +def test_published_release_cleanup_is_a_noop() -> None: + helper = _load_script("prepare_release_changelog.py") + changelog = _package_release_section("xlog-cli", "0.12.0", linked=True) + + updated, removed = helper.remove_unpublished_release_sections( + changelog, + version="0.12.0", + packages={"xlog-cli"}, + release_tag_exists=True, + ) + + assert updated == changelog + assert removed == 0 + + +def test_release_context_uses_one_authoritative_tag() -> None: + helper = _load_script("prepare_release_changelog.py") + context = helper.load_release_context( + ROOT / "Cargo.toml", ROOT / "release-plz.toml" + ) + + assert context.version == "0.12.0" + assert context.authoritative_package == "xlog-cli" + assert context.release_tag == "xlog-cli-v0.12.0" + assert len(context.changelog_packages) == 10 + + +def test_changelog_template_labels_packages_and_links_only_authoritative_tag() -> None: + config = tomllib.loads((ROOT / "release-plz.toml").read_text(encoding="utf-8")) + body = config["changelog"]["body"] + + assert "## {{ package }} [{{ version }}]" in body + assert 'package == "xlog-cli"' in body + assert "({{ release_link }})" in body + assert body.count("{{ release_link }}") == 1 + + +def test_release_pr_template_uses_workspace_targets_for_git_only_packages() -> None: + config = tomllib.loads((ROOT / "release-plz.toml").read_text(encoding="utf-8")) + body = config["workspace"]["pr_body"] + + assert "## Workspace release" in body + assert "{% if version" not in body + assert "workspace target" in body + assert 'release.package == "pyxlog"' in body + assert 'release.package != "pyxlog"' in body + assert "crates.io baseline" in body + assert "registry baseline" not in body + assert "published separately to PyPI" in body + assert "initial release" not in body.lower() + + +def test_historical_subject_normalization_uses_behavioral_release_notes() -> None: + config = tomllib.loads((ROOT / "release-plz.toml").read_text(encoding="utf-8")) + preprocessors = config["changelog"]["commit_preprocessors"] + replacements = {entry["replace"] for entry in preprocessors} + + assert ( + "feat(runtime): expose epistemic uncertainty over accepted world views" + in replacements + ) + assert "format accepted-world-view epistemic exports" in replacements + + +def test_generator_restores_recursive_epistemic_behavior_after_cleanup() -> None: + helper = _load_script("prepare_release_changelog.py") + historical_notes = "\n".join( + f"- {behavior}" for behavior in EPISTEMIC_RELEASE_BEHAVIORS + ) + changelog = ( + "# Changelog\n\n## [Unreleased]\n\n" + + _legacy_release_section("xlog-logic", "0.12.0", entry=historical_notes) + + _legacy_release_section("xlog-cli", "0.11.0", previous="0.10.0") + ) + + cleaned, removed = helper.remove_unpublished_release_sections( + changelog, + version="0.12.0", + packages={"xlog-logic"}, + release_tag_exists=False, + ) + + assert removed == 1 + assert all(behavior not in cleaned for behavior in EPISTEMIC_RELEASE_BEHAVIORS) + + config = tomllib.loads((ROOT / "release-plz.toml").read_text(encoding="utf-8")) + exact_subject = ( + "fix(logic): preserve predicate unions and recursive epistemic semantics (#195)" + ) + preprocessors = config["changelog"]["commit_preprocessors"] + + def normalize(message: str) -> str: + for preprocessor in preprocessors: + message = re.sub(preprocessor["pattern"], preprocessor["replace"], message) + return message + + normalized_subject = normalize(exact_subject) + assert normalized_subject.startswith("fix(logic): ") + assert normalized_subject.endswith(" (#195)") + assert all( + behavior in normalized_subject for behavior in EPISTEMIC_RELEASE_BEHAVIORS + ) + + commit_with_body = exact_subject + "\n\n* preserve the squash commit body" + normalized_commit = normalize(commit_with_body) + assert normalized_commit.splitlines()[0] == normalized_subject + assert normalized_commit.endswith("* preserve the squash commit body") + + near_matches = ( + "fix(runtime): preserve predicate unions and recursive epistemic semantics (#195)", + "docs(logic): preserve predicate unions and recursive epistemic semantics (#195)", + "fix(logic): preserve predicate unions and recursive epistemic semantics (#194)", + exact_subject + " follow-up", + "revert: " + exact_subject, + "docs: quote a historical subject\n\n" + exact_subject, + ) + assert all(normalize(candidate) == candidate for candidate in near_matches) + + +def _git(repo: Path, *args: str) -> str: + proc = subprocess.run( + ["git", *args], + cwd=repo, + check=True, + capture_output=True, + text=True, + ) + return proc.stdout.strip() + + +def test_commit_subject_validator_rejects_only_misplaced_breaking_bang() -> None: + validator = _load_script("validate_release_commit_subjects.py") + commits = [ + ("a" * 40, "feat(runtime)!: valid breaking subject"), + ("b" * 40, "fix: ordinary subject"), + ("c" * 40, "feat!(runtime): malformed breaking subject"), + ("d" * 40, "custom-type!(scope): malformed custom type"), + ] + + issues = validator.find_malformed_subjects(commits) + + assert [(issue.sha, issue.subject) for issue in issues] == commits[2:] + + +def test_commit_subject_cli_checks_exactly_base_exclusive_range(tmp_path: Path) -> None: + script = ROOT / "scripts" / "validate_release_commit_subjects.py" + assert script.exists() + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Release Test") + _git(repo, "config", "user.email", "release-test@example.invalid") + _git(repo, "commit", "--allow-empty", "-m", "feat!(runtime): historical malformed") + base = _git(repo, "rev-parse", "HEAD") + _git(repo, "commit", "--allow-empty", "-m", "feat(runtime)!: valid breaking") + valid_head = _git(repo, "rev-parse", "HEAD") + + valid = subprocess.run( + [sys.executable, str(script), base, valid_head], + cwd=repo, + check=False, + capture_output=True, + text=True, + ) + assert valid.returncode == 0, valid.stderr or valid.stdout + assert "1 newly introduced commit subject" in valid.stdout + + _git(repo, "commit", "--allow-empty", "-m", "fix!(parser): malformed new commit") + invalid_head = _git(repo, "rev-parse", "HEAD") + invalid = subprocess.run( + [sys.executable, str(script), valid_head, invalid_head], + cwd=repo, + check=False, + capture_output=True, + text=True, + ) + assert invalid.returncode == 1 + assert invalid_head in invalid.stderr + assert "fix!(parser): malformed new commit" in invalid.stderr + assert "feat!(runtime): historical malformed" not in invalid.stderr + + +def test_workflows_prepare_generation_and_lint_exact_event_range() -> None: + release_workflow = (ROOT / ".github/workflows/release-plz.yml").read_text( + encoding="utf-8" + ) + prepare_command = "python3 scripts/prepare_release_changelog.py" + generator_command = "release-plz release-pr" + tomli_install = "python3 -m pip install --no-cache-dir tomli==2.2.1" + assert tomli_install in release_workflow + assert release_workflow.index(tomli_install) < release_workflow.index( + prepare_command + ) + assert prepare_command in release_workflow + assert release_workflow.index(prepare_command) < release_workflow.index( + generator_command + ) + assert ( + "--allow-dirty" in release_workflow[release_workflow.index(generator_command) :] + ) + assert "git status --porcelain=v1 --untracked-files=all" in release_workflow + assert '[[ "$entry" == " M CHANGELOG.md" ]]' in release_workflow + + ci_workflow = (ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8") + git_hygiene_job = ci_workflow.split(" git-hygiene:\n", 1)[1].split( + " python-contract:\n", 1 + )[0] + assert "fetch-depth: 0" in git_hygiene_job + assert ( + "BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}" + in git_hygiene_job + ) + assert ( + "HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}" + in git_hygiene_job + ) + assert ( + 'python3 scripts/validate_release_commit_subjects.py "$BASE_SHA" "$HEAD_SHA"' + in git_hygiene_job + ) + + python_contract_job = ci_workflow.split(" python-contract:\n", 1)[1].split( + " caviar-examples:\n", 1 + )[0] + dependency_install = "python -m pip install --upgrade pip pytest PyYAML" + pytest_command = "python -m pytest -q" + assert dependency_install in python_contract_job + assert pytest_command in python_contract_job + assert python_contract_job.index(dependency_install) < python_contract_job.index( + pytest_command + ) + pytest_invocation = python_contract_job[python_contract_job.index(pytest_command) :] + for test_path in ( + "python/tests/test_relation_provenance_contract.py", + "python/tests/test_docs_contract.py", + "python/tests/test_release_changelog_generation.py", + ): + assert test_path in pytest_invocation + + +def test_current_changelog_uses_package_sections_and_one_cli_link() -> None: + helper = _load_script("prepare_release_changelog.py") + context = helper.load_release_context( + ROOT / "Cargo.toml", ROOT / "release-plz.toml" + ) + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + current = [ + section + for section in helper.release_sections(changelog) + if section.version == context.version + ] + + assert {section.package for section in current} == context.changelog_packages + assert len(current) == len(context.changelog_packages) + current_text = "".join( + changelog[section.start : section.end] for section in current + ) + compare_targets = re.findall(r"/compare/([^)]*)", current_text) + assert compare_targets == [f"xlog-cli-v0.11.0...{context.release_tag}"] + assert "feat!(runtime):" not in current_text + assert ( + "- *(runtime)* [**breaking**] mark the output stats structs non_exhaustive" + in current_text + ) + assert re.search(r"\b[A-Z]+-[0-9]+\b", current_text) is None + assert "expose epistemic uncertainty over accepted world views" in current_text + assert "format accepted-world-view epistemic exports" in current_text + assert all(behavior in current_text for behavior in EPISTEMIC_RELEASE_BEHAVIORS) diff --git a/release-plz.toml b/release-plz.toml index 4f3bc0302..51db1a76a 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -6,6 +6,19 @@ changelog_path = "CHANGELOG.md" max_analyze_commits = 5000 pr_branch_prefix = "release-plz-" pr_labels = ["release"] +pr_body = """ +## Workspace release + +This pull request prepares one shared XLOG workspace release. + +### Package targets + +{% for release in releases %} +- `{{ release.package }}`: workspace target `{{ release.next_version }}`{% if release.previous_version != release.next_version and release.package != "pyxlog" and release.package != "xlog-neural" and release.package != "xlog-induce" %} (crates.io baseline `{{ release.previous_version }}`){% endif %}{% if release.package == "pyxlog" %}; Python artifacts are published separately to PyPI{% elif release.package == "xlog-neural" or release.package == "xlog-induce" %}; workspace-only package, not published to crates.io{% endif %} +{% endfor %} + +Review the generated `CHANGELOG.md` for package-specific changes. Publication remains a separate maintainer action after the complete GPU release gate passes. +""" # Release worthiness is enforced as ONE workspace-level gate in # .github/workflows/release-plz.yml, not per package: a per-package # `release_commits` filter over a shared [workspace.package].version @@ -15,6 +28,31 @@ pr_labels = ["release"] git_release_enable = false git_tag_enable = false +[changelog] +# Package labels distinguish the ten sections in the shared changelog. Only +# xlog-cli owns a Git tag, so only its heading receives a compare link. +body = """ +## {{ package }} [{{ version }}]{% if package == "xlog-cli" and release_link %}({{ release_link }}){% endif %} - {{ timestamp | date(format="%Y-%m-%d") }} + +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | upper_first }} + +{% for commit in commits %} +{%- if commit.scope -%} +- *({{commit.scope}})* {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }}{%- if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}) {% endfor -%}){% endif %} +{% else -%} +- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }} +{% endif -%} +{% endfor -%} +{% endfor %} +""" +commit_preprocessors = [ + { pattern = '^([A-Za-z][A-Za-z0-9-]*)!\(([^()]+)\):', replace = '${1}(${2})!:' }, + { pattern = '^Merge [A-Z][A-Z0-9-]* epistemic possible-source probes into main$', replace = 'feat(runtime): expose epistemic uncertainty over accepted world views' }, + { pattern = 'apply rustfmt to the [A-Z][A-Z0-9-]* epistemic sources', replace = 'format accepted-world-view epistemic exports' }, + { pattern = '(?m)\Afix\(logic\): preserve predicate unions and recursive epistemic semantics \(#195\)$', replace = 'fix(logic): classify modal dependencies before single-pass planning, routing through Generate-Propagate-Test for acyclic programs, founded least fixpoints for positive FAEEL cycles, greatest compatible exact-tuple fixpoints for supported positive Gelfond-1991 possibility cycles, and GPU-backed WFS for supported cycles through negation; compute a GPU upper bound and reevaluate against frozen relation snapshots until concrete tuples converge, so disjoint tuple domains cannot manufacture support; return the empty founded extension for unseeded FAEEL cycles, including `p() :- possible p().` (#195)' }, +] + [[package]] name = "xlog-core" release = true @@ -69,8 +107,8 @@ git_release_enable = true git_tag_enable = true git_tag_name = "xlog-cli-v{{ version }}" git_release_name = "xlog-cli v{{ version }}" -# The shared workspace CHANGELOG.md contains one "## [x.y.z]" heading per crate -# in the version group, which can leave "{{ changelog }}" empty at release time. +# The shared CHANGELOG.md uses one package-labelled "## package [x.y.z]" heading +# per crate, which can leave "{{ changelog }}" empty at release time. # Keep the install/docs footer so the GitHub release body is never blank. git_release_body = """ {{ changelog }} diff --git a/scripts/prepare_release_changelog.py b/scripts/prepare_release_changelog.py new file mode 100644 index 000000000..0fe237618 --- /dev/null +++ b/scripts/prepare_release_changelog.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""Remove generated sections for the current unpublished workspace version.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +from dataclasses import dataclass +from pathlib import Path + +try: + import tomllib +except ModuleNotFoundError: # pragma: no cover - Python 3.10 local fallback + import tomli as tomllib + + +RELEASE_HEADING = re.compile( + r"^## (?:(?P[A-Za-z0-9][A-Za-z0-9_-]*) )?" + r"\[(?P[^\]]+)\](?:\((?P[^)]+)\))?" + r" - (?P\d{4}-\d{2}-\d{2})\s*$" +) + + +@dataclass(frozen=True) +class ReleaseContext: + version: str + changelog_packages: frozenset[str] + authoritative_package: str + release_tag: str + + +@dataclass(frozen=True) +class ReleaseSection: + version: str + package: str + start: int + end: int + + +def _load_toml(path: Path) -> dict: + with path.open("rb") as handle: + return tomllib.load(handle) + + +def load_release_context(manifest_path: Path, config_path: Path) -> ReleaseContext: + manifest = _load_toml(manifest_path) + config = _load_toml(config_path) + try: + version = manifest["workspace"]["package"]["version"] + except KeyError as exc: + raise ValueError( + f"{manifest_path} must define workspace.package.version" + ) from exc + + workspace = config.get("workspace", {}) + changelog_packages: set[str] = set() + tag_packages: list[tuple[str, str]] = [] + for package in config.get("package", []): + if not package.get("release", workspace.get("release", True)): + continue + if package.get("changelog_update", workspace.get("changelog_update", True)): + changelog_packages.add(package["name"]) + if package.get("git_tag_enable", workspace.get("git_tag_enable", True)): + tag_template = package.get("git_tag_name", workspace.get("git_tag_name")) + if not tag_template: + raise ValueError( + f"release tag package {package['name']} needs git_tag_name" + ) + tag_packages.append((package["name"], tag_template)) + + if not changelog_packages: + raise ValueError(f"{config_path} enables no package changelogs") + if len(tag_packages) != 1: + raise ValueError( + f"{config_path} must enable exactly one authoritative release tag; " + f"found {len(tag_packages)}" + ) + authoritative_package, tag_template = tag_packages[0] + if "{{ version }}" not in tag_template: + raise ValueError("the authoritative git_tag_name must contain {{ version }}") + + return ReleaseContext( + version=version, + changelog_packages=frozenset(changelog_packages), + authoritative_package=authoritative_package, + release_tag=tag_template.replace("{{ version }}", version), + ) + + +def _parse_compare_url(url: str, version: str) -> str | None: + if "/compare/" not in url: + return None + comparison = url.rsplit("/compare/", 1)[1] + if comparison.count("...") != 1: + return None + previous, current = comparison.split("...", 1) + if "-v" not in previous or "-v" not in current: + return None + previous_package, _ = previous.rsplit("-v", 1) + current_package, current_version = current.rsplit("-v", 1) + if previous_package != current_package or current_version != version: + return None + return current_package + + +def _parse_release_heading(line: str) -> tuple[str, str] | None: + match = RELEASE_HEADING.fullmatch(line.rstrip("\n")) + if match is None: + return None + version = match.group("version") + labelled_package = match.group("package") + url = match.group("url") + + if url is None: + if labelled_package is None: + return None + return version, labelled_package + + linked_package = _parse_compare_url(url, version) + if linked_package is None: + return None + if labelled_package is not None and labelled_package != linked_package: + return None + return version, labelled_package or linked_package + + +def release_sections(changelog: str) -> list[ReleaseSection]: + lines = changelog.splitlines(keepends=True) + offsets: list[int] = [] + offset = 0 + for line in lines: + offsets.append(offset) + offset += len(line) + + heading_lines = [ + index for index, line in enumerate(lines) if line.startswith("## ") + ] + sections: list[ReleaseSection] = [] + for heading_index, line_index in enumerate(heading_lines): + parsed = _parse_release_heading(lines[line_index]) + if parsed is None: + continue + next_line = ( + heading_lines[heading_index + 1] + if heading_index + 1 < len(heading_lines) + else len(lines) + ) + end = offsets[next_line] if next_line < len(lines) else len(changelog) + version, package = parsed + sections.append( + ReleaseSection( + version=version, + package=package, + start=offsets[line_index], + end=end, + ) + ) + return sections + + +def remove_unpublished_release_sections( + changelog: str, + *, + version: str, + packages: set[str] | frozenset[str], + release_tag_exists: bool, +) -> tuple[str, int]: + if release_tag_exists: + return changelog, 0 + + stale_sections = [ + section + for section in release_sections(changelog) + if section.version == version and section.package in packages + ] + if not stale_sections: + return changelog, 0 + + pieces: list[str] = [] + cursor = 0 + for section in stale_sections: + pieces.append(changelog[cursor : section.start]) + cursor = section.end + pieces.append(changelog[cursor:]) + return "".join(pieces), len(stale_sections) + + +def _tag_exists(repository: Path, tag: str) -> bool: + proc = subprocess.run( + ["git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}^{{commit}}"], + cwd=repository, + check=False, + capture_output=True, + text=True, + ) + if proc.returncode not in (0, 1): + detail = proc.stderr.strip() or proc.stdout.strip() or "git failed" + raise RuntimeError(f"could not inspect release tag {tag}: {detail}") + return proc.returncode == 0 + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--repository", type=Path, default=Path(".")) + parser.add_argument("--manifest", type=Path, default=Path("Cargo.toml")) + parser.add_argument("--config", type=Path, default=Path("release-plz.toml")) + parser.add_argument("--changelog", type=Path, default=Path("CHANGELOG.md")) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = _parse_args(argv) + repository = args.repository.resolve() + manifest = repository / args.manifest + config = repository / args.config + changelog_path = repository / args.changelog + context = load_release_context(manifest, config) + original = changelog_path.read_text(encoding="utf-8") + tag_exists = _tag_exists(repository, context.release_tag) + updated, removed = remove_unpublished_release_sections( + original, + version=context.version, + packages=context.changelog_packages, + release_tag_exists=tag_exists, + ) + if updated != original: + changelog_path.write_text(updated, encoding="utf-8") + + if tag_exists: + print( + f"Release tag {context.release_tag} exists; preserved published " + f"{context.version} changelog sections." + ) + else: + print( + f"Removed {removed} generated changelog section(s) for unpublished " + f"workspace version {context.version}." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_release_commit_subjects.py b/scripts/validate_release_commit_subjects.py new file mode 100644 index 000000000..931ecf4e7 --- /dev/null +++ b/scripts/validate_release_commit_subjects.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Reject malformed Conventional Commit breaking markers in a Git range.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + + +MISPLACED_BREAKING_BANG = re.compile(r"^[A-Za-z][A-Za-z0-9-]*!\([^()]+\):") + + +@dataclass(frozen=True) +class SubjectIssue: + sha: str + subject: str + + +def find_malformed_subjects( + commits: list[tuple[str, str]], +) -> list[SubjectIssue]: + return [ + SubjectIssue(sha=sha, subject=subject) + for sha, subject in commits + if MISPLACED_BREAKING_BANG.match(subject) + ] + + +def _run_git(repository: Path, *args: str) -> str: + proc = subprocess.run( + ["git", *args], + cwd=repository, + check=False, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + detail = proc.stderr.strip() or proc.stdout.strip() or "git failed" + raise RuntimeError(f"git {' '.join(args)} failed: {detail}") + return proc.stdout.strip() + + +def newly_introduced_subjects( + repository: Path, base: str, head: str +) -> list[tuple[str, str]]: + _run_git(repository, "rev-parse", "--verify", f"{base}^{{commit}}") + _run_git(repository, "rev-parse", "--verify", f"{head}^{{commit}}") + revision_output = _run_git(repository, "rev-list", "--reverse", head, f"^{base}") + revisions = [line for line in revision_output.splitlines() if line] + return [ + (revision, _run_git(repository, "show", "-s", "--format=%s", revision)) + for revision in revisions + ] + + +def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("base", help="base commit excluded from validation") + parser.add_argument("head", help="head commit included in validation") + parser.add_argument("--repository", type=Path, default=Path(".")) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = _parse_args(argv) + commits = newly_introduced_subjects(args.repository.resolve(), args.base, args.head) + issues = find_malformed_subjects(commits) + if issues: + print( + "Malformed breaking Conventional Commit subjects place `!` before " + "the scope; use `type(scope)!:` instead:", + file=sys.stderr, + ) + for issue in issues: + print(f" {issue.sha} {issue.subject}", file=sys.stderr) + return 1 + + noun = "subject" if len(commits) == 1 else "subjects" + print(f"Validated {len(commits)} newly introduced commit {noun}.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())