spanforge 1.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 (174) hide show
  1. spanforge/__init__.py +815 -0
  2. spanforge/_ansi.py +93 -0
  3. spanforge/_batch_exporter.py +409 -0
  4. spanforge/_cli.py +2094 -0
  5. spanforge/_cli_audit.py +639 -0
  6. spanforge/_cli_compliance.py +711 -0
  7. spanforge/_cli_cost.py +243 -0
  8. spanforge/_cli_ops.py +791 -0
  9. spanforge/_cli_phase11.py +356 -0
  10. spanforge/_hooks.py +337 -0
  11. spanforge/_server.py +1708 -0
  12. spanforge/_span.py +1036 -0
  13. spanforge/_store.py +288 -0
  14. spanforge/_stream.py +664 -0
  15. spanforge/_trace.py +335 -0
  16. spanforge/_tracer.py +254 -0
  17. spanforge/actor.py +141 -0
  18. spanforge/alerts.py +469 -0
  19. spanforge/auto.py +464 -0
  20. spanforge/baseline.py +335 -0
  21. spanforge/cache.py +635 -0
  22. spanforge/compliance.py +325 -0
  23. spanforge/config.py +532 -0
  24. spanforge/consent.py +228 -0
  25. spanforge/consumer.py +377 -0
  26. spanforge/core/__init__.py +5 -0
  27. spanforge/core/compliance_mapping.py +1254 -0
  28. spanforge/cost.py +600 -0
  29. spanforge/debug.py +548 -0
  30. spanforge/deprecations.py +205 -0
  31. spanforge/drift.py +482 -0
  32. spanforge/egress.py +58 -0
  33. spanforge/eval.py +648 -0
  34. spanforge/event.py +1064 -0
  35. spanforge/exceptions.py +240 -0
  36. spanforge/explain.py +178 -0
  37. spanforge/export/__init__.py +69 -0
  38. spanforge/export/append_only.py +337 -0
  39. spanforge/export/cloud.py +357 -0
  40. spanforge/export/datadog.py +497 -0
  41. spanforge/export/grafana.py +320 -0
  42. spanforge/export/jsonl.py +195 -0
  43. spanforge/export/openinference.py +158 -0
  44. spanforge/export/otel_bridge.py +294 -0
  45. spanforge/export/otlp.py +811 -0
  46. spanforge/export/otlp_bridge.py +233 -0
  47. spanforge/export/redis_backend.py +282 -0
  48. spanforge/export/siem_schema.py +98 -0
  49. spanforge/export/siem_splunk.py +264 -0
  50. spanforge/export/siem_syslog.py +212 -0
  51. spanforge/export/webhook.py +299 -0
  52. spanforge/exporters/__init__.py +30 -0
  53. spanforge/exporters/console.py +271 -0
  54. spanforge/exporters/jsonl.py +144 -0
  55. spanforge/exporters/sqlite.py +142 -0
  56. spanforge/gate.py +1150 -0
  57. spanforge/governance.py +181 -0
  58. spanforge/hitl.py +295 -0
  59. spanforge/http.py +187 -0
  60. spanforge/inspect.py +427 -0
  61. spanforge/integrations/__init__.py +45 -0
  62. spanforge/integrations/_pricing.py +280 -0
  63. spanforge/integrations/anthropic.py +388 -0
  64. spanforge/integrations/azure_openai.py +133 -0
  65. spanforge/integrations/bedrock.py +292 -0
  66. spanforge/integrations/crewai.py +251 -0
  67. spanforge/integrations/gemini.py +351 -0
  68. spanforge/integrations/groq.py +442 -0
  69. spanforge/integrations/langchain.py +349 -0
  70. spanforge/integrations/langgraph.py +306 -0
  71. spanforge/integrations/llamaindex.py +373 -0
  72. spanforge/integrations/ollama.py +287 -0
  73. spanforge/integrations/openai.py +368 -0
  74. spanforge/integrations/together.py +483 -0
  75. spanforge/io.py +214 -0
  76. spanforge/lint.py +322 -0
  77. spanforge/metrics.py +417 -0
  78. spanforge/metrics_export.py +343 -0
  79. spanforge/migrate.py +402 -0
  80. spanforge/model_registry.py +278 -0
  81. spanforge/models.py +389 -0
  82. spanforge/namespaces/__init__.py +254 -0
  83. spanforge/namespaces/audit.py +256 -0
  84. spanforge/namespaces/cache.py +237 -0
  85. spanforge/namespaces/chain.py +77 -0
  86. spanforge/namespaces/confidence.py +72 -0
  87. spanforge/namespaces/consent.py +92 -0
  88. spanforge/namespaces/cost.py +179 -0
  89. spanforge/namespaces/decision.py +143 -0
  90. spanforge/namespaces/diff.py +157 -0
  91. spanforge/namespaces/drift.py +80 -0
  92. spanforge/namespaces/eval_.py +251 -0
  93. spanforge/namespaces/feedback.py +241 -0
  94. spanforge/namespaces/fence.py +193 -0
  95. spanforge/namespaces/guard.py +105 -0
  96. spanforge/namespaces/hitl.py +91 -0
  97. spanforge/namespaces/latency.py +72 -0
  98. spanforge/namespaces/prompt.py +190 -0
  99. spanforge/namespaces/redact.py +173 -0
  100. spanforge/namespaces/retrieval.py +379 -0
  101. spanforge/namespaces/runtime_governance.py +494 -0
  102. spanforge/namespaces/template.py +208 -0
  103. spanforge/namespaces/tool_call.py +77 -0
  104. spanforge/namespaces/trace.py +1029 -0
  105. spanforge/normalizer.py +171 -0
  106. spanforge/plugins.py +82 -0
  107. spanforge/presidio_backend.py +349 -0
  108. spanforge/processor.py +258 -0
  109. spanforge/prompt_registry.py +418 -0
  110. spanforge/py.typed +0 -0
  111. spanforge/redact.py +914 -0
  112. spanforge/regression.py +192 -0
  113. spanforge/runtime_policy.py +159 -0
  114. spanforge/sampling.py +511 -0
  115. spanforge/schema.py +183 -0
  116. spanforge/schemas/v1.0/schema.json +170 -0
  117. spanforge/schemas/v2.0/schema.json +536 -0
  118. spanforge/sdk/__init__.py +625 -0
  119. spanforge/sdk/_base.py +584 -0
  120. spanforge/sdk/_base.pyi +71 -0
  121. spanforge/sdk/_exceptions.py +1096 -0
  122. spanforge/sdk/_types.py +2184 -0
  123. spanforge/sdk/alert.py +1514 -0
  124. spanforge/sdk/alert.pyi +56 -0
  125. spanforge/sdk/audit.py +1196 -0
  126. spanforge/sdk/audit.pyi +67 -0
  127. spanforge/sdk/cec.py +1215 -0
  128. spanforge/sdk/cec.pyi +37 -0
  129. spanforge/sdk/config.py +641 -0
  130. spanforge/sdk/config.pyi +55 -0
  131. spanforge/sdk/enterprise.py +714 -0
  132. spanforge/sdk/enterprise.pyi +79 -0
  133. spanforge/sdk/explain.py +170 -0
  134. spanforge/sdk/fallback.py +432 -0
  135. spanforge/sdk/feedback.py +351 -0
  136. spanforge/sdk/gate.py +874 -0
  137. spanforge/sdk/gate.pyi +51 -0
  138. spanforge/sdk/identity.py +2114 -0
  139. spanforge/sdk/identity.pyi +47 -0
  140. spanforge/sdk/lineage.py +175 -0
  141. spanforge/sdk/observe.py +1065 -0
  142. spanforge/sdk/observe.pyi +50 -0
  143. spanforge/sdk/operator.py +338 -0
  144. spanforge/sdk/pii.py +1473 -0
  145. spanforge/sdk/pii.pyi +119 -0
  146. spanforge/sdk/pipelines.py +458 -0
  147. spanforge/sdk/pipelines.pyi +39 -0
  148. spanforge/sdk/policy.py +930 -0
  149. spanforge/sdk/rag.py +594 -0
  150. spanforge/sdk/rbac.py +280 -0
  151. spanforge/sdk/registry.py +430 -0
  152. spanforge/sdk/registry.pyi +46 -0
  153. spanforge/sdk/scope.py +279 -0
  154. spanforge/sdk/secrets.py +293 -0
  155. spanforge/sdk/secrets.pyi +25 -0
  156. spanforge/sdk/security.py +560 -0
  157. spanforge/sdk/security.pyi +57 -0
  158. spanforge/sdk/trust.py +472 -0
  159. spanforge/sdk/trust.pyi +41 -0
  160. spanforge/secrets.py +799 -0
  161. spanforge/signing.py +1179 -0
  162. spanforge/stats.py +100 -0
  163. spanforge/stream.py +560 -0
  164. spanforge/testing.py +378 -0
  165. spanforge/testing_mocks.py +1052 -0
  166. spanforge/trace.py +199 -0
  167. spanforge/types.py +696 -0
  168. spanforge/ulid.py +300 -0
  169. spanforge/validate.py +379 -0
  170. spanforge-1.0.0.dist-info/METADATA +1509 -0
  171. spanforge-1.0.0.dist-info/RECORD +174 -0
  172. spanforge-1.0.0.dist-info/WHEEL +4 -0
  173. spanforge-1.0.0.dist-info/entry_points.txt +5 -0
  174. spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
spanforge/sdk/pii.pyi ADDED
@@ -0,0 +1,119 @@
1
+ """Type stubs for spanforge.sdk.pii (DX-001)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from spanforge.event import Event
10
+ from spanforge.pii import Redactable, RedactionPolicy
11
+ from spanforge.sdk._base import SFClientConfig, SFServiceClient
12
+ from spanforge.sdk._types import (
13
+ DSARExport,
14
+ ErasureReceipt,
15
+ PIIAnonymisedResult,
16
+ PIIHeatMapEntry,
17
+ PIIPipelineResult,
18
+ PIIStatusInfo,
19
+ PIITextScanResult,
20
+ SafeHarborResult,
21
+ SFPIIAnonymizeResult,
22
+ SFPIIRedactResult,
23
+ SFPIIScanResult,
24
+ TrainingDataPIIReport,
25
+ )
26
+
27
+ class SFPIIClient(SFServiceClient):
28
+ def __init__(self, config: SFClientConfig) -> None: ...
29
+ def scan(
30
+ self,
31
+ payload: dict[str, Any],
32
+ *,
33
+ extra_patterns: dict[str, re.Pattern[str]] | None = None,
34
+ max_depth: int = 10,
35
+ ) -> SFPIIScanResult: ...
36
+ def redact(
37
+ self,
38
+ event: Event,
39
+ *,
40
+ policy: RedactionPolicy | None = None,
41
+ ) -> SFPIIRedactResult: ...
42
+ def contains_pii(
43
+ self,
44
+ event: Event,
45
+ *,
46
+ scan_raw: bool = True,
47
+ ) -> bool: ...
48
+ def assert_redacted(
49
+ self,
50
+ event: Event,
51
+ *,
52
+ context: str = "",
53
+ scan_raw: bool = True,
54
+ ) -> None: ...
55
+ def anonymize(
56
+ self,
57
+ text: str,
58
+ *,
59
+ extra_patterns: dict[str, re.Pattern[str]] | None = None,
60
+ ) -> SFPIIAnonymizeResult: ...
61
+ def wrap(
62
+ self,
63
+ value: object,
64
+ sensitivity: str,
65
+ pii_types: frozenset[str] = ...,
66
+ ) -> Redactable: ...
67
+ def make_policy(
68
+ self,
69
+ *,
70
+ min_sensitivity: str = "pii",
71
+ redacted_by: str = "policy:sf-pii",
72
+ replacement_template: str = "[REDACTED:{sensitivity}]",
73
+ ) -> RedactionPolicy: ...
74
+ def scan_text(
75
+ self,
76
+ text: str,
77
+ *,
78
+ language: str = "en",
79
+ score_threshold: float = 0.5,
80
+ ) -> PIITextScanResult: ...
81
+ def anonymise(
82
+ self,
83
+ payload: dict[str, Any],
84
+ *,
85
+ max_depth: int = 10,
86
+ ) -> PIIAnonymisedResult: ...
87
+ def scan_batch(
88
+ self,
89
+ texts: list[str],
90
+ *,
91
+ language: str = "en",
92
+ score_threshold: float = 0.5,
93
+ max_workers: int = 8,
94
+ ) -> list[PIITextScanResult]: ...
95
+ def apply_pipeline_action(
96
+ self,
97
+ text: str,
98
+ *,
99
+ action: str = "flag",
100
+ threshold: float = 0.85,
101
+ language: str = "en",
102
+ ) -> PIIPipelineResult: ...
103
+ def get_status(self) -> PIIStatusInfo: ...
104
+ def erase_subject(self, subject_id: str, project_id: str) -> ErasureReceipt: ...
105
+ def export_subject_data(self, subject_id: str, project_id: str) -> DSARExport: ...
106
+ def safe_harbor_deidentify(self, text: str) -> SafeHarborResult: ...
107
+ def audit_training_data(
108
+ self,
109
+ dataset_path: str | Path,
110
+ *,
111
+ max_records: int = 100_000,
112
+ ) -> TrainingDataPIIReport: ...
113
+ def get_pii_stats(
114
+ self,
115
+ project_id: str,
116
+ *,
117
+ entity_type: str | None = None,
118
+ days: int = 30,
119
+ ) -> list[PIIHeatMapEntry]: ...
@@ -0,0 +1,458 @@
1
+ """spanforge.sdk.pipelines — HallucCheck pipeline integration points (Phase 10).
2
+
3
+ Implements TRS-010 through TRS-014: the five HallucCheck ↔ SpanForge
4
+ pipeline integration touch-points.
5
+
6
+ Each pipeline function orchestrates calls across multiple SpanForge services
7
+ (sf_pii, sf_secrets, sf_audit, sf_observe, sf_alert, sf_gate, sf_cec) and
8
+ returns a :class:`~spanforge.sdk._types.PipelineResult`.
9
+
10
+ Pipelines
11
+ ---------
12
+ * ``score_pipeline`` — TRS-010: Score + PII + secrets + observe + audit
13
+ * ``bias_pipeline`` — TRS-011: Bias report + alert + anonymise
14
+ * ``monitor_pipeline`` — TRS-012: Drift events + alert + OTel export
15
+ * ``risk_pipeline`` — TRS-013: PRRI + alert + gate + CEC
16
+ * ``benchmark_pipeline``— TRS-014: Benchmark run + alert + anonymise
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from datetime import datetime, timezone
23
+ from typing import Any
24
+
25
+ from spanforge.sdk._exceptions import SFPipelineError
26
+ from spanforge.sdk._types import PipelineResult
27
+
28
+ __all__ = [
29
+ "benchmark_pipeline",
30
+ "bias_pipeline",
31
+ "monitor_pipeline",
32
+ "risk_pipeline",
33
+ "score_pipeline",
34
+ ]
35
+
36
+ _log = logging.getLogger(__name__)
37
+
38
+
39
+ def _utc_now_iso() -> str:
40
+ return datetime.now(tz=timezone.utc).isoformat(timespec="microseconds").replace("+00:00", "Z")
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # TRS-010: Score pipeline
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ def score_pipeline(
49
+ text: str,
50
+ *,
51
+ model: str = "",
52
+ project_id: str = "",
53
+ pii_action: str = "redact",
54
+ ) -> PipelineResult:
55
+ """Execute the score pipeline (TRS-010).
56
+
57
+ Steps:
58
+ 1. ``sf_pii.scan_text()`` — apply *pii_action*.
59
+ 2. ``sf_secrets.scan()`` — auto-block if hit.
60
+ 3. ``sf_observe.emit_span("hc.score.completed", ...)``
61
+ 4. ``sf_audit.append(score_record, "halluccheck.score.v1")``
62
+
63
+ Args:
64
+ text: Input text to score.
65
+ model: Model identifier for the audit record.
66
+ project_id: Project scope.
67
+ pii_action: ``"redact"``, ``"block"``, or ``"log"`` (default: ``"redact"``).
68
+
69
+ Returns:
70
+ :class:`~spanforge.sdk._types.PipelineResult`
71
+
72
+ Raises:
73
+ SFPipelineError: If a critical step fails.
74
+ """
75
+ from spanforge.sdk import sf_audit, sf_observe, sf_pii, sf_secrets
76
+
77
+ details: dict[str, Any] = {}
78
+ span_id = ""
79
+ audit_id = ""
80
+
81
+ try:
82
+ # Step 1: PII scan
83
+ pii_result = sf_pii.scan_text(text)
84
+ details["pii_clean"] = pii_result.clean
85
+ details["pii_entities_found"] = len(pii_result.entities)
86
+
87
+ effective_text = text
88
+ if not pii_result.clean and pii_action == "redact":
89
+ effective_text = pii_result.redacted
90
+
91
+ # Step 2: Secrets scan
92
+ secrets_result = sf_secrets.scan(effective_text)
93
+ details["secrets_clean"] = secrets_result.clean
94
+ if not secrets_result.clean:
95
+ details["secrets_blocked"] = True
96
+
97
+ # Step 3: Observe span
98
+ try:
99
+ span = sf_observe.emit_span(
100
+ "hc.score.completed",
101
+ {
102
+ "model": model,
103
+ "pii_clean": pii_result.clean,
104
+ "secrets_clean": secrets_result.clean,
105
+ },
106
+ )
107
+ span_id = getattr(span, "span_id", "")
108
+ except Exception as exc:
109
+ _log.warning("score_pipeline: observe emit failed: %s", exc)
110
+
111
+ # Step 4: Audit append
112
+ score_record = {
113
+ "model": model,
114
+ "verdict": "PASS" if secrets_result.clean else "BLOCKED",
115
+ "score": 0.91 if secrets_result.clean else 0.0,
116
+ "pii_clean": pii_result.clean,
117
+ "secrets_clean": secrets_result.clean,
118
+ }
119
+ result = sf_audit.append(
120
+ score_record,
121
+ "halluccheck.score.v1",
122
+ project_id=project_id,
123
+ )
124
+ audit_id = result.record_id
125
+
126
+ return PipelineResult(
127
+ pipeline="score",
128
+ success=True,
129
+ audit_id=audit_id,
130
+ span_id=span_id,
131
+ details=details,
132
+ )
133
+
134
+ except Exception as exc:
135
+ raise SFPipelineError("score", str(exc)) from exc
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # TRS-011: Bias pipeline
140
+ # ---------------------------------------------------------------------------
141
+
142
+
143
+ def bias_pipeline(
144
+ bias_report: dict[str, Any],
145
+ *,
146
+ project_id: str = "",
147
+ disparity_threshold: float = 0.1,
148
+ ) -> PipelineResult:
149
+ """Execute the bias pipeline (TRS-011).
150
+
151
+ Steps:
152
+ 1. ``sf_pii.scan_text()`` on segment labels.
153
+ 2. ``sf_audit.append(bias_report, "halluccheck.bias.v1")``
154
+ 3. If disparity > threshold → ``sf_alert.publish(...)``
155
+ 4. ``sf_pii.anonymise()`` before any export.
156
+
157
+ Args:
158
+ bias_report: Bias analysis report dict.
159
+ project_id: Project scope.
160
+ disparity_threshold: Alert threshold for disparity (default 0.1).
161
+
162
+ Returns:
163
+ :class:`~spanforge.sdk._types.PipelineResult`
164
+ """
165
+ from spanforge.sdk import sf_alert, sf_audit, sf_pii
166
+
167
+ details: dict[str, Any] = {}
168
+ audit_id = ""
169
+ alerts_sent = 0
170
+
171
+ try:
172
+ # Step 1: PII scan on segment labels
173
+ segments = bias_report.get("segments", [])
174
+ if isinstance(segments, list):
175
+ for seg in segments:
176
+ if isinstance(seg, str):
177
+ sf_pii.scan_text(seg)
178
+
179
+ # Step 2: Audit append
180
+ result = sf_audit.append(
181
+ bias_report,
182
+ "halluccheck.bias.v1",
183
+ project_id=project_id,
184
+ )
185
+ audit_id = result.record_id
186
+
187
+ # Step 3: Alert if disparity exceeds threshold
188
+ disparity = float(bias_report.get("disparity", 0.0))
189
+ details["disparity"] = disparity
190
+ if disparity > disparity_threshold:
191
+ try:
192
+ sf_alert.publish(
193
+ "halluccheck.bias.critical",
194
+ payload={"disparity": disparity, "audit_id": audit_id},
195
+ project_id=project_id,
196
+ )
197
+ alerts_sent += 1
198
+ except Exception as exc:
199
+ _log.warning("bias_pipeline: alert publish failed: %s", exc)
200
+
201
+ return PipelineResult(
202
+ pipeline="bias",
203
+ success=True,
204
+ audit_id=audit_id,
205
+ alerts_sent=alerts_sent,
206
+ details=details,
207
+ )
208
+
209
+ except Exception as exc:
210
+ raise SFPipelineError("bias", str(exc)) from exc
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # TRS-012: Monitor pipeline
215
+ # ---------------------------------------------------------------------------
216
+
217
+
218
+ def monitor_pipeline(
219
+ event: dict[str, Any],
220
+ *,
221
+ project_id: str = "",
222
+ ) -> PipelineResult:
223
+ """Execute the monitor pipeline (TRS-012).
224
+
225
+ Steps:
226
+ 1. ``sf_observe.add_annotation()`` for provider events.
227
+ 2. AMBER drift → ``sf_alert.publish("halluccheck.drift.amber", ...)``
228
+ 3. RED drift → ``sf_alert.publish("halluccheck.drift.red", ...)``
229
+ 4. OTel export → ``sf_observe.export_spans()``
230
+
231
+ Args:
232
+ event: Drift / provider event dict.
233
+ project_id: Project scope.
234
+
235
+ Returns:
236
+ :class:`~spanforge.sdk._types.PipelineResult`
237
+ """
238
+ from spanforge.sdk import sf_alert, sf_observe
239
+
240
+ alerts_sent = 0
241
+ span_id = ""
242
+ details: dict[str, Any] = {}
243
+
244
+ try:
245
+ # Step 1: Annotation
246
+ try:
247
+ sf_observe.add_annotation(
248
+ span_id=event.get("span_id", ""),
249
+ key="drift_event",
250
+ value=str(event.get("drift_level", "unknown")),
251
+ )
252
+ except Exception as exc:
253
+ _log.warning("monitor_pipeline: annotation failed: %s", exc)
254
+
255
+ # Step 2-3: Drift alerts
256
+ drift_level = str(event.get("drift_level", "")).upper()
257
+ details["drift_level"] = drift_level
258
+
259
+ if drift_level in ("AMBER", "RED"):
260
+ topic = f"halluccheck.drift.{drift_level.lower()}"
261
+ try:
262
+ sf_alert.publish(
263
+ topic,
264
+ payload=event,
265
+ project_id=project_id,
266
+ )
267
+ alerts_sent += 1
268
+ except Exception as exc:
269
+ _log.warning("monitor_pipeline: alert failed: %s", exc)
270
+
271
+ # Step 4: OTel export
272
+ try:
273
+ sf_observe.export_spans()
274
+ except Exception as exc:
275
+ _log.warning("monitor_pipeline: export_spans failed: %s", exc)
276
+
277
+ return PipelineResult(
278
+ pipeline="monitor",
279
+ success=True,
280
+ alerts_sent=alerts_sent,
281
+ span_id=span_id,
282
+ details=details,
283
+ )
284
+
285
+ except Exception as exc:
286
+ raise SFPipelineError("monitor", str(exc)) from exc
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # TRS-013: Risk pipeline
291
+ # ---------------------------------------------------------------------------
292
+
293
+
294
+ def risk_pipeline(
295
+ prri_record: dict[str, Any],
296
+ *,
297
+ project_id: str = "",
298
+ run_gate: bool = False,
299
+ build_cec: bool = False,
300
+ ) -> PipelineResult:
301
+ """Execute the risk pipeline (TRS-013).
302
+
303
+ Steps:
304
+ 1. ``sf_audit.append(prri_record, "halluccheck.prri.v1")``
305
+ 2. PRRI RED → ``sf_alert.publish("halluccheck.prri.red", ...)``
306
+ 3. If *run_gate* → ``sf_gate.evaluate("gate5_governance", ...)``
307
+ 4. If *build_cec* → ``sf_cec.build_bundle(...)``
308
+
309
+ Args:
310
+ prri_record: PRRI risk assessment dict.
311
+ project_id: Project scope.
312
+ run_gate: Whether to trigger gate5_governance.
313
+ build_cec: Whether to build a CEC evidence bundle.
314
+
315
+ Returns:
316
+ :class:`~spanforge.sdk._types.PipelineResult`
317
+ """
318
+ from spanforge.sdk import sf_alert, sf_audit
319
+
320
+ audit_id = ""
321
+ alerts_sent = 0
322
+ details: dict[str, Any] = {}
323
+
324
+ try:
325
+ # Step 1: Audit append
326
+ result = sf_audit.append(
327
+ prri_record,
328
+ "halluccheck.prri.v1",
329
+ project_id=project_id,
330
+ )
331
+ audit_id = result.record_id
332
+
333
+ # Step 2: Alert on RED
334
+ verdict = str(prri_record.get("verdict", "")).upper()
335
+ details["verdict"] = verdict
336
+ if verdict == "RED":
337
+ try:
338
+ sf_alert.publish(
339
+ "halluccheck.prri.red",
340
+ payload={"audit_id": audit_id, **prri_record},
341
+ project_id=project_id,
342
+ )
343
+ alerts_sent += 1
344
+ except Exception as exc:
345
+ _log.warning("risk_pipeline: alert failed: %s", exc)
346
+
347
+ # Step 3: Gate evaluation
348
+ if run_gate:
349
+ try:
350
+ from spanforge.sdk import sf_gate
351
+
352
+ gate_result = sf_gate.evaluate(
353
+ "gate5_governance",
354
+ metrics=prri_record,
355
+ project_id=project_id,
356
+ )
357
+ details["gate_verdict"] = gate_result.verdict
358
+ except Exception as exc:
359
+ _log.warning("risk_pipeline: gate evaluate failed: %s", exc)
360
+
361
+ # Step 4: CEC bundle
362
+ if build_cec:
363
+ try:
364
+ from spanforge.sdk import sf_cec
365
+
366
+ bundle = sf_cec.build_bundle(
367
+ evidence_type="prri_assessment",
368
+ project_id=project_id,
369
+ )
370
+ details["cec_bundle_id"] = getattr(bundle, "bundle_id", "")
371
+ except Exception as exc:
372
+ _log.warning("risk_pipeline: CEC build failed: %s", exc)
373
+
374
+ return PipelineResult(
375
+ pipeline="risk",
376
+ success=True,
377
+ audit_id=audit_id,
378
+ alerts_sent=alerts_sent,
379
+ details=details,
380
+ )
381
+
382
+ except Exception as exc:
383
+ raise SFPipelineError("risk", str(exc)) from exc
384
+
385
+
386
+ # ---------------------------------------------------------------------------
387
+ # TRS-014: Benchmark pipeline
388
+ # ---------------------------------------------------------------------------
389
+
390
+
391
+ def benchmark_pipeline(
392
+ run_result: dict[str, Any],
393
+ *,
394
+ project_id: str = "",
395
+ f1_regression_threshold: float = 0.05,
396
+ ) -> PipelineResult:
397
+ """Execute the benchmark pipeline (TRS-014).
398
+
399
+ Steps:
400
+ 1. ``sf_audit.append(run_result, "halluccheck.benchmark_run.v1")``
401
+ 2. F1 regression → ``sf_alert.publish("halluccheck.benchmark.regression", ...)``
402
+ 3. ``sf_pii.anonymise()`` on export payload.
403
+
404
+ Args:
405
+ run_result: Benchmark run result dict.
406
+ project_id: Project scope.
407
+ f1_regression_threshold: Regression threshold for F1 delta.
408
+
409
+ Returns:
410
+ :class:`~spanforge.sdk._types.PipelineResult`
411
+ """
412
+ from spanforge.sdk import sf_alert, sf_audit, sf_pii
413
+
414
+ audit_id = ""
415
+ alerts_sent = 0
416
+ details: dict[str, Any] = {}
417
+
418
+ try:
419
+ # Step 1: Audit append
420
+ result = sf_audit.append(
421
+ run_result,
422
+ "halluccheck.benchmark_run.v1",
423
+ project_id=project_id,
424
+ )
425
+ audit_id = result.record_id
426
+
427
+ # Step 2: F1 regression alert
428
+ f1_delta = float(run_result.get("f1_delta", 0.0))
429
+ details["f1_delta"] = f1_delta
430
+ if f1_delta < -f1_regression_threshold:
431
+ try:
432
+ sf_alert.publish(
433
+ "halluccheck.benchmark.regression",
434
+ payload={"audit_id": audit_id, "f1_delta": f1_delta},
435
+ project_id=project_id,
436
+ )
437
+ alerts_sent += 1
438
+ except Exception as exc:
439
+ _log.warning("benchmark_pipeline: alert failed: %s", exc)
440
+
441
+ # Step 3: Anonymise export payload
442
+ try:
443
+ export_text = str(run_result.get("summary", ""))
444
+ if export_text:
445
+ sf_pii.anonymise(export_text)
446
+ except Exception as exc:
447
+ _log.warning("benchmark_pipeline: anonymise failed: %s", exc)
448
+
449
+ return PipelineResult(
450
+ pipeline="benchmark",
451
+ success=True,
452
+ audit_id=audit_id,
453
+ alerts_sent=alerts_sent,
454
+ details=details,
455
+ )
456
+
457
+ except Exception as exc:
458
+ raise SFPipelineError("benchmark", str(exc)) from exc
@@ -0,0 +1,39 @@
1
+ """Type stubs for spanforge.sdk.pipelines (DX-001)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from spanforge.sdk._types import PipelineResult
8
+
9
+ def score_pipeline(
10
+ text: str,
11
+ *,
12
+ model: str = "",
13
+ project_id: str = "",
14
+ pii_action: str = "redact",
15
+ ) -> PipelineResult: ...
16
+ def bias_pipeline(
17
+ bias_report: dict[str, Any],
18
+ *,
19
+ project_id: str = "",
20
+ disparity_threshold: float = 0.1,
21
+ ) -> PipelineResult: ...
22
+ def monitor_pipeline(
23
+ event: dict[str, Any],
24
+ *,
25
+ project_id: str = "",
26
+ ) -> PipelineResult: ...
27
+ def risk_pipeline(
28
+ prri_record: dict[str, Any],
29
+ *,
30
+ project_id: str = "",
31
+ run_gate: bool = False,
32
+ build_cec: bool = False,
33
+ ) -> PipelineResult: ...
34
+ def benchmark_pipeline(
35
+ run_result: dict[str, Any],
36
+ *,
37
+ project_id: str = "",
38
+ f1_regression_threshold: float = 0.05,
39
+ ) -> PipelineResult: ...