@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,355 @@
|
|
|
1
|
+
"""cli — the ``composer`` verbs; the Python core's command surface (D7, D8).
|
|
2
|
+
|
|
3
|
+
The composer is driven through a small set of verbs, all deriving their behaviour from the
|
|
4
|
+
registry so the agent never reasons from stale docs:
|
|
5
|
+
|
|
6
|
+
composer new <id> --archetype X ... — interview answers in, a COMPLETE, VALID graph
|
|
7
|
+
out, with mechanically bindable values bound and bespoke slots TODO-marked; a
|
|
8
|
+
feasibility gap report when answers reference unavailable registry names; refuses
|
|
9
|
+
to overwrite an edited graph without ``--force``.
|
|
10
|
+
composer check — validate + port typecheck + fn-node unit tests + compile +
|
|
11
|
+
fixture smoke, collapsed into ONE verdict.
|
|
12
|
+
composer deploy — idempotent, hard-gated on ``check``, targets the existing
|
|
13
|
+
external_scanner deploy path.
|
|
14
|
+
composer status — position DERIVED from artifact + validation + platform state
|
|
15
|
+
(the spec file IS the state; no journals), naming the next verb.
|
|
16
|
+
composer catalog / composer describe <node> — the registry-generated progressive
|
|
17
|
+
disclosure surface (index on demand, full ports/params/example per node).
|
|
18
|
+
|
|
19
|
+
This module is the Python entry point; TypeScript appears only as a thin CLI/verdict
|
|
20
|
+
wrapper (``--json``, deploy gate) in the existing ``@senpi-ai/runtime`` plugin
|
|
21
|
+
(``src/cli/composer-commands.ts``, D8). All verbs are wired as of slice 7: ``new`` and
|
|
22
|
+
``validate`` (slices 2/6), ``compile`` (slice 3), and the slice-7 lifecycle verbs
|
|
23
|
+
``check`` / ``deploy`` / ``status`` plus the ``catalog`` / ``describe`` disclosure surface
|
|
24
|
+
(implemented in ``composer.verbs``). Every verb takes ``--json`` for the TS rim.
|
|
25
|
+
|
|
26
|
+
composer validate <file> [--json] — teaching-error report; exit 0 when clean.
|
|
27
|
+
composer check <file> [--json] — ONE verdict (validate+tests+compile+smoke); 0 iff GREEN.
|
|
28
|
+
composer deploy <file> -o <dir> — stage the deployable artifact set; gated on check.
|
|
29
|
+
composer status <target> [-o <dir>] — derived lifecycle position + the next verb.
|
|
30
|
+
composer catalog / describe <node> — the registry-generated progressive-disclosure surface.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import argparse
|
|
36
|
+
import json
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from composer.authoring import compose_new, load_answers, load_answers_file
|
|
41
|
+
from composer.compiler import CompileError, compile_file
|
|
42
|
+
from composer.validator import validate_file
|
|
43
|
+
from composer.verbs import (
|
|
44
|
+
catalog_text,
|
|
45
|
+
check_file,
|
|
46
|
+
deploy_file,
|
|
47
|
+
describe_text,
|
|
48
|
+
run_status,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _split_csv(value: str) -> list[str]:
|
|
53
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _new_overrides(args: argparse.Namespace) -> dict:
|
|
57
|
+
"""Assemble the CLI-flag override dict (only flags the user actually passed).
|
|
58
|
+
|
|
59
|
+
Flags override the answers-file field-by-field (D7 "individual flag overrides"). Nested
|
|
60
|
+
fields (universe.*, risk.*) are placed under their sub-mapping so the deep-merge in
|
|
61
|
+
``load_answers`` composes them with the file rather than replacing whole sub-objects.
|
|
62
|
+
"""
|
|
63
|
+
over: dict = {"archetype": args.archetype, "strategy": args.id}
|
|
64
|
+
universe: dict = {}
|
|
65
|
+
risk: dict = {}
|
|
66
|
+
if args.whitelist is not None:
|
|
67
|
+
universe["whitelist"] = _split_csv(args.whitelist)
|
|
68
|
+
if args.long_basket is not None:
|
|
69
|
+
universe["long_basket"] = _split_csv(args.long_basket)
|
|
70
|
+
if args.short_basket is not None:
|
|
71
|
+
universe["short_basket"] = _split_csv(args.short_basket)
|
|
72
|
+
if args.discover:
|
|
73
|
+
universe["discover"] = True
|
|
74
|
+
if args.dex is not None:
|
|
75
|
+
universe["dex"] = args.dex
|
|
76
|
+
if args.name_prefix is not None:
|
|
77
|
+
universe["name_prefix"] = args.name_prefix
|
|
78
|
+
if args.max_leverage_cap is not None:
|
|
79
|
+
universe["max_leverage_cap"] = args.max_leverage_cap
|
|
80
|
+
if args.min_day_volume is not None:
|
|
81
|
+
universe["min_day_volume"] = args.min_day_volume
|
|
82
|
+
if universe:
|
|
83
|
+
over["universe"] = universe
|
|
84
|
+
if args.intervals is not None:
|
|
85
|
+
over["intervals"] = _split_csv(args.intervals)
|
|
86
|
+
if args.max_positions is not None:
|
|
87
|
+
over["max_positions"] = args.max_positions
|
|
88
|
+
if args.ttl_seconds is not None:
|
|
89
|
+
over["ttl_seconds"] = args.ttl_seconds
|
|
90
|
+
if args.exit_preset is not None:
|
|
91
|
+
over["exit_preset"] = args.exit_preset
|
|
92
|
+
if args.unmanaged:
|
|
93
|
+
over["unmanaged"] = True
|
|
94
|
+
if args.margin_pct is not None:
|
|
95
|
+
risk["margin_pct"] = args.margin_pct
|
|
96
|
+
if args.leverage is not None:
|
|
97
|
+
risk["leverage"] = args.leverage
|
|
98
|
+
if args.drawdown_halt_pct is not None:
|
|
99
|
+
risk["drawdown_halt_pct"] = args.drawdown_halt_pct
|
|
100
|
+
if args.max_consecutive_losses is not None:
|
|
101
|
+
risk["max_consecutive_losses"] = args.max_consecutive_losses
|
|
102
|
+
if args.cooldown_seconds is not None:
|
|
103
|
+
risk["cooldown_seconds"] = args.cooldown_seconds
|
|
104
|
+
if args.max_entries_per_day is not None:
|
|
105
|
+
risk["max_entries_per_day"] = args.max_entries_per_day
|
|
106
|
+
if risk:
|
|
107
|
+
over["risk"] = risk
|
|
108
|
+
if args.edge is not None:
|
|
109
|
+
over["edge"] = args.edge
|
|
110
|
+
return over
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _cmd_new(args: argparse.Namespace) -> int:
|
|
114
|
+
file_data = load_answers_file(args.answers) if args.answers else None
|
|
115
|
+
try:
|
|
116
|
+
answers = load_answers(file_data, _new_overrides(args))
|
|
117
|
+
except Exception as exc: # pydantic ValidationError etc. — a bad answer, not a crash
|
|
118
|
+
if args.json:
|
|
119
|
+
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
|
120
|
+
else:
|
|
121
|
+
print(f"invalid answers: {exc}", file=sys.stderr)
|
|
122
|
+
return 1
|
|
123
|
+
|
|
124
|
+
out_dir = Path(args.output) if args.output else Path.cwd()
|
|
125
|
+
out_path = out_dir / f"{args.id}.graph.yaml"
|
|
126
|
+
result = compose_new(answers, out_path, force=args.force)
|
|
127
|
+
|
|
128
|
+
if args.json:
|
|
129
|
+
print(json.dumps(result.to_json(), indent=2, sort_keys=True))
|
|
130
|
+
|
|
131
|
+
if result.gap_report is not None:
|
|
132
|
+
if not args.json:
|
|
133
|
+
print(result.gap_report.render(), end="")
|
|
134
|
+
return 2
|
|
135
|
+
if result.clobber_refused:
|
|
136
|
+
if not args.json:
|
|
137
|
+
print(result.message, file=sys.stderr)
|
|
138
|
+
return 3
|
|
139
|
+
if not result.ok:
|
|
140
|
+
if not args.json:
|
|
141
|
+
print(result.message, file=sys.stderr)
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
out_path.write_text(result.yaml_text, encoding="utf-8")
|
|
146
|
+
if not args.json:
|
|
147
|
+
print(f"wrote {out_path} ({result.message})")
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _cmd_validate(args: argparse.Namespace) -> int:
|
|
152
|
+
result = validate_file(args.file)
|
|
153
|
+
if args.json:
|
|
154
|
+
print(json.dumps(result.to_json(), indent=2, sort_keys=True))
|
|
155
|
+
else:
|
|
156
|
+
print(result.render(), end="")
|
|
157
|
+
return 0 if result.ok else 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _cmd_compile(args: argparse.Namespace) -> int:
|
|
161
|
+
try:
|
|
162
|
+
source = compile_file(args.file)
|
|
163
|
+
except CompileError as exc:
|
|
164
|
+
print(str(exc), file=sys.stderr)
|
|
165
|
+
return 1
|
|
166
|
+
if args.output:
|
|
167
|
+
with open(args.output, "w", encoding="utf-8") as fh:
|
|
168
|
+
fh.write(source)
|
|
169
|
+
print(f"wrote {args.output}")
|
|
170
|
+
else:
|
|
171
|
+
print(source, end="")
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _cmd_check(args: argparse.Namespace) -> int:
|
|
176
|
+
result = check_file(args.file)
|
|
177
|
+
if args.json:
|
|
178
|
+
print(json.dumps(result.to_json(), indent=2, sort_keys=True))
|
|
179
|
+
else:
|
|
180
|
+
print(result.render(), end="")
|
|
181
|
+
return 0 if result.ok else 1
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _cmd_deploy(args: argparse.Namespace) -> int:
|
|
185
|
+
result = deploy_file(args.file, args.out)
|
|
186
|
+
if args.json:
|
|
187
|
+
print(json.dumps(result.to_json(), indent=2, sort_keys=True))
|
|
188
|
+
else:
|
|
189
|
+
print(result.render(), end="")
|
|
190
|
+
return 0 if result.ok else 1
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _cmd_status(args: argparse.Namespace) -> int:
|
|
194
|
+
result = run_status(args.target, out_dir=args.out)
|
|
195
|
+
if args.json:
|
|
196
|
+
print(json.dumps(result.to_json(), indent=2, sort_keys=True))
|
|
197
|
+
else:
|
|
198
|
+
print(result.render(), end="")
|
|
199
|
+
return 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _cmd_catalog(args: argparse.Namespace) -> int:
|
|
203
|
+
if args.json:
|
|
204
|
+
print(json.dumps({"catalog": catalog_text()}, indent=2, sort_keys=True))
|
|
205
|
+
else:
|
|
206
|
+
print(catalog_text(), end="")
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _cmd_describe(args: argparse.Namespace) -> int:
|
|
211
|
+
try:
|
|
212
|
+
text = describe_text(args.node)
|
|
213
|
+
except KeyError as exc:
|
|
214
|
+
msg = str(exc).strip('"')
|
|
215
|
+
if args.json:
|
|
216
|
+
print(json.dumps({"ok": False, "error": msg}, indent=2))
|
|
217
|
+
else:
|
|
218
|
+
print(msg, file=sys.stderr)
|
|
219
|
+
return 1
|
|
220
|
+
if args.json:
|
|
221
|
+
print(json.dumps({"ok": True, "node": args.node, "describe": text}, indent=2, sort_keys=True))
|
|
222
|
+
else:
|
|
223
|
+
print(text, end="")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
228
|
+
parser = argparse.ArgumentParser(
|
|
229
|
+
prog="composer",
|
|
230
|
+
description="Strategy Composer — build, validate, and deploy trading strategy graphs.",
|
|
231
|
+
)
|
|
232
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
233
|
+
|
|
234
|
+
p_new = sub.add_parser(
|
|
235
|
+
"new",
|
|
236
|
+
help="Generate a COMPLETE, VALID strategy graph from interview answers (D7).",
|
|
237
|
+
)
|
|
238
|
+
p_new.add_argument("id", help="Strategy id; the graph is written to <id>.graph.yaml.")
|
|
239
|
+
p_new.add_argument("--archetype", required=True,
|
|
240
|
+
help="Archetype template (trend_momentum | classifier | thesis_fund); "
|
|
241
|
+
"an unknown one falls back to a minimal valid skeleton.")
|
|
242
|
+
p_new.add_argument("--answers", help="Path to a YAML/JSON answers-file (flags override it).")
|
|
243
|
+
p_new.add_argument("-o", "--output", help="Output directory (default: cwd).")
|
|
244
|
+
p_new.add_argument("--force", action="store_true",
|
|
245
|
+
help="Overwrite an edited graph (default: refuse to clobber edits).")
|
|
246
|
+
p_new.add_argument("--json", action="store_true",
|
|
247
|
+
help="Emit the structured result (path / gap report) as JSON.")
|
|
248
|
+
# individual flag overrides (D7): each overrides the answers-file field-by-field.
|
|
249
|
+
p_new.add_argument("--whitelist", help="Comma-separated fixed universe (trend_momentum).")
|
|
250
|
+
p_new.add_argument("--long-basket", dest="long_basket", help="Comma-separated LONG leg (thesis_fund).")
|
|
251
|
+
p_new.add_argument("--short-basket", dest="short_basket", help="Comma-separated SHORT leg (thesis_fund).")
|
|
252
|
+
p_new.add_argument("--discover", action="store_true", help="Derive the universe from the live board (classifier).")
|
|
253
|
+
p_new.add_argument("--dex", help="Venue filter for discovery (classifier).")
|
|
254
|
+
p_new.add_argument("--name-prefix", dest="name_prefix", help="Instrument name prefix for discovery (classifier).")
|
|
255
|
+
p_new.add_argument("--max-leverage-cap", dest="max_leverage_cap", type=float, help="Discovery leverage cap (classifier).")
|
|
256
|
+
p_new.add_argument("--min-day-volume", dest="min_day_volume", type=float, help="Discovery daily-volume floor (classifier).")
|
|
257
|
+
p_new.add_argument("--intervals", help="Comma-separated candle intervals (e.g. 1h,4h).")
|
|
258
|
+
p_new.add_argument("--max-positions", dest="max_positions", type=int, help="Cardinality: 1 = single best, >1 = pool-cap.")
|
|
259
|
+
p_new.add_argument("--ttl-seconds", dest="ttl_seconds", type=int, help="Signal-dedup TTL window (memory).")
|
|
260
|
+
p_new.add_argument("--exit-preset", dest="exit_preset",
|
|
261
|
+
help="Exit protection preset (protected_standard | protected_tight | protected_wide); "
|
|
262
|
+
"default protected_standard is bound if unset (protection-by-default).")
|
|
263
|
+
p_new.add_argument("--unmanaged", action="store_true",
|
|
264
|
+
help="Deliberately opt OUT of exit protection (no exit section, loud warning). "
|
|
265
|
+
"Without this, an exit-less unit is RED at check (CMP120).")
|
|
266
|
+
p_new.add_argument("--margin-pct", dest="margin_pct", type=float, help="Percent of withdrawable to allocate, (0,100].")
|
|
267
|
+
p_new.add_argument("--leverage", type=float, help="Requested leverage.")
|
|
268
|
+
p_new.add_argument("--drawdown-halt-pct", dest="drawdown_halt_pct", type=float,
|
|
269
|
+
help="Guard rail: halt the whole strategy at this equity drawdown %% (0-100).")
|
|
270
|
+
p_new.add_argument("--max-consecutive-losses", dest="max_consecutive_losses", type=int,
|
|
271
|
+
help="Guard rail: pause after N consecutive losses.")
|
|
272
|
+
p_new.add_argument("--cooldown-seconds", dest="cooldown_seconds", type=int,
|
|
273
|
+
help="Guard rail: post-loss cooldown seconds (>= 60).")
|
|
274
|
+
p_new.add_argument("--max-entries-per-day", dest="max_entries_per_day", type=int,
|
|
275
|
+
help="Guard rail: cap entries per day (>= 1).")
|
|
276
|
+
p_new.add_argument("--edge", help="The strategy's EDGE, carried verbatim into the graph (never interpreted).")
|
|
277
|
+
p_new.set_defaults(func=_cmd_new)
|
|
278
|
+
|
|
279
|
+
p_validate = sub.add_parser(
|
|
280
|
+
"validate",
|
|
281
|
+
help="Validate a strategy graph (schema + graph checks) with teaching errors.",
|
|
282
|
+
)
|
|
283
|
+
p_validate.add_argument("file", help="Path to the YAML strategy graph.")
|
|
284
|
+
p_validate.add_argument(
|
|
285
|
+
"--json",
|
|
286
|
+
action="store_true",
|
|
287
|
+
help="Emit the structured result as JSON instead of the plain-text report.",
|
|
288
|
+
)
|
|
289
|
+
p_validate.set_defaults(func=_cmd_validate)
|
|
290
|
+
|
|
291
|
+
p_compile = sub.add_parser(
|
|
292
|
+
"compile",
|
|
293
|
+
help="Compile a validated strategy graph into a scan.py on the external_scanner contract.",
|
|
294
|
+
)
|
|
295
|
+
p_compile.add_argument("file", help="Path to the YAML strategy graph.")
|
|
296
|
+
p_compile.add_argument(
|
|
297
|
+
"-o",
|
|
298
|
+
"--output",
|
|
299
|
+
help="Write the emitted scan.py here (default: stdout).",
|
|
300
|
+
)
|
|
301
|
+
p_compile.set_defaults(func=_cmd_compile)
|
|
302
|
+
|
|
303
|
+
p_check = sub.add_parser(
|
|
304
|
+
"check",
|
|
305
|
+
help="ONE verdict: validate + pure_fn tests + compile + fixture smoke (exit 0 iff GREEN).",
|
|
306
|
+
)
|
|
307
|
+
p_check.add_argument("file", help="Path to the YAML strategy graph.")
|
|
308
|
+
p_check.add_argument("--json", action="store_true", help="Emit the verdict as JSON.")
|
|
309
|
+
p_check.set_defaults(func=_cmd_check)
|
|
310
|
+
|
|
311
|
+
p_deploy = sub.add_parser(
|
|
312
|
+
"deploy",
|
|
313
|
+
help="Stage the deployable artifact set (idempotent); HARD-gated on a GREEN check.",
|
|
314
|
+
)
|
|
315
|
+
p_deploy.add_argument("file", help="Path to the YAML strategy graph.")
|
|
316
|
+
p_deploy.add_argument("-o", "--out", default="dist", help="Staging directory (default: dist).")
|
|
317
|
+
p_deploy.add_argument("--json", action="store_true", help="Emit the deploy verdict as JSON.")
|
|
318
|
+
p_deploy.set_defaults(func=_cmd_deploy)
|
|
319
|
+
|
|
320
|
+
p_status = sub.add_parser(
|
|
321
|
+
"status",
|
|
322
|
+
help="Derived lifecycle position + the next verb to run (no journals; state is the file).",
|
|
323
|
+
)
|
|
324
|
+
p_status.add_argument("target", help="A graph YAML file, or a staging directory.")
|
|
325
|
+
p_status.add_argument("-o", "--out", help="Staging directory to check for a deployed artifact.")
|
|
326
|
+
p_status.add_argument("--json", action="store_true", help="Emit the status as JSON.")
|
|
327
|
+
p_status.set_defaults(func=_cmd_status)
|
|
328
|
+
|
|
329
|
+
p_catalog = sub.add_parser(
|
|
330
|
+
"catalog",
|
|
331
|
+
help="Print the registry-generated catalog index (node names + one-liners + params).",
|
|
332
|
+
)
|
|
333
|
+
p_catalog.add_argument("--json", action="store_true", help="Wrap the catalog text in JSON.")
|
|
334
|
+
p_catalog.set_defaults(func=_cmd_catalog)
|
|
335
|
+
|
|
336
|
+
p_describe = sub.add_parser(
|
|
337
|
+
"describe",
|
|
338
|
+
help="Print one node's full ports/params/example from the registry.",
|
|
339
|
+
)
|
|
340
|
+
p_describe.add_argument("node", help="Node name (see `composer catalog`).")
|
|
341
|
+
p_describe.add_argument("--json", action="store_true", help="Wrap the describe text in JSON.")
|
|
342
|
+
p_describe.set_defaults(func=_cmd_describe)
|
|
343
|
+
|
|
344
|
+
return parser
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def main(argv: list[str] | None = None) -> int:
|
|
348
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
349
|
+
parser = build_parser()
|
|
350
|
+
args = parser.parse_args(argv)
|
|
351
|
+
return args.func(args)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if __name__ == "__main__":
|
|
355
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""compiler — the deterministic materializer: strategy graph -> runtime-owned scan.py.
|
|
2
|
+
|
|
3
|
+
Compilation over interpretation (D5): the compiler validates the graph, topo-sorts it,
|
|
4
|
+
walks per-node Jinja2 emission templates in that order, and produces straight-line Python
|
|
5
|
+
targeting the existing ``external_scanner`` contract — no IR (the graph is the IR), no
|
|
6
|
+
optimizer, no off-the-shelf codegen framework (D8). Semantics are compile-on-deploy with
|
|
7
|
+
freeze-on-deploy (works-forever); a consent-based upgrade recompiles. Determinism is a
|
|
8
|
+
hard property: the same graph must emit byte-identical ``scan.py``. Correctness is tested
|
|
9
|
+
against our declared semantics via self-authored golden vectors (per-primitive known
|
|
10
|
+
series -> known values, plus small composed synthetic cases with hand-computed expected
|
|
11
|
+
signals), NOT by bit-diffing the hand-written corpus originals — corpus fidelity is a
|
|
12
|
+
separate judged, recorded-replay oracle deferred to slice 8. (Implemented in slice 3.)
|
|
13
|
+
|
|
14
|
+
Public API:
|
|
15
|
+
compile_text / compile_file — validate then materialize graph YAML into scan.py source.
|
|
16
|
+
CompileError — invalid graph (surfaces the teaching-error report) or a
|
|
17
|
+
structural gap the slice-3 compiler cannot emit.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .compile import CompileError, compile_file, compile_text
|
|
21
|
+
|
|
22
|
+
__all__ = ["compile_text", "compile_file", "CompileError"]
|