causal-continuity-engine 0.1.0__py3-none-any.whl

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 (124) hide show
  1. causal_continuity_engine/__init__.py +21 -0
  2. causal_continuity_engine/api.py +1264 -0
  3. causal_continuity_engine/capabilities.py +548 -0
  4. causal_continuity_engine/capsule.py +996 -0
  5. causal_continuity_engine/cli.py +1338 -0
  6. causal_continuity_engine/core.py +468 -0
  7. causal_continuity_engine/engine.py +4354 -0
  8. causal_continuity_engine/evidence.py +971 -0
  9. causal_continuity_engine/extraction.py +263 -0
  10. causal_continuity_engine/github.py +657 -0
  11. causal_continuity_engine/graph.py +809 -0
  12. causal_continuity_engine/invalidation.py +1040 -0
  13. causal_continuity_engine/lamport.py +306 -0
  14. causal_continuity_engine/learning.py +466 -0
  15. causal_continuity_engine/memory.py +610 -0
  16. causal_continuity_engine/ontology.py +108 -0
  17. causal_continuity_engine/partial.py +309 -0
  18. causal_continuity_engine/policy.py +940 -0
  19. causal_continuity_engine/proof.py +920 -0
  20. causal_continuity_engine/py.typed +1 -0
  21. causal_continuity_engine/redaction.py +94 -0
  22. causal_continuity_engine/resume.py +653 -0
  23. causal_continuity_engine/store.py +1174 -0
  24. causal_continuity_engine/verifiers.py +1006 -0
  25. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/SPEC.md +515 -0
  26. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/__init__.py +0 -0
  27. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/continuitybench/__init__.py +0 -0
  28. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/continuitybench/run.py +146 -0
  29. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/benchmarks/continuitybench/scenarios.py +544 -0
  30. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.anchor.v1.json +62 -0
  31. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.capsule.v1.json +112 -0
  32. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.continuity-receipt.v1.json +315 -0
  33. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.event.v1.json +73 -0
  34. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.proof-predicate.v1.json +35 -0
  35. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.proof.v1.json +242 -0
  36. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.recovery.v1.json +109 -0
  37. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/schemas/cce.resume.v1.json +332 -0
  38. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/__init__.py +1 -0
  39. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/schema_validation.py +61 -0
  40. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_api_contract.py +1051 -0
  41. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_cli.py +664 -0
  42. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_conformance.py +758 -0
  43. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_distribution_environment.py +26 -0
  44. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_engine_e2e.py +286 -0
  45. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_github_redaction.py +193 -0
  46. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_instrument_validation.py +535 -0
  47. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_invalidation_resume.py +205 -0
  48. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_learning_partial_api.py +380 -0
  49. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_memory_extraction.py +151 -0
  50. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions.py +406 -0
  51. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round10_release.py +842 -0
  52. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round11_deep_integrity.py +831 -0
  53. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round12_release_blockers.py +206 -0
  54. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round13_jcs.py +212 -0
  55. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round14_state_integrity.py +1056 -0
  56. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round15_capsule_invalidation_github.py +251 -0
  57. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round15_public_boundaries.py +561 -0
  58. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round15_storage_boundaries.py +707 -0
  59. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round16_cli_api_core_capabilities.py +285 -0
  60. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round2.py +388 -0
  61. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round3.py +490 -0
  62. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round4.py +557 -0
  63. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round5.py +583 -0
  64. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round6.py +559 -0
  65. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round7.py +579 -0
  66. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_boundaries.py +558 -0
  67. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_engine.py +843 -0
  68. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_portability.py +2250 -0
  69. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round8_state.py +705 -0
  70. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round9_integrity.py +885 -0
  71. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_regressions_round9_scope_freshness.py +179 -0
  72. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_release_boundary_validation.py +676 -0
  73. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_release_controls.py +840 -0
  74. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_sqlite_lifecycle.py +52 -0
  75. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_store_graph.py +172 -0
  76. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/tests/test_trust.py +398 -0
  77. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/action_intent_wrong_type.json +170 -0
  78. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/actor_wrong_type.json +170 -0
  79. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/created_at_invalid_calendar_date.json +172 -0
  80. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/created_at_noncanonical_offset.json +172 -0
  81. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_authoritative_failure.json +114 -0
  82. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_authoritative_pass.json +107 -0
  83. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_no_results.json +99 -0
  84. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/empty_required_self_asserted_pass.json +105 -0
  85. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/generate.py +498 -0
  86. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/incomplete_failed_check.json +168 -0
  87. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/index.json +240 -0
  88. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/input_item_wrong_type.json +163 -0
  89. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/invalid_result_value.json +170 -0
  90. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/lamport_fingerprint_spoof.json +1458 -0
  91. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/lamport_no_registry.json +1454 -0
  92. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/lamport_truncated_key.json +1454 -0
  93. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/missing_required_field.json +157 -0
  94. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/non_binary64_integer.json +174 -0
  95. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/policy_decision_wrong_type.json +146 -0
  96. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/retry_cannot_launder_a_failure.json +197 -0
  97. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/self_asserted_cannot_satisfy.json +173 -0
  98. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/self_assertion_cannot_poison_authoritative_pass.json +194 -0
  99. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/status_contradicts_contents.json +173 -0
  100. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/summary_contradicts_contents.json +170 -0
  101. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/tamper_statement.json +172 -0
  102. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/tamper_statement_resealed.json +172 -0
  103. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/task_id_under_unrelated_field.json +172 -0
  104. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unbound_task.json +165 -0
  105. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_action_intent_field.json +173 -0
  106. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_algorithm.json +172 -0
  107. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_schema_version.json +170 -0
  108. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unknown_top_level_field.json +175 -0
  109. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unverified_forged_body_no_key.json +170 -0
  110. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/unverified_hmac_no_key.json +170 -0
  111. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_hmac.json +172 -0
  112. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_hmac_jcs_edges.json +186 -0
  113. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_hmac_unicode.json +172 -0
  114. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/valid_lamport.json +1458 -0
  115. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/vectors/wrong_project.json +173 -0
  116. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/verifiers/__init__.py +0 -0
  117. causal_continuity_engine-0.1.0.data/data/share/causal-continuity-engine/audit/verifiers/verify_proof.py +1109 -0
  118. causal_continuity_engine-0.1.0.dist-info/METADATA +720 -0
  119. causal_continuity_engine-0.1.0.dist-info/RECORD +124 -0
  120. causal_continuity_engine-0.1.0.dist-info/WHEEL +5 -0
  121. causal_continuity_engine-0.1.0.dist-info/entry_points.txt +2 -0
  122. causal_continuity_engine-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  123. causal_continuity_engine-0.1.0.dist-info/licenses/NOTICE +6 -0
  124. causal_continuity_engine-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,535 @@
1
+ """Instrument validation for the completion gate (ADR-056).
2
+
3
+ A verifier that only ever reports PASS is not evidence of anything. This file
4
+ therefore exercises negative controls at the completion boundary.
5
+
6
+ CCE's completion gate has accumulated twenty independent rejection paths
7
+ across repeated adversarial review rounds. Every one is exercised by a test that
8
+ proves it CAN fire. Nothing until now proved that each fires **for the
9
+ reason it exists** and that the others stay quiet — which matters here more
10
+ than most places, because three of this project's worst defects were
11
+ controls that existed, were tested, and did not run in the path that
12
+ decided (ADR-014, ADR-033/044, ADR-043).
13
+
14
+ So this harness works the other way round from the rest of the suite. It
15
+ builds one known-good completion, plants a specific defect, and asserts:
16
+
17
+ 1. the gate that SHOULD catch it does, identified by name; and
18
+ 2. the baseline — unmodified — completes cleanly, so a gate that fires on
19
+ everything is exposed rather than mistaken for rigour.
20
+
21
+ A defect caught by the WRONG gate is reported as a mismatch rather than a
22
+ pass. That is not pedantry: it means the model of which control covers what
23
+ is wrong, and a control believed to cover something it does not is exactly
24
+ how a gap survives a review round.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import shlex
31
+ import sys
32
+
33
+ import pytest
34
+
35
+ from causal_continuity_engine.core import digest_obj
36
+ from causal_continuity_engine.engine import Engine
37
+
38
+ PRJ = "prj_instrument"
39
+ REPOSITORY_ID = 18018
40
+ PRECHECK_SPENDER = "tsk_111111111111111111111111"
41
+ RACING_SPENDER = "tsk_222222222222222222222222"
42
+
43
+
44
+ def _python_command(source: str) -> str:
45
+ return shlex.join([sys.executable, "-c", source])
46
+
47
+
48
+ PASS_COMMAND = _python_command("raise SystemExit(0)")
49
+ FAIL_COMMAND = _python_command("raise SystemExit(1)")
50
+
51
+ #: Each gate's identifying phrase, taken from the message it raises. Keyed by
52
+ #: the ADR that introduced it so a renamed message fails loudly here.
53
+ GATES = {
54
+ "wrong_target": "is not a task in the requested project", # ADR-068
55
+ "open_invalidation": "unresolved invalidation control state", # CI-005
56
+ "already_complete": "is already verified", # ADR-074
57
+ "quarantined": "is quarantined", # ADR-050
58
+ "not_completable": "is not completable", # ADR-074
59
+ "no_proof": "none was provided", # PA-005
60
+ "malformed": "malformed proof envelope", # ADR-068
61
+ "tampered": "invalid or tampered", # PA-002
62
+ "not_verified": "not 'verified'", # PA-005
63
+ "wrong_project": "was issued for", # ADR-018
64
+ "wrong_intent": "proof intent is", # ADR-068
65
+ "unbound_task": "does not name task", # ADR-018
66
+ "already_spent": f"already used to complete {PRECHECK_SPENDER}",
67
+ "claim_race": f"already used to complete {RACING_SPENDER}",
68
+ "not_current": "no longer describes", # ADR-043
69
+ "no_verifiers": "no required verifiers", # ADR-045
70
+ "policy_moved": "policy now requires", # ADR-054
71
+ "policy_denied": "policy decision is not 'allow'", # ADR-068
72
+ "low_grade": "evidence grade", # ADR-027
73
+ "state_changed": "task state changed", # ADR-069
74
+ }
75
+
76
+
77
+ def _reads_deliverable() -> str:
78
+ return _python_command(
79
+ "import pathlib,sys; "
80
+ "sys.exit(0 if pathlib.Path('deliverable.py').read_text() else 1)")
81
+
82
+
83
+ def _good(tmp_path, **overrides):
84
+ """A completion that should be accepted, and the pieces to corrupt."""
85
+ (tmp_path / "deliverable.py").write_text("def f(): return 1\n")
86
+ cfg = {
87
+ "max_autonomy_level": 2,
88
+ "require_proof_for": ["task_complete"],
89
+ "min_evidence_grade": "C",
90
+ "required_verifiers": [{
91
+ "name": "unit-tests", "command": _reads_deliverable(),
92
+ "expect_fail_command": FAIL_COMMAND,
93
+ "artifacts": ["deliverable.py"]}],
94
+ **overrides,
95
+ }
96
+ engine = Engine(workdir=tmp_path)
97
+ engine.create_project(
98
+ "p", project_id=PRJ, repository_id=REPOSITORY_ID, config=cfg)
99
+ engine.policy.grant(project_id=PRJ, level=2, granted_by="lead")
100
+ engine.policy.set_project_config(PRJ, cfg)
101
+ task = engine.graph.put_node(
102
+ entity_type="task", tenant_id=engine.tenant_id, project_id=PRJ,
103
+ data={"title": "ship the exporter"}, status="open")
104
+ proof = engine.attest_action(
105
+ PRJ, intent_type="task_complete", intent_statement="exporter done",
106
+ actor={"agent": "test"}, action_type="run_verifier",
107
+ continuity={"task_ids": [task.id]})
108
+ return engine, task, proof, cfg
109
+
110
+
111
+ #: Exception types a REJECTION may use. Narrowing this to PermissionError
112
+ #: alone would make a gate that raised anything else look like a harness
113
+ #: error rather than a control firing, and it would go uncounted below —
114
+ #: an instrument blind to the very thing it exists to measure (ADR-067).
115
+ REJECTION_TYPES = (PermissionError, ValueError)
116
+
117
+
118
+ def _attempt(engine, task_id, proof):
119
+ """Try the completion. Returns the gate name that fired, or None."""
120
+ try:
121
+ engine.complete_task(PRJ, task_id, proof=proof)
122
+ return None
123
+ except REJECTION_TYPES as exc:
124
+ message = str(exc)
125
+ fired = [name for name, phrase in GATES.items() if phrase in message]
126
+ if not fired:
127
+ pytest.fail(f"a {type(exc).__name__} rejection fired with an "
128
+ f"unrecognised message: {message}")
129
+ if len(fired) > 1:
130
+ pytest.fail(f"ambiguous gate message matches {fired}: {message}")
131
+ return fired[0]
132
+
133
+
134
+ def _reseal(proof: dict) -> dict:
135
+ """Recompute the digest so the tamper is internally consistent.
136
+
137
+ Under HMAC an outsider cannot re-sign, so this isolates the SIGNATURE
138
+ check from the digest check: a resealed body still fails on the signature
139
+ alone, which is the property being asserted.
140
+ """
141
+ body = {k: v for k, v in proof.items()
142
+ if k not in ("signature", "proof_digest")}
143
+ proof["proof_digest"] = digest_obj(body)
144
+ return proof
145
+
146
+
147
+ def _trusted_reseal(engine: Engine, proof: dict) -> dict:
148
+ """Issue a modified envelope with the engine's actual trusted key.
149
+
150
+ This is used only for semantic gates whose input must remain both
151
+ internally consistent and authentic; otherwise the tamper gate would
152
+ correctly intercept the fixture first.
153
+ """
154
+ issued = _reseal(json.loads(json.dumps(proof)))
155
+ issued["signature"] = engine.signer.sign(issued)
156
+ return issued
157
+
158
+
159
+ # ─────────────────────────── the mutation table ───────────────────────────
160
+ # Each entry plants one defect and names the single gate that must catch it.
161
+
162
+ class TestBaselineCompletesCleanly:
163
+ def test_unmodified_completion_is_accepted(self, tmp_path):
164
+ """If this fails, every result below is meaningless."""
165
+ engine, task, proof, _ = _good(tmp_path)
166
+ assert _attempt(engine, task.id, proof) is None, \
167
+ "the known-good case is rejected: no gate result below can be trusted"
168
+ engine.close()
169
+
170
+ def test_the_good_proof_is_actually_verified(self, tmp_path):
171
+ engine, task, proof, _ = _good(tmp_path)
172
+ assert proof["status"] == "verified"
173
+ assert proof["evidence_context"]["mutation"]["bound"] is True
174
+ engine.close()
175
+
176
+
177
+ class TestEachDefectTripsItsOwnGate:
178
+ def test_target_is_not_a_task_in_the_requested_project(self, tmp_path):
179
+ engine, task, proof, _ = _good(tmp_path)
180
+ assumption = engine.graph.put_node(
181
+ entity_type="assumption", tenant_id=engine.tenant_id,
182
+ project_id=PRJ, data={"statement": "not executable work"},
183
+ status="active")
184
+ assert _attempt(engine, assumption.id, proof) == "wrong_target"
185
+ engine.close()
186
+
187
+ def test_a_different_claim_cannot_overwrite_verified_completion(self, tmp_path):
188
+ engine, task, proof, _ = _good(tmp_path)
189
+ assert _attempt(engine, task.id, proof) is None
190
+ version = engine.graph.get(task.id)["version"]
191
+ # An exact retry is a no-op, so transport retries remain safe.
192
+ assert engine.complete_task(PRJ, task.id, proof=proof)["version"] == version
193
+ replacement = engine.attest_action(
194
+ PRJ, intent_type="task_complete", intent_statement="replacement",
195
+ actor={"agent": "test"}, action_type="run_verifier",
196
+ continuity={"task_ids": [task.id]})
197
+ assert _attempt(engine, task.id, replacement) == "already_complete"
198
+ completed = engine.graph.get(task.id)
199
+ assert completed["version"] == version
200
+ assert completed["data"]["completion_evidence"] == proof["proof_id"]
201
+ engine.close()
202
+
203
+ def test_quarantined_task(self, tmp_path):
204
+ engine, task, proof, _ = _good(tmp_path)
205
+ engine.partial.quarantine(task.id, actor="cce", reason="ambiguous output")
206
+ assert _attempt(engine, task.id, proof) == "quarantined"
207
+ engine.close()
208
+
209
+ def test_blocked_task_cannot_bypass_state_gate_when_proof_is_optional(
210
+ self, tmp_path):
211
+ engine, task, proof, _ = _good(tmp_path, require_proof_for=[])
212
+ engine.graph.put_node(
213
+ entity_type="task", tenant_id=engine.tenant_id,
214
+ project_id=PRJ, node_id=task.id, status="blocked", data={})
215
+ assert _attempt(engine, task.id, None) == "not_completable"
216
+ assert engine.graph.get(task.id)["status"] == "blocked"
217
+ engine.close()
218
+
219
+ def test_no_proof_at_all(self, tmp_path):
220
+ engine, task, proof, _ = _good(tmp_path)
221
+ assert _attempt(engine, task.id, None) == "no_proof"
222
+ engine.close()
223
+
224
+ def test_required_proof_field_is_missing(self, tmp_path):
225
+ engine, task, proof, _ = _good(tmp_path)
226
+ malformed = json.loads(json.dumps(proof))
227
+ malformed.pop("proof_id")
228
+ assert _attempt(engine, task.id, malformed) == "malformed"
229
+ engine.close()
230
+
231
+ def test_body_tampered_without_resealing(self, tmp_path):
232
+ engine, task, proof, _ = _good(tmp_path)
233
+ forged = json.loads(json.dumps(proof))
234
+ forged["action_intent"]["statement"] = "something else entirely"
235
+ assert _attempt(engine, task.id, forged) == "tampered"
236
+ engine.close()
237
+
238
+ def test_body_tampered_and_resealed_still_fails_on_the_signature(self, tmp_path):
239
+ """The case that decides whether the signature check is decoration.
240
+
241
+ The digest is recomputed so it agrees with the altered body; only the
242
+ signature can catch it. This is the resealed-tamper negative control.
243
+ """
244
+ engine, task, proof, _ = _good(tmp_path)
245
+ forged = _reseal(json.loads(json.dumps(proof)))
246
+ forged["action_intent"]["statement"] = "something else entirely"
247
+ forged = _reseal(forged)
248
+ assert _attempt(engine, task.id, forged) == "tampered"
249
+ engine.close()
250
+
251
+ def test_status_forged_to_verified(self, tmp_path):
252
+ engine, task, proof, cfg = _good(
253
+ tmp_path, required_verifiers=[{
254
+ "name": "unit-tests", "command": FAIL_COMMAND}])
255
+ assert proof["status"] == "failed"
256
+ assert _attempt(engine, task.id, proof) == "not_verified"
257
+ engine.close()
258
+
259
+ def test_proof_from_another_project(self, tmp_path):
260
+ """A GENUINELY signed proof from elsewhere, not a JSON edit.
261
+
262
+ Editing project_id in the body breaks the digest, so a naive swap is
263
+ caught by the tamper gate and never reaches the scope check — which
264
+ is why this mutation has to mint a real foreign proof to test what it
265
+ claims to test.
266
+ """
267
+ engine, task, proof, cfg = _good(tmp_path)
268
+ other = Engine(workdir=tmp_path, signer=engine.signer) # same key
269
+ other.create_project("o", project_id="prj_elsewhere", config=cfg)
270
+ other.policy.grant(project_id="prj_elsewhere", level=2, granted_by="l")
271
+ other.policy.set_project_config("prj_elsewhere", cfg)
272
+ foreign_task = other.graph.put_node(
273
+ entity_type="task", tenant_id=other.tenant_id,
274
+ project_id="prj_elsewhere", data={"title": "their task"},
275
+ status="open")
276
+ foreign = other.attest_action(
277
+ "prj_elsewhere", intent_type="task_complete",
278
+ intent_statement="theirs", actor={"agent": "test"},
279
+ action_type="run_verifier",
280
+ continuity={"task_ids": [foreign_task.id]})
281
+ assert _attempt(engine, task.id, foreign) == "wrong_project"
282
+ other.close()
283
+ engine.close()
284
+
285
+ def test_proof_for_another_intent(self, tmp_path):
286
+ engine, task, proof, _ = _good(tmp_path)
287
+ wrong_intent = engine.attest_action(
288
+ PRJ, intent_type="pr_ready", intent_statement="ready for review",
289
+ actor={"agent": "test"}, action_type="run_verifier",
290
+ continuity={"task_ids": [task.id]})
291
+ assert wrong_intent["status"] == "verified"
292
+ assert _attempt(engine, task.id, wrong_intent) == "wrong_intent"
293
+ engine.close()
294
+
295
+ def test_proof_that_names_no_task(self, tmp_path):
296
+ engine, task, proof, _ = _good(tmp_path)
297
+ unbound = engine.attest_action(
298
+ PRJ, intent_type="task_complete", intent_statement="unbound",
299
+ actor={"agent": "test"}, action_type="run_verifier")
300
+ assert _attempt(engine, task.id, unbound) == "unbound_task"
301
+ engine.close()
302
+
303
+ def test_proof_replayed_onto_a_second_task(self, tmp_path):
304
+ engine, task, proof, _ = _good(tmp_path)
305
+ second = engine.graph.put_node(
306
+ entity_type="task", tenant_id=engine.tenant_id, project_id=PRJ,
307
+ node_id=PRECHECK_SPENDER, data={"title": "another"},
308
+ status="open")
309
+ both = engine.attest_action(
310
+ PRJ, intent_type="task_complete", intent_statement="both",
311
+ actor={"agent": "test"}, action_type="run_verifier",
312
+ continuity={"task_ids": [task.id, second.id]})
313
+ assert _attempt(engine, second.id, both) is None
314
+ assert _attempt(engine, task.id, both) == "already_spent"
315
+ engine.close()
316
+
317
+ def test_proof_is_claimed_by_a_peer_after_the_precheck(
318
+ self, tmp_path, monkeypatch):
319
+ """The final atomic claim must reject a precheck/claim race."""
320
+ engine, task, proof, _ = _good(tmp_path)
321
+
322
+ def peer_won_claim(project_id, proof_id, task_id):
323
+ return RACING_SPENDER
324
+
325
+ monkeypatch.setattr(engine, "_claim_proof", peer_won_claim)
326
+ assert _attempt(engine, task.id, proof) == "claim_race"
327
+ assert engine.graph.get(task.id)["status"] == "open"
328
+ engine.close()
329
+
330
+ def test_deliverable_changed_after_attestation(self, tmp_path):
331
+ engine, task, proof, _ = _good(tmp_path)
332
+ (tmp_path / "deliverable.py").write_text("def f(): return 999\n")
333
+ assert _attempt(engine, task.id, proof) == "not_current"
334
+ engine.close()
335
+
336
+ def test_invalidation_fired_after_attestation(self, tmp_path):
337
+ engine, task, proof, _ = _good(tmp_path)
338
+ asm = engine.graph.put_node(
339
+ entity_type="assumption", tenant_id=engine.tenant_id,
340
+ project_id=PRJ, data={"statement": "schema is stable"},
341
+ status="active", criticality="critical")
342
+ engine.invalidation.fire(
343
+ tenant_id=engine.tenant_id, project_id=PRJ, target_node_id=asm.id,
344
+ trigger_type="contradictory_evidence", trigger_confidence=0.95)
345
+ assert _attempt(engine, task.id, proof) == "open_invalidation"
346
+ engine.close()
347
+
348
+ def test_policy_declares_no_verifiers(self, tmp_path):
349
+ engine, task, proof, cfg = _good(tmp_path)
350
+ engine.policy.set_project_config(
351
+ PRJ, {**cfg, "required_verifiers": []})
352
+ assert _attempt(engine, task.id, proof) == "no_verifiers"
353
+ engine.close()
354
+
355
+ def test_policy_tightened_after_attestation(self, tmp_path):
356
+ engine, task, proof, cfg = _good(tmp_path)
357
+ engine.policy.set_project_config(PRJ, {
358
+ **cfg, "required_verifiers": cfg["required_verifiers"] + [
359
+ {"name": "security-scan", "command": PASS_COMMAND}]})
360
+ assert _attempt(engine, task.id, proof) == "policy_moved"
361
+ engine.close()
362
+
363
+ def test_authentic_proof_records_policy_denial(self, tmp_path):
364
+ engine, task, proof, _ = _good(tmp_path)
365
+ denied = json.loads(json.dumps(proof))
366
+ denied["policy_decision"] = {
367
+ "decision": "deny",
368
+ "reason": "trusted issuer recorded a denied action",
369
+ }
370
+ denied = _trusted_reseal(engine, denied)
371
+ assert _attempt(engine, task.id, denied) == "policy_denied"
372
+ engine.close()
373
+
374
+ def test_evidence_below_the_grade_floor(self, tmp_path):
375
+ """A check the claimant chose, rather than one the policy pinned."""
376
+ engine, task, proof, cfg = _good(
377
+ tmp_path, required_verifiers=["unit-tests"]) # bare name: unpinned
378
+ from causal_continuity_engine.verifiers import VerifierSpec
379
+ weak = engine.attest_action(
380
+ PRJ, intent_type="task_complete", intent_statement="weak",
381
+ actor={"agent": "test"}, action_type="run_verifier",
382
+ verifier_specs=[VerifierSpec(name="unit-tests",
383
+ command=PASS_COMMAND)],
384
+ continuity={"task_ids": [task.id]})
385
+ assert _attempt(engine, task.id, weak) == "low_grade"
386
+ engine.close()
387
+
388
+ def test_task_changes_during_validation(self, tmp_path, monkeypatch):
389
+ engine, task, proof, _ = _good(tmp_path)
390
+ original = engine.proof_currency
391
+
392
+ def plant_change(project_id, task_id, envelope):
393
+ result = original(project_id, task_id, envelope)
394
+ engine.graph.put_node(
395
+ entity_type="task", tenant_id=engine.tenant_id,
396
+ project_id=PRJ, node_id=task.id, status="quarantined",
397
+ data={"quarantine_reason": "arrived during validation"})
398
+ return result
399
+
400
+ monkeypatch.setattr(engine, "proof_currency", plant_change)
401
+ assert _attempt(engine, task.id, proof) == "state_changed"
402
+ # Rejection rolls the planted in-transaction mutation back too.
403
+ assert engine.graph.get(task.id)["status"] == "open"
404
+ engine.close()
405
+
406
+
407
+ class TestNoGateFiresSpuriously:
408
+ """The other half of instrument validation, which CCE had none of: a
409
+ control that fires on everything is not a control."""
410
+
411
+ @pytest.mark.parametrize("harmless", [
412
+ "add_an_unrelated_task",
413
+ "compose_a_resume_packet",
414
+ "run_an_evidence_probe",
415
+ "record_an_audit_entry",
416
+ "ingest_an_unrelated_event",
417
+ ])
418
+ def test_harmless_activity_does_not_block_completion(self, tmp_path, harmless):
419
+ engine, task, proof, _ = _good(tmp_path)
420
+ if harmless == "add_an_unrelated_task":
421
+ engine.graph.put_node(
422
+ entity_type="task", tenant_id=engine.tenant_id, project_id=PRJ,
423
+ data={"title": "unrelated"}, status="open")
424
+ elif harmless == "compose_a_resume_packet":
425
+ engine.resume_packet(PRJ)
426
+ elif harmless == "run_an_evidence_probe":
427
+ engine.probe_evidence(PRJ)
428
+ elif harmless == "record_an_audit_entry":
429
+ engine.store.audit(actor="someone", action="unrelated.thing")
430
+ elif harmless == "ingest_an_unrelated_event":
431
+ engine.ingest_github(PRJ, "issues", "unrelated-1", {
432
+ "action": "opened",
433
+ "issue": {"number": 99, "title": "T",
434
+ "body": "An unrelated observation.", "state": "open",
435
+ "labels": [], "author_association": "OWNER",
436
+ "created_at": "2026-07-31T10:00:00Z"},
437
+ "repository": {"id": REPOSITORY_ID, "full_name": "o/r"}})
438
+ assert _attempt(engine, task.id, proof) is None, \
439
+ f"{harmless} spuriously blocked an otherwise valid completion"
440
+ engine.close()
441
+
442
+ def test_a_non_critical_invalidation_elsewhere_does_not_block(self, tmp_path):
443
+ """Only invalidations that touch this task, or critical ones, count."""
444
+ engine, task, proof, _ = _good(tmp_path)
445
+ unrelated = engine.graph.put_node(
446
+ entity_type="assumption", tenant_id=engine.tenant_id,
447
+ project_id=PRJ, data={"statement": "the office wifi is fast"},
448
+ status="active", criticality="low")
449
+ engine.invalidation.fire(
450
+ tenant_id=engine.tenant_id, project_id=PRJ,
451
+ target_node_id=unrelated.id, trigger_type="dependency_drift",
452
+ trigger_confidence=0.95)
453
+ assert _attempt(engine, task.id, proof) is None, \
454
+ "an unrelated low-severity invalidation blocked completion"
455
+ engine.close()
456
+
457
+ def test_uncontested_atomic_proof_claim_does_not_block(
458
+ self, tmp_path, monkeypatch):
459
+ """Exercise the claim-race boundary with its harmless result."""
460
+ engine, task, proof, _ = _good(tmp_path)
461
+ original = engine._claim_proof
462
+ calls = []
463
+
464
+ def observe_claim(project_id, proof_id, task_id):
465
+ calls.append((project_id, proof_id, task_id))
466
+ return original(project_id, proof_id, task_id)
467
+
468
+ monkeypatch.setattr(engine, "_claim_proof", observe_claim)
469
+ assert _attempt(engine, task.id, proof) is None
470
+ assert calls == [(PRJ, proof["proof_id"], task.id)]
471
+ engine.close()
472
+
473
+
474
+ class TestGateCoverageIsComplete:
475
+ """If a new rejection path is added without a negative control, say so.
476
+
477
+ This class is the instrument for the instrument. Its own three defects —
478
+ a substring count that a comment could inflate, no check that a NAMED
479
+ gate is actually exercised, and a blind spot for any exception type but
480
+ one — were the kind that leave a harness reporting full coverage of
481
+ something it stopped covering (ADR-067).
482
+ """
483
+
484
+ @staticmethod
485
+ def _rejection_count() -> int:
486
+ """Rejections in complete_task, counted from the PARSED function.
487
+
488
+ Counting `source.count("raise PermissionError")` counted text: a
489
+ comment or a string literal mentioning the phrase inflated it, and a
490
+ gate raised through a helper did not appear at all. The AST counts
491
+ raise statements.
492
+ """
493
+ import ast
494
+ import inspect
495
+ import textwrap
496
+
497
+ from causal_continuity_engine import engine as engine_module
498
+ tree = ast.parse(textwrap.dedent(
499
+ inspect.getsource(engine_module.Engine._complete_task_locked)))
500
+ return sum(
501
+ 1 for node in ast.walk(tree)
502
+ if isinstance(node, ast.Raise)
503
+ and isinstance(node.exc, ast.Call)
504
+ and isinstance(node.exc.func, ast.Name)
505
+ and node.exc.func.id in {t.__name__ for t in REJECTION_TYPES})
506
+
507
+ def test_every_rejection_path_has_a_named_gate(self):
508
+ raises = self._rejection_count()
509
+ assert raises == len(GATES), (
510
+ f"complete_task has {raises} rejection paths but GATES names "
511
+ f"{len(GATES)}. Add the new gate here with a planted defect that "
512
+ f"trips it, or this harness silently stops covering it.")
513
+
514
+ def test_every_named_gate_has_a_planted_defect_that_trips_it(self):
515
+ """A name in GATES with no test behind it is coverage on paper.
516
+
517
+ The count above would still pass: GATES and the rejection paths would
518
+ agree while one of them was never exercised.
519
+ """
520
+ import inspect
521
+ source = inspect.getsource(TestEachDefectTripsItsOwnGate)
522
+ unexercised = sorted(name for name in GATES
523
+ if f'== "{name}"' not in source)
524
+ assert not unexercised, (
525
+ f"named but never planted: {unexercised}. Each gate needs a test "
526
+ f"that plants its defect and asserts THAT gate catches it.")
527
+
528
+ def test_the_counter_is_not_fooled_by_text(self):
529
+ """Validates the instrument's own instrument: the AST counter must
530
+ ignore a mention of the phrase that is not a raise."""
531
+ import ast
532
+ source = 'def f():\n x = "raise PermissionError" # raise ValueError\n'
533
+ tree = ast.parse(source)
534
+ assert not [n for n in ast.walk(tree) if isinstance(n, ast.Raise)], \
535
+ "the counter would credit a comment or string as a rejection path"