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
@@ -0,0 +1,325 @@
1
+ """Public compliance facade for SpanForge.
2
+
3
+ This module exposes the stable ``spanforge.compliance`` API referenced by the
4
+ CLI and documentation. It provides lightweight compatibility and isolation
5
+ checks while re-exporting the richer compliance evidence engine.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from types import MappingProxyType
12
+ from typing import TYPE_CHECKING
13
+
14
+ from spanforge.core.compliance_mapping import (
15
+ ClauseStatus,
16
+ ComplianceAttestation,
17
+ ComplianceEvidencePackage,
18
+ ComplianceFramework,
19
+ ComplianceMappingEngine,
20
+ EvidenceRecord,
21
+ GapReport,
22
+ verify_attestation_signature,
23
+ verify_pdf_attestation,
24
+ )
25
+ from spanforge.event import _SOURCE_PATTERN
26
+ from spanforge.signing import ChainVerificationResult, verify_chain
27
+ from spanforge.types import is_registered, validate_custom
28
+ from spanforge.ulid import validate as validate_ulid
29
+
30
+ if TYPE_CHECKING:
31
+ from collections.abc import Sequence
32
+
33
+ from spanforge.event import Event
34
+
35
+ __all__ = [
36
+ "ChainIntegrityResult",
37
+ "ChainIntegrityViolation",
38
+ "ClauseStatus",
39
+ "CompatibilityResult",
40
+ "CompatibilityViolation",
41
+ "ComplianceAttestation",
42
+ "ComplianceEvidencePackage",
43
+ "ComplianceFramework",
44
+ "ComplianceMappingEngine",
45
+ "EvidenceRecord",
46
+ "GapReport",
47
+ "IsolationResult",
48
+ "IsolationViolation",
49
+ "test_compatibility",
50
+ "verify_attestation_signature",
51
+ "verify_chain_integrity",
52
+ "verify_events_scoped",
53
+ "verify_pdf_attestation",
54
+ "verify_tenant_isolation",
55
+ ]
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class CompatibilityViolation:
60
+ """A single compatibility non-conformance."""
61
+
62
+ check_id: str
63
+ rule: str
64
+ detail: str
65
+ event_id: str | None
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class CompatibilityResult:
70
+ """Result of running the compatibility checklist."""
71
+
72
+ passed: bool
73
+ events_checked: int
74
+ violations: list[CompatibilityViolation]
75
+
76
+ def __bool__(self) -> bool:
77
+ return self.passed
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class ChainIntegrityViolation:
82
+ """A single audit-chain integrity issue."""
83
+
84
+ kind: str
85
+ detail: str
86
+ event_id: str | None = None
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class ChainIntegrityResult:
91
+ """Result of audit-chain integrity verification."""
92
+
93
+ passed: bool
94
+ chain_result: ChainVerificationResult
95
+ violations: list[ChainIntegrityViolation]
96
+ events_verified: int
97
+ gaps_detected: int
98
+
99
+ def __bool__(self) -> bool:
100
+ return self.passed
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class IsolationViolation:
105
+ """A single tenant-scoping violation."""
106
+
107
+ detail: str
108
+ event_id: str | None = None
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class IsolationResult:
113
+ """Result of tenant/isolation checks."""
114
+
115
+ passed: bool
116
+ violations: list[IsolationViolation]
117
+
118
+ def __bool__(self) -> bool:
119
+ return self.passed
120
+
121
+
122
+ def test_compatibility(events: Sequence[Event]) -> CompatibilityResult:
123
+ """Apply the public compatibility checklist to a batch of events."""
124
+ violations: list[CompatibilityViolation] = []
125
+
126
+ for event in events:
127
+ if not getattr(event, "schema_version", ""):
128
+ violations.append(
129
+ CompatibilityViolation(
130
+ check_id="CHK-1",
131
+ rule="required fields present",
132
+ detail="schema_version must be present and non-empty",
133
+ event_id=getattr(event, "event_id", None),
134
+ )
135
+ )
136
+ if not getattr(event, "source", ""):
137
+ violations.append(
138
+ CompatibilityViolation(
139
+ check_id="CHK-1",
140
+ rule="required fields present",
141
+ detail="source must be present and non-empty",
142
+ event_id=getattr(event, "event_id", None),
143
+ )
144
+ )
145
+ payload = getattr(event, "payload", None)
146
+ if not isinstance(payload, (dict, MappingProxyType)) or not payload:
147
+ violations.append(
148
+ CompatibilityViolation(
149
+ check_id="CHK-1",
150
+ rule="required fields present",
151
+ detail="payload must be a non-empty dict",
152
+ event_id=getattr(event, "event_id", None),
153
+ )
154
+ )
155
+
156
+ event_type = str(getattr(event, "event_type", ""))
157
+ if not event_type:
158
+ violations.append(
159
+ CompatibilityViolation(
160
+ check_id="CHK-2",
161
+ rule="event_type namespace validity",
162
+ detail="event_type must be present and non-empty",
163
+ event_id=getattr(event, "event_id", None),
164
+ )
165
+ )
166
+ elif not is_registered(event_type):
167
+ try:
168
+ validate_custom(event_type)
169
+ except Exception as exc:
170
+ violations.append(
171
+ CompatibilityViolation(
172
+ check_id="CHK-2",
173
+ rule="event_type namespace validity",
174
+ detail=str(exc),
175
+ event_id=getattr(event, "event_id", None),
176
+ )
177
+ )
178
+
179
+ source = str(getattr(event, "source", ""))
180
+ if source and not _SOURCE_PATTERN.match(source):
181
+ violations.append(
182
+ CompatibilityViolation(
183
+ check_id="CHK-3",
184
+ rule="source format",
185
+ detail="source must match <service>@<semver>",
186
+ event_id=getattr(event, "event_id", None),
187
+ )
188
+ )
189
+
190
+ event_id = str(getattr(event, "event_id", ""))
191
+ if event_id and not validate_ulid(event_id):
192
+ violations.append(
193
+ CompatibilityViolation(
194
+ check_id="CHK-5",
195
+ rule="event_id is a valid ULID",
196
+ detail="event_id must be a valid 26-character ULID",
197
+ event_id=event_id,
198
+ )
199
+ )
200
+
201
+ return CompatibilityResult(
202
+ passed=not violations,
203
+ events_checked=len(events),
204
+ violations=violations,
205
+ )
206
+
207
+
208
+ setattr(test_compatibility, "__test__", False) # noqa: B010
209
+
210
+
211
+ def verify_chain_integrity(
212
+ events: Sequence[Event],
213
+ org_secret: str,
214
+ *,
215
+ check_monotonic_timestamps: bool = True,
216
+ ) -> ChainIntegrityResult:
217
+ """Verify an ordered event chain for gaps, tampering, and timestamp regressions."""
218
+ chain_result = verify_chain(events, org_secret)
219
+ violations: list[ChainIntegrityViolation] = []
220
+
221
+ if chain_result.first_tampered is not None:
222
+ violations.append(
223
+ ChainIntegrityViolation(
224
+ kind="tampered_signature",
225
+ detail="one or more event signatures failed verification",
226
+ event_id=chain_result.first_tampered,
227
+ )
228
+ )
229
+
230
+ violations.extend(
231
+ ChainIntegrityViolation(
232
+ kind="broken_prev_id_link",
233
+ detail="prev_id chain linkage is broken",
234
+ event_id=event_id,
235
+ )
236
+ for event_id in chain_result.gaps
237
+ )
238
+
239
+ if check_monotonic_timestamps:
240
+ previous: str | None = None
241
+ previous_event_id: str | None = None
242
+ for event in events:
243
+ current = getattr(event, "timestamp", None)
244
+ if previous is not None and current is not None and str(current) < str(previous):
245
+ violations.append(
246
+ ChainIntegrityViolation(
247
+ kind="non_monotonic_timestamp",
248
+ detail="timestamps must be monotonically non-decreasing",
249
+ event_id=getattr(event, "event_id", previous_event_id),
250
+ )
251
+ )
252
+ break
253
+ previous = str(current) if current is not None else None
254
+ previous_event_id = getattr(event, "event_id", None)
255
+
256
+ return ChainIntegrityResult(
257
+ passed=not violations,
258
+ chain_result=chain_result,
259
+ violations=violations,
260
+ events_verified=len(events),
261
+ gaps_detected=len(chain_result.gaps),
262
+ )
263
+
264
+
265
+ def verify_tenant_isolation(
266
+ group_a: Sequence[Event],
267
+ group_b: Sequence[Event],
268
+ *,
269
+ strict: bool = False,
270
+ ) -> IsolationResult:
271
+ """Verify that two event batches are scoped to separate tenants."""
272
+ violations: list[IsolationViolation] = []
273
+ orgs_a = {event.org_id for event in group_a if event.org_id}
274
+ orgs_b = {event.org_id for event in group_b if event.org_id}
275
+ overlap = sorted(orgs_a & orgs_b)
276
+
277
+ if overlap:
278
+ violations.append(
279
+ IsolationViolation(
280
+ detail=f"tenant groups share org_id values: {', '.join(overlap)}",
281
+ )
282
+ )
283
+
284
+ if strict:
285
+ violations.extend(
286
+ IsolationViolation(
287
+ detail="strict tenant isolation requires org_id on every event",
288
+ event_id=event.event_id,
289
+ )
290
+ for event in list(group_a) + list(group_b)
291
+ if not event.org_id
292
+ )
293
+
294
+ return IsolationResult(passed=not violations, violations=violations)
295
+
296
+
297
+ def verify_events_scoped(
298
+ events: Sequence[Event],
299
+ *,
300
+ expected_org_id: str | None = None,
301
+ expected_team_id: str | None = None,
302
+ ) -> IsolationResult:
303
+ """Verify that events carry the expected tenant scope values."""
304
+ violations: list[IsolationViolation] = []
305
+ for event in events:
306
+ if expected_org_id is not None and event.org_id != expected_org_id:
307
+ violations.append(
308
+ IsolationViolation(
309
+ detail=(
310
+ f"expected org_id={expected_org_id!r}, found {event.org_id!r}"
311
+ ),
312
+ event_id=event.event_id,
313
+ )
314
+ )
315
+ if expected_team_id is not None and event.team_id != expected_team_id:
316
+ violations.append(
317
+ IsolationViolation(
318
+ detail=(
319
+ f"expected team_id={expected_team_id!r}, found {event.team_id!r}"
320
+ ),
321
+ event_id=event.event_id,
322
+ )
323
+ )
324
+
325
+ return IsolationResult(passed=not violations, violations=violations)