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.
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/verify_distributions.py +4 -1
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/CHANGELOG.md +74 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/CITATION.cff +1 -1
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/MANIFEST.in +1 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/PKG-INFO +1 -1
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/__init__.py +1 -1
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/extraction.py +167 -20
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/PKG-INFO +1 -1
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/SOURCES.txt +2 -0
- causal_continuity_engine-0.1.3/examples/backfill_github.py +230 -0
- causal_continuity_engine-0.1.3/examples/quickstart.py +199 -0
- causal_continuity_engine-0.1.3/tests/test_memory_extraction.py +493 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_portability.py +2 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round9_integrity.py +26 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_release_controls.py +2 -2
- causal_continuity_engine-0.1.2/tests/test_memory_extraction.py +0 -151
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.editorconfig +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.gitattributes +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.githooks/commit-msg +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.githooks/pre-commit +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/CODEOWNERS +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/CONTRIBUTING.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ISSUE_TEMPLATE/bug.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ISSUE_TEMPLATE/feature.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/SECURITY.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/SUPPORT.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/dependabot.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/pull_request_template.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/release.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ruleset.README.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/ruleset.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/bootstrap_tools.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/build_distributions.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_clean.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_release_metadata.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_release_tag.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/check_stdlib.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/prepare_release_tag.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/render_api_docs.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/run_gates.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/scripts/verify_public_schemas.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/tag-ruleset.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/ci.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/commit-signature-audit.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/dependency-review.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/no-ai-attribution.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/release.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.github/workflows/secret-scan.yml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.gitignore +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.gitleaks.toml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/.pre-commit-config.yaml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/AGENTS.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/LICENSE.txt +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/NOTICE +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/README.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/SPEC.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/__init__.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/continuitybench/__init__.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/continuitybench/run.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/benchmarks/continuitybench/scenarios.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/api.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/capabilities.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/capsule.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/cli.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/core.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/engine.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/evidence.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/github.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/graph.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/invalidation.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/lamport.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/learning.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/memory.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/ontology.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/partial.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/policy.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/proof.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/py.typed +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/redaction.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/resume.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/store.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine/verifiers.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/dependency_links.txt +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/entry_points.txt +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/requires.txt +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/causal_continuity_engine.egg-info/top_level.txt +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/API.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/CAPABILITIES.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/PUBLIC-FLIP.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/RELEASE.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/REQUIREMENTS.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/REQUIREMENTS_COVERAGE.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/RESEARCH-ROADMAP.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/TESTING.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/adr/ADR-INDEX.md +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/assets/demo.svg +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/docs/assets/social-preview.png +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/justfile +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/pyproject.toml +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/requirements-dev.in +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/requirements-dev.lock +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.anchor.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.capsule.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.continuity-receipt.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.event.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.proof-predicate.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.proof.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.recovery.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/schemas/cce.resume.v1.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/setup.cfg +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/__init__.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/schema_validation.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_api_contract.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_cli.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_conformance.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_distribution_environment.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_engine_e2e.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_github_redaction.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_instrument_validation.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_invalidation_resume.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_learning_partial_api.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round10_release.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round11_deep_integrity.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round12_release_blockers.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round13_jcs.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round14_state_integrity.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round15_capsule_invalidation_github.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round15_public_boundaries.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round15_storage_boundaries.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round16_cli_api_core_capabilities.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round2.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round3.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round4.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round5.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round6.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round7.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_boundaries.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_engine.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round8_state.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_regressions_round9_scope_freshness.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_release_boundary_validation.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_sqlite_lifecycle.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_store_graph.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/tests/test_trust.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/action_intent_wrong_type.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/actor_wrong_type.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/created_at_invalid_calendar_date.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/created_at_noncanonical_offset.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_authoritative_failure.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_authoritative_pass.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_no_results.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/empty_required_self_asserted_pass.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/generate.py +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/incomplete_failed_check.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/index.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/input_item_wrong_type.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/invalid_result_value.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/lamport_fingerprint_spoof.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/lamport_no_registry.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/lamport_truncated_key.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/missing_required_field.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/non_binary64_integer.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/policy_decision_wrong_type.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/retry_cannot_launder_a_failure.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/self_asserted_cannot_satisfy.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/self_assertion_cannot_poison_authoritative_pass.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/status_contradicts_contents.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/summary_contradicts_contents.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/tamper_statement.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/tamper_statement_resealed.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/task_id_under_unrelated_field.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unbound_task.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_action_intent_field.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_algorithm.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_schema_version.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unknown_top_level_field.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unverified_forged_body_no_key.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/unverified_hmac_no_key.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_hmac.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_hmac_jcs_edges.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_hmac_unicode.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/valid_lamport.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/vectors/wrong_project.json +0 -0
- {causal_continuity_engine-0.1.2 → causal_continuity_engine-0.1.3}/verifiers/__init__.py +0 -0
- {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 {
|
|
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
|
|
@@ -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
|
|
@@ -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.
|
|
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*[:\-])?)
|
|
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?)
|
|
60
|
-
|
|
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)
|
|
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>
|
|
65
|
-
r"
|
|
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>
|
|
68
|
-
|
|
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*
|
|
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
|
|
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(
|
|
153
|
-
statement = _clean(
|
|
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
|
-
|
|
197
|
-
|
|
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.
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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())
|