trackwright 0.3.0__py3-none-any.whl

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 (46) hide show
  1. trackwright/__init__.py +13 -0
  2. trackwright/__main__.py +7 -0
  3. trackwright/bridge.py +323 -0
  4. trackwright/cli.py +203 -0
  5. trackwright/commands.py +397 -0
  6. trackwright/config.py +149 -0
  7. trackwright/core/__init__.py +1 -0
  8. trackwright/core/automation.py +410 -0
  9. trackwright/core/base.py +313 -0
  10. trackwright/core/commanding.py +300 -0
  11. trackwright/core/exporting.py +143 -0
  12. trackwright/core/importing.py +127 -0
  13. trackwright/core/lifecycle.py +74 -0
  14. trackwright/core/models.py +467 -0
  15. trackwright/core/policy.py +58 -0
  16. trackwright/core/presets.py +88 -0
  17. trackwright/core/project.py +221 -0
  18. trackwright/core/ranges.py +355 -0
  19. trackwright/core/reading.py +229 -0
  20. trackwright/core/refs.py +88 -0
  21. trackwright/core/saving.py +72 -0
  22. trackwright/core/service.py +27 -0
  23. trackwright/core/settings.py +80 -0
  24. trackwright/core/writing.py +390 -0
  25. trackwright/coremidi.py +307 -0
  26. trackwright/cubase/Trackwright_Bridge.js +1130 -0
  27. trackwright/cubase/__init__.py +83 -0
  28. trackwright/doctor.py +69 -0
  29. trackwright/errors.py +120 -0
  30. trackwright/exports.py +54 -0
  31. trackwright/fake.py +1112 -0
  32. trackwright/host.py +238 -0
  33. trackwright/hostport.py +74 -0
  34. trackwright/installer.py +305 -0
  35. trackwright/launcher.py +150 -0
  36. trackwright/logs.py +50 -0
  37. trackwright/ops.py +46 -0
  38. trackwright/protocol.py +182 -0
  39. trackwright/py.typed +0 -0
  40. trackwright/server.py +472 -0
  41. trackwright/transport.py +130 -0
  42. trackwright-0.3.0.dist-info/METADATA +190 -0
  43. trackwright-0.3.0.dist-info/RECORD +46 -0
  44. trackwright-0.3.0.dist-info/WHEEL +4 -0
  45. trackwright-0.3.0.dist-info/entry_points.txt +3 -0
  46. trackwright-0.3.0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,397 @@
1
+ """Cubase commands bound by the script, with the behaviour observed for each one.
2
+
3
+ Commands give no result back: only ``canPerform`` (which depends on context) and whatever state
4
+ can be read afterwards. Each command is classified by what was **observed** in the POC
5
+ (phases 4 and 2b, Cubase Elements 15.0.30):
6
+
7
+ - ``direct``: acts immediately without UI (verified).
8
+ - ``dialog``: opens a Cubase dialog; the user must complete it (verified). Never report as done.
9
+ - ``confirm``: may ask the user to confirm (verified when a removed track has events).
10
+ - ``ui_selection``: acts on the GUI selection/focus, which the API cannot see; results are not
11
+ controllable from the script (verified: Paste went to another track than the one selected).
12
+ - ``unknown``: bound and accepted by Cubase but its effect was not observed.
13
+
14
+ The keys must match ``COMMANDS`` in ``cubase/Trackwright_Bridge.js`` (checked by a test).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import Literal
21
+
22
+ CommandClass = Literal["direct", "dialog", "confirm", "ui_selection", "unknown"]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class CommandSpec:
27
+ key: str
28
+ category: str
29
+ name: str
30
+ command_class: CommandClass
31
+ description: str
32
+ evidence: str
33
+
34
+
35
+ _SPECS = [
36
+ CommandSpec(
37
+ "zoom.in",
38
+ "Zoom",
39
+ "Zoom In",
40
+ "unknown",
41
+ "Zoom in the focused window",
42
+ "fired, no visible effect (focus-dependent)",
43
+ ),
44
+ CommandSpec("zoom.out", "Zoom", "Zoom Out", "unknown", "Zoom out the focused window", "not observed"),
45
+ CommandSpec(
46
+ "edit.undo",
47
+ "Edit",
48
+ "Undo",
49
+ "ui_selection",
50
+ "Undo the last step",
51
+ "verified: one step per call; a paste may create two steps",
52
+ ),
53
+ CommandSpec("edit.redo", "Edit", "Redo", "ui_selection", "Redo the last undone step", "not observed"),
54
+ CommandSpec(
55
+ "addtrack.audio", "AddTrack", "Audio", "dialog", "Add an audio track", "verified: opens the Add Track dialog"
56
+ ),
57
+ CommandSpec(
58
+ "addtrack.marker",
59
+ "AddTrack",
60
+ "Marker",
61
+ "direct",
62
+ "Add a marker track",
63
+ "verified: created and selected directly",
64
+ ),
65
+ CommandSpec(
66
+ "addtrack.group",
67
+ "AddTrack",
68
+ "Group Channel",
69
+ "dialog",
70
+ "Add a group channel",
71
+ "verified: opens the Add Track dialog",
72
+ ),
73
+ CommandSpec(
74
+ "addtrack.folder", "AddTrack", "Folder", "dialog", "Add a folder track", "verified: opens the Add Track dialog"
75
+ ),
76
+ CommandSpec(
77
+ "project.remove_selected_tracks",
78
+ "Project",
79
+ "Remove Selected Tracks",
80
+ "confirm",
81
+ "Remove the selected tracks",
82
+ "verified: may ask for confirmation when the track has events, unless the user disabled that prompt",
83
+ ),
84
+ CommandSpec(
85
+ "project.selected_to_new_folder",
86
+ "Project",
87
+ "Folding: Tracks To Folder",
88
+ "unknown",
89
+ "Move the selected tracks into a new folder",
90
+ "not observed",
91
+ ),
92
+ CommandSpec(
93
+ "project.select_track_next", "Project", "Select Track: Next", "direct", "Select the next track", "verified"
94
+ ),
95
+ CommandSpec(
96
+ "project.select_track_prev", "Project", "Select Track: Prev", "direct", "Select the previous track", "verified"
97
+ ),
98
+ CommandSpec(
99
+ "transport.return_to_zero",
100
+ "Transport",
101
+ "Return to Zero",
102
+ "unknown",
103
+ "Move the cursor to the project start",
104
+ "not observed",
105
+ ),
106
+ CommandSpec(
107
+ "transport.locate_next_marker",
108
+ "Transport",
109
+ "Locate Next Marker",
110
+ "direct",
111
+ "Move the cursor to the next marker",
112
+ "verified; without a next marker it goes to the end of the last event (Cubase 15)",
113
+ ),
114
+ CommandSpec(
115
+ "transport.locate_prev_marker",
116
+ "Transport",
117
+ "Locate Previous Marker",
118
+ "direct",
119
+ "Move the cursor to the previous marker",
120
+ "verified; ignores the locators; goes to the project start without markers",
121
+ ),
122
+ CommandSpec(
123
+ "transport.set_left_locator",
124
+ "Transport",
125
+ "Set Left Locator",
126
+ "direct",
127
+ "Set the left locator at the cursor",
128
+ "verified: left locator at the cursor (TW-1211)",
129
+ ),
130
+ CommandSpec(
131
+ "transport.set_right_locator",
132
+ "Transport",
133
+ "Set Right Locator",
134
+ "direct",
135
+ "Set the right locator at the cursor",
136
+ "verified: right locator at the cursor (TW-1211)",
137
+ ),
138
+ CommandSpec(
139
+ "transport.locators_to_selection",
140
+ "Transport",
141
+ "Locators to Selection",
142
+ "unknown",
143
+ "Set the locators to the selection range",
144
+ "not observed",
145
+ ),
146
+ CommandSpec(
147
+ "marker.insert",
148
+ "Marker",
149
+ "Insert Marker",
150
+ "direct",
151
+ "Insert a marker at the cursor",
152
+ "verified (needs a marker track)",
153
+ ),
154
+ CommandSpec("edit.cut", "Edit", "Cut", "ui_selection", "Cut the selection", "not observed"),
155
+ CommandSpec(
156
+ "edit.copy", "Edit", "Copy", "ui_selection", "Copy the selection", "verified: acts on the GUI selection"
157
+ ),
158
+ CommandSpec(
159
+ "edit.paste",
160
+ "Edit",
161
+ "Paste",
162
+ "ui_selection",
163
+ "Paste at the cursor",
164
+ "verified: pasted into another track than the one selected through the API",
165
+ ),
166
+ CommandSpec("edit.delete", "Edit", "Delete", "ui_selection", "Delete the selection", "not observed"),
167
+ CommandSpec(
168
+ "edit.split_at_cursor",
169
+ "Edit",
170
+ "Split at Cursor",
171
+ "ui_selection",
172
+ "Split the selected events at the cursor",
173
+ "verified: split a MIDI part at the cursor",
174
+ ),
175
+ CommandSpec(
176
+ "edit.split_loop",
177
+ "Edit",
178
+ "Split Loop",
179
+ "direct",
180
+ "Split every event crossing the locators",
181
+ "verified: all tracks with events in the range",
182
+ ),
183
+ CommandSpec(
184
+ "edit.split_range", "Edit", "Split Range", "ui_selection", "Split at the range selection", "not observed"
185
+ ),
186
+ CommandSpec("edit.select_all", "Edit", "Select All", "ui_selection", "Select all events", "not observed"),
187
+ CommandSpec("edit.select_none", "Edit", "Select None", "ui_selection", "Clear the selection", "not observed"),
188
+ CommandSpec(
189
+ "edit.select_in_loop",
190
+ "Edit",
191
+ "Select in Loop",
192
+ "ui_selection",
193
+ "Select the range between the locators",
194
+ "verified: with the Range tool, a range between the locators on all tracks (TW-1211)",
195
+ ),
196
+ # TW-1211: range editing and event probing (experiments/tw1211)
197
+ CommandSpec("tool.range", "Tool", "Range Tool", "direct", "Activate the Range Selection tool", "not observed"),
198
+ CommandSpec("tool.select", "Tool", "Select Tool", "direct", "Activate the Object Selection tool", "not observed"),
199
+ CommandSpec(
200
+ "file.export_audio_mixdown",
201
+ "File",
202
+ "Export Audio Mixdown",
203
+ "dialog",
204
+ "Open the Export Audio Mixdown dialog",
205
+ "not observed (TW-1204)",
206
+ ),
207
+ CommandSpec("file.save", "File", "Save", "direct", "Save the project", "verified: rewrites the .cpr (TW-1202)"),
208
+ CommandSpec(
209
+ "file.save_new_version",
210
+ "File",
211
+ "Save New Version",
212
+ "unknown",
213
+ "Save the project as a new numbered version",
214
+ "not observed (TW-1202)",
215
+ ),
216
+ CommandSpec(
217
+ "file.import_audio",
218
+ "File",
219
+ "Import Audio File",
220
+ "dialog",
221
+ "Open the Import Audio File dialog",
222
+ "not observed (TW-1203)",
223
+ ),
224
+ CommandSpec(
225
+ "preset.next", "Preset", "Next", "unknown", "Next preset of the focused plugin", "not observed (TW-1205)"
226
+ ),
227
+ CommandSpec(
228
+ "preset.previous",
229
+ "Preset",
230
+ "Previous",
231
+ "unknown",
232
+ "Previous preset of the focused plugin",
233
+ "not observed (TW-1205)",
234
+ ),
235
+ CommandSpec(
236
+ "edit.delete_time",
237
+ "Edit",
238
+ "Delete Time",
239
+ "ui_selection",
240
+ "Delete the range and close the gap",
241
+ "verified: removes the range selection and closes the gap on every track in it (TW-1211)",
242
+ ),
243
+ CommandSpec(
244
+ "edit.cut_time",
245
+ "Edit",
246
+ "Cut Time",
247
+ "ui_selection",
248
+ "Cut the range and close the gap",
249
+ "verified: like Delete Time, also copies (TW-1211)",
250
+ ),
251
+ CommandSpec(
252
+ "edit.insert_silence",
253
+ "Edit",
254
+ "Insert Silence",
255
+ "ui_selection",
256
+ "Insert silence over the range",
257
+ "verified: inserts silence over the range, shifting later events (TW-1211)",
258
+ ),
259
+ CommandSpec(
260
+ "edit.crop_range",
261
+ "Edit",
262
+ "Crop Range",
263
+ "ui_selection",
264
+ "Keep only the range",
265
+ "verified: keeps only the range selection (TW-1211)",
266
+ ),
267
+ CommandSpec(
268
+ "process.silence",
269
+ "Process",
270
+ "Silence",
271
+ "dialog",
272
+ "Silence the selected audio",
273
+ "verified: asks about audio files shared by several events (TW-1211)",
274
+ ),
275
+ CommandSpec(
276
+ "edit.select_all_on_tracks",
277
+ "Edit",
278
+ "Select All on Tracks",
279
+ "ui_selection",
280
+ "Select all events of the selected tracks",
281
+ "verified: all events of the selected tracks (TW-1211)",
282
+ ),
283
+ CommandSpec(
284
+ "edit.select_events_under_cursor",
285
+ "Edit",
286
+ "Select Events under Cursor",
287
+ "unknown",
288
+ "Select the events under the cursor",
289
+ "not observed",
290
+ ),
291
+ CommandSpec(
292
+ "transport.locate_next_event",
293
+ "Transport",
294
+ "Locate Next Event",
295
+ "direct",
296
+ "Move the cursor to the next event edge",
297
+ "verified: next event edge (start or end) on the selected tracks (TW-1211)",
298
+ ),
299
+ CommandSpec(
300
+ "transport.locate_prev_event",
301
+ "Transport",
302
+ "Locate Previous Event",
303
+ "direct",
304
+ "Move the cursor to the previous event edge",
305
+ "not observed; mirror of Locate Next Event",
306
+ ),
307
+ CommandSpec(
308
+ "transport.locate_selection",
309
+ "Transport",
310
+ "Locate Selection",
311
+ "direct",
312
+ "Move the cursor to the selection start",
313
+ "verified: cursor to the selection start (TW-1211)",
314
+ ),
315
+ CommandSpec(
316
+ "transport.locate_selection_end",
317
+ "Transport",
318
+ "Locate Selection End",
319
+ "direct",
320
+ "Move the cursor to the selection end",
321
+ "verified: cursor to the selection end (TW-1211)",
322
+ ),
323
+ CommandSpec(
324
+ "transport.to_left_locator",
325
+ "Transport",
326
+ "To Left Locator",
327
+ "unknown",
328
+ "Move the cursor to the left locator",
329
+ "not observed",
330
+ ),
331
+ CommandSpec(
332
+ "audio.detect_silence",
333
+ "Audio",
334
+ "Detect Silence",
335
+ "dialog",
336
+ "Open Detect Silence for the selected audio event",
337
+ "verified: opens its dialog; needs a selected audio event",
338
+ ),
339
+ CommandSpec(
340
+ "automation.mode_touch",
341
+ "Automation",
342
+ "Automation Mode - Touch",
343
+ "unknown",
344
+ "Automation mode Touch: writes only while a control is touched",
345
+ "not observed (TW-1206)",
346
+ ),
347
+ CommandSpec(
348
+ "automation.mode_auto_latch",
349
+ "Automation",
350
+ "Automation Mode - Auto-Latch",
351
+ "unknown",
352
+ "Automation mode Auto-Latch: writing starts on any change and lasts until stop",
353
+ "not observed (TW-1206)",
354
+ ),
355
+ CommandSpec(
356
+ "automation.mode_cross_over",
357
+ "Automation",
358
+ "Automation Mode - Cross-Over",
359
+ "unknown",
360
+ "Automation mode Cross-Over: like Auto-Latch, stops when crossing existing automation",
361
+ "not observed (TW-1206)",
362
+ ),
363
+ CommandSpec(
364
+ "automation.next_mode",
365
+ "Automation",
366
+ "Next Automation Mode",
367
+ "unknown",
368
+ "Switch to the next automation mode",
369
+ "not observed (TW-1206)",
370
+ ),
371
+ CommandSpec(
372
+ "automation.toggle_write_selected",
373
+ "Automation",
374
+ "Toggle Write Enable Selected Tracks",
375
+ "unknown",
376
+ "Toggle automation write on the selected tracks",
377
+ "used by factory scripts; not observed",
378
+ ),
379
+ CommandSpec(
380
+ "transport.start",
381
+ "Transport",
382
+ "Start",
383
+ "direct",
384
+ "Start playback (never records)",
385
+ "verified: the cursor advances (2026-11-13)",
386
+ ),
387
+ CommandSpec(
388
+ "transport.stop",
389
+ "Transport",
390
+ "Stop",
391
+ "direct",
392
+ "Stop playback",
393
+ "verified: the cursor stops at once (2026-11-13)",
394
+ ),
395
+ ]
396
+
397
+ COMMANDS: dict[str, CommandSpec] = {spec.key: spec for spec in _SPECS}
trackwright/config.py ADDED
@@ -0,0 +1,149 @@
1
+ """Configuration: TOML file + environment, validated with pydantic.
2
+
3
+ Lookup order for the file:
4
+ 1. the path in ``TRACKWRIGHT_CONFIG`` (must exist);
5
+ 2. ``~/.config/trackwright/config.toml`` (optional).
6
+
7
+ Defaults are safe: **writes are disabled** until ``[writes] enabled = true``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import tempfile
14
+ import tomllib
15
+ from pathlib import Path
16
+ from typing import Any, Literal
17
+
18
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
19
+
20
+ from trackwright.errors import ConfigError
21
+
22
+ CONFIG_ENV = "TRACKWRIGHT_CONFIG"
23
+ DEFAULT_CONFIG_PATH = Path("~/.config/trackwright/config.toml")
24
+ DEFAULT_SCRIPT_DIR = Path("~/Documents/Steinberg/Cubase/MIDI Remote/Driver Scripts/Local/trackwright/bridge")
25
+
26
+
27
+ def default_socket_dir() -> Path:
28
+ """Private per-user directory under ``$TMPDIR`` (never inside the project: the data volume may be ``noowners``)."""
29
+ base = Path(os.environ.get("TMPDIR") or tempfile.gettempdir())
30
+ return base / f"trackwright-{os.getuid()}"
31
+
32
+
33
+ class _Section(BaseModel):
34
+ model_config = ConfigDict(extra="forbid", frozen=True)
35
+
36
+
37
+ class BridgeConfig(_Section):
38
+ to_cubase_port: str = Field(default="Trackwright To Cubase", min_length=1)
39
+ """Virtual port Python writes to; Cubase sees it as the script's MIDI input."""
40
+ from_cubase_port: str = Field(default="Trackwright From Cubase", min_length=1)
41
+ """Virtual port Python reads from; Cubase sees it as the script's MIDI output."""
42
+ socket_dir: Path = Field(default_factory=default_socket_dir, validate_default=True)
43
+ """Directory of the IPC Unix socket (created with mode 0700)."""
44
+ request_timeout: float = Field(default=3.0, ge=0.05, le=30.0)
45
+ """Default timeout in seconds for one request to Cubase."""
46
+ write_timeout: float = Field(default=5.0, ge=0.05, le=30.0)
47
+ """Timeout for write operations (they run in an idle tick and read back)."""
48
+ backend: Literal["auto", "coremidi", "rtmidi"] = "auto"
49
+ """MIDI transport: ``coremidi`` (ctypes, nothing to compile; macOS), ``rtmidi`` (mido + python-rtmidi,
50
+ which must be installed: see pyproject.toml) or ``auto`` (coremidi on macOS, rtmidi elsewhere)."""
51
+ autostart: bool = True
52
+ """``trackwright serve`` starts the bridge when it is not running (TW-1201)."""
53
+ autostart_timeout: float = Field(default=10.0, ge=0.5, le=60.0)
54
+ """Seconds to wait for an auto-started bridge to accept connections."""
55
+
56
+ @field_validator("socket_dir", mode="after")
57
+ @classmethod
58
+ def _expand(cls, value: Path) -> Path:
59
+ return value.expanduser()
60
+
61
+ @property
62
+ def socket_path(self) -> Path:
63
+ return self.socket_dir / "bridge.sock"
64
+
65
+
66
+ class WritesConfig(_Section):
67
+ enabled: bool = False
68
+ """Master switch. While false every write tool fails with ``WRITES_DISABLED`` (dry runs still work)."""
69
+ allowed_tracks: tuple[str, ...] = ()
70
+ """Track names that may be modified. Empty means every track (when ``enabled``)."""
71
+ save: bool = False
72
+ """Allow cubase_save_project to save the project file on disk (TW-1202). Separate from ``enabled``."""
73
+
74
+
75
+ class LoggingConfig(_Section):
76
+ level: Literal["TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL"] = "INFO"
77
+ file: Path | None = None
78
+ """Optional log file (rotated at 10 MB, 5 files kept)."""
79
+
80
+ @field_validator("level", mode="before")
81
+ @classmethod
82
+ def _upper(cls, value: Any) -> Any:
83
+ return value.upper() if isinstance(value, str) else value
84
+
85
+ @field_validator("file", mode="after")
86
+ @classmethod
87
+ def _expand(cls, value: Path | None) -> Path | None:
88
+ return value.expanduser() if value is not None else None
89
+
90
+
91
+ class ScriptConfig(_Section):
92
+ # validate_default: pydantic does not run validators on defaults otherwise, and "~" would stay literal.
93
+ install_dir: Path = Field(default=DEFAULT_SCRIPT_DIR, validate_default=True)
94
+ """Where ``trackwright script install`` copies the MIDI Remote script."""
95
+
96
+ @field_validator("install_dir", mode="after")
97
+ @classmethod
98
+ def _expand(cls, value: Path) -> Path:
99
+ return value.expanduser()
100
+
101
+
102
+ class CubaseConfig(_Section):
103
+ app: str = Field(default="Cubase 15", min_length=1)
104
+ """Application name for ``open -a`` and process name for ``pgrep -x``."""
105
+ launch_timeout: float = Field(default=120.0, ge=5.0, le=600.0)
106
+ """Seconds cubase_launch waits for the script to answer after opening Cubase."""
107
+
108
+
109
+ class Settings(_Section):
110
+ bridge: BridgeConfig = Field(default_factory=BridgeConfig)
111
+ writes: WritesConfig = Field(default_factory=WritesConfig)
112
+ logging: LoggingConfig = Field(default_factory=LoggingConfig)
113
+ script: ScriptConfig = Field(default_factory=ScriptConfig)
114
+ cubase: CubaseConfig = Field(default_factory=CubaseConfig)
115
+ source: Path | None = None
116
+ """File the settings were loaded from (``None`` = built-in defaults)."""
117
+
118
+
119
+ def config_path(path: Path | None = None, *, environ: dict[str, str] | None = None) -> tuple[Path, bool]:
120
+ """The configuration file to use and whether it was given explicitly (argument or ``$TRACKWRIGHT_CONFIG``)."""
121
+ env = os.environ if environ is None else environ
122
+ explicit = path or (Path(env[CONFIG_ENV]) if env.get(CONFIG_ENV) else None)
123
+ return (explicit or DEFAULT_CONFIG_PATH).expanduser(), explicit is not None
124
+
125
+
126
+ def load_settings(
127
+ path: Path | None = None, *, environ: dict[str, str] | None = None, missing_ok: bool = False
128
+ ) -> Settings:
129
+ """Load settings from ``path``, ``$TRACKWRIGHT_CONFIG`` or the default location.
130
+
131
+ A missing default file means built-in defaults; a missing explicit file is an error unless ``missing_ok``
132
+ (``trackwright setup`` is about to create it).
133
+ """
134
+ candidate, explicit = config_path(path, environ=environ)
135
+ if not candidate.exists():
136
+ if explicit and not missing_ok:
137
+ raise ConfigError(f"configuration file not found: {candidate}", details={"path": str(candidate)})
138
+ return Settings()
139
+ try:
140
+ raw = tomllib.loads(candidate.read_text(encoding="utf-8"))
141
+ except (OSError, tomllib.TOMLDecodeError) as exc:
142
+ raise ConfigError(f"cannot read {candidate}: {exc}", details={"path": str(candidate)}) from exc
143
+ if "source" in raw:
144
+ raise ConfigError("'source' is not a configurable key", details={"path": str(candidate)})
145
+ try:
146
+ return Settings(**raw, source=candidate)
147
+ except ValidationError as exc:
148
+ errors = [{"loc": ".".join(str(p) for p in e["loc"]), "msg": e["msg"]} for e in exc.errors()]
149
+ raise ConfigError(f"invalid configuration in {candidate}", details={"errors": errors}) from exc
@@ -0,0 +1 @@
1
+ """Domain layer of the MCP server: references, policy, models and the Cubase service."""