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,135 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Quality tier classification and reporting (§05, §10).
4
+
5
+ Each CRP session runs at a quality tier that tells the user how well
6
+ the context pipeline is performing:
7
+
8
+ S — all signals green, overhead < 5 %
9
+ A — minor gaps, overhead < 10 %
10
+ B — acceptable, overhead < 15 %
11
+ C — degraded, hierarchical processing needed
12
+ D — minimal / fallback mode
13
+
14
+ QualityReporter takes a handful of easily-computed metrics and maps
15
+ them to one of these tiers. It can also detect *degradation* (tier
16
+ dropping below a previous high-water mark).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from enum import Enum
23
+ from typing import Any
24
+
25
+
26
+ class QualityTier(Enum):
27
+ """CRP quality tiers (best → worst)."""
28
+
29
+ S = "S"
30
+ A = "A"
31
+ B = "B"
32
+ C = "C"
33
+ D = "D"
34
+
35
+ def __lt__(self, other: QualityTier) -> bool:
36
+ order = ["S", "A", "B", "C", "D"]
37
+ return order.index(self.value) < order.index(other.value)
38
+
39
+
40
+ # Threshold rules: (max_overhead_pct, max_fact_miss_pct, tier)
41
+ # Checked in order — first match wins.
42
+ _TIER_RULES: list[tuple[float, float, QualityTier]] = [
43
+ (5.0, 2.0, QualityTier.S),
44
+ (10.0, 10.0, QualityTier.A),
45
+ (15.0, 25.0, QualityTier.B),
46
+ (30.0, 50.0, QualityTier.C),
47
+ # Everything else → D
48
+ ]
49
+
50
+
51
+ @dataclass
52
+ class QualityReport:
53
+ """Result of a quality assessment."""
54
+
55
+ tier: QualityTier
56
+ overhead_pct: float
57
+ fact_miss_pct: float
58
+ degraded: bool = False # True if tier dropped below previous high
59
+ details: dict[str, Any] | None = None
60
+
61
+ def to_dict(self) -> dict[str, Any]:
62
+ return {
63
+ "tier": self.tier.value,
64
+ "overhead_pct": round(self.overhead_pct, 2),
65
+ "fact_miss_pct": round(self.fact_miss_pct, 2),
66
+ "degraded": self.degraded,
67
+ "details": self.details or {},
68
+ }
69
+
70
+
71
+ class QualityReporter:
72
+ """Classify and track quality tiers across a session.
73
+
74
+ Usage::
75
+
76
+ qr = QualityReporter()
77
+ report = qr.assess(overhead_pct=4.2, fact_miss_pct=1.0)
78
+ assert report.tier == QualityTier.S
79
+
80
+ # Later, if overhead rises:
81
+ report2 = qr.assess(overhead_pct=18.0, fact_miss_pct=30.0)
82
+ assert report2.tier == QualityTier.C
83
+ assert report2.degraded # dropped from S → C
84
+ """
85
+
86
+ def __init__(self) -> None:
87
+ self._high_water: QualityTier | None = None
88
+ self._history: list[QualityReport] = []
89
+
90
+ def assess(
91
+ self,
92
+ overhead_pct: float,
93
+ fact_miss_pct: float,
94
+ details: dict[str, Any] | None = None,
95
+ ) -> QualityReport:
96
+ """Compute the current quality tier and check for degradation.
97
+
98
+ Args:
99
+ overhead_pct: Current overhead as a percentage (0-100).
100
+ fact_miss_pct: Percentage of available facts *not* included
101
+ in the envelope (0-100).
102
+ details: Optional opaque dict attached to the report.
103
+ """
104
+ tier = self._classify(overhead_pct, fact_miss_pct)
105
+ degraded = self._high_water is not None and self._high_water < tier
106
+
107
+ # Update high-water mark.
108
+ if self._high_water is None or tier < self._high_water:
109
+ self._high_water = tier
110
+
111
+ report = QualityReport(
112
+ tier=tier,
113
+ overhead_pct=overhead_pct,
114
+ fact_miss_pct=fact_miss_pct,
115
+ degraded=degraded,
116
+ details=details,
117
+ )
118
+ self._history.append(report)
119
+ return report
120
+
121
+ @property
122
+ def current_tier(self) -> QualityTier | None:
123
+ """Most recently assessed tier, or ``None`` if never assessed."""
124
+ return self._history[-1].tier if self._history else None
125
+
126
+ @property
127
+ def history(self) -> list[QualityReport]:
128
+ return list(self._history)
129
+
130
+ @staticmethod
131
+ def _classify(overhead_pct: float, fact_miss_pct: float) -> QualityTier:
132
+ for max_oh, max_fm, tier in _TIER_RULES:
133
+ if overhead_pct <= max_oh and fact_miss_pct <= max_fm:
134
+ return tier
135
+ return QualityTier.D
@@ -0,0 +1,81 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Structured logging with correlation IDs for CRP pipeline (§audit H9).
4
+
5
+ Provides:
6
+ - Per-request correlation IDs propagated across dispatch pipeline stages
7
+ - JSON-structured log formatter for machine-parseable logs
8
+ - Context-local storage for request metadata
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import contextvars
14
+ import json
15
+ import logging
16
+ import time
17
+ import uuid
18
+ from typing import Any
19
+
20
+ # Context variable for correlation ID — propagated across async boundaries
21
+ _correlation_id: contextvars.ContextVar[str] = contextvars.ContextVar(
22
+ "crp_correlation_id", default=""
23
+ )
24
+ _session_id: contextvars.ContextVar[str] = contextvars.ContextVar(
25
+ "crp_session_id", default=""
26
+ )
27
+
28
+
29
+ def new_correlation_id() -> str:
30
+ """Generate and set a new correlation ID for the current context."""
31
+ cid = uuid.uuid4().hex[:12]
32
+ _correlation_id.set(cid)
33
+ return cid
34
+
35
+
36
+ def get_correlation_id() -> str:
37
+ """Get the current correlation ID (empty string if none set)."""
38
+ return _correlation_id.get()
39
+
40
+
41
+ def set_session_context(session_id: str) -> None:
42
+ """Set the session ID for the current logging context."""
43
+ _session_id.set(session_id)
44
+
45
+
46
+ class StructuredFormatter(logging.Formatter):
47
+ """JSON log formatter with automatic correlation ID injection."""
48
+
49
+ def format(self, record: logging.LogRecord) -> str:
50
+ entry: dict[str, Any] = {
51
+ "ts": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(record.created)),
52
+ "level": record.levelname,
53
+ "logger": record.name,
54
+ "msg": record.getMessage(),
55
+ }
56
+
57
+ cid = _correlation_id.get()
58
+ if cid:
59
+ entry["correlation_id"] = cid
60
+
61
+ sid = _session_id.get()
62
+ if sid:
63
+ entry["session_id"] = sid
64
+
65
+ if record.exc_info and record.exc_info[1]:
66
+ entry["exception"] = str(record.exc_info[1])
67
+
68
+ return json.dumps(entry, separators=(",", ":"))
69
+
70
+
71
+ def configure_structured_logging(level: int = logging.INFO) -> None:
72
+ """Configure the root 'crp' logger with structured JSON output.
73
+
74
+ Call once at application startup for machine-parseable logs.
75
+ """
76
+ root_logger = logging.getLogger("crp")
77
+ if not root_logger.handlers:
78
+ handler = logging.StreamHandler()
79
+ handler.setFormatter(StructuredFormatter())
80
+ root_logger.addHandler(handler)
81
+ root_logger.setLevel(level)
@@ -0,0 +1,117 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Per-window telemetry writer — ``telemetry.jsonl`` output (§8.9).
4
+
5
+ TelemetryWriter appends one JSON object per line for every completed
6
+ window. The JSONL file can be consumed by downstream analytics,
7
+ dashboards, or the AuditLog.
8
+
9
+ Design goals:
10
+ * One line per window → easy to ``grep``, ``jq``, or stream-parse.
11
+ * Flush after every write so no data is lost on crash.
12
+ * Works with file paths or any writable file-like object.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import atexit
18
+ import json
19
+ import os
20
+ import time
21
+ from dataclasses import asdict, dataclass, field
22
+ from typing import IO, Any
23
+
24
+
25
+ @dataclass
26
+ class WindowTelemetry:
27
+ """Telemetry record for a single completed window.
28
+
29
+ Fields mirror the most useful per-window stats. Extra fields can
30
+ be added via ``extra``.
31
+ """
32
+
33
+ session_id: str
34
+ window_id: str
35
+ timestamp: float = field(default_factory=time.time)
36
+ input_tokens: int = 0
37
+ output_tokens: int = 0
38
+ overhead_tokens: int = 0
39
+ facts_included: int = 0
40
+ facts_available: int = 0
41
+ latency_ms: float = 0.0
42
+ quality_tier: str = ""
43
+ extra: dict[str, Any] = field(default_factory=dict)
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ d = asdict(self)
47
+ # Flatten extra into the top level for ease of querying.
48
+ extra = d.pop("extra", {})
49
+ d.update(extra)
50
+ return d
51
+
52
+
53
+ class TelemetryWriter:
54
+ """Append-only JSONL writer for per-window telemetry.
55
+
56
+ Usage::
57
+
58
+ tw = TelemetryWriter("telemetry.jsonl")
59
+ tw.write(WindowTelemetry(session_id="abc", window_id="w0", ...))
60
+ tw.close()
61
+
62
+ Also works as a context manager::
63
+
64
+ with TelemetryWriter("telemetry.jsonl") as tw:
65
+ tw.write(record)
66
+ """
67
+
68
+ def __init__(self, path_or_file: str | IO[str]) -> None:
69
+ if isinstance(path_or_file, str):
70
+ # Ensure parent directory exists.
71
+ parent = os.path.dirname(path_or_file)
72
+ if parent:
73
+ os.makedirs(parent, exist_ok=True)
74
+ self._file: IO[str] = open(path_or_file, "a", encoding="utf-8") # noqa: SIM115
75
+ self._owns_file = True
76
+ else:
77
+ self._file = path_or_file
78
+ self._owns_file = False
79
+ self._count = 0
80
+ self._closed = False
81
+ # Ensure close on interpreter shutdown (§audit H3)
82
+ atexit.register(self.close)
83
+
84
+ # -- writing ----------------------------------------------------------
85
+
86
+ def write(self, record: WindowTelemetry) -> None:
87
+ """Serialize *record* and append one JSON line. Flushes immediately."""
88
+ line = json.dumps(record.to_dict(), separators=(",", ":"))
89
+ self._file.write(line + "\n")
90
+ self._file.flush()
91
+ self._count += 1
92
+
93
+ @property
94
+ def records_written(self) -> int:
95
+ return self._count
96
+
97
+ # -- lifecycle --------------------------------------------------------
98
+
99
+ def close(self) -> None:
100
+ if self._closed:
101
+ return
102
+ self._closed = True
103
+ if self._owns_file:
104
+ self._file.close()
105
+
106
+ def __del__(self) -> None:
107
+ """Safety net: close file if not already closed (§audit H3)."""
108
+ try:
109
+ self.close()
110
+ except Exception: # noqa: BLE001
111
+ pass
112
+
113
+ def __enter__(self) -> TelemetryWriter:
114
+ return self
115
+
116
+ def __exit__(self, *exc: Any) -> None:
117
+ self.close()
@@ -0,0 +1,314 @@
1
+ # Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
2
+ # Licensed under Elastic License 2.0 — see LICENSE.md for details.
3
+ """Decision Provenance Engine (DPE) — §7.14.3.
4
+
5
+ Orchestrates claim detection, attribution scoring, provenance chain
6
+ construction, and report generation for every LLM dispatch window.
7
+
8
+ Usage::
9
+
10
+ from crp.provenance import DecisionProvenanceEngine, ProvenanceConfig
11
+
12
+ dpe = DecisionProvenanceEngine(config=ProvenanceConfig())
13
+ report = dpe.analyse(
14
+ output_text="The server uses AES-256 encryption...",
15
+ packed_facts=envelope_result.packed_facts,
16
+ session_id=session.id,
17
+ window_id=window.id,
18
+ )
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import time
24
+ from collections.abc import Sequence
25
+
26
+ from crp.envelope.packer import PackedFact
27
+
28
+ from ._types import (
29
+ AttributionType,
30
+ ClaimAttribution,
31
+ ClaimType,
32
+ EntailmentResult,
33
+ FactScore,
34
+ FidelityReport,
35
+ HallucinationRiskReport,
36
+ OmissionSeverity,
37
+ ProvenanceChain,
38
+ ProvenanceConfig,
39
+ ProvenanceLink,
40
+ ProvenanceReport,
41
+ )
42
+ from .attribution_scorer import score_all_claims
43
+ from .claim_detector import DetectedClaim, detect_claims
44
+ from .contradiction_detector import detect_contradictions
45
+ from .distortion_detector import detect_distortions
46
+ from .entailment_verifier import verify_entailment
47
+ from .fabrication_detector import detect_fabrications
48
+ from .hallucination_scorer import score_hallucination_risk
49
+ from .omission_analyzer import analyze_omissions
50
+ from .provenance_chain import build_all_chains, enrich_fact_metadata
51
+ from .report_generator import generate_json_report, generate_markdown_report
52
+
53
+
54
+ __all__ = [
55
+ "DecisionProvenanceEngine",
56
+ "ProvenanceConfig",
57
+ "ProvenanceReport",
58
+ "FidelityReport",
59
+ "ClaimAttribution",
60
+ "ClaimType",
61
+ "AttributionType",
62
+ "FactScore",
63
+ "ProvenanceChain",
64
+ "ProvenanceLink",
65
+ "DetectedClaim",
66
+ "detect_claims",
67
+ "score_all_claims",
68
+ "build_all_chains",
69
+ "enrich_fact_metadata",
70
+ "generate_markdown_report",
71
+ "generate_json_report",
72
+ "detect_distortions",
73
+ "detect_fabrications",
74
+ "analyze_omissions",
75
+ "detect_contradictions",
76
+ "verify_entailment",
77
+ "score_hallucination_risk",
78
+ "EntailmentResult",
79
+ "HallucinationRiskReport",
80
+ ]
81
+
82
+
83
+ class DecisionProvenanceEngine:
84
+ """Main entry point for the Decision Provenance Engine.
85
+
86
+ Ties together claim detection → attribution scoring → provenance chain
87
+ construction → report generation into a single ``analyse()`` call.
88
+ """
89
+
90
+ def __init__(self, *, config: ProvenanceConfig | None = None) -> None:
91
+ self._config = config or ProvenanceConfig()
92
+
93
+ # -- Properties ---------------------------------------------------------
94
+
95
+ @property
96
+ def config(self) -> ProvenanceConfig:
97
+ return self._config
98
+
99
+ @property
100
+ def enabled(self) -> bool:
101
+ return self._config.enabled
102
+
103
+ # -- Main pipeline ------------------------------------------------------
104
+
105
+ def analyse(
106
+ self,
107
+ output_text: str,
108
+ packed_facts: Sequence[PackedFact],
109
+ *,
110
+ session_id: str = "",
111
+ window_id: str = "",
112
+ envelope_saturation: float = 0.0,
113
+ task_input_preview: str = "",
114
+ fact_metadata: dict[str, dict[str, object]] | None = None,
115
+ ) -> ProvenanceReport:
116
+ """Run the full DPE pipeline and return a provenance report.
117
+
118
+ Args:
119
+ output_text: Raw LLM output text for this window.
120
+ packed_facts: Facts that were packed into the envelope.
121
+ session_id: Current CRP session ID.
122
+ window_id: Current dispatch window ID.
123
+ envelope_saturation: Envelope token saturation ratio (0.0-1.0).
124
+ task_input_preview: First 120 chars of the task input.
125
+ fact_metadata: Optional mapping of fact_id → metadata dict
126
+ containing ``source_window_id`` and
127
+ ``extraction_stage`` for chain enrichment.
128
+
129
+ Returns:
130
+ ProvenanceReport with attribution data, chains, and counts.
131
+ """
132
+ if not self._config.enabled:
133
+ return ProvenanceReport(
134
+ session_id=session_id,
135
+ window_id=window_id,
136
+ timestamp=time.time(),
137
+ )
138
+
139
+ facts_list = list(packed_facts)
140
+
141
+ # Step 1: Detect claims
142
+ claims = detect_claims(
143
+ output_text,
144
+ min_length=self._config.min_claim_length,
145
+ max_claims=self._config.max_claims_per_output,
146
+ )
147
+
148
+ # Step 2: Score claims against facts
149
+ attributions = score_all_claims(claims, facts_list, config=self._config)
150
+
151
+ # Step 3: Enrich with fact metadata (if available)
152
+ if fact_metadata:
153
+ enrich_fact_metadata(attributions, fact_metadata)
154
+
155
+ # Step 4: Build provenance chains
156
+ chains = build_all_chains(
157
+ attributions,
158
+ session_id=session_id,
159
+ window_id=window_id,
160
+ envelope_saturation=envelope_saturation,
161
+ envelope_facts_included=len(facts_list),
162
+ task_input_preview=task_input_preview,
163
+ )
164
+
165
+ # Step 5: Fidelity verification
166
+ distortions = detect_distortions(attributions, facts_list)
167
+ omissions = analyze_omissions(attributions, facts_list)
168
+ fabrications = detect_fabrications(attributions, facts_list)
169
+ contradictions = detect_contradictions(attributions)
170
+
171
+ critical_omissions = sum(
172
+ 1 for o in omissions if o.severity == OmissionSeverity.CRITICAL
173
+ )
174
+
175
+ # Composite fidelity score: start at 1.0, deduct for issues
176
+ # Normalize penalties by total claims to avoid score collapse on
177
+ # long outputs. With 30 claims, 3 distortions should not
178
+ # obliterate the score the same way it would for 3 claims.
179
+ _total = max(len(attributions), 1)
180
+ fidelity_score = 1.0
181
+ fidelity_score -= (len(distortions) / _total) * 0.40
182
+ fidelity_score -= (len(fabrications) / _total) * 0.35
183
+ fidelity_score -= (critical_omissions / _total) * 0.25
184
+ fidelity_score -= (len(contradictions) / _total) * 0.50
185
+ fidelity_score = max(0.0, round(fidelity_score, 4))
186
+
187
+ fidelity = FidelityReport(
188
+ distortions=distortions,
189
+ fabrications=fabrications,
190
+ omissions=omissions,
191
+ contradictions=contradictions,
192
+ distortion_count=len(distortions),
193
+ fabrication_count=len(fabrications),
194
+ critical_omission_count=critical_omissions,
195
+ contradiction_count=len(contradictions),
196
+ fidelity_score=fidelity_score,
197
+ )
198
+
199
+ # Step 6: Semantic entailment verification
200
+ entailment_results: list[EntailmentResult] = []
201
+ if self._config.entailment_enabled:
202
+ entailment_results = verify_entailment(
203
+ attributions, facts_list, config=self._config,
204
+ )
205
+
206
+ # Step 7: Hallucination risk scoring
207
+ risk_report: HallucinationRiskReport | None = None
208
+ if self._config.risk_scoring_enabled:
209
+ risk_report = score_hallucination_risk(
210
+ attributions,
211
+ fidelity=fidelity,
212
+ entailment_results=entailment_results,
213
+ config=self._config,
214
+ )
215
+
216
+ # Step 8: Compute counts and build report
217
+ report = self._build_report(
218
+ claims=claims,
219
+ attributions=attributions,
220
+ chains=chains,
221
+ session_id=session_id,
222
+ window_id=window_id,
223
+ output_text=output_text,
224
+ facts_count=len(facts_list),
225
+ fidelity=fidelity,
226
+ entailment_results=entailment_results,
227
+ risk_report=risk_report,
228
+ )
229
+
230
+ return report
231
+
232
+ # -- Report assembly ----------------------------------------------------
233
+
234
+ def _build_report(
235
+ self,
236
+ *,
237
+ claims: list[DetectedClaim],
238
+ attributions: list[ClaimAttribution],
239
+ chains: list[ProvenanceChain],
240
+ session_id: str,
241
+ window_id: str,
242
+ output_text: str,
243
+ facts_count: int,
244
+ fidelity: FidelityReport | None = None,
245
+ entailment_results: list[EntailmentResult] | None = None,
246
+ risk_report: HallucinationRiskReport | None = None,
247
+ ) -> ProvenanceReport:
248
+ """Assemble counts and build the ProvenanceReport."""
249
+ # Claim type counts
250
+ factual = sum(1 for c in claims if c.claim_type == ClaimType.FACTUAL_CLAIM)
251
+ opinion = sum(1 for c in claims if c.claim_type == ClaimType.OPINION)
252
+ procedural = sum(1 for c in claims if c.claim_type == ClaimType.PROCEDURAL)
253
+ hedge = sum(1 for c in claims if c.claim_type == ClaimType.HEDGE)
254
+ connective = sum(1 for c in claims if c.claim_type == ClaimType.CONNECTIVE)
255
+
256
+ # Attribution type counts
257
+ grounded = sum(
258
+ 1 for a in attributions
259
+ if a.attribution_type == AttributionType.CONTEXT_GROUNDED
260
+ )
261
+ parametric = sum(
262
+ 1 for a in attributions
263
+ if a.attribution_type == AttributionType.PARAMETRIC
264
+ )
265
+ mixed = sum(
266
+ 1 for a in attributions
267
+ if a.attribution_type == AttributionType.MIXED
268
+ )
269
+ uncertain = sum(
270
+ 1 for a in attributions
271
+ if a.attribution_type == AttributionType.UNCERTAIN
272
+ )
273
+
274
+ # Grounding ratio: context-grounded factual claims / total factual claims
275
+ scorable = sum(
276
+ 1 for a in attributions
277
+ if a.claim_type in (ClaimType.FACTUAL_CLAIM, ClaimType.HEDGE)
278
+ )
279
+ grounding_ratio = grounded / scorable if scorable > 0 else 0.0
280
+
281
+ return ProvenanceReport(
282
+ session_id=session_id,
283
+ window_id=window_id,
284
+ timestamp=time.time(),
285
+ total_claims=len(claims),
286
+ factual_claims=factual,
287
+ opinion_claims=opinion,
288
+ procedural_claims=procedural,
289
+ hedge_claims=hedge,
290
+ connective_claims=connective,
291
+ context_grounded_count=grounded,
292
+ parametric_count=parametric,
293
+ mixed_count=mixed,
294
+ uncertain_count=uncertain,
295
+ grounding_ratio=round(grounding_ratio, 4),
296
+ attributions=attributions,
297
+ chains=chains,
298
+ chain_verified=True,
299
+ output_token_count=len(output_text.split()),
300
+ envelope_facts_count=facts_count,
301
+ fidelity=fidelity,
302
+ entailment_results=entailment_results or [],
303
+ risk_report=risk_report,
304
+ )
305
+
306
+ # -- Report formatting helpers ------------------------------------------
307
+
308
+ def generate_markdown(self, report: ProvenanceReport) -> str:
309
+ """Generate a human-readable markdown provenance report."""
310
+ return generate_markdown_report(report)
311
+
312
+ def generate_json(self, report: ProvenanceReport) -> dict:
313
+ """Generate a machine-readable JSON provenance report."""
314
+ return generate_json_report(report)