tnfr 4.5.0__py3-none-any.whl → 4.5.2__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.

Potentially problematic release.


This version of tnfr might be problematic. Click here for more details.

Files changed (78) hide show
  1. tnfr/__init__.py +91 -89
  2. tnfr/alias.py +546 -0
  3. tnfr/cache.py +578 -0
  4. tnfr/callback_utils.py +388 -0
  5. tnfr/cli/__init__.py +75 -0
  6. tnfr/cli/arguments.py +177 -0
  7. tnfr/cli/execution.py +288 -0
  8. tnfr/cli/utils.py +36 -0
  9. tnfr/collections_utils.py +300 -0
  10. tnfr/config.py +19 -28
  11. tnfr/constants/__init__.py +174 -0
  12. tnfr/constants/core.py +159 -0
  13. tnfr/constants/init.py +31 -0
  14. tnfr/constants/metric.py +110 -0
  15. tnfr/constants_glyphs.py +98 -0
  16. tnfr/dynamics/__init__.py +658 -0
  17. tnfr/dynamics/dnfr.py +733 -0
  18. tnfr/dynamics/integrators.py +267 -0
  19. tnfr/dynamics/sampling.py +31 -0
  20. tnfr/execution.py +201 -0
  21. tnfr/flatten.py +283 -0
  22. tnfr/gamma.py +302 -88
  23. tnfr/glyph_history.py +290 -0
  24. tnfr/grammar.py +285 -96
  25. tnfr/graph_utils.py +84 -0
  26. tnfr/helpers/__init__.py +71 -0
  27. tnfr/helpers/numeric.py +87 -0
  28. tnfr/immutable.py +178 -0
  29. tnfr/import_utils.py +228 -0
  30. tnfr/initialization.py +197 -0
  31. tnfr/io.py +246 -0
  32. tnfr/json_utils.py +162 -0
  33. tnfr/locking.py +37 -0
  34. tnfr/logging_utils.py +116 -0
  35. tnfr/metrics/__init__.py +41 -0
  36. tnfr/metrics/coherence.py +829 -0
  37. tnfr/metrics/common.py +151 -0
  38. tnfr/metrics/core.py +101 -0
  39. tnfr/metrics/diagnosis.py +234 -0
  40. tnfr/metrics/export.py +137 -0
  41. tnfr/metrics/glyph_timing.py +189 -0
  42. tnfr/metrics/reporting.py +148 -0
  43. tnfr/metrics/sense_index.py +120 -0
  44. tnfr/metrics/trig.py +181 -0
  45. tnfr/metrics/trig_cache.py +109 -0
  46. tnfr/node.py +214 -159
  47. tnfr/observers.py +126 -128
  48. tnfr/ontosim.py +134 -134
  49. tnfr/operators/__init__.py +420 -0
  50. tnfr/operators/jitter.py +203 -0
  51. tnfr/operators/remesh.py +485 -0
  52. tnfr/presets.py +46 -14
  53. tnfr/rng.py +254 -0
  54. tnfr/selector.py +210 -0
  55. tnfr/sense.py +284 -131
  56. tnfr/structural.py +207 -79
  57. tnfr/tokens.py +60 -0
  58. tnfr/trace.py +329 -94
  59. tnfr/types.py +43 -17
  60. tnfr/validators.py +70 -24
  61. tnfr/value_utils.py +59 -0
  62. tnfr-4.5.2.dist-info/METADATA +379 -0
  63. tnfr-4.5.2.dist-info/RECORD +67 -0
  64. tnfr/cli.py +0 -322
  65. tnfr/constants.py +0 -277
  66. tnfr/dynamics.py +0 -814
  67. tnfr/helpers.py +0 -264
  68. tnfr/main.py +0 -47
  69. tnfr/metrics.py +0 -597
  70. tnfr/operators.py +0 -525
  71. tnfr/program.py +0 -176
  72. tnfr/scenarios.py +0 -34
  73. tnfr-4.5.0.dist-info/METADATA +0 -109
  74. tnfr-4.5.0.dist-info/RECORD +0 -28
  75. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
  76. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
  77. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
  78. {tnfr-4.5.0.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
tnfr/callback_utils.py ADDED
@@ -0,0 +1,388 @@
1
+ """Callback registration and invocation helpers.
2
+
3
+ This module is thread-safe: all mutations of the callback registry stored in a
4
+ graph's ``G.graph`` are serialised using a process-wide lock obtained via
5
+ ``locking.get_lock("callbacks")``. Callback functions themselves execute
6
+ outside of the lock and must therefore be independently thread-safe if they
7
+ modify shared state.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+
13
+ from typing import Any, TypedDict
14
+ from enum import Enum
15
+ from collections import defaultdict, deque
16
+ from collections.abc import Callable, Mapping, Iterable
17
+
18
+ import traceback
19
+ import threading
20
+ from .logging_utils import get_logger
21
+ from .constants import DEFAULTS
22
+ from .locking import get_lock
23
+
24
+ from .trace import CallbackSpec
25
+ from .collections_utils import is_non_string_sequence
26
+
27
+ import networkx as nx # type: ignore[import-untyped]
28
+
29
+ __all__ = (
30
+ "CallbackEvent",
31
+ "CallbackManager",
32
+ "callback_manager",
33
+ "CallbackError",
34
+ )
35
+
36
+ logger = get_logger(__name__)
37
+
38
+
39
+ class CallbackEvent(str, Enum):
40
+ """Supported callback events."""
41
+
42
+ BEFORE_STEP = "before_step"
43
+ AFTER_STEP = "after_step"
44
+ ON_REMESH = "on_remesh"
45
+
46
+
47
+ class CallbackManager:
48
+ """Centralised registry and error tracking for callbacks."""
49
+
50
+ def __init__(self) -> None:
51
+ self._lock = get_lock("callbacks")
52
+ self._error_limit_lock = threading.Lock()
53
+ self._error_limit = 100
54
+ self._error_limit_cache = self._error_limit
55
+
56
+ # ------------------------------------------------------------------
57
+ # Error limit management
58
+ # ------------------------------------------------------------------
59
+ def get_callback_error_limit(self) -> int:
60
+ """Return the current callback error retention limit."""
61
+ with self._error_limit_lock:
62
+ return self._error_limit
63
+
64
+ def set_callback_error_limit(self, limit: int) -> int:
65
+ """Set the maximum number of callback errors retained."""
66
+ if limit < 1:
67
+ raise ValueError("limit must be positive")
68
+ with self._error_limit_lock:
69
+ previous = self._error_limit
70
+ self._error_limit = int(limit)
71
+ self._error_limit_cache = self._error_limit
72
+ return previous
73
+
74
+ # ------------------------------------------------------------------
75
+ # Registry helpers
76
+ # ------------------------------------------------------------------
77
+ def _record_callback_error(
78
+ self,
79
+ G: "nx.Graph",
80
+ event: str,
81
+ ctx: dict[str, Any],
82
+ spec: CallbackSpec,
83
+ err: Exception,
84
+ ) -> None:
85
+ """Log and store a callback error for later inspection."""
86
+
87
+ logger.exception("callback %r failed for %s: %s", spec.name, event, err)
88
+ limit = self._error_limit_cache
89
+ err_list = G.graph.setdefault(
90
+ "_callback_errors", deque[CallbackError](maxlen=limit)
91
+ )
92
+ if err_list.maxlen != limit:
93
+ err_list = deque[CallbackError](err_list, maxlen=limit)
94
+ G.graph["_callback_errors"] = err_list
95
+ error: CallbackError = {
96
+ "event": event,
97
+ "step": ctx.get("step"),
98
+ "error": repr(err),
99
+ "traceback": traceback.format_exc(),
100
+ "fn": _func_id(spec.func),
101
+ "name": spec.name,
102
+ }
103
+ err_list.append(error)
104
+
105
+ def _ensure_callbacks_nolock(self, G: "nx.Graph") -> CallbackRegistry:
106
+ cbs = G.graph.setdefault("callbacks", defaultdict(dict))
107
+ dirty: set[str] = set(G.graph.pop("_callbacks_dirty", ()))
108
+ return _validate_registry(G, cbs, dirty)
109
+
110
+ def _ensure_callbacks(self, G: "nx.Graph") -> CallbackRegistry:
111
+ with self._lock:
112
+ return self._ensure_callbacks_nolock(G)
113
+
114
+ def register_callback(
115
+ self,
116
+ G: "nx.Graph",
117
+ event: CallbackEvent | str,
118
+ func: Callback,
119
+ *,
120
+ name: str | None = None,
121
+ ) -> Callback:
122
+ """Register ``func`` as callback for ``event``."""
123
+
124
+ event = _normalize_event(event)
125
+ _ensure_known_event(event)
126
+ if not callable(func):
127
+ raise TypeError("func must be callable")
128
+ with self._lock:
129
+ cbs = self._ensure_callbacks_nolock(G)
130
+
131
+ cb_name = name or getattr(func, "__name__", None)
132
+ spec = CallbackSpec(cb_name, func)
133
+ existing_map = cbs[event]
134
+ strict = bool(
135
+ G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"])
136
+ )
137
+ key = _reconcile_callback(event, existing_map, spec, strict)
138
+
139
+ existing_map[key] = spec
140
+ dirty = G.graph.setdefault("_callbacks_dirty", set())
141
+ dirty.add(event)
142
+ return func
143
+
144
+ def invoke_callbacks(
145
+ self,
146
+ G: "nx.Graph",
147
+ event: CallbackEvent | str,
148
+ ctx: dict[str, Any] | None = None,
149
+ ) -> None:
150
+ """Invoke all callbacks registered for ``event`` with context ``ctx``."""
151
+
152
+ event = _normalize_event(event)
153
+ with self._lock:
154
+ cbs = dict(self._ensure_callbacks_nolock(G).get(event, {}))
155
+ strict = bool(
156
+ G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"])
157
+ )
158
+ if ctx is None:
159
+ ctx = {}
160
+ for spec in cbs.values():
161
+ try:
162
+ spec.func(G, ctx)
163
+ except (
164
+ RuntimeError,
165
+ ValueError,
166
+ TypeError,
167
+ ) as e:
168
+ with self._lock:
169
+ self._record_callback_error(G, event, ctx, spec, e)
170
+ if strict:
171
+ raise
172
+ except nx.NetworkXError as err:
173
+ with self._lock:
174
+ self._record_callback_error(G, event, ctx, spec, err)
175
+ logger.exception(
176
+ "callback %r raised NetworkXError for %s with ctx=%r",
177
+ spec.name,
178
+ event,
179
+ ctx,
180
+ )
181
+ raise
182
+
183
+
184
+ Callback = Callable[["nx.Graph", dict[str, Any]], None]
185
+ CallbackRegistry = dict[str, dict[str, "CallbackSpec"]]
186
+
187
+
188
+ class CallbackError(TypedDict):
189
+ """Metadata for a failed callback invocation."""
190
+
191
+ event: str
192
+ step: int | None
193
+ error: str
194
+ traceback: str
195
+ fn: str
196
+ name: str | None
197
+
198
+
199
+ def _func_id(fn: Callable[..., Any]) -> str:
200
+ """Return a deterministic identifier for ``fn``.
201
+
202
+ Combines the function's module and qualified name to avoid the
203
+ nondeterminism of ``repr(fn)`` which includes the memory address.
204
+ """
205
+ module = getattr(fn, "__module__", fn.__class__.__module__)
206
+ qualname = getattr(
207
+ fn,
208
+ "__qualname__",
209
+ getattr(fn, "__name__", fn.__class__.__qualname__),
210
+ )
211
+ return f"{module}.{qualname}"
212
+
213
+
214
+ def _validate_registry(
215
+ G: "nx.Graph", cbs: Any, dirty: set[str]
216
+ ) -> CallbackRegistry:
217
+ """Validate and normalise the callback registry.
218
+
219
+ ``cbs`` is coerced to a ``defaultdict(dict)`` and any events listed in
220
+ ``dirty`` are rebuilt using :func:`_normalize_callbacks`. Unknown events are
221
+ removed. The cleaned registry is stored back on the graph and returned.
222
+ """
223
+
224
+ if not isinstance(cbs, Mapping):
225
+ logger.warning(
226
+ "Invalid callbacks registry on graph; resetting to empty",
227
+ )
228
+ cbs = defaultdict(dict)
229
+ elif not isinstance(cbs, defaultdict) or cbs.default_factory is not dict:
230
+ cbs = defaultdict(
231
+ dict,
232
+ {
233
+ event: _normalize_callbacks(entries)
234
+ for event, entries in dict(cbs).items()
235
+ if _is_known_event(event)
236
+ },
237
+ )
238
+ else:
239
+ for event in dirty:
240
+ if _is_known_event(event):
241
+ cbs[event] = _normalize_callbacks(cbs.get(event))
242
+ else:
243
+ cbs.pop(event, None)
244
+
245
+ G.graph["callbacks"] = cbs
246
+ return cbs
247
+
248
+
249
+
250
+
251
+ def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]:
252
+ """Return ``entries`` normalised into a callback mapping."""
253
+ if isinstance(entries, Mapping):
254
+ entries_iter = entries.values()
255
+ elif isinstance(entries, Iterable) and not isinstance(entries, (str, bytes, bytearray)):
256
+ entries_iter = entries
257
+ else:
258
+ return {}
259
+
260
+ new_map: dict[str, CallbackSpec] = {}
261
+ for entry in entries_iter:
262
+ spec = _normalize_callback_entry(entry)
263
+ if spec is None:
264
+ continue
265
+ key = spec.name or _func_id(spec.func)
266
+ new_map[key] = spec
267
+ return new_map
268
+
269
+
270
+ def _normalize_event(event: CallbackEvent | str) -> str:
271
+ """Return ``event`` as a string."""
272
+ return event.value if isinstance(event, CallbackEvent) else str(event)
273
+
274
+
275
+ def _is_known_event(event: str) -> bool:
276
+ """Return ``True`` when ``event`` matches a declared :class:`CallbackEvent`."""
277
+
278
+ try:
279
+ CallbackEvent(event)
280
+ except ValueError:
281
+ return False
282
+ else:
283
+ return True
284
+
285
+
286
+ def _ensure_known_event(event: str) -> None:
287
+ """Raise :class:`ValueError` when ``event`` is not a known callback."""
288
+
289
+ try:
290
+ CallbackEvent(event)
291
+ except ValueError as exc: # pragma: no cover - defensive branch
292
+ raise ValueError(f"Unknown event: {event}") from exc
293
+
294
+
295
+ def _normalize_callback_entry(entry: Any) -> "CallbackSpec | None":
296
+ """Normalize a callback specification.
297
+
298
+ Supported formats
299
+ -----------------
300
+ * :class:`CallbackSpec` instances (returned unchanged).
301
+ * Sequences ``(name: str, func: Callable)`` such as lists, tuples or other
302
+ iterables.
303
+ * Bare callables ``func`` whose name is taken from ``func.__name__``.
304
+
305
+ ``None`` is returned when ``entry`` does not match any of the accepted
306
+ formats. The original ``entry`` is never mutated. Sequence inputs are
307
+ converted to ``tuple`` before validation to support generators; the
308
+ materialization consumes the iterable and failure results in ``None``.
309
+ """
310
+
311
+ if isinstance(entry, CallbackSpec):
312
+ return entry
313
+ elif is_non_string_sequence(entry):
314
+ try:
315
+ entry = tuple(entry)
316
+ except TypeError:
317
+ return None
318
+ if len(entry) != 2:
319
+ return None
320
+ name, fn = entry
321
+ if not isinstance(name, str) or not callable(fn):
322
+ return None
323
+ return CallbackSpec(name, fn)
324
+ elif callable(entry):
325
+ name = getattr(entry, "__name__", None)
326
+ return CallbackSpec(name, entry)
327
+ else:
328
+ return None
329
+
330
+
331
+
332
+ def _reconcile_callback(
333
+ event: str,
334
+ existing_map: dict[str, CallbackSpec],
335
+ spec: CallbackSpec,
336
+ strict: bool,
337
+ ) -> str:
338
+ """Reconcile ``spec`` with ``existing_map``.
339
+
340
+ Ensures that callbacks remain unique by explicit name or function identity.
341
+ When a name collision occurs with a different function, ``strict`` controls
342
+ whether a :class:`ValueError` is raised or a warning is logged.
343
+
344
+ Parameters
345
+ ----------
346
+ event:
347
+ Event under which ``spec`` will be registered. Only used for messages.
348
+ existing_map:
349
+ Current mapping of callbacks for ``event``.
350
+ spec:
351
+ Callback specification being registered.
352
+ strict:
353
+ Whether to raise on name collisions instead of logging a warning.
354
+
355
+ Returns
356
+ -------
357
+ str
358
+ Key under which ``spec`` should be stored in ``existing_map``.
359
+ """
360
+
361
+ key = spec.name or _func_id(spec.func)
362
+
363
+ if spec.name is not None:
364
+ existing_spec = existing_map.get(key)
365
+ if existing_spec is not None and existing_spec.func is not spec.func:
366
+ msg = f"Callback {spec.name!r} already registered for {event}"
367
+ if strict:
368
+ raise ValueError(msg)
369
+ logger.warning(msg)
370
+
371
+ # Remove existing entries under the same key and any other using the same
372
+ # function identity to avoid duplicates.
373
+ existing_map.pop(key, None)
374
+ fn_key = next((k for k, s in existing_map.items() if s.func is spec.func), None)
375
+ if fn_key is not None:
376
+ existing_map.pop(fn_key, None)
377
+
378
+ return key
379
+
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # Default manager instance and convenience wrappers
383
+ # ---------------------------------------------------------------------------
384
+
385
+ callback_manager = CallbackManager()
386
+
387
+
388
+
tnfr/cli/__init__.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from .arguments import (
9
+ add_common_args,
10
+ add_grammar_args,
11
+ add_grammar_selector_args,
12
+ add_history_export_args,
13
+ add_canon_toggle,
14
+ _add_run_parser,
15
+ _add_sequence_parser,
16
+ _add_metrics_parser,
17
+ )
18
+ from .execution import (
19
+ build_basic_graph,
20
+ apply_cli_config,
21
+ register_callbacks_and_observer,
22
+ run_program,
23
+ resolve_program,
24
+ )
25
+ from ..logging_utils import get_logger
26
+ from .. import __version__
27
+
28
+ logger = get_logger(__name__)
29
+
30
+ __all__ = (
31
+ "main",
32
+ "add_common_args",
33
+ "add_grammar_args",
34
+ "add_grammar_selector_args",
35
+ "add_history_export_args",
36
+ "add_canon_toggle",
37
+ "build_basic_graph",
38
+ "apply_cli_config",
39
+ "register_callbacks_and_observer",
40
+ "run_program",
41
+ "resolve_program",
42
+ )
43
+
44
+
45
+ def main(argv: Optional[list[str]] = None) -> int:
46
+ logging.basicConfig(
47
+ level=logging.INFO, format="%(message)s", stream=sys.stdout, force=True
48
+ )
49
+
50
+ p = argparse.ArgumentParser(
51
+ prog="tnfr",
52
+ formatter_class=argparse.RawDescriptionHelpFormatter,
53
+ epilog=(
54
+ "Ejemplo: tnfr sequence --sequence-file secuencia.json\n"
55
+ "secuencia.json:\n"
56
+ '[\n {"WAIT": 1},\n {"TARGET": "A"}\n]'
57
+ ),
58
+ )
59
+ p.add_argument(
60
+ "--version", action="store_true", help="muestra versión y sale"
61
+ )
62
+ sub = p.add_subparsers(dest="cmd")
63
+
64
+ _add_run_parser(sub)
65
+ _add_sequence_parser(sub)
66
+ _add_metrics_parser(sub)
67
+
68
+ args = p.parse_args(argv)
69
+ if args.version:
70
+ logger.info("%s", __version__)
71
+ return 0
72
+ if not hasattr(args, "func"):
73
+ p.print_help()
74
+ return 1
75
+ return int(args.func(args))
tnfr/cli/arguments.py ADDED
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Any
5
+
6
+ from ..gamma import GAMMA_REGISTRY
7
+ from .utils import spec
8
+
9
+
10
+ GRAMMAR_ARG_SPECS = (
11
+ spec("--grammar.enabled", action=argparse.BooleanOptionalAction),
12
+ spec("--grammar.zhir_requires_oz_window", type=int),
13
+ spec("--grammar.zhir_dnfr_min", type=float),
14
+ spec("--grammar.thol_min_len", type=int),
15
+ spec("--grammar.thol_max_len", type=int),
16
+ spec("--grammar.thol_close_dnfr", type=float),
17
+ spec("--grammar.si_high", type=float),
18
+ spec("--glyph.hysteresis_window", type=int),
19
+ )
20
+
21
+
22
+ # Especificaciones para opciones relacionadas con el histórico
23
+ HISTORY_ARG_SPECS = (
24
+ spec("--save-history", type=str),
25
+ spec("--export-history-base", type=str),
26
+ spec("--export-format", choices=["csv", "json"], default="json"),
27
+ )
28
+
29
+
30
+ # Argumentos comunes a los subcomandos
31
+ COMMON_ARG_SPECS = (
32
+ spec("--nodes", type=int, default=24),
33
+ spec("--topology", choices=["ring", "complete", "erdos"], default="ring"),
34
+ spec("--seed", type=int, default=1),
35
+ spec(
36
+ "--p",
37
+ type=float,
38
+ help="Probabilidad de arista si topology=erdos",
39
+ ),
40
+ spec("--observer", action="store_true", help="Adjunta observador estándar"),
41
+ spec("--config", type=str),
42
+ spec("--dt", type=float),
43
+ spec("--integrator", choices=["euler", "rk4"]),
44
+ spec("--remesh-mode", choices=["knn", "mst", "community"]),
45
+ spec("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none"),
46
+ spec("--gamma-beta", type=float, default=0.0),
47
+ spec("--gamma-R0", type=float, default=0.0),
48
+ )
49
+
50
+
51
+ def add_arg_specs(parser: argparse._ActionsContainer, specs) -> None:
52
+ """Register arguments from ``specs`` on ``parser``."""
53
+ for opt, kwargs in specs:
54
+ parser.add_argument(opt, **kwargs)
55
+
56
+
57
+ def _args_to_dict(args: argparse.Namespace, prefix: str) -> dict[str, Any]:
58
+ """Extract arguments matching a prefix."""
59
+ return {
60
+ k.removeprefix(prefix): v
61
+ for k, v in vars(args).items()
62
+ if k.startswith(prefix) and v is not None
63
+ }
64
+
65
+
66
+ def add_common_args(parser: argparse.ArgumentParser) -> None:
67
+ """Add arguments shared across subcommands."""
68
+ add_arg_specs(parser, COMMON_ARG_SPECS)
69
+
70
+
71
+ def add_grammar_args(parser: argparse.ArgumentParser) -> None:
72
+ """Add grammar and glyph hysteresis options."""
73
+ group = parser.add_argument_group("Grammar")
74
+ add_arg_specs(group, GRAMMAR_ARG_SPECS)
75
+
76
+
77
+ def add_grammar_selector_args(parser: argparse.ArgumentParser) -> None:
78
+ """Add grammar options and glyph selector."""
79
+ add_grammar_args(parser)
80
+ parser.add_argument(
81
+ "--selector", choices=["basic", "param"], default="basic"
82
+ )
83
+
84
+
85
+ def add_history_export_args(parser: argparse.ArgumentParser) -> None:
86
+ """Add arguments to save or export history."""
87
+ add_arg_specs(parser, HISTORY_ARG_SPECS)
88
+
89
+
90
+ def add_canon_toggle(parser: argparse.ArgumentParser) -> None:
91
+ """Add option to disable canonical grammar."""
92
+ parser.add_argument(
93
+ "--no-canon",
94
+ dest="grammar_canon",
95
+ action="store_false",
96
+ default=True,
97
+ help="Desactiva gramática canónica",
98
+ )
99
+
100
+
101
+ def _add_run_parser(sub: argparse._SubParsersAction) -> None:
102
+ """Configure the ``run`` subcommand."""
103
+ from .execution import cmd_run, DEFAULT_SUMMARY_SERIES_LIMIT
104
+
105
+ p_run = sub.add_parser(
106
+ "run",
107
+ help=(
108
+ "Correr escenario libre o preset y opcionalmente exportar history"
109
+ ),
110
+ )
111
+ add_common_args(p_run)
112
+ p_run.add_argument("--steps", type=int, default=100)
113
+ add_canon_toggle(p_run)
114
+ add_grammar_selector_args(p_run)
115
+ add_history_export_args(p_run)
116
+ p_run.add_argument("--preset", type=str, default=None)
117
+ p_run.add_argument("--sequence-file", type=str, default=None)
118
+ p_run.add_argument("--summary", action="store_true")
119
+ p_run.add_argument(
120
+ "--summary-limit",
121
+ type=int,
122
+ default=DEFAULT_SUMMARY_SERIES_LIMIT,
123
+ help=(
124
+ "Número máximo de muestras por serie en el resumen (<=0 para"
125
+ " desactivar el recorte)"
126
+ ),
127
+ )
128
+ p_run.set_defaults(func=cmd_run)
129
+
130
+
131
+ def _add_sequence_parser(sub: argparse._SubParsersAction) -> None:
132
+ """Configure the ``sequence`` subcommand."""
133
+ from .execution import cmd_sequence
134
+
135
+ p_seq = sub.add_parser(
136
+ "sequence",
137
+ help="Ejecutar una secuencia (preset o YAML/JSON)",
138
+ formatter_class=argparse.RawDescriptionHelpFormatter,
139
+ epilog=(
140
+ "Ejemplo de secuencia JSON:\n"
141
+ "[\n"
142
+ ' "A",\n'
143
+ ' {"WAIT": 1},\n'
144
+ ' {"THOL": {"body": ["A", {"WAIT": 2}], "repeat": 2}}\n'
145
+ "]"
146
+ ),
147
+ )
148
+ add_common_args(p_seq)
149
+ p_seq.add_argument("--preset", type=str, default=None)
150
+ p_seq.add_argument("--sequence-file", type=str, default=None)
151
+ add_history_export_args(p_seq)
152
+ add_grammar_args(p_seq)
153
+ p_seq.set_defaults(func=cmd_sequence)
154
+
155
+
156
+ def _add_metrics_parser(sub: argparse._SubParsersAction) -> None:
157
+ """Configure the ``metrics`` subcommand."""
158
+ from .execution import cmd_metrics
159
+
160
+ p_met = sub.add_parser(
161
+ "metrics", help="Correr breve y volcar métricas clave"
162
+ )
163
+ add_common_args(p_met)
164
+ p_met.add_argument("--steps", type=int, default=None)
165
+ add_canon_toggle(p_met)
166
+ add_grammar_selector_args(p_met)
167
+ p_met.add_argument("--save", type=str, default=None)
168
+ p_met.add_argument(
169
+ "--summary-limit",
170
+ type=int,
171
+ default=None,
172
+ help=(
173
+ "Número máximo de muestras por serie en el resumen (<=0 para"
174
+ " desactivar el recorte)"
175
+ ),
176
+ )
177
+ p_met.set_defaults(func=cmd_metrics)