crprotocol 2.0.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 (153) hide show
  1. crp/__init__.py +126 -0
  2. crp/__main__.py +8 -0
  3. crp/_typing.py +27 -0
  4. crp/_version.py +5 -0
  5. crp/adapters.py +31 -0
  6. crp/advanced/__init__.py +40 -0
  7. crp/advanced/auto_ingest.py +400 -0
  8. crp/advanced/cqs.py +235 -0
  9. crp/advanced/cross_window.py +477 -0
  10. crp/advanced/curator.py +265 -0
  11. crp/advanced/feedback.py +146 -0
  12. crp/advanced/hierarchical.py +211 -0
  13. crp/advanced/meta_learning.py +401 -0
  14. crp/advanced/parallel.py +98 -0
  15. crp/advanced/review_cycle.py +329 -0
  16. crp/advanced/scale_mode.py +129 -0
  17. crp/advanced/source_grounding.py +207 -0
  18. crp/ckf/__init__.py +35 -0
  19. crp/ckf/community.py +377 -0
  20. crp/ckf/fabric.py +445 -0
  21. crp/ckf/gc.py +175 -0
  22. crp/ckf/graph_walk.py +87 -0
  23. crp/ckf/merge.py +133 -0
  24. crp/ckf/pattern_query.py +122 -0
  25. crp/ckf/pubsub.py +128 -0
  26. crp/ckf/semantic.py +207 -0
  27. crp/cli/__init__.py +7 -0
  28. crp/cli/main.py +329 -0
  29. crp/cli/sidecar.py +929 -0
  30. crp/cli/startup.py +272 -0
  31. crp/continuation/__init__.py +103 -0
  32. crp/continuation/completion.py +348 -0
  33. crp/continuation/degradation.py +157 -0
  34. crp/continuation/document_map.py +160 -0
  35. crp/continuation/flow.py +109 -0
  36. crp/continuation/gap.py +419 -0
  37. crp/continuation/manager.py +484 -0
  38. crp/continuation/quality_monitor.py +179 -0
  39. crp/continuation/stitch.py +419 -0
  40. crp/continuation/trigger.py +142 -0
  41. crp/continuation/voice.py +157 -0
  42. crp/core/__init__.py +69 -0
  43. crp/core/batch.py +77 -0
  44. crp/core/circuit_breaker.py +116 -0
  45. crp/core/config.py +377 -0
  46. crp/core/context_tools.py +540 -0
  47. crp/core/dispatch_router.py +3977 -0
  48. crp/core/errors.py +128 -0
  49. crp/core/extraction_facade.py +384 -0
  50. crp/core/facilitator.py +713 -0
  51. crp/core/idempotency.py +215 -0
  52. crp/core/orchestrator.py +1435 -0
  53. crp/core/relay_strategies.py +613 -0
  54. crp/core/security_manager.py +140 -0
  55. crp/core/session.py +134 -0
  56. crp/core/task_intent.py +36 -0
  57. crp/core/window.py +363 -0
  58. crp/envelope/__init__.py +30 -0
  59. crp/envelope/builder.py +288 -0
  60. crp/envelope/decomposer.py +236 -0
  61. crp/envelope/formatter.py +168 -0
  62. crp/envelope/packer.py +211 -0
  63. crp/envelope/reranker.py +209 -0
  64. crp/envelope/scoring.py +310 -0
  65. crp/extraction/__init__.py +45 -0
  66. crp/extraction/complexity.py +96 -0
  67. crp/extraction/contradiction.py +132 -0
  68. crp/extraction/pipeline.py +360 -0
  69. crp/extraction/quality_gate.py +237 -0
  70. crp/extraction/stage1_regex.py +173 -0
  71. crp/extraction/stage2_statistical.py +244 -0
  72. crp/extraction/stage3_gliner.py +210 -0
  73. crp/extraction/stage4_uie.py +183 -0
  74. crp/extraction/stage5_discourse.py +175 -0
  75. crp/extraction/stage6_llm.py +178 -0
  76. crp/extraction/structured_output.py +219 -0
  77. crp/extraction/types.py +299 -0
  78. crp/license_guard.py +722 -0
  79. crp/observability/__init__.py +30 -0
  80. crp/observability/audit.py +118 -0
  81. crp/observability/events.py +233 -0
  82. crp/observability/metrics.py +264 -0
  83. crp/observability/quality.py +135 -0
  84. crp/observability/structured_logging.py +81 -0
  85. crp/observability/telemetry.py +117 -0
  86. crp/provenance/__init__.py +314 -0
  87. crp/provenance/_embeddings.py +97 -0
  88. crp/provenance/_types.py +378 -0
  89. crp/provenance/attribution_scorer.py +252 -0
  90. crp/provenance/claim_detector.py +229 -0
  91. crp/provenance/contradiction_detector.py +243 -0
  92. crp/provenance/distortion_detector.py +397 -0
  93. crp/provenance/entailment_verifier.py +358 -0
  94. crp/provenance/fabrication_detector.py +203 -0
  95. crp/provenance/hallucination_scorer.py +320 -0
  96. crp/provenance/omission_analyzer.py +106 -0
  97. crp/provenance/provenance_chain.py +205 -0
  98. crp/provenance/report_generator.py +440 -0
  99. crp/providers/__init__.py +43 -0
  100. crp/providers/anthropic.py +270 -0
  101. crp/providers/base.py +135 -0
  102. crp/providers/custom.py +63 -0
  103. crp/providers/diagnostic.py +251 -0
  104. crp/providers/llamacpp.py +224 -0
  105. crp/providers/manager.py +139 -0
  106. crp/providers/ollama.py +243 -0
  107. crp/providers/openai.py +628 -0
  108. crp/providers/tokenizers.py +48 -0
  109. crp/py.typed +0 -0
  110. crp/resources/__init__.py +53 -0
  111. crp/resources/adaptive_allocator.py +525 -0
  112. crp/resources/cost_model.py +388 -0
  113. crp/resources/overhead_manager.py +217 -0
  114. crp/resources/resource_manager.py +262 -0
  115. crp/schemas/__init__.py +20 -0
  116. crp/schemas/cost-estimate.json +33 -0
  117. crp/schemas/crp-error.json +43 -0
  118. crp/schemas/envelope-preview.json +40 -0
  119. crp/schemas/persisted-state-header.json +27 -0
  120. crp/schemas/quality-report.json +94 -0
  121. crp/schemas/session-handle.json +33 -0
  122. crp/schemas/session-status.json +57 -0
  123. crp/schemas/stream-event.json +18 -0
  124. crp/schemas/task-intent.json +42 -0
  125. crp/security/__init__.py +93 -0
  126. crp/security/audit_trail.py +392 -0
  127. crp/security/binding.py +192 -0
  128. crp/security/compliance.py +813 -0
  129. crp/security/consent.py +593 -0
  130. crp/security/embedding_defense.py +161 -0
  131. crp/security/encryption.py +202 -0
  132. crp/security/injection.py +335 -0
  133. crp/security/integrity.py +267 -0
  134. crp/security/privacy.py +662 -0
  135. crp/security/quarantine.py +249 -0
  136. crp/security/rbac.py +221 -0
  137. crp/security/validation.py +164 -0
  138. crp/state/__init__.py +31 -0
  139. crp/state/cold_storage.py +258 -0
  140. crp/state/compaction.py +263 -0
  141. crp/state/critical_state.py +104 -0
  142. crp/state/event_log.py +313 -0
  143. crp/state/fact.py +189 -0
  144. crp/state/serialization.py +189 -0
  145. crp/state/session_cleanup.py +77 -0
  146. crp/state/snapshot.py +290 -0
  147. crp/state/warm_store.py +346 -0
  148. crprotocol-2.0.0.dist-info/METADATA +1295 -0
  149. crprotocol-2.0.0.dist-info/RECORD +153 -0
  150. crprotocol-2.0.0.dist-info/WHEEL +4 -0
  151. crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
  152. crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
  153. crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,77 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Session file TTL cleanup — automatic expiry of persisted sessions (§audit H11).
4
+
5
+ Production deployments accumulate session JSON files indefinitely.
6
+ This module provides automatic cleanup based on file age.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ import time
14
+
15
+ logger = logging.getLogger("crp.state.session_cleanup")
16
+
17
+ # Default TTL: 7 days
18
+ DEFAULT_TTL_SECONDS = 7 * 24 * 3600
19
+
20
+
21
+ def cleanup_expired_sessions(
22
+ sessions_dir: str | None = None,
23
+ ttl_seconds: float = DEFAULT_TTL_SECONDS,
24
+ dry_run: bool = False,
25
+ ) -> list[str]:
26
+ """Remove session files older than *ttl_seconds*.
27
+
28
+ Args:
29
+ sessions_dir: Path to the session storage directory.
30
+ Defaults to ``./crp_sessions``.
31
+ ttl_seconds: Maximum age in seconds (default 7 days).
32
+ dry_run: If True, list but do not delete files.
33
+
34
+ Returns:
35
+ List of removed (or would-be-removed) file paths.
36
+ """
37
+ if sessions_dir is None:
38
+ data_dir = os.environ.get("CRP_DATA_DIR", ".")
39
+ sessions_dir = os.path.join(data_dir, "crp_sessions")
40
+
41
+ if not os.path.isdir(sessions_dir):
42
+ return []
43
+
44
+ now = time.time()
45
+ cutoff = now - ttl_seconds
46
+ removed: list[str] = []
47
+
48
+ for entry in os.scandir(sessions_dir):
49
+ if not entry.is_file():
50
+ continue
51
+ # Only clean CRP-generated files (.json, .events.bin, .audit.jsonl)
52
+ if not (entry.name.endswith(".json") or entry.name.endswith(".events.bin")
53
+ or entry.name.endswith(".audit.jsonl")):
54
+ continue
55
+ try:
56
+ mtime = entry.stat().st_mtime
57
+ except OSError:
58
+ continue
59
+ if mtime < cutoff:
60
+ path = entry.path
61
+ if dry_run:
62
+ logger.info("Would remove expired session file: %s (age %.0f h)",
63
+ path, (now - mtime) / 3600)
64
+ else:
65
+ try:
66
+ os.remove(path)
67
+ logger.info("Removed expired session file: %s (age %.0f h)",
68
+ path, (now - mtime) / 3600)
69
+ except OSError as exc:
70
+ logger.warning("Failed to remove %s: %s", path, exc)
71
+ continue
72
+ removed.append(path)
73
+
74
+ if removed:
75
+ logger.info("Session cleanup: %d file(s) %s",
76
+ len(removed), "would be removed" if dry_run else "removed")
77
+ return removed
crp/state/snapshot.py ADDED
@@ -0,0 +1,290 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Snapshots & session resume — periodic checkpoint + restore (§3.3, §22.4).
4
+
5
+ Snapshots capture warm store state at regular intervals so the event log can
6
+ be truncated. Session resume protocol: load snapshot → verify integrity →
7
+ replay events → rebuild ANN.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import time
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from .event_log import FactEventLog
21
+ from .warm_store import WarmStateStore
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Snapshot data
25
+ # ---------------------------------------------------------------------------
26
+
27
+ SNAPSHOT_INTERVAL = 50 # windows between snapshots
28
+ SCHEMA_VERSION = 1
29
+
30
+
31
+ @dataclass
32
+ class EventLogSnapshot:
33
+ """Captured state at a point in time (§3.3)."""
34
+
35
+ snapshot_id: str = ""
36
+ schema_version: int = SCHEMA_VERSION
37
+ window_id: str = ""
38
+ window_count: int = 0
39
+ timestamp: float = field(default_factory=time.time)
40
+ warm_store_data: dict[str, Any] = field(default_factory=dict)
41
+ last_event_id: int = 0
42
+ checksum: str = "" # BLAKE3 or SHA-256
43
+
44
+ def compute_checksum(self) -> str:
45
+ """Compute integrity checksum over the snapshot payload."""
46
+ payload = json.dumps(self.warm_store_data, sort_keys=True, default=str)
47
+ try:
48
+ import blake3 # type: ignore[import-untyped]
49
+
50
+ return blake3.blake3(payload.encode()).hexdigest()
51
+ except ImportError:
52
+ return hashlib.sha256(payload.encode()).hexdigest()
53
+
54
+ def verify_checksum(self) -> bool:
55
+ """Verify the stored checksum matches the payload."""
56
+ return self.checksum == self.compute_checksum()
57
+
58
+ def to_dict(self) -> dict[str, Any]:
59
+ return {
60
+ "snapshot_id": self.snapshot_id,
61
+ "schema_version": self.schema_version,
62
+ "window_id": self.window_id,
63
+ "window_count": self.window_count,
64
+ "timestamp": self.timestamp,
65
+ "warm_store_data": self.warm_store_data,
66
+ "last_event_id": self.last_event_id,
67
+ "checksum": self.checksum,
68
+ }
69
+
70
+ @classmethod
71
+ def from_dict(cls, data: dict[str, Any]) -> EventLogSnapshot:
72
+ return cls(
73
+ snapshot_id=data.get("snapshot_id", ""),
74
+ schema_version=data.get("schema_version", SCHEMA_VERSION),
75
+ window_id=data.get("window_id", ""),
76
+ window_count=data.get("window_count", 0),
77
+ timestamp=data.get("timestamp", 0.0),
78
+ warm_store_data=data.get("warm_store_data", {}),
79
+ last_event_id=data.get("last_event_id", 0),
80
+ checksum=data.get("checksum", ""),
81
+ )
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Snapshot manager
86
+ # ---------------------------------------------------------------------------
87
+
88
+
89
+ class SnapshotManager:
90
+ """Manages periodic snapshots of warm store state (§3.3).
91
+
92
+ Usage:
93
+ mgr = SnapshotManager(warm_store, event_log)
94
+ mgr.maybe_snapshot(window_id) # called each window
95
+ # On resume:
96
+ mgr.restore_from_file(path)
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ warm_store: WarmStateStore,
102
+ event_log: FactEventLog,
103
+ interval: int = SNAPSHOT_INTERVAL,
104
+ ) -> None:
105
+ self._store = warm_store
106
+ self._log = event_log
107
+ self._interval = interval
108
+ self._snapshots: list[EventLogSnapshot] = []
109
+ self._windows_since_snapshot: int = 0
110
+
111
+ @property
112
+ def snapshot_count(self) -> int:
113
+ return len(self._snapshots)
114
+
115
+ @property
116
+ def last_snapshot(self) -> EventLogSnapshot | None:
117
+ return self._snapshots[-1] if self._snapshots else None
118
+
119
+ # --- Snapshot creation ----------------------------------------------------
120
+
121
+ def snapshot(self, window_id: str) -> EventLogSnapshot:
122
+ """Create a snapshot of the current warm store state."""
123
+ snap = EventLogSnapshot(
124
+ snapshot_id=f"snap-{len(self._snapshots) + 1}-{int(time.time())}",
125
+ window_id=window_id,
126
+ window_count=self._store.window_count,
127
+ warm_store_data=self._store.to_dict(),
128
+ last_event_id=self._log.size,
129
+ )
130
+ snap.checksum = snap.compute_checksum()
131
+ self._snapshots.append(snap)
132
+ self._windows_since_snapshot = 0
133
+ return snap
134
+
135
+ def maybe_snapshot(self, window_id: str) -> EventLogSnapshot | None:
136
+ """Create snapshot if interval has elapsed."""
137
+ self._windows_since_snapshot += 1
138
+ if self._windows_since_snapshot >= self._interval:
139
+ return self.snapshot(window_id)
140
+ return None
141
+
142
+ def truncate_before_snapshot(self) -> int:
143
+ """Truncate event log before the last snapshot. Returns events removed."""
144
+ snap = self.last_snapshot
145
+ if snap is None:
146
+ return 0
147
+ removed = self._log.truncate_before(snap.last_event_id)
148
+ return len(removed)
149
+
150
+ # --- Restore --------------------------------------------------------------
151
+
152
+ def restore_from_snapshot(self, snapshot: EventLogSnapshot) -> list[str]:
153
+ """Restore warm store from *snapshot*, returning validation warnings.
154
+
155
+ Performs consistency checks (§22.4):
156
+ 1. Schema version compatibility
157
+ 2. Checksum integrity
158
+ 3. Fact count consistency
159
+ 4. Edge integrity (both endpoints exist)
160
+ 5. No orphaned edges
161
+ 6. Critical state present
162
+ 7. Window count non-negative
163
+ 8. Fact IDs unique
164
+ 9. No circular supersession
165
+ 10. Timestamps reasonable
166
+ """
167
+ warnings: list[str] = []
168
+
169
+ # Check 1: Schema version
170
+ if snapshot.schema_version != SCHEMA_VERSION:
171
+ warnings.append(f"Schema version mismatch: {snapshot.schema_version} vs {SCHEMA_VERSION}")
172
+
173
+ # Check 2: Checksum
174
+ if not snapshot.verify_checksum():
175
+ warnings.append("Checksum verification FAILED — data may be corrupted")
176
+
177
+ # Load the data
178
+ data = snapshot.warm_store_data
179
+ self._store.load_from_dict(data)
180
+
181
+ # Check 3: Fact count
182
+ expected_count = len(data.get("facts", {}))
183
+ actual_count = self._store.fact_count
184
+ if expected_count != actual_count:
185
+ warnings.append(f"Fact count mismatch: expected {expected_count}, got {actual_count}")
186
+
187
+ # Check 4-5: Edge integrity
188
+ graph = self._store.graph
189
+ fact_ids = set(graph.nodes.keys())
190
+ for edge in graph.edges:
191
+ if edge.source_id not in fact_ids:
192
+ warnings.append(f"Orphaned edge source: {edge.source_id}")
193
+ if edge.target_id not in fact_ids:
194
+ warnings.append(f"Orphaned edge target: {edge.target_id}")
195
+
196
+ # Check 6: Critical state
197
+ cs = self._store.critical_state
198
+ if not cs.goal and not cs.phase:
199
+ warnings.append("Critical state has no goal or phase set")
200
+
201
+ # Check 7: Window count
202
+ if self._store.window_count < 0:
203
+ warnings.append(f"Invalid window count: {self._store.window_count}")
204
+
205
+ # Check 8: Unique fact IDs (guaranteed by dict, but verify)
206
+ all_facts = self._store.get_facts(include_superseded=True)
207
+ ids = [f.id for f in all_facts]
208
+ if len(ids) != len(set(ids)):
209
+ warnings.append("Duplicate fact IDs detected")
210
+
211
+ # Check 9: No circular supersession
212
+ for sf in all_facts:
213
+ if sf.is_superseded and sf.superseded_by == sf.id:
214
+ warnings.append(f"Circular supersession: {sf.id}")
215
+
216
+ # Check 10: Timestamps
217
+ now = time.time()
218
+ for sf in all_facts:
219
+ if sf.created_at > now + 3600:
220
+ warnings.append(f"Future timestamp on fact {sf.id}")
221
+
222
+ return warnings
223
+
224
+ # --- File I/O -------------------------------------------------------------
225
+
226
+ def save_to_file(self, path: str | Path) -> None:
227
+ """Save latest snapshot to disk using atomic write (§4G.4)."""
228
+ snap = self.last_snapshot
229
+ if snap is None:
230
+ snap = self.snapshot("auto-save")
231
+ p = Path(path)
232
+ p.parent.mkdir(parents=True, exist_ok=True)
233
+ # Atomic write: temp file then rename
234
+ tmp = p.with_suffix(".tmp")
235
+ try:
236
+ tmp.write_text(json.dumps(snap.to_dict(), default=str), encoding="utf-8")
237
+ os.replace(str(tmp), str(p))
238
+ except BaseException:
239
+ tmp.unlink(missing_ok=True)
240
+ raise
241
+
242
+ def restore_from_file(self, path: str | Path) -> list[str]:
243
+ """Load snapshot from disk, verify, restore. Returns warnings.
244
+
245
+ Handles partial-write corruption by checking for .tmp files (§4G.4).
246
+ After snapshot restore, replays event log entries since the snapshot
247
+ to rebuild full state (§4D.3).
248
+ """
249
+ p = Path(path)
250
+ if not p.exists():
251
+ # Check for interrupted atomic write (§4G.4)
252
+ tmp = p.with_suffix(".tmp")
253
+ if tmp.exists():
254
+ try:
255
+ data = json.loads(tmp.read_text(encoding="utf-8"))
256
+ snapshot = EventLogSnapshot.from_dict(data)
257
+ warnings = self.restore_from_snapshot(snapshot)
258
+ warnings.insert(0, "Recovered from interrupted write (.tmp file)")
259
+ return warnings
260
+ except (json.JSONDecodeError, OSError):
261
+ return [f"Snapshot file not found and .tmp is corrupt: {path}"]
262
+ return [f"Snapshot file not found: {path}"]
263
+ try:
264
+ data = json.loads(p.read_text(encoding="utf-8"))
265
+ except (json.JSONDecodeError, OSError) as exc:
266
+ return [f"Failed to read snapshot: {exc}"]
267
+
268
+ snapshot = EventLogSnapshot.from_dict(data)
269
+ warnings = self.restore_from_snapshot(snapshot)
270
+
271
+ # Replay events since snapshot to rebuild full state (§4D.3)
272
+ replayed = self._replay_events_since(snapshot.last_event_id)
273
+ if replayed > 0:
274
+ warnings.append(f"Replayed {replayed} events since snapshot")
275
+
276
+ return warnings
277
+
278
+ def _replay_events_since(self, last_event_id: int) -> int:
279
+ """Replay event log entries after a snapshot to rebuild state (§4D.3)."""
280
+ from .event_log import EVENT_SUPERSEDED
281
+ replayed = 0
282
+ for event in self._log.all_events():
283
+ if event.event_id <= last_event_id:
284
+ continue
285
+ if event.event_type == EVENT_SUPERSEDED:
286
+ new_id = event.payload.get("superseded_by", "")
287
+ conf = event.payload.get("confidence", 1.0)
288
+ self._store.supersede(event.fact_id, new_id, conf)
289
+ replayed += 1
290
+ return replayed