ellements 0.2.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 (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. ellements-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,551 @@
1
+ """Rich renderers for human-facing FSLM output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import json
7
+ from collections.abc import Mapping
8
+ from pathlib import Path
9
+ from types import ModuleType
10
+ from typing import Any, Literal
11
+
12
+ from rich import box
13
+ from rich.align import Align
14
+ from rich.console import Console, Group, RenderableType
15
+ from rich.json import JSON
16
+ from rich.padding import Padding
17
+ from rich.panel import Panel
18
+ from rich.table import Table
19
+ from rich.text import Text
20
+ from rich.theme import Theme
21
+
22
+ from .definition import MachineDefinition
23
+ from .models import FSLMEvent, MachineSnapshot, MachineSpec, StepResult
24
+
25
+ ColorMode = Literal["auto", "always", "never"]
26
+
27
+ THEME = Theme(
28
+ {
29
+ "brand": "bold #6C63FF",
30
+ "brand.dim": "#4A42B0",
31
+ "accent": "bold #56D6C1",
32
+ "accent.dim": "#3BA899",
33
+ "amber": "bold #FFB347",
34
+ "amber.dim": "#CC8F39",
35
+ "danger": "bold #FF6B6B",
36
+ "muted": "#A8A4CE",
37
+ "success": "bold #56D6C1",
38
+ "state": "bold #56D6C1",
39
+ "transition": "bold #FFB347",
40
+ "blocked": "bold #FF6B6B",
41
+ }
42
+ )
43
+
44
+
45
+ def make_console(color: ColorMode = "auto") -> Console:
46
+ """Create the standard FSLM Rich console."""
47
+
48
+ return Console(
49
+ theme=THEME,
50
+ force_terminal=True if color == "always" else None,
51
+ no_color=color == "never",
52
+ )
53
+
54
+
55
+ def render_validation(
56
+ spec: MachineSpec,
57
+ *,
58
+ console: Console | None = None,
59
+ color: ColorMode = "auto",
60
+ ) -> None:
61
+ """Render a successful machine validation."""
62
+
63
+ out = console or make_console(color)
64
+ _app_header(
65
+ "FSLM",
66
+ {"Machine": spec.name, "States": str(len(spec.states)), "Transitions": str(len(spec.transitions))},
67
+ icon="◆",
68
+ border_style="brand",
69
+ console=out,
70
+ )
71
+ out.print(
72
+ Panel(
73
+ "[success]✓ Machine definition is valid[/]\n"
74
+ "[muted]The graph, declared states, transitions, and runtime policy loaded successfully.[/]",
75
+ title="[success]Ready[/]",
76
+ title_align="left",
77
+ border_style="accent.dim",
78
+ box=box.ROUNDED,
79
+ padding=(1, 2),
80
+ )
81
+ )
82
+
83
+
84
+ def render_snapshot(
85
+ snapshot: MachineSnapshot,
86
+ *,
87
+ spec: MachineSpec | None = None,
88
+ title: str = "FSLM Snapshot",
89
+ console: Console | None = None,
90
+ color: ColorMode = "auto",
91
+ ) -> None:
92
+ """Render a snapshot in a compact, human-readable layout."""
93
+
94
+ out = console or make_console(color)
95
+ _app_header(
96
+ title,
97
+ {
98
+ "Machine": snapshot.machine_name,
99
+ "Instance": snapshot.machine_id,
100
+ "Step": str(snapshot.step_index),
101
+ },
102
+ icon="◆",
103
+ border_style="brand",
104
+ console=out,
105
+ )
106
+ out.print(
107
+ Panel(
108
+ _state_card(
109
+ snapshot.current_state,
110
+ spec,
111
+ label="Current state",
112
+ border_style="accent.dim",
113
+ ),
114
+ border_style="brand.dim",
115
+ box=box.ROUNDED,
116
+ padding=(1, 2),
117
+ )
118
+ )
119
+ if snapshot.variables:
120
+ out.print(_mapping_panel("Snapshot variables", snapshot.variables, border_style="brand.dim"))
121
+
122
+
123
+ def render_step(
124
+ result: StepResult,
125
+ *,
126
+ definition: MachineDefinition | None = None,
127
+ event: FSLMEvent | None = None,
128
+ state_dir: Path | None = None,
129
+ console: Console | None = None,
130
+ color: ColorMode = "auto",
131
+ ) -> None:
132
+ """Render one machine step as a polished state-transition view."""
133
+
134
+ out = console or make_console(color)
135
+ spec = definition.spec if definition is not None else None
136
+ metadata = {
137
+ "Machine": result.new_snapshot.machine_name,
138
+ "Instance": result.new_snapshot.machine_id,
139
+ "Step": str(result.new_snapshot.step_index),
140
+ "Event": event.type if event is not None else result.event_id[:8],
141
+ }
142
+ _app_header(
143
+ "FSLM State Movement",
144
+ metadata,
145
+ icon="◆",
146
+ border_style=_status_border(result.status),
147
+ console=out,
148
+ )
149
+ out.print(_movement_panel(result, spec))
150
+ out.print(_decision_panel(result))
151
+ _render_outputs(out, result)
152
+ _render_actions(out, result)
153
+ if result.violations:
154
+ out.print(_list_panel("Violations", result.violations, border_style="danger"))
155
+ out.print(_trace_panel(result, event))
156
+ footer: dict[str, str | int | float] = {
157
+ "Status": result.status,
158
+ "Transition": result.selected_transition or "none",
159
+ "State": result.target_state,
160
+ }
161
+ if state_dir is not None and not state_dir.name.startswith("tmp."):
162
+ footer["Saved"] = _short_path(state_dir)
163
+ _stats_footer(footer, border_style="brand.dim", console=out)
164
+
165
+
166
+ def _movement_panel(result: StepResult, spec: MachineSpec | None) -> Panel:
167
+ grid = Table.grid(expand=True)
168
+ grid.add_column(ratio=5)
169
+ grid.add_column(ratio=3)
170
+ grid.add_column(ratio=5)
171
+ grid.add_row(
172
+ _state_card(result.source_state, spec, label="From", border_style="brand.dim"),
173
+ _transition_card(result),
174
+ _state_card(result.target_state, spec, label="To", border_style=_status_border(result.status)),
175
+ )
176
+ return Panel(
177
+ grid,
178
+ title="[bold white]Machine movement[/]",
179
+ title_align="left",
180
+ border_style=_status_border(result.status),
181
+ box=box.DOUBLE,
182
+ padding=(1, 2),
183
+ )
184
+
185
+
186
+ def _state_card(
187
+ state_name: str,
188
+ spec: MachineSpec | None,
189
+ *,
190
+ label: str,
191
+ border_style: str,
192
+ ) -> Panel:
193
+ title = Text(label, style="muted")
194
+ state = spec.states.get(state_name) if spec is not None else None
195
+ lines: list[RenderableType] = [
196
+ Text(_humanize(state_name), style="state"),
197
+ Text(state_name, style="muted"),
198
+ ]
199
+ objective = state.objective if state is not None else ""
200
+ if objective:
201
+ lines.append(Text(objective, style="white"))
202
+ if state is not None and state.terminal:
203
+ lines.append(Text("terminal state", style="amber"))
204
+ return Panel(
205
+ Group(*lines),
206
+ title=title,
207
+ title_align="left",
208
+ border_style=border_style,
209
+ box=box.ROUNDED,
210
+ padding=(1, 2),
211
+ )
212
+
213
+
214
+ def _transition_card(result: StepResult) -> Panel:
215
+ selected = result.selected_transition or "No transition selected"
216
+ body = Table.grid(expand=True)
217
+ body.add_column(justify="center")
218
+ body.add_row(Text("→", style="bold white"))
219
+ body.add_row(Text(_humanize(selected), style="transition"))
220
+ body.add_row(Text(selected, style="muted"))
221
+ body.add_row(_status_badge(result.status))
222
+ return Panel(
223
+ Align.center(body),
224
+ title="[muted]Transition[/]",
225
+ title_align="left",
226
+ border_style=_status_border(result.status),
227
+ box=box.ROUNDED,
228
+ padding=(1, 2),
229
+ )
230
+
231
+
232
+ def _decision_panel(result: StepResult) -> Panel:
233
+ if not result.guard_results and not result.invariant_results:
234
+ return Panel(
235
+ "[muted]No guards or invariants were evaluated for this step.[/]",
236
+ title="[bold white]Decision[/]",
237
+ title_align="left",
238
+ border_style="brand.dim",
239
+ box=box.ROUNDED,
240
+ padding=(1, 2),
241
+ )
242
+ table = Table(
243
+ box=box.SIMPLE_HEAVY,
244
+ border_style="brand.dim",
245
+ show_header=True,
246
+ header_style="#A8A4CE",
247
+ expand=True,
248
+ )
249
+ table.add_column("Check", style="white", ratio=3)
250
+ table.add_column("Decision", ratio=2)
251
+ table.add_column("Confidence", justify="right", ratio=1)
252
+ table.add_column("Notes", style="muted", ratio=4)
253
+ for decision in [*result.guard_results, *result.invariant_results]:
254
+ table.add_row(
255
+ decision.id,
256
+ "[success]✓ allowed[/]" if decision.allowed else "[blocked]✗ blocked[/]",
257
+ f"{decision.confidence:.0%}",
258
+ _decision_notes(decision.evidence, decision.uncertainties, decision.alternatives),
259
+ )
260
+ return Panel(
261
+ table,
262
+ title="[bold white]Decision path[/]",
263
+ title_align="left",
264
+ border_style=_status_border(result.status),
265
+ box=box.ROUNDED,
266
+ padding=(1, 2),
267
+ )
268
+
269
+
270
+ def _render_outputs(console: Console, result: StepResult) -> None:
271
+ if not result.outputs:
272
+ console.print(
273
+ Panel(
274
+ "[muted]This transition emitted no outputs.[/]",
275
+ title="[bold white]Outputs[/]",
276
+ title_align="left",
277
+ border_style="brand.dim",
278
+ box=box.ROUNDED,
279
+ padding=(1, 2),
280
+ )
281
+ )
282
+ return
283
+ panels = []
284
+ for output in result.outputs:
285
+ parts: list[RenderableType] = []
286
+ if output.description:
287
+ parts.append(Text(output.description, style="muted"))
288
+ parts.append(_mapping_table(output.payload))
289
+ if output.destination:
290
+ parts.append(Text(f"Destination: {output.destination}", style="accent"))
291
+ panels.append(
292
+ Panel(
293
+ Group(*parts),
294
+ title=f"[accent]{output.type}[/]",
295
+ title_align="left",
296
+ border_style="accent.dim",
297
+ box=box.ROUNDED,
298
+ padding=(1, 2),
299
+ )
300
+ )
301
+ console.print(
302
+ Panel(
303
+ Group(*panels),
304
+ title="[bold white]Emitted outputs[/]",
305
+ title_align="left",
306
+ border_style="accent.dim",
307
+ box=box.ROUNDED,
308
+ padding=(1, 2),
309
+ )
310
+ )
311
+
312
+
313
+ def _render_actions(console: Console, result: StepResult) -> None:
314
+ if not result.actions:
315
+ return
316
+ table = Table(
317
+ box=box.SIMPLE,
318
+ border_style="brand.dim",
319
+ header_style="#A8A4CE",
320
+ expand=True,
321
+ )
322
+ table.add_column("Action")
323
+ table.add_column("Tool")
324
+ table.add_column("Status")
325
+ table.add_column("Message", style="muted")
326
+ for action in result.actions:
327
+ table.add_row(
328
+ action.action_name,
329
+ action.tool,
330
+ _action_status(action.status),
331
+ action.message,
332
+ )
333
+ console.print(
334
+ Panel(
335
+ table,
336
+ title="[bold white]Actions[/]",
337
+ title_align="left",
338
+ border_style="brand.dim",
339
+ box=box.ROUNDED,
340
+ padding=(1, 2),
341
+ )
342
+ )
343
+
344
+
345
+ def _trace_panel(result: StepResult, event: FSLMEvent | None) -> Panel:
346
+ table = Table.grid(expand=True)
347
+ table.add_column(style="muted", no_wrap=True, ratio=1)
348
+ table.add_column(style="white", ratio=4)
349
+ if event is not None:
350
+ table.add_row("Event", f"{event.type} [muted]{event.id[:8]}[/]")
351
+ candidates = result.trace.get("candidate_transitions", [])
352
+ legal = result.trace.get("legal_transitions", [])
353
+ if isinstance(candidates, list):
354
+ table.add_row("Candidates", _join_names(candidates))
355
+ if isinstance(legal, list):
356
+ table.add_row("Legal", _join_names(legal))
357
+ table.add_row("Snapshot", result.new_snapshot.id[:8])
358
+ return Panel(
359
+ table,
360
+ title="[bold white]Trace[/]",
361
+ title_align="left",
362
+ border_style="brand.dim",
363
+ box=box.ROUNDED,
364
+ padding=(1, 2),
365
+ )
366
+
367
+
368
+ def _mapping_panel(title: str, mapping: Mapping[str, Any], *, border_style: str) -> Panel:
369
+ return Panel(
370
+ _mapping_table(mapping),
371
+ title=f"[bold white]{title}[/]",
372
+ title_align="left",
373
+ border_style=border_style,
374
+ box=box.ROUNDED,
375
+ padding=(1, 2),
376
+ )
377
+
378
+
379
+ def _mapping_table(mapping: Mapping[str, Any]) -> Table:
380
+ table = Table.grid(expand=True)
381
+ table.add_column(style="muted", no_wrap=True, ratio=1)
382
+ table.add_column(style="white", ratio=4)
383
+ for key, value in mapping.items():
384
+ table.add_row(_humanize(str(key)), _value_renderable(value))
385
+ return table
386
+
387
+
388
+ def _list_panel(title: str, values: list[str], *, border_style: str) -> Panel:
389
+ body = "\n".join(f"• {value}" for value in values)
390
+ return Panel(
391
+ body,
392
+ title=f"[bold white]{title}[/]",
393
+ title_align="left",
394
+ border_style=border_style,
395
+ box=box.ROUNDED,
396
+ padding=(1, 2),
397
+ )
398
+
399
+
400
+ def _value_renderable(value: Any) -> RenderableType:
401
+ if isinstance(value, str):
402
+ return Text(value)
403
+ if isinstance(value, bool):
404
+ return Text("true" if value else "false", style="success" if value else "danger")
405
+ if isinstance(value, int | float) or value is None:
406
+ return Text(str(value), style="accent")
407
+ summary = _compact_summary(value)
408
+ if summary is not None:
409
+ return Text(summary)
410
+ return Padding(
411
+ JSON(json.dumps(value, ensure_ascii=False)),
412
+ (0, 0, 0, 0),
413
+ )
414
+
415
+
416
+ def _compact_summary(value: Any) -> str | None:
417
+ if isinstance(value, Mapping):
418
+ if "news" in value and isinstance(value["news"], Mapping):
419
+ news = value["news"]
420
+ title = str(news.get("title", "news item"))
421
+ tone = news.get("tone")
422
+ severity = news.get("severity")
423
+ parts = [title]
424
+ if tone is not None:
425
+ parts.append(f"tone={tone}")
426
+ if severity is not None:
427
+ parts.append(f"severity={severity}")
428
+ return " · ".join(parts)
429
+ simple: list[str] = []
430
+ for key, item in value.items():
431
+ if len(simple) >= 4:
432
+ simple.append("…")
433
+ break
434
+ if isinstance(item, str | int | float | bool) or item is None:
435
+ simple.append(f"{key}={item}")
436
+ if simple and len(simple) >= len(value):
437
+ return " · ".join(simple)
438
+ if isinstance(value, list) and all(isinstance(item, str) for item in value):
439
+ return " · ".join(value)
440
+ return None
441
+
442
+
443
+ def _decision_notes(
444
+ evidence: list[str],
445
+ uncertainties: list[str],
446
+ alternatives: list[str],
447
+ ) -> str:
448
+ notes = []
449
+ if evidence:
450
+ notes.append("evidence: " + "; ".join(evidence))
451
+ if uncertainties:
452
+ notes.append("uncertainty: " + "; ".join(uncertainties))
453
+ if alternatives:
454
+ notes.append("alternatives: " + "; ".join(alternatives))
455
+ return " · ".join(notes) if notes else "—"
456
+
457
+
458
+ def _status_badge(status: str) -> Text:
459
+ text = Text()
460
+ if status == "transitioned":
461
+ text.append("✓ transitioned", style="success")
462
+ elif status == "blocked":
463
+ text.append("✗ blocked", style="blocked")
464
+ else:
465
+ text.append("• no transition", style="amber")
466
+ return text
467
+
468
+
469
+ def _action_status(status: str) -> str:
470
+ if status in {"executed", "planned"}:
471
+ return f"[success]{status}[/]"
472
+ if status == "blocked":
473
+ return f"[amber]{status}[/]"
474
+ return f"[danger]{status}[/]"
475
+
476
+
477
+ def _status_border(status: str) -> str:
478
+ if status == "transitioned":
479
+ return "accent"
480
+ if status == "blocked":
481
+ return "danger"
482
+ return "amber"
483
+
484
+
485
+ def _join_names(values: list[Any]) -> str:
486
+ if not values:
487
+ return "—"
488
+ return " · ".join(str(value) for value in values)
489
+
490
+
491
+ def _humanize(value: str) -> str:
492
+ if not value:
493
+ return "—"
494
+ return value.replace("_", " ").replace("-", " ").strip().capitalize()
495
+
496
+
497
+ def _short_path(path: Path) -> str:
498
+ if path.name.startswith("tmp."):
499
+ return path.name
500
+ text = str(path)
501
+ home = str(Path.home())
502
+ if text.startswith(f"{home}/"):
503
+ text = f"~/{text[len(home) + 1:]}"
504
+ if len(text) <= 56:
505
+ return text
506
+ return f"…{text[-55:]}"
507
+
508
+
509
+ def _app_header(
510
+ title: str,
511
+ subtitle_items: Mapping[str, str],
512
+ *,
513
+ icon: str,
514
+ border_style: str,
515
+ console: Console,
516
+ ) -> None:
517
+ components = _cli_components()
518
+ components.app_header(
519
+ title,
520
+ dict(subtitle_items),
521
+ icon=icon,
522
+ border_style=border_style,
523
+ console=console,
524
+ )
525
+
526
+
527
+ def _stats_footer(
528
+ stats: Mapping[str, str | int | float],
529
+ *,
530
+ border_style: str,
531
+ console: Console,
532
+ ) -> None:
533
+ components = _cli_components()
534
+ components.stats_footer(
535
+ dict(stats),
536
+ border_style=border_style,
537
+ console=console,
538
+ )
539
+
540
+
541
+ def _cli_components() -> ModuleType:
542
+ return importlib.import_module("ellements.cli.components")
543
+
544
+
545
+ __all__ = [
546
+ "ColorMode",
547
+ "make_console",
548
+ "render_snapshot",
549
+ "render_step",
550
+ "render_validation",
551
+ ]
@@ -0,0 +1,25 @@
1
+ """Mermaid rendering for `MachineSpec`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .models import MachineSpec
6
+
7
+
8
+ def to_mermaid(spec: MachineSpec) -> str:
9
+ """Render a machine spec as a Mermaid state diagram."""
10
+ lines = ["stateDiagram-v2", f" [*] --> {spec.initial}"]
11
+ for state in spec.states.values():
12
+ if state.terminal:
13
+ lines.append(f" {state.name} --> [*]")
14
+ for transition in spec.transitions.values():
15
+ label = transition.name
16
+ if transition.weight is not None:
17
+ label = f"{label} ({transition.weight:g})"
18
+ if transition.kind == "recovery":
19
+ label = f"recovery: {label}"
20
+ lines.append(f" {transition.source} --> {transition.target}: {label}")
21
+ return "\n".join(lines) + "\n"
22
+
23
+
24
+ __all__ = ["to_mermaid"]
25
+
@@ -0,0 +1,12 @@
1
+ """Reporting, visualization, and export helpers."""
2
+
3
+ from .charts import create_chart_artifacts
4
+ from .html_generation import HTMLReportGenerator, MultiFormatReportExporter
5
+ from .visualization import ChartSerializer
6
+
7
+ __all__ = [
8
+ "ChartSerializer",
9
+ "HTMLReportGenerator",
10
+ "MultiFormatReportExporter",
11
+ "create_chart_artifacts",
12
+ ]