patchr 0.1.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 (116) hide show
  1. apps/__init__.py +2 -0
  2. apps/api/__init__.py +2 -0
  3. apps/api/main.py +652 -0
  4. apps/benchmarks/__init__.py +1 -0
  5. apps/benchmarks/main.py +20 -0
  6. apps/sandbox/__init__.py +1 -0
  7. apps/sandbox/main.py +20 -0
  8. apps/worker/__init__.py +2 -0
  9. apps/worker/main.py +15 -0
  10. apps/worker/verify.py +14 -0
  11. patchr/__init__.py +12 -0
  12. patchr/sdk/__init__.py +20 -0
  13. patchr/sdk/client.py +12 -0
  14. patchr-0.1.0.dist-info/METADATA +137 -0
  15. patchr-0.1.0.dist-info/RECORD +116 -0
  16. patchr-0.1.0.dist-info/WHEEL +5 -0
  17. patchr-0.1.0.dist-info/entry_points.txt +5 -0
  18. patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
  19. patchr-0.1.0.dist-info/top_level.txt +3 -0
  20. picux/__init__.py +6 -0
  21. picux/agents/__init__.py +5 -0
  22. picux/agents/registry.py +204 -0
  23. picux/api/__init__.py +5 -0
  24. picux/api/service.py +5075 -0
  25. picux/audit/__init__.py +31 -0
  26. picux/audit/activity.py +97 -0
  27. picux/audit/observability.py +55 -0
  28. picux/audit/verification/__init__.py +21 -0
  29. picux/audit/verification/ledger.py +633 -0
  30. picux/benchmarks/__init__.py +5 -0
  31. picux/benchmarks/local.py +286 -0
  32. picux/config.py +140 -0
  33. picux/contracts/__init__.py +22 -0
  34. picux/contracts/handshake.py +122 -0
  35. picux/contracts/integration.py +385 -0
  36. picux/contracts/openapi.py +187 -0
  37. picux/contracts/protocol_map.py +152 -0
  38. picux/contracts/routes.py +980 -0
  39. picux/contracts/schema_catalog.py +125 -0
  40. picux/core/__init__.py +17 -0
  41. picux/core/models.py +148 -0
  42. picux/core/router.py +131 -0
  43. picux/core/runtime.py +42 -0
  44. picux/core/state_machine.py +38 -0
  45. picux/domains/__init__.py +2 -0
  46. picux/domains/bridge/HostRun.py +1104 -0
  47. picux/domains/bridge/__init__.py +6 -0
  48. picux/domains/bridge/engine.py +345 -0
  49. picux/domains/hunt/__init__.py +6 -0
  50. picux/domains/hunt/engine.py +307 -0
  51. picux/domains/hunt/models.py +88 -0
  52. picux/domains/pay/__init__.py +16 -0
  53. picux/domains/pay/adapters.py +607 -0
  54. picux/domains/pay/engine.py +950 -0
  55. picux/domains/pay/models.py +95 -0
  56. picux/domains/proxy/__init__.py +5 -0
  57. picux/domains/proxy/engine.py +466 -0
  58. picux/domains/resolve/__init__.py +5 -0
  59. picux/domains/resolve/engine.py +546 -0
  60. picux/orchestrator/__init__.py +3 -0
  61. picux/orchestrator/engine.py +2840 -0
  62. picux/portals/__init__.py +17 -0
  63. picux/portals/templates.py +272 -0
  64. picux/protocols/__init__.py +1 -0
  65. picux/protocols/a2a/__init__.py +6 -0
  66. picux/protocols/a2a/client.py +51 -0
  67. picux/protocols/a2a/envelope.py +132 -0
  68. picux/protocols/mcp/__init__.py +7 -0
  69. picux/protocols/mcp/client.py +69 -0
  70. picux/protocols/mcp/contract.py +67 -0
  71. picux/protocols/mcp/server.py +76 -0
  72. picux/sandbox/__init__.py +6 -0
  73. picux/sandbox/midnight_arbitrage.py +215 -0
  74. picux/sandbox/models.py +90 -0
  75. picux/sdk/__init__.py +13 -0
  76. picux/sdk/client.py +768 -0
  77. picux/sdk/external.py +245 -0
  78. picux/security/__init__.py +18 -0
  79. picux/security/auth.py +86 -0
  80. picux/security/config_validator.py +58 -0
  81. picux/security/policy.py +158 -0
  82. picux/security/secrets.py +144 -0
  83. picux/signals/__init__.py +1 -0
  84. picux/signals/community/__init__.py +24 -0
  85. picux/signals/community/adapters/__init__.py +7 -0
  86. picux/signals/community/adapters/reddit.py +37 -0
  87. picux/signals/community/adapters/shopify.py +23 -0
  88. picux/signals/community/adapters/web.py +23 -0
  89. picux/signals/community/disambiguation.py +51 -0
  90. picux/signals/community/intake.py +227 -0
  91. picux/signals/community/models.py +102 -0
  92. picux/signals/community/rules.py +91 -0
  93. picux/signals/community/scoring.py +64 -0
  94. picux/storage/__init__.py +41 -0
  95. picux/storage/agents.py +50 -0
  96. picux/storage/cases.py +440 -0
  97. picux/storage/channels.py +476 -0
  98. picux/storage/connectors.py +411 -0
  99. picux/storage/envelopes.py +137 -0
  100. picux/storage/escrows.py +168 -0
  101. picux/storage/events.py +989 -0
  102. picux/storage/keyspace.py +60 -0
  103. picux/storage/mandates.py +107 -0
  104. picux/storage/portals.py +222 -0
  105. picux/storage/postgres.py +2049 -0
  106. picux/storage/providers.py +148 -0
  107. picux/storage/proxy.py +231 -0
  108. picux/storage/receipts.py +131 -0
  109. picux/storage/signals.py +147 -0
  110. picux/storage/tasks.py +179 -0
  111. picux/tools/__init__.py +11 -0
  112. picux/tools/shared.py +2048 -0
  113. picux/verification/__init__.py +5 -0
  114. picux/verification/rollout.py +183 -0
  115. picux/workflows/__init__.py +5 -0
  116. picux/workflows/templates.py +74 -0
@@ -0,0 +1,633 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+
11
+ ALLOWED_RECEIPT_STATUS = {"settled", "rejected", "frozen"}
12
+ Clock = Callable[[], datetime | str]
13
+
14
+
15
+ def canonicalHash(payload: dict[str, Any]) -> str:
16
+ """Return a stable digest for agent-readable audit payloads."""
17
+
18
+ return hashlib.sha256(_canonicalJson(payload).encode("utf-8")).hexdigest()
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class EvidenceArtifact:
23
+ artifactId: str
24
+ kind: str
25
+ source: str
26
+ payload: dict[str, Any]
27
+ payloadHash: str
28
+ createdAt: str
29
+ meta: dict[str, Any] = field(default_factory=dict)
30
+
31
+ @classmethod
32
+ def fromPayload(
33
+ cls,
34
+ payload: dict[str, Any],
35
+ *,
36
+ clock: Clock,
37
+ ) -> "EvidenceArtifact":
38
+ kind = str(payload.get("kind", payload.get("type", "evidence")) or "evidence")
39
+ source = str(payload.get("source", payload.get("uri", "inline")) or "inline")
40
+ body = payload.get("payload") if isinstance(payload.get("payload"), dict) else payload.get("data", {})
41
+ if not isinstance(body, dict):
42
+ body = {"value": body}
43
+ meta = payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {}
44
+ createdAt = _time(clock)
45
+ base = {
46
+ "kind": kind,
47
+ "source": source,
48
+ "payload": body,
49
+ "meta": meta,
50
+ }
51
+ return cls(
52
+ artifactId="art_" + canonicalHash(base)[:24],
53
+ kind=kind,
54
+ source=source,
55
+ payload=body,
56
+ payloadHash=canonicalHash(body),
57
+ createdAt=createdAt,
58
+ meta=meta,
59
+ )
60
+
61
+ def toMap(self) -> dict[str, Any]:
62
+ return {
63
+ "artifactId": self.artifactId,
64
+ "kind": self.kind,
65
+ "source": self.source,
66
+ "payload": self.payload,
67
+ "payloadHash": self.payloadHash,
68
+ "createdAt": self.createdAt,
69
+ "meta": self.meta,
70
+ }
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class ProofOfValueRecord:
75
+ povId: str
76
+ taskId: str
77
+ domain: str
78
+ rules: tuple[str, ...]
79
+ value: dict[str, Any]
80
+ artifacts: tuple[dict[str, Any], ...]
81
+ prevHash: str
82
+ chainHash: str
83
+ createdAt: str
84
+ meta: dict[str, Any] = field(default_factory=dict)
85
+
86
+ @classmethod
87
+ def fromPayload(cls, payload: dict[str, Any], *, clock: Clock, prevHash: str = "") -> "ProofOfValueRecord":
88
+ taskId = str(payload.get("taskId", "") or "")
89
+ domain = str(payload.get("domain", "") or "")
90
+ rules = tuple(str(item) for item in payload.get("rules", []) if str(item).strip())
91
+ value = payload.get("value") if isinstance(payload.get("value"), dict) else {}
92
+ meta = payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {}
93
+ artifacts = tuple(_normalizeArtifact(item) for item in payload.get("artifacts", []) if isinstance(item, dict))
94
+ createdAt = _time(clock)
95
+ base = {
96
+ "taskId": taskId,
97
+ "domain": domain,
98
+ "rules": list(rules),
99
+ "value": value,
100
+ "artifacts": list(artifacts),
101
+ "prevHash": prevHash,
102
+ "createdAt": createdAt,
103
+ "meta": meta,
104
+ }
105
+ chainHash = canonicalHash(base)
106
+ return cls(
107
+ povId="pov_" + chainHash[:24],
108
+ taskId=taskId,
109
+ domain=domain,
110
+ rules=rules,
111
+ value=value,
112
+ artifacts=artifacts,
113
+ prevHash=prevHash,
114
+ chainHash=chainHash,
115
+ createdAt=createdAt,
116
+ meta=meta,
117
+ )
118
+
119
+ def toMap(self) -> dict[str, Any]:
120
+ return {
121
+ "povId": self.povId,
122
+ "taskId": self.taskId,
123
+ "domain": self.domain,
124
+ "rules": list(self.rules),
125
+ "value": self.value,
126
+ "artifacts": list(self.artifacts),
127
+ "prevHash": self.prevHash,
128
+ "chainHash": self.chainHash,
129
+ "createdAt": self.createdAt,
130
+ "meta": self.meta,
131
+ }
132
+
133
+
134
+ class VerificationLedger:
135
+ """Deterministic audit ledger for evidence, proof-of-value, receipts, and proof packs."""
136
+
137
+ def __init__(self, *, clock: Clock | None = None, backing: Any | None = None) -> None:
138
+ self.clock = clock or (lambda: datetime.now(timezone.utc))
139
+ self.backing = backing
140
+ self._artifacts: dict[str, dict[str, Any]] = {}
141
+ self._pov: dict[str, dict[str, Any]] = {}
142
+ self._proofPacks: dict[str, dict[str, Any]] = {}
143
+ self._chainTail = ""
144
+
145
+ def recordEvidence(self, payload: dict[str, Any]) -> dict[str, Any]:
146
+ artifact = EvidenceArtifact.fromPayload(payload, clock=self.clock).toMap()
147
+ self._artifacts[artifact["artifactId"]] = artifact
148
+ if self._backingEnabled() and hasattr(self.backing, "insertEvidence"):
149
+ self.backing.insertEvidence(artifact)
150
+ return {"ok": True, "artifact": artifact}
151
+
152
+ def recordPov(self, payload: dict[str, Any]) -> dict[str, Any]:
153
+ errors = _povInputErrors(payload)
154
+ if errors:
155
+ return {"ok": False, "errors": errors}
156
+ normalized = dict(payload)
157
+ normalized["artifacts"] = self._resolveArtifacts(payload.get("artifacts", []))
158
+ prevHash = str(payload.get("prevHash", self._chainTail) or "")
159
+ pov = ProofOfValueRecord.fromPayload(normalized, clock=self.clock, prevHash=prevHash).toMap()
160
+ self._pov[pov["povId"]] = pov
161
+ self._chainTail = pov["chainHash"]
162
+ return {"ok": True, "pov": pov}
163
+
164
+ def verifyReceipt(self, payload: dict[str, Any]) -> dict[str, Any]:
165
+ receipt = payload.get("receipt") if isinstance(payload.get("receipt"), dict) else payload
166
+ pov = payload.get("pov") if isinstance(payload.get("pov"), dict) else {}
167
+ if not pov and receipt.get("povRef"):
168
+ pov = self._pov.get(str(receipt.get("povRef")), {})
169
+
170
+ errors = self._receiptErrors(receipt, pov)
171
+ return {
172
+ "ok": not errors,
173
+ "receiptId": str(receipt.get("receiptId", "") or ""),
174
+ "povId": str(pov.get("povId", "") or receipt.get("povRef", "") or ""),
175
+ "errors": errors,
176
+ "hash": receiptHash(receipt),
177
+ }
178
+
179
+ def verifyChain(self, records: list[dict[str, Any]]) -> dict[str, Any]:
180
+ errors: list[str] = []
181
+ prevHash = ""
182
+ for index, record in enumerate(records):
183
+ expectedPrev = str(record.get("prevHash", "") or "")
184
+ if expectedPrev != prevHash:
185
+ errors.append(f"chainBreak:{index}")
186
+ expected = povChainHash(record)
187
+ if str(record.get("chainHash", "") or "") != expected:
188
+ errors.append(f"hashMismatch:{index}")
189
+ prevHash = str(record.get("chainHash", "") or "")
190
+ return {"ok": not errors, "errors": errors, "count": len(records)}
191
+
192
+ def createProofPack(self, payload: dict[str, Any]) -> dict[str, Any]:
193
+ artifacts = self._resolveArtifacts(payload.get("artifacts", []))
194
+ povRecords = self._resolvePov(payload.get("pov", payload.get("povRecords", [])))
195
+ receipts = _cleanDicts(payload.get("receipts", []))
196
+ decisions = _cleanDicts(payload.get("decisions", payload.get("decisionLogs", [])))
197
+ actors = _cleanDicts(payload.get("actors", []))
198
+ snapshots = _cleanDicts(payload.get("sourceSnapshots", payload.get("snapshots", [])))
199
+ typedEvidence = _cleanDicts(payload.get("typedEvidence", payload.get("vaultEvidence", [])))
200
+ fraudIndicators = _cleanDicts(payload.get("fraudIndicators", []))
201
+ sourceClasses = _cleanDicts(payload.get("sourceClasses", []))
202
+ caseOpsTimeline = _cleanDicts(payload.get("caseOpsTimeline", []))
203
+ timeline = _proofTimeline(payload, artifacts, povRecords, receipts, decisions, snapshots)
204
+ share = _proofShare(payload.get("share", {}))
205
+ createdAt = _time(self.clock)
206
+ base = {
207
+ "status": str(payload.get("status", "ready") or "ready"),
208
+ "taskId": str(payload.get("taskId", "") or ""),
209
+ "domain": str(payload.get("domain", "") or ""),
210
+ "title": str(payload.get("title", "Portable case file") or "Portable case file"),
211
+ "summary": str(payload.get("summary", "") or ""),
212
+ "artifacts": artifacts,
213
+ "pov": povRecords,
214
+ "receipts": receipts,
215
+ "decisions": decisions,
216
+ "actors": actors,
217
+ "timeline": timeline,
218
+ "vault": {
219
+ "kind": "autoAuditVault",
220
+ "sourceSnapshots": snapshots,
221
+ "decisionLogs": decisions,
222
+ "complaintEvidence": artifacts,
223
+ "typedEvidence": typedEvidence,
224
+ "fraudIndicators": fraudIndicators,
225
+ "sourceClasses": sourceClasses,
226
+ "pov": povRecords,
227
+ },
228
+ "share": share,
229
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
230
+ "caseOpsTimeline": caseOpsTimeline,
231
+ "createdAt": createdAt,
232
+ }
233
+ chainHash = canonicalHash(base)
234
+ packId = _proofPackId(payload.get("packId", ""), chainHash)
235
+ proofCard = _proofCard(payload, packId, chainHash, base)
236
+ pack = {"packId": packId, **base, "proofCard": proofCard, "chainHash": chainHash}
237
+ self._proofPacks[packId] = pack
238
+ return {"ok": True, "proofPack": pack}
239
+
240
+ def getPov(self, povId: str) -> dict[str, Any]:
241
+ pov = self._pov.get(povId)
242
+ if not pov:
243
+ return {"ok": False, "error": "povNotFound", "povId": povId}
244
+ return {"ok": True, "pov": pov}
245
+
246
+ def getEvidence(self, artifactId: str) -> dict[str, Any]:
247
+ artifactId = str(artifactId or "")
248
+ artifact = self._artifacts.get(artifactId)
249
+ if artifact is None and self._backingEnabled() and hasattr(self.backing, "fetchEvidence"):
250
+ artifact = self.backing.fetchEvidence(artifactId)
251
+ if artifact:
252
+ self._artifacts[artifactId] = artifact
253
+ if not artifact:
254
+ return {"ok": False, "error": "artifactNotFound", "artifactId": artifactId}
255
+ return {"ok": True, "artifact": artifact}
256
+
257
+ def listEvidence(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
258
+ self._loadEvidence()
259
+ filters = filters or {}
260
+ kind = str(filters.get("kind", "") or "")
261
+ source = str(filters.get("source", "") or "")
262
+ payloadHash = str(filters.get("payloadHash", "") or "")
263
+ records = list(self._artifacts.values())
264
+ if kind:
265
+ records = [record for record in records if record.get("kind") == kind]
266
+ if source:
267
+ records = [record for record in records if record.get("source") == source]
268
+ if payloadHash:
269
+ records = [record for record in records if record.get("payloadHash") == payloadHash]
270
+ records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("artifactId", ""))), reverse=True)
271
+ limit = _limit(filters.get("limit", 100))
272
+ return {
273
+ "ok": True,
274
+ "artifacts": [dict(record) for record in records[:limit]],
275
+ "count": min(len(records), limit),
276
+ }
277
+
278
+ def getProofPack(self, packId: str) -> dict[str, Any]:
279
+ packId = str(packId or "")
280
+ proofPack = self._proofPacks.get(packId)
281
+ if not proofPack:
282
+ return {"ok": False, "error": "proofPackNotFound", "packId": packId}
283
+ return {"ok": True, "proofPack": dict(proofPack)}
284
+
285
+ def listProofPacks(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
286
+ filters = filters or {}
287
+ taskId = str(filters.get("taskId", "") or "")
288
+ domain = str(filters.get("domain", "") or "")
289
+ status = str(filters.get("status", "") or "")
290
+ records = list(self._proofPacks.values())
291
+ if taskId:
292
+ records = [record for record in records if record.get("taskId") == taskId]
293
+ if domain:
294
+ records = [record for record in records if record.get("domain") == domain]
295
+ if status:
296
+ records = [record for record in records if record.get("status") == status]
297
+ records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("packId", ""))), reverse=True)
298
+ limit = _limit(filters.get("limit", 100))
299
+ return {
300
+ "ok": True,
301
+ "proofPacks": [dict(record) for record in records[:limit]],
302
+ "count": min(len(records), limit),
303
+ }
304
+
305
+ def _loadEvidence(self) -> None:
306
+ if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listEvidence"):
307
+ return
308
+ for artifact in self.backing.listEvidence():
309
+ artifactId = str(artifact.get("artifactId", "") or "")
310
+ if artifactId:
311
+ self._artifacts[artifactId] = artifact
312
+
313
+ def _backingEnabled(self) -> bool:
314
+ if self.backing is None:
315
+ return False
316
+ return bool(getattr(self.backing, "enabled", True))
317
+
318
+ def _resolveArtifacts(self, artifacts: Any) -> list[dict[str, Any]]:
319
+ resolved: list[dict[str, Any]] = []
320
+ if not isinstance(artifacts, list):
321
+ return resolved
322
+ for item in artifacts:
323
+ if isinstance(item, str) and item in self._artifacts:
324
+ resolved.append(self._artifacts[item])
325
+ elif isinstance(item, dict):
326
+ artifactId = str(item.get("artifactId", "") or "")
327
+ resolved.append(self._artifacts.get(artifactId, _normalizeArtifact(item)))
328
+ return resolved
329
+
330
+ def _resolvePov(self, records: Any) -> list[dict[str, Any]]:
331
+ resolved: list[dict[str, Any]] = []
332
+ if isinstance(records, dict):
333
+ records = [records]
334
+ if isinstance(records, str):
335
+ records = [records]
336
+ if not isinstance(records, list):
337
+ return resolved
338
+ for item in records:
339
+ if isinstance(item, str) and item in self._pov:
340
+ resolved.append(dict(self._pov[item]))
341
+ elif isinstance(item, dict):
342
+ povId = str(item.get("povId", "") or "")
343
+ resolved.append(dict(self._pov.get(povId, item)))
344
+ return resolved
345
+
346
+ @staticmethod
347
+ def _receiptErrors(receipt: dict[str, Any], pov: dict[str, Any]) -> list[str]:
348
+ errors: list[str] = []
349
+ if not receipt:
350
+ return ["missing:receipt"]
351
+ if str(receipt.get("receiptId", "") or "") != receiptId(receipt):
352
+ errors.append("receiptHashMismatch")
353
+ if str(receipt.get("status", "") or "") not in ALLOWED_RECEIPT_STATUS:
354
+ errors.append("invalid:status")
355
+ if not str(receipt.get("taskId", "") or ""):
356
+ errors.append("missing:taskId")
357
+ if not str(receipt.get("mandateId", "") or ""):
358
+ errors.append("missing:mandateId")
359
+
360
+ povRef = str(receipt.get("povRef", "") or "")
361
+ povId = str(pov.get("povId", "") or "")
362
+ if povRef and povId and povRef != povId:
363
+ errors.append("povMismatch")
364
+ if pov and str(pov.get("taskId", "") or "") and str(receipt.get("taskId", "") or "") != str(
365
+ pov.get("taskId", "") or ""
366
+ ):
367
+ errors.append("taskMismatch")
368
+ value = pov.get("value", {}) if isinstance(pov.get("value"), dict) else {}
369
+ amount = value.get("amount")
370
+ currency = str(value.get("currency", "") or "")
371
+ receiptAmount = _amount(receipt.get("amount", 0.0))
372
+ povAmount = _amount(amount)
373
+ if amount is not None and (receiptAmount is None or povAmount is None):
374
+ errors.append("invalid:amount")
375
+ elif amount is not None and round(receiptAmount or 0.0, 2) != round(povAmount or 0.0, 2):
376
+ errors.append("amountMismatch")
377
+ if currency and str(receipt.get("currency", "") or "").upper() != currency.upper():
378
+ errors.append("currencyMismatch")
379
+ return errors
380
+
381
+
382
+ def receiptHash(receipt: dict[str, Any]) -> str:
383
+ return canonicalHash(receiptBase(receipt))
384
+
385
+
386
+ def receiptId(receipt: dict[str, Any]) -> str:
387
+ return "rcpt_" + receiptHash(receipt)[:24]
388
+
389
+
390
+ def receiptBase(receipt: dict[str, Any]) -> dict[str, Any]:
391
+ return {
392
+ "mandateId": str(receipt.get("mandateId", "") or ""),
393
+ "taskId": str(receipt.get("taskId", "") or ""),
394
+ "status": str(receipt.get("status", "") or ""),
395
+ "amount": receipt.get("amount", 0.0),
396
+ "currency": str(receipt.get("currency", "") or ""),
397
+ "povRef": str(receipt.get("povRef", "") or ""),
398
+ "errors": receipt.get("errors", []) if isinstance(receipt.get("errors", []), list) else [],
399
+ "createdAt": str(receipt.get("createdAt", "") or ""),
400
+ }
401
+
402
+
403
+ def povChainHash(record: dict[str, Any]) -> str:
404
+ base = {
405
+ "taskId": str(record.get("taskId", "") or ""),
406
+ "domain": str(record.get("domain", "") or ""),
407
+ "rules": list(record.get("rules", []) if isinstance(record.get("rules"), list) else []),
408
+ "value": record.get("value", {}) if isinstance(record.get("value"), dict) else {},
409
+ "artifacts": list(record.get("artifacts", []) if isinstance(record.get("artifacts"), list) else []),
410
+ "prevHash": str(record.get("prevHash", "") or ""),
411
+ "createdAt": str(record.get("createdAt", "") or ""),
412
+ "meta": record.get("meta", {}) if isinstance(record.get("meta"), dict) else {},
413
+ }
414
+ return canonicalHash(base)
415
+
416
+
417
+ def _canonicalJson(payload: dict[str, Any]) -> str:
418
+ return json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
419
+
420
+
421
+ def _normalizeArtifact(item: dict[str, Any]) -> dict[str, Any]:
422
+ if "artifactId" in item and "payloadHash" in item:
423
+ return item
424
+ payload = item.get("payload") if isinstance(item.get("payload"), dict) else item
425
+ return {
426
+ "artifactId": str(item.get("artifactId", "") or "art_" + canonicalHash(payload)[:24]),
427
+ "kind": str(item.get("kind", "evidence") or "evidence"),
428
+ "source": str(item.get("source", "inline") or "inline"),
429
+ "payload": payload,
430
+ "payloadHash": canonicalHash(payload),
431
+ "createdAt": str(item.get("createdAt", "") or ""),
432
+ "meta": item.get("meta", {}) if isinstance(item.get("meta"), dict) else {},
433
+ }
434
+
435
+
436
+ def _proofTimeline(
437
+ payload: dict[str, Any],
438
+ artifacts: list[dict[str, Any]],
439
+ povRecords: list[dict[str, Any]],
440
+ receipts: list[dict[str, Any]],
441
+ decisions: list[dict[str, Any]],
442
+ snapshots: list[dict[str, Any]],
443
+ ) -> list[dict[str, Any]]:
444
+ entries = _cleanDicts(payload.get("timeline", []))
445
+ for artifact in artifacts:
446
+ entries.append(
447
+ {
448
+ "at": str(artifact.get("createdAt", "") or ""),
449
+ "kind": "evidence",
450
+ "ref": str(artifact.get("artifactId", "") or ""),
451
+ "label": str(artifact.get("kind", "evidence") or "evidence"),
452
+ "source": str(artifact.get("source", "") or ""),
453
+ }
454
+ )
455
+ for pov in povRecords:
456
+ entries.append(
457
+ {
458
+ "at": str(pov.get("createdAt", "") or ""),
459
+ "kind": "pov",
460
+ "ref": str(pov.get("povId", "") or ""),
461
+ "label": str(pov.get("domain", "proof") or "proof"),
462
+ }
463
+ )
464
+ for receipt in receipts:
465
+ entries.append(
466
+ {
467
+ "at": str(receipt.get("createdAt", "") or ""),
468
+ "kind": "receipt",
469
+ "ref": str(receipt.get("receiptId", "") or ""),
470
+ "label": str(receipt.get("status", "receipt") or "receipt"),
471
+ }
472
+ )
473
+ for decision in decisions:
474
+ entries.append(
475
+ {
476
+ "at": str(decision.get("at", decision.get("createdAt", "")) or ""),
477
+ "kind": "decision",
478
+ "ref": str(decision.get("decisionId", decision.get("ref", "")) or ""),
479
+ "label": str(decision.get("label", decision.get("reason", "decision")) or "decision"),
480
+ }
481
+ )
482
+ for snapshot in snapshots:
483
+ entries.append(
484
+ {
485
+ "at": str(snapshot.get("capturedAt", snapshot.get("at", "")) or ""),
486
+ "kind": "sourceSnapshot",
487
+ "ref": str(snapshot.get("snapshotId", snapshot.get("url", "")) or ""),
488
+ "label": str(snapshot.get("label", "source snapshot") or "source snapshot"),
489
+ }
490
+ )
491
+ return sorted(entries, key=lambda item: (str(item.get("at", "")), str(item.get("kind", "")), str(item.get("ref", ""))))
492
+
493
+
494
+ def _proofShare(value: Any) -> dict[str, Any]:
495
+ raw = value if isinstance(value, dict) else {}
496
+ formats = raw.get("formats", ["json"]) if isinstance(raw.get("formats", ["json"]), list) else ["json"]
497
+ share = {
498
+ "type": str(raw.get("type", "portableCaseFile") or "portableCaseFile"),
499
+ "label": str(raw.get("label", "From opaque workflow to portable case file.") or ""),
500
+ "audience": str(raw.get("audience", "external") or "external"),
501
+ "redacted": bool(raw.get("redacted", True)),
502
+ "formats": _cleanStrings(formats),
503
+ }
504
+ if raw.get("url"):
505
+ share["url"] = str(raw.get("url") or "")
506
+ return share
507
+
508
+
509
+ def _proofCard(payload: dict[str, Any], packId: str, chainHash: str, base: dict[str, Any]) -> dict[str, Any]:
510
+ raw = payload.get("proofCard", {}) if isinstance(payload.get("proofCard"), dict) else {}
511
+ openQuestions = _cleanStrings(raw.get("openQuestions", payload.get("openQuestions", [])))
512
+ escalation = raw.get("escalation", payload.get("escalation", {}))
513
+ escalation = escalation if isinstance(escalation, dict) else {}
514
+ if openQuestions and "recommended" not in escalation:
515
+ escalation = {"recommended": True, "routeDomain": "proxy", "reason": "openQuestions"}
516
+ elif not escalation:
517
+ escalation = {"recommended": False, "routeDomain": "", "reason": ""}
518
+ evidenceStatus = str(raw.get("evidenceStatus", payload.get("evidenceStatus", "")) or "")
519
+ if not evidenceStatus:
520
+ evidenceStatus = _evidenceStatus(base)
521
+ confidence = raw.get("confidence", payload.get("confidence", None))
522
+ if confidence is None:
523
+ confidence = _proofConfidence(base, openQuestions, escalation)
524
+ cardBase = {
525
+ "packId": packId,
526
+ "packHash": chainHash,
527
+ "title": str(raw.get("title", base.get("title", "Proof card")) or "Proof card"),
528
+ "summary": str(raw.get("summary", base.get("summary", "")) or ""),
529
+ "evidenceStatus": evidenceStatus,
530
+ "confidence": round(max(0.0, min(1.0, float(confidence or 0.0))), 3),
531
+ "openQuestions": openQuestions,
532
+ "recommendedRestraint": str(raw.get("recommendedRestraint", payload.get("recommendedRestraint", _restraint(openQuestions, escalation))) or ""),
533
+ "escalation": escalation,
534
+ }
535
+ cardHash = canonicalHash(cardBase)
536
+ cardId = "card_" + cardHash[:24]
537
+ return {
538
+ "cardId": cardId,
539
+ "kind": "proofCard",
540
+ **cardBase,
541
+ "cite": f"picux:proofPack:{packId}#{chainHash[:12]}",
542
+ "embed": {"href": f"/v1/audit/proofPacks/{packId}", "format": "application/json"},
543
+ "post": {
544
+ "title": cardBase["title"],
545
+ "summary": cardBase["summary"],
546
+ "evidenceStatus": evidenceStatus,
547
+ "confidence": cardBase["confidence"],
548
+ },
549
+ "cardHash": cardHash,
550
+ }
551
+
552
+
553
+ def _evidenceStatus(base: dict[str, Any]) -> str:
554
+ if base.get("artifacts") and base.get("pov"):
555
+ return "verified"
556
+ if base.get("artifacts"):
557
+ return "sourceBound"
558
+ return "needsEvidence"
559
+
560
+
561
+ def _proofConfidence(base: dict[str, Any], openQuestions: list[str], escalation: dict[str, Any]) -> float:
562
+ confidence = 0.35
563
+ if base.get("artifacts"):
564
+ confidence += 0.25
565
+ if base.get("pov"):
566
+ confidence += 0.20
567
+ if base.get("decisions"):
568
+ confidence += 0.10
569
+ if openQuestions:
570
+ confidence -= min(0.20, 0.05 * len(openQuestions))
571
+ if escalation.get("recommended"):
572
+ confidence -= 0.10
573
+ return confidence
574
+
575
+
576
+ def _restraint(openQuestions: list[str], escalation: dict[str, Any]) -> str:
577
+ if escalation.get("recommended"):
578
+ return "escalateBeforePublishing"
579
+ if openQuestions:
580
+ return "citeWithOpenQuestions"
581
+ return "shareableWithSource"
582
+
583
+
584
+ def _cleanDicts(value: Any) -> list[dict[str, Any]]:
585
+ if isinstance(value, dict):
586
+ return [dict(value)]
587
+ if not isinstance(value, list):
588
+ return []
589
+ return [dict(item) for item in value if isinstance(item, dict)]
590
+
591
+
592
+ def _cleanStrings(value: Any) -> list[str]:
593
+ if not isinstance(value, list):
594
+ return []
595
+ return [str(item) for item in value if str(item).strip()]
596
+
597
+
598
+ def _proofPackId(value: Any, chainHash: str) -> str:
599
+ raw = str(value or "")
600
+ if raw.startswith("pack_") and len(raw) == 29 and all(char in "0123456789abcdef" for char in raw[5:]):
601
+ return raw
602
+ return "pack_" + chainHash[:24]
603
+
604
+
605
+ def _povInputErrors(payload: dict[str, Any]) -> list[str]:
606
+ errors: list[str] = []
607
+ if not str(payload.get("taskId", "") or ""):
608
+ errors.append("missing:taskId")
609
+ if not str(payload.get("domain", "") or ""):
610
+ errors.append("missing:domain")
611
+ return errors
612
+
613
+
614
+ def _amount(value: Any) -> float | None:
615
+ try:
616
+ return float(value or 0.0)
617
+ except (TypeError, ValueError):
618
+ return None
619
+
620
+
621
+ def _limit(value: Any) -> int:
622
+ try:
623
+ parsed = int(value)
624
+ except (TypeError, ValueError):
625
+ return 100
626
+ return max(1, min(parsed, 500))
627
+
628
+
629
+ def _time(clock: Clock) -> str:
630
+ value = clock()
631
+ if isinstance(value, datetime):
632
+ return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
633
+ return str(value)
@@ -0,0 +1,5 @@
1
+ """Local benchmark evidence primitives."""
2
+
3
+ from .local import LocalBenchmark
4
+
5
+ __all__ = ["LocalBenchmark"]