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.
- crp/__init__.py +126 -0
- crp/__main__.py +8 -0
- crp/_typing.py +27 -0
- crp/_version.py +5 -0
- crp/adapters.py +31 -0
- crp/advanced/__init__.py +40 -0
- crp/advanced/auto_ingest.py +400 -0
- crp/advanced/cqs.py +235 -0
- crp/advanced/cross_window.py +477 -0
- crp/advanced/curator.py +265 -0
- crp/advanced/feedback.py +146 -0
- crp/advanced/hierarchical.py +211 -0
- crp/advanced/meta_learning.py +401 -0
- crp/advanced/parallel.py +98 -0
- crp/advanced/review_cycle.py +329 -0
- crp/advanced/scale_mode.py +129 -0
- crp/advanced/source_grounding.py +207 -0
- crp/ckf/__init__.py +35 -0
- crp/ckf/community.py +377 -0
- crp/ckf/fabric.py +445 -0
- crp/ckf/gc.py +175 -0
- crp/ckf/graph_walk.py +87 -0
- crp/ckf/merge.py +133 -0
- crp/ckf/pattern_query.py +122 -0
- crp/ckf/pubsub.py +128 -0
- crp/ckf/semantic.py +207 -0
- crp/cli/__init__.py +7 -0
- crp/cli/main.py +329 -0
- crp/cli/sidecar.py +929 -0
- crp/cli/startup.py +272 -0
- crp/continuation/__init__.py +103 -0
- crp/continuation/completion.py +348 -0
- crp/continuation/degradation.py +157 -0
- crp/continuation/document_map.py +160 -0
- crp/continuation/flow.py +109 -0
- crp/continuation/gap.py +419 -0
- crp/continuation/manager.py +484 -0
- crp/continuation/quality_monitor.py +179 -0
- crp/continuation/stitch.py +419 -0
- crp/continuation/trigger.py +142 -0
- crp/continuation/voice.py +157 -0
- crp/core/__init__.py +69 -0
- crp/core/batch.py +77 -0
- crp/core/circuit_breaker.py +116 -0
- crp/core/config.py +377 -0
- crp/core/context_tools.py +540 -0
- crp/core/dispatch_router.py +3977 -0
- crp/core/errors.py +128 -0
- crp/core/extraction_facade.py +384 -0
- crp/core/facilitator.py +713 -0
- crp/core/idempotency.py +215 -0
- crp/core/orchestrator.py +1435 -0
- crp/core/relay_strategies.py +613 -0
- crp/core/security_manager.py +140 -0
- crp/core/session.py +134 -0
- crp/core/task_intent.py +36 -0
- crp/core/window.py +363 -0
- crp/envelope/__init__.py +30 -0
- crp/envelope/builder.py +288 -0
- crp/envelope/decomposer.py +236 -0
- crp/envelope/formatter.py +168 -0
- crp/envelope/packer.py +211 -0
- crp/envelope/reranker.py +209 -0
- crp/envelope/scoring.py +310 -0
- crp/extraction/__init__.py +45 -0
- crp/extraction/complexity.py +96 -0
- crp/extraction/contradiction.py +132 -0
- crp/extraction/pipeline.py +360 -0
- crp/extraction/quality_gate.py +237 -0
- crp/extraction/stage1_regex.py +173 -0
- crp/extraction/stage2_statistical.py +244 -0
- crp/extraction/stage3_gliner.py +210 -0
- crp/extraction/stage4_uie.py +183 -0
- crp/extraction/stage5_discourse.py +175 -0
- crp/extraction/stage6_llm.py +178 -0
- crp/extraction/structured_output.py +219 -0
- crp/extraction/types.py +299 -0
- crp/license_guard.py +722 -0
- crp/observability/__init__.py +30 -0
- crp/observability/audit.py +118 -0
- crp/observability/events.py +233 -0
- crp/observability/metrics.py +264 -0
- crp/observability/quality.py +135 -0
- crp/observability/structured_logging.py +81 -0
- crp/observability/telemetry.py +117 -0
- crp/provenance/__init__.py +314 -0
- crp/provenance/_embeddings.py +97 -0
- crp/provenance/_types.py +378 -0
- crp/provenance/attribution_scorer.py +252 -0
- crp/provenance/claim_detector.py +229 -0
- crp/provenance/contradiction_detector.py +243 -0
- crp/provenance/distortion_detector.py +397 -0
- crp/provenance/entailment_verifier.py +358 -0
- crp/provenance/fabrication_detector.py +203 -0
- crp/provenance/hallucination_scorer.py +320 -0
- crp/provenance/omission_analyzer.py +106 -0
- crp/provenance/provenance_chain.py +205 -0
- crp/provenance/report_generator.py +440 -0
- crp/providers/__init__.py +43 -0
- crp/providers/anthropic.py +270 -0
- crp/providers/base.py +135 -0
- crp/providers/custom.py +63 -0
- crp/providers/diagnostic.py +251 -0
- crp/providers/llamacpp.py +224 -0
- crp/providers/manager.py +139 -0
- crp/providers/ollama.py +243 -0
- crp/providers/openai.py +628 -0
- crp/providers/tokenizers.py +48 -0
- crp/py.typed +0 -0
- crp/resources/__init__.py +53 -0
- crp/resources/adaptive_allocator.py +525 -0
- crp/resources/cost_model.py +388 -0
- crp/resources/overhead_manager.py +217 -0
- crp/resources/resource_manager.py +262 -0
- crp/schemas/__init__.py +20 -0
- crp/schemas/cost-estimate.json +33 -0
- crp/schemas/crp-error.json +43 -0
- crp/schemas/envelope-preview.json +40 -0
- crp/schemas/persisted-state-header.json +27 -0
- crp/schemas/quality-report.json +94 -0
- crp/schemas/session-handle.json +33 -0
- crp/schemas/session-status.json +57 -0
- crp/schemas/stream-event.json +18 -0
- crp/schemas/task-intent.json +42 -0
- crp/security/__init__.py +93 -0
- crp/security/audit_trail.py +392 -0
- crp/security/binding.py +192 -0
- crp/security/compliance.py +813 -0
- crp/security/consent.py +593 -0
- crp/security/embedding_defense.py +161 -0
- crp/security/encryption.py +202 -0
- crp/security/injection.py +335 -0
- crp/security/integrity.py +267 -0
- crp/security/privacy.py +662 -0
- crp/security/quarantine.py +249 -0
- crp/security/rbac.py +221 -0
- crp/security/validation.py +164 -0
- crp/state/__init__.py +31 -0
- crp/state/cold_storage.py +258 -0
- crp/state/compaction.py +263 -0
- crp/state/critical_state.py +104 -0
- crp/state/event_log.py +313 -0
- crp/state/fact.py +189 -0
- crp/state/serialization.py +189 -0
- crp/state/session_cleanup.py +77 -0
- crp/state/snapshot.py +290 -0
- crp/state/warm_store.py +346 -0
- crprotocol-2.0.0.dist-info/METADATA +1295 -0
- crprotocol-2.0.0.dist-info/RECORD +153 -0
- crprotocol-2.0.0.dist-info/WHEEL +4 -0
- crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
- crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
- 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
|
crp/state/compaction.py
ADDED
|
@@ -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
|
+
)
|