@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.
Files changed (94) hide show
  1. package/dist/cli/composer-commands.d.ts +37 -0
  2. package/dist/cli/composer-commands.d.ts.map +1 -0
  3. package/dist/cli/composer-commands.js +287 -0
  4. package/dist/cli/composer-commands.js.map +1 -0
  5. package/dist/cli/senpi-commands.d.ts.map +1 -1
  6. package/dist/cli/senpi-commands.js +2 -0
  7. package/dist/cli/senpi-commands.js.map +1 -1
  8. package/dist/cli/validate-runtime-config.mjs +124 -0
  9. package/dist/composer/AUTHORING.md +233 -0
  10. package/dist/composer/__init__.py +29 -0
  11. package/dist/composer/authoring/__init__.py +49 -0
  12. package/dist/composer/authoring/answers.py +226 -0
  13. package/dist/composer/authoring/archetypes/__init__.py +25 -0
  14. package/dist/composer/authoring/archetypes/_common.py +218 -0
  15. package/dist/composer/authoring/archetypes/classifier.py +109 -0
  16. package/dist/composer/authoring/archetypes/skeleton.py +54 -0
  17. package/dist/composer/authoring/archetypes/thesis_fund.py +103 -0
  18. package/dist/composer/authoring/archetypes/trend_momentum.py +104 -0
  19. package/dist/composer/authoring/builder.py +191 -0
  20. package/dist/composer/authoring/emit.py +264 -0
  21. package/dist/composer/authoring/gaps.py +115 -0
  22. package/dist/composer/authoring/generate.py +131 -0
  23. package/dist/composer/authoring/presets.py +168 -0
  24. package/dist/composer/authoring/runtime_emit.py +266 -0
  25. package/dist/composer/authoring/strategy_manifest.py +94 -0
  26. package/dist/composer/cli.py +355 -0
  27. package/dist/composer/compiler/__init__.py +22 -0
  28. package/dist/composer/compiler/compile.py +550 -0
  29. package/dist/composer/examples/breakout_swing.graph.yaml +121 -0
  30. package/dist/composer/examples/conviction_scorer.graph.yaml +216 -0
  31. package/dist/composer/examples/macro_thesis_basket.graph.yaml +247 -0
  32. package/dist/composer/examples/pre_listing_classifier.graph.yaml +204 -0
  33. package/dist/composer/examples/trend_momentum_majors.graph.yaml +133 -0
  34. package/dist/composer/generated/README.md +22 -0
  35. package/dist/composer/generated/catalog.md +84 -0
  36. package/dist/composer/generated/describe.md +703 -0
  37. package/dist/composer/generated/graph.schema.json +1203 -0
  38. package/dist/composer/primitives/__init__.py +239 -0
  39. package/dist/composer/primitives/_candles.py +76 -0
  40. package/dist/composer/primitives/adapter.py +120 -0
  41. package/dist/composer/primitives/emitters.py +142 -0
  42. package/dist/composer/primitives/filters.py +71 -0
  43. package/dist/composer/primitives/indicators.py +203 -0
  44. package/dist/composer/primitives/rankers.py +40 -0
  45. package/dist/composer/primitives/scorers.py +100 -0
  46. package/dist/composer/primitives/sources.py +405 -0
  47. package/dist/composer/primitives/stateful.py +52 -0
  48. package/dist/composer/pure_fn/__init__.py +34 -0
  49. package/dist/composer/pure_fn/checks.py +39 -0
  50. package/dist/composer/pure_fn/models.py +177 -0
  51. package/dist/composer/pure_fn/purity.py +311 -0
  52. package/dist/composer/pure_fn/runtests.py +155 -0
  53. package/dist/composer/registry/__init__.py +40 -0
  54. package/dist/composer/registry/generate.py +422 -0
  55. package/dist/composer/registry/models.py +331 -0
  56. package/dist/composer/registry/nodes.py +673 -0
  57. package/dist/composer/requirements.txt +21 -0
  58. package/dist/composer/templates/__init__.py +13 -0
  59. package/dist/composer/templates/scan.py.j2 +31 -0
  60. package/dist/composer/validator/__init__.py +29 -0
  61. package/dist/composer/validator/desugar.py +133 -0
  62. package/dist/composer/validator/errors.py +191 -0
  63. package/dist/composer/validator/graph_checks.py +348 -0
  64. package/dist/composer/validator/loader.py +110 -0
  65. package/dist/composer/validator/schema_checks.py +147 -0
  66. package/dist/composer/validator/validate.py +86 -0
  67. package/dist/composer/verbs/__init__.py +33 -0
  68. package/dist/composer/verbs/check.py +193 -0
  69. package/dist/composer/verbs/deploy.py +223 -0
  70. package/dist/composer/verbs/docs.py +30 -0
  71. package/dist/composer/verbs/runtime_validate.py +262 -0
  72. package/dist/composer/verbs/smoke.py +115 -0
  73. package/dist/composer/verbs/status.py +205 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +2 -9
  76. package/dist/index.js.map +1 -1
  77. package/dist/runtime-api/config.d.ts +2 -2
  78. package/dist/runtime-api/config.js +4 -4
  79. package/dist/runtime-api/config.js.map +1 -1
  80. package/dist/runtime-api/handlers/ingest-signals.d.ts +3 -5
  81. package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
  82. package/dist/runtime-api/handlers/ingest-signals.js +5 -14
  83. package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
  84. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  85. package/dist/runtime-api/intake/intake-mount.js +2 -8
  86. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  87. package/dist/skills-manager/manifest.js +1 -1
  88. package/dist/skills-manager/manifest.js.map +1 -1
  89. package/dist/skills-manager/skills-manifest.json +1 -1
  90. package/package.json +4 -2
  91. package/dist/runtime-api/intake/ingest-mutex.d.ts +0 -16
  92. package/dist/runtime-api/intake/ingest-mutex.d.ts.map +0 -1
  93. package/dist/runtime-api/intake/ingest-mutex.js +0 -18
  94. 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
+ )
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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
- // Serialize dispatch behind the shared per-key lock (engine guard
491
- // identity) so a same-scanner batch or overlapping POSTs ingest one
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
  })