@senpi/runtime 3.0.56 → 3.0.58

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 (99) hide show
  1. package/dist/cli/guide-commands.d.ts.map +1 -1
  2. package/dist/cli/guide-commands.js +28 -2
  3. package/dist/cli/guide-commands.js.map +1 -1
  4. package/dist/cli/senpi-commands.d.ts.map +1 -1
  5. package/dist/cli/senpi-commands.js +2 -0
  6. package/dist/cli/senpi-commands.js.map +1 -1
  7. package/dist/cli/validate-command.d.ts +29 -0
  8. package/dist/cli/validate-command.d.ts.map +1 -0
  9. package/dist/cli/validate-command.js +89 -0
  10. package/dist/cli/validate-command.js.map +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +62 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/runtime/load-runtime-config.d.ts +38 -0
  15. package/dist/runtime/load-runtime-config.d.ts.map +1 -1
  16. package/dist/runtime/load-runtime-config.js +33 -25
  17. package/dist/runtime/load-runtime-config.js.map +1 -1
  18. package/dist/runtime/runtime-schema.d.ts +100 -100
  19. package/dist/scanners/external/scaffold/__main__.py +8 -0
  20. package/dist/scanners/external/scaffold/ctx.py +15 -1
  21. package/dist/scanners/external/scaffold/http_post.py +30 -0
  22. package/dist/scanners/external/scaffold/import_check.py +310 -0
  23. package/dist/scanners/external/scaffold/preflight.py +545 -0
  24. package/dist/scanners/external/scaffold/scaffold.py +19 -6
  25. package/dist/scanners/implementations/market-regime.d.ts +1 -1
  26. package/dist/telemetry/metrics.d.ts +6 -0
  27. package/dist/telemetry/metrics.d.ts.map +1 -1
  28. package/dist/telemetry/metrics.js +45 -0
  29. package/dist/telemetry/metrics.js.map +1 -1
  30. package/dist/utils/event-catalog.d.ts +40 -0
  31. package/dist/utils/event-catalog.d.ts.map +1 -1
  32. package/dist/utils/event-catalog.js +12 -0
  33. package/dist/utils/event-catalog.js.map +1 -1
  34. package/dist/validate/codes.d.ts +165 -0
  35. package/dist/validate/codes.d.ts.map +1 -0
  36. package/dist/validate/codes.js +381 -0
  37. package/dist/validate/codes.js.map +1 -0
  38. package/dist/validate/coverage.d.ts +34 -0
  39. package/dist/validate/coverage.d.ts.map +1 -0
  40. package/dist/validate/coverage.js +34 -0
  41. package/dist/validate/coverage.js.map +1 -0
  42. package/dist/validate/import-stage.d.ts +56 -0
  43. package/dist/validate/import-stage.d.ts.map +1 -0
  44. package/dist/validate/import-stage.js +244 -0
  45. package/dist/validate/import-stage.js.map +1 -0
  46. package/dist/validate/index.d.ts +28 -0
  47. package/dist/validate/index.d.ts.map +1 -0
  48. package/dist/validate/index.js +27 -0
  49. package/dist/validate/index.js.map +1 -0
  50. package/dist/validate/install-gate.d.ts +44 -0
  51. package/dist/validate/install-gate.d.ts.map +1 -0
  52. package/dist/validate/install-gate.js +66 -0
  53. package/dist/validate/install-gate.js.map +1 -0
  54. package/dist/validate/live-stage.d.ts +134 -0
  55. package/dist/validate/live-stage.d.ts.map +1 -0
  56. package/dist/validate/live-stage.js +472 -0
  57. package/dist/validate/live-stage.js.map +1 -0
  58. package/dist/validate/proof.d.ts +104 -0
  59. package/dist/validate/proof.d.ts.map +1 -0
  60. package/dist/validate/proof.js +269 -0
  61. package/dist/validate/proof.js.map +1 -0
  62. package/dist/validate/recipe-checks.d.ts +79 -0
  63. package/dist/validate/recipe-checks.d.ts.map +1 -0
  64. package/dist/validate/recipe-checks.js +205 -0
  65. package/dist/validate/recipe-checks.js.map +1 -0
  66. package/dist/validate/recipe.d.ts +41 -0
  67. package/dist/validate/recipe.d.ts.map +1 -0
  68. package/dist/validate/recipe.js +215 -0
  69. package/dist/validate/recipe.js.map +1 -0
  70. package/dist/validate/report.d.ts +35 -0
  71. package/dist/validate/report.d.ts.map +1 -0
  72. package/dist/validate/report.js +80 -0
  73. package/dist/validate/report.js.map +1 -0
  74. package/dist/validate/run.d.ts +101 -0
  75. package/dist/validate/run.d.ts.map +1 -0
  76. package/dist/validate/run.js +471 -0
  77. package/dist/validate/run.js.map +1 -0
  78. package/dist/validate/signal-stage.d.ts +40 -0
  79. package/dist/validate/signal-stage.d.ts.map +1 -0
  80. package/dist/validate/signal-stage.js +116 -0
  81. package/dist/validate/signal-stage.js.map +1 -0
  82. package/dist/validate/target.d.ts +70 -0
  83. package/dist/validate/target.d.ts.map +1 -0
  84. package/dist/validate/target.js +231 -0
  85. package/dist/validate/target.js.map +1 -0
  86. package/dist/validate/telemetry.d.ts +24 -0
  87. package/dist/validate/telemetry.d.ts.map +1 -0
  88. package/dist/validate/telemetry.js +60 -0
  89. package/dist/validate/telemetry.js.map +1 -0
  90. package/dist/validate/types.d.ts +115 -0
  91. package/dist/validate/types.d.ts.map +1 -0
  92. package/dist/validate/types.js +31 -0
  93. package/dist/validate/types.js.map +1 -0
  94. package/dist/validate/wallet.d.ts +63 -0
  95. package/dist/validate/wallet.d.ts.map +1 -0
  96. package/dist/validate/wallet.js +66 -0
  97. package/dist/validate/wallet.js.map +1 -0
  98. package/openclaw.plugin.json +1 -1
  99. package/package.json +2 -1
@@ -0,0 +1,545 @@
1
+ """Run one tick, observe everything, deliver nothing.
2
+
3
+ python3 -m scaffold.preflight <launch-config.json>
4
+
5
+ Runs the author's `scan()` exactly once against live data, through the same loop production uses,
6
+ and reports what happened as one JSON document, written to the `result_path` the caller names.
7
+
8
+ Signals are collected and returned rather than delivered — the runtime's own delivery funnel is
9
+ closed for the duration and raises if used. That covers the sanctioned path, which is the one
10
+ authored code is supposed to take; a scanner reaching for a socket directly is caught statically at
11
+ the import depth, not stopped here. So: nothing leaves by the route the scaffold owns.
12
+
13
+ ## What this adds beyond running the loop
14
+
15
+ **Reads are counted.** A tick that returns nothing having read nothing is indistinguishable, from
16
+ the outside, from a healthy scanner with nothing to trade. Counting successful reads is what tells
17
+ those apart — and it is the difference between reporting a strategy as fine and reporting that
18
+ nothing about it was established.
19
+
20
+ **Suppressed exceptions are recorded.** The author contract tells scanners to return `[]` on any
21
+ failure rather than crash, which is right for production and blinding for validation: a scanner
22
+ whose every read fails looks exactly like one with no setups. Tracing catches exceptions as they
23
+ are raised, before the author's own handler swallows them.
24
+
25
+ **The early exit is located.** When a tick returns without reading, the line it returned from turns
26
+ "something stopped early" into an edit.
27
+
28
+ **Author output cannot corrupt the result.** The tick shares this process's stdout, so the result
29
+ does not travel on it: the caller names a `result_path` and the document is written there, atomically.
30
+ Author stdout is captured as evidence and never parsed. Redirecting `sys.stdout` alone would only
31
+ cover `print()` — `os.write(1, ...)`, a subprocess, or a C extension write to the same descriptor.
32
+ """
33
+
34
+ import io
35
+ import json
36
+ import math
37
+ import os
38
+ import sys
39
+ import time
40
+ import traceback
41
+ from typing import Any, Dict, List, Optional
42
+
43
+ from . import http_post
44
+ from .signal_envelope import candidate_to_signal
45
+
46
+ # Evidence is meant to be complete, but a scanner that prints in a loop will produce megabytes of
47
+ # it. Bounded with the ends kept and the middle marked, so the output stays honest about what was
48
+ # dropped rather than trailing off.
49
+ _STREAM_LIMIT = 32_768
50
+
51
+
52
+ def _bounded(text: str, limit: int = _STREAM_LIMIT) -> str:
53
+ if len(text) <= limit:
54
+ return text
55
+ half = limit // 2
56
+ dropped = len(text) - limit
57
+ return "{}\n… truncated {} characters …\n{}".format(text[:half], dropped, text[-half:])
58
+
59
+
60
+ class _Watched(dict):
61
+ """A response that remembers whether the scanner looked inside it.
62
+
63
+ Fetching a payload and reading nothing out of it usually means the shape is not what the code
64
+ expects — a renamed key, a nesting level missed. That failure is silent: the read succeeded, the
65
+ scanner carried on, and the score was computed from a default. Recording access is what turns it
66
+ into something reportable.
67
+ """
68
+
69
+ def __init__(self, source: dict) -> None:
70
+ super().__init__(source)
71
+ self.touched = False
72
+
73
+ def _touch(self) -> None:
74
+ self.touched = True
75
+
76
+ def __getitem__(self, key: Any) -> Any:
77
+ self._touch()
78
+ return super().__getitem__(key)
79
+
80
+ def get(self, key: Any, default: Any = None) -> Any:
81
+ self._touch()
82
+ return super().get(key, default)
83
+
84
+ def __contains__(self, key: Any) -> bool:
85
+ self._touch()
86
+ return super().__contains__(key)
87
+
88
+ def __iter__(self) -> Any:
89
+ self._touch()
90
+ return super().__iter__()
91
+
92
+ def keys(self) -> Any:
93
+ self._touch()
94
+ return super().keys()
95
+
96
+ def values(self) -> Any:
97
+ self._touch()
98
+ return super().values()
99
+
100
+ def items(self) -> Any:
101
+ self._touch()
102
+ return super().items()
103
+
104
+
105
+ class CountingMcp:
106
+ """Records every call and forwards it unchanged.
107
+
108
+ A pass-through, not a stand-in: the real client is called, over the real network, and the real
109
+ payload is returned. Only the record is new. A substituted payload could not surface the type
110
+ and shape problems that make up most of what a live tick is for.
111
+
112
+ Nothing here decides what a scanner may call. The client refuses money- and state-changing
113
+ tools itself, before any transport is opened, and validation inherits that — which is the right
114
+ place for it, since the same boundary protects production.
115
+ """
116
+
117
+ def __init__(self, inner: Any) -> None:
118
+ self._inner = inner
119
+ self.calls: List[Dict[str, Any]] = []
120
+ self._watched: List[Dict[str, Any]] = []
121
+
122
+ def call_tool(self, name: str, args: Optional[dict] = None) -> Any:
123
+ started = time.monotonic()
124
+ try:
125
+ result = self._inner.call_tool(name, args)
126
+ except Exception as exc: # noqa: BLE001
127
+ # Deliberately not BaseException: a tick timeout unwinds through here and must not be
128
+ # recorded as a failed read or, worse, absorbed.
129
+ record = {
130
+ "tool": name,
131
+ "ms": round((time.monotonic() - started) * 1000),
132
+ "ok": False,
133
+ "error_type": type(exc).__name__,
134
+ "error": str(exc)[:500],
135
+ }
136
+ # The client refuses money- and state-changing tools before it opens a transport, and
137
+ # owns the list of which those are. Marked rather than re-derived here: a second
138
+ # opinion about what counts as a write would be a worse one, and would drift.
139
+ if isinstance(exc, PermissionError):
140
+ record["blocked"] = True
141
+ self.calls.append(record)
142
+ raise
143
+
144
+ record: Dict[str, Any] = {
145
+ "tool": name,
146
+ "ms": round((time.monotonic() - started) * 1000),
147
+ "ok": _payload_ok(result),
148
+ }
149
+ if isinstance(result, dict):
150
+ # Inspected before wrapping — reading keys here would otherwise count as the scanner
151
+ # having looked. Only recorded when it might explain something: a tick that read
152
+ # successfully and emitted nothing is the case where the shape is worth seeing.
153
+ record["response_keys"] = sorted(result.keys())[:20]
154
+ if "_cast_dropped" in result:
155
+ record["cast_dropped"] = True
156
+ if not record["ok"]:
157
+ record["error"] = _envelope_error(result)
158
+ self.calls.append(record)
159
+
160
+ if isinstance(result, dict) and record["ok"]:
161
+ watched = _Watched(result)
162
+ self._watched.append({"record": record, "response": watched})
163
+ return watched
164
+ return result
165
+
166
+ def settle_unread(self) -> None:
167
+ """Mark the responses nothing ever looked inside. Called once the tick is over."""
168
+ for entry in self._watched:
169
+ if not entry["response"].touched:
170
+ entry["record"]["unread"] = True
171
+
172
+ def __getattr__(self, name: str) -> Any:
173
+ # Keeps the wrapper transparent for anything beyond the documented surface.
174
+ return getattr(self._inner, name)
175
+
176
+ @property
177
+ def reads_ok(self) -> int:
178
+ return sum(1 for c in self.calls if c["ok"])
179
+
180
+ @property
181
+ def reads_failed(self) -> int:
182
+ return len(self.calls) - self.reads_ok
183
+
184
+
185
+ def _payload_ok(payload: Any) -> bool:
186
+ """Whether a returned payload represents a successful read.
187
+
188
+ Not simply "did not raise". The transport raises on a protocol-level error, but an
189
+ application-level failure comes back as an ordinary response carrying a false success flag —
190
+ counting that as a read would reproduce, one layer down, exactly the confusion this is here to
191
+ remove. Shape-tolerant, because not every response carries the flag.
192
+ """
193
+ return not (isinstance(payload, dict) and "success" in payload and not payload["success"])
194
+
195
+
196
+ def _envelope_error(payload: Any) -> str:
197
+ if isinstance(payload, dict):
198
+ err = payload.get("error")
199
+ if isinstance(err, dict):
200
+ return str(err.get("message") or err)
201
+ if err is not None:
202
+ return str(err)
203
+ return "the response reported failure"
204
+
205
+
206
+ class _Tracer:
207
+ """Records exceptions raised inside the scanner's own files, and where it returned from.
208
+
209
+ A trace function sees an exception at the moment it is raised, not when it propagates — so it
210
+ sees the ones the author's handler is about to swallow, which is the entire point.
211
+
212
+ Three things this deliberately does NOT do:
213
+
214
+ **Line tracing.** Returning a handler from `'call'` arms per-line tracing for that frame, which
215
+ costs ~12x on compute-heavy code — enough to make a scanner blow a budget here that it meets in
216
+ production, which would be this command lying about production. `'exception'` and `'return'`
217
+ still fire with lines off.
218
+
219
+ **Module frames.** The tick runs after the entrypoint is imported, but the import itself is
220
+ traced, and `try: import optional_dep / except ImportError` is a completely ordinary module-level
221
+ idiom. Reporting it as a suppressed exception would flag a clean scanner.
222
+
223
+ **Unbounded growth.** A `try/except` inside a large loop raises once per row; the report caps at
224
+ 50, but building 100k dicts to throw away is the sort of cost that only shows up on the scanner
225
+ least able to afford it.
226
+ """
227
+
228
+ #: Kept during the tick, not just at report time — see the class docstring.
229
+ LIMIT = 50
230
+
231
+ def __init__(self, root: str) -> None:
232
+ # The separator matters: a bare prefix test makes root `/x/scanner` claim
233
+ # `/x/scanner-utils/evil.py`, which is plausible with sibling per-instance scanner dirs.
234
+ self.root = os.path.abspath(root)
235
+ self._prefix = self.root.rstrip(os.sep) + os.sep
236
+ self.suppressed: List[Dict[str, Any]] = []
237
+ self.dropped = 0
238
+ self.last_return: Optional[Dict[str, Any]] = None
239
+ # Grouping is by object identity, held as a live reference — `id()` alone is unsound here,
240
+ # because CPython reuses an address as soon as the previous exception is collected, so two
241
+ # unrelated failures of the same class routinely share one. Exactly one reference is kept
242
+ # at a time (replaced on the next raise, dropped at the end of the tick), so this cannot
243
+ # pin a chain of tracebacks.
244
+ self._live_exc: Any = None
245
+ self._group = 0
246
+
247
+ def _own(self, frame: Any) -> bool:
248
+ return os.path.abspath(frame.f_code.co_filename).startswith(self._prefix)
249
+
250
+ def __call__(self, frame: Any, event: str, arg: Any) -> Any:
251
+ if event == "call":
252
+ if not self._own(frame) or frame.f_code.co_name == "<module>":
253
+ return None
254
+ # Exceptions and returns still fire; per-line events do not.
255
+ frame.f_trace_lines = False
256
+ return self
257
+ if event == "exception":
258
+ exc_type, exc_value, _tb = arg
259
+ # One exception propagating outward fires once per author frame. Consecutive events for
260
+ # the same object are one failure; a different object starts a new group.
261
+ if exc_value is not self._live_exc:
262
+ self._live_exc = exc_value
263
+ self._group += 1
264
+ if len(self.suppressed) >= self.LIMIT:
265
+ self.dropped += 1
266
+ return self
267
+ self.suppressed.append(
268
+ {
269
+ "file": os.path.relpath(frame.f_code.co_filename, self.root),
270
+ "line": frame.f_lineno,
271
+ "type": exc_type.__name__,
272
+ "message": str(exc_value)[:300],
273
+ "_group": self._group,
274
+ }
275
+ )
276
+ elif event == "return":
277
+ self.last_return = {
278
+ "file": os.path.relpath(frame.f_code.co_filename, self.root),
279
+ "line": frame.f_lineno,
280
+ "function": frame.f_code.co_name,
281
+ }
282
+ return self
283
+
284
+ def claim_ending(self, error_type: str) -> Optional[Dict[str, Any]]:
285
+ """Take every entry belonging to the exception that ended the tick; return its raise site.
286
+
287
+ Two things this gets right that matching one entry by type does not.
288
+
289
+ The ending exception is not something the scanner "carried on from" — leaving *any* of its
290
+ entries in `suppressed` says the opposite of what happened. It fires once per author frame
291
+ it propagates through, so a helper raising into `scan()` leaves two, and removing one leaves
292
+ the other reading as a caught failure.
293
+
294
+ And of those entries, the FIRST is the raise site; the later ones are call sites further out.
295
+ Reporting a call site as the location points an edit a line above the defect. The tick ends
296
+ at the last exception raised, so the last entry of that type identifies it — and identity,
297
+ not type, gathers the rest of its frames.
298
+ """
299
+ for entry in reversed(self.suppressed):
300
+ if entry["type"] != error_type:
301
+ continue
302
+ group = entry.get("_group")
303
+ mine = [e for e in self.suppressed if e.get("_group") == group]
304
+ self.suppressed = [e for e in self.suppressed if e.get("_group") != group]
305
+ return mine[0]
306
+ return None
307
+
308
+ def release(self) -> None:
309
+ """Drop the reference held for grouping, so no traceback outlives the tick."""
310
+ self._live_exc = None
311
+
312
+ def report(self) -> List[Dict[str, Any]]:
313
+ """The suppressed list as it goes on the wire — identity stripped, it is bookkeeping."""
314
+ return [{k: v for k, v in e.items() if k != "_group"} for e in self.suppressed]
315
+
316
+
317
+ class _CollectingSink:
318
+ """Takes what the tick produced, and what went wrong, without sending either anywhere.
319
+
320
+ Both halves matter. The loop does not raise a scanner's own failure — it records the tick as
321
+ failed and reports it through the sink's error channel, so a preflight that only watched for an
322
+ exception would call a tick that raised "ok". Having the attribute is what opts into that
323
+ channel, which is why this is an object rather than a function.
324
+ """
325
+
326
+ def __init__(self) -> None:
327
+ self.signals: List[Any] = []
328
+ self.errors: List[Dict[str, Any]] = []
329
+
330
+ def __call__(self, candidates: Any) -> None:
331
+ self.signals.extend(candidates)
332
+
333
+ def post_error(self, error: Dict[str, Any]) -> None:
334
+ self.errors.append(error)
335
+
336
+
337
+ def run_preflight(config: Dict[str, Any]) -> Dict[str, Any]:
338
+ """Execute one tick and describe it. Never raises for a scanner's own failure."""
339
+ from . import scaffold as scaffold_mod
340
+
341
+ root = config["path"]
342
+ sink = _CollectingSink()
343
+ tracer = _Tracer(root)
344
+
345
+ inner = scaffold_mod._build_senpi_mcp() # noqa: SLF001 — the module's own construction seam
346
+ if inner is None:
347
+ return {
348
+ "ok": False,
349
+ "reason": "no_credentials",
350
+ "detail": "no data-client credentials are configured in this environment",
351
+ }
352
+ counter = CountingMcp(inner)
353
+
354
+ captured_out = io.StringIO()
355
+ captured_err = io.StringIO()
356
+ real_stdout, real_stderr = sys.stdout, sys.stderr
357
+ status = "ok"
358
+ failure: Optional[Dict[str, Any]] = None
359
+
360
+ http_post.PREFLIGHT = True
361
+ sys.stdout = captured_out
362
+ sys.stderr = captured_err
363
+ sys.settrace(tracer)
364
+ try:
365
+ scaffold_mod.run_scaffold(
366
+ root,
367
+ config["entrypoint"],
368
+ interval=float(config["interval_seconds"]),
369
+ timeout_seconds=config.get("timeout_seconds"),
370
+ inputs=config.get("inputs") or {},
371
+ state_path=config.get("state_path"),
372
+ scanner_name=config["scanner_name"],
373
+ wallet=config["wallet"],
374
+ default_signal_validity_seconds=int(config["default_signal_validity_seconds"]),
375
+ state_history_max_count=config.get("state_history_max_count") or 0,
376
+ sink=sink,
377
+ max_ticks=1,
378
+ install_signal_handlers=False,
379
+ senpi_mcp=counter,
380
+ dry_run=True,
381
+ )
382
+ except BaseException as exc: # noqa: BLE001 — a scanner may raise anything at all
383
+ # Only launch failures reach here: the loop handles a failing tick itself.
384
+ status = "error"
385
+ failure = {
386
+ "type": type(exc).__name__,
387
+ "message": str(exc)[:1000],
388
+ "traceback": _bounded("".join(traceback.format_exception(type(exc), exc, exc.__traceback__))),
389
+ }
390
+ finally:
391
+ sys.settrace(None)
392
+ sys.stdout, sys.stderr = real_stdout, real_stderr
393
+ http_post.PREFLIGHT = False
394
+ counter.settle_unread()
395
+ tracer.release()
396
+
397
+ # A tick that failed was reported through the sink rather than raised. The loop's error record
398
+ # carries the type and message but not the location — so it is composed with what the tracer
399
+ # saw. That gives a file and line, which is what an edit needs and a stack string is not.
400
+ if status == "ok" and sink.errors:
401
+ first = sink.errors[0]
402
+ status = "timeout" if first.get("type") == "timeout" else "error"
403
+ error_type = first.get("error_type") or first.get("type") or "tick_error"
404
+
405
+ # The loop reports a failed tick through the sink rather than raising it, and its record
406
+ # carries the type and message but not the location — so it is composed with what the
407
+ # tracer saw. A file and line is what an edit needs; a stack string is not.
408
+ where = tracer.claim_ending(error_type)
409
+
410
+ message = str(first.get("message", ""))[:1000]
411
+ if not message:
412
+ # A malformed return arrives with an empty message and its reason only in stderr, which
413
+ # is the one place a finding cannot quote. Recover what the loop knows.
414
+ message = str(first.get("error") or first.get("reason") or error_type)[:1000]
415
+
416
+ failure = {
417
+ "type": error_type,
418
+ "message": message,
419
+ "where": {"file": where["file"], "line": where["line"]} if where else None,
420
+ "reported_by": "scaffold",
421
+ }
422
+
423
+ return {
424
+ "ok": True,
425
+ "status": status,
426
+ "failure": failure,
427
+ "reads": counter.calls,
428
+ "reads_ok": counter.reads_ok,
429
+ "reads_failed": counter.reads_failed,
430
+ "signals": _to_wire(sink.signals, config),
431
+ "suppressed": tracer.report(),
432
+ "suppressed_dropped": tracer.dropped,
433
+ "last_return": tracer.last_return,
434
+ "stdout": _bounded(captured_out.getvalue()),
435
+ "stderr": _bounded(captured_err.getvalue()),
436
+ }
437
+
438
+
439
+ def _to_wire(candidates: List[Any], config: Dict[str, Any]) -> List[Dict[str, Any]]:
440
+ """Build each candidate into the shape intake would actually receive.
441
+
442
+ Uses the runtime's own envelope builder rather than describing what it would do. Judging a
443
+ candidate on its pre-envelope shape would answer a different question from "would this be
444
+ accepted", which is the only question worth asking here.
445
+ """
446
+ produced_at = int(time.time() * 1000)
447
+ out: List[Dict[str, Any]] = []
448
+ for candidate in candidates:
449
+ rejections: List[Dict[str, Any]] = []
450
+ signal = candidate_to_signal(
451
+ candidate,
452
+ produced_at,
453
+ signal_data_schema=config.get("signal_data_schema"),
454
+ default_signal_validity_seconds=int(config["default_signal_validity_seconds"]),
455
+ on_reject=rejections.append,
456
+ )
457
+ out.append(
458
+ {
459
+ "emitted": candidate if isinstance(candidate, dict) else repr(candidate),
460
+ "wire": signal,
461
+ "rejected_by_scaffold": rejections,
462
+ }
463
+ )
464
+ return out
465
+
466
+
467
+ def _finite(value: Any) -> Any:
468
+ """Replace non-finite floats with their text, recursively.
469
+
470
+ A 0/0 in scoring produces NaN, and `json.dumps` writes it as a bare `NaN` token that no strict
471
+ parser accepts — so the tick that surfaced the bug would be reported as the harness breaking.
472
+ Keeping the value as text says what happened instead of losing the document over it.
473
+ """
474
+ if isinstance(value, float):
475
+ return value if math.isfinite(value) else repr(value)
476
+ if isinstance(value, dict):
477
+ return {k: _finite(v) for k, v in value.items()}
478
+ if isinstance(value, (list, tuple)):
479
+ return [_finite(v) for v in value]
480
+ return value
481
+
482
+
483
+ def _serialize(result: Dict[str, Any]) -> str:
484
+ """One document, always parseable.
485
+
486
+ `allow_nan=False` turns a non-finite float into a raised error rather than a token that only
487
+ Python reads back. Anything still unserialisable after sanitising — a circular candidate, an
488
+ exotic object — must not cost the whole report, so the signals are dropped to text and the rest
489
+ survives.
490
+ """
491
+ try:
492
+ return json.dumps(_finite(result), allow_nan=False, default=str)
493
+ except (ValueError, TypeError, RecursionError) as exc:
494
+ salvaged = dict(result)
495
+ salvaged["signals"] = [{"emitted": "<unserialisable>", "wire": None, "rejected_by_scaffold": []}]
496
+ salvaged["serialization_error"] = str(exc)[:300]
497
+ try:
498
+ return json.dumps(_finite(salvaged), allow_nan=False, default=str)
499
+ except Exception: # noqa: BLE001 — a report is worth more than the detail in it
500
+ return json.dumps({"ok": True, "status": result.get("status", "error"),
501
+ "serialization_error": str(exc)[:300]})
502
+
503
+
504
+ def _emit(result: Dict[str, Any], config: Dict[str, Any]) -> None:
505
+ """Deliver the result where author code cannot reach it.
506
+
507
+ The tick runs in this process and shares its stdout. Swapping `sys.stdout` catches `print()`,
508
+ which is what authors do — but `os.write(1, ...)`, a subprocess, a C extension, or a thread
509
+ printing after the restore all write to the same descriptor, and the reader parses the whole
510
+ buffer. That fails closed (never a false pass) but reports a scanner's debug line as a broken
511
+ environment, which points at the wrong person entirely.
512
+
513
+ The caller names a file it owns; writing there makes the guarantee structural rather than
514
+ conventional. Stdout stays available as evidence, and is never parsed.
515
+ """
516
+ document = _serialize(result)
517
+ path = config.get("result_path")
518
+ if path:
519
+ tmp = "{}.tmp".format(path)
520
+ with open(tmp, "w", encoding="utf-8") as handle:
521
+ handle.write(document)
522
+ os.replace(tmp, path) # the reader never sees a half-written document
523
+ return
524
+ # No path given: the caller is reading stdout (older harness, or a direct invocation).
525
+ sys.stdout.write(document)
526
+
527
+
528
+ def main(argv: Optional[List[str]] = None) -> int:
529
+ argv = list(sys.argv if argv is None else argv)
530
+ if len(argv) < 2:
531
+ sys.stderr.write("usage: python3 -m scaffold.preflight <launch-config.json>\n")
532
+ return 2
533
+ try:
534
+ with open(argv[1], "r", encoding="utf-8") as handle:
535
+ config = json.load(handle)
536
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
537
+ sys.stdout.write(json.dumps({"ok": False, "reason": "bad_config", "detail": str(exc)}))
538
+ return 0
539
+
540
+ _emit(run_preflight(config), config)
541
+ return 0
542
+
543
+
544
+ if __name__ == "__main__":
545
+ sys.exit(main())
@@ -88,6 +88,8 @@ def run_scaffold(
88
88
  sink: Optional[Callable] = None,
89
89
  max_ticks: Optional[int] = None,
90
90
  install_signal_handlers: bool = True,
91
+ senpi_mcp: Optional[Any] = None,
92
+ dry_run: bool = False,
91
93
  ) -> int:
92
94
  """Load the author's entrypoint and run its scan() on an interval loop (v2).
93
95
 
@@ -108,7 +110,11 @@ def run_scaffold(
108
110
  Bound on the persisted state series. 0/unset = history disabled
109
111
  (ctx.state.append raises).
110
112
  sink: Callable(candidates) invoked after each clean tick. Default no-op.
111
- max_ticks: Stop after this many ticks (for tests). None = unbounded.
113
+ max_ticks: Stop after this many ticks. None = unbounded. Used by tests and by a
114
+ validation run, which forces exactly one tick.
115
+ senpi_mcp: Data client to hand to scan(). Default: built from the launch environment.
116
+ Injected by a validation run so reads can be observed.
117
+ dry_run: Sets ctx.dry_run. True only during validation.
112
118
  install_signal_handlers:
113
119
  Install SIGTERM/SIGINT graceful-stop handlers. False for tests.
114
120
 
@@ -155,7 +161,9 @@ def run_scaffold(
155
161
  # SENPI_API_KEY / SENPI_MCP_URL). With no creds the slot stays None and launch
156
162
  # must NOT crash — the client is loud at FIRST USE, not at construction. Import
157
163
  # is deferred so a stub patched onto scaffold.senpi_mcp in tests is honored.
158
- senpi_mcp = _build_senpi_mcp()
164
+ # An injected client wins, so a validation run can observe what the tick reads without
165
+ # changing how it reads it. Absent one this behaves exactly as before.
166
+ senpi_mcp = senpi_mcp if senpi_mcp is not None else _build_senpi_mcp()
159
167
 
160
168
  ctx = ScanContext(
161
169
  state=state,
@@ -163,6 +171,7 @@ def run_scaffold(
163
171
  scanner_name=scanner_name,
164
172
  interval_seconds=interval_seconds,
165
173
  senpi_mcp=senpi_mcp,
174
+ dry_run=dry_run,
166
175
  )
167
176
 
168
177
  # ---------- Lifecycle setup ----------
@@ -209,13 +218,17 @@ def run_scaffold(
209
218
  # malformed (a tuple — the old v1 shape — included).
210
219
  if not isinstance(result, list):
211
220
  tick_status = "malformed"
221
+ malformed_reason = (
222
+ f"scan() returned {type(result).__name__!r}; expected a "
223
+ f"plain list[dict] (the v1 (candidates, state) tuple is gone)"
224
+ )
225
+ # Carried on the error record, not only in this log line: the sink is what
226
+ # a caller reads, and "malformed" without the reason is not actionable.
227
+ tick_error = {"type": "malformed", "message": malformed_reason}
212
228
  log_event(
213
229
  "scaffold_tick_malformed",
214
230
  tick=tick_count,
215
- reason=(
216
- f"scan() returned {type(result).__name__!r}; expected a "
217
- f"plain list[dict] (the v1 (candidates, state) tuple is gone)"
218
- ),
231
+ reason=malformed_reason,
219
232
  )
220
233
  else:
221
234
  candidates = result
@@ -41,9 +41,9 @@ export declare function analyzeAssetBias(candles: Candle[], emaFast: number, ema
41
41
  * market intelligence.
42
42
  */
43
43
  export declare function marketRegimeScanner(): import("../index.js").Scanner<{
44
- assets?: string[] | undefined;
45
44
  btcWeight?: number | undefined;
46
45
  ethWeight?: number | undefined;
46
+ assets?: string[] | undefined;
47
47
  emaFast?: number | undefined;
48
48
  emaSlow?: number | undefined;
49
49
  neutralThreshold?: number | undefined;
@@ -177,4 +177,10 @@ export interface DslActivePositionsSource {
177
177
  * collection. Returns a disposer the runtime must call on stop to drop its observation.
178
178
  */
179
179
  export declare function registerDslActivePositionsGauge(source: DslActivePositionsSource): () => void;
180
+ import type { ValidateResult } from "../validate/run.js";
181
+ import type { VerifyReason } from "../validate/proof.js";
182
+ /** One validate run and every finding it produced. Duration in milliseconds. */
183
+ export declare function recordValidateRun(result: ValidateResult, durationMs: number): void;
184
+ /** One proof verification, by outcome alone. */
185
+ export declare function recordProofVerification(outcome: "ok" | VerifyReason): void;
180
186
  //# sourceMappingURL=metrics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/telemetry/metrics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/telemetry/metrics.ts"],"names":[],"mappings":"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"}