@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.
- 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 +396 -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 +190 -0
- package/dist/composer/AUTHORING.md +435 -0
- package/dist/composer/__init__.py +29 -0
- package/dist/composer/authoring/__init__.py +59 -0
- package/dist/composer/authoring/answers.py +297 -0
- package/dist/composer/authoring/archetypes/__init__.py +26 -0
- package/dist/composer/authoring/archetypes/_common.py +410 -0
- package/dist/composer/authoring/archetypes/breakout.py +134 -0
- package/dist/composer/authoring/archetypes/classifier.py +109 -0
- package/dist/composer/authoring/archetypes/skeleton.py +76 -0
- package/dist/composer/authoring/archetypes/thesis_fund.py +103 -0
- package/dist/composer/authoring/archetypes/trend_momentum.py +132 -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 +487 -0
- package/dist/composer/compiler/__init__.py +22 -0
- package/dist/composer/compiler/compile.py +739 -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 +118 -0
- package/dist/composer/generated/describe.md +1168 -0
- package/dist/composer/generated/graph.schema.json +1946 -0
- package/dist/composer/primitives/__init__.py +320 -0
- package/dist/composer/primitives/_candles.py +76 -0
- package/dist/composer/primitives/adapter.py +363 -0
- package/dist/composer/primitives/emitters.py +142 -0
- package/dist/composer/primitives/filters.py +71 -0
- package/dist/composer/primitives/indicators.py +326 -0
- package/dist/composer/primitives/rankers.py +40 -0
- package/dist/composer/primitives/scorers.py +100 -0
- package/dist/composer/primitives/sources.py +979 -0
- package/dist/composer/primitives/stateful.py +254 -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 +425 -0
- package/dist/composer/registry/mcp-tools-snapshot.json +193 -0
- package/dist/composer/registry/mcp_closure.py +311 -0
- package/dist/composer/registry/models.py +417 -0
- package/dist/composer/registry/nodes.py +1297 -0
- package/dist/composer/requirements.txt +21 -0
- package/dist/composer/scaffold_path.py +118 -0
- package/dist/composer/templates/__init__.py +13 -0
- package/dist/composer/templates/scan.py.j2 +34 -0
- package/dist/composer/validator/__init__.py +29 -0
- package/dist/composer/validator/desugar.py +133 -0
- package/dist/composer/validator/errors.py +227 -0
- package/dist/composer/validator/graph_checks.py +443 -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/vendoring.py +103 -0
- package/dist/composer/verbs/__init__.py +49 -0
- package/dist/composer/verbs/_box.py +308 -0
- package/dist/composer/verbs/check.py +254 -0
- package/dist/composer/verbs/close.py +494 -0
- package/dist/composer/verbs/deploy.py +279 -0
- package/dist/composer/verbs/docs.py +30 -0
- package/dist/composer/verbs/fund.py +498 -0
- package/dist/composer/verbs/install.py +586 -0
- package/dist/composer/verbs/runtime_validate.py +422 -0
- package/dist/composer/verbs/smoke.py +134 -0
- package/dist/composer/verbs/status.py +383 -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/load-runtime-config.d.ts +17 -0
- package/dist/runtime/load-runtime-config.d.ts.map +1 -1
- package/dist/runtime/load-runtime-config.js +2 -2
- package/dist/runtime/load-runtime-config.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.d.ts.map +1 -1
- package/dist/skills-manager/manifest.js +1 -3
- package/dist/skills-manager/manifest.js.map +1 -1
- package/dist/skills-manager/skills-manifest.json +1 -3
- 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,297 @@
|
|
|
1
|
+
"""answers — the typed interview-answers object (D7 step 1; the elicitation contract).
|
|
2
|
+
|
|
3
|
+
The 7 decisions are the *type signature of* ``scan(...)`` (HLD "Elicitation"): archetype,
|
|
4
|
+
universe, intervals, cardinality, memory, exit preset, and risk knobs, plus the user's
|
|
5
|
+
EDGE carried verbatim as free text the machine never interprets. This module is the typed
|
|
6
|
+
form of that contract. It is deliberately entry-mode agnostic — a structured authoring
|
|
7
|
+
session and an ambient "make a strategy from this" both land here; ``composer new`` does
|
|
8
|
+
not care where the answers came from.
|
|
9
|
+
|
|
10
|
+
Two acceptance paths, one contract (D7):
|
|
11
|
+
* an answers-file (YAML or JSON) parsed into a dict, and/or
|
|
12
|
+
* individual CLI flags that OVERRIDE the file field-by-field.
|
|
13
|
+
Both feed :func:`load_answers`, which deep-merges (flags win) and constructs :class:`Answers`.
|
|
14
|
+
|
|
15
|
+
Answer domains are registry-derived where possible (D4): a strategy that names a source or
|
|
16
|
+
capability the registry does not have is not a crash — it is turned into a structured GAP
|
|
17
|
+
REPORT before anything is written (see ``composer.authoring.gaps``). The
|
|
18
|
+
:attr:`Answers.requires` field is how an answer references registry capability names: each
|
|
19
|
+
entry is checked against the registry and, if absent, named in the gap report.
|
|
20
|
+
|
|
21
|
+
Exit / protection (slice 9 — PROTECTION BY DEFAULT). ``exit_preset`` is now LIVE (default
|
|
22
|
+
``protected_standard``): the composer emits a complete standalone ``runtime.yaml`` whose
|
|
23
|
+
``exit:`` section is the resolved preset (``composer.authoring.presets``). ``unmanaged=True``
|
|
24
|
+
is the explicit, deliberate opt-out (no exit section); leaving it False without an exit is a
|
|
25
|
+
RED check (CMP120). The ``Risk`` model also carries the guard-rail / pause knobs the composer
|
|
26
|
+
owns (drawdown halt, post-loss cooldown, entry cap) — declarative ``runtime.yaml`` fields the
|
|
27
|
+
runtime enforces (design §4).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Optional
|
|
35
|
+
|
|
36
|
+
import yaml
|
|
37
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Risk(BaseModel):
|
|
41
|
+
"""Sizing knobs + the guard-rail / pause knobs the composer owns (D7 step 3; design §4).
|
|
42
|
+
|
|
43
|
+
``margin_pct``/``leverage`` size the emitted signal. The four pause knobs are optional
|
|
44
|
+
overrides onto the preset's ``risk.guard_rails`` default — ``None`` means "use the
|
|
45
|
+
preset default" (``composer.authoring.presets.resolve_guard_rails``). These are
|
|
46
|
+
declarative ``runtime.yaml`` fields the runtime enforces (auto-halt / post-loss cooldown /
|
|
47
|
+
per-day entry cap); a LIVE operational pause (stopping the box) is an ops action, not the
|
|
48
|
+
composer's (design §4)."""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(extra="forbid")
|
|
51
|
+
|
|
52
|
+
margin_pct: float = 5.0 # PERCENT of withdrawable, (0,100] (never a fraction/USD)
|
|
53
|
+
leverage: float = 3.0
|
|
54
|
+
# guard_rails overrides (None -> preset default); see presets.resolve_guard_rails.
|
|
55
|
+
drawdown_halt_pct: Optional[float] = None # equity-drawdown circuit breaker (halt %)
|
|
56
|
+
max_consecutive_losses: Optional[int] = None # stop after N losses
|
|
57
|
+
cooldown_seconds: Optional[int] = None # post-loss cooldown (>= 60)
|
|
58
|
+
max_entries_per_day: Optional[int] = None # per-day entry cap (>= 1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Universe(BaseModel):
|
|
62
|
+
"""How the tradeable set is chosen. A superset of the corpus shapes; each archetype
|
|
63
|
+
reads the fields it needs (D4 — the answer stays declarative, the template binds it):
|
|
64
|
+
|
|
65
|
+
* ``whitelist`` — a fixed asset list (stag/hawk/bison shape).
|
|
66
|
+
* ``long_basket``/``short_basket`` — the two legs of a fixed macro basket (thesis-fund).
|
|
67
|
+
* ``discover`` (+ params) — derive the universe each tick from the live instrument
|
|
68
|
+
board by a funding/leverage/volume signature (magpie).
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(extra="forbid")
|
|
72
|
+
|
|
73
|
+
whitelist: tuple[str, ...] = ()
|
|
74
|
+
long_basket: tuple[str, ...] = ()
|
|
75
|
+
short_basket: tuple[str, ...] = ()
|
|
76
|
+
discover: bool = False
|
|
77
|
+
dex: str = ""
|
|
78
|
+
name_prefix: str = ""
|
|
79
|
+
max_leverage_cap: float = 5.0
|
|
80
|
+
min_day_volume: float = 0.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Scoring(BaseModel):
|
|
84
|
+
"""OPTIONAL re-selection of the scorer's hard gates / weighted contributors / floor (F1).
|
|
85
|
+
|
|
86
|
+
The scoring surface lets the answers move the hard-vs-weighted split AMONG the graph's
|
|
87
|
+
feature nodes without hand-editing the emitted graph. Every field is ``None`` = "keep the
|
|
88
|
+
archetype's working default"; a set field OVERRIDES it (D7 "answers override else current
|
|
89
|
+
defaults"). Names are SOURCE NODE IDS — the same keys the scorer merges its ``features`` by
|
|
90
|
+
(e.g. ``breakout_detect`` wired as ``brk`` is referenced as ``"brk"``). A name matching no
|
|
91
|
+
feature node in the graph is a generation error (``archetypes._common.resolve_scoring``),
|
|
92
|
+
never a silent drop. Absent entirely (the default), the emitted graph is byte-identical to
|
|
93
|
+
the pre-F1 output."""
|
|
94
|
+
|
|
95
|
+
model_config = ConfigDict(extra="forbid")
|
|
96
|
+
|
|
97
|
+
required_gates: Optional[tuple[str, ...]] = None # feature ids that must ALL be truthy
|
|
98
|
+
weights: Optional[dict[str, float]] = None # feature id -> weighted-sum weight
|
|
99
|
+
min_score: Optional[float] = None # minimum score to emit
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Indicator(BaseModel):
|
|
103
|
+
"""An ADDITIONAL registry indicator node the answers ask the archetype to add and wire (F1).
|
|
104
|
+
|
|
105
|
+
Each entry adds a real registry node (``node``) under ``id``, feeds it the graph's candles,
|
|
106
|
+
and merges its feature output into the scorer's ``features`` — so the id becomes addressable
|
|
107
|
+
from :class:`Scoring` (as a gate or a weight). ``with`` carries the node's params; ``port``
|
|
108
|
+
names which output feeds the scorer (defaults to the node's sole output). A ``node`` the
|
|
109
|
+
registry lacks — or a ``port`` the node does not declare — is a generation error, never a
|
|
110
|
+
silent drop. This is how an answer requests, e.g., ``breakout_detect`` with ``window: 168``
|
|
111
|
+
on an archetype whose fixed indicator set does not already carry it.
|
|
112
|
+
|
|
113
|
+
``source`` (optional, C1b) CHAINS this indicator's input off another feature node instead
|
|
114
|
+
of the graph's candles: when set, the node's data input wires from ``source``'s output
|
|
115
|
+
(``source`` names an EARLIER ``indicators`` entry's id or a built-in feature node of the
|
|
116
|
+
archetype), with the port types checked against the registry — a mismatch or an unknown
|
|
117
|
+
``source`` is a generation error. This is what lets a ``threshold_gate`` read a raw
|
|
118
|
+
``recent_high_bars_ago`` output (``fresh = threshold_gate(op: lte, value: 5, source:
|
|
119
|
+
fresh_raw)``) — a thresholded gate, not the inverted truthiness gate the raw feature had."""
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
122
|
+
|
|
123
|
+
id: str
|
|
124
|
+
node: str
|
|
125
|
+
with_: dict[str, Any] = Field(default_factory=dict, alias="with")
|
|
126
|
+
port: Optional[str] = None
|
|
127
|
+
source: Optional[str] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class RequiredCapability(BaseModel):
|
|
131
|
+
"""A registry capability an answer says the strategy needs (D4 feasibility surface).
|
|
132
|
+
|
|
133
|
+
``name`` is matched against the registry; a name the registry lacks becomes a gap-report
|
|
134
|
+
entry. ``kind`` and ``need`` are carried into the report so the boundary reads honestly
|
|
135
|
+
("``top_traders_discovery`` (source): rank traders by realized PnL") rather than as a
|
|
136
|
+
bare missing symbol.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
model_config = ConfigDict(extra="forbid")
|
|
140
|
+
|
|
141
|
+
name: str
|
|
142
|
+
kind: str = "node"
|
|
143
|
+
need: str = ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Answers(BaseModel):
|
|
147
|
+
"""The typed interview answers — a COMPLETE, VALID graph's input (D7 step 1)."""
|
|
148
|
+
|
|
149
|
+
model_config = ConfigDict(extra="forbid")
|
|
150
|
+
|
|
151
|
+
archetype: str
|
|
152
|
+
strategy: str = "unnamed"
|
|
153
|
+
universe: Universe = Field(default_factory=Universe)
|
|
154
|
+
intervals: tuple[str, ...] = ("4h",)
|
|
155
|
+
max_positions: int = 1 # cardinality -> ranker choice (single-best vs pool-cap)
|
|
156
|
+
ttl_seconds: int = 240 # memory -> ttl_dedup_state dedup window
|
|
157
|
+
# LIVE: emitted as the runtime.yaml exit: section. Default None = "unset"; `composer new`
|
|
158
|
+
# BINDS protected_standard unless overridden (protection-by-default). A graph reaching
|
|
159
|
+
# `check` with exit_preset unset AND unmanaged False is the RED CMP120 contradiction.
|
|
160
|
+
exit_preset: Optional[str] = None
|
|
161
|
+
unmanaged: bool = False # explicit opt-out: no exit section (else CMP120 RED at check)
|
|
162
|
+
risk: Risk = Field(default_factory=Risk)
|
|
163
|
+
edge: str = "" # the user's EDGE, verbatim; the machine never interprets it
|
|
164
|
+
requires: tuple[RequiredCapability, ...] = ()
|
|
165
|
+
# F1 — OPTIONAL expressiveness surface. Absent (the defaults below) => byte-identical to
|
|
166
|
+
# the pre-F1 output; present => the archetype re-selects gates/weights (scoring) and adds
|
|
167
|
+
# registry indicator nodes (indicators) mechanically, no hand edit. See the two submodels.
|
|
168
|
+
scoring: Optional[Scoring] = None
|
|
169
|
+
indicators: tuple[Indicator, ...] = ()
|
|
170
|
+
|
|
171
|
+
@field_validator("exit_preset")
|
|
172
|
+
@classmethod
|
|
173
|
+
def _known_preset(cls, value: Optional[str]) -> Optional[str]:
|
|
174
|
+
"""Reject an unknown exit preset early (a typo, not a crash). ``None`` is allowed (unset
|
|
175
|
+
-> `composer new` binds the default, or `check` flags CMP120). Names live in
|
|
176
|
+
``composer.authoring.presets.EXIT_PRESETS`` (protection-by-default policy)."""
|
|
177
|
+
if value is None:
|
|
178
|
+
return None
|
|
179
|
+
from .presets import EXIT_PRESETS # local import: presets is answers-independent
|
|
180
|
+
if value not in EXIT_PRESETS:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"unknown exit_preset {value!r}; choose one of {sorted(EXIT_PRESETS)} "
|
|
183
|
+
f"(or pass unmanaged=true to deliberately run with NO exit protection)."
|
|
184
|
+
)
|
|
185
|
+
return value
|
|
186
|
+
|
|
187
|
+
@field_validator("requires", mode="before")
|
|
188
|
+
@classmethod
|
|
189
|
+
def _coerce_requires(cls, value: Any) -> Any:
|
|
190
|
+
"""Accept `requires:` entries as bare strings or full mappings."""
|
|
191
|
+
if value is None:
|
|
192
|
+
return ()
|
|
193
|
+
out = []
|
|
194
|
+
for item in value:
|
|
195
|
+
if isinstance(item, str):
|
|
196
|
+
out.append({"name": item})
|
|
197
|
+
else:
|
|
198
|
+
out.append(item)
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
def metadata_block(self) -> dict:
|
|
202
|
+
"""The answers as an ordered plain-dict for the emitted graph's metadata (D7:
|
|
203
|
+
the spec file IS the state; interview answers live in the metadata block)."""
|
|
204
|
+
block: dict = {
|
|
205
|
+
"archetype": self.archetype,
|
|
206
|
+
"strategy": self.strategy,
|
|
207
|
+
"universe": {
|
|
208
|
+
"whitelist": list(self.universe.whitelist),
|
|
209
|
+
"long_basket": list(self.universe.long_basket),
|
|
210
|
+
"short_basket": list(self.universe.short_basket),
|
|
211
|
+
"discover": self.universe.discover,
|
|
212
|
+
"dex": self.universe.dex,
|
|
213
|
+
"name_prefix": self.universe.name_prefix,
|
|
214
|
+
"max_leverage_cap": self.universe.max_leverage_cap,
|
|
215
|
+
"min_day_volume": self.universe.min_day_volume,
|
|
216
|
+
},
|
|
217
|
+
"intervals": list(self.intervals),
|
|
218
|
+
"max_positions": self.max_positions,
|
|
219
|
+
"ttl_seconds": self.ttl_seconds,
|
|
220
|
+
"exit_preset": self.exit_preset, # LIVE: the emitted runtime.yaml exit preset
|
|
221
|
+
"unmanaged": self.unmanaged, # explicit protection opt-out
|
|
222
|
+
"risk": {
|
|
223
|
+
"margin_pct": self.risk.margin_pct,
|
|
224
|
+
"leverage": self.risk.leverage,
|
|
225
|
+
"drawdown_halt_pct": self.risk.drawdown_halt_pct,
|
|
226
|
+
"max_consecutive_losses": self.risk.max_consecutive_losses,
|
|
227
|
+
"cooldown_seconds": self.risk.cooldown_seconds,
|
|
228
|
+
"max_entries_per_day": self.risk.max_entries_per_day,
|
|
229
|
+
},
|
|
230
|
+
"edge": self.edge,
|
|
231
|
+
"requires": [
|
|
232
|
+
{"name": c.name, "kind": c.kind, "need": c.need} for c in self.requires
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
# F1: emit the optional surface ONLY when set, so answers without it round-trip
|
|
236
|
+
# byte-identically to the pre-F1 metadata block (the HARD regression constraint).
|
|
237
|
+
# Both survive `Answers(**block)` deterministically (the deploy metadata re-derive path).
|
|
238
|
+
if self.scoring is not None:
|
|
239
|
+
scoring: dict = {}
|
|
240
|
+
if self.scoring.required_gates is not None:
|
|
241
|
+
scoring["required_gates"] = list(self.scoring.required_gates)
|
|
242
|
+
if self.scoring.weights is not None:
|
|
243
|
+
scoring["weights"] = dict(self.scoring.weights)
|
|
244
|
+
if self.scoring.min_score is not None:
|
|
245
|
+
scoring["min_score"] = self.scoring.min_score
|
|
246
|
+
block["scoring"] = scoring
|
|
247
|
+
if self.indicators:
|
|
248
|
+
block["indicators"] = [
|
|
249
|
+
{"id": i.id, "node": i.node, "with": dict(i.with_), "port": i.port,
|
|
250
|
+
"source": i.source}
|
|
251
|
+
for i in self.indicators
|
|
252
|
+
]
|
|
253
|
+
return block
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _deep_merge(base: dict, over: dict) -> dict:
|
|
257
|
+
"""Field-by-field merge (``over`` wins); nested dicts merge, scalars/lists replace."""
|
|
258
|
+
out = dict(base)
|
|
259
|
+
for key, val in over.items():
|
|
260
|
+
if isinstance(val, dict) and isinstance(out.get(key), dict):
|
|
261
|
+
out[key] = _deep_merge(out[key], val)
|
|
262
|
+
else:
|
|
263
|
+
out[key] = val
|
|
264
|
+
return out
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def load_answers_file(path: str | Path) -> dict:
|
|
268
|
+
"""Parse a YAML or JSON answers-file into a plain dict (raises on non-mapping)."""
|
|
269
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
270
|
+
data = yaml.safe_load(text) # YAML is a JSON superset, so this reads both (D6)
|
|
271
|
+
if data is None:
|
|
272
|
+
return {}
|
|
273
|
+
if not isinstance(data, dict):
|
|
274
|
+
raise ValueError(f"answers file {path} must be a mapping, got {type(data).__name__}")
|
|
275
|
+
return data
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def load_answers(
|
|
279
|
+
file_data: Optional[dict] = None,
|
|
280
|
+
overrides: Optional[dict] = None,
|
|
281
|
+
) -> Answers:
|
|
282
|
+
"""Build :class:`Answers` from an optional answers-file dict + CLI-flag overrides.
|
|
283
|
+
|
|
284
|
+
Overrides win field-by-field (D7: "individual flag overrides"). Unknown keys raise a
|
|
285
|
+
pydantic ``ValidationError`` (``extra="forbid"``) so a typo is caught, not silently
|
|
286
|
+
dropped.
|
|
287
|
+
"""
|
|
288
|
+
merged = _deep_merge(file_data or {}, overrides or {})
|
|
289
|
+
return Answers(**merged)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def dumps_answers_template(archetype: str) -> str:
|
|
293
|
+
"""A blank answers-file skeleton for an archetype (used by `composer new --help`-style
|
|
294
|
+
scaffolding and the docs); deterministic."""
|
|
295
|
+
return yaml.safe_dump(
|
|
296
|
+
Answers(archetype=archetype).metadata_block(), sort_keys=False
|
|
297
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""archetypes — the CI-checked archetype templates (D7 step 2; HLD "templates are derived").
|
|
2
|
+
|
|
3
|
+
Each archetype is CODE that builds a graph from the same registry types (see
|
|
4
|
+
``composer.authoring.builder``), so a template that names a nonexistent node/port fails at
|
|
5
|
+
construction — the CI guarantee that templates cannot drift from the vocabulary. The
|
|
6
|
+
archetypes the POC graphs need are here (trend_momentum / breakout / classifier /
|
|
7
|
+
thesis_fund); an unrecognised archetype falls to the minimal ``skeleton`` (D7 step 5), which
|
|
8
|
+
is NOT in ``ARCHETYPES`` because "no matching archetype" is its trigger.
|
|
9
|
+
|
|
10
|
+
Each module exposes ``build(answers) -> GraphBuilder`` and a ``BANNER`` header.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from . import breakout, classifier, skeleton, thesis_fund, trend_momentum
|
|
16
|
+
|
|
17
|
+
ARCHETYPES = {
|
|
18
|
+
"trend_momentum": trend_momentum,
|
|
19
|
+
"breakout": breakout,
|
|
20
|
+
"classifier": classifier,
|
|
21
|
+
"thesis_fund": thesis_fund,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
SKELETON = skeleton
|
|
25
|
+
|
|
26
|
+
__all__ = ["ARCHETYPES", "SKELETON"]
|