ragfence 0.1.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 (282) hide show
  1. ragfence-0.1.0/.env.example +21 -0
  2. ragfence-0.1.0/.github/workflows/ci.yml +65 -0
  3. ragfence-0.1.0/.github/workflows/publish-pypi.yml +35 -0
  4. ragfence-0.1.0/.github/workflows/ragfence-test.yml +287 -0
  5. ragfence-0.1.0/.gitignore +33 -0
  6. ragfence-0.1.0/CHANGELOG.md +38 -0
  7. ragfence-0.1.0/CODE_OF_CONDUCT.md +20 -0
  8. ragfence-0.1.0/CONTRIBUTING.md +40 -0
  9. ragfence-0.1.0/LICENSE +201 -0
  10. ragfence-0.1.0/PKG-INFO +162 -0
  11. ragfence-0.1.0/README.md +134 -0
  12. ragfence-0.1.0/alembic/env.py +72 -0
  13. ragfence-0.1.0/alembic/script.py.mako +27 -0
  14. ragfence-0.1.0/alembic/versions/0001_base.py +28 -0
  15. ragfence-0.1.0/alembic/versions/0002_enums.py +44 -0
  16. ragfence-0.1.0/alembic/versions/0003_tables.py +206 -0
  17. ragfence-0.1.0/alembic/versions/0004_indexes.py +93 -0
  18. ragfence-0.1.0/alembic.ini +44 -0
  19. ragfence-0.1.0/docker-compose.yml +22 -0
  20. ragfence-0.1.0/docs/APP_FLOW.md +231 -0
  21. ragfence-0.1.0/docs/BACKEND_SCHEMA.md +331 -0
  22. ragfence-0.1.0/docs/IMPLEMENTATION_PLAN.md +406 -0
  23. ragfence-0.1.0/docs/PRD.md +217 -0
  24. ragfence-0.1.0/docs/README.md +19 -0
  25. ragfence-0.1.0/docs/TRD.md +415 -0
  26. ragfence-0.1.0/docs/UI_UX_DESIGN_BRIEF.md +150 -0
  27. ragfence-0.1.0/examples/generic_http.toml +15 -0
  28. ragfence-0.1.0/examples/reference.toml +17 -0
  29. ragfence-0.1.0/opencode.json +27 -0
  30. ragfence-0.1.0/openspec/changes/.gitkeep +1 -0
  31. ragfence-0.1.0/openspec/changes/archive/.gitkeep +1 -0
  32. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-acme-dataset/design.md +106 -0
  33. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-acme-dataset/proposal.md +70 -0
  34. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-acme-dataset/specs/synthetic-dataset/spec.md +109 -0
  35. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-acme-dataset/tasks.md +42 -0
  36. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-acme-dataset/verify-report.md +158 -0
  37. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-auth-context/design.md +128 -0
  38. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-auth-context/proposal.md +73 -0
  39. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-auth-context/specs/auth-context/spec.md +114 -0
  40. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-auth-context/tasks.md +72 -0
  41. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-auth-context/verify-report.md +161 -0
  42. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/design.md +111 -0
  43. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/proposal.md +77 -0
  44. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/specs/domain-models/spec.md +53 -0
  45. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/specs/policy-engine/spec.md +65 -0
  46. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/specs/project-bootstrap/spec.md +75 -0
  47. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/specs/storage-schema/spec.md +82 -0
  48. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/tasks.md +51 -0
  49. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-foundation/verify-report.md +176 -0
  50. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-secure-retrieval/design.md +101 -0
  51. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-secure-retrieval/proposal.md +74 -0
  52. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-secure-retrieval/specs/secure-retrieval/spec.md +95 -0
  53. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-secure-retrieval/tasks.md +53 -0
  54. ragfence-0.1.0/openspec/changes/archive/2026-08-13-ragfence-secure-retrieval/verify-report.md +148 -0
  55. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/apply-progress.md +32 -0
  56. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/build-output.txt +4 -0
  57. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/design.md +78 -0
  58. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/proposal.md +49 -0
  59. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/specs/attack-engine/spec.md +80 -0
  60. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/tasks.md +40 -0
  61. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/test-output.txt +20 -0
  62. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-attack-engine/verify-report.md +71 -0
  63. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/apply-progress.md +28 -0
  64. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/build-output.txt +4 -0
  65. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/design.md +59 -0
  66. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/proposal.md +48 -0
  67. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/specs/cli/spec.md +117 -0
  68. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/tasks.md +41 -0
  69. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/test-output.txt +19 -0
  70. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-cli/verify-report.md +73 -0
  71. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/apply-progress.md +67 -0
  72. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/build-output.txt +4 -0
  73. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/design.md +69 -0
  74. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/proposal.md +42 -0
  75. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/specs/evaluation/spec.md +156 -0
  76. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/tasks.md +65 -0
  77. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/test-output.txt +5 -0
  78. ragfence-0.1.0/openspec/changes/archive/2026-08-17-ragfence-evaluation/verify-report.md +88 -0
  79. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/apply-progress.md +19 -0
  80. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/build-output.txt +4 -0
  81. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/design.md +68 -0
  82. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/proposal.md +41 -0
  83. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/specs/ci-integration/spec.md +155 -0
  84. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/tasks.md +48 -0
  85. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/test-output.txt +7 -0
  86. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-ci-integration/verify-report.md +60 -0
  87. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/apply-progress.md +35 -0
  88. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/build-output.txt +4 -0
  89. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/design.md +72 -0
  90. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/proposal.md +38 -0
  91. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/specs/generic-http/spec.md +163 -0
  92. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/tasks.md +59 -0
  93. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/test-output.txt +6 -0
  94. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-generic-http/verify-report.md +69 -0
  95. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/apply-progress.md +24 -0
  96. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/build-output.txt +4 -0
  97. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/design.md +55 -0
  98. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/proposal.md +37 -0
  99. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/specs/llm-providers/spec.md +162 -0
  100. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/tasks.md +59 -0
  101. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/test-output.txt +7 -0
  102. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-llm-providers/verify-report.md +63 -0
  103. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/apply-progress.md +39 -0
  104. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/build-output.txt +4 -0
  105. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/design.md +35 -0
  106. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/proposal.md +42 -0
  107. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/specs/release-readiness/spec.md +119 -0
  108. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/tasks.md +58 -0
  109. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/test-output.txt +7 -0
  110. ragfence-0.1.0/openspec/changes/archive/2026-08-18-ragfence-release-readiness/verify-report.md +60 -0
  111. ragfence-0.1.0/openspec/changes/ragfence-production-e2e/design.md +19 -0
  112. ragfence-0.1.0/openspec/changes/ragfence-production-e2e/proposal.md +23 -0
  113. ragfence-0.1.0/openspec/changes/ragfence-production-e2e/specs/production-e2e/spec.md +77 -0
  114. ragfence-0.1.0/openspec/changes/ragfence-production-e2e/tasks.md +11 -0
  115. ragfence-0.1.0/openspec/config.yaml +50 -0
  116. ragfence-0.1.0/openspec/specs/.gitkeep +1 -0
  117. ragfence-0.1.0/openspec/specs/auth-context/spec.md +114 -0
  118. ragfence-0.1.0/openspec/specs/domain-models/spec.md +53 -0
  119. ragfence-0.1.0/openspec/specs/policy-engine/spec.md +65 -0
  120. ragfence-0.1.0/openspec/specs/project-bootstrap/spec.md +75 -0
  121. ragfence-0.1.0/openspec/specs/secure-retrieval/spec.md +95 -0
  122. ragfence-0.1.0/openspec/specs/storage-schema/spec.md +82 -0
  123. ragfence-0.1.0/openspec/specs/synthetic-dataset/spec.md +109 -0
  124. ragfence-0.1.0/pyproject.toml +67 -0
  125. ragfence-0.1.0/src/ragfence/__init__.py +3 -0
  126. ragfence-0.1.0/src/ragfence/adapters/__init__.py +6 -0
  127. ragfence-0.1.0/src/ragfence/adapters/errors.py +38 -0
  128. ragfence-0.1.0/src/ragfence/adapters/factory.py +16 -0
  129. ragfence-0.1.0/src/ragfence/adapters/generic_http.py +466 -0
  130. ragfence-0.1.0/src/ragfence/adapters/mapping.py +67 -0
  131. ragfence-0.1.0/src/ragfence/adapters/models.py +38 -0
  132. ragfence-0.1.0/src/ragfence/adapters/reference.py +154 -0
  133. ragfence-0.1.0/src/ragfence/adapters/wire.py +57 -0
  134. ragfence-0.1.0/src/ragfence/attacks/__init__.py +17 -0
  135. ragfence-0.1.0/src/ragfence/attacks/generators.py +301 -0
  136. ragfence-0.1.0/src/ragfence/attacks/runner.py +131 -0
  137. ragfence-0.1.0/src/ragfence/attacks/scenarios.py +83 -0
  138. ragfence-0.1.0/src/ragfence/auth/__init__.py +33 -0
  139. ragfence-0.1.0/src/ragfence/auth/builder.py +32 -0
  140. ragfence-0.1.0/src/ragfence/auth/db.py +86 -0
  141. ragfence-0.1.0/src/ragfence/auth/errors.py +18 -0
  142. ragfence-0.1.0/src/ragfence/auth/identity.py +31 -0
  143. ragfence-0.1.0/src/ragfence/auth/providers.py +78 -0
  144. ragfence-0.1.0/src/ragfence/auth/registry.py +28 -0
  145. ragfence-0.1.0/src/ragfence/cli/__init__.py +1 -0
  146. ragfence-0.1.0/src/ragfence/cli/config.py +93 -0
  147. ragfence-0.1.0/src/ragfence/cli/errors.py +9 -0
  148. ragfence-0.1.0/src/ragfence/cli/main.py +227 -0
  149. ragfence-0.1.0/src/ragfence/cli/reporting.py +89 -0
  150. ragfence-0.1.0/src/ragfence/cli/seams.py +211 -0
  151. ragfence-0.1.0/src/ragfence/core/__init__.py +1 -0
  152. ragfence-0.1.0/src/ragfence/core/enums.py +48 -0
  153. ragfence-0.1.0/src/ragfence/core/models.py +177 -0
  154. ragfence-0.1.0/src/ragfence/core/protocols.py +49 -0
  155. ragfence-0.1.0/src/ragfence/datasets/__init__.py +28 -0
  156. ragfence-0.1.0/src/ragfence/datasets/acme.py +373 -0
  157. ragfence-0.1.0/src/ragfence/datasets/checksums.py +27 -0
  158. ragfence-0.1.0/src/ragfence/datasets/constants.py +90 -0
  159. ragfence-0.1.0/src/ragfence/datasets/loader.py +233 -0
  160. ragfence-0.1.0/src/ragfence/db/__init__.py +1 -0
  161. ragfence-0.1.0/src/ragfence/db/base.py +12 -0
  162. ragfence-0.1.0/src/ragfence/db/models.py +509 -0
  163. ragfence-0.1.0/src/ragfence/db/repositories.py +152 -0
  164. ragfence-0.1.0/src/ragfence/db/vector_store.py +82 -0
  165. ragfence-0.1.0/src/ragfence/evaluation/__init__.py +1 -0
  166. ragfence-0.1.0/src/ragfence/evaluation/contracts.py +58 -0
  167. ragfence-0.1.0/src/ragfence/evaluation/engine.py +377 -0
  168. ragfence-0.1.0/src/ragfence/evaluation/findings.py +154 -0
  169. ragfence-0.1.0/src/ragfence/evaluation/gate.py +117 -0
  170. ragfence-0.1.0/src/ragfence/evaluation/matrix_runner.py +315 -0
  171. ragfence-0.1.0/src/ragfence/evaluation/metrics.py +152 -0
  172. ragfence-0.1.0/src/ragfence/evaluation/redaction.py +105 -0
  173. ragfence-0.1.0/src/ragfence/evaluation/report.py +166 -0
  174. ragfence-0.1.0/src/ragfence/evaluation/repositories.py +353 -0
  175. ragfence-0.1.0/src/ragfence/evaluation/scoring.py +83 -0
  176. ragfence-0.1.0/src/ragfence/observability/__init__.py +1 -0
  177. ragfence-0.1.0/src/ragfence/policies/__init__.py +1 -0
  178. ragfence-0.1.0/src/ragfence/policies/decision.py +52 -0
  179. ragfence-0.1.0/src/ragfence/policies/guard.py +80 -0
  180. ragfence-0.1.0/src/ragfence/providers/__init__.py +21 -0
  181. ragfence-0.1.0/src/ragfence/providers/errors.py +24 -0
  182. ragfence-0.1.0/src/ragfence/providers/fakes.py +51 -0
  183. ragfence-0.1.0/src/ragfence/providers/llm.py +466 -0
  184. ragfence-0.1.0/src/ragfence/providers/tokens.py +23 -0
  185. ragfence-0.1.0/src/ragfence/providers/transport.py +85 -0
  186. ragfence-0.1.0/src/ragfence/reference/alembic/env.py +72 -0
  187. ragfence-0.1.0/src/ragfence/reference/alembic/script.py.mako +27 -0
  188. ragfence-0.1.0/src/ragfence/reference/alembic/versions/0001_base.py +28 -0
  189. ragfence-0.1.0/src/ragfence/reference/alembic/versions/0002_enums.py +44 -0
  190. ragfence-0.1.0/src/ragfence/reference/alembic/versions/0003_tables.py +206 -0
  191. ragfence-0.1.0/src/ragfence/reference/alembic/versions/0004_indexes.py +93 -0
  192. ragfence-0.1.0/src/ragfence/reference/alembic.ini +44 -0
  193. ragfence-0.1.0/src/ragfence/reference/docker-compose.yml +22 -0
  194. ragfence-0.1.0/src/ragfence/release_readiness/__init__.py +1 -0
  195. ragfence-0.1.0/src/ragfence/release_readiness/audit.py +113 -0
  196. ragfence-0.1.0/src/ragfence/release_readiness/benchmark.py +97 -0
  197. ragfence-0.1.0/src/ragfence/release_readiness/checklist.py +79 -0
  198. ragfence-0.1.0/src/ragfence/retrieval/__init__.py +32 -0
  199. ragfence-0.1.0/src/ragfence/retrieval/citations.py +54 -0
  200. ragfence-0.1.0/src/ragfence/retrieval/result.py +39 -0
  201. ragfence-0.1.0/src/ragfence/retrieval/service.py +144 -0
  202. ragfence-0.1.0/src/ragfence/retrieval/trace.py +24 -0
  203. ragfence-0.1.0/tests/__init__.py +0 -0
  204. ragfence-0.1.0/tests/conftest.py +48 -0
  205. ragfence-0.1.0/tests/fixtures/ci_report.json +17 -0
  206. ragfence-0.1.0/tests/integration/__init__.py +3 -0
  207. ragfence-0.1.0/tests/integration/conftest.py +38 -0
  208. ragfence-0.1.0/tests/integration/test_auth_db.py +121 -0
  209. ragfence-0.1.0/tests/integration/test_datasets_seed.py +140 -0
  210. ragfence-0.1.0/tests/integration/test_evaluation_e2e_db.py +209 -0
  211. ragfence-0.1.0/tests/integration/test_evaluation_persistence_g2_g4_db.py +101 -0
  212. ragfence-0.1.0/tests/integration/test_generic_http_stub.py +184 -0
  213. ragfence-0.1.0/tests/integration/test_migrations.py +254 -0
  214. ragfence-0.1.0/tests/integration/test_reference_adapter.py +76 -0
  215. ragfence-0.1.0/tests/integration/test_release_e2e_db.py +72 -0
  216. ragfence-0.1.0/tests/integration/test_retrieval_db.py +275 -0
  217. ragfence-0.1.0/tests/support/__init__.py +0 -0
  218. ragfence-0.1.0/tests/support/generic_http_stub.py +91 -0
  219. ragfence-0.1.0/tests/support/vector_store.py +83 -0
  220. ragfence-0.1.0/tests/test_acl.py +135 -0
  221. ragfence-0.1.0/tests/test_attack_generators.py +71 -0
  222. ragfence-0.1.0/tests/test_attack_runner.py +185 -0
  223. ragfence-0.1.0/tests/test_attack_scenarios.py +31 -0
  224. ragfence-0.1.0/tests/test_auth_builder.py +137 -0
  225. ragfence-0.1.0/tests/test_auth_db.py +138 -0
  226. ragfence-0.1.0/tests/test_auth_errors.py +45 -0
  227. ragfence-0.1.0/tests/test_auth_providers.py +124 -0
  228. ragfence-0.1.0/tests/test_auth_registry.py +84 -0
  229. ragfence-0.1.0/tests/test_bootstrap.py +38 -0
  230. ragfence-0.1.0/tests/test_ci.py +62 -0
  231. ragfence-0.1.0/tests/test_ci_workflow_structure.py +229 -0
  232. ragfence-0.1.0/tests/test_cli.py +78 -0
  233. ragfence-0.1.0/tests/test_cli_config.py +41 -0
  234. ragfence-0.1.0/tests/test_cli_reporting.py +292 -0
  235. ragfence-0.1.0/tests/test_compose.py +44 -0
  236. ragfence-0.1.0/tests/test_datasets_determinism.py +103 -0
  237. ragfence-0.1.0/tests/test_datasets_loader.py +175 -0
  238. ragfence-0.1.0/tests/test_datasets_org.py +118 -0
  239. ragfence-0.1.0/tests/test_datasets_security.py +122 -0
  240. ragfence-0.1.0/tests/test_db_gating.py +53 -0
  241. ragfence-0.1.0/tests/test_decision.py +144 -0
  242. ragfence-0.1.0/tests/test_enums.py +53 -0
  243. ragfence-0.1.0/tests/test_evaluation_cli_seam.py +130 -0
  244. ragfence-0.1.0/tests/test_evaluation_contracts.py +36 -0
  245. ragfence-0.1.0/tests/test_evaluation_control_matrix.py +115 -0
  246. ragfence-0.1.0/tests/test_evaluation_engine.py +134 -0
  247. ragfence-0.1.0/tests/test_evaluation_findings.py +85 -0
  248. ragfence-0.1.0/tests/test_evaluation_gate.py +187 -0
  249. ragfence-0.1.0/tests/test_evaluation_generation_gating.py +168 -0
  250. ragfence-0.1.0/tests/test_evaluation_metrics.py +68 -0
  251. ragfence-0.1.0/tests/test_evaluation_persistence.py +131 -0
  252. ragfence-0.1.0/tests/test_evaluation_redaction.py +68 -0
  253. ragfence-0.1.0/tests/test_evaluation_report.py +101 -0
  254. ragfence-0.1.0/tests/test_evaluation_repositories.py +214 -0
  255. ragfence-0.1.0/tests/test_evaluation_scoring.py +37 -0
  256. ragfence-0.1.0/tests/test_generic_http_adapter.py +122 -0
  257. ragfence-0.1.0/tests/test_generic_http_cli.py +107 -0
  258. ragfence-0.1.0/tests/test_generic_http_config.py +90 -0
  259. ragfence-0.1.0/tests/test_generic_http_mapping.py +124 -0
  260. ragfence-0.1.0/tests/test_generic_http_transport.py +182 -0
  261. ragfence-0.1.0/tests/test_generic_http_wire.py +75 -0
  262. ragfence-0.1.0/tests/test_guard.py +182 -0
  263. ragfence-0.1.0/tests/test_identity_adapter.py +127 -0
  264. ragfence-0.1.0/tests/test_identity_config.py +80 -0
  265. ragfence-0.1.0/tests/test_identity_secrets_hygiene.py +397 -0
  266. ragfence-0.1.0/tests/test_llm_config.py +117 -0
  267. ragfence-0.1.0/tests/test_llm_end_to_end.py +240 -0
  268. ragfence-0.1.0/tests/test_llm_transport.py +157 -0
  269. ragfence-0.1.0/tests/test_matrix_runner.py +197 -0
  270. ragfence-0.1.0/tests/test_migration_ddl.py +87 -0
  271. ragfence-0.1.0/tests/test_models.py +258 -0
  272. ragfence-0.1.0/tests/test_protocols.py +122 -0
  273. ragfence-0.1.0/tests/test_release_basics.py +77 -0
  274. ragfence-0.1.0/tests/test_release_e2e.py +69 -0
  275. ragfence-0.1.0/tests/test_release_packaging.py +131 -0
  276. ragfence-0.1.0/tests/test_release_readiness_docs.py +49 -0
  277. ragfence-0.1.0/tests/test_release_readiness_examples.py +98 -0
  278. ragfence-0.1.0/tests/test_release_work_units_6_9.py +69 -0
  279. ragfence-0.1.0/tests/test_repositories.py +214 -0
  280. ragfence-0.1.0/tests/test_schema_metadata.py +511 -0
  281. ragfence-0.1.0/tests/test_secure_retrieval.py +304 -0
  282. ragfence-0.1.0/uv.lock +921 -0
@@ -0,0 +1,21 @@
1
+ # RAGFence environment configuration template.
2
+ # Copy this file to `.env` and fill in real values for your environment.
3
+ # NEVER commit real credentials. Every value below is a placeholder.
4
+
5
+ # PostgreSQL DSN for the reference implementation (see docs/TRD.md, Section 11).
6
+ RAGFENCE_DATABASE_DSN=postgresql+psycopg://ragfence:CHANGEME@localhost:5432/ragfence
7
+
8
+ # Embedding dimension for pgvector columns (TRD default: 1536).
9
+ RAGFENCE_EMBEDDING_DIM=1536
10
+
11
+ # Default evaluation pass threshold (0-100).
12
+ RAGFENCE_EVALUATION_THRESHOLD=80
13
+
14
+ # Optional BYOK provider keys (see docs/TRD.md, Section 10).
15
+ # Leave unset to use the fake default providers with zero external dependencies.
16
+ RAGFENCE_LLM_API_KEY=CHANGEME
17
+ RAGFENCE_EMBEDDING_API_KEY=CHANGEME
18
+
19
+ # Optional OIDC identity provider settings for `auth` (synthetic source otherwise).
20
+ RAGFENCE_OIDC_ISSUER=CHANGEME
21
+ RAGFENCE_OIDC_CLIENT_ID=CHANGEME
@@ -0,0 +1,65 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ ci:
13
+ name: Lint, type, and test
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ matrix:
17
+ python-version: ["3.12"]
18
+ services:
19
+ postgres:
20
+ image: pgvector/pgvector:pg16
21
+ env:
22
+ POSTGRES_USER: ragfence
23
+ POSTGRES_PASSWORD: ragfence
24
+ POSTGRES_DB: ragfence
25
+ ports:
26
+ - 5434:5432
27
+ options: >-
28
+ --health-cmd "pg_isready -U ragfence -d ragfence"
29
+ --health-interval 10s
30
+ --health-timeout 5s
31
+ --health-retries 5
32
+ env:
33
+ RAGFENCE_TEST_DATABASE_DSN: postgresql+psycopg://ragfence:ragfence@localhost:5434/ragfence
34
+ steps:
35
+ - name: Checkout
36
+ uses: actions/checkout@v4
37
+
38
+ - name: Set up Python ${{ matrix.python-version }}
39
+ uses: actions/setup-python@v5
40
+ with:
41
+ python-version: ${{ matrix.python-version }}
42
+
43
+ - name: Install dependencies
44
+ run: |
45
+ python -m pip install --upgrade pip
46
+ python -m pip install -e ".[dev]"
47
+
48
+ - name: Migrate database schema
49
+ run: alembic upgrade head
50
+
51
+ - name: Seed reference database
52
+ run: |
53
+ python -c "from sqlalchemy import create_engine; from sqlalchemy.orm import Session; from ragfence.datasets import seed_reference_corp; import os; engine = create_engine(os.environ['RAGFENCE_TEST_DATABASE_DSN']); sess = Session(engine); seed_reference_corp(sess); sess.commit(); engine.dispose()"
54
+
55
+ - name: Lint (ruff check)
56
+ run: ruff check .
57
+
58
+ - name: Format (ruff format --check)
59
+ run: ruff format --check .
60
+
61
+ - name: Type check (mypy)
62
+ run: mypy
63
+
64
+ - name: Tests (pytest)
65
+ run: python -m pytest
@@ -0,0 +1,35 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ name: Build and publish distribution
15
+ runs-on: ubuntu-latest
16
+ environment:
17
+ name: pypi
18
+ url: https://pypi.org/p/ragfence
19
+ steps:
20
+ - name: Check out release source
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ - name: Install build tooling
29
+ run: python -m pip install --upgrade build
30
+
31
+ - name: Build wheel and source distribution
32
+ run: python -m build
33
+
34
+ - name: Publish distributions with PyPI Trusted Publishing
35
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,287 @@
1
+ name: RAGFence test
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ adapter:
7
+ description: Adapter passed to ragfence test
8
+ type: string
9
+ required: false
10
+ default: reference
11
+ target:
12
+ description: Target service name
13
+ type: string
14
+ required: false
15
+ default: target
16
+ base-url:
17
+ description: Target base URL
18
+ type: string
19
+ required: false
20
+ default: ""
21
+ threshold:
22
+ description: Inclusive score threshold from 0 to 100
23
+ type: number
24
+ required: false
25
+ default: 80
26
+ start-target-command:
27
+ description: Quoted command used to start the target service
28
+ type: string
29
+ required: false
30
+ default: ""
31
+ comment:
32
+ description: Enable the trusted same-repository PR comment path
33
+ type: boolean
34
+ required: false
35
+ default: true
36
+ fake-providers:
37
+ description: Use deterministic local fake providers
38
+ type: boolean
39
+ required: false
40
+ default: true
41
+
42
+ permissions:
43
+ contents: read
44
+ actions: read
45
+
46
+ jobs:
47
+ ragfence-test:
48
+ name: RAGFence evaluation (Python ${{ matrix.python-version }})
49
+ runs-on: ubuntu-latest
50
+ strategy:
51
+ matrix:
52
+ python-version: ["3.12"]
53
+ services:
54
+ postgres:
55
+ image: pgvector/pgvector:pg16
56
+ env:
57
+ POSTGRES_USER: ragfence
58
+ POSTGRES_PASSWORD: ragfence
59
+ POSTGRES_DB: ragfence
60
+ ports:
61
+ - 5434:5432
62
+ options: >-
63
+ --health-cmd "pg_isready -U ragfence -d ragfence"
64
+ --health-interval 10s
65
+ --health-timeout 5s
66
+ --health-retries 5
67
+ env:
68
+ REPORT_PATH: .ragfence/reports/ci.json
69
+ ARTIFACT_NAME: ragfence-report-${{ matrix.python-version }}
70
+ ADAPTER: ${{ inputs.adapter }}
71
+ TARGET: ${{ inputs.target }}
72
+ BASE_URL: ${{ inputs.base-url }}
73
+ THRESHOLD: ${{ inputs.threshold }}
74
+ START_TARGET_COMMAND: ${{ inputs.start-target-command }}
75
+ FAKE_PROVIDERS: ${{ inputs.fake-providers }}
76
+ RAGFENCE_TEST_DATABASE_DSN: postgresql+psycopg://ragfence:ragfence@localhost:5434/ragfence
77
+ steps:
78
+ - name: Checkout
79
+ uses: actions/checkout@v4
80
+
81
+ - name: Set up Python
82
+ uses: actions/setup-python@v5
83
+ with:
84
+ python-version: ${{ matrix.python-version }}
85
+
86
+ - name: Install dependencies
87
+ run: |
88
+ python -m pip install --upgrade pip
89
+ python -m pip install -e ".[dev]"
90
+
91
+ - name: Validate inputs
92
+ env:
93
+ INPUT_ADAPTER: ${{ inputs.adapter }}
94
+ INPUT_TARGET: ${{ inputs.target }}
95
+ INPUT_BASE_URL: ${{ inputs.base-url }}
96
+ INPUT_THRESHOLD: ${{ inputs.threshold }}
97
+ INPUT_START_COMMAND: ${{ inputs.start-target-command }}
98
+ run: |
99
+ python - <<'PY'
100
+ import math
101
+ import os
102
+ from urllib.parse import urlparse
103
+
104
+ adapter = os.environ["INPUT_ADAPTER"].strip()
105
+ target = os.environ["INPUT_TARGET"].strip()
106
+ base_url = os.environ["INPUT_BASE_URL"].strip()
107
+ start_command = os.environ["INPUT_START_COMMAND"].strip()
108
+ try:
109
+ threshold = float(os.environ["INPUT_THRESHOLD"])
110
+ except ValueError as error:
111
+ raise SystemExit("Invalid threshold configuration.") from error
112
+ if not adapter or not target:
113
+ raise SystemExit("Invalid RAGFence CI input configuration.")
114
+ if adapter == "generic_http":
115
+ parsed = urlparse(base_url)
116
+ if not start_command or parsed.scheme not in {"http", "https"} or not parsed.netloc:
117
+ raise SystemExit("Invalid RAGFence CI input configuration.")
118
+ if not math.isfinite(threshold) or not 0 <= threshold <= 100:
119
+ raise SystemExit("threshold must satisfy 0 <= threshold <= 100")
120
+ PY
121
+
122
+ - name: Initialize RAGFence
123
+ run: |
124
+ mkdir -p .ragfence/reports
125
+ ragfence init
126
+
127
+ - name: Migrate and seed reference database
128
+ run: |
129
+ alembic upgrade head
130
+ python -c "from sqlalchemy import create_engine; from sqlalchemy.orm import Session; from ragfence.datasets import seed_reference_corp; import os; engine = create_engine(os.environ['RAGFENCE_TEST_DATABASE_DSN']); sess = Session(engine); seed_reference_corp(sess); sess.commit(); engine.dispose()"
131
+
132
+ - name: Start target
133
+ if: ${{ inputs.start-target-command != '' }}
134
+ run: |
135
+ bash -c -- "$START_TARGET_COMMAND" >.ragfence/target-start.log 2>&1 &
136
+ printf '%s\n' "$!" > .ragfence/target.pid
137
+
138
+ - name: Wait for target readiness
139
+ if: ${{ inputs.start-target-command != '' && inputs.base-url != '' }}
140
+ env:
141
+ READINESS_ATTEMPTS: 30
142
+ READINESS_DELAY_SECONDS: 2
143
+ run: |
144
+ python - <<'PY'
145
+ import os
146
+ import sys
147
+ import time
148
+ import urllib.error
149
+ import urllib.request
150
+
151
+ url = os.environ["BASE_URL"]
152
+ attempts = int(os.environ["READINESS_ATTEMPTS"])
153
+ delay = int(os.environ["READINESS_DELAY_SECONDS"])
154
+ for attempt in range(attempts):
155
+ try:
156
+ with urllib.request.urlopen(url, timeout=2) as response:
157
+ if response.status < 500:
158
+ raise SystemExit(0)
159
+ except (OSError, urllib.error.URLError):
160
+ pass
161
+ if attempt + 1 < attempts:
162
+ time.sleep(delay)
163
+ print("Target readiness check failed after bounded retries.", file=sys.stderr)
164
+ raise SystemExit(2)
165
+ PY
166
+
167
+ - name: Evaluate target
168
+ id: evaluate
169
+ env:
170
+ CLI_THRESHOLD: ${{ inputs.threshold }}
171
+ run: |
172
+ set +e
173
+ if [ "$FAKE_PROVIDERS" = "true" ]; then
174
+ export RAGFENCE_LLM_PROVIDER=fake
175
+ export RAGFENCE_EMBEDDING_PROVIDER=fake
176
+ fi
177
+ LEGACY_THRESHOLD=$(python -c "print(float('$CLI_THRESHOLD') / 100.0)")
178
+ if [ "$ADAPTER" = "generic_http" ]; then
179
+ ragfence test --adapter "$ADAPTER" --base-url "$BASE_URL" --threshold "$LEGACY_THRESHOLD" --json --output "$REPORT_PATH"
180
+ else
181
+ ragfence test --adapter "$ADAPTER" --threshold "$LEGACY_THRESHOLD" --json --output "$REPORT_PATH"
182
+ fi
183
+ cli_status=$?
184
+ set -e
185
+ case "$cli_status" in
186
+ 0|1|2) ;;
187
+ *) cli_status=2 ;;
188
+ esac
189
+ if [ -s "$REPORT_PATH" ]; then report_exists=true; else report_exists=false; fi
190
+ {
191
+ printf 'status=%s\n' "$cli_status"
192
+ printf 'report_exists=%s\n' "$report_exists"
193
+ } >> "$GITHUB_OUTPUT"
194
+ exit "$cli_status"
195
+
196
+ - name: Upload JSON report
197
+ if: always()
198
+ uses: actions/upload-artifact@v4
199
+ with:
200
+ name: ${{ env.ARTIFACT_NAME }}
201
+ path: ${{ env.REPORT_PATH }}
202
+ if-no-files-found: error
203
+ retention-days: 14
204
+
205
+ - name: Write job summary
206
+ if: always()
207
+ env:
208
+ CLI_STATUS: ${{ steps.evaluate.outputs.status || '2' }}
209
+ REPORT_EXISTS: ${{ steps.evaluate.outputs.report_exists || 'false' }}
210
+ run: |
211
+ : "${GITHUB_STEP_SUMMARY:?summary file is required}"
212
+ python - <<'PY'
213
+ import json
214
+ import os
215
+ from pathlib import Path
216
+
217
+ path = Path(os.environ["REPORT_PATH"])
218
+ status = int(os.environ["CLI_STATUS"])
219
+ exists = os.environ["REPORT_EXISTS"] == "true" and path.is_file()
220
+ score = "unavailable"
221
+ outcome = "execution/configuration error"
222
+ passed = "false"
223
+ if exists:
224
+ try:
225
+ report = json.loads(path.read_text(encoding="utf-8"))
226
+ score_value = report.get("score")
227
+ if isinstance(score_value, (int, float)):
228
+ score = str(score_value)[:32]
229
+ outcome_value = report.get("outcome")
230
+ if isinstance(outcome_value, str):
231
+ outcome = outcome_value[:80]
232
+ passed_value = report.get("gate_passed")
233
+ if passed_value is None:
234
+ passed_value = report.get("passed_threshold")
235
+ if isinstance(passed_value, bool):
236
+ passed = str(passed_value).lower()
237
+ except (OSError, ValueError, TypeError):
238
+ outcome = "execution/configuration error"
239
+ if status == 1 and outcome == "execution/configuration error":
240
+ outcome = "below threshold"
241
+ summary = [
242
+ "## RAGFence evaluation",
243
+ "",
244
+ f"- Score: `{score}`",
245
+ f"- Threshold: `{os.environ['THRESHOLD'][:32]}`",
246
+ f"- Outcome: `{outcome}`",
247
+ f"- Passed threshold: `{passed}`",
248
+ f"- CLI exit status: `{status}`",
249
+ f"- Report: `{os.environ['REPORT_PATH']}`",
250
+ f"- Artifact: `{os.environ['ARTIFACT_NAME']}`",
251
+ ]
252
+ with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a", encoding="utf-8") as handle:
253
+ handle.write("\n".join(summary) + "\n")
254
+ PY
255
+
256
+ - name: Comment on trusted same-repository PR
257
+ if: ${{ inputs.comment && github.event_name == 'pull_request' && github.event_name != 'pull_request_target' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.base.repo.full_name == github.repository }}
258
+ uses: actions/github-script@v7
259
+ env:
260
+ CLI_STATUS: ${{ steps.evaluate.outputs.status || '2' }}
261
+ REPORT_PATH: ${{ env.REPORT_PATH }}
262
+ with:
263
+ script: |
264
+ const marker = '<!-- ragfence-ci-summary -->';
265
+ const body = `${marker}\n## RAGFence evaluation\n- CLI exit status: ${process.env.CLI_STATUS}\n- Report artifact: ${process.env.REPORT_PATH}`;
266
+ const { owner, repo } = context.repo;
267
+ const issue_number = context.issue.number;
268
+ const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 });
269
+ const existing = comments.find((comment) => comment.body && comment.body.includes(marker));
270
+ if (existing) {
271
+ await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
272
+ } else {
273
+ await github.rest.issues.createComment({ owner, repo, issue_number, body });
274
+ }
275
+
276
+ - name: Final gate
277
+ if: always()
278
+ env:
279
+ CLI_STATUS: ${{ steps.evaluate.outputs.status || '2' }}
280
+ REPORT_EXISTS: ${{ steps.evaluate.outputs.report_exists || 'false' }}
281
+ run: |
282
+ status="$CLI_STATUS"
283
+ if [ "$REPORT_EXISTS" != "true" ]; then status=2; fi
284
+ case "$status" in
285
+ 0|1|2) exit "$status" ;;
286
+ *) exit 2 ;;
287
+ esac
@@ -0,0 +1,33 @@
1
+ # Python bytecode and build artifacts
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+
9
+ # Virtual environments
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Tooling caches
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # Local agent tooling state (never ship or commit)
22
+ .atl/
23
+ .codegraph/
24
+ .hermes/
25
+
26
+ # Environment files — keep the committed template, never the real one
27
+ .env
28
+ .env.*
29
+ !.env.example
30
+
31
+ # OS noise
32
+ .DS_Store
33
+ Thumbs.db
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to RAGFence are documented here. This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) conventions.
4
+
5
+ ## [0.1.0] - 2026-08-21
6
+
7
+ ### Added
8
+
9
+ - Production evaluation path: declarative control matrix (8 required controls),
10
+ fail-closed `EvaluationGate` with PASS / FAIL / INCONCLUSIVE / SKIPPED statuses,
11
+ and security / utility / overall scores.
12
+ - ReferenceAdapter backed by PostgreSQL 16 + pgvector with DB-resolved identities:
13
+ Acme + Globex tenants, tenant isolation enforced before vector search,
14
+ soft-delete and clearance blocking, MUST_ALLOW / MUST_BLOCK semantics.
15
+ - Identity strategies for `generic_http`: header templates with allowlisted actor
16
+ placeholders and bearer-token resolution from environment configuration.
17
+ - Secret redaction across reports, findings, reasons, and errors.
18
+ - Self-contained wheel: `ragfence init --up` bootstraps the bundled pgvector
19
+ reference environment without a repository checkout.
20
+ - Idempotent bootstrap: a reachable reference database is migrated and reseeded
21
+ in place without invoking Docker Compose.
22
+ - Reusable GitHub Actions evaluation workflow with mandatory pgvector service,
23
+ healthchecks, real Alembic migrations, seeded data, and JSON report artifacts.
24
+
25
+ ### Changed
26
+
27
+ - Reports with a `.json` destination are always machine-parseable, with or
28
+ without `--json`.
29
+ - Explicit `--config` paths that do not exist fail closed with an actionable
30
+ error instead of silently falling back to defaults.
31
+ - Unwritable report destinations fail closed with exit code 2 and an actionable
32
+ message.
33
+
34
+ ### Security
35
+
36
+ - Least-privilege permissions on both CI workflows.
37
+ - Dependency audit clean (`pip-audit`); no secrets in package artifacts;
38
+ sdist excludes local agent tooling state.
@@ -0,0 +1,20 @@
1
+ # Code of Conduct
2
+
3
+ RAGFence is committed to a welcoming, respectful, and harassment-free community for everyone, regardless of identity, background, experience, or viewpoint.
4
+
5
+ ## Expected behavior
6
+
7
+ - Be kind, constructive, and respectful.
8
+ - Assume good intent while addressing impact.
9
+ - Focus feedback on the work, not the person.
10
+ - Respect privacy and do not publish private information.
11
+
12
+ ## Unacceptable behavior
13
+
14
+ Harassment, discrimination, threats, sexualized conduct, personal attacks, doxxing, and disruptive or abusive behavior are not tolerated.
15
+
16
+ ## Reporting
17
+
18
+ Report a concern privately to the project maintainers through the repository's issue/contact channel. Do not include sensitive information in a public issue. Reports will be reviewed promptly and handled confidentially where possible.
19
+
20
+ Maintainers may remove content, restrict participation, or take other action they consider appropriate. Retaliation against anyone who reports a concern in good faith is not acceptable.
@@ -0,0 +1,40 @@
1
+ # Contributing to RAGFence
2
+
3
+ Thanks for contributing. This project follows a spec-driven development (SDD)
4
+ workflow: significant changes start from a proposal in `openspec/changes/` and
5
+ flow through spec, design, tasks, implementation, and verification.
6
+
7
+ ## Development setup
8
+
9
+ Requires Python >= 3.12.
10
+
11
+ ```console
12
+ python -m venv .venv
13
+ .venv\Scripts\python -m pip install -e ".[dev]" # Windows
14
+ # source .venv/bin/activate && pip install -e ".[dev]" # POSIX
15
+ ```
16
+
17
+ ## Quality gates
18
+
19
+ All gates must pass before a change is reviewable:
20
+
21
+ ```console
22
+ python -m pytest # tests
23
+ ruff check . # lint
24
+ ruff format --check . # formatting
25
+ mypy # strict type checking
26
+ ```
27
+
28
+ Tests must pass **without Docker and without API keys**. Database-backed tests are
29
+ marked `@pytest.mark.db` and skip when PostgreSQL is unavailable.
30
+
31
+ ## Committing
32
+
33
+ - Use Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, `ci:`, ...).
34
+ - Tests ship with the behavior they verify — one work unit per commit.
35
+ - Do not add AI/Co-Authored-By attribution lines.
36
+ - Keep PRs under ~400 changed lines; larger changes are delivered as stacked PRs.
37
+
38
+ ## Reporting issues
39
+
40
+ Open an issue with reproduction steps and expected vs. actual behavior.