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,76 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from picux import __version__
7
+ from picux.protocols.mcp.contract import PicuxMCPContract
8
+
9
+
10
+ class PicuxMCPServer:
11
+ """JSON-RPC MCP endpoint backed by the Picux route registry."""
12
+
13
+ def __init__(self, *, service: Any | None = None) -> None:
14
+ self.contract = PicuxMCPContract(service=service)
15
+
16
+ def handle(self, request: dict[str, Any]) -> dict[str, Any]:
17
+ requestId = request.get("id")
18
+ method = str(request.get("method", "") or "")
19
+ params = request.get("params", {}) if isinstance(request.get("params"), dict) else {}
20
+ if request.get("jsonrpc") != "2.0":
21
+ return self._error(requestId, -32600, "invalidRequest", {"reason": "jsonrpc"})
22
+ try:
23
+ result = self._dispatch(method, params)
24
+ except MCPDispatchError as exc:
25
+ return self._error(requestId, exc.code, exc.message, exc.data)
26
+ return {"jsonrpc": "2.0", "id": requestId, "result": result}
27
+
28
+ def _dispatch(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
29
+ if method == "initialize":
30
+ return {
31
+ "protocolVersion": "2024-11-05",
32
+ "capabilities": {"tools": {}},
33
+ "serverInfo": {"name": "picux", "version": __version__},
34
+ }
35
+ if method == "ping":
36
+ return {"ok": True}
37
+ if method == "notifications/initialized":
38
+ return {"ok": True}
39
+ if method == "tools/list":
40
+ return self.contract.listTools()
41
+ if method == "tools/call":
42
+ name = str(params.get("name", params.get("tool", "")) or "")
43
+ args = params.get("arguments", params.get("args", {}))
44
+ if not isinstance(args, dict):
45
+ args = {}
46
+ if not name:
47
+ raise MCPDispatchError(-32602, "invalidParams", {"missing": "name"})
48
+ return self._toolResult(self.contract.callTool(name, args))
49
+ if method.startswith("picux."):
50
+ return self.contract.callTool(method, params)
51
+ raise MCPDispatchError(-32601, "methodNotFound", {"method": method})
52
+
53
+ @staticmethod
54
+ def _toolResult(result: dict[str, Any]) -> dict[str, Any]:
55
+ isError = not bool(result.get("ok"))
56
+ return {
57
+ **result,
58
+ "isError": isError,
59
+ "structuredContent": result.get("payload", result),
60
+ "content": [{"type": "text", "text": json.dumps(result, ensure_ascii=True, sort_keys=True)}],
61
+ }
62
+
63
+ @staticmethod
64
+ def _error(requestId: Any, code: int, message: str, data: dict[str, Any] | None = None) -> dict[str, Any]:
65
+ error = {"code": code, "message": message}
66
+ if data:
67
+ error["data"] = data
68
+ return {"jsonrpc": "2.0", "id": requestId, "error": error}
69
+
70
+
71
+ class MCPDispatchError(Exception):
72
+ def __init__(self, code: int, message: str, data: dict[str, Any] | None = None) -> None:
73
+ super().__init__(message)
74
+ self.code = code
75
+ self.message = message
76
+ self.data = data or {}
@@ -0,0 +1,6 @@
1
+ """Picux developer sandbox runtimes."""
2
+
3
+ from .midnight_arbitrage import MidnightArbitrageSandbox
4
+ from .models import SandboxResult, TraceEvent, VendorQuote
5
+
6
+ __all__ = ["MidnightArbitrageSandbox", "SandboxResult", "TraceEvent", "VendorQuote"]
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+
7
+ from picux.api import PicuxApiService
8
+ from picux.domains.pay import PayDomain
9
+ from picux.domains.pay.models import Money, ProofOfValue, SettlementRequest
10
+ from picux.protocols.mcp import PicuxMCPContract
11
+ from picux.sandbox.models import SandboxResult, TraceEvent, VendorQuote
12
+
13
+
14
+ CHAOS_MODES = {"", "latency", "modelError", "unauthorizedSpend", "freeze"}
15
+
16
+
17
+ class MidnightArbitrageSandbox:
18
+ """Deterministic local sandbox for the HUNT -> RESOLVE -> PAY loop."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ clock: Callable[[], datetime] | None = None,
24
+ mcp: PicuxMCPContract | None = None,
25
+ pay: PayDomain | None = None,
26
+ ) -> None:
27
+ self.clock = clock or (lambda: datetime(2026, 5, 9, 12, 0, tzinfo=timezone.utc))
28
+ self.mcp = mcp or PicuxMCPContract(service=PicuxApiService())
29
+ self.pay = pay or PayDomain(verifier=self._verifyMandate, clock=self.clock)
30
+
31
+ def run(self, *, chaos: str = "") -> dict[str, Any]:
32
+ if chaos not in CHAOS_MODES:
33
+ raise ValueError(f"unknownChaos:{chaos}")
34
+
35
+ trace: list[TraceEvent] = []
36
+ task = self._createTask(trace)
37
+ vendors = self._hunt(trace, chaos)
38
+ decisionTree = self._resolve(trace, vendors, chaos)
39
+ mandate = self.mandate()
40
+ self._event(trace, "pay", "mandateView", "ready", "Intent Mandate loaded", {"mandateId": mandate["mandateId"]})
41
+
42
+ if chaos == "modelError":
43
+ freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="modelErrorRollback")
44
+ self._event(trace, "resolve", "rollback", "failedClosed", "Model error detected; settlement blocked", {"receiptId": freeze["receiptId"]})
45
+ return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, freezeReceipt=freeze)
46
+
47
+ if chaos == "freeze":
48
+ freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="protocolFreeze")
49
+ self._event(trace, "pay", "killSwitch", "failedClosed", "Protocol freeze stopped settlement", {"receiptId": freeze["receiptId"]})
50
+ return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, freezeReceipt=freeze)
51
+
52
+ selected = decisionTree.get("selected", "")
53
+ if not selected:
54
+ freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="noSafeVendor")
55
+ self._event(trace, "pay", "freeze", "failedClosed", "No safe vendor remained after pruning", {"receiptId": freeze["receiptId"]})
56
+ return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, freezeReceipt=freeze)
57
+
58
+ quote = next(vendor for vendor in vendors if vendor.vendorId == selected)
59
+ amount = 120.0 if chaos == "unauthorizedSpend" else quote.netUsd
60
+ request = SettlementRequest(
61
+ taskId=task["taskId"],
62
+ vendorId=quote.vendorId,
63
+ amount=Money(amount, "USD"),
64
+ pov=ProofOfValue(
65
+ povId=f"pov_{quote.vendorId}",
66
+ rules=("inventoryVerified", "priceArbitrage20", "regionMatched", "hiddenFeeClear"),
67
+ meta={"discountPct": quote.discountPct, "netUsd": quote.netUsd},
68
+ ),
69
+ )
70
+ settled = self.pay.settle(mandate, request)
71
+ receipt = settled["receipt"]
72
+ if settled["ok"]:
73
+ self._event(trace, "pay", "microEscrow", "settled", "Proof-of-value verified; escrow settled", {"receiptId": receipt["receiptId"]})
74
+ self._event(trace, "pay", "receiptView", "ready", "Settlement receipt available", {"receipt": receipt})
75
+ return self._result(True, "settled", chaos, task, trace, vendors, decisionTree, mandate, receipt=receipt)
76
+
77
+ freeze = self.pay.freeze(mandateId=mandate["mandateId"], taskId=task["taskId"], reason="unauthorizedSpend")
78
+ self._event(
79
+ trace,
80
+ "pay",
81
+ "unauthorizedSpend",
82
+ "failedClosed",
83
+ "PAY rejected unauthorized spend and froze the rail",
84
+ {"errors": settled["decision"]["errors"], "receiptId": freeze["receiptId"]},
85
+ )
86
+ return self._result(False, "failedClosed", chaos, task, trace, vendors, decisionTree, mandate, receipt=receipt, freezeReceipt=freeze)
87
+
88
+ def mandate(self) -> dict[str, Any]:
89
+ return {
90
+ "mandateId": "mandate_midnight_arbitrage",
91
+ "issuer": {"entityId": "tenant_sandbox", "publicKey": "sandboxPub"},
92
+ "validUntil": "2099-01-01T00:00:00Z",
93
+ "constraints": {
94
+ "maxSpend": {"amount": 100.0, "currency": "USD"},
95
+ "allowedVendors": ["vendorA"],
96
+ "resolveRules": ["inventoryVerified", "priceArbitrage20", "regionMatched", "hiddenFeeClear"],
97
+ },
98
+ "signature": "sandboxSig",
99
+ }
100
+
101
+ def _createTask(self, trace: list[TraceEvent]) -> dict[str, Any]:
102
+ tools = self.mcp.listTools()["tools"]
103
+ self._event(trace, "mcp", "toolDiscovery", "ready", "Picux MCP tools discovered", {"toolCount": len(tools)})
104
+ result = self.mcp.callTool(
105
+ "picux.createTask",
106
+ {
107
+ "userId": "sandbox",
108
+ "channel": "sandbox",
109
+ "domain": "hunt",
110
+ "goal": "Midnight Arbitrage: capture Resource-X discount before reset",
111
+ },
112
+ )
113
+ task = result["payload"]["task"]
114
+ self._event(trace, "hunt", "marketAnomaly", "started", "Market anomaly triggered", {"taskId": task["taskId"]})
115
+ return task
116
+
117
+ def _hunt(self, trace: list[TraceEvent], chaos: str) -> tuple[VendorQuote, ...]:
118
+ vendors = (
119
+ VendorQuote("vendorA", 100.0, 80.0, 0.0, 12, "SE", 42),
120
+ VendorQuote("vendorB", 100.0, 75.0, 30.0, 30, "SE", 51),
121
+ VendorQuote("vendorC", 100.0, 70.0, 0.0, 0, "NO", 38),
122
+ )
123
+ if chaos == "latency":
124
+ vendors = tuple(VendorQuote(v.vendorId, v.listUsd, v.offerUsd, v.hiddenFeeUsd, v.stock, v.region, v.latencyMs + (250 if v.vendorId == "vendorA" else 0)) for v in vendors)
125
+ self._event(trace, "bridge", "latencyInject", "rerouted", "Latency injected; protocol route stayed live", {"vendorId": "vendorA", "addedMs": 250})
126
+
127
+ for vendor in vendors:
128
+ self._event(
129
+ trace,
130
+ "hunt",
131
+ "telemetryTick",
132
+ "observed",
133
+ f"{vendor.vendorId} quote observed",
134
+ {"vendor": vendor.toMap()},
135
+ )
136
+ self._event(trace, "hunt", "dealFound", "ready", "20% Resource-X discount candidate found", {"vendorId": "vendorA"})
137
+ return vendors
138
+
139
+ def _resolve(self, trace: list[TraceEvent], vendors: tuple[VendorQuote, ...], chaos: str) -> dict[str, Any]:
140
+ paths: list[dict[str, Any]] = []
141
+ selected = ""
142
+ for vendor in vendors:
143
+ reasons: list[str] = []
144
+ if vendor.discountPct < 20:
145
+ reasons.append("discountBelowThreshold")
146
+ if vendor.hiddenFeeUsd > 0:
147
+ reasons.append("hiddenFee")
148
+ if vendor.stock <= 0:
149
+ reasons.append("stockLimit")
150
+ if vendor.region != "SE":
151
+ reasons.append("regionMismatch")
152
+ status = "pruned" if reasons else "selected"
153
+ if status == "selected":
154
+ selected = vendor.vendorId
155
+ paths.append({"vendorId": vendor.vendorId, "status": status, "reasons": reasons, "netUsd": vendor.netUsd})
156
+ self._event(
157
+ trace,
158
+ "resolve",
159
+ "pathPrune" if reasons else "pathSelect",
160
+ status,
161
+ f"{vendor.vendorId} {status}",
162
+ {"vendorId": vendor.vendorId, "reasons": reasons},
163
+ )
164
+
165
+ if chaos == "modelError":
166
+ paths.append({"vendorId": "vendorZ", "status": "pruned", "reasons": ["modelHallucination"], "netUsd": 0.0})
167
+ self._event(trace, "resolve", "modelGuard", "failedClosed", "Hallucinated vendor pruned", {"vendorId": "vendorZ"})
168
+ selected = ""
169
+
170
+ tree = {"root": "marketAnomaly", "selected": selected, "paths": paths}
171
+ self._event(trace, "resolve", "decisionTree", "ready", "Decision tree ready", tree)
172
+ return tree
173
+
174
+ def _result(
175
+ self,
176
+ ok: bool,
177
+ status: str,
178
+ chaos: str,
179
+ task: dict[str, Any],
180
+ trace: list[TraceEvent],
181
+ vendors: tuple[VendorQuote, ...],
182
+ decisionTree: dict[str, Any],
183
+ mandate: dict[str, Any],
184
+ *,
185
+ receipt: dict[str, Any] | None = None,
186
+ freezeReceipt: dict[str, Any] | None = None,
187
+ ) -> dict[str, Any]:
188
+ return SandboxResult(
189
+ ok=ok,
190
+ scenario="midnightArbitrage",
191
+ status=status,
192
+ chaos=chaos,
193
+ task=task,
194
+ trace=tuple(trace),
195
+ vendors=vendors,
196
+ decisionTree=decisionTree,
197
+ mandate=mandate,
198
+ receipt=receipt,
199
+ freezeReceipt=freezeReceipt,
200
+ ).toMap()
201
+
202
+ def _event(
203
+ self,
204
+ trace: list[TraceEvent],
205
+ phase: str,
206
+ name: str,
207
+ status: str,
208
+ msg: str,
209
+ data: dict[str, Any] | None = None,
210
+ ) -> None:
211
+ trace.append(TraceEvent(len(trace) + 1, phase, name, status, msg, len(trace) * 37, data or {}))
212
+
213
+ @staticmethod
214
+ def _verifyMandate(payload: dict[str, Any], signature: str, publicKey: str) -> bool:
215
+ return payload.get("mandateId") == "mandate_midnight_arbitrage" and signature == "sandboxSig" and publicKey == "sandboxPub"
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class VendorQuote:
9
+ vendorId: str
10
+ listUsd: float
11
+ offerUsd: float
12
+ hiddenFeeUsd: float
13
+ stock: int
14
+ region: str
15
+ latencyMs: int = 0
16
+
17
+ @property
18
+ def netUsd(self) -> float:
19
+ return round(max(0.0, float(self.offerUsd) + float(self.hiddenFeeUsd)), 2)
20
+
21
+ @property
22
+ def discountPct(self) -> float:
23
+ if self.listUsd <= 0:
24
+ return 0.0
25
+ return round(((self.listUsd - self.offerUsd) / self.listUsd) * 100, 2)
26
+
27
+ def toMap(self) -> dict[str, Any]:
28
+ return {
29
+ "vendorId": self.vendorId,
30
+ "listUsd": round(self.listUsd, 2),
31
+ "offerUsd": round(self.offerUsd, 2),
32
+ "hiddenFeeUsd": round(self.hiddenFeeUsd, 2),
33
+ "netUsd": self.netUsd,
34
+ "discountPct": self.discountPct,
35
+ "stock": self.stock,
36
+ "region": self.region,
37
+ "latencyMs": self.latencyMs,
38
+ }
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class TraceEvent:
43
+ step: int
44
+ phase: str
45
+ name: str
46
+ status: str
47
+ msg: str
48
+ atMs: int
49
+ data: dict[str, Any] = field(default_factory=dict)
50
+
51
+ def toMap(self) -> dict[str, Any]:
52
+ return {
53
+ "step": self.step,
54
+ "phase": self.phase,
55
+ "name": self.name,
56
+ "status": self.status,
57
+ "msg": self.msg,
58
+ "atMs": self.atMs,
59
+ "data": self.data,
60
+ }
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class SandboxResult:
65
+ ok: bool
66
+ scenario: str
67
+ status: str
68
+ chaos: str
69
+ task: dict[str, Any]
70
+ trace: tuple[TraceEvent, ...]
71
+ vendors: tuple[VendorQuote, ...]
72
+ decisionTree: dict[str, Any]
73
+ mandate: dict[str, Any]
74
+ receipt: dict[str, Any] | None = None
75
+ freezeReceipt: dict[str, Any] | None = None
76
+
77
+ def toMap(self) -> dict[str, Any]:
78
+ return {
79
+ "ok": self.ok,
80
+ "scenario": self.scenario,
81
+ "status": self.status,
82
+ "chaos": self.chaos,
83
+ "task": self.task,
84
+ "trace": [event.toMap() for event in self.trace],
85
+ "vendors": [vendor.toMap() for vendor in self.vendors],
86
+ "decisionTree": self.decisionTree,
87
+ "mandate": self.mandate,
88
+ "receipt": self.receipt,
89
+ "freezeReceipt": self.freezeReceipt,
90
+ }
picux/sdk/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """Picux SDK."""
2
+
3
+ from .client import HttpTransport, InProcessTransport, PicuxApiError, PicuxClient
4
+ from .external import ExternalAppClient, ExternalPricing
5
+
6
+ __all__ = [
7
+ "ExternalAppClient",
8
+ "ExternalPricing",
9
+ "HttpTransport",
10
+ "InProcessTransport",
11
+ "PicuxApiError",
12
+ "PicuxClient",
13
+ ]