causal-continuity-engine 0.1.2__tar.gz → 0.1.3__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 (187) hide show
  1. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/verify_distributions.py +4 -1
  2. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/CHANGELOG.md +74 -0
  3. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/CITATION.cff +1 -1
  4. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/MANIFEST.in +1 -0
  5. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/PKG-INFO +1 -1
  6. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/__init__.py +1 -1
  7. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/extraction.py +167 -20
  8. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/PKG-INFO +1 -1
  9. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/SOURCES.txt +2 -0
  10. causal_continuity_engine-0.1.3/examples/backfill_github.py +230 -0
  11. causal_continuity_engine-0.1.3/examples/quickstart.py +199 -0
  12. causal_continuity_engine-0.1.3/tests/test_memory_extraction.py +493 -0
  13. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_portability.py +2 -0
  14. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round9_integrity.py +26 -0
  15. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_release_controls.py +2 -2
  16. causal_continuity_engine-0.1.2/tests/test_memory_extraction.py +0 -151
  17. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.editorconfig +0 -0
  18. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.gitattributes +0 -0
  19. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.githooks/commit-msg +0 -0
  20. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.githooks/pre-commit +0 -0
  21. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/CODEOWNERS +0 -0
  22. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/CONTRIBUTING.md +0 -0
  23. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
  24. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
  26. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/SECURITY.md +0 -0
  27. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/SUPPORT.md +0 -0
  28. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/dependabot.yml +0 -0
  29. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/pull_request_template.md +0 -0
  30. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/release.yml +0 -0
  31. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ruleset.README.md +0 -0
  32. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ruleset.json +0 -0
  33. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/bootstrap_tools.py +0 -0
  34. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/build_distributions.py +0 -0
  35. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_clean.py +0 -0
  36. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_release_metadata.py +0 -0
  37. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_release_tag.py +0 -0
  38. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_stdlib.py +0 -0
  39. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/prepare_release_tag.py +0 -0
  40. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/render_api_docs.py +0 -0
  41. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/run_gates.py +0 -0
  42. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/verify_public_schemas.py +0 -0
  43. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/tag-ruleset.json +0 -0
  44. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/ci.yml +0 -0
  45. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/commit-signature-audit.yml +0 -0
  46. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/dependency-review.yml +0 -0
  47. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/no-ai-attribution.yml +0 -0
  48. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/release.yml +0 -0
  49. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/secret-scan.yml +0 -0
  50. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.gitignore +0 -0
  51. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.gitleaks.toml +0 -0
  52. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.pre-commit-config.yaml +0 -0
  53. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/AGENTS.md +0 -0
  54. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/LICENSE.txt +0 -0
  55. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/NOTICE +0 -0
  56. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/README.md +0 -0
  57. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/SPEC.md +0 -0
  58. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/__init__.py +0 -0
  59. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/continuitybench/__init__.py +0 -0
  60. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/continuitybench/run.py +0 -0
  61. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/continuitybench/scenarios.py +0 -0
  62. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/api.py +0 -0
  63. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/capabilities.py +0 -0
  64. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/capsule.py +0 -0
  65. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/cli.py +0 -0
  66. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/core.py +0 -0
  67. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/engine.py +0 -0
  68. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/evidence.py +0 -0
  69. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/github.py +0 -0
  70. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/graph.py +0 -0
  71. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/invalidation.py +0 -0
  72. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/lamport.py +0 -0
  73. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/learning.py +0 -0
  74. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/memory.py +0 -0
  75. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/ontology.py +0 -0
  76. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/partial.py +0 -0
  77. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/policy.py +0 -0
  78. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/proof.py +0 -0
  79. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/py.typed +0 -0
  80. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/redaction.py +0 -0
  81. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/resume.py +0 -0
  82. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/store.py +0 -0
  83. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/verifiers.py +0 -0
  84. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/dependency_links.txt +0 -0
  85. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/entry_points.txt +0 -0
  86. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/requires.txt +0 -0
  87. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/top_level.txt +0 -0
  88. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/API.md +0 -0
  89. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/CAPABILITIES.md +0 -0
  90. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/PUBLIC-FLIP.md +0 -0
  91. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/RELEASE.md +0 -0
  92. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/REQUIREMENTS.md +0 -0
  93. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/REQUIREMENTS_COVERAGE.md +0 -0
  94. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/RESEARCH-ROADMAP.md +0 -0
  95. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/TESTING.md +0 -0
  96. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/adr/ADR-INDEX.md +0 -0
  97. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/assets/demo.svg +0 -0
  98. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/assets/social-preview.png +0 -0
  99. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/justfile +0 -0
  100. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/pyproject.toml +0 -0
  101. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/requirements-dev.in +0 -0
  102. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/requirements-dev.lock +0 -0
  103. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.anchor.v1.json +0 -0
  104. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.capsule.v1.json +0 -0
  105. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.continuity-receipt.v1.json +0 -0
  106. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.event.v1.json +0 -0
  107. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.proof-predicate.v1.json +0 -0
  108. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.proof.v1.json +0 -0
  109. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.recovery.v1.json +0 -0
  110. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.resume.v1.json +0 -0
  111. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/setup.cfg +0 -0
  112. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/__init__.py +0 -0
  113. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/schema_validation.py +0 -0
  114. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_api_contract.py +0 -0
  115. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_cli.py +0 -0
  116. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_conformance.py +0 -0
  117. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_distribution_environment.py +0 -0
  118. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_engine_e2e.py +0 -0
  119. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_github_redaction.py +0 -0
  120. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_instrument_validation.py +0 -0
  121. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_invalidation_resume.py +0 -0
  122. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_learning_partial_api.py +0 -0
  123. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions.py +0 -0
  124. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round10_release.py +0 -0
  125. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round11_deep_integrity.py +0 -0
  126. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round12_release_blockers.py +0 -0
  127. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round13_jcs.py +0 -0
  128. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round14_state_integrity.py +0 -0
  129. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round15_capsule_invalidation_github.py +0 -0
  130. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round15_public_boundaries.py +0 -0
  131. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round15_storage_boundaries.py +0 -0
  132. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round16_cli_api_core_capabilities.py +0 -0
  133. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round2.py +0 -0
  134. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round3.py +0 -0
  135. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round4.py +0 -0
  136. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round5.py +0 -0
  137. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round6.py +0 -0
  138. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round7.py +0 -0
  139. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_boundaries.py +0 -0
  140. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_engine.py +0 -0
  141. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_state.py +0 -0
  142. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round9_scope_freshness.py +0 -0
  143. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_release_boundary_validation.py +0 -0
  144. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_sqlite_lifecycle.py +0 -0
  145. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_store_graph.py +0 -0
  146. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_trust.py +0 -0
  147. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/action_intent_wrong_type.json +0 -0
  148. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/actor_wrong_type.json +0 -0
  149. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/created_at_invalid_calendar_date.json +0 -0
  150. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/created_at_noncanonical_offset.json +0 -0
  151. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_authoritative_failure.json +0 -0
  152. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_authoritative_pass.json +0 -0
  153. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_no_results.json +0 -0
  154. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_self_asserted_pass.json +0 -0
  155. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/generate.py +0 -0
  156. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/incomplete_failed_check.json +0 -0
  157. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/index.json +0 -0
  158. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/input_item_wrong_type.json +0 -0
  159. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/invalid_result_value.json +0 -0
  160. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/lamport_fingerprint_spoof.json +0 -0
  161. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/lamport_no_registry.json +0 -0
  162. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/lamport_truncated_key.json +0 -0
  163. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/missing_required_field.json +0 -0
  164. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/non_binary64_integer.json +0 -0
  165. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/policy_decision_wrong_type.json +0 -0
  166. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/retry_cannot_launder_a_failure.json +0 -0
  167. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/self_asserted_cannot_satisfy.json +0 -0
  168. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/self_assertion_cannot_poison_authoritative_pass.json +0 -0
  169. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/status_contradicts_contents.json +0 -0
  170. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/summary_contradicts_contents.json +0 -0
  171. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/tamper_statement.json +0 -0
  172. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/tamper_statement_resealed.json +0 -0
  173. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/task_id_under_unrelated_field.json +0 -0
  174. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unbound_task.json +0 -0
  175. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_action_intent_field.json +0 -0
  176. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_algorithm.json +0 -0
  177. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_schema_version.json +0 -0
  178. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_top_level_field.json +0 -0
  179. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unverified_forged_body_no_key.json +0 -0
  180. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unverified_hmac_no_key.json +0 -0
  181. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_hmac.json +0 -0
  182. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_hmac_jcs_edges.json +0 -0
  183. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_hmac_unicode.json +0 -0
  184. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_lamport.json +0 -0
  185. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/wrong_project.json +0 -0
  186. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/verifiers/__init__.py +0 -0
  187. {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/verifiers/verify_proof.py +0 -0
@@ -78,6 +78,7 @@ SDIST_SOURCE_TREES = {
78
78
  "benchmarks": {".py"},
79
79
  IMPORT_PACKAGE: {".py", "py.typed"},
80
80
  "docs": {".md", ".png", ".svg"},
81
+ "examples": {".py"},
81
82
  "schemas": {".json"},
82
83
  "tests": {".py"},
83
84
  "vectors": {".json", ".py"},
@@ -516,7 +517,9 @@ def _source_path_is_allowed(relative: str) -> bool:
516
517
  selectors = SDIST_SOURCE_TREES.get(parts[0])
517
518
  if selectors is None:
518
519
  return False
519
- if parts[0] in {"schemas", "tests", "vectors", "verifiers"} and len(parts) != 2:
520
+ if parts[0] in {
521
+ "examples", "schemas", "tests", "vectors", "verifiers",
522
+ } and len(parts) != 2:
520
523
  return False
521
524
  if parts[0] == "benchmarks" and not (
522
525
  len(parts) == 2
@@ -9,6 +9,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  No unreleased changes.
11
11
 
12
+ ## 0.1.3 — 2026-08-09
13
+
14
+ The deterministic extractor met real prose for the first time and did badly.
15
+ Twenty-two defects were found by running it against actual repository history
16
+ rather than fixtures, and every one is fixed here with a regression test that
17
+ was confirmed to fail against the code it pins. `AD-002` treats this extractor
18
+ as the degradation path rather than the primary one; a degradation path that
19
+ mangles its input is worse than useless, because nothing downstream can tell
20
+ the difference.
21
+
22
+ ### Fixed
23
+
24
+ - **Statements were fabricated across masked regions.** The gap between a cue
25
+ word and its clause matched newlines, so it walked over a blanked code fence
26
+ and joined the text either side — recording "The exporter must stream rows to
27
+ the client" from a body where those words never appear together. A fabricated
28
+ statement is worse than a lost one: nothing downstream can tell.
29
+ - **Sentences were truncated at the line wrap.** Every clause rule excluded
30
+ `\n`, so a sentence wrapped at eighty columns — which is how comment bodies
31
+ are written — was cut at the break. "must stream rows … instead of" survives
32
+ as a grammatical clause with its point removed. Identical text on one line
33
+ extracted in full, so the record depended on the author's editor.
34
+ - **Clauses began and ended mid-word.** An unanchored character budget started
35
+ the statement wherever the count landed ("stale capsules" recorded as "le
36
+ capsules"), and the tail bound ended it the same way in 23 of 40 measured
37
+ cases.
38
+ - **Dotted identifiers truncated or silently dropped statements.** A `.` was
39
+ always a sentence end, so `generate.py` and `v0.1.2` cut a clause short — and
40
+ because the forward patterns have a minimum length, "We assume numpy 1.26.4
41
+ is installed" extracted nothing at all while "We decided to pin pip 26.1.2"
42
+ recorded the false statement "pin pip 26".
43
+ - **Text nobody wrote became authority.** Code fences, blockquotes, HTML
44
+ comments and four-space indented blocks were read as the author's own prose.
45
+ A requirement hidden inside `<!-- ... -->` is invisible in every rendered
46
+ view, so no reviewer could catch it; a blockquote is by definition someone
47
+ else's words, frequently quoted in order to disagree with them.
48
+ - **A hidden checklist became actionable open work.** The checklist scan read
49
+ raw text while every other pattern read the mask, so `- [ ]` items inside a
50
+ comment or fence arrived as `open` task nodes in the resume packet, needing
51
+ no injection wording at all.
52
+ - **An invisible character inverted a prohibition.** A zero-width space inside
53
+ "not" left the sentence identical to a reader while re-typing the constraint
54
+ as a requirement, so control state mandated what the sentence forbids. Word
55
+ joiners and soft hyphens behaved the same, and word processors insert soft
56
+ hyphens routinely. Matching now ignores them while the statement keeps them,
57
+ so the packet still shows that the text carried them.
58
+ - **Non-ASCII statements collided.** The dedup key discarded every character
59
+ outside ASCII, so "must not exceed €500" and "must not exceed £500" shared a
60
+ key and one was dropped. In a script with no ASCII the key was empty, giving
61
+ every statement the same identity.
62
+ - Markdown decoration was stored inside statements; `must never X` was filed as
63
+ both a constraint and a requirement; consecutive table rows merged into one
64
+ statement; and longer or CRLF code fences never found their closer, masking
65
+ the remainder of the body and discarding every statement after it.
66
+
67
+ ### Added
68
+
69
+ - `examples/quickstart.py` — the whole arc end to end in a temporary directory:
70
+ a proof minted by actually running a pinned verifier, accepted for the task
71
+ it names, refused for one it does not, and checked by the independent
72
+ verifier, which reports UNVERIFIED without the key and VALID with it.
73
+ - `examples/backfill_github.py` — builds a project from a repository's real
74
+ history through the REST API, with no webhook, App or public endpoint. This
75
+ is the instrument that found everything above.
76
+ - A regression test for cross-project rejection in `Memory.promote`.
77
+
78
+ ### Changed
79
+
80
+ - **Node identity changes for statements containing non-ASCII characters.**
81
+ `stable_node_id` is derived from the dedup key, so such a statement now
82
+ hashes to a different id than it did in 0.1.2. A project that re-ingests one
83
+ records a new node rather than updating the old; the previous node remains,
84
+ with its history intact. Pure-ASCII projects are unaffected.
85
+
12
86
  ## 0.1.2 — 2026-08-09
13
87
 
14
88
  A documentation and hygiene release. No engine, schema, proof, or HTTP
@@ -8,7 +8,7 @@ authors:
8
8
  - given-names: Thomas
9
9
  family-names: Albrecht
10
10
  alias: The Quantum Falcon
11
- version: 0.1.2
11
+ version: 0.1.3
12
12
  date-released: 2026-08-09
13
13
  license: Apache-2.0
14
14
  repository-code: https://github.com/thequantumfalcon/causal-continuity-engine
@@ -4,6 +4,7 @@ include requirements-dev.in requirements-dev.lock
4
4
  include .editorconfig .gitattributes .gitignore .gitleaks.toml .pre-commit-config.yaml
5
5
  recursive-include causal_continuity_engine *.py py.typed
6
6
  recursive-include benchmarks *.py
7
+ recursive-include examples *.py
7
8
  recursive-include docs *.md
8
9
  include docs/assets/social-preview.png
9
10
  include docs/assets/demo.svg
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: causal-continuity-engine
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Causal Continuity Engine - continuity, causal invalidation, and proof for long-running coding agents
5
5
  Author: Thomas Albrecht
6
6
  License-Expression: Apache-2.0
@@ -7,7 +7,7 @@ authenticated runtime records are not replay-derived (ADR-001). Storage is
7
7
  SQLite implementing the PostgreSQL-first relational adjacency design (ADR-011).
8
8
  """
9
9
 
10
- __version__ = "0.1.2"
10
+ __version__ = "0.1.3"
11
11
 
12
12
  SCHEMA_VERSIONS = {
13
13
  "anchor": "cce.anchor.v1",
@@ -21,6 +21,7 @@ emitted as low-confidence 'proposed' items).
21
21
  from __future__ import annotations
22
22
 
23
23
  import re
24
+ import unicodedata
24
25
  from dataclasses import dataclass, field
25
26
 
26
27
  from .core import canonical_json, strict_json_loads
@@ -50,27 +51,74 @@ class ExtractionResult:
50
51
  extractor_version: str = EXTRACTOR_VERSION
51
52
 
52
53
 
54
+ # Modal patterns ("... must never ...") have to look backwards for the start of
55
+ # the clause, and both edges of that lookback need care on real prose.
56
+ #
57
+ # _CLAUSE_START anchors the beginning to a place a clause can actually begin.
58
+ # A bare `{0,80}?` lookback has no left boundary, so once the clause is longer
59
+ # than the budget the match starts wherever the character count lands — which
60
+ # on real text is reliably mid-word ("stale capsules" arriving as "le
61
+ # capsules").
62
+ #
63
+ # _IN_CLAUSE / _CLAUSE_TAIL treat `.` as a sentence end only when whitespace or
64
+ # the end of the text follows it. A dot inside an identifier — `generate.py`,
65
+ # `v0.1.0`, `README.md` — is ordinary clause content, and excluding it outright
66
+ # truncated statements to the fragment after the dot.
67
+ #
68
+ # The forward-capturing patterns need the same treatment, and failed harder
69
+ # without it: their tails have a minimum length, so a dotted identifier near
70
+ # the start of the clause left too few characters to match and the statement
71
+ # was dropped in silence. "We assume numpy 1.26.4 is installed" extracted
72
+ # nothing at all, and "We decided to pin pip 26.1.2" recorded the false
73
+ # statement "pin pip 26". In this domain those are the common sentences.
74
+ # Markdown soft-wraps: a single newline continues the paragraph, and comment
75
+ # bodies are wrapped at roughly eighty columns by every editor that touches
76
+ # them. Treating any newline as a clause end truncated most real sentences at
77
+ # the wrap — "must stream rows instead of" losing the half that says instead of
78
+ # what. A blank line, list item, heading or quote marker does begin a new
79
+ # logical unit, so those still bound the clause.
80
+ _SOFT_WRAP = r"\n(?![ \t]*(?:\n|[-*+>#|]|\d+[.)]))"
81
+ _CLAUSE_START = r"(?:\A|[\n;:]\s*|\.\s+)"
82
+ _IN_CLAUSE = r"(?:[^.\n;]|\.(?!\s|\Z)|" + _SOFT_WRAP + r")"
83
+ _CLAUSE_TAIL = r"(?:[^.\n]|\.(?!\s|\Z)|" + _SOFT_WRAP + r")"
84
+
85
+ # The gap between a cue word and the clause it introduces. A plain `\s+` also
86
+ # matches newlines, so it walked across paragraph breaks and across masked
87
+ # regions — stitching "The exporter must" to "stream rows to the client" over a
88
+ # code fence and recording a sentence the author never wrote. Fabricating a
89
+ # statement is worse than dropping one: nothing downstream can tell.
90
+ _GAP = r"(?:[ \t]|" + _SOFT_WRAP + r")+"
91
+
53
92
  # Patterns: (kind, regex, base_confidence)
54
93
  _PATTERNS: list[tuple[str, re.Pattern, float]] = [
55
94
  ("assumption", re.compile(
56
- r"\b(?:we\s+)?assum(?:e|es|ing|ption(?:\s*[:\-])?)\s+(?:that\s+)?(?P<s>[^.\n]{8,300})",
57
- re.I), 0.85),
95
+ r"\b(?:we\s+)?assum(?:e|es|ing|ption(?:\s*[:\-])?)" + _GAP + r"(?:that" + _GAP + r")?(?P<s>"
96
+ + _CLAUSE_TAIL + r"{8,300}(?!\w))", re.I), 0.85),
58
97
  ("assumption", re.compile(
59
- r"\b(?:relies|relying|depends?)\s+on\s+(?:the\s+fact\s+that\s+)?(?P<s>[^.\n]{8,300})",
60
- re.I), 0.7),
98
+ r"\b(?:relies|relying|depends?)" + _GAP + r"on" + _GAP
99
+ + r"(?:the" + _GAP + r"fact" + _GAP + r"that" + _GAP + r")?"
100
+ + r"(?P<s>" + _CLAUSE_TAIL + r"{8,300}(?!\w))", re.I), 0.7),
61
101
  ("assumption", re.compile(
62
- r"\b(?:provided|as\s+long\s+as|expects?\s+that)\s+(?P<s>[^.\n]{8,300})", re.I), 0.6),
102
+ r"\b(?:provided|as\s+long\s+as|expects?\s+that)" + _GAP + r"(?P<s>"
103
+ + _CLAUSE_TAIL + r"{8,300}(?!\w))", re.I), 0.6),
63
104
  ("constraint", re.compile(
64
- r"(?P<s>[^.\n;]{0,80}?\b(?:must\s+not|may\s+not|never|shall\s+not|"
65
- r"do\s+not\s+ever)\s+[^.\n]{4,300})", re.I), 0.85),
105
+ _CLAUSE_START + r"(?P<s>" + _IN_CLAUSE +
106
+ r"{0,120}?\b(?:must\s+not|may\s+not|never|shall\s+not|"
107
+ r"do\s+not\s+ever)" + _GAP + _CLAUSE_TAIL + r"{4,300}(?!\w))", re.I), 0.85),
66
108
  ("requirement", re.compile(
67
- r"(?P<s>[^.\n;]{0,80}?\b(?:must|shall|is\s+required\s+to|needs?\s+to)"
68
- r"(?!\s+not)\s+[^.\n]{4,300})", re.I), 0.8),
109
+ _CLAUSE_START + r"(?P<s>" + _IN_CLAUSE +
110
+ # A prohibition is a constraint and only a constraint; without the
111
+ # `never` exclusion "must never X" was filed as both, recording the
112
+ # same sentence twice under two different types.
113
+ r"{0,120}?\b(?:must|shall|is\s+required\s+to|needs?\s+to)"
114
+ r"(?!\s+(?:not|never))" + _GAP + _CLAUSE_TAIL + r"{4,300}(?!\w))", re.I), 0.8),
69
115
  ("requirement", re.compile(
70
- r"\bacceptance\s+criteri(?:a|on)\s*[:\-]\s*(?P<s>[^\n]{4,300})", re.I), 0.9),
116
+ r"\bacceptance\s+criteri(?:a|on)\s*[:\-]\s*"
117
+ + r"(?P<s>" + _CLAUSE_TAIL + r"{4,300}(?!\w))", re.I), 0.9),
71
118
  ("decision", re.compile(
72
119
  r"\b(?:we\s+)?(?:decided|decision(?:\s*[:\-])?|chose|will\s+use|agreed)\s+"
73
- r"(?:to\s+|on\s+|that\s+)?(?P<s>[^.\n]{4,300})", re.I), 0.8),
120
+ r"(?:(?:to|on|that)" + _GAP + r")?"
121
+ + r"(?P<s>" + _CLAUSE_TAIL + r"{4,300}(?!\w))", re.I), 0.8),
74
122
  ]
75
123
 
76
124
  _INJECTION_PATTERNS = re.compile(
@@ -147,10 +195,24 @@ class DeterministicExtractor:
147
195
  # matches (highest confidence wins) so one sentence never yields two
148
196
  # near-duplicate variants (e.g. "Acceptance criteria: X must Y" and
149
197
  # "X must Y").
198
+ # Patterns run over prose only; spans and context still quote the
199
+ # original text, so a report points at what the author actually wrote.
200
+ # The injection screen above deliberately reads the raw text: a payload
201
+ # hidden inside a fence is still a payload.
202
+ # Characters that render as nothing must not change how the text is
203
+ # READ, but they must still be VISIBLE in what is recorded: a reader
204
+ # has to be able to see that a sentence carried them. So matching runs
205
+ # over a copy with them removed, while every statement and span is
206
+ # sliced from the original through `offsets`. A zero-width space
207
+ # inside "not" had re-typed a prohibition as a requirement, so control
208
+ # state mandated what the sentence forbids.
209
+ visible, offsets = _without_invisibles(text)
210
+ prose = _prose_only(visible)
150
211
  candidates = []
151
212
  for kind, pattern, base_conf in _PATTERNS:
152
- for m in pattern.finditer(text):
153
- statement = _clean(m.group("s"))
213
+ for m in pattern.finditer(prose):
214
+ statement = _clean(
215
+ text[offsets[m.start("s")]:offsets[m.end("s")]])
154
216
  if not _plausible(statement):
155
217
  result.abstained += 1
156
218
  continue
@@ -193,8 +255,12 @@ class DeterministicExtractor:
193
255
  item.meta["demotion_reason"] = "untrusted source cannot mandate"
194
256
  result.items.append(item)
195
257
 
196
- for m in _CHECKLIST.finditer(text):
197
- statement = _clean(m.group("s"))
258
+ # Masking applies to EVERY extractor. Reading raw text here let a
259
+ # checklist hidden in an HTML comment or a code fence arrive as
260
+ # actionable open work — the exact harm the mask exists to prevent.
261
+ for m in _CHECKLIST.finditer(prose):
262
+ statement = _clean(
263
+ text[offsets[m.start("s")]:offsets[m.end("s")]])
198
264
  if not _plausible(statement):
199
265
  result.abstained += 1
200
266
  continue
@@ -207,7 +273,7 @@ class DeterministicExtractor:
207
273
  # payload arrives as actionable open work rather than as prose.
208
274
  result.items.append(Extracted(
209
275
  kind="task", statement=statement,
210
- span=m.group(0).strip(),
276
+ span=text[offsets[m.start()]:offsets[m.end()]].strip(),
211
277
  confidence=_calibrate(0.85, source_authority),
212
278
  criticality="medium", scope=scope,
213
279
  suspected_injection=block_compromised,
@@ -220,16 +286,97 @@ class DeterministicExtractor:
220
286
 
221
287
 
222
288
  def normalize_statement(statement: str) -> str:
223
- """Canonical key for deduplication (AD-004)."""
224
- s = statement.lower()
225
- s = re.sub(r"[^a-z0-9 ]+", " ", s)
289
+ """Canonical key for deduplication (AD-004).
290
+
291
+ Reducing to `[a-z0-9 ]` discarded every character outside ASCII, so two
292
+ statements collided whenever they differed only in what it threw away:
293
+ "must not exceed €500" and "must not exceed £500" produced one key, and
294
+ one of the two rules was dropped as a duplicate. In a non-Latin script the
295
+ loss was total — every statement reduced to the empty string, so a
296
+ Japanese or Russian project collapsed its whole control state onto a
297
+ single node per kind, this key being what seeds `stable_node_id`.
298
+
299
+ Compatibility forms are folded first so width and composition variants of
300
+ the same text still agree. Letters, digits, marks and symbols are kept;
301
+ only punctuation and separators become spaces.
302
+ """
303
+ s = unicodedata.normalize("NFKC", statement).casefold()
304
+ s = "".join(
305
+ c if unicodedata.category(c)[0] in "LNMS" else " " for c in s)
226
306
  s = re.sub(r"\b(the|a|an|is|are|was|were|be|been|that|this|it|its|of|to|in|on|for)\b",
227
307
  " ", s)
228
308
  return re.sub(r"\s+", " ", s).strip()
229
309
 
230
310
 
311
+ # Regions of a comment that are not the author asserting something. A fenced
312
+ # block is code, and code comments are full of modal verbs; a blockquote is
313
+ # someone else's words, frequently quoted in order to disagree with them; an
314
+ # HTML comment renders invisibly, so its text is something no reader of the
315
+ # thread ever saw. Extracting any of them attributes a statement to an author
316
+ # who did not make it — and in the HTML case, to one who could not have read
317
+ # it either.
318
+ #
319
+ # The fence closer is tied to its opener by backreference. Matching a bare
320
+ # three markers meant a longer fence never found its end and masked the rest
321
+ # of the body, silently swallowing the prose that followed; `\r?` does the
322
+ # same for CRLF text. Both failed closed in the wrong direction — losing real
323
+ # statements rather than admitting code.
324
+ _NON_PROSE = (
325
+ re.compile(r"^[ \t]*(?P<fence>`{3,}|~{3,}).*?"
326
+ r"(?:^[ \t]*(?P=fence)[`~]*[ \t]*\r?$|\Z)", re.S | re.M),
327
+ re.compile(r"^[ \t]*>.*$", re.M),
328
+ re.compile(r"<!--.*?(?:-->|\Z)", re.S),
329
+ # A four-space indented block is also code, and was masked only when fenced.
330
+ re.compile(r"(?:^(?:[ ]{4}|\t).*(?:\n|\Z))+", re.M),
331
+ )
332
+
333
+ # Zero-width and bidirectional formatting characters: invisible to a reader,
334
+ # but they broke `\s`-based word boundaries, so "must no<ZWSP>t" no longer
335
+ # matched the prohibition pattern and was recorded as a requirement instead.
336
+ _INVISIBLE = re.compile(
337
+ r"[\u00ad\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff]")
338
+
339
+
340
+ def _without_invisibles(text: str) -> tuple[str, list[int]]:
341
+ """Text with zero-width characters removed, plus a map back to the source.
342
+
343
+ `offsets[i]` is the index in `text` of character `i` of the returned
344
+ string, with a final entry for the end, so a match on the stripped copy
345
+ can be sliced out of the original.
346
+ """
347
+ kept: list[str] = []
348
+ offsets: list[int] = []
349
+ for index, char in enumerate(text):
350
+ if not _INVISIBLE.match(char):
351
+ kept.append(char)
352
+ offsets.append(index)
353
+ offsets.append(len(text))
354
+ return "".join(kept), offsets
355
+
356
+ # Markdown that decorates a line rather than forming part of the statement.
357
+ _LEADING_MARKUP = re.compile(r"^(?:[-*+]\s+|\d+[.)]\s+|#{1,6}\s+|\|\s*)+")
358
+
359
+
360
+ def _prose_only(text: str) -> str:
361
+ """Replace non-prose regions with hard boundaries, preserving offsets.
362
+
363
+ Masking to spaces was not a barrier: the gap between a cue word and its
364
+ clause could walk straight across the blanked region, stitching "The
365
+ exporter must" to "stream rows to the client" over a code fence and
366
+ recording a sentence nobody wrote. Masking to newlines makes the region a
367
+ run of blank lines, which every clause rule already treats as an end.
368
+ """
369
+ masked = list(text)
370
+ for pattern in _NON_PROSE:
371
+ for match in pattern.finditer(text):
372
+ for index in range(match.start(), match.end()):
373
+ masked[index] = "\n"
374
+ return "".join(masked)
375
+
376
+
231
377
  def _clean(s: str) -> str:
232
- return re.sub(r"\s+", " ", s).strip().rstrip(".;,:")
378
+ collapsed = re.sub(r"\s+", " ", s).strip()
379
+ return _LEADING_MARKUP.sub("", collapsed).strip().rstrip(".;,:| ")
233
380
 
234
381
 
235
382
  def _plausible(statement: str) -> bool:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: causal-continuity-engine
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Causal Continuity Engine - continuity, causal invalidation, and proof for long-running coding agents
5
5
  Author: Thomas Albrecht
6
6
  License-Expression: Apache-2.0
@@ -88,6 +88,8 @@ docs/TESTING.md
88
88
  docs/adr/ADR-INDEX.md
89
89
  docs/assets/demo.svg
90
90
  docs/assets/social-preview.png
91
+ examples/backfill_github.py
92
+ examples/quickstart.py
91
93
  justfile
92
94
  pyproject.toml
93
95
  requirements-dev.in
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ """Build a CCE project from a repository's real history, without a webhook.
3
+
4
+ GITHUB_TOKEN=$(gh auth token) python examples/backfill_github.py owner/repo
5
+
6
+ Webhook delivery is the live ingest path, but every event type the engine
7
+ understands is also retrievable from the REST API after the fact. This script
8
+ reads issues and issue comments through the public API, re-shapes them into the
9
+ webhook envelopes `ingest_github` already accepts, and feeds them in — so a
10
+ project can be populated from months of existing history in one pass, on a
11
+ laptop, with no App, tunnel, or public endpoint.
12
+
13
+ It then prints what the engine extracted and a Resume Packet built from that
14
+ real history. That packet is the thing worth reading: it is the product's
15
+ actual output on real data rather than on fixtures.
16
+
17
+ Standard library plus this package only. Read-only against GitHub; the project
18
+ is written to a directory you name (default: a temporary one).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import sys
27
+ import tempfile
28
+ import urllib.error
29
+ import urllib.request
30
+ from pathlib import Path
31
+
32
+ ROOT = Path(__file__).resolve().parent.parent
33
+ if not (ROOT / "causal_continuity_engine").is_dir(): # pragma: no cover
34
+ raise SystemExit("run this script from a checkout of the repository")
35
+ sys.path.append(str(ROOT))
36
+
37
+ from causal_continuity_engine.engine import Engine # noqa: E402
38
+
39
+ API = "https://api.github.com"
40
+ PAGE_SIZE = 100
41
+
42
+
43
+ def _get(path: str, token: str | None) -> object:
44
+ request = urllib.request.Request(
45
+ f"{API}{path}",
46
+ headers={
47
+ "Accept": "application/vnd.github+json",
48
+ "User-Agent": "cce-backfill-example",
49
+ **({"Authorization": f"Bearer {token}"} if token else {}),
50
+ })
51
+ try:
52
+ with urllib.request.urlopen(request, timeout=30) as response:
53
+ return json.loads(response.read().decode("utf-8"))
54
+ except urllib.error.HTTPError as exc:
55
+ detail = exc.read().decode("utf-8", "replace")[:200]
56
+ raise SystemExit(
57
+ f"GitHub returned {exc.code} for {path}: {detail}") from None
58
+ except urllib.error.URLError as exc:
59
+ raise SystemExit(f"cannot reach GitHub: {exc.reason}") from None
60
+
61
+
62
+ def _paged(path: str, token: str | None, *, pages: int) -> list[dict]:
63
+ collected: list[dict] = []
64
+ for page in range(1, pages + 1):
65
+ joiner = "&" if "?" in path else "?"
66
+ batch = _get(
67
+ f"{path}{joiner}per_page={PAGE_SIZE}&page={page}", token)
68
+ if not isinstance(batch, list) or not batch:
69
+ break
70
+ collected.extend(item for item in batch if isinstance(item, dict))
71
+ if len(batch) < PAGE_SIZE:
72
+ break
73
+ return collected
74
+
75
+
76
+ def _repository(meta: dict) -> dict:
77
+ return {"id": meta["id"], "full_name": meta["full_name"]}
78
+
79
+
80
+ def _issue_event(issue: dict, meta: dict) -> dict:
81
+ """Reshape a REST issue into the `issues` webhook envelope."""
82
+ return {
83
+ "action": "opened",
84
+ "repository": _repository(meta),
85
+ "issue": {
86
+ "number": issue["number"],
87
+ "title": issue.get("title") or "",
88
+ "state": issue.get("state") or "open",
89
+ "body": issue.get("body") or "",
90
+ "author_association": issue.get("author_association") or "NONE",
91
+ "created_at": issue["created_at"],
92
+ "updated_at": issue.get("updated_at") or issue["created_at"],
93
+ "labels": [{"name": label.get("name", "")}
94
+ for label in issue.get("labels", [])
95
+ if isinstance(label, dict)],
96
+ },
97
+ }
98
+
99
+
100
+ def _comment_event(comment: dict, issue_number: int, meta: dict) -> dict:
101
+ """Reshape a REST issue comment into the `issue_comment` envelope."""
102
+ return {
103
+ "action": "created",
104
+ "repository": _repository(meta),
105
+ "issue": {"number": issue_number},
106
+ "comment": {
107
+ "id": comment["id"],
108
+ "body": comment.get("body") or "",
109
+ "author_association": comment.get("author_association") or "NONE",
110
+ "created_at": comment["created_at"],
111
+ "updated_at": comment.get("updated_at") or comment["created_at"],
112
+ },
113
+ }
114
+
115
+
116
+ def _frontier_event(head_sha: str, meta: dict) -> dict:
117
+ """A tracked-ref event, so evidence has an observed commit to attach to."""
118
+ return {
119
+ "ref": f"refs/heads/{meta.get('default_branch', 'main')}",
120
+ "before": "0" * 40,
121
+ "after": head_sha,
122
+ "created": True,
123
+ "deleted": False,
124
+ "forced": False,
125
+ "commits": [],
126
+ "repository": _repository(meta),
127
+ }
128
+
129
+
130
+ def main(argv: list[str] | None = None) -> int:
131
+ parser = argparse.ArgumentParser(description=__doc__)
132
+ parser.add_argument("repository", help="owner/repo")
133
+ parser.add_argument(
134
+ "--dir", default=None,
135
+ help="project directory (default: a temporary one, discarded)")
136
+ parser.add_argument(
137
+ "--pages", type=int, default=2,
138
+ help=f"pages of {PAGE_SIZE} issues to read (default: 2)")
139
+ parser.add_argument(
140
+ "--token-env", default="GITHUB_TOKEN",
141
+ help="environment variable holding a token (default: GITHUB_TOKEN)")
142
+ args = parser.parse_args(argv)
143
+
144
+ if "/" not in args.repository:
145
+ raise SystemExit("repository must be owner/repo")
146
+ token = os.environ.get(args.token_env)
147
+ if not token:
148
+ print(f"no {args.token_env} set — using unauthenticated requests,"
149
+ " which are rate limited to 60/hour", file=sys.stderr)
150
+
151
+ print(f"reading {args.repository} ...")
152
+ meta = _get(f"/repos/{args.repository}", token)
153
+ if not isinstance(meta, dict):
154
+ raise SystemExit("unexpected repository response")
155
+ head = _get(
156
+ f"/repos/{args.repository}/commits/{meta['default_branch']}", token)
157
+ head_sha = head["sha"] if isinstance(head, dict) else "0" * 40
158
+
159
+ issues = _paged(
160
+ f"/repos/{args.repository}/issues?state=all", token, pages=args.pages)
161
+ # The issues endpoint returns pull requests too; they arrive again through
162
+ # their own event type, so keep this pass to genuine issues.
163
+ issues = [issue for issue in issues if "pull_request" not in issue]
164
+ comments = _paged(
165
+ f"/repos/{args.repository}/issues/comments", token, pages=args.pages)
166
+ print(f" {len(issues)} issues, {len(comments)} comments,"
167
+ f" head {head_sha[:8]}")
168
+
169
+ holder = args.dir or tempfile.mkdtemp(prefix="cce-backfill-")
170
+ workdir = Path(holder)
171
+ workdir.mkdir(parents=True, exist_ok=True)
172
+ engine = Engine(workdir / "cce.db", tenant_id="ten_backfill",
173
+ workdir=workdir)
174
+ try:
175
+ project = engine.create_project(
176
+ meta["name"], project_id="prj_backfill",
177
+ repository=meta["full_name"], repository_id=meta["id"])
178
+ project_id = project["project_id"] if isinstance(project, dict) \
179
+ else "prj_backfill"
180
+
181
+ events: list[tuple[str, str, dict]] = [
182
+ ("push", f"backfill-frontier-{head_sha[:8]}",
183
+ _frontier_event(head_sha, meta))]
184
+ for issue in issues:
185
+ events.append((
186
+ "issues", f"backfill-issue-{issue['number']}",
187
+ _issue_event(issue, meta)))
188
+ for comment in comments:
189
+ number = int(
190
+ str(comment.get("issue_url", "")).rsplit("/", 1)[-1] or 0)
191
+ if number:
192
+ events.append((
193
+ "issue_comment", f"backfill-comment-{comment['id']}",
194
+ _comment_event(comment, number, meta)))
195
+
196
+ nodes = invalidations = conflicts = 0
197
+ for event_name, delivery_id, payload in events:
198
+ report = engine.ingest_github(
199
+ project_id, event_name, delivery_id, payload)
200
+ if report:
201
+ nodes += len(report.get("nodes", []))
202
+ invalidations += len(report.get("invalidations", []))
203
+ conflicts += len(report.get("conflicts", []))
204
+ print(f" ingested {len(events)} events -> {nodes} node(s),"
205
+ f" {invalidations} invalidation(s), {conflicts} conflict(s)")
206
+
207
+ print("\n== What the extractor typed as control state")
208
+ for entity in ("assumption", "constraint", "requirement", "decision"):
209
+ found = engine.graph.current(
210
+ project_id, entity, tenant_id=engine.tenant_id)
211
+ for node in found[:5]:
212
+ statement = (node.get("data", {}).get("statement")
213
+ or node.get("data", {}).get("title") or "")
214
+ print(f" [{entity}/{node.get('status')}] {statement[:96]}")
215
+ if found:
216
+ print(f" ... {len(found)} {entity}(s) total")
217
+
218
+ print("\n== Resume Packet from real history")
219
+ print(engine.resume_packet(
220
+ project_id, token_budget=1500, fmt="markdown"))
221
+
222
+ print(f"\nproject kept at {workdir}" if args.dir else
223
+ f"\ntemporary project at {workdir} (delete when done)")
224
+ finally:
225
+ engine.close()
226
+ return 0
227
+
228
+
229
+ if __name__ == "__main__":
230
+ raise SystemExit(main())