@senpi/runtime 3.0.187 → 3.0.191
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.
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +6 -0
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +4 -2
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/deploy/box-cap.d.ts +7 -0
- package/dist/deploy/box-cap.d.ts.map +1 -0
- package/dist/deploy/box-cap.js +13 -0
- package/dist/deploy/box-cap.js.map +1 -0
- package/dist/deploy/funding.d.ts +13 -1
- package/dist/deploy/funding.d.ts.map +1 -1
- package/dist/deploy/funding.js +13 -4
- package/dist/deploy/funding.js.map +1 -1
- package/dist/deploy/job.d.ts +2 -0
- package/dist/deploy/job.d.ts.map +1 -1
- package/dist/deploy/job.js +2 -2
- package/dist/deploy/job.js.map +1 -1
- package/dist/deploy/messages.d.ts +14 -0
- package/dist/deploy/messages.d.ts.map +1 -1
- package/dist/deploy/messages.js +59 -25
- package/dist/deploy/messages.js.map +1 -1
- package/dist/deploy/orchestrator.d.ts +23 -1
- package/dist/deploy/orchestrator.d.ts.map +1 -1
- package/dist/deploy/orchestrator.js +87 -30
- package/dist/deploy/orchestrator.js.map +1 -1
- package/dist/deploy/package.d.ts +14 -1
- package/dist/deploy/package.d.ts.map +1 -1
- package/dist/deploy/package.js +37 -2
- package/dist/deploy/package.js.map +1 -1
- package/dist/deploy/register.d.ts +2 -0
- package/dist/deploy/register.d.ts.map +1 -1
- package/dist/deploy/register.js +48 -32
- package/dist/deploy/register.js.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -12
- package/dist/index.js.map +1 -1
- package/dist/runtime/boot-skip.d.ts +18 -0
- package/dist/runtime/boot-skip.d.ts.map +1 -1
- package/dist/runtime/boot-skip.js +25 -0
- package/dist/runtime/boot-skip.js.map +1 -1
- package/dist/runtime/lifecycle-events.d.ts +2 -2
- package/dist/runtime/lifecycle-events.d.ts.map +1 -1
- package/dist/runtime/lifecycle-events.js.map +1 -1
- package/dist/runtime/lifecycle-reaper.d.ts +11 -6
- package/dist/runtime/lifecycle-reaper.d.ts.map +1 -1
- package/dist/runtime/lifecycle-reaper.js +25 -15
- package/dist/runtime/lifecycle-reaper.js.map +1 -1
- package/dist/runtime/notifications.d.ts +10 -0
- package/dist/runtime/notifications.d.ts.map +1 -1
- package/dist/runtime/notifications.js +17 -6
- package/dist/runtime/notifications.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +4 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/scanners/external/scaffold/preflight.py +492 -12
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.d.ts +44 -0
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.d.ts.map +1 -0
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.js +129 -0
- package/dist/senpi/__tests__/helpers/fake-paper-mcp.js.map +1 -0
- package/dist/senpi/client.d.ts +35 -1
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +62 -8
- package/dist/senpi/client.js.map +1 -1
- package/dist/senpi/paper.d.ts +29 -0
- package/dist/senpi/paper.d.ts.map +1 -0
- package/dist/senpi/paper.js +44 -0
- package/dist/senpi/paper.js.map +1 -0
- package/dist/senpi/strategy-status.d.ts.map +1 -1
- package/dist/senpi/strategy-status.js +2 -0
- package/dist/senpi/strategy-status.js.map +1 -1
- package/dist/senpi/types.d.ts +8 -1
- package/dist/senpi/types.d.ts.map +1 -1
- package/dist/telemetry/box-memory.d.ts +29 -0
- package/dist/telemetry/box-memory.d.ts.map +1 -0
- package/dist/telemetry/box-memory.js +113 -0
- package/dist/telemetry/box-memory.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +21 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +82 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/telemetry/spans/span-hooks.d.ts.map +1 -1
- package/dist/telemetry/spans/span-hooks.js +4 -1
- package/dist/telemetry/spans/span-hooks.js.map +1 -1
- package/dist/telemetry/trace-identity.d.ts.map +1 -1
- package/dist/telemetry/trace-identity.js +2 -0
- package/dist/telemetry/trace-identity.js.map +1 -1
- package/dist/universe/package-universe.js +1 -1
- package/dist/universe/package-universe.js.map +1 -1
- package/dist/utils/event-catalog.d.ts +7 -3
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/validate/codes.d.ts +2 -0
- package/dist/validate/codes.d.ts.map +1 -1
- package/dist/validate/codes.js +6 -0
- package/dist/validate/codes.js.map +1 -1
- package/dist/validate/live-stage.d.ts +12 -0
- package/dist/validate/live-stage.d.ts.map +1 -1
- package/dist/validate/live-stage.js +16 -0
- package/dist/validate/live-stage.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -31,14 +31,18 @@ Author stdout is captured as evidence and never parsed. Redirecting `sys.stdout`
|
|
|
31
31
|
cover `print()` — `os.write(1, ...)`, a subprocess, or a C extension write to the same descriptor.
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
+
import ast
|
|
35
|
+
import dis
|
|
34
36
|
import io
|
|
37
|
+
import itertools
|
|
35
38
|
import json
|
|
36
39
|
import math
|
|
37
40
|
import os
|
|
38
41
|
import sys
|
|
42
|
+
import threading
|
|
39
43
|
import time
|
|
40
44
|
import traceback
|
|
41
|
-
from typing import Any, Dict, List, Optional
|
|
45
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
42
46
|
|
|
43
47
|
from . import http_post
|
|
44
48
|
from .signal_envelope import candidate_to_signal
|
|
@@ -57,6 +61,390 @@ def _bounded(text: str, limit: int = _STREAM_LIMIT) -> str:
|
|
|
57
61
|
return "{}\n… truncated {} characters …\n{}".format(text[:half], dropped, text[-half:])
|
|
58
62
|
|
|
59
63
|
|
|
64
|
+
#: Longest key name echoed into a record. Key names come from the response.
|
|
65
|
+
_KEY_LIMIT = 64
|
|
66
|
+
|
|
67
|
+
#: Where an expression ends, (end_lineno, end_col_offset). The compiler can move a wrapped method
|
|
68
|
+
#: call's start onto the `.get` line, but never its end, so the AST and `co_positions()` agree on it,
|
|
69
|
+
#: and it is distinct for every call and subscript.
|
|
70
|
+
Key = Tuple[int, int]
|
|
71
|
+
|
|
72
|
+
_FILES: Dict[str, "_FileReads"] = {}
|
|
73
|
+
_POSITIONS: Dict[Any, List[Any]] = {}
|
|
74
|
+
_RESUMES: Dict[Any, int] = {}
|
|
75
|
+
_SCOPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
76
|
+
_GENERATOR = 0x20 | 0x80 | 0x200 # CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR
|
|
77
|
+
#: Literal defaults that stand in for a value rather than being one.
|
|
78
|
+
_STAND_INS = (None, 0, 0.0, "", "0", "0.0")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _key(node: ast.AST) -> Key:
|
|
82
|
+
return (node.end_lineno, node.end_col_offset) # type: ignore[attr-defined]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _keyed(node: ast.AST) -> bool:
|
|
86
|
+
return getattr(node, "end_col_offset", None) is not None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_get(node: ast.AST) -> bool:
|
|
90
|
+
return isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "get"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_or(node: ast.AST) -> bool:
|
|
94
|
+
return isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _assigned(node: ast.AST) -> Optional[Tuple[str, ast.AST]]:
|
|
98
|
+
"""(name, value) for `x = ...`, `x: T = ...` and `(x := ...)`."""
|
|
99
|
+
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
100
|
+
return node.targets[0].id, node.value
|
|
101
|
+
if isinstance(node, (ast.AnnAssign, ast.NamedExpr)) and isinstance(node.target, ast.Name) and node.value:
|
|
102
|
+
return node.target.id, node.value
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _own_nodes(scope: ast.AST) -> Any:
|
|
107
|
+
"""The nodes under `scope`, not those of the functions and classes inside it."""
|
|
108
|
+
stack = list(ast.iter_child_nodes(scope))
|
|
109
|
+
while stack:
|
|
110
|
+
node = stack.pop()
|
|
111
|
+
yield node
|
|
112
|
+
if not isinstance(node, _SCOPES):
|
|
113
|
+
stack.extend(ast.iter_child_nodes(node))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _within(stmts: List[ast.stmt]) -> Any:
|
|
117
|
+
for stmt in stmts:
|
|
118
|
+
yield stmt
|
|
119
|
+
yield from _own_nodes(stmt)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _reads_name(node: ast.AST, name: str) -> bool:
|
|
123
|
+
return any(isinstance(n, ast.Name) and n.id == name for n in ast.walk(node))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _truth_test(node: ast.AST, parents: Dict[Any, Any]) -> bool:
|
|
127
|
+
"""Whether a read's result is only tested: `if row.get(k):`, `not row.get(k)`,
|
|
128
|
+
`row.get(k) is None`. Such a read checks an optional key; it does not take a value."""
|
|
129
|
+
parent = parents.get(node)
|
|
130
|
+
while _is_or(parent) or (isinstance(parent, ast.BoolOp) and isinstance(parent.op, ast.And)):
|
|
131
|
+
node, parent = parent, parents.get(parent)
|
|
132
|
+
if isinstance(parent, (ast.If, ast.While, ast.IfExp, ast.Assert)) and parent.test is node:
|
|
133
|
+
return True
|
|
134
|
+
if isinstance(parent, ast.UnaryOp) and isinstance(parent.op, ast.Not):
|
|
135
|
+
return True
|
|
136
|
+
return (isinstance(parent, ast.Compare) and len(parent.ops) == 1
|
|
137
|
+
and isinstance(parent.ops[0], (ast.Is, ast.IsNot))
|
|
138
|
+
and any(isinstance(c, ast.Constant) and c.value is None for c in [parent.left, *parent.comparators]))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _FileReads:
|
|
142
|
+
"""How each `.get(...)` and subscript in one scanner file takes part in fallback chains.
|
|
143
|
+
|
|
144
|
+
`roots` maps each read to its chain. One chain: `a.get(k, b.get(k2, 0))`, `a.get(k) or b[k2]`,
|
|
145
|
+
`a.get(k) if ... else b.get(k2)`, helper calls as alternatives (`_f(a, k) or _f(b, k)`), and an
|
|
146
|
+
assignment with its fallback: `x = c.get(k)` then `if x is None: x = ...`, `x = x or ...`, the
|
|
147
|
+
same name in an `except`, or `x or ...` later. A miss in a chain is answered by a hit anywhere in it.
|
|
148
|
+
|
|
149
|
+
`receivers` are reads on the way to an alternative (`i["context"]` in `i["context"]["volume"]`,
|
|
150
|
+
a ternary's test). They join the chain; finding them does not answer it.
|
|
151
|
+
|
|
152
|
+
`solo` are `.get`s alone in their chain. Their caller's expression may be the chain.
|
|
153
|
+
|
|
154
|
+
`defaults` says, per `.get`, how a miss is judged from its default as written: "keep" (absent, or
|
|
155
|
+
a literal stand-in such as 0, None, "" or "0"), "drop" (a real value, a computed fallback such as
|
|
156
|
+
`a + b`, a bool, or a read only tested), or "value" (a name or another read: judged by its value).
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, filename: str) -> None:
|
|
160
|
+
self.roots: Dict[Key, Any] = {}
|
|
161
|
+
self.receivers: set = set()
|
|
162
|
+
self.solo: set = set()
|
|
163
|
+
self.defaults: Dict[Key, str] = {}
|
|
164
|
+
self._labels = itertools.count()
|
|
165
|
+
try:
|
|
166
|
+
with open(filename, encoding="utf-8") as fh:
|
|
167
|
+
tree = ast.parse(fh.read(), filename)
|
|
168
|
+
except (OSError, SyntaxError, ValueError):
|
|
169
|
+
return
|
|
170
|
+
self._chains(tree)
|
|
171
|
+
for scope in [tree] + [n for n in ast.walk(tree) if isinstance(n, _SCOPES[:2])]:
|
|
172
|
+
self._join_assignments(scope)
|
|
173
|
+
counts: Dict[Any, int] = {}
|
|
174
|
+
for key, root in self.roots.items():
|
|
175
|
+
if key not in self.receivers:
|
|
176
|
+
counts[root] = counts.get(root, 0) + 1
|
|
177
|
+
self.solo = {k for k, r in self.roots.items() if k not in self.receivers and counts[r] == 1}
|
|
178
|
+
parents = {c: p for p in ast.walk(tree) for c in ast.iter_child_nodes(p)}
|
|
179
|
+
for node in ast.walk(tree):
|
|
180
|
+
if _is_get(node):
|
|
181
|
+
self.defaults[_key(node)] = "drop" if _truth_test(node, parents) else self._judge(node)
|
|
182
|
+
|
|
183
|
+
def _chains(self, tree: ast.AST) -> None:
|
|
184
|
+
claimed = set()
|
|
185
|
+
|
|
186
|
+
def receiver(node: ast.AST, root: Any) -> None:
|
|
187
|
+
claimed.add(id(node))
|
|
188
|
+
self.roots[_key(node)] = root
|
|
189
|
+
self.receivers.add(_key(node))
|
|
190
|
+
|
|
191
|
+
def through(node: ast.AST, root: Any) -> None:
|
|
192
|
+
while _is_get(node) or isinstance(node, ast.Subscript):
|
|
193
|
+
receiver(node, root)
|
|
194
|
+
node = node.func.value if _is_get(node) else node.value # type: ignore[attr-defined]
|
|
195
|
+
|
|
196
|
+
def mark(node: ast.AST, root: Any) -> None:
|
|
197
|
+
claimed.add(id(node))
|
|
198
|
+
if _is_get(node):
|
|
199
|
+
self.roots[_key(node)] = root
|
|
200
|
+
through(node.func.value, root) # type: ignore[attr-defined]
|
|
201
|
+
for alt in node.args[1:2]: # type: ignore[attr-defined]
|
|
202
|
+
mark(alt, root)
|
|
203
|
+
elif isinstance(node, ast.Subscript):
|
|
204
|
+
self.roots[_key(node)] = root
|
|
205
|
+
through(node.value, root)
|
|
206
|
+
elif _is_or(node):
|
|
207
|
+
for alt in node.values: # type: ignore[attr-defined]
|
|
208
|
+
mark(alt, root)
|
|
209
|
+
elif isinstance(node, ast.IfExp):
|
|
210
|
+
for read in ast.walk(node.test):
|
|
211
|
+
if _is_get(read) or isinstance(read, ast.Subscript):
|
|
212
|
+
receiver(read, root)
|
|
213
|
+
mark(node.body, root)
|
|
214
|
+
mark(node.orelse, root)
|
|
215
|
+
elif isinstance(node, ast.Call): # `_f(a, k) or _f(b, k)`, `float(a.get(k)) or ...`
|
|
216
|
+
self.roots[_key(node)] = root
|
|
217
|
+
for arg in node.args:
|
|
218
|
+
mark(arg, root)
|
|
219
|
+
|
|
220
|
+
# Breadth-first, so a chain is claimed from its outermost node before its inner ones.
|
|
221
|
+
for node in ast.walk(tree):
|
|
222
|
+
if id(node) not in claimed and (
|
|
223
|
+
_is_get(node) or _is_or(node) or isinstance(node, (ast.IfExp, ast.Subscript))
|
|
224
|
+
):
|
|
225
|
+
mark(node, _key(node))
|
|
226
|
+
|
|
227
|
+
def _join(self, *groups: set) -> None:
|
|
228
|
+
members = set().union(*groups)
|
|
229
|
+
if len(members) > 1:
|
|
230
|
+
label = ("join", next(self._labels))
|
|
231
|
+
for key, root in list(self.roots.items()):
|
|
232
|
+
if root in members:
|
|
233
|
+
self.roots[key] = label
|
|
234
|
+
|
|
235
|
+
def _chains_in(self, node: ast.AST) -> set:
|
|
236
|
+
return {self.roots[_key(n)] for n in ast.walk(node) if _keyed(n) and _key(n) in self.roots}
|
|
237
|
+
|
|
238
|
+
def _join_assignments(self, scope: ast.AST) -> None:
|
|
239
|
+
parents = {c: p for p in ast.walk(scope) for c in ast.iter_child_nodes(p)}
|
|
240
|
+
own = list(_own_nodes(scope))
|
|
241
|
+
assigns = sorted((n for n in own if _assigned(n) and self._chains_in(_assigned(n)[1])),
|
|
242
|
+
key=lambda n: (n.lineno, n.col_offset))
|
|
243
|
+
latest: Dict[str, ast.AST] = {}
|
|
244
|
+
for node in assigns:
|
|
245
|
+
name, value = _assigned(node) # type: ignore[misc]
|
|
246
|
+
if name in latest and self._falls_back(node, name, value, parents, scope):
|
|
247
|
+
self._join(self._chains_in(latest[name]), self._chains_in(value))
|
|
248
|
+
latest[name] = value
|
|
249
|
+
for node in own:
|
|
250
|
+
if isinstance(node, ast.Try): # `try: x = a.get(k) ...` / `except ...: x = b[k]`
|
|
251
|
+
tried = {a[0]: a[1] for a in map(_assigned, _within(node.body)) if a}
|
|
252
|
+
for handler in node.handlers:
|
|
253
|
+
for a in filter(None, map(_assigned, _within(handler.body))):
|
|
254
|
+
if a[0] in tried:
|
|
255
|
+
self._join(self._chains_in(tried[a[0]]), self._chains_in(a[1]))
|
|
256
|
+
elif _is_or(node): # `legacy = c.get(k)` ... `legacy or c.get(k2)`
|
|
257
|
+
for operand in node.values: # type: ignore[attr-defined]
|
|
258
|
+
if isinstance(operand, ast.Name):
|
|
259
|
+
before = [n for n in assigns if _assigned(n)[0] == operand.id # type: ignore[index]
|
|
260
|
+
and (n.lineno, n.col_offset) < (node.lineno, node.col_offset)]
|
|
261
|
+
if before:
|
|
262
|
+
self._join(self._chains_in(_assigned(before[-1])[1]), self._chains_in(node)) # type: ignore[index]
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _falls_back(assign: ast.AST, name: str, value: ast.AST, parents: Dict[Any, Any], scope: ast.AST) -> bool:
|
|
266
|
+
"""Whether an assignment to `name` is the fallback of the one before it: `x = x or ...`, or it
|
|
267
|
+
sits in the body of an `if` that tests `x`. A walrus in that test is the first assignment.
|
|
268
|
+
`x = x + ...` accumulates; it is not a fallback."""
|
|
269
|
+
if _is_or(value) and isinstance(value.values[0], ast.Name) and value.values[0].id == name: # type: ignore[attr-defined]
|
|
270
|
+
return True
|
|
271
|
+
child, node = assign, parents.get(assign)
|
|
272
|
+
while node is not None and node is not scope:
|
|
273
|
+
if isinstance(node, ast.If) and child is not node.test and _reads_name(node.test, name):
|
|
274
|
+
return True
|
|
275
|
+
child, node = node, parents.get(node)
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def _judge(self, call: ast.Call) -> str:
|
|
279
|
+
if len(call.args) < 2:
|
|
280
|
+
return "keep"
|
|
281
|
+
default = call.args[1]
|
|
282
|
+
if isinstance(default, ast.Constant):
|
|
283
|
+
value = default.value
|
|
284
|
+
return "keep" if not isinstance(value, bool) and value in _STAND_INS else "drop"
|
|
285
|
+
if isinstance(default, (ast.Dict, ast.List, ast.Tuple, ast.Set)):
|
|
286
|
+
return "drop" if (getattr(default, "keys", None) or getattr(default, "elts", None)) else "keep"
|
|
287
|
+
if _is_get(default) or _is_or(default) or isinstance(default, (ast.Name, ast.IfExp, ast.Subscript)):
|
|
288
|
+
return "value"
|
|
289
|
+
return "drop"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _file_reads(filename: str) -> _FileReads:
|
|
293
|
+
if filename not in _FILES:
|
|
294
|
+
_FILES[filename] = _FileReads(filename)
|
|
295
|
+
return _FILES[filename]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _position(frame: Any) -> Optional[Tuple[int, int, int, int]]:
|
|
299
|
+
"""(lineno, end_lineno, col, end_col) of the expression a frame is running. 3.11+."""
|
|
300
|
+
code = frame.f_code
|
|
301
|
+
try:
|
|
302
|
+
positions = _POSITIONS.get(code)
|
|
303
|
+
if positions is None:
|
|
304
|
+
positions = _POSITIONS[code] = list(code.co_positions())
|
|
305
|
+
span = positions[frame.f_lasti // 2]
|
|
306
|
+
except (AttributeError, IndexError):
|
|
307
|
+
return None
|
|
308
|
+
return None if None in span else span
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _first_resume(code: Any) -> int:
|
|
312
|
+
if code not in _RESUMES:
|
|
313
|
+
_RESUMES[code] = next((i.offset for i in dis.get_instructions(code) if i.opname == "RESUME"), -1)
|
|
314
|
+
return _RESUMES[code]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _placeholder(value: Any) -> bool:
|
|
318
|
+
"""Whether a default's value stands in for one (0, None, "", {}, []). A bool is a flag's answer."""
|
|
319
|
+
if isinstance(value, bool):
|
|
320
|
+
return False
|
|
321
|
+
try:
|
|
322
|
+
return not value
|
|
323
|
+
except Exception: # noqa: BLE001 — a value whose truth raises is not a placeholder
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class _Reads:
|
|
328
|
+
"""Row-key reads across every response of one tick: how often each (tool, path) was found, and
|
|
329
|
+
the misses that nothing else in their fallback chain answered.
|
|
330
|
+
|
|
331
|
+
Chains come from the source (`_FileReads`). A read alone in its chain in a helper is judged at
|
|
332
|
+
its caller's expression when that is a chain (`_f(a, k) or _f(b, k)`). A loop over spellings —
|
|
333
|
+
`for k in ("traderAddress", "address"): v = t.get(k)` — runs one `.get` once per spelling, so a
|
|
334
|
+
miss is also dropped when the same expression, in the same call, finds a key on the same row (or
|
|
335
|
+
a dict inside it), before or after it.
|
|
336
|
+
|
|
337
|
+
Calls are told apart by the number the preflight tracer gives each one as it starts, so no frame
|
|
338
|
+
is kept alive. Chains are per thread, so a scanner scoring rows on a pool does not interleave them.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def __init__(self, calls: Optional[Dict[int, int]] = None) -> None:
|
|
342
|
+
self.found: Dict[Tuple[str, str], int] = {}
|
|
343
|
+
self.missed: Dict[Tuple[str, str], Dict[str, Any]] = {}
|
|
344
|
+
self._calls = calls if calls is not None else {}
|
|
345
|
+
self._lock = threading.Lock()
|
|
346
|
+
self._chains: Dict[int, Dict[Any, Dict[str, Any]]] = {} # per thread: open chains by (file, root)
|
|
347
|
+
# Rows are held for the tick, so a row's id is never reused. They are reachable from the
|
|
348
|
+
# response already, so this keeps nothing alive that would not be.
|
|
349
|
+
self._rows: Dict[int, Any] = {}
|
|
350
|
+
# Per row id: the (call, read) pairs that found a key there, and the misses each made.
|
|
351
|
+
self._row_hits: Dict[int, set] = {}
|
|
352
|
+
self._row_misses: Dict[int, Dict[Any, List[Tuple[str, str]]]] = {}
|
|
353
|
+
|
|
354
|
+
def read(self, tool: str, path: str, found: bool, default: Any, carried: Optional[List[str]], row: Any) -> None:
|
|
355
|
+
frame = sys._getframe(2) # noqa: SLF001 — 0 is here, 1 the accessor, 2 the scanner
|
|
356
|
+
with self._lock:
|
|
357
|
+
if found:
|
|
358
|
+
self.found[(tool, path)] = self.found.get((tool, path), 0) + 1
|
|
359
|
+
span = _position(frame)
|
|
360
|
+
if span is None:
|
|
361
|
+
return # nothing to tell a fallback from a guess by, so record no miss
|
|
362
|
+
file = frame.f_code.co_filename
|
|
363
|
+
reads = _file_reads(file)
|
|
364
|
+
key = (span[1], span[3])
|
|
365
|
+
rule = reads.defaults.get(key)
|
|
366
|
+
keep = not found and (rule == "keep" or (rule != "drop" and _placeholder(default)))
|
|
367
|
+
if key not in reads.roots or key in reads.solo:
|
|
368
|
+
outer = frame.f_back
|
|
369
|
+
while outer is not None and id(outer) in self._calls:
|
|
370
|
+
outer_span = _position(outer)
|
|
371
|
+
if outer_span is not None:
|
|
372
|
+
outer_reads = _file_reads(outer.f_code.co_filename)
|
|
373
|
+
outer_key = (outer_span[1], outer_span[3])
|
|
374
|
+
if outer_key in outer_reads.roots and outer_key not in outer_reads.solo:
|
|
375
|
+
frame, span, reads, key = outer, outer_span, outer_reads, outer_key
|
|
376
|
+
file = outer.f_code.co_filename
|
|
377
|
+
break
|
|
378
|
+
outer = outer.f_back
|
|
379
|
+
owner = row._owner
|
|
380
|
+
call = (id(frame), self._calls.get(id(frame), 0))
|
|
381
|
+
here = (call, key)
|
|
382
|
+
self._rows.setdefault(id(owner), owner)
|
|
383
|
+
if found:
|
|
384
|
+
self._row_hits.setdefault(id(owner), set()).add(here)
|
|
385
|
+
for missed in self._row_misses.get(id(owner), {}).pop(here, []):
|
|
386
|
+
self._forget(missed)
|
|
387
|
+
root = reads.roots.get(key, key)
|
|
388
|
+
# Every chain stays open until its own expression comes round again (the next row), its
|
|
389
|
+
# call changes, or the tick ends: an unrelated read between two halves of a fallback
|
|
390
|
+
# (`c = inst.get("context", {})`) does not end it.
|
|
391
|
+
open_chains = self._chains.setdefault(threading.get_ident(), {})
|
|
392
|
+
slot = (file, root)
|
|
393
|
+
chain = open_chains.get(slot)
|
|
394
|
+
if chain is not None and (chain["call"] != call or key in chain["keys"]):
|
|
395
|
+
self._close(open_chains.pop(slot))
|
|
396
|
+
chain = None
|
|
397
|
+
if chain is None:
|
|
398
|
+
chain = open_chains[slot] = {"call": call, "keys": set(), "found": False, "misses": []}
|
|
399
|
+
chain["keys"].add(key)
|
|
400
|
+
if found:
|
|
401
|
+
if key not in reads.receivers:
|
|
402
|
+
chain["found"] = True
|
|
403
|
+
elif keep:
|
|
404
|
+
chain["misses"].append((tool, path, carried, (file, span[0]), id(owner), here, row._path))
|
|
405
|
+
if key == root: # the outermost `.get` runs last, after every alternative
|
|
406
|
+
self._close(open_chains.pop(slot))
|
|
407
|
+
|
|
408
|
+
def _close(self, chain: Dict[str, Any]) -> None:
|
|
409
|
+
if chain["found"]:
|
|
410
|
+
return
|
|
411
|
+
for tool, path, carried, where, owner_id, here, row_path in chain["misses"]:
|
|
412
|
+
if here in self._row_hits.get(owner_id, ()):
|
|
413
|
+
continue # this expression, in this call, found another spelling on this row
|
|
414
|
+
entry = self.missed.setdefault(
|
|
415
|
+
(tool, path), {"reads": 0, "carried": carried, "where": where, "row": row_path})
|
|
416
|
+
entry["reads"] += 1
|
|
417
|
+
self._row_misses.setdefault(owner_id, {}).setdefault(here, []).append((tool, path))
|
|
418
|
+
|
|
419
|
+
def _forget(self, missed: Tuple[str, str]) -> None:
|
|
420
|
+
entry = self.missed.get(missed)
|
|
421
|
+
if entry is not None:
|
|
422
|
+
entry["reads"] -= 1
|
|
423
|
+
if entry["reads"] <= 0:
|
|
424
|
+
del self.missed[missed]
|
|
425
|
+
|
|
426
|
+
def never_found(self, root: str) -> Dict[str, List[Dict[str, Any]]]:
|
|
427
|
+
"""Per tool, keys read on rows of a list that no row of any of its responses carried.
|
|
428
|
+
Top-level keys are left out: an optional `error` or cursor is routinely absent."""
|
|
429
|
+
with self._lock:
|
|
430
|
+
for open_chains in self._chains.values():
|
|
431
|
+
for chain in open_chains.values():
|
|
432
|
+
self._close(chain)
|
|
433
|
+
self._chains.clear()
|
|
434
|
+
prefix = os.path.abspath(root).rstrip(os.sep) + os.sep
|
|
435
|
+
out: Dict[str, List[Dict[str, Any]]] = {}
|
|
436
|
+
for (tool, path), entry in self.missed.items():
|
|
437
|
+
if "[]" not in path or self.found.get((tool, path)):
|
|
438
|
+
continue
|
|
439
|
+
item: Dict[str, Any] = {
|
|
440
|
+
"path": path, "row": entry["row"], "reads": entry["reads"], "carried": entry["carried"]}
|
|
441
|
+
file, line = entry["where"]
|
|
442
|
+
if os.path.abspath(file).startswith(prefix):
|
|
443
|
+
item.update(file=os.path.relpath(file, root), line=line)
|
|
444
|
+
out.setdefault(tool, []).append(item)
|
|
445
|
+
return {tool: items[:10] for tool, items in out.items()}
|
|
446
|
+
|
|
447
|
+
|
|
60
448
|
class _Watched(dict):
|
|
61
449
|
"""A response that remembers whether the scanner looked inside it.
|
|
62
450
|
|
|
@@ -64,22 +452,56 @@ class _Watched(dict):
|
|
|
64
452
|
expects — a renamed key, a nesting level missed. That failure is silent: the read succeeded, the
|
|
65
453
|
scanner carried on, and the score was computed from a default. Recording access is what turns it
|
|
66
454
|
into something reportable.
|
|
455
|
+
|
|
456
|
+
The same failure one level down is a row key that does not exist: `resp["data"]` was read, so
|
|
457
|
+
the response counts as used, while every `row.get("volume", 0)` returned its default. Nested
|
|
458
|
+
dicts and lists are watched too, and every key read is reported to the tick's `_Reads`.
|
|
67
459
|
"""
|
|
68
460
|
|
|
69
|
-
def __init__(self, source:
|
|
461
|
+
def __init__(self, source: Any = (), tool: str = "", path: str = "", reads: Optional[_Reads] = None,
|
|
462
|
+
owner: Any = None) -> None:
|
|
70
463
|
super().__init__(source)
|
|
71
464
|
self.touched = False
|
|
465
|
+
self._tool = tool
|
|
466
|
+
self._path = path
|
|
467
|
+
self._reads = reads if reads is not None else _Reads()
|
|
468
|
+
# The list row this dict belongs to (itself for a row): `row["context"]` answers for `row`.
|
|
469
|
+
self._owner = owner if owner is not None else self
|
|
72
470
|
|
|
73
471
|
def _touch(self) -> None:
|
|
74
472
|
self.touched = True
|
|
75
473
|
|
|
474
|
+
def _at(self, key: Any) -> str:
|
|
475
|
+
key = str(key)[:_KEY_LIMIT]
|
|
476
|
+
return f"{self._path}.{key}" if self._path else key
|
|
477
|
+
|
|
478
|
+
def _child(self, key: Any, path: str, value: Any) -> Any:
|
|
479
|
+
# Wrapped once and stored back, so the scanner holds one object however often it looks.
|
|
480
|
+
if isinstance(value, dict) and not isinstance(value, _Watched):
|
|
481
|
+
value = _Watched(value, self._tool, path, self._reads, self._owner if "[]" in self._path else None)
|
|
482
|
+
elif isinstance(value, list) and not isinstance(value, _WatchedList):
|
|
483
|
+
value = _WatchedList(value, self._tool, path + "[]", self._reads)
|
|
484
|
+
else:
|
|
485
|
+
return value
|
|
486
|
+
dict.__setitem__(self, key, value)
|
|
487
|
+
return value
|
|
488
|
+
|
|
76
489
|
def __getitem__(self, key: Any) -> Any:
|
|
77
490
|
self._touch()
|
|
78
|
-
|
|
491
|
+
value = super().__getitem__(key) # a missing key raises, which is already visible
|
|
492
|
+
path = self._at(key)
|
|
493
|
+
self._reads.read(self._tool, path, True, None, None, self)
|
|
494
|
+
return self._child(key, path, value)
|
|
79
495
|
|
|
80
|
-
def get(self, key: Any, default: Any = None) -> Any:
|
|
496
|
+
def get(self, key: Any, default: Any = None, /) -> Any: # positional, as `dict.get` is
|
|
81
497
|
self._touch()
|
|
82
|
-
|
|
498
|
+
path = self._at(key)
|
|
499
|
+
if dict.__contains__(self, key):
|
|
500
|
+
self._reads.read(self._tool, path, True, None, None, self)
|
|
501
|
+
return self._child(key, path, dict.__getitem__(self, key))
|
|
502
|
+
carried = sorted(str(k)[:_KEY_LIMIT] for k in dict.keys(self))[:20]
|
|
503
|
+
self._reads.read(self._tool, path, False, default, carried, self)
|
|
504
|
+
return default
|
|
83
505
|
|
|
84
506
|
def __contains__(self, key: Any) -> bool:
|
|
85
507
|
self._touch()
|
|
@@ -101,6 +523,47 @@ class _Watched(dict):
|
|
|
101
523
|
self._touch()
|
|
102
524
|
return super().items()
|
|
103
525
|
|
|
526
|
+
def __reduce_ex__(self, protocol: Any) -> Any:
|
|
527
|
+
# A copy or pickle is plain data, not the tick's bookkeeping.
|
|
528
|
+
return (dict, (dict(self),))
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class _WatchedList(list):
|
|
532
|
+
"""A list inside a watched response. Its rows (dicts, and lists of rows) are watched as they are
|
|
533
|
+
handed out, and stored back, so a slice or a loop holds the same objects as the list."""
|
|
534
|
+
|
|
535
|
+
def __init__(self, source: Any = (), tool: str = "", path: str = "", reads: Optional[_Reads] = None) -> None:
|
|
536
|
+
super().__init__(source)
|
|
537
|
+
self._tool = tool
|
|
538
|
+
self._path = path
|
|
539
|
+
self._reads = reads if reads is not None else _Reads()
|
|
540
|
+
|
|
541
|
+
def _row(self, index: int, value: Any) -> Any:
|
|
542
|
+
if isinstance(value, dict) and not isinstance(value, _Watched):
|
|
543
|
+
value = _Watched(value, self._tool, self._path, self._reads)
|
|
544
|
+
elif isinstance(value, list) and not isinstance(value, _WatchedList):
|
|
545
|
+
value = _WatchedList(value, self._tool, self._path + "[]", self._reads)
|
|
546
|
+
else:
|
|
547
|
+
return value
|
|
548
|
+
list.__setitem__(self, index, value)
|
|
549
|
+
return value
|
|
550
|
+
|
|
551
|
+
def __getitem__(self, index: Any) -> Any:
|
|
552
|
+
if isinstance(index, slice): # `rows[:20]` is how a universe gets capped
|
|
553
|
+
for i in range(*index.indices(len(self))):
|
|
554
|
+
self._row(i, list.__getitem__(self, i))
|
|
555
|
+
return _WatchedList(list.__getitem__(self, index), self._tool, self._path, self._reads)
|
|
556
|
+
return self._row(index, list.__getitem__(self, index))
|
|
557
|
+
|
|
558
|
+
def __iter__(self) -> Any:
|
|
559
|
+
index = 0
|
|
560
|
+
while index < len(self): # a list iterator's own rule, so mid-loop removes and appends match
|
|
561
|
+
yield self._row(index, list.__getitem__(self, index))
|
|
562
|
+
index += 1
|
|
563
|
+
|
|
564
|
+
def __reduce_ex__(self, protocol: Any) -> Any:
|
|
565
|
+
return (list, (list(list.__iter__(self)),))
|
|
566
|
+
|
|
104
567
|
|
|
105
568
|
class CountingMcp:
|
|
106
569
|
"""Records every call and forwards it unchanged.
|
|
@@ -114,10 +577,11 @@ class CountingMcp:
|
|
|
114
577
|
place for it, since the same boundary protects production.
|
|
115
578
|
"""
|
|
116
579
|
|
|
117
|
-
def __init__(self, inner: Any) -> None:
|
|
580
|
+
def __init__(self, inner: Any, call_ids: Optional[Dict[int, int]] = None) -> None:
|
|
118
581
|
self._inner = inner
|
|
119
582
|
self.calls: List[Dict[str, Any]] = []
|
|
120
583
|
self._watched: List[Dict[str, Any]] = []
|
|
584
|
+
self._reads = _Reads(call_ids)
|
|
121
585
|
|
|
122
586
|
def call_tool(self, name: str, args: Optional[dict] = None) -> Any:
|
|
123
587
|
started = time.monotonic()
|
|
@@ -163,16 +627,22 @@ class CountingMcp:
|
|
|
163
627
|
self.calls.append(record)
|
|
164
628
|
|
|
165
629
|
if isinstance(result, dict) and record["ok"]:
|
|
166
|
-
watched = _Watched(result)
|
|
630
|
+
watched = _Watched(result, name, "", self._reads)
|
|
167
631
|
self._watched.append({"record": record, "response": watched})
|
|
168
632
|
return watched
|
|
169
633
|
return result
|
|
170
634
|
|
|
171
|
-
def
|
|
172
|
-
"""Mark the responses nothing
|
|
635
|
+
def settle(self, root: str) -> None:
|
|
636
|
+
"""Mark the responses nothing looked inside, and the row keys looked for and never found.
|
|
637
|
+
Called once the tick is over. Missing keys are judged per tool, across all its responses,
|
|
638
|
+
and land on that tool's first record."""
|
|
639
|
+
missing = self._reads.never_found(root)
|
|
173
640
|
for entry in self._watched:
|
|
641
|
+
record = entry["record"]
|
|
174
642
|
if not entry["response"].touched:
|
|
175
|
-
|
|
643
|
+
record["unread"] = True
|
|
644
|
+
if record["tool"] in missing:
|
|
645
|
+
record["missing_keys"] = missing.pop(record["tool"])
|
|
176
646
|
|
|
177
647
|
def __getattr__(self, name: str) -> Any:
|
|
178
648
|
# Keeps the wrapper transparent for anything beyond the documented surface.
|
|
@@ -282,6 +752,10 @@ class _Tracer:
|
|
|
282
752
|
# pin a chain of tracebacks.
|
|
283
753
|
self._live_exc: Any = None
|
|
284
754
|
self._group = 0
|
|
755
|
+
# The call each live scanner frame belongs to, numbered as calls start. A frame's id is
|
|
756
|
+
# reused once it returns; this tells two calls apart without keeping either frame alive.
|
|
757
|
+
self.call_ids: Dict[int, int] = {}
|
|
758
|
+
self._call_seq = itertools.count(1)
|
|
285
759
|
|
|
286
760
|
def _own(self, frame: Any) -> bool:
|
|
287
761
|
return os.path.abspath(frame.f_code.co_filename).startswith(self._prefix)
|
|
@@ -292,6 +766,11 @@ class _Tracer:
|
|
|
292
766
|
return None
|
|
293
767
|
# Exceptions and returns still fire; per-line events do not.
|
|
294
768
|
frame.f_trace_lines = False
|
|
769
|
+
# A generator fires 'call' on every resume; it keeps the number of its first entry.
|
|
770
|
+
code = frame.f_code
|
|
771
|
+
if not (code.co_flags & _GENERATOR and id(frame) in self.call_ids
|
|
772
|
+
and frame.f_lasti != _first_resume(code)):
|
|
773
|
+
self.call_ids[id(frame)] = next(self._call_seq)
|
|
295
774
|
return self
|
|
296
775
|
if event == "exception":
|
|
297
776
|
exc_type, exc_value, _tb = arg
|
|
@@ -378,6 +857,7 @@ def run_preflight(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
378
857
|
from . import scaffold as scaffold_mod
|
|
379
858
|
|
|
380
859
|
root = config["path"]
|
|
860
|
+
_FILES.clear() # the scanner's source is read fresh for each tick
|
|
381
861
|
sink = _CollectingSink()
|
|
382
862
|
tracer = _Tracer(root)
|
|
383
863
|
|
|
@@ -388,7 +868,7 @@ def run_preflight(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
388
868
|
"reason": "no_credentials",
|
|
389
869
|
"detail": "no data-client credentials are configured in this environment",
|
|
390
870
|
}
|
|
391
|
-
counter = CountingMcp(inner)
|
|
871
|
+
counter = CountingMcp(inner, tracer.call_ids)
|
|
392
872
|
|
|
393
873
|
captured_out = io.StringIO()
|
|
394
874
|
captured_err = io.StringIO()
|
|
@@ -430,7 +910,7 @@ def run_preflight(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
430
910
|
sys.settrace(None)
|
|
431
911
|
sys.stdout, sys.stderr = real_stdout, real_stderr
|
|
432
912
|
http_post.PREFLIGHT = False
|
|
433
|
-
counter.
|
|
913
|
+
counter.settle(root)
|
|
434
914
|
tracer.release()
|
|
435
915
|
|
|
436
916
|
# A tick that failed was reported through the sink rather than raised. The loop's error record
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fake Senpi MCP that holds a user's live strategies and simulations and answers `strategy_list`
|
|
3
|
+
* and `strategy_create_custom_strategy` the way the paper router does (SPEC §6.3, §6.5): one world
|
|
4
|
+
* per call, `paper_routing: true` at the head of every paper reply, `paper: true` on every paper
|
|
5
|
+
* row, and `PAPER_IDENTIFIER_MISMATCH` for a flag that disagrees with its filter.
|
|
6
|
+
*
|
|
7
|
+
* `router: false` plays an MCP without the router: it drops the unknown `paper` key and answers
|
|
8
|
+
* from the live world with no marker, and its strict UUID check refuses a `paper-` id.
|
|
9
|
+
*
|
|
10
|
+
* Every call is recorded so a test can assert what reached the MCP.
|
|
11
|
+
*/
|
|
12
|
+
import type { McpTransport } from "../../types.js";
|
|
13
|
+
export interface FakeStrategyRow {
|
|
14
|
+
id: string;
|
|
15
|
+
strategyWalletAddress: string;
|
|
16
|
+
status: string;
|
|
17
|
+
strategyName: string;
|
|
18
|
+
skillName?: string;
|
|
19
|
+
totalFunded?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface FakePaperMcpOptions {
|
|
22
|
+
router?: boolean;
|
|
23
|
+
live?: FakeStrategyRow[];
|
|
24
|
+
paper?: FakeStrategyRow[];
|
|
25
|
+
/** Statuses a new simulation reports on successive reads; the last one sticks. */
|
|
26
|
+
paperCreateStatuses?: string[];
|
|
27
|
+
/** The wallet a new simulation gets. Defaults to a hypaper-format address. */
|
|
28
|
+
paperCreateWallet?: string;
|
|
29
|
+
/** Rows the router forgot to flag `paper: true` (a broken router, for the row filter). */
|
|
30
|
+
unflaggedPaperRows?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface FakeCall {
|
|
33
|
+
tool: string;
|
|
34
|
+
args: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface FakePaperMcp {
|
|
37
|
+
transport: McpTransport;
|
|
38
|
+
calls: FakeCall[];
|
|
39
|
+
callsTo(tool: string): Array<Record<string, unknown>>;
|
|
40
|
+
live: FakeStrategyRow[];
|
|
41
|
+
paper: FakeStrategyRow[];
|
|
42
|
+
}
|
|
43
|
+
export declare function createFakePaperMcp(options?: FakePaperMcpOptions): FakePaperMcp;
|
|
44
|
+
//# sourceMappingURL=fake-paper-mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fake-paper-mcp.d.ts","sourceRoot":"","sources":["../../../../src/senpi/__tests__/helpers/fake-paper-mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,qBAAqB,EAAE,MAAM,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,kFAAkF;IAClF,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,0FAA0F;IAC1F,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,YAAY,CAAC;IACxB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,KAAK,EAAE,eAAe,EAAE,CAAC;CAC1B;AA2BD,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,mBAAwB,GAAG,YAAY,CA0GlF"}
|