@senpi/runtime 3.0.9-dev.20260710204324 → 3.0.9-dev.feat-strategy-compos.20260716013009
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/composer-commands.d.ts +37 -0
- package/dist/cli/composer-commands.d.ts.map +1 -0
- package/dist/cli/composer-commands.js +287 -0
- package/dist/cli/composer-commands.js.map +1 -0
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +2 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/validate-runtime-config.mjs +124 -0
- package/dist/composer/AUTHORING.md +233 -0
- package/dist/composer/__init__.py +29 -0
- package/dist/composer/authoring/__init__.py +49 -0
- package/dist/composer/authoring/answers.py +226 -0
- package/dist/composer/authoring/archetypes/__init__.py +25 -0
- package/dist/composer/authoring/archetypes/_common.py +218 -0
- package/dist/composer/authoring/archetypes/classifier.py +109 -0
- package/dist/composer/authoring/archetypes/skeleton.py +54 -0
- package/dist/composer/authoring/archetypes/thesis_fund.py +103 -0
- package/dist/composer/authoring/archetypes/trend_momentum.py +104 -0
- package/dist/composer/authoring/builder.py +191 -0
- package/dist/composer/authoring/emit.py +264 -0
- package/dist/composer/authoring/gaps.py +115 -0
- package/dist/composer/authoring/generate.py +131 -0
- package/dist/composer/authoring/presets.py +168 -0
- package/dist/composer/authoring/runtime_emit.py +266 -0
- package/dist/composer/authoring/strategy_manifest.py +94 -0
- package/dist/composer/cli.py +355 -0
- package/dist/composer/compiler/__init__.py +22 -0
- package/dist/composer/compiler/compile.py +550 -0
- package/dist/composer/examples/breakout_swing.graph.yaml +121 -0
- package/dist/composer/examples/conviction_scorer.graph.yaml +216 -0
- package/dist/composer/examples/macro_thesis_basket.graph.yaml +247 -0
- package/dist/composer/examples/pre_listing_classifier.graph.yaml +204 -0
- package/dist/composer/examples/trend_momentum_majors.graph.yaml +133 -0
- package/dist/composer/generated/README.md +22 -0
- package/dist/composer/generated/catalog.md +84 -0
- package/dist/composer/generated/describe.md +703 -0
- package/dist/composer/generated/graph.schema.json +1203 -0
- package/dist/composer/primitives/__init__.py +239 -0
- package/dist/composer/primitives/_candles.py +76 -0
- package/dist/composer/primitives/adapter.py +120 -0
- package/dist/composer/primitives/emitters.py +142 -0
- package/dist/composer/primitives/filters.py +71 -0
- package/dist/composer/primitives/indicators.py +203 -0
- package/dist/composer/primitives/rankers.py +40 -0
- package/dist/composer/primitives/scorers.py +100 -0
- package/dist/composer/primitives/sources.py +405 -0
- package/dist/composer/primitives/stateful.py +52 -0
- package/dist/composer/pure_fn/__init__.py +34 -0
- package/dist/composer/pure_fn/checks.py +39 -0
- package/dist/composer/pure_fn/models.py +177 -0
- package/dist/composer/pure_fn/purity.py +311 -0
- package/dist/composer/pure_fn/runtests.py +155 -0
- package/dist/composer/registry/__init__.py +40 -0
- package/dist/composer/registry/generate.py +422 -0
- package/dist/composer/registry/models.py +331 -0
- package/dist/composer/registry/nodes.py +673 -0
- package/dist/composer/requirements.txt +21 -0
- package/dist/composer/templates/__init__.py +13 -0
- package/dist/composer/templates/scan.py.j2 +31 -0
- package/dist/composer/validator/__init__.py +29 -0
- package/dist/composer/validator/desugar.py +133 -0
- package/dist/composer/validator/errors.py +191 -0
- package/dist/composer/validator/graph_checks.py +348 -0
- package/dist/composer/validator/loader.py +110 -0
- package/dist/composer/validator/schema_checks.py +147 -0
- package/dist/composer/validator/validate.py +86 -0
- package/dist/composer/verbs/__init__.py +33 -0
- package/dist/composer/verbs/check.py +193 -0
- package/dist/composer/verbs/deploy.py +223 -0
- package/dist/composer/verbs/docs.py +30 -0
- package/dist/composer/verbs/runtime_validate.py +262 -0
- package/dist/composer/verbs/smoke.py +115 -0
- package/dist/composer/verbs/status.py +205 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -9
- package/dist/index.js.map +1 -1
- package/dist/runtime-api/config.d.ts +2 -2
- package/dist/runtime-api/config.js +4 -4
- package/dist/runtime-api/config.js.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.d.ts +3 -5
- package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
- package/dist/runtime-api/handlers/ingest-signals.js +5 -14
- package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +2 -8
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/skills-manager/manifest.js +1 -1
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -1
- package/package.json +4 -2
- package/dist/runtime-api/intake/ingest-mutex.d.ts +0 -16
- package/dist/runtime-api/intake/ingest-mutex.d.ts.map +0 -1
- package/dist/runtime-api/intake/ingest-mutex.js +0 -18
- package/dist/runtime-api/intake/ingest-mutex.js.map +0 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""runtime_validate — the Zod-oracle stage: judge the emitted runtime.yaml (slice 9 steps 4+5).
|
|
2
|
+
|
|
3
|
+
Two layers, mirroring where the truth lives:
|
|
4
|
+
|
|
5
|
+
1. **Composer POLICY (pure Python, always runs)** — PROTECTION BY DEFAULT. A unit with no
|
|
6
|
+
``exit:`` section and no explicit ``unmanaged`` opt-out is RED (CMP120). This is the
|
|
7
|
+
composer's own rule (the runtime schema legally allows exit-less configs), so it never
|
|
8
|
+
needs node/dist and runs even when the Zod oracle can't.
|
|
9
|
+
|
|
10
|
+
2. **The REAL Zod + DSL oracle (spawned node, needs dist/)** — the emitted runtime.yaml is
|
|
11
|
+
validated by the exact schema + DSL parser the runtime boots with, via the rim entry
|
|
12
|
+
``src/cli/validate-runtime-config.mjs``. Its structured ``{path, message, code}`` issues
|
|
13
|
+
are mapped to the CMP1xx teaching-error class by the table below. When node or dist is
|
|
14
|
+
absent (CI runs pytest without ``npm run build``), the oracle is SKIPPED with an actionable
|
|
15
|
+
reason — NOT RED — so the policy layer still gates and the caller/tests can skip cleanly.
|
|
16
|
+
|
|
17
|
+
The Python↔node spawn mirrors ``src/cli/composer-commands.ts`` (COMPOSER_NODE bin, dist
|
|
18
|
+
resolution, ``detectEnvError`` actionable one-liner); see design §5.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from composer.validator.errors import Location, TeachingError
|
|
32
|
+
|
|
33
|
+
# repo root = the dir CONTAINING the `composer/` package (…/composer/verbs/ -> parents[2]).
|
|
34
|
+
_ROOT = Path(__file__).resolve().parents[2]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _dist_dir() -> Path:
|
|
38
|
+
override = os.environ.get("COMPOSER_RUNTIME_DIST")
|
|
39
|
+
if override:
|
|
40
|
+
return Path(override)
|
|
41
|
+
dev = _ROOT / "dist"
|
|
42
|
+
if (dev / "runtime" / "runtime-schema.js").is_file():
|
|
43
|
+
return dev
|
|
44
|
+
# Shipped package: the composer lives INSIDE the runtime dist (dist/composer/), so
|
|
45
|
+
# _ROOT *is* the dist dir. runtime/runtime-schema.js under _ROOT is unambiguous — a
|
|
46
|
+
# dev-checkout root keeps it only under dist/ — so both layouts resolve with no env.
|
|
47
|
+
if (_ROOT / "runtime" / "runtime-schema.js").is_file():
|
|
48
|
+
return _ROOT
|
|
49
|
+
return dev
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _rim_entry() -> Optional[Path]:
|
|
53
|
+
# Candidates track _dist_dir() so COMPOSER_RUNTIME_DIST resolves the rim AND the schema
|
|
54
|
+
# from the same place end-to-end. On a dist-only claw box the composer ships to
|
|
55
|
+
# dist/composer/, so _ROOT is the dist dir itself and the rim is _dist_dir()/cli/… —
|
|
56
|
+
# NOT _ROOT/dist/cli/…; without this the Zod oracle silently skips even with the env set.
|
|
57
|
+
for cand in (
|
|
58
|
+
_ROOT / "src" / "cli" / "validate-runtime-config.mjs", # dev checkout (source)
|
|
59
|
+
_dist_dir() / "cli" / "validate-runtime-config.mjs", # built/shipped (honors env)
|
|
60
|
+
):
|
|
61
|
+
if cand.is_file():
|
|
62
|
+
return cand
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _node_bin() -> str:
|
|
67
|
+
return os.environ.get("COMPOSER_NODE", "node")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def oracle_available() -> bool:
|
|
71
|
+
"""True when the real Zod+DSL oracle can run (dist built, rim entry present, node on PATH)."""
|
|
72
|
+
return (
|
|
73
|
+
(_dist_dir() / "runtime" / "runtime-schema.js").is_file()
|
|
74
|
+
and _rim_entry() is not None
|
|
75
|
+
and shutil.which(_node_bin()) is not None
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class RuntimeValidateResult:
|
|
81
|
+
ok: bool
|
|
82
|
+
errors: list[TeachingError]
|
|
83
|
+
skipped: bool = False # the Zod oracle couldn't run (node/dist absent) — not RED
|
|
84
|
+
skip_reason: Optional[str] = None
|
|
85
|
+
oracle_ran: bool = False # the node oracle actually executed
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------- CMP1xx mapper
|
|
89
|
+
|
|
90
|
+
def _map_oracle_issue(path: str, message: str, code: str) -> TeachingError:
|
|
91
|
+
"""Table-driven map of one raw Zod/DSL issue -> a CMP1xx teaching error (design §8).
|
|
92
|
+
|
|
93
|
+
Keyed so a NEW Zod message can never produce an unlabelled error: unrecognised issues
|
|
94
|
+
fall to CMP199 (still located + hinted), never to a bare pass-through."""
|
|
95
|
+
p = path or ""
|
|
96
|
+
loc = Location(path=p)
|
|
97
|
+
|
|
98
|
+
# F1: misplaced top-level dsl_preset (schema superRefine at runtime-schema.ts:378-395).
|
|
99
|
+
if code == "custom" and p == "dsl_preset":
|
|
100
|
+
return TeachingError(
|
|
101
|
+
"CMP111", "`dsl_preset` is at the top level; the runtime reads only `exit.dsl_preset`.",
|
|
102
|
+
"Nest the preset under `exit:` — `exit:\\n dsl_preset:\\n ...`. The composer "
|
|
103
|
+
"emits this shape automatically; do not hand-place `dsl_preset`.",
|
|
104
|
+
loc,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# risk block is .strict() — unknown / renamed-legacy key.
|
|
108
|
+
if code in ("unrecognized_keys",) or (p.startswith("risk") and "renamed" in message):
|
|
109
|
+
return TeachingError(
|
|
110
|
+
"CMP101", f"the `risk` block has an unknown/renamed key: {message}",
|
|
111
|
+
"Use only the v2 keys; `data_retention_hours` -> `data_retention_seconds`, "
|
|
112
|
+
"`cooldown_minutes` -> `cooldown_seconds`. Let the composer emit the risk block.",
|
|
113
|
+
loc,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# DSL preset parse errors (uppercase DSL codes) — breaches, tier order, preset config.
|
|
117
|
+
if code.isupper() and code not in ("", None):
|
|
118
|
+
return TeachingError(
|
|
119
|
+
"CMP110", f"the exit DSL preset is invalid: {message} ({code}).",
|
|
120
|
+
"Use a named preset (protected_standard | protected_tight | protected_wide); its "
|
|
121
|
+
"phase1/phase2 always parse. If you edited the tiers, keep trigger_pct ascending "
|
|
122
|
+
"and consecutive_breaches_required an integer >= 1.",
|
|
123
|
+
loc,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Missing required top-level field.
|
|
127
|
+
if p in ("name", "strategy", "strategy.wallet") and code in ("invalid_type", "too_small", "invalid_string"):
|
|
128
|
+
return TeachingError(
|
|
129
|
+
"CMP100", f"missing/invalid required runtime field `{p}`: {message}",
|
|
130
|
+
"`name` (non-empty) and `strategy.wallet` (a `${...}` env placeholder) are "
|
|
131
|
+
"mandatory. The composer binds both from the strategy id.",
|
|
132
|
+
loc,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# guard_rails range issue.
|
|
136
|
+
if p.startswith("risk.guard_rails") and code in ("too_small", "too_big"):
|
|
137
|
+
return TeachingError(
|
|
138
|
+
"CMP102", f"a `risk.guard_rails` value is out of range: {message}",
|
|
139
|
+
"Respect the field bounds (e.g. drawdown_halt_pct 0–100, cooldown_seconds >= 60). "
|
|
140
|
+
"The composer's presets stay in-bounds; a risk knob override must too.",
|
|
141
|
+
loc,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Anything else the schema rejected — still labelled + located.
|
|
145
|
+
return TeachingError(
|
|
146
|
+
"CMP199", f"runtime.yaml failed schema validation at `{p or '(root)'}`: {message} [{code}].",
|
|
147
|
+
"Regenerate the runtime.yaml with `composer deploy` (it emits a Zod-valid unit); if "
|
|
148
|
+
"you hand-edited it, revert the offending field to the composer's shape.",
|
|
149
|
+
loc,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _run_node_oracle(runtime_yaml_path: Path) -> RuntimeValidateResult:
|
|
154
|
+
"""Spawn the node Zod oracle over a runtime.yaml file. Returns SKIPPED (not RED) when the
|
|
155
|
+
node/dist environment is absent (mirrors composer-commands.ts detectEnvError)."""
|
|
156
|
+
dist = _dist_dir()
|
|
157
|
+
schema_js = dist / "runtime" / "runtime-schema.js"
|
|
158
|
+
rim = _rim_entry()
|
|
159
|
+
if not schema_js.is_file():
|
|
160
|
+
return RuntimeValidateResult(
|
|
161
|
+
ok=True, errors=[], skipped=True,
|
|
162
|
+
skip_reason=(
|
|
163
|
+
f"Zod oracle skipped: runtime dist not built ({schema_js} absent). "
|
|
164
|
+
f"Run `npm run build`, or set COMPOSER_RUNTIME_DIST. Protection policy still enforced."
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
if rim is None:
|
|
168
|
+
return RuntimeValidateResult(
|
|
169
|
+
ok=True, errors=[], skipped=True,
|
|
170
|
+
skip_reason="Zod oracle skipped: rim entry validate-runtime-config.mjs not found under src/cli or dist/cli.",
|
|
171
|
+
)
|
|
172
|
+
node = _node_bin()
|
|
173
|
+
if shutil.which(node) is None:
|
|
174
|
+
return RuntimeValidateResult(
|
|
175
|
+
ok=True, errors=[], skipped=True,
|
|
176
|
+
skip_reason=f"Zod oracle skipped: '{node}' not on PATH (set COMPOSER_NODE). Protection policy still enforced.",
|
|
177
|
+
)
|
|
178
|
+
try:
|
|
179
|
+
proc = subprocess.run(
|
|
180
|
+
[node, str(rim), str(runtime_yaml_path), str(dist)],
|
|
181
|
+
capture_output=True, text=True, timeout=60,
|
|
182
|
+
)
|
|
183
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
184
|
+
return RuntimeValidateResult(
|
|
185
|
+
ok=True, errors=[], skipped=True,
|
|
186
|
+
skip_reason=f"Zod oracle skipped: node spawn failed ({exc}).",
|
|
187
|
+
)
|
|
188
|
+
try:
|
|
189
|
+
verdict = json.loads(proc.stdout.strip().splitlines()[-1]) if proc.stdout.strip() else {}
|
|
190
|
+
except (json.JSONDecodeError, IndexError):
|
|
191
|
+
return RuntimeValidateResult(
|
|
192
|
+
ok=True, errors=[], skipped=True,
|
|
193
|
+
skip_reason=f"Zod oracle skipped: unparseable output (stderr: {proc.stderr.strip()[:200]}).",
|
|
194
|
+
)
|
|
195
|
+
if verdict.get("env_absent"):
|
|
196
|
+
return RuntimeValidateResult(
|
|
197
|
+
ok=True, errors=[], skipped=True,
|
|
198
|
+
skip_reason=f"Zod oracle skipped: {verdict.get('message', 'runtime dist not importable')}.",
|
|
199
|
+
)
|
|
200
|
+
if verdict.get("ok"):
|
|
201
|
+
return RuntimeValidateResult(ok=True, errors=[], oracle_ran=True)
|
|
202
|
+
errors = [
|
|
203
|
+
_map_oracle_issue(e.get("path", ""), e.get("message", ""), e.get("code", ""))
|
|
204
|
+
for e in verdict.get("errors", [])
|
|
205
|
+
]
|
|
206
|
+
return RuntimeValidateResult(ok=not errors, errors=errors, oracle_ran=True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def validate_runtime_yaml_file(path) -> RuntimeValidateResult:
|
|
210
|
+
"""Run ONLY the Zod oracle over an existing runtime.yaml file (deploy uses this to prove
|
|
211
|
+
the staged unit validates). The policy layer lives in ``check_runtime_unit``."""
|
|
212
|
+
return _run_node_oracle(Path(path))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _protection_policy_error(answers) -> Optional[TeachingError]:
|
|
216
|
+
"""CMP120: PROTECTION BY DEFAULT. A unit with no exit and no explicit opt-out is RED.
|
|
217
|
+
|
|
218
|
+
The composer always resolves a preset, so an emitted unit is protected unless the author
|
|
219
|
+
deliberately set ``unmanaged``. This gate exists so that opt-out is a conscious, logged
|
|
220
|
+
choice — never an accident of an absent field."""
|
|
221
|
+
if getattr(answers, "unmanaged", False):
|
|
222
|
+
return None
|
|
223
|
+
if not getattr(answers, "exit_preset", None):
|
|
224
|
+
return TeachingError(
|
|
225
|
+
"CMP120",
|
|
226
|
+
"this strategy would run UNMANAGED — no exit protection — but you did not opt out.",
|
|
227
|
+
"Set an exit_preset (protected_standard | protected_tight | protected_wide) to "
|
|
228
|
+
"protect open positions, OR pass `unmanaged=true` to deliberately opt out (a loud "
|
|
229
|
+
"warning is emitted). Protection is the default; opting out must be explicit.",
|
|
230
|
+
Location(path="exit"),
|
|
231
|
+
)
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def check_runtime_unit(answers) -> tuple[RuntimeValidateResult, list[TeachingError]]:
|
|
236
|
+
"""The full ``check`` runtime_validate stage: PROTECTION policy (CMP120, always) + the
|
|
237
|
+
real Zod+DSL oracle over the emitted runtime.yaml. Returns ``(oracle_result, policy_errors)``.
|
|
238
|
+
|
|
239
|
+
Emits the runtime.yaml to a temp file (the artifact the oracle judges is exactly what
|
|
240
|
+
deploy stages). When ``unmanaged`` is set, a warning is surfaced by the caller, not an error.
|
|
241
|
+
"""
|
|
242
|
+
import tempfile
|
|
243
|
+
|
|
244
|
+
from composer.authoring.runtime_emit import render_runtime_yaml
|
|
245
|
+
|
|
246
|
+
policy_errors: list[TeachingError] = []
|
|
247
|
+
err = _protection_policy_error(answers)
|
|
248
|
+
if err is not None:
|
|
249
|
+
policy_errors.append(err)
|
|
250
|
+
|
|
251
|
+
text = render_runtime_yaml(answers)
|
|
252
|
+
with tempfile.NamedTemporaryFile("w", suffix=".runtime.yaml", delete=False, encoding="utf-8") as fh:
|
|
253
|
+
fh.write(text)
|
|
254
|
+
tmp = fh.name
|
|
255
|
+
try:
|
|
256
|
+
oracle = _run_node_oracle(Path(tmp))
|
|
257
|
+
finally:
|
|
258
|
+
try:
|
|
259
|
+
os.unlink(tmp)
|
|
260
|
+
except OSError:
|
|
261
|
+
pass
|
|
262
|
+
return oracle, policy_errors
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""smoke — the fixture-smoke stage of ``composer check`` (promoted from the pytest fixtures).
|
|
2
|
+
|
|
3
|
+
``composer check`` must prove the emitted ``scan.py`` doesn't merely compile but IMPORTS and
|
|
4
|
+
RUNS: a graph can typecheck yet still blow up on the first tick (a bad primitive call, a
|
|
5
|
+
port wired to the wrong shape). The slice-3 golden tests already exercised this through
|
|
6
|
+
``python-tests/composer/fixtures/composed.py`` (a real scaffold ``ScanContext`` +
|
|
7
|
+
``StateManager`` + a fake MCP), but that machinery lived under the test tree. This module
|
|
8
|
+
promotes the MINIMAL slice of it into ``composer/`` proper so ``check`` runs the same smoke
|
|
9
|
+
OUTSIDE pytest:
|
|
10
|
+
|
|
11
|
+
* import the emitted module (exec the compiled source as a live module),
|
|
12
|
+
* build a real frozen ``ScanContext`` over a temp ``StateManager`` and a benign stub MCP
|
|
13
|
+
(``StubMcp`` — every corpus read returns ``None`` / empty, which every source
|
|
14
|
+
normalizer tolerates by dropping the asset; the tick therefore runs clean and returns
|
|
15
|
+
``[]``),
|
|
16
|
+
* call ``scan(inputs, ctx)`` ONCE and assert it returns a list.
|
|
17
|
+
|
|
18
|
+
The point is liveness, not correctness (correctness is the golden tests): a green smoke
|
|
19
|
+
means "this artifact loads and one tick executes end-to-end without raising". ``scaffold``
|
|
20
|
+
is made importable the same way the pytest run does it (pyproject ``pythonpath`` =
|
|
21
|
+
``src/scanners/external``) by putting that dir on ``sys.path`` here, so the verb works when
|
|
22
|
+
invoked as a plain CLI with no pytest/conftest in the picture.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import importlib.util
|
|
28
|
+
import sys
|
|
29
|
+
import tempfile
|
|
30
|
+
import uuid
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
# Make `scaffold` importable outside pytest (mirrors pyproject `pythonpath`). composer/
|
|
34
|
+
# lives at the repo root, so the scaffold package is repo_root/src/scanners/external.
|
|
35
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
36
|
+
_SCAFFOLD_PARENT = _REPO_ROOT / "src" / "scanners" / "external"
|
|
37
|
+
if _SCAFFOLD_PARENT.is_dir() and str(_SCAFFOLD_PARENT) not in sys.path:
|
|
38
|
+
sys.path.insert(0, str(_SCAFFOLD_PARENT))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StubMcp:
|
|
42
|
+
"""A benign ``ctx.senpi_mcp``: every corpus read returns ``None``.
|
|
43
|
+
|
|
44
|
+
Source normalizers (``composer.primitives.sources``) all treat a falsy/None response as
|
|
45
|
+
"drop this asset", and the adapter swallows read errors — so a tick over this stub runs
|
|
46
|
+
to completion and emits nothing. That is exactly the liveness signal smoke wants: the
|
|
47
|
+
emitted code's control flow executes over empty-but-valid data without raising.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self.calls: list = []
|
|
52
|
+
|
|
53
|
+
def call_tool(self, name: str, args: dict):
|
|
54
|
+
self.calls.append((name, args))
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SmokeError(Exception):
|
|
59
|
+
"""The emitted module failed to import or its first ``scan()`` tick raised/misbehaved."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _import_scan(source: str, module_name: str):
|
|
63
|
+
"""Exec the compiled ``scan.py`` source as a live module; return its ``scan`` callable."""
|
|
64
|
+
with tempfile.TemporaryDirectory(prefix="composer-smoke-") as tmp:
|
|
65
|
+
path = Path(tmp) / f"{module_name}.py"
|
|
66
|
+
path.write_text(source, encoding="utf-8")
|
|
67
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
68
|
+
module = importlib.util.module_from_spec(spec)
|
|
69
|
+
# Register so a traceback inside scan() can resolve the module frame.
|
|
70
|
+
sys.modules[module_name] = module
|
|
71
|
+
try:
|
|
72
|
+
spec.loader.exec_module(module)
|
|
73
|
+
except Exception as exc: # noqa: BLE001 — surfaced as a teaching-shaped smoke failure
|
|
74
|
+
raise SmokeError(f"emitted module failed to import: {exc!r}") from exc
|
|
75
|
+
finally:
|
|
76
|
+
sys.modules.pop(module_name, None)
|
|
77
|
+
scan = getattr(module, "scan", None)
|
|
78
|
+
if not callable(scan):
|
|
79
|
+
raise SmokeError("emitted module has no callable `scan(inputs, ctx)`.")
|
|
80
|
+
return scan
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def run_smoke(scan_source: str, *, scanner_name: str = "composer-smoke") -> dict:
|
|
84
|
+
"""Import the emitted source and run one ``scan()`` tick over a stub ctx.
|
|
85
|
+
|
|
86
|
+
Returns a small detail dict (``{signals, mcp_calls}``) on success; raises
|
|
87
|
+
:class:`SmokeError` if import fails, the tick raises, or the return is not a list.
|
|
88
|
+
"""
|
|
89
|
+
from scaffold.ctx import ScanContext
|
|
90
|
+
from scaffold.state_manager import StateManager
|
|
91
|
+
|
|
92
|
+
module_name = f"composer_smoke_{uuid.uuid4().hex}"
|
|
93
|
+
scan = _import_scan(scan_source, module_name)
|
|
94
|
+
|
|
95
|
+
with tempfile.TemporaryDirectory(prefix="composer-smoke-state-") as tmp:
|
|
96
|
+
state = StateManager(str(Path(tmp) / "state.json"), state_history_max_count=50)
|
|
97
|
+
mcp = StubMcp()
|
|
98
|
+
ctx = ScanContext(
|
|
99
|
+
state=state,
|
|
100
|
+
wallet="0xSMOKE",
|
|
101
|
+
scanner_name=scanner_name,
|
|
102
|
+
interval_seconds=3600,
|
|
103
|
+
senpi_mcp=mcp,
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
result = scan({}, ctx)
|
|
107
|
+
except Exception as exc: # noqa: BLE001
|
|
108
|
+
raise SmokeError(f"first scan() tick raised {exc!r}.") from exc
|
|
109
|
+
|
|
110
|
+
if not isinstance(result, list):
|
|
111
|
+
raise SmokeError(
|
|
112
|
+
f"scan() returned {type(result).__name__}, expected list[dict] (the "
|
|
113
|
+
f"external_scanner contract)."
|
|
114
|
+
)
|
|
115
|
+
return {"signals": len(result), "mcp_calls": len(mcp.calls)}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""status — DERIVED lifecycle position + the next verb to run (D7: state is never journaled).
|
|
2
|
+
|
|
3
|
+
The spec file IS the state (D7). ``composer status`` writes nothing and reads no journal; it
|
|
4
|
+
DERIVES where a graph sits in the ``new → check → deploy`` lifecycle from four observable
|
|
5
|
+
facts and then NAMES THE NEXT VERB:
|
|
6
|
+
|
|
7
|
+
1. does the graph file exist? (else -> new)
|
|
8
|
+
2. its no-clobber provenance (the content-hash marker `composer new` stamps):
|
|
9
|
+
generated-unedited / hand-edited-since-generated / hand-authored.
|
|
10
|
+
3. the check verdict — RUN it (GREEN/RED). (RED -> fix + check)
|
|
11
|
+
4. staged/deployed artifacts, if an ``--out`` staging dir is given: is there a manifest,
|
|
12
|
+
and does its recorded graph sha match the current file (deployed-current) or not
|
|
13
|
+
(deployed-stale = edited since last deploy)?
|
|
14
|
+
|
|
15
|
+
Positions and their next verb:
|
|
16
|
+
|
|
17
|
+
absent file missing -> composer new
|
|
18
|
+
invalid check RED -> fix errors, then composer check
|
|
19
|
+
valid_undeployed check GREEN, no staged manifest -> composer deploy
|
|
20
|
+
deployed_stale staged, but graph edited since -> composer check, then composer deploy
|
|
21
|
+
deployed_current staged, graph matches, GREEN -> (deployed; observe the tick — slice 8)
|
|
22
|
+
|
|
23
|
+
You may pass a graph file (``status foo.graph.yaml [--out staged/]``) or a staging directory
|
|
24
|
+
(``status staged/foo/`` — it reads the graph copy + manifest inside).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Optional
|
|
34
|
+
|
|
35
|
+
from composer.authoring import emit
|
|
36
|
+
|
|
37
|
+
from .check import run_check
|
|
38
|
+
|
|
39
|
+
ABSENT = "absent"
|
|
40
|
+
INVALID = "invalid"
|
|
41
|
+
VALID_UNDEPLOYED = "valid_undeployed"
|
|
42
|
+
DEPLOYED_STALE = "deployed_stale"
|
|
43
|
+
DEPLOYED_CURRENT = "deployed_current"
|
|
44
|
+
|
|
45
|
+
_NEXT = {
|
|
46
|
+
ABSENT: ("composer new", "no graph here — run `composer new <id> --archetype ...` to generate one."),
|
|
47
|
+
INVALID: ("composer check", "graph has errors — fix them (see `composer check`), then re-check."),
|
|
48
|
+
VALID_UNDEPLOYED: ("composer deploy", "graph is GREEN and not yet deployed — run `composer deploy`."),
|
|
49
|
+
DEPLOYED_STALE: ("composer check", "graph edited since last deploy — run `composer check`, then `composer deploy`."),
|
|
50
|
+
DEPLOYED_CURRENT: ("(none)", "deployed and current — observe the live tick (slice 8); nothing to do."),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sha256(text: str) -> str:
|
|
55
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _provenance(text: str) -> str:
|
|
59
|
+
state = emit.is_unedited_generated(text)
|
|
60
|
+
if state is True:
|
|
61
|
+
return "generated_unedited"
|
|
62
|
+
if state is False:
|
|
63
|
+
return "edited"
|
|
64
|
+
return "authored"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class StatusResult:
|
|
69
|
+
target: str
|
|
70
|
+
position: str
|
|
71
|
+
exists: bool = False
|
|
72
|
+
provenance: Optional[str] = None
|
|
73
|
+
content_hash: Optional[str] = None
|
|
74
|
+
check_verdict: Optional[str] = None
|
|
75
|
+
check_error_count: int = 0
|
|
76
|
+
staged: bool = False
|
|
77
|
+
staged_dir: Optional[str] = None
|
|
78
|
+
staged_current: Optional[bool] = None
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def next_verb(self) -> str:
|
|
82
|
+
return _NEXT[self.position][0]
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def next_hint(self) -> str:
|
|
86
|
+
return _NEXT[self.position][1]
|
|
87
|
+
|
|
88
|
+
def to_json(self) -> dict:
|
|
89
|
+
return {
|
|
90
|
+
"verb": "status",
|
|
91
|
+
"target": self.target,
|
|
92
|
+
"exists": self.exists,
|
|
93
|
+
"position": self.position,
|
|
94
|
+
"provenance": self.provenance,
|
|
95
|
+
"content_hash": self.content_hash,
|
|
96
|
+
"check": {"verdict": self.check_verdict, "error_count": self.check_error_count},
|
|
97
|
+
"deploy": {
|
|
98
|
+
"staged": self.staged,
|
|
99
|
+
"dir": self.staged_dir,
|
|
100
|
+
"current": self.staged_current,
|
|
101
|
+
},
|
|
102
|
+
"next_verb": self.next_verb,
|
|
103
|
+
"next": self.next_hint,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def render(self) -> str:
|
|
107
|
+
lines = [f"status: {self.target}", f" position : {self.position}"]
|
|
108
|
+
if self.exists:
|
|
109
|
+
lines.append(f" provenance : {self.provenance}")
|
|
110
|
+
lines.append(f" check : {self.check_verdict} ({self.check_error_count} error(s))")
|
|
111
|
+
deployed = "yes" if self.staged else "no"
|
|
112
|
+
if self.staged:
|
|
113
|
+
deployed += " (current)" if self.staged_current else " (STALE — edited since)"
|
|
114
|
+
lines.append(f" deployed : {deployed}")
|
|
115
|
+
lines.append(f" next : {self.next_hint}")
|
|
116
|
+
return "\n".join(lines) + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _resolve(target, out_dir) -> tuple[str, Optional[str], Optional[Path]]:
|
|
120
|
+
"""Resolve the target into (graph_text|None, label, staged_dir|None).
|
|
121
|
+
|
|
122
|
+
A directory containing a manifest.json is treated as a staging dir (read its graph
|
|
123
|
+
copy). Otherwise the target is a graph file and ``out_dir/<strategy>`` (if given) is the
|
|
124
|
+
staging dir to inspect.
|
|
125
|
+
"""
|
|
126
|
+
p = Path(target)
|
|
127
|
+
if p.is_dir():
|
|
128
|
+
manifest = p / "manifest.json"
|
|
129
|
+
if manifest.is_file():
|
|
130
|
+
man = json.loads(manifest.read_text(encoding="utf-8"))
|
|
131
|
+
graph_name = f"{man.get('strategy', p.name)}.graph.yaml"
|
|
132
|
+
# slice 9 package tree: the graph copy lives under the instance dir (main/); fall
|
|
133
|
+
# back to the flat legacy location for older stagings.
|
|
134
|
+
gpath = next(
|
|
135
|
+
(c for c in (p / "main" / graph_name, p / graph_name) if c.is_file()),
|
|
136
|
+
p / "main" / graph_name,
|
|
137
|
+
)
|
|
138
|
+
text = gpath.read_text(encoding="utf-8") if gpath.is_file() else None
|
|
139
|
+
return text, str(gpath if gpath.is_file() else p), p
|
|
140
|
+
return None, str(p), None
|
|
141
|
+
if not p.is_file():
|
|
142
|
+
return None, str(p), None
|
|
143
|
+
text = p.read_text(encoding="utf-8")
|
|
144
|
+
staged = None
|
|
145
|
+
if out_dir is not None:
|
|
146
|
+
# derive the strategy name for the staging subdir
|
|
147
|
+
try:
|
|
148
|
+
from composer.validator.loader import load_graph
|
|
149
|
+
doc, _ = load_graph(text)
|
|
150
|
+
strategy = str((doc.get("metadata") or {}).get("strategy")
|
|
151
|
+
or p.stem.replace(".graph", ""))
|
|
152
|
+
except Exception: # noqa: BLE001
|
|
153
|
+
strategy = p.stem.replace(".graph", "")
|
|
154
|
+
candidate = Path(out_dir) / strategy
|
|
155
|
+
if (candidate / "manifest.json").is_file():
|
|
156
|
+
staged = candidate
|
|
157
|
+
return text, str(p), staged
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_status(target, out_dir=None) -> StatusResult:
|
|
161
|
+
text, label, staged_dir = _resolve(target, out_dir)
|
|
162
|
+
|
|
163
|
+
if text is None:
|
|
164
|
+
return StatusResult(target=label, position=ABSENT, exists=False)
|
|
165
|
+
|
|
166
|
+
content_hash = _sha256(text)
|
|
167
|
+
provenance = _provenance(text)
|
|
168
|
+
check = run_check(text, Path(label).name)
|
|
169
|
+
err_count = check.to_json()["error_count"]
|
|
170
|
+
|
|
171
|
+
# deploy facts
|
|
172
|
+
staged = False
|
|
173
|
+
staged_current: Optional[bool] = None
|
|
174
|
+
staged_dir_str: Optional[str] = None
|
|
175
|
+
if staged_dir is not None:
|
|
176
|
+
manifest_path = staged_dir / "manifest.json"
|
|
177
|
+
try:
|
|
178
|
+
man = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
179
|
+
except (OSError, json.JSONDecodeError):
|
|
180
|
+
man = {}
|
|
181
|
+
staged = True
|
|
182
|
+
staged_dir_str = str(staged_dir)
|
|
183
|
+
staged_current = man.get("graph_sha256") == content_hash
|
|
184
|
+
|
|
185
|
+
if not check.ok:
|
|
186
|
+
position = INVALID
|
|
187
|
+
elif not staged:
|
|
188
|
+
position = VALID_UNDEPLOYED
|
|
189
|
+
elif staged_current:
|
|
190
|
+
position = DEPLOYED_CURRENT
|
|
191
|
+
else:
|
|
192
|
+
position = DEPLOYED_STALE
|
|
193
|
+
|
|
194
|
+
return StatusResult(
|
|
195
|
+
target=label,
|
|
196
|
+
position=position,
|
|
197
|
+
exists=True,
|
|
198
|
+
provenance=provenance,
|
|
199
|
+
content_hash=content_hash,
|
|
200
|
+
check_verdict=check.verdict,
|
|
201
|
+
check_error_count=err_count,
|
|
202
|
+
staged=staged,
|
|
203
|
+
staged_dir=staged_dir_str,
|
|
204
|
+
staged_current=staged_current,
|
|
205
|
+
)
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmCH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,6 @@ import { startApi } from "./runtime-api/lifecycle.js";
|
|
|
49
49
|
import { ingestSignalsHandler } from "./runtime-api/handlers/ingest-signals.js";
|
|
50
50
|
import { createIntakeContext } from "./runtime-api/intake/intake-context.js";
|
|
51
51
|
import { wireRuntime, intakeUrlFor } from "./runtime-api/intake/intake-mount.js";
|
|
52
|
-
import { ingestDispatchMutex, ingestDispatchKey } from "./runtime-api/intake/ingest-mutex.js";
|
|
53
52
|
import { loadRuntimeConfig } from "./runtime/load-runtime-config.js";
|
|
54
53
|
/** Initialize all core features (DSL, risk, health) on the given event bus. */
|
|
55
54
|
export function initCore(bus) {
|
|
@@ -487,14 +486,8 @@ const senpiRuntimePlugin = {
|
|
|
487
486
|
// Trace the sizing-relevant fields the narrowed request carries, so a
|
|
488
487
|
// field dropped at this seam is visible instead of vanishing silently.
|
|
489
488
|
apiLogger.debug(`[intake] ingest req signal_id=${signal.signal_id} scanner=${resolved.scannerName} asset=${signal.asset} direction=${signal.direction} marginPct=${signal.marginPct} leverage=${signal.leverage}`);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
// at a time instead of the engine rejecting all but the first as
|
|
493
|
-
// UNAVAILABLE. Still void — the POST response never blocks on the
|
|
494
|
-
// trade, just on lock ordering.
|
|
495
|
-
const key = ingestDispatchKey(resolved.runtimeId, resolved.scannerName);
|
|
496
|
-
void ingestDispatchMutex
|
|
497
|
-
.withKeyLock(key, () => h.ingestExternalScannerData(request))
|
|
489
|
+
void h
|
|
490
|
+
.ingestExternalScannerData(request)
|
|
498
491
|
.then(() => {
|
|
499
492
|
apiLogger.info(`[intake] ingest ok signal_id=${signal.signal_id} scanner=${resolved.scannerName} asset=${signal.asset} scanner_id=${scannerId}`);
|
|
500
493
|
})
|