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/trust.py ADDED
@@ -0,0 +1,472 @@
1
+ """spanforge.sdk.trust — T.R.U.S.T. Scorecard service (Phase 10).
2
+
3
+ Implements TRS-001 through TRS-006: the five-pillar T.R.U.S.T. dimension
4
+ model (Transparency, Reliability, UserTrust, Security, Traceability) with
5
+ configurable weights, weighted scoring, badge generation, and scorecard API.
6
+
7
+ Architecture
8
+ ------------
9
+ * Reads from the existing T.R.U.S.T. store written by
10
+ :meth:`~spanforge.sdk.audit.SFAuditClient.append`.
11
+ * Maps the existing 5 audit dimensions to the T.R.U.S.T. acronym pillars:
12
+ - ``hallucination`` → Reliability
13
+ - ``pii_hygiene`` → Security
14
+ - ``secrets_hygiene`` → Security (merged)
15
+ - ``gate_pass_rate`` → Transparency
16
+ - ``compliance_posture`` → Traceability
17
+ * UserTrust is derived from bias audit records.
18
+ * Each dimension scored 0–100. Overall = weighted average.
19
+ * Colour bands: green ≥ 80, amber ≥ 60, red < 60.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import hashlib
25
+ import logging
26
+ from datetime import datetime, timezone
27
+ from typing import Any
28
+
29
+ from spanforge.sdk._base import SFClientConfig, SFServiceClient
30
+ from spanforge.sdk._exceptions import SFTrustComputeError
31
+ from spanforge.sdk._types import (
32
+ TrustBadgeResult,
33
+ TrustDimension,
34
+ TrustDimensionWeights,
35
+ TrustHistoryEntry,
36
+ TrustScorecardResponse,
37
+ TrustStatusInfo,
38
+ )
39
+
40
+ __all__ = ["SFTrustClient"]
41
+
42
+ _log = logging.getLogger(__name__)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Colour-band logic
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ def _colour_band(score: float) -> str:
51
+ """Return the colour band for a T.R.U.S.T. score."""
52
+ if score >= 80.0:
53
+ return "green"
54
+ if score >= 60.0:
55
+ return "amber"
56
+ return "red"
57
+
58
+
59
+ def _weighted_average(
60
+ dimensions: dict[str, float],
61
+ weights: TrustDimensionWeights,
62
+ ) -> float:
63
+ """Compute the weighted average of five dimension scores."""
64
+ w = {
65
+ "transparency": weights.transparency,
66
+ "reliability": weights.reliability,
67
+ "user_trust": weights.user_trust,
68
+ "security": weights.security,
69
+ "traceability": weights.traceability,
70
+ }
71
+ total_weight = sum(w.values())
72
+ if total_weight == 0:
73
+ return 0.0
74
+ weighted_sum = sum(dimensions[k] * w[k] for k in dimensions)
75
+ return round(weighted_sum / total_weight, 2)
76
+
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # Dimension mapping from existing audit trust records
80
+ # ---------------------------------------------------------------------------
81
+
82
+ # Existing audit dimension → T.R.U.S.T. pillar
83
+ _AUDIT_DIM_TO_TRUST: dict[str, str] = {
84
+ "hallucination": "reliability",
85
+ "pii_hygiene": "security",
86
+ "secrets_hygiene": "security",
87
+ "gate_pass_rate": "transparency", # nosec B105
88
+ "compliance_posture": "traceability",
89
+ }
90
+
91
+
92
+ def _compute_dim_score(records: list[dict[str, Any]]) -> tuple[float, str]:
93
+ """Compute score and trend from trust records (same logic as audit.py)."""
94
+ if not records:
95
+ return 50.0, "flat"
96
+
97
+ raw_scores: list[float] = []
98
+ for r in records:
99
+ s = r.get("score")
100
+ try:
101
+ v = float(s) # type: ignore[arg-type]
102
+ except (TypeError, ValueError):
103
+ v = 0.5
104
+ raw_scores.append(v * 100 if v <= 1.0 else min(v, 100.0))
105
+
106
+ if not raw_scores:
107
+ return 50.0, "flat"
108
+
109
+ avg = sum(raw_scores) / len(raw_scores)
110
+
111
+ # Trend: compare first half vs second half
112
+ mid = max(1, len(raw_scores) // 2)
113
+ first_half = sum(raw_scores[:mid]) / mid
114
+ second_half = sum(raw_scores[mid:]) / max(1, len(raw_scores) - mid)
115
+
116
+ delta = second_half - first_half
117
+ if delta > 2.0:
118
+ trend = "up"
119
+ elif delta < -2.0:
120
+ trend = "down"
121
+ else:
122
+ trend = "flat"
123
+
124
+ return round(avg, 2), trend
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # SVG badge template (TRS-006)
129
+ # ---------------------------------------------------------------------------
130
+
131
+ _SVG_BADGE_TEMPLATE = """\
132
+ <svg xmlns="http://www.w3.org/2000/svg" width="140" height="20">
133
+ <linearGradient id="b" x2="0" y2="100%">
134
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
135
+ <stop offset="1" stop-opacity=".1"/>
136
+ </linearGradient>
137
+ <mask id="a"><rect width="140" height="20" rx="3" fill="#fff"/></mask>
138
+ <g mask="url(#a)">
139
+ <rect width="80" height="20" fill="#555"/>
140
+ <rect x="80" width="60" height="20" fill="{colour}"/>
141
+ <rect width="140" height="20" fill="url(#b)"/>
142
+ </g>
143
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,sans-serif" font-size="11">
144
+ <text x="40" y="14">T.R.U.S.T.</text>
145
+ <text x="110" y="14">{score}</text>
146
+ </g>
147
+ </svg>"""
148
+
149
+ _COLOUR_MAP = {"green": "#4c1", "amber": "#dfb317", "red": "#e05d44"}
150
+
151
+
152
+ def _generate_badge_svg(score: float, band: str) -> str:
153
+ """Return an SVG badge string for the given score and colour band."""
154
+ colour = _COLOUR_MAP.get(band, "#9f9f9f")
155
+ return _SVG_BADGE_TEMPLATE.format(colour=colour, score=round(score))
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # SFTrustClient
160
+ # ---------------------------------------------------------------------------
161
+
162
+
163
+ class SFTrustClient(SFServiceClient):
164
+ """T.R.U.S.T. scorecard service client (Phase 10).
165
+
166
+ Reads trust records from sf-audit and computes the five T.R.U.S.T.
167
+ dimensions with configurable weights.
168
+
169
+ Args:
170
+ config: Client configuration.
171
+ weights: Dimension weights for the weighted average computation.
172
+ Defaults to equal weights (1.0 each).
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ config: SFClientConfig,
178
+ *,
179
+ weights: TrustDimensionWeights | None = None,
180
+ ) -> None:
181
+ super().__init__(config, service_name="trust")
182
+ self._weights = weights or TrustDimensionWeights()
183
+ self._last_computed: str | None = None
184
+
185
+ # ------------------------------------------------------------------
186
+ # TRS-001 / TRS-005: get_scorecard
187
+ # ------------------------------------------------------------------
188
+
189
+ def get_scorecard(
190
+ self,
191
+ project_id: str | None = None,
192
+ *,
193
+ from_dt: str | None = None,
194
+ to_dt: str | None = None,
195
+ weights: TrustDimensionWeights | None = None,
196
+ ) -> TrustScorecardResponse:
197
+ """Return the T.R.U.S.T. scorecard for *project_id* (TRS-005).
198
+
199
+ Aggregates trust records from sf-audit and maps to the five
200
+ T.R.U.S.T. dimensions.
201
+
202
+ Args:
203
+ project_id: Scoping project. Defaults to ``config.project_id``.
204
+ from_dt: ISO-8601 UTC start of reporting window.
205
+ to_dt: ISO-8601 UTC end of reporting window.
206
+ weights: Override the instance default weights.
207
+
208
+ Returns:
209
+ :class:`~spanforge.sdk._types.TrustScorecardResponse`
210
+
211
+ Raises:
212
+ SFTrustComputeError: If the underlying audit store is unreachable.
213
+ """
214
+ from spanforge.sdk import sf_audit
215
+
216
+ effective_project = project_id or self._config.project_id
217
+ effective_weights = weights or self._weights
218
+ now_iso = self._utc_now_iso()
219
+ from_iso = from_dt or "1970-01-01T00:00:00.000000Z"
220
+ to_iso = to_dt or now_iso
221
+
222
+ try:
223
+ trust_records = sf_audit._store.query_trust(effective_project or None, from_iso, to_iso)
224
+ except Exception as exc:
225
+ raise SFTrustComputeError(f"Failed to query trust store: {exc}") from exc
226
+
227
+ # Bucket records by T.R.U.S.T. pillar
228
+ by_pillar: dict[str, list[dict[str, Any]]] = {
229
+ "transparency": [],
230
+ "reliability": [],
231
+ "user_trust": [],
232
+ "security": [],
233
+ "traceability": [],
234
+ }
235
+
236
+ for rec in trust_records:
237
+ audit_dim = rec.get("trust_dimension", "compliance_posture")
238
+ pillar = _AUDIT_DIM_TO_TRUST.get(audit_dim)
239
+ if pillar and pillar in by_pillar:
240
+ by_pillar[pillar].append(rec)
241
+
242
+ # Derive UserTrust from bias records
243
+ for rec in trust_records:
244
+ if rec.get("record_type") == "halluccheck.bias.v1":
245
+ by_pillar["user_trust"].append(rec)
246
+
247
+ def _dim(name: str) -> TrustDimension:
248
+ recs = by_pillar[name]
249
+ score, trend = _compute_dim_score(recs)
250
+ last_rec = recs[-1] if recs else None
251
+ last_ts = last_rec["timestamp"] if last_rec else now_iso
252
+ return TrustDimension(score=score, trend=trend, last_updated=last_ts)
253
+
254
+ dims = {k: _dim(k) for k in by_pillar}
255
+ dim_scores = {k: dims[k].score for k in dims}
256
+ overall = _weighted_average(dim_scores, effective_weights)
257
+ band = _colour_band(overall)
258
+
259
+ self._last_computed = now_iso
260
+
261
+ return TrustScorecardResponse(
262
+ project_id=effective_project,
263
+ overall_score=overall,
264
+ colour_band=band,
265
+ transparency=dims["transparency"],
266
+ reliability=dims["reliability"],
267
+ user_trust=dims["user_trust"],
268
+ security=dims["security"],
269
+ traceability=dims["traceability"],
270
+ from_dt=from_iso,
271
+ to_dt=to_iso,
272
+ record_count=len(trust_records),
273
+ weights=effective_weights,
274
+ )
275
+
276
+ # ------------------------------------------------------------------
277
+ # TRS-005: get_history
278
+ # ------------------------------------------------------------------
279
+
280
+ def get_history(
281
+ self,
282
+ project_id: str | None = None,
283
+ *,
284
+ from_dt: str | None = None,
285
+ to_dt: str | None = None,
286
+ buckets: int = 10,
287
+ ) -> list[TrustHistoryEntry]:
288
+ """Return T.R.U.S.T. scorecard history time series.
289
+
290
+ Divides the time range into *buckets* equal intervals and computes
291
+ a scorecard snapshot for each.
292
+
293
+ Args:
294
+ project_id: Scoping project.
295
+ from_dt: ISO-8601 UTC start.
296
+ to_dt: ISO-8601 UTC end.
297
+ buckets: Number of time buckets (default 10).
298
+
299
+ Returns:
300
+ List of :class:`~spanforge.sdk._types.TrustHistoryEntry`.
301
+ """
302
+ from spanforge.sdk import sf_audit
303
+
304
+ effective_project = project_id or self._config.project_id
305
+ now_iso = self._utc_now_iso()
306
+ from_iso = from_dt or "1970-01-01T00:00:00.000000Z"
307
+ to_iso = to_dt or now_iso
308
+
309
+ # Parse range into bucket boundaries
310
+ try:
311
+ t_start = datetime.fromisoformat(from_iso.replace("Z", "+00:00"))
312
+ t_end = datetime.fromisoformat(to_iso.replace("Z", "+00:00"))
313
+ except ValueError:
314
+ t_start = datetime(1970, 1, 1, tzinfo=timezone.utc)
315
+ t_end = datetime.now(tz=timezone.utc)
316
+
317
+ if t_end <= t_start:
318
+ return []
319
+
320
+ delta = (t_end - t_start) / max(buckets, 1)
321
+ entries: list[TrustHistoryEntry] = []
322
+
323
+ for i in range(buckets):
324
+ bucket_end = t_start + delta * (i + 1)
325
+ bucket_iso = bucket_end.isoformat(timespec="microseconds").replace("+00:00", "Z")
326
+
327
+ try:
328
+ trust_records = sf_audit._store.query_trust(
329
+ effective_project or None, from_iso, bucket_iso
330
+ )
331
+ except Exception:
332
+ trust_records = []
333
+ if not trust_records:
334
+ continue
335
+
336
+ by_pillar: dict[str, list[dict[str, Any]]] = {
337
+ "transparency": [],
338
+ "reliability": [],
339
+ "user_trust": [],
340
+ "security": [],
341
+ "traceability": [],
342
+ }
343
+ for rec in trust_records:
344
+ audit_dim = rec.get("trust_dimension", "compliance_posture")
345
+ pillar = _AUDIT_DIM_TO_TRUST.get(audit_dim)
346
+ if pillar and pillar in by_pillar:
347
+ by_pillar[pillar].append(rec)
348
+ for rec in trust_records:
349
+ if rec.get("record_type") == "halluccheck.bias.v1":
350
+ by_pillar["user_trust"].append(rec)
351
+
352
+ dim_scores: dict[str, float] = {}
353
+ for k in by_pillar:
354
+ score, _ = _compute_dim_score(by_pillar[k])
355
+ dim_scores[k] = score
356
+
357
+ overall = _weighted_average(dim_scores, self._weights)
358
+
359
+ entries.append(
360
+ TrustHistoryEntry(
361
+ timestamp=bucket_iso,
362
+ overall=overall,
363
+ transparency=dim_scores["transparency"],
364
+ reliability=dim_scores["reliability"],
365
+ user_trust=dim_scores["user_trust"],
366
+ security=dim_scores["security"],
367
+ traceability=dim_scores["traceability"],
368
+ )
369
+ )
370
+
371
+ return entries
372
+
373
+ # ------------------------------------------------------------------
374
+ # TRS-006: get_badge
375
+ # ------------------------------------------------------------------
376
+
377
+ def get_badge(
378
+ self,
379
+ project_id: str | None = None,
380
+ ) -> TrustBadgeResult:
381
+ """Return a T.R.U.S.T. badge SVG for *project_id* (TRS-006).
382
+
383
+ Args:
384
+ project_id: Scoping project.
385
+
386
+ Returns:
387
+ :class:`~spanforge.sdk._types.TrustBadgeResult`
388
+ """
389
+ scorecard = self.get_scorecard(project_id=project_id)
390
+ svg = _generate_badge_svg(scorecard.overall_score, scorecard.colour_band)
391
+ etag = hashlib.md5(svg.encode(), usedforsecurity=False).hexdigest()
392
+
393
+ return TrustBadgeResult(
394
+ svg=svg,
395
+ overall=scorecard.overall_score,
396
+ colour_band=scorecard.colour_band,
397
+ etag=etag,
398
+ )
399
+
400
+ # ------------------------------------------------------------------
401
+ # get_scorecard_async (F-10)
402
+ # ------------------------------------------------------------------
403
+
404
+ async def get_scorecard_async(
405
+ self,
406
+ project_id: str | None = None,
407
+ *,
408
+ from_dt: str | None = None,
409
+ to_dt: str | None = None,
410
+ weights=None,
411
+ ):
412
+ """Async variant of :meth:`get_scorecard` (F-10).
413
+
414
+ Runs :meth:`get_scorecard` in a thread-pool executor via
415
+ :func:`asyncio.run_in_executor`, making it safe to ``await``
416
+ from async code without blocking the event loop.
417
+
418
+ Args:
419
+ project_id: Scoping project.
420
+ from_dt: ISO-8601 UTC start of reporting window.
421
+ to_dt: ISO-8601 UTC end of reporting window.
422
+ weights: Override the instance default weights.
423
+
424
+ Returns:
425
+ :class:`~spanforge.sdk._types.TrustScorecardResponse` — same as
426
+ :meth:`get_scorecard`.
427
+ """
428
+ import asyncio
429
+ import functools
430
+
431
+ loop = asyncio.get_event_loop()
432
+ return await loop.run_in_executor(
433
+ None,
434
+ functools.partial(
435
+ self.get_scorecard,
436
+ project_id,
437
+ from_dt=from_dt,
438
+ to_dt=to_dt,
439
+ weights=weights,
440
+ ),
441
+ )
442
+
443
+ # ------------------------------------------------------------------
444
+ # Status
445
+ # ------------------------------------------------------------------
446
+
447
+ def get_status(self) -> TrustStatusInfo:
448
+ """Return T.R.U.S.T. service health information."""
449
+ from spanforge.sdk import sf_audit
450
+
451
+ try:
452
+ total = len(sf_audit._store._trust_records)
453
+ except Exception:
454
+ total = 0
455
+
456
+ return TrustStatusInfo(
457
+ status="ok",
458
+ dimension_count=5,
459
+ total_trust_records=total,
460
+ pipelines_registered=5,
461
+ last_scorecard_computed=self._last_computed,
462
+ )
463
+
464
+ # ------------------------------------------------------------------
465
+ # Helpers
466
+ # ------------------------------------------------------------------
467
+
468
+ @staticmethod
469
+ def _utc_now_iso() -> str:
470
+ return (
471
+ datetime.now(tz=timezone.utc).isoformat(timespec="microseconds").replace("+00:00", "Z")
472
+ )
@@ -0,0 +1,41 @@
1
+ """Type stubs for spanforge.sdk.trust (DX-001)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from spanforge.sdk._base import SFClientConfig, SFServiceClient
6
+ from spanforge.sdk._types import (
7
+ TrustBadgeResult,
8
+ TrustDimensionWeights,
9
+ TrustHistoryEntry,
10
+ TrustScorecardResponse,
11
+ TrustStatusInfo,
12
+ )
13
+
14
+ class SFTrustClient(SFServiceClient):
15
+ def __init__(
16
+ self,
17
+ config: SFClientConfig,
18
+ *,
19
+ weights: TrustDimensionWeights | None = None,
20
+ ) -> None: ...
21
+ def get_scorecard(
22
+ self,
23
+ project_id: str | None = None,
24
+ *,
25
+ from_dt: str | None = None,
26
+ to_dt: str | None = None,
27
+ weights: TrustDimensionWeights | None = None,
28
+ ) -> TrustScorecardResponse: ...
29
+ def get_history(
30
+ self,
31
+ project_id: str | None = None,
32
+ *,
33
+ from_dt: str | None = None,
34
+ to_dt: str | None = None,
35
+ buckets: int = 10,
36
+ ) -> list[TrustHistoryEntry]: ...
37
+ def get_badge(
38
+ self,
39
+ project_id: str | None = None,
40
+ ) -> TrustBadgeResult: ...
41
+ def get_status(self) -> TrustStatusInfo: ...