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
crp/core/errors.py ADDED
@@ -0,0 +1,128 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """CRP error taxonomy — codes 1001-1031 per §6.8."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import IntEnum
8
+ from typing import Any
9
+
10
+
11
+ class ErrorCode(IntEnum):
12
+ """All CRP error codes."""
13
+
14
+ # Budget / session (1001-1005)
15
+ BUDGET_EXHAUSTED = 1001
16
+ RATE_LIMIT_EXCEEDED = 1002
17
+ SESSION_EXPIRED = 1003
18
+ SESSION_LIMIT_EXCEEDED = 1004
19
+ SESSION_CLOSED = 1005
20
+
21
+ # Validation / security (1010-1013)
22
+ VALIDATION_ERROR = 1010
23
+ SECURITY_INVARIANT_ERROR = 1011
24
+ SIGNATURE_INVALID = 1012
25
+ RBAC_DENIED = 1013
26
+
27
+ # Provider (1020-1021)
28
+ PROVIDER_ERROR = 1020
29
+ PROVIDER_TIMEOUT = 1021
30
+
31
+ # State integrity (1030-1031)
32
+ STATE_CORRUPTED = 1030
33
+ CHAIN_VERIFICATION_FAILED = 1031
34
+
35
+
36
+ class CRPError(Exception):
37
+ """Base exception for all CRP errors.
38
+
39
+ Each error carries a numeric code (1001-1031) per §6.8.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ code: int | ErrorCode,
45
+ message: str,
46
+ details: dict[str, Any] | None = None,
47
+ ) -> None:
48
+ self.code = int(code)
49
+ self.message = message
50
+ self.details: dict[str, Any] = details or {}
51
+ super().__init__(f"CRP-{self.code}: {message}")
52
+
53
+
54
+ class BudgetExhaustedError(CRPError):
55
+ """Any cost cap (windows, tokens, rate) exceeded."""
56
+
57
+ def __init__(self, message: str = "Budget exhausted", **details: Any) -> None:
58
+ super().__init__(ErrorCode.BUDGET_EXHAUSTED, message, details)
59
+
60
+
61
+ class RateLimitExceededError(CRPError):
62
+ """Per-session rate limit hit."""
63
+
64
+ def __init__(self, message: str = "Rate limit exceeded", **details: Any) -> None:
65
+ super().__init__(ErrorCode.RATE_LIMIT_EXCEEDED, message, details)
66
+
67
+
68
+ class SessionExpiredError(CRPError):
69
+ """Session timeout reached."""
70
+
71
+ def __init__(self, message: str = "Session expired", **details: Any) -> None:
72
+ super().__init__(ErrorCode.SESSION_EXPIRED, message, details)
73
+
74
+
75
+ class SessionClosedError(CRPError):
76
+ """Operation on closed session."""
77
+
78
+ def __init__(self, message: str = "Session is closed", **details: Any) -> None:
79
+ super().__init__(ErrorCode.SESSION_CLOSED, message, details)
80
+
81
+
82
+ class ValidationError(CRPError):
83
+ """Structural validation failure (§7.4)."""
84
+
85
+ def __init__(self, message: str = "Validation error", **details: Any) -> None:
86
+ super().__init__(ErrorCode.VALIDATION_ERROR, message, details)
87
+
88
+
89
+ class ProviderError(CRPError):
90
+ """LLM provider returned an error."""
91
+
92
+ def __init__(self, message: str = "Provider error", **details: Any) -> None:
93
+ super().__init__(ErrorCode.PROVIDER_ERROR, message, details)
94
+
95
+
96
+ class ProviderTimeoutError(CRPError):
97
+ """LLM provider did not respond within timeout."""
98
+
99
+ def __init__(self, message: str = "Provider timeout", **details: Any) -> None:
100
+ super().__init__(ErrorCode.PROVIDER_TIMEOUT, message, details)
101
+
102
+
103
+ class StateCorruptedError(CRPError):
104
+ """Cold state integrity verification failed."""
105
+
106
+ def __init__(self, message: str = "State corrupted", **details: Any) -> None:
107
+ super().__init__(ErrorCode.STATE_CORRUPTED, message, details)
108
+
109
+
110
+ class ChainVerificationFailedError(CRPError):
111
+ """Window DAG chain integrity check failed (§7.6)."""
112
+
113
+ def __init__(self, message: str = "Chain verification failed", **details: Any) -> None:
114
+ super().__init__(ErrorCode.CHAIN_VERIFICATION_FAILED, message, details)
115
+
116
+
117
+ class SecurityInvariantError(CRPError):
118
+ """CRP security invariant violated (§7.4)."""
119
+
120
+ def __init__(self, message: str = "Security invariant violated", **details: Any) -> None:
121
+ super().__init__(ErrorCode.SECURITY_INVARIANT_ERROR, message, details)
122
+
123
+
124
+ class SignatureInvalidError(CRPError):
125
+ """Cryptographic signature verification failed (§7.3)."""
126
+
127
+ def __init__(self, message: str = "Signature invalid", **details: Any) -> None:
128
+ super().__init__(ErrorCode.SIGNATURE_INVALID, message, details)
@@ -0,0 +1,384 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Extraction facade mixin — fact extraction and ingestion (§2.5).
4
+
5
+ Extracted from orchestrator.py for maintainability. This mixin provides
6
+ extraction and ingestion method implementations. The CRPOrchestrator
7
+ inherits from this class.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import re
14
+ import time
15
+ import uuid
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, TYPE_CHECKING
18
+
19
+ from crp.core.task_intent import TaskIntent
20
+
21
+ if TYPE_CHECKING:
22
+ from crp.extraction.types import ExtractionResult as PipelineExtractionResult
23
+
24
+ logger = logging.getLogger("crp.orchestrator")
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # ExtractionResult (for ingest)
29
+ # ---------------------------------------------------------------------------
30
+
31
+ @dataclass
32
+ class ExtractionResult:
33
+ """Result of zero-LLM ingestion."""
34
+ facts_extracted: int = 0
35
+ source_label: str = ""
36
+ fact_ids: list[str] = field(default_factory=list)
37
+
38
+
39
+ class ExtractionMixin:
40
+ """Mixin providing CRP extraction and ingestion methods.
41
+
42
+ Methods access orchestrator state via ``self`` (multiple inheritance).
43
+ """
44
+
45
+ def _extract_and_store(
46
+ self,
47
+ text: str,
48
+ source_window_id: str,
49
+ task_intent: TaskIntent | None = None,
50
+ ) -> PipelineExtractionResult:
51
+ """Run graduated extraction on text, store facts in WarmStore + CKF."""
52
+ from crp.extraction.quality_gate import run_quality_gate
53
+
54
+ result = self._extraction.extract(
55
+ text,
56
+ task_intent=task_intent,
57
+ source_window_id=source_window_id,
58
+ )
59
+
60
+ # Quality gate (3-tier validation)
61
+ result = run_quality_gate(
62
+ result,
63
+ history=list(self._extraction_history)[-10:] if self._extraction_history else None,
64
+ )
65
+ self._extraction_history.append(result)
66
+
67
+ # ── Output-side injection firewall (§7.5.2) ───────────
68
+ # Scan EACH extracted fact for injection patterns.
69
+ # If an LLM output contains injection payloads that survive
70
+ # extraction as verbatim text (e.g. key_sentence, list_item),
71
+ # penalize their confidence so they rank lower in envelope
72
+ # packing and are less likely to propagate to the next window.
73
+ if result.facts:
74
+ for fact in result.facts:
75
+ fact_report = self._injection_detector.scan(fact.text)
76
+ if fact_report.has_flags:
77
+ # Penalize confidence: 0.3× for high-confidence injection,
78
+ # 0.6× for lower-confidence matches
79
+ penalty = 0.3 if fact_report.highest_confidence >= 0.80 else 0.6
80
+ fact.confidence *= penalty
81
+ fact.flagged_confidence = True
82
+ fact.confidence_flag_reason = (
83
+ f"injection_in_fact:{fact_report.flags[0].pattern_name}"
84
+ f":{fact_report.highest_confidence:.2f}"
85
+ )
86
+ logger.warning(
87
+ "Output-side injection detected in fact %s: %s (conf=%.2f, "
88
+ "fact confidence penalized to %.2f)",
89
+ fact.id, fact_report.flags[0].pattern_name,
90
+ fact_report.highest_confidence, fact.confidence,
91
+ )
92
+
93
+ # Store in WarmStateStore (fact ranking, aging, seen tracking)
94
+ if result.facts:
95
+ self._warm_store.add_facts(result.facts, result.edges or None)
96
+
97
+ # Add facts to integrity chain (§7.2, §7.7)
98
+ if result.facts:
99
+ for fact in result.facts:
100
+ self._integrity_chain.add_fact(fact.id, fact.text)
101
+
102
+ # Store in CKF (4-mode retrieval: graph walk, pattern, semantic, community)
103
+ if result.facts:
104
+ self._ckf.store(result.facts, window_id=source_window_id)
105
+ if result.edges:
106
+ self._ckf.store_edges(result.edges)
107
+
108
+ # Store source passages for high-confidence facts (§17)
109
+ if result.facts:
110
+ from crp.advanced.source_grounding import SourcePassage
111
+ for fact in result.facts:
112
+ passage = SourcePassage(
113
+ passage_id=f"{source_window_id}:{fact.id}",
114
+ text=fact.text,
115
+ source_window=self._windows_completed,
116
+ linked_fact_ids=[fact.id],
117
+ relevance_score=fact.confidence,
118
+ )
119
+ self._source_grounding.store_passage(
120
+ passage=passage,
121
+ fact_confidence=fact.confidence,
122
+ )
123
+
124
+ # Auto-compact warm store if thresholds exceeded (§7.4)
125
+ from crp.state.compaction import should_compact, compact
126
+ if should_compact(self._warm_store):
127
+ try:
128
+ compact(
129
+ self._warm_store,
130
+ self._ckf._event_log,
131
+ source_window_id,
132
+ )
133
+ logger.debug("Auto-compaction complete")
134
+ except Exception as exc:
135
+ logger.debug("Compaction skipped: %s", exc)
136
+
137
+ return result
138
+
139
+ # ------------------------------------------------------------------
140
+ # Internal: envelope construction
141
+ # ------------------------------------------------------------------
142
+
143
+ def ingest_batch(
144
+ self,
145
+ texts: list[str],
146
+ task_intent: str = "",
147
+ ) -> list[int]:
148
+ """Batch ingest multiple texts, returning facts extracted per text (§6.6)."""
149
+ self._check_session()
150
+ results: list[int] = []
151
+ for text in texts:
152
+ try:
153
+ facts_count = self.ingest(text)
154
+ results.append(facts_count)
155
+ except Exception as exc:
156
+ results.append(0)
157
+ logger.warning("Batch ingest item failed: %s", exc)
158
+ return results
159
+
160
+ # ------------------------------------------------------------------
161
+ # Session operations
162
+ # ------------------------------------------------------------------
163
+
164
+ def ingest(
165
+ self,
166
+ raw_text: str,
167
+ source_label: str | None = None,
168
+ ) -> ExtractionResult:
169
+ """Ingest raw text without LLM invocation (§2.5).
170
+
171
+ Runs graduated extraction pipeline on raw_text, adds facts to
172
+ WarmStateStore and CKF. No LLM call, no window creation, no envelope.
173
+ """
174
+ with self._lock:
175
+ return self._ingest_locked(raw_text, source_label=source_label)
176
+
177
+ def _ingest_locked(
178
+ self,
179
+ raw_text: str,
180
+ source_label: str | None = None,
181
+ ) -> ExtractionResult:
182
+ """Internal ingest implementation — called under self._lock."""
183
+ # Set correlation ID for structured log tracing (§audit M10)
184
+ from crp.observability.structured_logging import new_correlation_id
185
+ cid = new_correlation_id()
186
+ logger.info("ingest started [correlation_id=%s, label=%s]", cid, source_label)
187
+
188
+ self._check_session()
189
+
190
+ # ---------- Input sanitization (§audit H10) ----------
191
+ max_ingest_bytes = self._config.get("max_ram_mb", 512) * 1024 * 1024
192
+ # Cap single ingest to 10 MB or 1% of memory budget, whichever is smaller
193
+ max_ingest_size = min(10 * 1024 * 1024, max_ingest_bytes // 100)
194
+ if len(raw_text) > max_ingest_size:
195
+ raise ValidationError(
196
+ f"Ingest text too large ({len(raw_text):,} chars, "
197
+ f"max {max_ingest_size:,})"
198
+ )
199
+
200
+ # ---------- Compliance imports (§7.14) ----------
201
+ from crp.security.audit_trail import ComplianceEventType
202
+ from crp.security.consent import ProcessingPurpose
203
+ from crp.security.privacy import DataClassification
204
+
205
+ # ---------- Compliance audit: ingest started (§7.14) ----------
206
+ label = source_label or "unnamed"
207
+ self._compliance_audit.record(
208
+ ComplianceEventType.DATA_INGESTED,
209
+ session_id=self._session.session_id,
210
+ data={"operation": "ingest", "phase": "started",
211
+ "source_label": label,
212
+ "input_length": len(raw_text)},
213
+ )
214
+
215
+ # ---------- Consent verification (§7.13) ----------
216
+ self._consent_manager.check_required(ProcessingPurpose.FACT_EXTRACTION)
217
+
218
+ # RBAC permission + rate limit check (§7.10)
219
+ from crp.security.rbac import Permission
220
+ perm_result = self._rbac.check_permission(Permission.INGEST)
221
+ if not perm_result.allowed:
222
+ self._compliance_audit.record(
223
+ ComplianceEventType.RBAC_DENIED,
224
+ session_id=self._session.session_id,
225
+ data={"operation": "ingest", "reason": perm_result.reason},
226
+ )
227
+ raise RateLimitExceededError(perm_result.reason)
228
+
229
+ if not raw_text:
230
+ return ExtractionResult(
231
+ facts_extracted=0,
232
+ source_label=source_label or "",
233
+ )
234
+
235
+ # Input validation — Layer 1, cannot disable (§7.4)
236
+ val_result = self._input_validator.validate(raw_text)
237
+ if not val_result.valid:
238
+ raise ValidationError(val_result.warnings[0] if val_result.warnings else "Input validation failed")
239
+ raw_text = val_result.sanitized_text
240
+
241
+ # Rate limit check with payload size
242
+ payload_bytes = len(raw_text.encode("utf-8"))
243
+ rate_result = self._rbac.check_rate_limit(Permission.INGEST, payload_bytes=payload_bytes)
244
+ if not rate_result.allowed:
245
+ self._compliance_audit.record(
246
+ ComplianceEventType.RATE_LIMIT_HIT,
247
+ session_id=self._session.session_id,
248
+ data={"operation": "ingest", "reason": rate_result.reason},
249
+ )
250
+ raise RateLimitExceededError(rate_result.reason)
251
+
252
+ source_window_id = f"ingest:{label}"
253
+
254
+ # ---------- PII scanning on ingested text (§7.12) ----------
255
+ pii_result = self._pii_scanner.scan(raw_text)
256
+ if pii_result.has_pii:
257
+ self._compliance_audit.record(
258
+ ComplianceEventType.PII_DETECTED,
259
+ session_id=self._session.session_id,
260
+ data={
261
+ "operation": "ingest",
262
+ "source_label": label,
263
+ "pii_types": sorted(pii_result.pii_types_found),
264
+ "detection_count": len(pii_result.detections),
265
+ "classification": pii_result.highest_classification.name,
266
+ },
267
+ )
268
+ if self._human_oversight.should_halt_on_pii():
269
+ self._human_oversight.record_halt(
270
+ "ingest", "PII detected in ingested text",
271
+ details={"pii_types": sorted(pii_result.pii_types_found)},
272
+ )
273
+
274
+ # Advisory injection scan on ingested text (§7.5.2)
275
+ ingest_report = self._injection_detector.scan(raw_text)
276
+ if ingest_report.has_flags:
277
+ logger.warning(
278
+ "Injection patterns detected in ingested text '%s': %d flags, "
279
+ "highest confidence=%.2f — facts will be quarantined + penalized",
280
+ label, len(ingest_report.flags), ingest_report.highest_confidence,
281
+ )
282
+ self._compliance_audit.record(
283
+ ComplianceEventType.INJECTION_DETECTED,
284
+ session_id=self._session.session_id,
285
+ data={
286
+ "operation": "ingest",
287
+ "source_label": label,
288
+ "flags_count": len(ingest_report.flags),
289
+ "highest_confidence": ingest_report.highest_confidence,
290
+ },
291
+ )
292
+ if self._human_oversight.should_halt_on_injection():
293
+ self._human_oversight.record_halt(
294
+ "ingest", "Injection detected in ingested text",
295
+ details={"flags_count": len(ingest_report.flags)},
296
+ )
297
+
298
+ # Run full graduated extraction pipeline
299
+ # (output-side injection firewall in _extract_and_store will
300
+ # penalize any facts that carry injection patterns)
301
+ pipeline_result = self._extract_and_store(raw_text, source_window_id)
302
+
303
+ # Quarantine ingested facts with 0.7× confidence penalty (§7.8)
304
+ if pipeline_result.facts:
305
+ window_id = f"ingest-q:{label}"
306
+ self._quarantine.quarantine_facts(
307
+ [(f.id, f.confidence, f.text) for f in pipeline_result.facts],
308
+ window_id=window_id,
309
+ source_label=label,
310
+ )
311
+
312
+ # ---------- Retention + lineage tracking for ingested facts (§7.12) ----------
313
+ _classification = (
314
+ pii_result.highest_classification if pii_result.has_pii
315
+ else DataClassification.INTERNAL
316
+ )
317
+ for fact in pipeline_result.facts:
318
+ self._retention_manager.register(
319
+ data_id=fact.id,
320
+ classification=_classification,
321
+ source_label=f"ingest:{label}",
322
+ )
323
+ self._lineage_tracker.record(
324
+ data_id=fact.id,
325
+ origin="ingest",
326
+ source_label=label,
327
+ classification=_classification,
328
+ )
329
+
330
+ # ---------- Processing record (§7.13 — GDPR Art. 30) ----------
331
+ self._processing_records.record(
332
+ purpose=ProcessingPurpose.FACT_EXTRACTION,
333
+ data_categories=["raw_text", "extracted_facts"],
334
+ legal_basis="legitimate_interest",
335
+ input_size_bytes=payload_bytes,
336
+ output_size_bytes=sum(len(f.text.encode("utf-8")) for f in pipeline_result.facts),
337
+ automated_decision=True,
338
+ human_oversight=False,
339
+ retention_period="session",
340
+ )
341
+
342
+ # Record ingest for rate limiting
343
+ self._rbac.record_ingest(payload_bytes)
344
+
345
+ # ---------- Compliance audit: ingest completed (§7.14) ----------
346
+ self._compliance_audit.record(
347
+ ComplianceEventType.DATA_INGESTED,
348
+ session_id=self._session.session_id,
349
+ data={
350
+ "operation": "ingest",
351
+ "phase": "completed",
352
+ "source_label": label,
353
+ "facts_extracted": pipeline_result.total_facts,
354
+ "quarantined": self._quarantine.quarantine_count,
355
+ "pii_detected": pii_result.has_pii,
356
+ "injection_detected": ingest_report.has_flags,
357
+ },
358
+ )
359
+
360
+ # ---------- Retention enforcement (§7.12) ----------
361
+ expired_ids = self._retention_manager.enforce()
362
+ if expired_ids:
363
+ self._compliance_audit.record(
364
+ ComplianceEventType.RETENTION_PURGED,
365
+ session_id=self._session.session_id,
366
+ data={"purged_count": len(expired_ids), "purged_ids": expired_ids[:10]},
367
+ )
368
+
369
+ logger.info(
370
+ "Ingested %d facts from source '%s' (stages=%s, quarantined=%d)",
371
+ pipeline_result.total_facts, label, pipeline_result.stages_run,
372
+ self._quarantine.quarantine_count,
373
+ )
374
+
375
+ return ExtractionResult(
376
+ facts_extracted=pipeline_result.total_facts,
377
+ source_label=label,
378
+ fact_ids=[f.id for f in pipeline_result.facts],
379
+ )
380
+
381
+ # ------------------------------------------------------------------
382
+ # State export (§2.5)
383
+ # ------------------------------------------------------------------
384
+