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,258 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Cold storage — Tier 3 cross-session persistence (§3.6).
4
+
5
+ Persists facts, graph structure, community IDs, and metadata to disk.
6
+ PersistedStateHeader provides schema versioning + integrity checksums.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from .event_log import FactEventLog
20
+ from .warm_store import WarmStateStore
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Persisted state header
24
+ # ---------------------------------------------------------------------------
25
+
26
+ COLD_SCHEMA_VERSION = 1
27
+
28
+
29
+ @dataclass
30
+ class PersistedStateHeader:
31
+ """Schema-versioned header for cold storage files (§3.6)."""
32
+
33
+ schema_version: int = COLD_SCHEMA_VERSION
34
+ created_at: float = field(default_factory=time.time)
35
+ session_id: str = ""
36
+ fact_count: int = 0
37
+ edge_count: int = 0
38
+ window_count: int = 0
39
+ checksum: str = "" # BLAKE3 or SHA-256 of payload
40
+ hmac_signature: str = "" # HMAC chain signature (§4G.3)
41
+
42
+ def to_dict(self) -> dict[str, Any]:
43
+ return {
44
+ "schema_version": self.schema_version,
45
+ "created_at": self.created_at,
46
+ "session_id": self.session_id,
47
+ "fact_count": self.fact_count,
48
+ "edge_count": self.edge_count,
49
+ "window_count": self.window_count,
50
+ "checksum": self.checksum,
51
+ "hmac_signature": self.hmac_signature,
52
+ }
53
+
54
+ @classmethod
55
+ def from_dict(cls, data: dict[str, Any]) -> PersistedStateHeader:
56
+ return cls(
57
+ schema_version=data.get("schema_version", COLD_SCHEMA_VERSION),
58
+ created_at=data.get("created_at", 0.0),
59
+ session_id=data.get("session_id", ""),
60
+ fact_count=data.get("fact_count", 0),
61
+ edge_count=data.get("edge_count", 0),
62
+ window_count=data.get("window_count", 0),
63
+ checksum=data.get("checksum", ""),
64
+ hmac_signature=data.get("hmac_signature", ""),
65
+ )
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Cold storage operations
70
+ # ---------------------------------------------------------------------------
71
+
72
+
73
+ def _compute_checksum(payload: str) -> str:
74
+ """Compute integrity checksum (BLAKE3 preferred, SHA-256 fallback)."""
75
+ try:
76
+ import blake3 # type: ignore[import-untyped]
77
+
78
+ return blake3.blake3(payload.encode()).hexdigest()
79
+ except ImportError:
80
+ return hashlib.sha256(payload.encode()).hexdigest()
81
+
82
+
83
+ def persist_to_cold(
84
+ warm_store: WarmStateStore,
85
+ event_log: FactEventLog,
86
+ path: str | Path,
87
+ session_id: str = "",
88
+ community_map: dict[str, int] | None = None,
89
+ hmac_key: bytes | None = None,
90
+ ) -> PersistedStateHeader:
91
+ """Persist warm store + event log to cold storage.
92
+
93
+ File format: JSON with header + payload.
94
+ Includes community IDs (§4E.1) and HMAC chain (§4G.3) when provided.
95
+ Uses atomic writes to prevent partial-write corruption (§4G.4).
96
+ """
97
+ store_data = warm_store.to_dict()
98
+ event_data = event_log.to_list()
99
+
100
+ payload: dict[str, Any] = {
101
+ "warm_store": store_data,
102
+ "event_log": event_data,
103
+ }
104
+
105
+ # Include community IDs if available (§4E.1)
106
+ if community_map:
107
+ payload["community_map"] = community_map
108
+
109
+ payload_str = json.dumps(payload, sort_keys=True, default=str)
110
+
111
+ header = PersistedStateHeader(
112
+ session_id=session_id,
113
+ fact_count=warm_store.fact_count,
114
+ edge_count=len(warm_store.graph.edges),
115
+ window_count=warm_store.window_count,
116
+ checksum=_compute_checksum(payload_str),
117
+ )
118
+
119
+ # Compute HMAC chain signature if key provided (§4G.3)
120
+ if hmac_key:
121
+ import hmac as _hmac
122
+ header.hmac_signature = _hmac.new(
123
+ hmac_key, payload_str.encode(), hashlib.sha256
124
+ ).hexdigest()
125
+
126
+ cold_data = {
127
+ "header": header.to_dict(),
128
+ "payload": payload,
129
+ }
130
+
131
+ p = Path(path)
132
+ p.parent.mkdir(parents=True, exist_ok=True)
133
+
134
+ # Atomic write: write to temp, then rename (§4G.4)
135
+ tmp = p.with_suffix(".tmp")
136
+ try:
137
+ tmp.write_text(json.dumps(cold_data, default=str), encoding="utf-8")
138
+ # os.replace is atomic on POSIX; best-effort on Windows
139
+ os.replace(str(tmp), str(p))
140
+ except BaseException:
141
+ # Clean up partial temp file on failure
142
+ tmp.unlink(missing_ok=True)
143
+ raise
144
+
145
+ return header
146
+
147
+
148
+ def restore_from_cold(
149
+ warm_store: WarmStateStore,
150
+ event_log: FactEventLog,
151
+ path: str | Path,
152
+ hmac_key: bytes | None = None,
153
+ ) -> tuple[PersistedStateHeader, list[str], dict[str, int]]:
154
+ """Load cold storage into warm store + event log.
155
+
156
+ Returns (header, warnings, community_map) where:
157
+ - warnings list integrity issues
158
+ - community_map has restored community assignments (§4E.1)
159
+
160
+ Performs HMAC chain verification when key provided (§4G.3).
161
+ Handles partial-write corruption (§4G.4).
162
+ Applies schema migrations (§4G.2).
163
+ """
164
+ p = Path(path)
165
+ warnings: list[str] = []
166
+ community_map: dict[str, int] = {}
167
+
168
+ if not p.exists():
169
+ # Check for temp file from interrupted atomic write (§4G.4)
170
+ tmp = p.with_suffix(".tmp")
171
+ if tmp.exists():
172
+ warnings.append("Found interrupted write (.tmp file); attempting recovery")
173
+ try:
174
+ cold_data = json.loads(tmp.read_text(encoding="utf-8"))
175
+ warnings.append("Recovered from .tmp file")
176
+ except (json.JSONDecodeError, OSError):
177
+ warnings.append(f"Cold storage file not found and .tmp is corrupt: {path}")
178
+ return PersistedStateHeader(), warnings, community_map
179
+ else:
180
+ warnings.append(f"Cold storage file not found: {path}")
181
+ return PersistedStateHeader(), warnings, community_map
182
+ else:
183
+ try:
184
+ cold_data = json.loads(p.read_text(encoding="utf-8"))
185
+ except (json.JSONDecodeError, OSError) as exc:
186
+ warnings.append(f"Failed to read cold storage: {exc}")
187
+ return PersistedStateHeader(), warnings, community_map
188
+
189
+ header = PersistedStateHeader.from_dict(cold_data.get("header", {}))
190
+ payload = cold_data.get("payload", {})
191
+
192
+ # Schema migration (§4G.2)
193
+ if header.schema_version < COLD_SCHEMA_VERSION:
194
+ warnings.append(
195
+ f"Migrating schema v{header.schema_version} → v{COLD_SCHEMA_VERSION}"
196
+ )
197
+ payload = _migrate_payload(payload, header.schema_version, COLD_SCHEMA_VERSION)
198
+ elif header.schema_version > COLD_SCHEMA_VERSION:
199
+ warnings.append(
200
+ f"Schema version {header.schema_version} is newer than current "
201
+ f"{COLD_SCHEMA_VERSION} — forward compat may lose data"
202
+ )
203
+
204
+ # Verify checksum
205
+ payload_str = json.dumps(payload, sort_keys=True, default=str)
206
+ expected_checksum = _compute_checksum(payload_str)
207
+ integrity_failed = False
208
+ if header.checksum and header.checksum != expected_checksum:
209
+ warnings.append("Checksum verification FAILED — data may be corrupted")
210
+ integrity_failed = True
211
+
212
+ # Verify HMAC chain (§4G.3)
213
+ if hmac_key and header.hmac_signature:
214
+ import hmac as _hmac
215
+ expected_hmac = _hmac.new(
216
+ hmac_key, payload_str.encode(), hashlib.sha256
217
+ ).hexdigest()
218
+ if not _hmac.compare_digest(header.hmac_signature, expected_hmac):
219
+ warnings.append("HMAC chain verification FAILED — data may be tampered")
220
+ integrity_failed = True
221
+ elif hmac_key and not header.hmac_signature:
222
+ warnings.append("HMAC key provided but no HMAC signature in stored data")
223
+
224
+ # SECURITY: fail-closed — reject corrupted/tampered data (§audit5 SEC-H1)
225
+ if integrity_failed:
226
+ warnings.append("REJECTED: data not loaded due to integrity failure")
227
+ return header, warnings, community_map
228
+
229
+ # Load warm store
230
+ warm_store.load_from_dict(payload.get("warm_store", {}))
231
+
232
+ # Load event log
233
+ event_log.load_from_list(payload.get("event_log", []))
234
+
235
+ # Restore community IDs (§4E.1)
236
+ if "community_map" in payload:
237
+ community_map = payload["community_map"]
238
+
239
+ # Verify counts
240
+ if header.fact_count != warm_store.fact_count:
241
+ warnings.append(
242
+ f"Fact count mismatch: header={header.fact_count}, "
243
+ f"loaded={warm_store.fact_count}"
244
+ )
245
+
246
+ return header, warnings, community_map
247
+
248
+
249
+ def _migrate_payload(
250
+ payload: dict[str, Any],
251
+ from_version: int,
252
+ to_version: int,
253
+ ) -> dict[str, Any]:
254
+ """Apply schema migrations sequentially (§4G.2)."""
255
+ # Currently only v1 exists. Future migrations will be added here:
256
+ # if from_version < 2:
257
+ # payload = _migrate_v1_to_v2(payload)
258
+ return payload
@@ -0,0 +1,263 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Compaction engine — deduplicate and summarize warm state (§3.6).
4
+
5
+ Trigger: fact_count > 5000 OR envelope_latency > 500ms.
6
+ Algorithm:
7
+ 1. Archive superseded facts
8
+ 2. Cluster remaining by cosine similarity (>0.80 threshold)
9
+ 3. TextRank summarize clusters
10
+ 4. Rebuild ANN index
11
+ 5. Compact graph
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import math
17
+ from dataclasses import dataclass
18
+
19
+ from crp.extraction.types import Fact
20
+
21
+ from .event_log import FactEventLog
22
+ from .fact import StateFact
23
+ from .warm_store import WarmStateStore
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Compaction config
27
+ # ---------------------------------------------------------------------------
28
+
29
+ COMPACTION_FACT_THRESHOLD = 5000
30
+ COMPACTION_LATENCY_THRESHOLD_MS = 500.0
31
+ CLUSTER_SIMILARITY_THRESHOLD = 0.80
32
+
33
+
34
+ @dataclass
35
+ class CompactionConfig:
36
+ """Tuneable compaction parameters."""
37
+
38
+ fact_threshold: int = COMPACTION_FACT_THRESHOLD
39
+ latency_threshold_ms: float = COMPACTION_LATENCY_THRESHOLD_MS
40
+ cluster_sim_threshold: float = CLUSTER_SIMILARITY_THRESHOLD
41
+ max_cluster_size: int = 20
42
+
43
+
44
+ @dataclass
45
+ class CompactionResult:
46
+ """Result of a compaction pass."""
47
+
48
+ facts_before: int = 0
49
+ facts_after: int = 0
50
+ superseded_archived: int = 0
51
+ clusters_found: int = 0
52
+ summaries_created: int = 0
53
+ elapsed_ms: float = 0.0
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Simple cosine similarity (no ML deps)
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ def _cosine_sim_texts(a: str, b: str) -> float:
62
+ """Word-overlap cosine similarity between two texts."""
63
+ words_a = set(a.lower().split())
64
+ words_b = set(b.lower().split())
65
+ if not words_a or not words_b:
66
+ return 0.0
67
+ intersection = words_a & words_b
68
+ return len(intersection) / (math.sqrt(len(words_a)) * math.sqrt(len(words_b)))
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Clustering
73
+ # ---------------------------------------------------------------------------
74
+
75
+
76
+ def _cluster_facts(
77
+ facts: list[StateFact],
78
+ threshold: float,
79
+ max_cluster_size: int,
80
+ ) -> list[list[StateFact]]:
81
+ """Simple greedy clustering by pairwise cosine similarity.
82
+
83
+ When ML embeddings are available, uses them. Fallback: word overlap.
84
+ """
85
+ clusters: list[list[StateFact]] = []
86
+ assigned: set[str] = set()
87
+
88
+ for sf in facts:
89
+ if sf.id in assigned:
90
+ continue
91
+ cluster = [sf]
92
+ assigned.add(sf.id)
93
+
94
+ for other in facts:
95
+ if other.id in assigned:
96
+ continue
97
+ if len(cluster) >= max_cluster_size:
98
+ break
99
+
100
+ # Use embeddings if both have them
101
+ if sf.has_embedding() and other.has_embedding():
102
+ emb_a = sf.embedding
103
+ emb_b = other.embedding
104
+ if emb_a and emb_b:
105
+ dot = sum(x * y for x, y in zip(emb_a, emb_b))
106
+ na = math.sqrt(sum(x * x for x in emb_a)) or 1e-12
107
+ nb = math.sqrt(sum(x * x for x in emb_b)) or 1e-12
108
+ sim = dot / (na * nb)
109
+ else:
110
+ sim = _cosine_sim_texts(sf.text, other.text)
111
+ else:
112
+ sim = _cosine_sim_texts(sf.text, other.text)
113
+
114
+ if sim >= threshold:
115
+ cluster.append(other)
116
+ assigned.add(other.id)
117
+
118
+ clusters.append(cluster)
119
+
120
+ return clusters
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # TextRank-inspired summarization (single-fact reduction)
125
+ # ---------------------------------------------------------------------------
126
+
127
+
128
+ def _summarize_cluster(cluster: list[StateFact]) -> str:
129
+ """Pick the most representative fact from a cluster.
130
+
131
+ Uses a simple centrality heuristic: the fact with the highest average
132
+ similarity to all others. When ML embeddings are unavailable, uses the
133
+ highest-confidence fact.
134
+ """
135
+ if len(cluster) == 1:
136
+ return cluster[0].text
137
+
138
+ # Try embedding-based centrality
139
+ has_all_emb = all(sf.has_embedding() for sf in cluster)
140
+ if has_all_emb:
141
+ best_idx = 0
142
+ best_score = -1.0
143
+ for i, sf in enumerate(cluster):
144
+ emb_i = sf.embedding
145
+ if not emb_i:
146
+ continue
147
+ total_sim = 0.0
148
+ for j, other in enumerate(cluster):
149
+ if i == j:
150
+ continue
151
+ emb_j = other.embedding
152
+ if emb_j:
153
+ dot = sum(x * y for x, y in zip(emb_i, emb_j))
154
+ na = math.sqrt(sum(x * x for x in emb_i)) or 1e-12
155
+ nb = math.sqrt(sum(x * x for x in emb_j)) or 1e-12
156
+ total_sim += dot / (na * nb)
157
+ if total_sim > best_score:
158
+ best_score = total_sim
159
+ best_idx = i
160
+ return cluster[best_idx].text
161
+
162
+ # Fallback: highest confidence
163
+ return max(cluster, key=lambda sf: sf.confidence).text
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Compaction engine
168
+ # ---------------------------------------------------------------------------
169
+
170
+
171
+ def should_compact(
172
+ store: WarmStateStore,
173
+ last_envelope_latency_ms: float = 0.0,
174
+ config: CompactionConfig | None = None,
175
+ ) -> bool:
176
+ """Check if compaction should run."""
177
+ cfg = config or CompactionConfig()
178
+ if store.fact_count > cfg.fact_threshold:
179
+ return True
180
+ return last_envelope_latency_ms > cfg.latency_threshold_ms
181
+
182
+
183
+ def compact(
184
+ store: WarmStateStore,
185
+ event_log: FactEventLog,
186
+ window_id: str,
187
+ config: CompactionConfig | None = None,
188
+ ) -> CompactionResult:
189
+ """Run a compaction pass on the warm store.
190
+
191
+ 1. Archive superseded facts → cold (emit ARCHIVED events)
192
+ 2. Cluster remaining active facts by similarity
193
+ 3. Summarize multi-fact clusters → single representative fact
194
+ 4. Update graph with summary facts
195
+ """
196
+ import time as _time
197
+ import uuid
198
+
199
+ cfg = config or CompactionConfig()
200
+ t0 = _time.perf_counter()
201
+ result = CompactionResult(facts_before=store.fact_count)
202
+
203
+ # Step 1: Archive superseded
204
+ all_facts = store.get_facts(include_superseded=True)
205
+ superseded = [sf for sf in all_facts if sf.is_superseded]
206
+ for sf in superseded:
207
+ store.remove_fact(sf.id)
208
+ event_log.record_archived(sf.id, window_id)
209
+ result.superseded_archived += 1
210
+
211
+ # Step 2: Cluster active facts
212
+ active = store.get_facts()
213
+ if len(active) <= cfg.fact_threshold // 2:
214
+ # Not enough to bother clustering
215
+ result.facts_after = store.fact_count
216
+ result.elapsed_ms = (_time.perf_counter() - t0) * 1000
217
+ return result
218
+
219
+ clusters = _cluster_facts(active, cfg.cluster_sim_threshold, cfg.max_cluster_size)
220
+ result.clusters_found = len(clusters)
221
+
222
+ # Step 3: Summarize multi-fact clusters
223
+ for cluster in clusters:
224
+ if len(cluster) <= 1:
225
+ continue
226
+ summary_text = _summarize_cluster(cluster)
227
+ summary_fact = Fact(
228
+ id=str(uuid.uuid4()),
229
+ text=summary_text,
230
+ category="compacted_summary",
231
+ source_window_id=window_id,
232
+ confidence=max(sf.confidence for sf in cluster),
233
+ extraction_stage=0,
234
+ )
235
+
236
+ # Remove cluster members, add summary
237
+ for sf in cluster:
238
+ store.remove_fact(sf.id)
239
+ event_log.record_compaction(sf.id, window_id, summary_id=summary_fact.id)
240
+
241
+ store.add_facts([summary_fact])
242
+ event_log.record_fact_created(summary_fact, window_id)
243
+ result.summaries_created += 1
244
+
245
+ # Step 4: Clean up stale graph edges (§4D.5)
246
+ _cleanup_stale_edges(store)
247
+
248
+ result.facts_after = store.fact_count
249
+ result.elapsed_ms = (_time.perf_counter() - t0) * 1000
250
+ return result
251
+
252
+
253
+ def _cleanup_stale_edges(store: WarmStateStore) -> int:
254
+ """Remove graph edges that reference non-existent facts (§4D.5)."""
255
+ active_ids = {sf.id for sf in store.get_facts(include_superseded=True)}
256
+ graph = store.graph
257
+ stale = [
258
+ e for e in graph.edges
259
+ if e.source_id not in active_ids or e.target_id not in active_ids
260
+ ]
261
+ for edge in stale:
262
+ graph.edges.remove(edge)
263
+ return len(stale)
@@ -0,0 +1,104 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Critical & structural state — always-included envelope sections (§3.1).
4
+
5
+ CriticalState: goal, phase, blockers, constraints (Tier 0 — never evicted).
6
+ StructuralState: continuation tracking for document position (§04 §3.5.2).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+
15
+ @dataclass
16
+ class CriticalState:
17
+ """Tier-0 critical state — ALWAYS included in every envelope (§3.1).
18
+
19
+ Tracks the task's fundamental parameters that must survive every window.
20
+ """
21
+
22
+ goal: str = ""
23
+ phase: str = ""
24
+ blockers: list[str] = field(default_factory=list)
25
+ constraints: list[str] = field(default_factory=list)
26
+ window_id: str = "" # last window that updated this
27
+
28
+ def to_sections(self) -> dict[str, str]:
29
+ """Convert to envelope section dict for the formatter."""
30
+ sections: dict[str, str] = {}
31
+ if self.goal:
32
+ sections["GOAL"] = self.goal
33
+ if self.phase:
34
+ sections["PHASE"] = self.phase
35
+ if self.blockers:
36
+ sections["BLOCKER"] = "\n".join(f"- {b}" for b in self.blockers)
37
+ if self.constraints:
38
+ sections["CONSTRAINT"] = "\n".join(f"- {c}" for c in self.constraints)
39
+ return sections
40
+
41
+ def update(self, **kwargs: Any) -> None:
42
+ """Partial update of critical state fields."""
43
+ for key, value in kwargs.items():
44
+ if hasattr(self, key):
45
+ setattr(self, key, value)
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ return {
49
+ "goal": self.goal,
50
+ "phase": self.phase,
51
+ "blockers": self.blockers,
52
+ "constraints": self.constraints,
53
+ "window_id": self.window_id,
54
+ }
55
+
56
+ @classmethod
57
+ def from_dict(cls, data: dict[str, Any]) -> CriticalState:
58
+ return cls(
59
+ goal=data.get("goal", ""),
60
+ phase=data.get("phase", ""),
61
+ blockers=data.get("blockers", []),
62
+ constraints=data.get("constraints", []),
63
+ window_id=data.get("window_id", ""),
64
+ )
65
+
66
+
67
+ @dataclass
68
+ class StructuralState:
69
+ """Document structure tracking for continuation stitching (§04 §3.5.2).
70
+
71
+ Tracks where the LLM is in its output so continuation windows can resume
72
+ from the correct position.
73
+ """
74
+
75
+ current_section: str = ""
76
+ list_position: int = 0
77
+ open_blocks: list[str] = field(default_factory=list) # e.g. ["```python", "- item"]
78
+ markdown_depth: int = 0 # heading level (1-6)
79
+ last_heading: str = ""
80
+ code_block_open: bool = False
81
+ code_language: str = ""
82
+
83
+ def to_dict(self) -> dict[str, Any]:
84
+ return {
85
+ "current_section": self.current_section,
86
+ "list_position": self.list_position,
87
+ "open_blocks": self.open_blocks,
88
+ "markdown_depth": self.markdown_depth,
89
+ "last_heading": self.last_heading,
90
+ "code_block_open": self.code_block_open,
91
+ "code_language": self.code_language,
92
+ }
93
+
94
+ @classmethod
95
+ def from_dict(cls, data: dict[str, Any]) -> StructuralState:
96
+ return cls(
97
+ current_section=data.get("current_section", ""),
98
+ list_position=data.get("list_position", 0),
99
+ open_blocks=data.get("open_blocks", []),
100
+ markdown_depth=data.get("markdown_depth", 0),
101
+ last_heading=data.get("last_heading", ""),
102
+ code_block_open=data.get("code_block_open", False),
103
+ code_language=data.get("code_language", ""),
104
+ )