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
crp/state/warm_store.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Warm state store — in-memory Tier 2 with optional SQLite WAL persist (§3.0, §3.1).
|
|
4
|
+
|
|
5
|
+
The warm store holds all active facts, their graph, and provides the ranked
|
|
6
|
+
retrieval interface used by the envelope builder.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from crp.extraction.types import Fact, FactEdge, FactGraph
|
|
19
|
+
|
|
20
|
+
from .critical_state import CriticalState, StructuralState
|
|
21
|
+
from .fact import StateFact
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("crp.state.warm_store")
|
|
24
|
+
|
|
25
|
+
# Schema version for session file format (§audit L6 / L7)
|
|
26
|
+
SCHEMA_VERSION = 2
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# WarmStateStore
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class WarmStoreConfig:
|
|
35
|
+
"""Configuration for the warm state store."""
|
|
36
|
+
|
|
37
|
+
max_facts: int = 10_000
|
|
38
|
+
persist_enabled: bool = False
|
|
39
|
+
persist_path: str = ""
|
|
40
|
+
compact_threshold: int = 5000
|
|
41
|
+
compact_latency_ms: float = 500.0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class WarmStateStore:
|
|
45
|
+
"""In-memory warm state (Tier 2) with thread-safe access.
|
|
46
|
+
|
|
47
|
+
Provides:
|
|
48
|
+
- add_facts / get_facts / get_ranked_facts / mark_seen / supersede
|
|
49
|
+
- Critical state & structural state management
|
|
50
|
+
- Optional async SQLite WAL persistence (Phase 4E)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, config: WarmStoreConfig | None = None) -> None:
|
|
54
|
+
self._config = config or WarmStoreConfig()
|
|
55
|
+
self._lock = threading.Lock()
|
|
56
|
+
|
|
57
|
+
# Fact storage
|
|
58
|
+
self._facts: dict[str, StateFact] = {}
|
|
59
|
+
self._fact_text_hashes: set[str] = set()
|
|
60
|
+
self._graph = FactGraph()
|
|
61
|
+
|
|
62
|
+
# Critical & structural state
|
|
63
|
+
self._critical = CriticalState()
|
|
64
|
+
self._structural = StructuralState()
|
|
65
|
+
|
|
66
|
+
# Event callbacks
|
|
67
|
+
self._on_fact_added: list[Callable[[StateFact], None]] = []
|
|
68
|
+
self._on_fact_superseded: list[Callable[[StateFact, str], None]] = []
|
|
69
|
+
|
|
70
|
+
# Window tracking
|
|
71
|
+
self._current_window_id: str = ""
|
|
72
|
+
self._window_count: int = 0
|
|
73
|
+
|
|
74
|
+
# --- Properties -----------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def fact_count(self) -> int:
|
|
78
|
+
return len(self._facts)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def graph(self) -> FactGraph:
|
|
82
|
+
return self._graph
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def critical_state(self) -> CriticalState:
|
|
86
|
+
return self._critical
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def structural_state(self) -> StructuralState:
|
|
90
|
+
return self._structural
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def window_count(self) -> int:
|
|
94
|
+
return self._window_count
|
|
95
|
+
|
|
96
|
+
# --- Fact operations ------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def add_facts(self, facts: list[Fact], edges: list[FactEdge] | None = None) -> list[StateFact]:
|
|
99
|
+
"""Add extraction facts to the warm store, wrapping them as StateFacts."""
|
|
100
|
+
added: list[StateFact] = []
|
|
101
|
+
with self._lock:
|
|
102
|
+
for fact in facts:
|
|
103
|
+
if len(self._facts) >= self._config.max_facts: # §audit3 H3: enforce cap
|
|
104
|
+
logger.warning("Warm store fact cap reached (%d), dropping new facts", self._config.max_facts)
|
|
105
|
+
break
|
|
106
|
+
if fact.id in self._facts:
|
|
107
|
+
continue # deduplicate by ID
|
|
108
|
+
text_hash = hashlib.sha256(fact.text.encode()).hexdigest()
|
|
109
|
+
if text_hash in self._fact_text_hashes:
|
|
110
|
+
continue # deduplicate by content
|
|
111
|
+
sf = StateFact.from_fact(fact)
|
|
112
|
+
self._facts[fact.id] = sf
|
|
113
|
+
self._fact_text_hashes.add(text_hash)
|
|
114
|
+
self._graph.add_fact(fact)
|
|
115
|
+
added.append(sf)
|
|
116
|
+
if edges:
|
|
117
|
+
for edge in edges:
|
|
118
|
+
self._graph.add_edge(edge)
|
|
119
|
+
# Fire callbacks outside lock
|
|
120
|
+
for sf in added:
|
|
121
|
+
for cb in self._on_fact_added:
|
|
122
|
+
try:
|
|
123
|
+
cb(sf)
|
|
124
|
+
except Exception: # noqa: BLE001
|
|
125
|
+
logger.exception("Fact-added callback failed for fact %s", sf.id)
|
|
126
|
+
return added
|
|
127
|
+
|
|
128
|
+
def get_facts(self, *, include_superseded: bool = False) -> list[StateFact]:
|
|
129
|
+
"""Return all active facts (or all including superseded)."""
|
|
130
|
+
with self._lock:
|
|
131
|
+
if include_superseded:
|
|
132
|
+
return list(self._facts.values())
|
|
133
|
+
return [sf for sf in self._facts.values() if not sf.is_superseded]
|
|
134
|
+
|
|
135
|
+
def get_fact(self, fact_id: str) -> StateFact | None:
|
|
136
|
+
"""Get a specific fact by ID."""
|
|
137
|
+
return self._facts.get(fact_id)
|
|
138
|
+
|
|
139
|
+
def get_active_facts_as_extraction(self) -> list[Fact]:
|
|
140
|
+
"""Return active facts as extraction Fact objects (for envelope builder)."""
|
|
141
|
+
with self._lock:
|
|
142
|
+
return [sf.fact for sf in self._facts.values() if not sf.is_superseded]
|
|
143
|
+
|
|
144
|
+
def get_ranked_facts(self, *, top_k: int | None = None, limit: int | None = None) -> list[StateFact]:
|
|
145
|
+
"""Return active facts sorted by relevance heuristic (confidence × recency)."""
|
|
146
|
+
k = top_k or limit
|
|
147
|
+
with self._lock:
|
|
148
|
+
active = [sf for sf in self._facts.values() if not sf.is_superseded]
|
|
149
|
+
# Simple ranking: confidence × inverse age
|
|
150
|
+
active.sort(
|
|
151
|
+
key=lambda sf: sf.confidence * (1.0 / (1.0 + sf.age_in_windows)),
|
|
152
|
+
reverse=True,
|
|
153
|
+
)
|
|
154
|
+
if k is not None:
|
|
155
|
+
return active[:k]
|
|
156
|
+
return active
|
|
157
|
+
|
|
158
|
+
def mark_seen(self, fact_ids: list[str], window_id: str) -> None:
|
|
159
|
+
"""Record that facts were included in an envelope for *window_id*."""
|
|
160
|
+
with self._lock:
|
|
161
|
+
for fid in fact_ids:
|
|
162
|
+
sf = self._facts.get(fid)
|
|
163
|
+
if sf:
|
|
164
|
+
sf.mark_seen(window_id)
|
|
165
|
+
|
|
166
|
+
def supersede(self, old_fact_id: str, new_fact_id: str, confidence: float = 1.0) -> None:
|
|
167
|
+
"""Mark *old_fact_id* as superseded by *new_fact_id*."""
|
|
168
|
+
with self._lock:
|
|
169
|
+
old = self._facts.get(old_fact_id)
|
|
170
|
+
if old:
|
|
171
|
+
old.supersede(new_fact_id, confidence)
|
|
172
|
+
# Fire callback outside lock
|
|
173
|
+
if old:
|
|
174
|
+
for cb in self._on_fact_superseded:
|
|
175
|
+
try:
|
|
176
|
+
cb(old, new_fact_id)
|
|
177
|
+
except Exception: # noqa: BLE001
|
|
178
|
+
logger.exception("Fact-superseded callback failed for fact %s", old_fact_id)
|
|
179
|
+
|
|
180
|
+
def remove_fact(self, fact_id: str) -> StateFact | None:
|
|
181
|
+
"""Remove a fact (for compaction/archival)."""
|
|
182
|
+
with self._lock:
|
|
183
|
+
sf = self._facts.pop(fact_id, None)
|
|
184
|
+
if sf is not None:
|
|
185
|
+
# Clean content-dedup hash (§audit2 STATE-H1)
|
|
186
|
+
text_hash = hashlib.sha256(sf.fact.text.encode()).hexdigest()
|
|
187
|
+
self._fact_text_hashes.discard(text_hash)
|
|
188
|
+
# Remove from graph (§audit2 STATE-H2)
|
|
189
|
+
self._graph.remove_fact(fact_id)
|
|
190
|
+
return sf
|
|
191
|
+
|
|
192
|
+
def boost_confidence(self, fact_id: str, delta: float) -> None:
|
|
193
|
+
"""Increase a fact's confidence by *delta*, capped at 1.0 (§22 curation)."""
|
|
194
|
+
with self._lock:
|
|
195
|
+
sf = self._facts.get(fact_id)
|
|
196
|
+
if sf:
|
|
197
|
+
sf.fact.confidence = min(1.0, sf.fact.confidence + delta)
|
|
198
|
+
|
|
199
|
+
def reduce_confidence(self, fact_id: str, delta: float) -> None:
|
|
200
|
+
"""Decrease a fact's confidence by *delta*, floored at 0.0 (§22 curation)."""
|
|
201
|
+
with self._lock:
|
|
202
|
+
sf = self._facts.get(fact_id)
|
|
203
|
+
if sf:
|
|
204
|
+
sf.fact.confidence = max(0.0, sf.fact.confidence - delta)
|
|
205
|
+
|
|
206
|
+
# --- Window lifecycle -----------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def advance_window(self, window_id: str) -> None:
|
|
209
|
+
"""Called when a new window begins — age all facts, update window tracking."""
|
|
210
|
+
with self._lock:
|
|
211
|
+
self._current_window_id = window_id
|
|
212
|
+
self._window_count += 1
|
|
213
|
+
for sf in self._facts.values():
|
|
214
|
+
sf.increment_age()
|
|
215
|
+
|
|
216
|
+
# --- Critical state -------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
def get_critical_state(self) -> CriticalState:
|
|
219
|
+
return self._critical
|
|
220
|
+
|
|
221
|
+
def update_critical_state(self, **kwargs: Any) -> None:
|
|
222
|
+
"""Update critical state fields (goal, phase, blockers, constraints)."""
|
|
223
|
+
with self._lock:
|
|
224
|
+
self._critical.update(**kwargs)
|
|
225
|
+
self._critical.window_id = self._current_window_id
|
|
226
|
+
|
|
227
|
+
def update_phase(self, phase: str) -> None:
|
|
228
|
+
with self._lock:
|
|
229
|
+
self._critical.phase = phase
|
|
230
|
+
self._critical.window_id = self._current_window_id
|
|
231
|
+
|
|
232
|
+
# --- Structural state -----------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def get_structural_state(self) -> StructuralState:
|
|
235
|
+
return self._structural
|
|
236
|
+
|
|
237
|
+
def update_structural_state(self, **kwargs: Any) -> None:
|
|
238
|
+
with self._lock:
|
|
239
|
+
for key, value in kwargs.items():
|
|
240
|
+
if hasattr(self._structural, key):
|
|
241
|
+
setattr(self._structural, key, value)
|
|
242
|
+
|
|
243
|
+
# --- Scoring helpers (for envelope builder) -------------------------------
|
|
244
|
+
|
|
245
|
+
def get_seen_counts(self) -> dict[str, int]:
|
|
246
|
+
"""Return {fact_id: seen_count} for all facts."""
|
|
247
|
+
with self._lock:
|
|
248
|
+
return {fid: sf.seen_count for fid, sf in self._facts.items()}
|
|
249
|
+
|
|
250
|
+
def get_fact_window_indices(self) -> dict[str, int]:
|
|
251
|
+
"""Return {fact_id: creation_window_index} for scoring recency."""
|
|
252
|
+
with self._lock:
|
|
253
|
+
return {
|
|
254
|
+
fid: self._window_count - sf.age_in_windows
|
|
255
|
+
for fid, sf in self._facts.items()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# --- Event subscriptions --------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def on_fact_added(self, callback: Callable[[StateFact], None]) -> None:
|
|
261
|
+
self._on_fact_added.append(callback)
|
|
262
|
+
|
|
263
|
+
def on_fact_superseded(self, callback: Callable[[StateFact, str], None]) -> None:
|
|
264
|
+
self._on_fact_superseded.append(callback)
|
|
265
|
+
|
|
266
|
+
# --- Compaction check -----------------------------------------------------
|
|
267
|
+
|
|
268
|
+
def needs_compaction(self) -> bool:
|
|
269
|
+
"""Check if warm store exceeds compaction thresholds."""
|
|
270
|
+
return self.fact_count > self._config.compact_threshold
|
|
271
|
+
|
|
272
|
+
# --- Serialization --------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
def to_dict(self) -> dict[str, Any]:
|
|
275
|
+
"""Serialize entire warm store state."""
|
|
276
|
+
with self._lock:
|
|
277
|
+
return {
|
|
278
|
+
"schema_version": SCHEMA_VERSION,
|
|
279
|
+
"facts": {fid: sf.to_dict() for fid, sf in self._facts.items()},
|
|
280
|
+
"edges": [
|
|
281
|
+
{
|
|
282
|
+
"id": e.id,
|
|
283
|
+
"source_id": e.source_id,
|
|
284
|
+
"target_id": e.target_id,
|
|
285
|
+
"relation_type": e.relation_type.value if hasattr(e.relation_type, "value") else str(e.relation_type),
|
|
286
|
+
"confidence": e.confidence,
|
|
287
|
+
"source_stage": e.source_stage,
|
|
288
|
+
}
|
|
289
|
+
for e in self._graph.edges
|
|
290
|
+
],
|
|
291
|
+
"critical_state": self._critical.to_dict(),
|
|
292
|
+
"structural_state": self._structural.to_dict(),
|
|
293
|
+
"window_count": self._window_count,
|
|
294
|
+
"current_window_id": self._current_window_id,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def load_from_dict(self, data: dict[str, Any]) -> None:
|
|
298
|
+
"""Restore warm store from serialized dict."""
|
|
299
|
+
file_version = data.get("schema_version", 1)
|
|
300
|
+
if file_version > SCHEMA_VERSION:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"Session file schema v{file_version} is newer than "
|
|
303
|
+
f"supported v{SCHEMA_VERSION}. Upgrade CRP."
|
|
304
|
+
)
|
|
305
|
+
if file_version < SCHEMA_VERSION:
|
|
306
|
+
data = self._migrate(data, file_version)
|
|
307
|
+
with self._lock:
|
|
308
|
+
self._facts.clear()
|
|
309
|
+
self._graph = FactGraph()
|
|
310
|
+
self._fact_text_hashes.clear()
|
|
311
|
+
|
|
312
|
+
for _fid, fdata in data.get("facts", {}).items():
|
|
313
|
+
sf = StateFact.from_dict(fdata)
|
|
314
|
+
self._facts[sf.id] = sf
|
|
315
|
+
self._graph.add_fact(sf.fact)
|
|
316
|
+
# Rebuild content-dedup hashes (§audit2 STATE-C1)
|
|
317
|
+
text_hash = hashlib.sha256(sf.fact.text.encode()).hexdigest()
|
|
318
|
+
self._fact_text_hashes.add(text_hash)
|
|
319
|
+
|
|
320
|
+
for edata in data.get("edges", []):
|
|
321
|
+
edge = FactEdge(
|
|
322
|
+
id=edata.get("id", ""),
|
|
323
|
+
source_id=edata.get("source_id", ""),
|
|
324
|
+
target_id=edata.get("target_id", ""),
|
|
325
|
+
relation_type=edata.get("relation_type", "RELATED"),
|
|
326
|
+
confidence=edata.get("confidence", 0.0),
|
|
327
|
+
source_stage=edata.get("source_stage", 0),
|
|
328
|
+
)
|
|
329
|
+
self._graph.add_edge(edge)
|
|
330
|
+
|
|
331
|
+
self._critical = CriticalState.from_dict(data.get("critical_state", {}))
|
|
332
|
+
self._structural = StructuralState.from_dict(data.get("structural_state", {}))
|
|
333
|
+
self._window_count = data.get("window_count", 0)
|
|
334
|
+
self._current_window_id = data.get("current_window_id", "")
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def _migrate(data: dict[str, Any], from_version: int) -> dict[str, Any]:
|
|
338
|
+
"""Incrementally migrate session data from *from_version* to current (§audit L7)."""
|
|
339
|
+
d = dict(data)
|
|
340
|
+
if from_version < 2:
|
|
341
|
+
# v1 → v2: add schema_version, ensure edges have source_stage
|
|
342
|
+
d.setdefault("schema_version", 2)
|
|
343
|
+
for edata in d.get("edges", []):
|
|
344
|
+
edata.setdefault("source_stage", 0)
|
|
345
|
+
logger.info("Migrated session data v%d → v%d", from_version, SCHEMA_VERSION)
|
|
346
|
+
return d
|