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
picux/storage/cases.py ADDED
@@ -0,0 +1,440 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import time
7
+ from typing import Any
8
+
9
+
10
+ CASE_STATUSES = {"open", "inReview", "awaitingStakeholder", "awaitingProxy", "resolved", "closed", "archived"}
11
+ DEADLINE_STATUSES = {"open", "met", "missed", "canceled"}
12
+ STAKEHOLDER_ROLES = {"claimant", "merchant", "carrier", "bank", "advisor", "reviewer", "proxy", "other"}
13
+ NOTE_VISIBILITIES = {"internal", "client", "shared", "private"}
14
+ ACTION_STATUSES = {"open", "inProgress", "blocked", "done", "canceled"}
15
+
16
+
17
+ class CaseBook:
18
+ """Formal case workspace for disputes, audits, evidence bundles, and collaboration."""
19
+
20
+ def __init__(self, *, backing: Any | None = None) -> None:
21
+ self.backing = backing
22
+ self._cases: dict[str, dict[str, Any]] = {}
23
+ self._events: dict[str, dict[str, Any]] = {}
24
+
25
+ def createCase(self, payload: dict[str, Any]) -> dict[str, Any]:
26
+ now = int(time.time())
27
+ caseId = str(payload.get("caseId", "") or _stableId("case", _caseIdentity(payload)))
28
+ current = self.getCase(caseId)
29
+ createdAt = int(current.get("case", {}).get("createdAt", now) if current.get("ok") else now)
30
+ record = _caseFromPayload({**payload, "caseId": caseId}, createdAt=createdAt, updatedAt=now)
31
+ self._saveCase(record)
32
+ if not current.get("ok"):
33
+ self.addEvent(caseId, {"type": "case.created", "label": "Case workspace created", "actor": str(payload.get("actor", "picux") or "picux")})
34
+ return {"ok": True, "case": copy.deepcopy(record)}
35
+
36
+ def getCase(self, caseId: str) -> dict[str, Any]:
37
+ caseId = str(caseId or "")
38
+ record = self._cases.get(caseId)
39
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchCaseWorkspace"):
40
+ record = self.backing.fetchCaseWorkspace(caseId)
41
+ if record:
42
+ self._cases[caseId] = copy.deepcopy(record)
43
+ if not record:
44
+ return {"ok": False, "error": "caseNotFound", "caseId": caseId}
45
+ return {"ok": True, "case": copy.deepcopy(record)}
46
+
47
+ def listCases(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
48
+ self._loadCases()
49
+ filters = filters or {}
50
+ records = list(self._cases.values())
51
+ for key in ("status", "ownerId", "conversationId"):
52
+ value = str(filters.get(key, "") or "")
53
+ if value:
54
+ records = [record for record in records if str(record.get(key, "") or "") == value]
55
+ domain = str(filters.get("domain", "") or "")
56
+ if domain:
57
+ records = [record for record in records if domain in record.get("domains", [])]
58
+ records.sort(key=lambda item: (int(item.get("updatedAt", 0) or 0), str(item.get("caseId", ""))), reverse=True)
59
+ limit = _limit(filters.get("limit", 100))
60
+ return {"ok": True, "cases": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
61
+
62
+ def updateCase(self, caseId: str, updates: dict[str, Any]) -> dict[str, Any]:
63
+ current = self.getCase(caseId)
64
+ if not current.get("ok"):
65
+ return current
66
+ record = _caseFromPayload(
67
+ {**current["case"], **updates, "caseId": caseId},
68
+ createdAt=int(current["case"].get("createdAt", int(time.time())) or int(time.time())),
69
+ updatedAt=int(time.time()),
70
+ )
71
+ self._saveCase(record)
72
+ self.addEvent(caseId, {"type": "case.updated", "label": "Case workspace updated", "actor": str(updates.get("actor", "picux") or "picux")})
73
+ return {"ok": True, "case": copy.deepcopy(record)}
74
+
75
+ def addStakeholder(self, caseId: str, payload: dict[str, Any]) -> dict[str, Any]:
76
+ current = self.getCase(caseId)
77
+ if not current.get("ok"):
78
+ return current
79
+ stakeholder = _stakeholder(payload)
80
+ if not stakeholder.get("stakeholderId"):
81
+ stakeholder["stakeholderId"] = _stableId("stake", {"caseId": caseId, "name": stakeholder.get("name", ""), "role": stakeholder.get("role", "")})
82
+ case = current["case"]
83
+ stakeholders = [item for item in case.get("stakeholders", []) if isinstance(item, dict) and item.get("stakeholderId") != stakeholder["stakeholderId"]]
84
+ updated = self.updateCase(caseId, {"stakeholders": [*stakeholders, stakeholder]})
85
+ if updated.get("ok"):
86
+ updated["stakeholder"] = stakeholder
87
+ self.addEvent(caseId, {"type": "case.stakeholder.added", "label": f"Stakeholder added: {stakeholder.get('name', stakeholder.get('role', 'stakeholder'))}", "actor": "picux", "payload": stakeholder})
88
+ return updated
89
+
90
+ def addDeadline(self, caseId: str, payload: dict[str, Any]) -> dict[str, Any]:
91
+ current = self.getCase(caseId)
92
+ if not current.get("ok"):
93
+ return current
94
+ deadline = _deadline(payload)
95
+ if not deadline.get("deadlineId"):
96
+ deadline["deadlineId"] = _stableId("due", {"caseId": caseId, "label": deadline.get("label", ""), "dueAt": deadline.get("dueAt", "")})
97
+ case = current["case"]
98
+ deadlines = [item for item in case.get("deadlines", []) if isinstance(item, dict) and item.get("deadlineId") != deadline["deadlineId"]]
99
+ updated = self.updateCase(caseId, {"deadlines": [*deadlines, deadline]})
100
+ if updated.get("ok"):
101
+ updated["deadline"] = deadline
102
+ self.addEvent(caseId, {"type": "case.deadline.added", "label": f"Deadline added: {deadline.get('label', 'deadline')}", "actor": "picux", "payload": deadline})
103
+ return updated
104
+
105
+ def addAction(self, caseId: str, payload: dict[str, Any]) -> dict[str, Any]:
106
+ current = self.getCase(caseId)
107
+ if not current.get("ok"):
108
+ return current
109
+ action = _caseAction({**payload, "caseId": caseId})
110
+ if not action.get("actionId"):
111
+ action["actionId"] = _stableId("caseAction", {"caseId": caseId, "title": action.get("title", ""), "owner": action.get("owner", ""), "dueAt": action.get("dueAt", "")})
112
+ case = current["case"]
113
+ actions = [item for item in case.get("actions", []) if isinstance(item, dict) and item.get("actionId") != action["actionId"]]
114
+ updated = self.updateCase(caseId, {"actions": [*actions, action], "actor": str(payload.get("actor", action.get("owner", "picux")) or "picux")})
115
+ if updated.get("ok"):
116
+ updated["action"] = action
117
+ self.addEvent(caseId, {"type": "case.action.added", "label": f"Action added: {action.get('title', action.get('actionId', 'action'))}", "actor": str(payload.get("actor", "picux") or "picux"), "payload": action})
118
+ return updated
119
+
120
+ def updateAction(self, caseId: str, actionId: str, updates: dict[str, Any]) -> dict[str, Any]:
121
+ current = self.getCase(caseId)
122
+ if not current.get("ok"):
123
+ return current
124
+ actionId = str(actionId or "")
125
+ case = current["case"]
126
+ actions = [item for item in case.get("actions", []) if isinstance(item, dict)]
127
+ found = next((item for item in actions if str(item.get("actionId", "") or "") == actionId), None)
128
+ if not found:
129
+ return {"ok": False, "error": "caseActionNotFound", "caseId": caseId, "actionId": actionId}
130
+ nextAction = _caseAction({**found, **updates, "caseId": caseId, "actionId": actionId, "updatedAt": int(time.time())})
131
+ merged = [nextAction if str(item.get("actionId", "") or "") == actionId else item for item in actions]
132
+ updated = self.updateCase(caseId, {"actions": merged, "actor": str(updates.get("actor", nextAction.get("owner", "picux")) or "picux")})
133
+ if updated.get("ok"):
134
+ updated["action"] = nextAction
135
+ self.addEvent(caseId, {"type": "case.action.updated", "label": f"Action updated: {nextAction.get('title', actionId)}", "actor": str(updates.get("actor", "picux") or "picux"), "status": nextAction.get("status", ""), "payload": nextAction})
136
+ return updated
137
+
138
+ def listActions(self, caseId: str, filters: dict[str, Any] | None = None) -> dict[str, Any]:
139
+ current = self.getCase(caseId)
140
+ if not current.get("ok"):
141
+ return current
142
+ filters = filters or {}
143
+ actions = [copy.deepcopy(item) for item in current["case"].get("actions", []) if isinstance(item, dict)]
144
+ for key in ("status", "owner", "stakeholderId"):
145
+ value = str(filters.get(key, "") or "")
146
+ if value:
147
+ actions = [item for item in actions if str(item.get(key, "") or "") == value]
148
+ actions.sort(key=lambda item: (int(item.get("updatedAt", item.get("createdAt", 0)) or 0), str(item.get("actionId", ""))), reverse=True)
149
+ limit = _limit(filters.get("limit", 100))
150
+ return {"ok": True, "caseId": caseId, "actions": actions[:limit], "count": min(len(actions), limit), "total": len(actions)}
151
+
152
+ def addEvidence(self, caseId: str, payload: dict[str, Any]) -> dict[str, Any]:
153
+ current = self.getCase(caseId)
154
+ if not current.get("ok"):
155
+ return current
156
+ evidence = _evidenceRef(payload)
157
+ if not evidence.get("artifactId"):
158
+ evidence["artifactId"] = _stableId("evidence", {"caseId": caseId, "payload": payload})
159
+ case = current["case"]
160
+ refs = [item for item in case.get("evidenceRefs", []) if isinstance(item, dict) and item.get("artifactId") != evidence["artifactId"]]
161
+ updated = self.updateCase(caseId, {"evidenceRefs": [*refs, evidence]})
162
+ if updated.get("ok"):
163
+ updated["evidence"] = evidence
164
+ self.addEvent(caseId, {"type": "case.evidence.added", "label": f"Evidence added: {evidence.get('label', evidence.get('artifactId', 'evidence'))}", "actor": "picux", "payload": evidence})
165
+ return updated
166
+
167
+ def addNote(self, caseId: str, payload: dict[str, Any]) -> dict[str, Any]:
168
+ current = self.getCase(caseId)
169
+ if not current.get("ok"):
170
+ return current
171
+ note = _caseNote({**payload, "caseId": caseId})
172
+ case = current["case"]
173
+ collaboration = case.get("collaboration", {}) if isinstance(case.get("collaboration"), dict) else {}
174
+ notes = [item for item in collaboration.get("notes", []) if isinstance(item, dict) and item.get("noteId") != note["noteId"]]
175
+ updated = self.updateCase(caseId, {"collaboration": {**collaboration, "notes": [*notes, note]}, "actor": note["author"]})
176
+ if updated.get("ok"):
177
+ updated["note"] = note
178
+ self.addEvent(
179
+ caseId,
180
+ {
181
+ "type": "case.note.added",
182
+ "label": f"Note added: {note.get('text', '')[:80] or note.get('noteId', 'case note')}",
183
+ "actor": note["author"],
184
+ "payload": {"noteId": note["noteId"], "visibility": note["visibility"], "relatedRefs": note["relatedRefs"]},
185
+ },
186
+ )
187
+ return updated
188
+
189
+ def listNotes(self, caseId: str, filters: dict[str, Any] | None = None) -> dict[str, Any]:
190
+ current = self.getCase(caseId)
191
+ if not current.get("ok"):
192
+ return current
193
+ filters = filters or {}
194
+ collaboration = current["case"].get("collaboration", {}) if isinstance(current["case"].get("collaboration"), dict) else {}
195
+ notes = [copy.deepcopy(item) for item in collaboration.get("notes", []) if isinstance(item, dict)]
196
+ visibility = str(filters.get("visibility", "") or "")
197
+ if visibility:
198
+ notes = [item for item in notes if str(item.get("visibility", "") or "") == visibility]
199
+ author = str(filters.get("author", filters.get("authorId", "")) or "")
200
+ if author:
201
+ notes = [item for item in notes if str(item.get("author", "") or "") == author or str(item.get("authorId", "") or "") == author]
202
+ notes.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("noteId", ""))))
203
+ limit = _limit(filters.get("limit", 100))
204
+ return {"ok": True, "caseId": caseId, "notes": notes[-limit:], "count": min(len(notes), limit), "total": len(notes)}
205
+
206
+ def addEvent(self, caseId: str, payload: dict[str, Any]) -> dict[str, Any]:
207
+ current = self.getCase(caseId)
208
+ if not current.get("ok"):
209
+ return current
210
+ event = _caseEvent({**payload, "caseId": caseId})
211
+ self._saveEvent(event)
212
+ case = current["case"]
213
+ timeline = [item for item in case.get("timeline", []) if isinstance(item, dict) and item.get("eventId") != event["eventId"]]
214
+ record = {**case, "timeline": [*timeline, event], "updatedAt": int(time.time())}
215
+ self._saveCase(record)
216
+ return {"ok": True, "event": copy.deepcopy(event), "case": copy.deepcopy(record)}
217
+
218
+ def timeline(self, caseId: str, filters: dict[str, Any] | None = None) -> dict[str, Any]:
219
+ current = self.getCase(caseId)
220
+ if not current.get("ok"):
221
+ return current
222
+ self._loadEvents(caseId)
223
+ events = [event for event in self._events.values() if event.get("caseId") == caseId]
224
+ events.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("eventId", ""))))
225
+ limit = _limit((filters or {}).get("limit", 250))
226
+ return {"ok": True, "caseId": caseId, "timeline": [copy.deepcopy(item) for item in events[-limit:]], "count": min(len(events), limit)}
227
+
228
+ def exportBundle(self, caseId: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
229
+ current = self.getCase(caseId)
230
+ if not current.get("ok"):
231
+ return current
232
+ payload = payload or {}
233
+ case = current["case"]
234
+ timeline = self.timeline(caseId).get("timeline", [])
235
+ evidenceRefs = case.get("evidenceRefs", []) if isinstance(case.get("evidenceRefs"), list) else []
236
+ actions = case.get("actions", []) if isinstance(case.get("actions"), list) else []
237
+ collaboration = case.get("collaboration", {}) if isinstance(case.get("collaboration"), dict) else {}
238
+ collaborationNotes = [item for item in collaboration.get("notes", []) if isinstance(item, dict)]
239
+ bundle = {
240
+ "bundleId": str(payload.get("bundleId", "") or _stableId("bundle", {"caseId": caseId, "updatedAt": case.get("updatedAt", 0)})),
241
+ "caseId": caseId,
242
+ "title": str(case.get("title", "") or ""),
243
+ "status": str(case.get("status", "") or ""),
244
+ "formats": _cleanList(payload.get("formats", ["json", "markdown"])) or ["json"],
245
+ "manifest": {
246
+ "stakeholders": len(case.get("stakeholders", []) if isinstance(case.get("stakeholders"), list) else []),
247
+ "deadlines": len(case.get("deadlines", []) if isinstance(case.get("deadlines"), list) else []),
248
+ "caseActions": len(actions),
249
+ "timeline": len(timeline),
250
+ "evidence": len(evidenceRefs),
251
+ "collaborationNotes": len(collaborationNotes),
252
+ },
253
+ "case": case,
254
+ "timeline": timeline,
255
+ "actions": actions,
256
+ "evidenceRefs": evidenceRefs,
257
+ "collaboration": {"notes": collaborationNotes},
258
+ "redaction": payload.get("redaction", {}) if isinstance(payload.get("redaction"), dict) else {"mode": "sourceBound"},
259
+ "share": {
260
+ "label": "portableCaseFile",
261
+ "evidenceStatus": "sourceBound" if evidenceRefs else "needsEvidence",
262
+ },
263
+ "createdAt": int(time.time()),
264
+ }
265
+ self.addEvent(caseId, {"type": "case.export.created", "label": "Evidence bundle exported", "actor": str(payload.get("actor", "picux") or "picux"), "payload": {"bundleId": bundle["bundleId"], "formats": bundle["formats"]}})
266
+ return {"ok": True, "bundle": bundle}
267
+
268
+ def _saveCase(self, record: dict[str, Any]) -> None:
269
+ caseId = str(record.get("caseId", "") or "")
270
+ if not caseId:
271
+ return
272
+ self._cases[caseId] = copy.deepcopy(record)
273
+ if self._backingEnabled() and hasattr(self.backing, "upsertCaseWorkspace"):
274
+ self.backing.upsertCaseWorkspace(record)
275
+
276
+ def _saveEvent(self, record: dict[str, Any]) -> None:
277
+ eventId = str(record.get("eventId", "") or "")
278
+ if not eventId:
279
+ return
280
+ self._events[eventId] = copy.deepcopy(record)
281
+ if self._backingEnabled() and hasattr(self.backing, "upsertCaseEvent"):
282
+ self.backing.upsertCaseEvent(record)
283
+
284
+ def _loadCases(self) -> None:
285
+ if self._backingEnabled() and hasattr(self.backing, "listCaseWorkspaces"):
286
+ for record in self.backing.listCaseWorkspaces():
287
+ if record.get("caseId"):
288
+ self._cases[str(record["caseId"])] = copy.deepcopy(record)
289
+
290
+ def _loadEvents(self, caseId: str) -> None:
291
+ if self._backingEnabled() and hasattr(self.backing, "listCaseEvents"):
292
+ for record in self.backing.listCaseEvents(caseId=caseId):
293
+ if record.get("eventId"):
294
+ self._events[str(record["eventId"])] = copy.deepcopy(record)
295
+
296
+ def _backingEnabled(self) -> bool:
297
+ return bool(self.backing is not None and getattr(self.backing, "enabled", True))
298
+
299
+
300
+ def _caseFromPayload(payload: dict[str, Any], *, createdAt: int, updatedAt: int) -> dict[str, Any]:
301
+ return {
302
+ "caseId": str(payload.get("caseId", "") or ""),
303
+ "status": _choice(payload.get("status", "open"), CASE_STATUSES, "open"),
304
+ "title": str(payload.get("title", payload.get("subject", "Untitled case")) or "Untitled case"),
305
+ "summary": str(payload.get("summary", payload.get("description", "")) or ""),
306
+ "ownerId": str(payload.get("ownerId", payload.get("userId", "")) or ""),
307
+ "conversationId": str(payload.get("conversationId", "") or ""),
308
+ "domains": _cleanList(payload.get("domains", payload.get("domain", []))),
309
+ "stakeholders": [_stakeholder(item) for item in payload.get("stakeholders", []) if isinstance(item, dict)],
310
+ "deadlines": [_deadline(item) for item in payload.get("deadlines", []) if isinstance(item, dict)],
311
+ "actions": [_caseAction(item) for item in payload.get("actions", []) if isinstance(item, dict)],
312
+ "timeline": [item for item in payload.get("timeline", []) if isinstance(item, dict)],
313
+ "evidenceRefs": [_evidenceRef(item) for item in payload.get("evidenceRefs", payload.get("evidence", [])) if isinstance(item, dict)],
314
+ "collaboration": payload.get("collaboration", {}) if isinstance(payload.get("collaboration"), dict) else {},
315
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
316
+ "createdAt": int(createdAt),
317
+ "updatedAt": int(updatedAt),
318
+ }
319
+
320
+
321
+ def _stakeholder(payload: dict[str, Any]) -> dict[str, Any]:
322
+ return {
323
+ "stakeholderId": str(payload.get("stakeholderId", "") or ""),
324
+ "role": _choice(payload.get("role", "other"), STAKEHOLDER_ROLES, "other"),
325
+ "name": str(payload.get("name", "") or ""),
326
+ "org": str(payload.get("org", payload.get("organization", "")) or ""),
327
+ "contactRef": str(payload.get("contactRef", "") or ""),
328
+ "channelPrefs": _cleanList(payload.get("channelPrefs", payload.get("channels", []))),
329
+ "permissions": _cleanList(payload.get("permissions", [])),
330
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
331
+ }
332
+
333
+
334
+ def _deadline(payload: dict[str, Any]) -> dict[str, Any]:
335
+ return {
336
+ "deadlineId": str(payload.get("deadlineId", "") or ""),
337
+ "label": str(payload.get("label", payload.get("name", "deadline")) or "deadline"),
338
+ "dueAt": str(payload.get("dueAt", payload.get("date", "")) or ""),
339
+ "status": _choice(payload.get("status", "open"), DEADLINE_STATUSES, "open"),
340
+ "owner": str(payload.get("owner", payload.get("ownerId", "")) or ""),
341
+ "source": str(payload.get("source", "") or ""),
342
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
343
+ }
344
+
345
+
346
+ def _caseAction(payload: dict[str, Any]) -> dict[str, Any]:
347
+ now = int(time.time())
348
+ createdAt = int(payload.get("createdAt", now) or now)
349
+ return {
350
+ "actionId": str(payload.get("actionId", "") or ""),
351
+ "caseId": str(payload.get("caseId", "") or ""),
352
+ "title": str(payload.get("title", payload.get("label", "case action")) or "case action"),
353
+ "status": _choice(payload.get("status", "open"), ACTION_STATUSES, "open"),
354
+ "owner": str(payload.get("owner", payload.get("ownerId", "")) or ""),
355
+ "stakeholderId": str(payload.get("stakeholderId", "") or ""),
356
+ "dueAt": str(payload.get("dueAt", payload.get("date", "")) or ""),
357
+ "refs": _cleanList(payload.get("refs", payload.get("relatedRefs", []))),
358
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
359
+ "createdAt": createdAt,
360
+ "updatedAt": int(payload.get("updatedAt", createdAt) or createdAt),
361
+ }
362
+
363
+
364
+ def _caseNote(payload: dict[str, Any]) -> dict[str, Any]:
365
+ now = int(payload.get("createdAt", int(time.time())) or int(time.time()))
366
+ text = str(payload.get("text", payload.get("body", payload.get("note", ""))) or "")
367
+ author = str(payload.get("author", payload.get("authorId", payload.get("actor", "picux"))) or "picux")
368
+ noteId = str(payload.get("noteId", "") or _stableId("caseNote", {"caseId": payload.get("caseId", ""), "text": text, "author": author, "createdAt": now}))
369
+ return {
370
+ "noteId": noteId,
371
+ "caseId": str(payload.get("caseId", "") or ""),
372
+ "text": text,
373
+ "author": author,
374
+ "authorId": str(payload.get("authorId", "") or ""),
375
+ "visibility": _choice(payload.get("visibility", "internal"), NOTE_VISIBILITIES, "internal"),
376
+ "relatedRefs": _cleanList(payload.get("relatedRefs", payload.get("refs", []))),
377
+ "tags": _cleanList(payload.get("tags", [])),
378
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
379
+ "createdAt": now,
380
+ }
381
+
382
+
383
+ def _caseEvent(payload: dict[str, Any]) -> dict[str, Any]:
384
+ now = int(time.time())
385
+ return {
386
+ "eventId": str(payload.get("eventId", "") or _stableId("caseEvent", {"caseId": payload.get("caseId", ""), "type": payload.get("type", ""), "createdAt": now, "label": payload.get("label", "")})),
387
+ "caseId": str(payload.get("caseId", "") or ""),
388
+ "type": str(payload.get("type", "case.note") or "case.note"),
389
+ "label": str(payload.get("label", payload.get("summary", "")) or ""),
390
+ "actor": str(payload.get("actor", "picux") or "picux"),
391
+ "status": str(payload.get("status", "") or ""),
392
+ "payload": payload.get("payload", {}) if isinstance(payload.get("payload"), dict) else {},
393
+ "createdAt": int(payload.get("createdAt", now) or now),
394
+ }
395
+
396
+
397
+ def _evidenceRef(payload: dict[str, Any]) -> dict[str, Any]:
398
+ return {
399
+ "artifactId": str(payload.get("artifactId", payload.get("id", "")) or ""),
400
+ "kind": str(payload.get("kind", "") or ""),
401
+ "label": str(payload.get("label", payload.get("name", "")) or ""),
402
+ "source": str(payload.get("source", "") or ""),
403
+ "hash": str(payload.get("hash", payload.get("sha256", "")) or ""),
404
+ "status": str(payload.get("status", "sourceBound") or "sourceBound"),
405
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
406
+ }
407
+
408
+
409
+ def _caseIdentity(payload: dict[str, Any]) -> dict[str, Any]:
410
+ return {
411
+ "title": str(payload.get("title", payload.get("subject", "")) or ""),
412
+ "ownerId": str(payload.get("ownerId", payload.get("userId", "")) or ""),
413
+ "conversationId": str(payload.get("conversationId", "") or ""),
414
+ "extRef": str(payload.get("extRef", payload.get("requestId", "")) or ""),
415
+ }
416
+
417
+
418
+ def _choice(value: Any, allowed: set[str], default: str) -> str:
419
+ clean = str(value or default)
420
+ return clean if clean in allowed else default
421
+
422
+
423
+ def _cleanList(value: Any) -> list[Any]:
424
+ if isinstance(value, list):
425
+ return [item for item in value if item not in {"", None}]
426
+ if value in {"", None}:
427
+ return []
428
+ return [value]
429
+
430
+
431
+ def _limit(value: Any) -> int:
432
+ try:
433
+ return max(1, min(int(value or 100), 500))
434
+ except Exception:
435
+ return 100
436
+
437
+
438
+ def _stableId(prefix: str, payload: dict[str, Any]) -> str:
439
+ digest = hashlib.sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True).encode("utf-8")).hexdigest()
440
+ return f"{prefix}_{digest[:24]}"