causalrl 0.99.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. causalrl-0.99.0/.devcontainer/devcontainer.json +10 -0
  2. causalrl-0.99.0/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  3. causalrl-0.99.0/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
  4. causalrl-0.99.0/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  5. causalrl-0.99.0/.github/workflows/ci.yml +56 -0
  6. causalrl-0.99.0/.github/workflows/docs.yml +38 -0
  7. causalrl-0.99.0/.github/workflows/publish.yml +28 -0
  8. causalrl-0.99.0/.gitignore +19 -0
  9. causalrl-0.99.0/.pre-commit-config.yaml +7 -0
  10. causalrl-0.99.0/.python-version +1 -0
  11. causalrl-0.99.0/CHANGELOG.md +370 -0
  12. causalrl-0.99.0/CITATION.cff +12 -0
  13. causalrl-0.99.0/CONTRIBUTING.md +30 -0
  14. causalrl-0.99.0/Dockerfile +13 -0
  15. causalrl-0.99.0/LICENSE +21 -0
  16. causalrl-0.99.0/PKG-INFO +392 -0
  17. causalrl-0.99.0/README.md +346 -0
  18. causalrl-0.99.0/SECURITY.md +15 -0
  19. causalrl-0.99.0/benchmarks/scbandit_report.py +41 -0
  20. causalrl-0.99.0/docs/api.md +155 -0
  21. causalrl-0.99.0/docs/benchmarks.md +40 -0
  22. causalrl-0.99.0/docs/classics.md +32 -0
  23. causalrl-0.99.0/docs/discovery.md +55 -0
  24. causalrl-0.99.0/docs/guarantees.md +160 -0
  25. causalrl-0.99.0/docs/index.md +35 -0
  26. causalrl-0.99.0/docs/superpowers/plans/2026-05-23-causalrl-v0.1.md +1685 -0
  27. causalrl-0.99.0/docs/superpowers/plans/2026-05-23-causalrl-v0.2.md +2050 -0
  28. causalrl-0.99.0/docs/superpowers/plans/2026-05-23-causalrl-v0.3.md +988 -0
  29. causalrl-0.99.0/docs/superpowers/plans/2026-05-26-causalrl-correctness-hardening.md +134 -0
  30. causalrl-0.99.0/docs/superpowers/plans/2026-05-26-causalrl-task2-pomis.md +1133 -0
  31. causalrl-0.99.0/docs/superpowers/plans/2026-05-27-causalrl-top-class-foundation.md +229 -0
  32. causalrl-0.99.0/docs/superpowers/plans/2026-05-27-complete-transportability-sid-mz-meta.md +722 -0
  33. causalrl-0.99.0/docs/superpowers/specs/2026-05-23-causalrl-library-design.md +215 -0
  34. causalrl-0.99.0/docs/superpowers/specs/2026-05-23-causalrl-v0.2-design.md +223 -0
  35. causalrl-0.99.0/docs/superpowers/specs/2026-05-23-causalrl-v0.3-design.md +250 -0
  36. causalrl-0.99.0/docs/superpowers/specs/2026-05-26-causalrl-correctness-hardening-design.md +67 -0
  37. causalrl-0.99.0/docs/superpowers/specs/2026-05-26-causalrl-nonmanipulable-pomis-design.md +176 -0
  38. causalrl-0.99.0/docs/superpowers/specs/2026-05-26-causalrl-task2-pomis-design.md +248 -0
  39. causalrl-0.99.0/docs/superpowers/specs/2026-05-26-causalrl-v0.3.0-hardening-design.md +133 -0
  40. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-task3-counterfactual-ett-design.md +140 -0
  41. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-task4-transportability-design.md +127 -0
  42. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-task5-discovery-design.md +110 -0
  43. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-task6-imitation-design.md +105 -0
  44. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-task7-curriculum-design.md +84 -0
  45. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-task8-reward-shaping-design.md +86 -0
  46. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-task9-causal-games-design.md +85 -0
  47. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-causalrl-top-class-foundation-design.md +137 -0
  48. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-complete-transportability-sid-mz-meta-design.md +248 -0
  49. causalrl-0.99.0/docs/superpowers/specs/2026-05-27-fci-latent-discovery-design.md +161 -0
  50. causalrl-0.99.0/docs/transportability.md +68 -0
  51. causalrl-0.99.0/examples/mabuc_vertical_slice.ipynb +144 -0
  52. causalrl-0.99.0/examples/offline_to_online.ipynb +229 -0
  53. causalrl-0.99.0/examples/where_to_intervene.ipynb +108 -0
  54. causalrl-0.99.0/mkdocs.yml +34 -0
  55. causalrl-0.99.0/pyproject.toml +86 -0
  56. causalrl-0.99.0/src/causalrl/__init__.py +274 -0
  57. causalrl-0.99.0/src/causalrl/_backend/__init__.py +15 -0
  58. causalrl-0.99.0/src/causalrl/agents/__init__.py +1 -0
  59. causalrl-0.99.0/src/causalrl/agents/bandits.py +61 -0
  60. causalrl-0.99.0/src/causalrl/agents/base.py +22 -0
  61. causalrl-0.99.0/src/causalrl/agents/baselines.py +59 -0
  62. causalrl-0.99.0/src/causalrl/agents/counterfactual.py +64 -0
  63. causalrl-0.99.0/src/causalrl/agents/deep_deconfounded.py +80 -0
  64. causalrl-0.99.0/src/causalrl/agents/dovi.py +139 -0
  65. causalrl-0.99.0/src/causalrl/agents/offline_online.py +63 -0
  66. causalrl-0.99.0/src/causalrl/agents/primitives.py +25 -0
  67. causalrl-0.99.0/src/causalrl/agents/scbandit.py +100 -0
  68. causalrl-0.99.0/src/causalrl/curriculum.py +117 -0
  69. causalrl-0.99.0/src/causalrl/data/__init__.py +1 -0
  70. causalrl-0.99.0/src/causalrl/data/dataset.py +90 -0
  71. causalrl-0.99.0/src/causalrl/discovery.py +709 -0
  72. causalrl-0.99.0/src/causalrl/envs/__init__.py +1 -0
  73. causalrl-0.99.0/src/causalrl/envs/base.py +49 -0
  74. causalrl-0.99.0/src/causalrl/envs/suite/__init__.py +1 -0
  75. causalrl-0.99.0/src/causalrl/envs/suite/counterfactual_bandit.py +93 -0
  76. causalrl-0.99.0/src/causalrl/envs/suite/curriculum.py +24 -0
  77. causalrl-0.99.0/src/causalrl/envs/suite/discovery.py +59 -0
  78. causalrl-0.99.0/src/causalrl/envs/suite/dtr.py +96 -0
  79. causalrl-0.99.0/src/causalrl/envs/suite/games.py +38 -0
  80. causalrl-0.99.0/src/causalrl/envs/suite/gridworld.py +91 -0
  81. causalrl-0.99.0/src/causalrl/envs/suite/imitation.py +82 -0
  82. causalrl-0.99.0/src/causalrl/envs/suite/mabuc.py +90 -0
  83. causalrl-0.99.0/src/causalrl/envs/suite/scbandit.py +170 -0
  84. causalrl-0.99.0/src/causalrl/envs/suite/seq_dtr.py +103 -0
  85. causalrl-0.99.0/src/causalrl/envs/suite/seq_mabuc.py +96 -0
  86. causalrl-0.99.0/src/causalrl/envs/suite/shaping.py +34 -0
  87. causalrl-0.99.0/src/causalrl/envs/suite/transport.py +58 -0
  88. causalrl-0.99.0/src/causalrl/eval/__init__.py +1 -0
  89. causalrl-0.99.0/src/causalrl/eval/benchmark.py +168 -0
  90. causalrl-0.99.0/src/causalrl/eval/harness.py +29 -0
  91. causalrl-0.99.0/src/causalrl/eval/metrics.py +8 -0
  92. causalrl-0.99.0/src/causalrl/eval/ope.py +20 -0
  93. causalrl-0.99.0/src/causalrl/exceptions.py +25 -0
  94. causalrl-0.99.0/src/causalrl/experimental/__init__.py +1 -0
  95. causalrl-0.99.0/src/causalrl/experimental/ope.py +19 -0
  96. causalrl-0.99.0/src/causalrl/games.py +331 -0
  97. causalrl-0.99.0/src/causalrl/identification/__init__.py +1 -0
  98. causalrl-0.99.0/src/causalrl/identification/_separation.py +50 -0
  99. causalrl-0.99.0/src/causalrl/identification/bounds.py +114 -0
  100. causalrl-0.99.0/src/causalrl/identification/counterfactual.py +131 -0
  101. causalrl-0.99.0/src/causalrl/identification/criteria.py +33 -0
  102. causalrl-0.99.0/src/causalrl/identification/id_algorithm.py +816 -0
  103. causalrl-0.99.0/src/causalrl/identification/intervention_sets.py +152 -0
  104. causalrl-0.99.0/src/causalrl/identification/transport.py +204 -0
  105. causalrl-0.99.0/src/causalrl/imitation.py +124 -0
  106. causalrl-0.99.0/src/causalrl/py.typed +0 -0
  107. causalrl-0.99.0/src/causalrl/scm/__init__.py +1 -0
  108. causalrl-0.99.0/src/causalrl/scm/graph.py +169 -0
  109. causalrl-0.99.0/src/causalrl/scm/mechanisms.py +57 -0
  110. causalrl-0.99.0/src/causalrl/scm/scm.py +131 -0
  111. causalrl-0.99.0/src/causalrl/shaping.py +116 -0
  112. causalrl-0.99.0/tests/test_bandits.py +42 -0
  113. causalrl-0.99.0/tests/test_baselines.py +21 -0
  114. causalrl-0.99.0/tests/test_benchmark.py +33 -0
  115. causalrl-0.99.0/tests/test_bounds.py +44 -0
  116. causalrl-0.99.0/tests/test_bounds_properties.py +34 -0
  117. causalrl-0.99.0/tests/test_counterfactual.py +107 -0
  118. causalrl-0.99.0/tests/test_counterfactual_agents.py +53 -0
  119. causalrl-0.99.0/tests/test_counterfactual_bandit.py +63 -0
  120. causalrl-0.99.0/tests/test_counterfactual_decision.py +45 -0
  121. causalrl-0.99.0/tests/test_curriculum.py +57 -0
  122. causalrl-0.99.0/tests/test_curriculum_rl.py +67 -0
  123. causalrl-0.99.0/tests/test_dataset.py +45 -0
  124. causalrl-0.99.0/tests/test_deep_deconfounded.py +31 -0
  125. causalrl-0.99.0/tests/test_discovery.py +109 -0
  126. causalrl-0.99.0/tests/test_discovery_interventional.py +100 -0
  127. causalrl-0.99.0/tests/test_dovi.py +136 -0
  128. causalrl-0.99.0/tests/test_dtr_env.py +46 -0
  129. causalrl-0.99.0/tests/test_fci.py +357 -0
  130. causalrl-0.99.0/tests/test_games.py +55 -0
  131. causalrl-0.99.0/tests/test_games_mixed.py +104 -0
  132. causalrl-0.99.0/tests/test_gid.py +157 -0
  133. causalrl-0.99.0/tests/test_graph.py +50 -0
  134. causalrl-0.99.0/tests/test_graph_pomis_ops.py +48 -0
  135. causalrl-0.99.0/tests/test_graph_projection.py +45 -0
  136. causalrl-0.99.0/tests/test_gridworld_env.py +29 -0
  137. causalrl-0.99.0/tests/test_gymnasium_contract.py +77 -0
  138. causalrl-0.99.0/tests/test_harness.py +43 -0
  139. causalrl-0.99.0/tests/test_id_algorithm.py +144 -0
  140. causalrl-0.99.0/tests/test_identification.py +37 -0
  141. causalrl-0.99.0/tests/test_imitation.py +58 -0
  142. causalrl-0.99.0/tests/test_imitation_env.py +37 -0
  143. causalrl-0.99.0/tests/test_integration_curriculum.py +27 -0
  144. causalrl-0.99.0/tests/test_integration_deep.py +26 -0
  145. causalrl-0.99.0/tests/test_integration_discovery.py +16 -0
  146. causalrl-0.99.0/tests/test_integration_dovi.py +92 -0
  147. causalrl-0.99.0/tests/test_integration_games.py +12 -0
  148. causalrl-0.99.0/tests/test_integration_imitation.py +44 -0
  149. causalrl-0.99.0/tests/test_integration_mabuc.py +35 -0
  150. causalrl-0.99.0/tests/test_integration_nonmanip.py +46 -0
  151. causalrl-0.99.0/tests/test_integration_scbandit.py +52 -0
  152. causalrl-0.99.0/tests/test_integration_shaping.py +21 -0
  153. causalrl-0.99.0/tests/test_integration_transport.py +35 -0
  154. causalrl-0.99.0/tests/test_integration_ucdtr.py +32 -0
  155. causalrl-0.99.0/tests/test_intervention_sets.py +71 -0
  156. causalrl-0.99.0/tests/test_intervention_sets_nonmanip.py +93 -0
  157. causalrl-0.99.0/tests/test_intervention_sets_properties.py +44 -0
  158. causalrl-0.99.0/tests/test_literature_classics.py +238 -0
  159. causalrl-0.99.0/tests/test_mabuc_env.py +56 -0
  160. causalrl-0.99.0/tests/test_mechanisms.py +40 -0
  161. causalrl-0.99.0/tests/test_ope.py +27 -0
  162. causalrl-0.99.0/tests/test_ope_bounds.py +77 -0
  163. causalrl-0.99.0/tests/test_packaging.py +32 -0
  164. causalrl-0.99.0/tests/test_primitives.py +32 -0
  165. causalrl-0.99.0/tests/test_public_api.py +76 -0
  166. causalrl-0.99.0/tests/test_scbandit_agents.py +71 -0
  167. causalrl-0.99.0/tests/test_scbandit_agents_nonmanip.py +41 -0
  168. causalrl-0.99.0/tests/test_scbandit_env.py +47 -0
  169. causalrl-0.99.0/tests/test_scbandit_frontdoor.py +21 -0
  170. causalrl-0.99.0/tests/test_scm_counterfactual.py +49 -0
  171. causalrl-0.99.0/tests/test_scm_do.py +42 -0
  172. causalrl-0.99.0/tests/test_scm_properties.py +71 -0
  173. causalrl-0.99.0/tests/test_scm_see.py +94 -0
  174. causalrl-0.99.0/tests/test_separation.py +20 -0
  175. causalrl-0.99.0/tests/test_seq_dtr_env.py +51 -0
  176. causalrl-0.99.0/tests/test_seq_mabuc_env.py +66 -0
  177. causalrl-0.99.0/tests/test_shaping.py +42 -0
  178. causalrl-0.99.0/tests/test_sid.py +102 -0
  179. causalrl-0.99.0/tests/test_sid_complete.py +218 -0
  180. causalrl-0.99.0/tests/test_transport.py +82 -0
  181. causalrl-0.99.0/tests/test_transport_domains.py +26 -0
  182. causalrl-0.99.0/tests/test_ucdtr.py +46 -0
  183. causalrl-0.99.0/tests/test_when_to_intervene.py +24 -0
  184. causalrl-0.99.0/uv.lock +2728 -0
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "causalrl",
3
+ "build": { "dockerfile": "../Dockerfile" },
4
+ "customizations": {
5
+ "vscode": {
6
+ "extensions": ["charliermarsh.ruff", "ms-python.python", "ms-python.vscode-pylance"]
7
+ }
8
+ },
9
+ "postCreateCommand": "uv sync --extra dev"
10
+ }
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report incorrect behavior or a crash
4
+ labels: bug
5
+ ---
6
+
7
+ **Describe the bug**
8
+ A clear description of what is wrong.
9
+
10
+ **To reproduce**
11
+ A minimal snippet:
12
+
13
+ ```python
14
+ # ...
15
+ ```
16
+
17
+ **Expected behavior**
18
+ What you expected — and, for causal methods, the assumption or criterion you relied on.
19
+
20
+ **Environment**
21
+ - causalrl version:
22
+ - Python version:
23
+ - OS:
24
+ - torch installed? (yes/no)
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest a method, environment, or improvement
4
+ labels: enhancement
5
+ ---
6
+
7
+ **What and why**
8
+ The capability you want and the problem it solves.
9
+
10
+ **Causal grounding**
11
+ If proposing a causal method, cite the primary source and note its assumptions and scope.
12
+
13
+ **Alternatives considered**
14
+ Anything else you tried or considered.
@@ -0,0 +1,15 @@
1
+ ## Summary
2
+
3
+ What this changes and why.
4
+
5
+ ## Causal / software contract
6
+
7
+ For causal methods: the criterion or guarantee, its assumptions, and the primary source cited.
8
+
9
+ ## Validation
10
+
11
+ - [ ] Tests added/updated (oracle fixtures or reproducible benchmark evidence where applicable)
12
+ - [ ] `uv run pytest` passes
13
+ - [ ] `uv run ruff check .` and `uv run ruff format --check .` clean
14
+ - [ ] `uv run pyright src` clean
15
+ - [ ] Docs/CHANGELOG updated for public API changes
@@ -0,0 +1,56 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ${{ matrix.os }}
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest, macos-latest, windows-latest]
14
+ python-version: ["3.11", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v3
19
+ with:
20
+ python-version: "${{ matrix.python-version }}"
21
+ - name: Sync dependencies
22
+ run: uv sync --extra dev
23
+ - name: Lint
24
+ run: uv run ruff check .
25
+ - name: Format check
26
+ run: uv run ruff format --check .
27
+ - name: Type check
28
+ run: uv run pyright src
29
+ - name: Test
30
+ run: uv run pytest -v --cov-fail-under=90
31
+
32
+ docs:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v3
38
+ with:
39
+ python-version: "3.11"
40
+ - name: Sync documentation dependencies
41
+ run: uv sync --extra docs
42
+ - name: Build documentation
43
+ run: uv run --extra docs mkdocs build --strict
44
+
45
+ notebooks:
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+ - name: Install uv
50
+ uses: astral-sh/setup-uv@v3
51
+ with:
52
+ python-version: "3.11"
53
+ - name: Sync dependencies
54
+ run: uv sync --extra dev
55
+ - name: Execute example notebooks
56
+ run: uv run pytest --nbmake --no-cov examples/
@@ -0,0 +1,38 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: pages
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ deploy:
19
+ environment:
20
+ name: github-pages
21
+ url: ${{ steps.deployment.outputs.page_url }}
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v3
27
+ with:
28
+ python-version: "3.11"
29
+ - name: Build documentation
30
+ run: uv run --extra docs mkdocs build --strict
31
+ - uses: actions/configure-pages@v5
32
+ with:
33
+ enablement: true
34
+ - uses: actions/upload-pages-artifact@v3
35
+ with:
36
+ path: site
37
+ - id: deployment
38
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,28 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ pypi:
12
+ name: Publish to PyPI
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v3
21
+ with:
22
+ python-version: "3.11"
23
+ - name: Build distributions
24
+ run: uv build
25
+ - name: Check distributions
26
+ run: uvx twine check dist/*
27
+ - name: Publish through trusted publishing
28
+ run: uv publish
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .uv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ site/
13
+ .ipynb_checkpoints/
14
+
15
+ # Claude Code local worktrees / settings (not part of the project)
16
+ .claude/
17
+
18
+ # Reference papers — kept locally for citation/reproducibility, not redistributed (copyright)
19
+ papers/*.pdf
@@ -0,0 +1,7 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.5.0
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,370 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.99.0] - 2026-05-27
11
+
12
+ ### Stable
13
+ - **API stabilized, humbly labelled v0.99** — a deliberate step short of a 1.0 tag while it settles
14
+ in real use. The public API (the names exported from `causalrl`) follows semantic
15
+ versioning. The full Bareinboim 9-task causal-RL taxonomy is implemented, with the depth
16
+ extensions of v0.13–v0.20: the complete Shpitser–Pearl ID algorithm, gID, sID / mz / meta
17
+ transportability, FCI (latent-confounder discovery), mixed Nash for any number of players,
18
+ validated Manski and marginal-sensitivity-model OPE bounds, and a "reproducing the literature"
19
+ gallery.
20
+
21
+ ### Removed
22
+ - Deprecated shims: `POMISThompsonSampling` now requires `manipulable=` explicitly (arm-inference
23
+ removed); the deprecated `causalrl.eval.ope.confounding_sensitivity_bounds` bridge is gone (use
24
+ `causalrl.identification.bounds.ipw_sensitivity_bounds`).
25
+ - The partial hand-maintained `causalrl/__init__.pyi` stub — `py.typed` plus complete inline
26
+ annotations (checked by pyright in strict mode) are now the authoritative types.
27
+
28
+ ## [0.20.0] - 2026-05-27
29
+
30
+ ### Added
31
+ - **"Reproducing the literature" gallery** (`tests/test_literature_classics.py` + a
32
+ [docs gallery](docs/classics.md)): classic causal cases reproduced end-to-end with the library —
33
+ Simpson's paradox (kidney stones), the front-door criterion (smoking → tar → cancer), Pearl's
34
+ napkin, the instrumental variable (non-identified but Manski-bounded), the bow arc, and
35
+ cross-domain transport (LA → NYC).
36
+ - **Difficult RL problems where causal beats associational RL**: MABUC (a confounding-aware bandit
37
+ beats the naive one), the counterfactual "Greedy Casino" (acting on the counterfactual ≈ 0.80
38
+ doubles the best fixed interventional arm ≈ 0.37), and curriculum-driven hard exploration (a causal
39
+ prerequisite curriculum reaches a sparse goal flat Q-learning misses on the same budget).
40
+
41
+ ## [0.19.0] - 2026-05-27
42
+
43
+ ### Added
44
+ - **Validated partial-identification / OPE bounds** (closing the sensitivity-bounds gap):
45
+ - `manski_bounds` — sharp no-assumptions (Manski 1990) bounds on `E[outcome | do(treatment)]` from
46
+ observational data (the observational counterpart of `causal_q_bounds`).
47
+ - `ipw_sensitivity_bounds` — the marginal sensitivity model (Tan 2006; Zhao–Small–Bhattacharya
48
+ 2019): an odds-ratio-Γ interval on the treated counterfactual mean that collapses to the IPW
49
+ point at Γ=1, widens monotonically with Γ, and contains the truth once Γ exceeds the true
50
+ confounding odds ratio. Validated against a confounded SCM with a known effect.
51
+
52
+ ## [0.18.0] - 2026-05-27
53
+
54
+ ### Added
55
+ - **Mixed-strategy Nash equilibria for three or more players** (taxonomy Task 9): `mixed_nash_equilibria`
56
+ now handles any number of agents. Two-player games stay exact (rational support enumeration); games
57
+ with ≥3 agents use support enumeration with a numerical Newton solve of the multilinear indifference
58
+ system, and **every returned profile is verified to be an ε-Nash equilibrium**. Validated on a
59
+ three-player cyclic matching game (recovers the uniform `(1/2, 1/2)` equilibrium).
60
+
61
+ ### Changed
62
+ - `mixed_nash_equilibria` no longer raises `NotImplementedError` for more than two agents; it raises
63
+ `CausalGraphError` only for fewer than two agents.
64
+
65
+ ## [0.17.0] - 2026-05-27
66
+
67
+ ### Added
68
+ - **FCI: causal discovery with latent confounders** (taxonomy Task 5): `discover_latent` learns a
69
+ `PAG` (partial ancestral graph) without assuming causal sufficiency — PC skeleton + Possible-D-SEP
70
+ refinement + the complete orientation rules R1-R10 (Zhang 2008, sound and complete for latent
71
+ confounders and selection bias). `a <-> b` marks a latent confounder; a circle endpoint is
72
+ undetermined by the equivalence class. Validated against the true MAG of the data-generating
73
+ DAG-with-latents (latent-confounder detection and the M-bias collider), with per-rule unit
74
+ fixtures for R1-R10.
75
+ - `PAG` (endpoint marks `o` / `>` / `-`, with `is_directed` / `is_bidirected` / `render`) and a
76
+ Causal Discovery guide (`docs/discovery.md`).
77
+
78
+ ### Changed
79
+ - Internal: the PC skeleton phase is factored into `_pc_skeleton`, shared by `discover` and
80
+ `discover_latent` (no behaviour change to `discover`).
81
+
82
+ ## [0.16.0] - 2026-05-27
83
+
84
+ ### Added
85
+ - **Multi-domain and experimental transportability (mz / meta)** (taxonomy Task 4): a general
86
+ engine resolves each c-factor of the target effect by searching the domains that can supply it.
87
+ - `Domain` describes a source domain — its selection-marked variables and the surrogate
88
+ experiments it offers.
89
+ - `identify_transport_general` / `is_transportable_general` / `estimate_transport_general` decide
90
+ and compute `P*(y | do(x))` across one or more `Domain`s plus the target. With a single
91
+ observational source they coincide with `identify_transport`; with no selection and no
92
+ experiments they reduce to the ID algorithm.
93
+ - **mz**: a surrogate experiment in a source domain supplies a c-factor that no observational
94
+ distribution can (validated: a source `do(X)` breaks a bow-arc hedge, matching simulation).
95
+ - **meta**: invariant c-factors are contributed by different source domains (validated: an effect
96
+ assembled from two sources marked on different covariates matches the target's true `do()`).
97
+ - A [Transportability guide](docs/transportability.md) with runnable covariate-shift, mz, and meta
98
+ examples.
99
+
100
+ ### Changed
101
+ - The single-source `identify_transport` now delegates to the general engine — behaviour-preserving;
102
+ its signature and results are unchanged.
103
+ - Internal: S-node separation helpers moved to `causalrl.identification._separation` (shared by the
104
+ transport code and the ID engine; no public API change).
105
+
106
+ ### Notes
107
+ - At c-factor granularity, invariance is exactly "touches no selection-marked variable", so
108
+ single-source observational transport was already complete; this release adds surrogate
109
+ experiments and multiple source domains. The one remaining edge — a single c-factor identifiable
110
+ only by *combining several experiments* — is reported non-transportable rather than guessed.
111
+
112
+ ## [0.15.0] - 2026-05-27
113
+
114
+ ### Added
115
+ - **Cross-domain transportability (sID)** (taxonomy Task 4): `identify_transport` /
116
+ `is_transportable_effect` / `estimate_transported_effect` (and the `transport_estimand`
117
+ `SelectionDiagram` adapter) decide and compute the target effect `P*(y | do(x))` across a
118
+ selection diagram by routing each c-factor — invariant factors transfer from the source,
119
+ selection-marked factors are identified from the target. With no selection it reduces to ID.
120
+ Validated by simulation: under a covariate shift the transported estimate matches the target's
121
+ true `do()` distribution (and differs from naively reusing the source). It subsumes the
122
+ direct / S-admissible-adjustment cases; it is sound but not the *complete* sID.
123
+
124
+ ### Fixed
125
+ - gID: Tian's `Identify` assumes its domain is a single c-component. Both gID and sID now route
126
+ through `_c_factor_from`, which decomposes the domain into c-components before extracting — fixing
127
+ a latent error on multi-component experiment/source domains (previous gID tests only hit the
128
+ single-component case).
129
+
130
+ ### Notes
131
+ - The complete sID (transport c-factors identifiable only by combining source and target) is still
132
+ out of scope and reported as non-transportable rather than guessed.
133
+
134
+ ## [0.14.0] - 2026-05-27
135
+
136
+ ### Added
137
+ - **General identification from surrogate experiments (gID)** (taxonomy Task 4):
138
+ `identify_effect_with_experiments` extends the ID recursion so that a c-factor observation cannot
139
+ identify (a hedge) is instead obtained from an available experiment (Tian's `Identify`
140
+ subroutine). `is_gid_identifiable` gives the decision and `estimate_effect_with_experiments`
141
+ evaluates the estimand on observational plus randomized-experimental data. With no experiments it
142
+ coincides exactly with the ID algorithm. Validated by simulation: the bow-arc and a
143
+ confounded-mediator graph (neither observationally identifiable) are recovered from a surrogate
144
+ experiment, matching the true `do()` distribution.
145
+
146
+ ### Notes
147
+ - Full cross-domain transportability (the complete sID algorithm) remains out of scope: it reduces
148
+ to a conditional-gID over an augmented selection diagram and is documented as the next frontier.
149
+ Transportability stays at the direct / S-admissible-adjustment slice for now.
150
+
151
+ ## [0.13.0] - 2026-05-27
152
+
153
+ Depth pass closing the taxonomy gaps surfaced by re-checking the library against the Bareinboim
154
+ causal-RL program page.
155
+
156
+ ### Added
157
+ - **General causal-effect identification** (taxonomy Task 4): `identify_effect` runs the sound and
158
+ complete Shpitser-Pearl ID algorithm, returning a do-free `Estimand` for `P(y | do(x))` in any
159
+ ADMG or raising `NotIdentifiableError` with the witnessing hedge. `estimate_effect` evaluates the
160
+ estimand on data; `is_identifiable_effect` gives the decision. Validated by simulation:
161
+ back-door/front-door estimands match the true `do()` distribution, and the bow-arc and
162
+ instrumental-variable graphs are correctly non-identifiable.
163
+ - **Interventional causal discovery** (Task 5): `discover_interventional` combines observational
164
+ (L1) and experimental (L2) data, orienting edges incident to each intervention target by the
165
+ invariance principle to recover the interventional essential graph.
166
+ - **Mixed-strategy Nash equilibria** (Task 9): `mixed_nash_equilibria` finds all equilibria of a
167
+ two-player game exactly by support enumeration over rational arithmetic.
168
+ - **Curriculum-driven RL** (Task 7): `curriculum_q_learning` trains Q-learning through a sequence of
169
+ subtasks with warm-start transfer, reaching a sparse target that flat learning misses.
170
+ - **"When to intervene"** (Task 2): `requires_experiment` reports when an experiment is necessary —
171
+ exactly when the effect is not observationally identifiable.
172
+
173
+ ### Changed
174
+ - `is_identifiable` now delegates to the complete ID algorithm, returning a definite boolean for any
175
+ ADMG (no longer `None` for front-door-style cases).
176
+ - Documentation: the API reference and `guarantees.md` now cover the full taxonomy (tasks 3-9).
177
+ - Packaging: added `authors`, `keywords`, per-version classifiers, `Typing :: Typed`, and a
178
+ Changelog URL to the project metadata.
179
+ - CI: the test matrix now spans Linux/macOS/Windows; added a 90% coverage gate, example-notebook
180
+ execution (`nbmake`), and a `twine check` before publishing.
181
+ - Repository hygiene: added `SECURITY.md` and issue/PR templates; stopped tracking a reference PDF
182
+ (`papers/*.pdf` is gitignored).
183
+
184
+ ### Fixed
185
+ - `examples/offline_to_online.ipynb`: multi-stage `DOVI` now passes
186
+ `transition_assumption="unconfounded"` (made mandatory by the correctness-hardening pass); the
187
+ notebook previously raised `UnverifiedAssumptionError`.
188
+
189
+ ## [0.12.0] - 2026-05-27
190
+
191
+ ### Added
192
+ - **Causal game theory** (taxonomy Task 9, completing the 9-task taxonomy). `CausalGame` represents a
193
+ finite game as a multi-agent causal influence diagram (a decision and a utility node per agent);
194
+ `best_response`, `is_nash_equilibrium`, and `pure_nash_equilibria` reason about equilibria by
195
+ enumeration. The canonical games recover their textbook pure equilibria: Prisoner's Dilemma (mutual
196
+ defection), a coordination game (two equilibria), matching pennies (none). Faithful to Koller &
197
+ Milch (MAIDs, 2003) and Hammond et al., *Reasoning about Causality in Games* (2023).
198
+ - `prisoners_dilemma`, `coordination_game`, `matching_pennies` demo games.
199
+
200
+ ## [0.11.0] - 2026-05-27
201
+
202
+ ### Added
203
+ - **Causal reward shaping** (taxonomy Task 8). `apply_potential_shaping` adds `gamma*Phi(s') - Phi(s)`
204
+ to an MDP's rewards — policy-invariant for any potential (Ng, Harada & Russell, ICML 1999) — and
205
+ `causal_potential` supplies the ideal potential `V*` from the causal model. With `TabularMDP`,
206
+ `value_iteration`, and tabular `q_learning`, causal-potential shaping makes a sparse reward dense:
207
+ the shaped learner reaches the optimal policy within a few episodes while unshaped Q-learning lags,
208
+ and the optimal policy is provably unchanged.
209
+ - `make_sparse_chain_mdp` — the sparse-reward chain demo.
210
+
211
+ ## [0.10.0] - 2026-05-27
212
+
213
+ ### Added
214
+ - **Causal curriculum learning** (taxonomy Task 7). `causal_curriculum` orders skills by a
215
+ topological sort of the causal/prerequisite graph (learn causes before effects),
216
+ `is_valid_curriculum` checks an order respects prerequisites, and `PrerequisiteLearner` models
217
+ causally-gated mastery (a skill is learned only once its parents are). On a skill chain/diamond the
218
+ causal curriculum masters the goal while a prerequisite-violating order does not. Faithful to
219
+ Bengio, Louradour, Collobert & Weston, *Curriculum Learning* (ICML 2009).
220
+ - `make_skill_chain` / `make_skill_diamond` prerequisite-graph demos.
221
+
222
+ ## [0.9.0] - 2026-05-27
223
+
224
+ ### Added
225
+ - **Causal imitation learning** (taxonomy Task 6). `is_imitable` / `imitation_backdoor_set` decide
226
+ whether an expert can be imitated from observed demonstrations and return the observed back-door
227
+ set to clone on; `CausalImitator` clones `P(A | Z)` and reproduces the expert's reward, while the
228
+ `BehavioralCloning` baseline (cloning the marginal `P(A)`) is biased by the confounding.
229
+ Conservative: returns `None` / `False` when no observed admissible set exists rather than a biased
230
+ policy. Faithful to Zhang, Kumor & Bareinboim, *Causal Imitation Learning with Unobserved
231
+ Confounders* (NeurIPS 2020).
232
+ - `ImitationEnv` (`make_imitation_diagram`, `generate_demonstrations`, `expert_policy`): a confounded
233
+ one-step demo where the causal imitator matches the expert (~0.9) and naive BC is stuck near ~0.5.
234
+ - `is_backdoor_admissible` (the back-door criterion check) is now public, shared by transportability
235
+ and imitation.
236
+
237
+ ## [0.8.0] - 2026-05-27
238
+
239
+ ### Added
240
+ - **Causal discovery** (taxonomy Task 5, learning causal models). `discover` runs the PC algorithm
241
+ over discrete data — conditional-independence tests by thresholded conditional mutual information
242
+ (`conditional_mutual_information`), then collider orientation and Meek rules R1–R3 — returning a
243
+ `CPDAG`. `CPDAG.to_causal_graph()` bridges a fully oriented result into the rest of the library, so
244
+ a discovered structure feeds straight into POMIS planning. Faithful to Spirtes, Glymour & Scheines
245
+ (PC) and Meek (UAI 1995). Conservative: assumes causal sufficiency, and the bridge raises rather
246
+ than guessing an orientation.
247
+ - `build_discovery_scm` / `sample_discovery_data` — a collider demo (`X→Z←Y`, `Z→W`) whose CPDAG is
248
+ recovered from data and then handed to `pomis`.
249
+
250
+ ## [0.7.0] - 2026-05-27
251
+
252
+ ### Added
253
+ - **Transportability** (taxonomy Task 4, generalizability & robustness). `SelectionDiagram`
254
+ represents a source/target pair differing in some mechanisms; `transport_formula` /
255
+ `is_transportable` decide whether `P*(y | do(x))` transfers and return the transport formula
256
+ (direct or S-admissible adjustment); `transported_effect` computes the transported estimate by
257
+ reweighting source conditionals with the target covariate marginal. Conservative — returns `None`
258
+ outside the supported class (no hedge-based sID completeness check). Faithful to Bareinboim &
259
+ Pearl (AAAI 2012; J. Causal Inference 2013) and Pearl & Bareinboim (Statistical Science 2014).
260
+ - `make_transport_domains` — the canonical covariate-shift demo (`Z→X, Z→Y, X→Y`, selection on
261
+ `Z`): the transport formula recovers the true target effect (~0.82) while naively reusing the
262
+ source effect is biased (~0.58).
263
+ - `CausalGraph.directed_edges` and `CausalGraph.bidirected_edges` accessors.
264
+
265
+ ## [0.6.0] - 2026-05-27
266
+
267
+ ### Added
268
+ - **Counterfactual decision-making** (taxonomy Task 3, Layer 3). `counterfactual_expectation`
269
+ returns `E[Y_{do(x)} | evidence]` and `effect_of_treatment_on_treated` returns the ETT
270
+ `E[Y_{treated} − Y_{control} | X = treated]`, both computed on an executable
271
+ `StructuralCausalModel` via abduction-action-prediction (`causalrl.identification.counterfactual`).
272
+ Faithful to Bareinboim, Forney & Pearl, *Bandits with Unobserved Confounders: A Causal Approach*
273
+ (NeurIPS 2015) and Pearl, *Causality* (2nd ed.) §8.2.1.
274
+ - `CounterfactualOptimalPolicy` — a model-based Regret Decision Criterion policy that decides by
275
+ `argmax_a E[Y_{do(a)} | intent]` — and `CounterfactualBanditEnv` /
276
+ `make_counterfactual_bandit_env`, a 3-arm confounded bandit. The counterfactual-optimal policy and
277
+ a trained `CausalThompsonSampling` reach the per-intent optimum (~0.8); a confounding-naive agent
278
+ is stuck at the best fixed intervention (the `do`-optimum, ~0.367).
279
+
280
+ ### Changed
281
+ - Public environments now satisfy Gymnasium's checker, rollout helpers handle truncation, SCM
282
+ sampling isolates Torch RNG state, executable SCM definitions validate their graph contract,
283
+ and structural-bandit Thompson sampling supports bounded fractional rewards correctly.
284
+ - Multi-stage `DOVI` exposes whether transition-value propagation is causally certified, and
285
+ `POMISThompsonSampling` accepts an explicit validated `manipulable` contract.
286
+ - Deterministic multi-seed benchmark reporting, API documentation, citation/contribution
287
+ metadata, documentation CI, and release-only trusted PyPI publishing scaffolding were added.
288
+
289
+ ## [0.5.0] - 2026-05-26
290
+
291
+ ### Added
292
+ - **Non-manipulable variables** (extends taxonomy Task 2): `pomis` and
293
+ `minimal_intervention_sets` accept an optional `manipulable` subset. With non-manipulable set
294
+ `N`, POMIS equals the unconstrained POMIS of the latent projection onto `V\N` (Lee &
295
+ Bareinboim, *Structural Causal Bandits with Non-Manipulable Variables*, AAAI 2019, R-40,
296
+ Theorem 4); MIS simply filters to sets disjoint from `N`.
297
+ - `CausalGraph.latent_projection(keep)` — the Tian-Pearl / Verma latent projection.
298
+ - `make_frontdoor_env` (the R-40 front-door / cholesterol demo, with `Z` non-manipulable) and
299
+ `NaivePOMISThompsonSampling`; the manipulability-aware `POMISThompsonSampling` reaches the
300
+ `do(X)` optimum (~0.56) that the naive filter baseline (stuck at ~0.50 observation) cannot see.
301
+
302
+ ### Changed
303
+ - `POMISThompsonSampling` infers its manipulable set from the environment's arms, so it
304
+ respects non-manipulable variables automatically (identical behavior when all variables are
305
+ manipulable).
306
+ - Stable causal-method contracts are now conservative: `StructuralCausalModel` executes
307
+ explicit-latent DAGs only, `backdoor_adjustment_set` refuses latent-confounded treatments,
308
+ and `is_identifiable` returns unknown for unsupported ADMG cases rather than an optimistic
309
+ positive result.
310
+ - The qualitative `confounding_sensitivity_bounds` helper moved to
311
+ `causalrl.experimental.ope`; its previous module path remains as a deprecated bridge and it
312
+ is no longer part of the stable top-level exports.
313
+ - Stable public exports are loaded lazily. Core graph, POMIS, tabular-agent, and tabular-env
314
+ use no longer requires PyTorch; install the `torch` extra for SCM/neural/Torch-backed
315
+ components. Supported Python now begins at 3.11 and CI covers 3.11 and 3.14.
316
+
317
+ ## [0.4.0] - 2026-05-26
318
+
319
+ ### Added
320
+ - **POMIS engine** (taxonomy Task 2, "where to intervene"): `pomis` and
321
+ `minimal_intervention_sets` compute the possibly-optimal / minimal intervention sets of a
322
+ single-reward ADMG via MUCT (minimal unobserved-confounder territory) and the
323
+ interventional border. Adapted from the MIT-licensed reference implementation of
324
+ Lee & Bareinboim, *Structural Causal Bandits: Where to Intervene?* (NeurIPS 2018),
325
+ github.com/sanghack81/SCMMAB-NIPS2018 (Copyright (c) 2018 Sanghack Lee).
326
+ - `StructuralCausalBanditEnv` — an SCM-backed bandit whose arms are interventions, plus the
327
+ `make_confounded_chain_env` demo where observing beats every fixed intervention.
328
+ - `POMISThompsonSampling`, `BruteForceInterventionTS`, and `FixedSetThompsonSampling` agents;
329
+ the POMIS agent converges to the optimal arm far faster than brute force and beats a naive
330
+ fixed-set agent.
331
+ - `CausalGraph` gains `ancestors`, `descendants`, `induced_subgraph`, and `do_mutilate`.
332
+
333
+ ## [0.3.0] - 2026-05-26
334
+
335
+ ### Added
336
+ - Horizon-indexed **DOVI** (Deconfounded Optimistic Value Iteration): finite-horizon backward
337
+ induction with Manski-bound-capped rewards; reduces exactly to the v0.2 backup at `H=1`.
338
+ - `SequentialDTREnv` — a genuinely confounded, multi-stage dynamic-treatment-regime environment
339
+ with a foresight gap (the immediate-greedy and lookahead-optimal first actions diverge).
340
+ - Curated top-level public API with an explicit `__all__`:
341
+ `from causalrl import DOVI, StructuralCausalModel, DTREnv, generate_logs, ...`.
342
+ - `py.typed` marker (PEP 561) so downstream consumers receive our type information.
343
+ - `LICENSE` file (MIT) and this changelog.
344
+
345
+ ### Changed
346
+ - `SequentialMABUCEnv` rewritten to be genuinely confounded: a hidden confounder drives both
347
+ logging and reward, so the naive per-context mean is biased above the true interventional value.
348
+ - Sharpened the Manski-bounds property tests (higher sample size, tighter propensity, smaller slack).
349
+ - Version is single-sourced from `pyproject.toml` and read at runtime via `importlib.metadata`.
350
+
351
+ ### Fixed
352
+ - The offline-to-online example re-imported names already imported in an earlier cell, tripping
353
+ ruff's `F811` and failing the CI lint step; the redundant imports are removed (the notebook
354
+ runs identically). `ruff check .` is now clean across the whole repository, including notebooks.
355
+
356
+ ## [0.2.0] - 2026-05-23
357
+
358
+ ### Added
359
+ - Causal **offline-to-online** learning (taxonomy Task 1): `UCDTR`, `DOVI`, and
360
+ `DeepDeconfoundedQ` agents that read confounded logs through causal bounds.
361
+ - Confounded environments: `DTREnv`, `ConfoundedGridworld`, `SequentialMABUCEnv`.
362
+ - Manski natural bounds (`causal_q_bounds`) and the offline-to-online evaluation harness.
363
+
364
+ ## [0.1.0] - 2026-05-23
365
+
366
+ ### Added
367
+ - Structural causal model core: `CausalGraph`, mechanisms, and `StructuralCausalModel` with
368
+ `see` (L1), `do` (L2), and `counterfactual` (L3) queries.
369
+ - Scoped identification: back-door parent set and bow-arc detection.
370
+ - MABUC bandit slice with causal Thompson sampling that beats a confounding-naive baseline.
@@ -0,0 +1,12 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use causalrl in research, please cite this software."
3
+ title: "causalrl: Causal intervention-selection and causal-RL research tools"
4
+ type: software
5
+ authors:
6
+ - family-names: "Coelho"
7
+ given-names: "Raphael"
8
+ repository-code: "https://github.com/raphaelrrcoelho/causalrl"
9
+ url: "https://github.com/raphaelrrcoelho/causalrl"
10
+ license: MIT
11
+ version: "0.5.0"
12
+ date-released: "2026-05-26"
@@ -0,0 +1,30 @@
1
+ # Contributing
2
+
3
+ `causalrl` accepts focused fixes, reference-validated causal algorithms, benchmark
4
+ improvements, and documentation corrections.
5
+
6
+ ## Development Setup
7
+
8
+ ```bash
9
+ git clone https://github.com/raphaelrrcoelho/causalrl.git
10
+ cd causalrl
11
+ uv sync --extra dev --extra docs
12
+ uv run --extra dev pytest
13
+ uv run --extra dev ruff check .
14
+ uv run --extra dev pyright src
15
+ uv run --extra docs mkdocs build --strict
16
+ ```
17
+
18
+ ## Expectations
19
+
20
+ - Add tests before changing behavior.
21
+ - Cite a primary source for implemented causal algorithms and include oracle fixtures or
22
+ reproducible benchmark evidence where applicable.
23
+ - State method assumptions in public APIs and documentation; do not present experimental
24
+ helpers as validated estimators.
25
+ - Keep new public API changes typed and documented.
26
+
27
+ ## Pull Requests
28
+
29
+ Describe the causal or software contract changed, the evidence used for validation, and the
30
+ commands run. Small, reviewable changes are preferred.
@@ -0,0 +1,13 @@
1
+ # Multi-stage build using the official uv image.
2
+ FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim AS base
3
+ WORKDIR /app
4
+
5
+ # Install dependencies first (cached layer)
6
+ COPY pyproject.toml ./
7
+ COPY README.md ./
8
+ COPY src ./src
9
+ RUN uv sync --extra dev --no-install-project
10
+ RUN uv sync --extra dev
11
+
12
+ # Default: run the test suite
13
+ CMD ["uv", "run", "pytest", "-v"]