@senpi/runtime 3.0.9-dev.20260710204324 → 3.0.9-dev.feat-strategy-compos.20260716233548

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 (108) 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 +396 -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 +190 -0
  9. package/dist/composer/AUTHORING.md +435 -0
  10. package/dist/composer/__init__.py +29 -0
  11. package/dist/composer/authoring/__init__.py +59 -0
  12. package/dist/composer/authoring/answers.py +297 -0
  13. package/dist/composer/authoring/archetypes/__init__.py +26 -0
  14. package/dist/composer/authoring/archetypes/_common.py +410 -0
  15. package/dist/composer/authoring/archetypes/breakout.py +134 -0
  16. package/dist/composer/authoring/archetypes/classifier.py +109 -0
  17. package/dist/composer/authoring/archetypes/skeleton.py +76 -0
  18. package/dist/composer/authoring/archetypes/thesis_fund.py +103 -0
  19. package/dist/composer/authoring/archetypes/trend_momentum.py +132 -0
  20. package/dist/composer/authoring/builder.py +191 -0
  21. package/dist/composer/authoring/emit.py +264 -0
  22. package/dist/composer/authoring/gaps.py +115 -0
  23. package/dist/composer/authoring/generate.py +131 -0
  24. package/dist/composer/authoring/presets.py +168 -0
  25. package/dist/composer/authoring/runtime_emit.py +266 -0
  26. package/dist/composer/authoring/strategy_manifest.py +94 -0
  27. package/dist/composer/cli.py +487 -0
  28. package/dist/composer/compiler/__init__.py +22 -0
  29. package/dist/composer/compiler/compile.py +739 -0
  30. package/dist/composer/examples/breakout_swing.graph.yaml +121 -0
  31. package/dist/composer/examples/conviction_scorer.graph.yaml +216 -0
  32. package/dist/composer/examples/macro_thesis_basket.graph.yaml +247 -0
  33. package/dist/composer/examples/pre_listing_classifier.graph.yaml +204 -0
  34. package/dist/composer/examples/trend_momentum_majors.graph.yaml +133 -0
  35. package/dist/composer/generated/README.md +22 -0
  36. package/dist/composer/generated/catalog.md +118 -0
  37. package/dist/composer/generated/describe.md +1168 -0
  38. package/dist/composer/generated/graph.schema.json +1946 -0
  39. package/dist/composer/primitives/__init__.py +320 -0
  40. package/dist/composer/primitives/_candles.py +76 -0
  41. package/dist/composer/primitives/adapter.py +363 -0
  42. package/dist/composer/primitives/emitters.py +142 -0
  43. package/dist/composer/primitives/filters.py +71 -0
  44. package/dist/composer/primitives/indicators.py +326 -0
  45. package/dist/composer/primitives/rankers.py +40 -0
  46. package/dist/composer/primitives/scorers.py +100 -0
  47. package/dist/composer/primitives/sources.py +979 -0
  48. package/dist/composer/primitives/stateful.py +254 -0
  49. package/dist/composer/pure_fn/__init__.py +34 -0
  50. package/dist/composer/pure_fn/checks.py +39 -0
  51. package/dist/composer/pure_fn/models.py +177 -0
  52. package/dist/composer/pure_fn/purity.py +311 -0
  53. package/dist/composer/pure_fn/runtests.py +155 -0
  54. package/dist/composer/registry/__init__.py +40 -0
  55. package/dist/composer/registry/generate.py +425 -0
  56. package/dist/composer/registry/mcp-tools-snapshot.json +193 -0
  57. package/dist/composer/registry/mcp_closure.py +311 -0
  58. package/dist/composer/registry/models.py +417 -0
  59. package/dist/composer/registry/nodes.py +1297 -0
  60. package/dist/composer/requirements.txt +21 -0
  61. package/dist/composer/scaffold_path.py +118 -0
  62. package/dist/composer/templates/__init__.py +13 -0
  63. package/dist/composer/templates/scan.py.j2 +34 -0
  64. package/dist/composer/validator/__init__.py +29 -0
  65. package/dist/composer/validator/desugar.py +133 -0
  66. package/dist/composer/validator/errors.py +227 -0
  67. package/dist/composer/validator/graph_checks.py +443 -0
  68. package/dist/composer/validator/loader.py +110 -0
  69. package/dist/composer/validator/schema_checks.py +147 -0
  70. package/dist/composer/validator/validate.py +86 -0
  71. package/dist/composer/vendoring.py +103 -0
  72. package/dist/composer/verbs/__init__.py +49 -0
  73. package/dist/composer/verbs/_box.py +308 -0
  74. package/dist/composer/verbs/check.py +254 -0
  75. package/dist/composer/verbs/close.py +494 -0
  76. package/dist/composer/verbs/deploy.py +279 -0
  77. package/dist/composer/verbs/docs.py +30 -0
  78. package/dist/composer/verbs/fund.py +498 -0
  79. package/dist/composer/verbs/install.py +586 -0
  80. package/dist/composer/verbs/runtime_validate.py +422 -0
  81. package/dist/composer/verbs/smoke.py +134 -0
  82. package/dist/composer/verbs/status.py +383 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +2 -9
  85. package/dist/index.js.map +1 -1
  86. package/dist/runtime/load-runtime-config.d.ts +17 -0
  87. package/dist/runtime/load-runtime-config.d.ts.map +1 -1
  88. package/dist/runtime/load-runtime-config.js +2 -2
  89. package/dist/runtime/load-runtime-config.js.map +1 -1
  90. package/dist/runtime-api/config.d.ts +2 -2
  91. package/dist/runtime-api/config.js +4 -4
  92. package/dist/runtime-api/config.js.map +1 -1
  93. package/dist/runtime-api/handlers/ingest-signals.d.ts +3 -5
  94. package/dist/runtime-api/handlers/ingest-signals.d.ts.map +1 -1
  95. package/dist/runtime-api/handlers/ingest-signals.js +5 -14
  96. package/dist/runtime-api/handlers/ingest-signals.js.map +1 -1
  97. package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
  98. package/dist/runtime-api/intake/intake-mount.js +2 -8
  99. package/dist/runtime-api/intake/intake-mount.js.map +1 -1
  100. package/dist/skills-manager/manifest.d.ts.map +1 -1
  101. package/dist/skills-manager/manifest.js +1 -3
  102. package/dist/skills-manager/manifest.js.map +1 -1
  103. package/dist/skills-manager/skills-manifest.json +1 -3
  104. package/package.json +4 -2
  105. package/dist/runtime-api/intake/ingest-mutex.d.ts +0 -16
  106. package/dist/runtime-api/intake/ingest-mutex.d.ts.map +0 -1
  107. package/dist/runtime-api/intake/ingest-mutex.js +0 -18
  108. package/dist/runtime-api/intake/ingest-mutex.js.map +0 -1
@@ -0,0 +1,147 @@
1
+ """schema_checks — stage (c): JSON Schema validation, translated to teaching errors.
2
+
3
+ The registry-generated ``composer/generated/graph.schema.json`` already carries the FULL
4
+ per-node parameter validation (types, bounds, choices, required, unknown-key rejection)
5
+ via its ``allOf`` if/then branches. This module does NOT re-implement any of that — it
6
+ runs the schema with ``jsonschema`` and translates each finding into a located, fix-hinted
7
+ :class:`TeachingError`, so schema violations read the same way as the graph-semantic
8
+ checks layered on top of them.
9
+
10
+ Two deliberate translations:
11
+ * The ``enum`` failure on a node's ``node:`` field (an unknown node type) is SKIPPED
12
+ here — the graph checker emits a far better CMP030 that names the nearest valid
13
+ alternatives. Reporting both would be noise.
14
+ * ``additionalProperties`` inside a ``with:`` block is turned into a per-parameter
15
+ "unknown parameter" error that names the offending key and points at `composer
16
+ describe <node>`.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from pathlib import Path
23
+
24
+ from jsonschema import Draft202012Validator
25
+
26
+ from .errors import Location, TeachingError
27
+
28
+ _SCHEMA_PATH = (
29
+ Path(__file__).resolve().parent.parent / "generated" / "graph.schema.json"
30
+ )
31
+
32
+
33
+ def load_schema() -> dict:
34
+ return json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
35
+
36
+
37
+ def _node_id_at(doc: dict, path) -> tuple[str | None, int | None, dict]:
38
+ """Given a jsonschema error path, resolve (node_id, none, node_index) context."""
39
+ path = list(path)
40
+ if len(path) >= 2 and path[0] == "nodes" and isinstance(path[1], int):
41
+ idx = path[1]
42
+ try:
43
+ entry = doc["nodes"][idx]
44
+ except (KeyError, IndexError, TypeError):
45
+ return None, None, {}
46
+ if isinstance(entry, dict):
47
+ return entry.get("id"), idx, entry
48
+ return None, None, {}
49
+
50
+
51
+ def schema_errors(doc: dict, line_index: dict) -> list[TeachingError]:
52
+ schema = load_schema()
53
+ validator = Draft202012Validator(schema)
54
+ out: list[TeachingError] = []
55
+
56
+ for err in validator.iter_errors(doc):
57
+ path = list(err.absolute_path)
58
+ node_id, _idx, _entry = _node_id_at(doc, path)
59
+ line = line_index.get("nodes", {}).get(node_id)
60
+ loc = Location(node=node_id, line=line)
61
+
62
+ # Unknown node type: let the graph checker produce the good message.
63
+ if err.validator == "enum" and path and path[-1] == "node":
64
+ continue
65
+
66
+ if err.validator == "additionalProperties" and path and path[-1] == "with":
67
+ allowed = set((err.schema or {}).get("properties", {}))
68
+ extra = sorted(k for k in (err.instance or {}) if k not in allowed)
69
+ names = ", ".join(f"`{k}`" for k in extra) or "a parameter"
70
+ node_name = _entry.get("node") if isinstance(_entry, dict) else None
71
+ hint_node = node_name or (node_id or "<node>")
72
+ out.append(
73
+ TeachingError(
74
+ code="CMP010",
75
+ message=f"unknown parameter(s) {names} in `with:`.",
76
+ fix_hint=f"Remove {names}; run `composer describe {hint_node}` to see valid params.",
77
+ location=loc,
78
+ )
79
+ )
80
+ continue
81
+
82
+ if err.validator in ("minimum", "maximum"):
83
+ param = path[-1] if path else None
84
+ loc = Location(node=node_id, port=None, line=line)
85
+ bound = err.validator
86
+ limit = err.validator_value
87
+ out.append(
88
+ TeachingError(
89
+ code="CMP011",
90
+ message=f"parameter `{param}` = {err.instance!r} violates {bound} {limit}.",
91
+ fix_hint=f"Set `{param}` to a value with {bound} {limit}.",
92
+ location=loc,
93
+ )
94
+ )
95
+ continue
96
+
97
+ if err.validator == "type":
98
+ param = path[-1] if path and path[-1] != "with" else None
99
+ want = err.validator_value
100
+ desc = f"parameter `{param}`" if param else "value"
101
+ out.append(
102
+ TeachingError(
103
+ code="CMP012",
104
+ message=f"{desc} = {err.instance!r} must be of type {want}.",
105
+ fix_hint=(
106
+ f"Change `{param}` to a {want} value."
107
+ if param
108
+ else f"Provide a {want} here."
109
+ ),
110
+ location=loc,
111
+ )
112
+ )
113
+ continue
114
+
115
+ if err.validator == "required":
116
+ # e.message like "'x' is a required property"
117
+ missing = err.message.split("'")[1] if "'" in err.message else "a field"
118
+ if node_id or (path and path[0] == "nodes"):
119
+ out.append(
120
+ TeachingError(
121
+ code="CMP013",
122
+ message=f"required parameter `{missing}` is missing.",
123
+ fix_hint=f"Add `{missing}` under this node's `with:` block.",
124
+ location=loc,
125
+ )
126
+ )
127
+ else:
128
+ out.append(
129
+ TeachingError(
130
+ code="CMP013",
131
+ message=f"required top-level field `{missing}` is missing.",
132
+ fix_hint=f"Add a top-level `{missing}:` key to the document.",
133
+ location=Location(),
134
+ )
135
+ )
136
+ continue
137
+
138
+ # Fallback: any other schema keyword.
139
+ out.append(
140
+ TeachingError(
141
+ code="CMP014",
142
+ message=err.message,
143
+ fix_hint="Adjust the document to satisfy the graph schema.",
144
+ location=loc,
145
+ )
146
+ )
147
+ return out
@@ -0,0 +1,86 @@
1
+ """validate — the pipeline that runs the stages and collects every error together.
2
+
3
+ Stage order (each stage's findings are added to the same :class:`ValidationResult`;
4
+ stages do not stop at the first error — the whole point is to report every defect at
5
+ once so the agent repairs in one pass):
6
+
7
+ (a) load + anchor/alias/merge rejection -> loader
8
+ (b) `pipeline:` desugaring into nodes+edges -> desugar (before schema)
9
+ (c) JSON Schema validation (registry-generated) -> schema_checks
10
+ (d) graph-semantic checks -> graph_checks
11
+ (e) pure_fn checks (purity + inline tests) -> composer.pure_fn.checks
12
+
13
+ A hard load failure (a) is terminal — there is no document to check further. Everything
14
+ after that accumulates.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+
21
+ from composer.pure_fn import checks as pure_fn_checks
22
+
23
+ from . import desugar as _desugar
24
+ from . import graph_checks, schema_checks
25
+ from .errors import Location, TeachingError, ValidationResult
26
+ from .loader import AnchorError, YamlLoadError, load_graph
27
+
28
+
29
+ def validate_text(text: str, source: str = "<text>") -> ValidationResult:
30
+ result = ValidationResult(source=source)
31
+
32
+ # (a) load + anchor rejection
33
+ try:
34
+ data, line_index = load_graph(text)
35
+ except AnchorError as exc:
36
+ result.add(
37
+ TeachingError(
38
+ code="CMP002",
39
+ message=f"YAML {exc.what} is not allowed in a strategy graph.",
40
+ fix_hint="Remove the anchor/alias/merge key and write the value out in full (D6 bans them).",
41
+ location=Location(line=exc.line),
42
+ )
43
+ )
44
+ return result
45
+ except YamlLoadError as exc:
46
+ result.add(
47
+ TeachingError(
48
+ code="CMP001",
49
+ message=f"YAML syntax error: {exc.message}",
50
+ fix_hint="Fix the YAML syntax at the reported line (check indentation and quoting).",
51
+ location=Location(line=exc.line),
52
+ )
53
+ )
54
+ return result
55
+
56
+ if not isinstance(data, dict):
57
+ result.add(
58
+ TeachingError(
59
+ code="CMP003",
60
+ message="the document root is empty or not a mapping.",
61
+ fix_hint="A graph is a mapping with at least a `nodes:` list (or a `pipeline:` shorthand).",
62
+ )
63
+ )
64
+ return result
65
+
66
+ # (b) desugar pipeline -> nodes+edges (before schema)
67
+ if _desugar.has_pipeline(data):
68
+ data, desugar_errors = _desugar.desugar(data)
69
+ result.extend(desugar_errors)
70
+
71
+ # (c) JSON Schema validation
72
+ result.extend(schema_checks.schema_errors(data, line_index))
73
+
74
+ # (d) graph-semantic checks
75
+ result.extend(graph_checks.check(data, line_index))
76
+
77
+ # (e) pure_fn purity + inline fixture tests (D2 kind 2)
78
+ result.extend(pure_fn_checks.check(data, line_index))
79
+
80
+ return result
81
+
82
+
83
+ def validate_file(path: str | Path) -> ValidationResult:
84
+ p = Path(path)
85
+ text = p.read_text(encoding="utf-8")
86
+ return validate_text(text, source=str(p))
@@ -0,0 +1,103 @@
1
+ """Vendor ``composer.primitives`` into an emitted strategy package as ``strategy_primitives``.
2
+
3
+ The composer compiles a graph into ``scan.py`` + ``runtime.yaml``. On a user's box the
4
+ runtime scaffold puts the SCANNER DIRECTORY on ``sys.path`` and imports ``scan.py`` by module
5
+ name (``src/scanners/external/scaffold/entrypoint_loader.py``) — NOTHING from the composer
6
+ checkout exists there. So the emitted ``scan.py`` must never ``import composer.primitives``;
7
+ it imports ``strategy_primitives``, a verbatim, point-in-time SNAPSHOT of
8
+ ``composer/primitives/`` staged as a SIBLING package of ``scan.py`` (same scanner dir → the
9
+ scaffold's ``sys.path`` entry resolves it exactly like ``scan`` itself).
10
+
11
+ This module is the SINGLE place that copy happens, so:
12
+
13
+ * ``composer deploy`` stages a self-contained tree
14
+ (``main/scanners/{scan.py, strategy_primitives/}``);
15
+ * the in-checkout harnesses that EXEC emitted ``scan.py`` (``composer.verbs.smoke``,
16
+ ``composer.eval.replay``, the golden-test fixtures) stage the SAME snapshot beside the
17
+ temp ``scan.py`` and put its dir on ``sys.path`` — the on-box layout — so they exercise
18
+ the real vendored package rather than the in-checkout ``composer.primitives``;
19
+ * the deploy manifest hashes every vendored file DETERMINISTICALLY (sorted relpaths).
20
+
21
+ Snapshot semantics are the point: the copy freezes the primitives at deploy/compile time.
22
+ Evolving ``composer/primitives/`` later never mutates an already-staged strategy.
23
+
24
+ All primitives-internal imports are RELATIVE (``from .`` / ``from ._candles``) and no
25
+ primitive references ``composer`` in code, so a verbatim copy under a new package name
26
+ imports cleanly — there are no absolute ``composer.`` imports to rewrite. The
27
+ self-containment test (``python-tests/composer/test_vendored_primitives.py``) guards this by
28
+ importing a STAGED ``scan.py`` in a subprocess where the ``composer`` checkout is NOT
29
+ importable.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import contextlib
35
+ import hashlib
36
+ import sys
37
+ from pathlib import Path
38
+ from typing import Iterator
39
+
40
+ # The package name the emitted scan.py imports (the vendored sibling of scan.py).
41
+ VENDORED_PACKAGE = "strategy_primitives"
42
+
43
+ _PRIMITIVES_DIR = Path(__file__).resolve().parent / "primitives"
44
+
45
+
46
+ def _snapshot_files() -> list[Path]:
47
+ """Every ``.py`` under ``composer/primitives/``, deterministically sorted, ``__pycache__``
48
+ excluded. Stable ordering is what makes the deploy manifest reproducible."""
49
+ return sorted(
50
+ p
51
+ for p in _PRIMITIVES_DIR.rglob("*.py")
52
+ if "__pycache__" not in p.relative_to(_PRIMITIVES_DIR).parts
53
+ )
54
+
55
+
56
+ def vendor_primitives(dest_dir: str | Path) -> dict[str, str]:
57
+ """Copy ``composer/primitives/`` VERBATIM into ``<dest_dir>/strategy_primitives/``.
58
+
59
+ Returns an ordered ``{posix_relpath: sha256}`` map over every file written — relpaths are
60
+ ``strategy_primitives/<file>`` — for the deploy manifest. Iteration is over
61
+ :func:`_snapshot_files` (sorted), so the returned dict is deterministic. Idempotent:
62
+ re-running overwrites the snapshot with identical bytes.
63
+ """
64
+ dest = Path(dest_dir)
65
+ pkg_dir = dest / VENDORED_PACKAGE
66
+ hashes: dict[str, str] = {}
67
+ for src in _snapshot_files():
68
+ rel = src.relative_to(_PRIMITIVES_DIR)
69
+ target = pkg_dir / rel
70
+ target.parent.mkdir(parents=True, exist_ok=True)
71
+ data = src.read_bytes()
72
+ target.write_bytes(data)
73
+ hashes[f"{VENDORED_PACKAGE}/{rel.as_posix()}"] = hashlib.sha256(data).hexdigest()
74
+ return hashes
75
+
76
+
77
+ @contextlib.contextmanager
78
+ def emitted_scan_on_syspath(
79
+ source: str, dest_dir: str | Path, *, module_filename: str = "scan.py"
80
+ ) -> Iterator[Path]:
81
+ """Stage emitted ``scan.py`` + a vendored ``strategy_primitives`` snapshot into ``dest_dir``
82
+ and put ``dest_dir`` on ``sys.path`` for the duration — reproducing the on-box scanner-dir
83
+ layout so ``import strategy_primitives`` resolves exactly as it will in production.
84
+
85
+ Yields the written ``scan.py`` path. Used by the in-checkout harnesses that EXEC emitted
86
+ output; the ``sys.path`` entry is inserted idempotently and removed on exit (the snapshot
87
+ is byte-identical every time, and the primitives package stays cached in ``sys.modules``
88
+ after the first import, so later execs resolve it from cache).
89
+ """
90
+ dest = Path(dest_dir)
91
+ dest.mkdir(parents=True, exist_ok=True)
92
+ vendor_primitives(dest)
93
+ scan_path = dest / module_filename
94
+ scan_path.write_text(source, encoding="utf-8")
95
+ added = str(dest) not in sys.path
96
+ if added:
97
+ sys.path.insert(0, str(dest))
98
+ try:
99
+ yield scan_path
100
+ finally:
101
+ if added:
102
+ with contextlib.suppress(ValueError):
103
+ sys.path.remove(str(dest))
@@ -0,0 +1,49 @@
1
+ """verbs — the slice-7 composer verbs (check / deploy / status / catalog / describe).
2
+
3
+ Each verb is a thin, pure-ish Python surface producing a structured result with both a
4
+ human ``render()`` and a ``to_json()`` (the shape the TS rim passes through). The lifecycle
5
+ they implement — ``new → check → deploy``, with ``status`` deriving position and naming the
6
+ next verb — is D7; the TS wrapper is verdict-passthrough only (D8).
7
+
8
+ check — ONE verdict aggregating validate + pure_fn tests + compile + fixture smoke.
9
+ deploy — idempotent, HARD-gated on check; stages the deployable artifact set.
10
+ install — copy a staged unit to a canonical hash dir, bind the wallet, hand to the
11
+ frozen `openclaw senpi runtime create` consumer (slice A2).
12
+ close — safe teardown: stop the runtime → confirm gone → trigger MCP strategy_close (B1).
13
+ status — DERIVED lifecycle position + next verb (no journals; the spec file is state),
14
+ extended down the install chain (landed → registered → running) when a box is named.
15
+ docs — catalog index + per-node describe (registry-generated).
16
+ """
17
+
18
+ from .check import CheckResult, check_file, run_check
19
+ from .close import CloseResult, run_close
20
+ from .deploy import DeployResult, deploy_file, run_deploy
21
+ from .docs import catalog_text, describe_text
22
+ from .fund import FundResult, fund_target, run_fund
23
+ from .install import CreateResult, InstallResult, install_target, run_install
24
+ from .smoke import SmokeError, run_smoke
25
+ from .status import StatusResult, run_status
26
+
27
+ __all__ = [
28
+ "CheckResult",
29
+ "run_check",
30
+ "check_file",
31
+ "DeployResult",
32
+ "run_deploy",
33
+ "deploy_file",
34
+ "InstallResult",
35
+ "CreateResult",
36
+ "run_install",
37
+ "install_target",
38
+ "FundResult",
39
+ "run_fund",
40
+ "fund_target",
41
+ "CloseResult",
42
+ "run_close",
43
+ "StatusResult",
44
+ "run_status",
45
+ "catalog_text",
46
+ "describe_text",
47
+ "SmokeError",
48
+ "run_smoke",
49
+ ]