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.
- spanforge/__init__.py +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- 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
|
+
)
|
spanforge/sdk/trust.pyi
ADDED
|
@@ -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: ...
|