@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,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"]