detectkit 0.43.0__tar.gz → 0.44.0__tar.gz

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 (138) hide show
  1. {detectkit-0.43.0/detectkit.egg-info → detectkit-0.44.0}/PKG-INFO +1 -1
  2. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/autotuner.py +30 -2
  4. detectkit-0.44.0/detectkit/cli/_output.py +112 -0
  5. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/autotune.md +6 -4
  6. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/cli.md +6 -4
  7. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/autotune.py +11 -30
  8. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/tuning/assets/tune.js +3 -3
  9. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/tuning/server.py +128 -3
  10. {detectkit-0.43.0 → detectkit-0.44.0/detectkit.egg-info}/PKG-INFO +1 -1
  11. detectkit-0.43.0/detectkit/cli/_output.py +0 -50
  12. {detectkit-0.43.0 → detectkit-0.44.0}/LICENSE +0 -0
  13. {detectkit-0.43.0 → detectkit-0.44.0}/MANIFEST.in +0 -0
  14. {detectkit-0.43.0 → detectkit-0.44.0}/README.md +0 -0
  15. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/__init__.py +0 -0
  16. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/__init__.py +0 -0
  17. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/base.py +0 -0
  18. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/branding.py +0 -0
  19. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/email.py +0 -0
  20. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/factory.py +0 -0
  21. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/mattermost.py +0 -0
  22. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/slack.py +0 -0
  23. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/telegram.py +0 -0
  24. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/channels/webhook.py +0 -0
  25. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  26. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  27. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  28. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  29. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  30. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  31. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  32. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  33. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  34. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/__init__.py +0 -0
  35. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/_base.py +0 -0
  36. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/_types.py +0 -0
  37. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/config_emitter.py +0 -0
  38. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/crossval.py +0 -0
  39. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/detector_select.py +0 -0
  40. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/distribution.py +0 -0
  41. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/grid_search.py +0 -0
  42. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/labels.py +0 -0
  43. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/result.py +0 -0
  44. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/runner.py +0 -0
  45. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/scoring.py +0 -0
  46. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/seasonality_search.py +0 -0
  47. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/settings.py +0 -0
  48. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/autotune/window_select.py +0 -0
  49. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/__init__.py +0 -0
  50. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  51. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  52. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  53. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  54. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  55. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  56. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  57. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  58. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  59. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  60. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/__init__.py +0 -0
  61. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/clean.py +0 -0
  62. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/init.py +0 -0
  63. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/init_claude.py +0 -0
  64. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/run.py +0 -0
  65. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/test_alert.py +0 -0
  66. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/tune.py +0 -0
  67. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/commands/unlock.py +0 -0
  68. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/cli/main.py +0 -0
  69. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/config/__init__.py +0 -0
  70. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/config/metric_config.py +0 -0
  71. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/config/profile.py +0 -0
  72. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/config/project_config.py +0 -0
  73. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/config/validator.py +0 -0
  74. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/core/__init__.py +0 -0
  75. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/core/interval.py +0 -0
  76. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/core/models.py +0 -0
  77. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/__init__.py +0 -0
  78. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/_sql_manager.py +0 -0
  79. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/clickhouse_manager.py +0 -0
  80. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/__init__.py +0 -0
  81. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  82. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  83. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_base.py +0 -0
  84. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  85. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_detections.py +0 -0
  86. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  87. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  88. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_schema.py +0 -0
  89. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  90. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/internal_tables/manager.py +0 -0
  91. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/manager.py +0 -0
  92. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/mysql_manager.py +0 -0
  93. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/postgres_manager.py +0 -0
  94. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/database/tables.py +0 -0
  95. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/__init__.py +0 -0
  96. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/base.py +0 -0
  97. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/factory.py +0 -0
  98. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/seasonality.py +0 -0
  99. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/statistical/__init__.py +0 -0
  100. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  101. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/statistical/iqr.py +0 -0
  102. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/statistical/mad.py +0 -0
  103. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  104. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/detectors/statistical/zscore.py +0 -0
  105. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/loaders/__init__.py +0 -0
  106. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/loaders/metric_loader.py +0 -0
  107. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/loaders/query_template.py +0 -0
  108. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/__init__.py +0 -0
  109. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/error_dispatch.py +0 -0
  110. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  111. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  112. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  113. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  114. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  115. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  116. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  117. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/reporting/__init__.py +0 -0
  118. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/reporting/assets/report.js +0 -0
  119. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/reporting/builder.py +0 -0
  120. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/reporting/html_report.py +0 -0
  121. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/tuning/__init__.py +0 -0
  122. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/tuning/config_writer.py +0 -0
  123. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/tuning/html.py +0 -0
  124. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/tuning/payload.py +0 -0
  125. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/utils/__init__.py +0 -0
  126. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/utils/datetime_utils.py +0 -0
  127. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/utils/env_interpolation.py +0 -0
  128. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/utils/json_utils.py +0 -0
  129. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit/utils/stats.py +0 -0
  130. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit.egg-info/SOURCES.txt +0 -0
  131. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit.egg-info/dependency_links.txt +0 -0
  132. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit.egg-info/entry_points.txt +0 -0
  133. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit.egg-info/requires.txt +0 -0
  134. {detectkit-0.43.0 → detectkit-0.44.0}/detectkit.egg-info/top_level.txt +0 -0
  135. {detectkit-0.43.0 → detectkit-0.44.0}/pyproject.toml +0 -0
  136. {detectkit-0.43.0 → detectkit-0.44.0}/requirements.txt +0 -0
  137. {detectkit-0.43.0 → detectkit-0.44.0}/setup.cfg +0 -0
  138. {detectkit-0.43.0 → detectkit-0.44.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.43.0
3
+ Version: 0.44.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
4
4
  A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
5
5
  """
6
6
 
7
- __version__ = "0.43.0"
7
+ __version__ = "0.44.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -6,7 +6,9 @@ command handles loading, persistence, config emission and candidate cleanup.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from collections.abc import Callable
9
+ import contextlib
10
+ import logging
11
+ from collections.abc import Callable, Iterator
10
12
  from datetime import datetime, timedelta
11
13
  from typing import Any
12
14
 
@@ -27,6 +29,31 @@ from detectkit.detectors.factory import DetectorFactory
27
29
 
28
30
  _ALERT_WINDOW_GRID = (1, 2, 3, 4, 5)
29
31
 
32
+ # The windowed detectors emit a one-time "seasonality group can't fill this
33
+ # window → falls back to global" warning per *instance*. A tune builds dozens of
34
+ # throwaway candidate detectors, so that warning would flood the terminal and
35
+ # bury the structured decision log. The engine already surfaces an under-fill of
36
+ # the *chosen* seasonality as a `window` advisory in its own log, so the raw
37
+ # per-candidate warnings are pure noise here.
38
+ _WINDOWED_LOGGER = "detectkit.detectors.statistical._windowed"
39
+
40
+
41
+ @contextlib.contextmanager
42
+ def _quiet_per_candidate_warnings() -> Iterator[None]:
43
+ """Silence the windowed detectors' per-instance under-fill warning for a tune.
44
+
45
+ Scoped + restored, so a real ``dtk run`` (which builds one detector and *does*
46
+ want the warning) is unaffected — only the candidate sweep inside the engine
47
+ is quieted.
48
+ """
49
+ logger = logging.getLogger(_WINDOWED_LOGGER)
50
+ prev_level = logger.level
51
+ logger.setLevel(logging.ERROR)
52
+ try:
53
+ yield
54
+ finally:
55
+ logger.setLevel(prev_level)
56
+
30
57
 
31
58
  def _ts_to_dt(ts64: np.datetime64) -> datetime:
32
59
  ms = int(ts64.astype("datetime64[ms]").astype(np.int64))
@@ -196,4 +223,5 @@ def run_autotune_engine(
196
223
  settings=settings,
197
224
  on_stage=on_stage,
198
225
  )
199
- return tuner.tune()
226
+ with _quiet_per_candidate_warnings():
227
+ return tuner.tune()
@@ -0,0 +1,112 @@
1
+ """Shared CLI output helpers so every command renders in one house style.
2
+
3
+ Mirrors the load → detect → alert pipeline's tree look (``┌─ / │ / └─``) used by
4
+ ``dtk run`` so the maintenance commands (``dtk clean``, ``dtk unlock``) match it
5
+ instead of each inventing its own formatting.
6
+
7
+ House conventions:
8
+ - A metric *with* something to report is a tree: a cyan-bold ``┌─ <name>``
9
+ header followed by one child line per item (``│ `` for all but the last,
10
+ ``└─ `` for the last).
11
+ - A metric with *nothing* to do is a single ``•`` line.
12
+ - A per-metric error is a red ``✗`` line (to stderr).
13
+ - The final summary is a cyan-bold ``Done. …`` line.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Callable
19
+
20
+ import click
21
+
22
+ # Autotune-engine stage name → display title for the streamed run-log tree.
23
+ # Most stages render as ``stage.upper()``; only the two underscored names need a
24
+ # space so the header reads cleanly (``DETECTOR SELECT`` / ``GRID SEARCH``).
25
+ AUTOTUNE_STAGE_TITLES = {
26
+ "labels": "LABELS",
27
+ "seasonality": "SEASONALITY",
28
+ "detector_select": "DETECTOR SELECT",
29
+ "grid_search": "GRID SEARCH",
30
+ "window": "WINDOW",
31
+ }
32
+
33
+
34
+ def echo_block(
35
+ title: str,
36
+ children: list[str],
37
+ *,
38
+ warnings: list[str] | None = None,
39
+ echo: Callable[[str], None] = click.echo,
40
+ ) -> None:
41
+ """Print a cyan-bold ``┌─ title`` header with ``│``/``└─`` child lines.
42
+
43
+ The injectable core of the house tree style (``dtk run``'s load/detect/alert
44
+ blocks): ``warnings`` render as yellow ``│`` continuation lines above the
45
+ children, the last child gets the ``└─`` elbow. ``echo`` defaults to
46
+ ``click.echo`` but can be any line sink, so the tune server can stream the
47
+ same blocks through its own output callback. ``children`` must be non-empty.
48
+ """
49
+ echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
50
+ for warning in warnings or []:
51
+ echo(click.style(f" │ ⚠ {warning}", fg="yellow", bold=True))
52
+ last = len(children) - 1
53
+ for i, child in enumerate(children):
54
+ prefix = " └─ " if i == last else " │ "
55
+ echo(f"{prefix}{child}")
56
+
57
+
58
+ def echo_tree(name: str, children: list[str], *, warnings: list[str] | None = None) -> None:
59
+ """Print a ``┌─ name`` header with ``│``/``└─`` child lines.
60
+
61
+ ``warnings`` (if any) are rendered as yellow ``│`` continuation lines above
62
+ the children. ``children`` must be non-empty (a metric with no items should
63
+ use :func:`echo_noop` instead). Thin wrapper over :func:`echo_block` (the
64
+ CLI-default ``click.echo`` sink).
65
+ """
66
+ echo_block(name, children, warnings=warnings)
67
+
68
+
69
+ class StageLogRenderer:
70
+ """Stream ``(stage, line)`` engine progress as the run-log tree.
71
+
72
+ Opens a cyan-bold ``┌─ TITLE`` header the first time each stage appears and
73
+ prints every subsequent line for that stage as a ``│ `` child — the same
74
+ look as ``dtk run``'s load/detect/alert blocks. ``titles`` maps an engine
75
+ stage name to its display title (unmapped names fall back to ``upper()``);
76
+ ``echo`` is injectable so one renderer drives both the CLI (``click.echo``)
77
+ and the ``dtk tune`` server (its own output callback). Build a fresh renderer
78
+ per run so the first stage re-opens its header.
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ *,
84
+ titles: dict[str, str] | None = None,
85
+ echo: Callable[[str], None] = click.echo,
86
+ ) -> None:
87
+ self._open: str | None = None
88
+ self._titles = titles or {}
89
+ self._echo = echo
90
+
91
+ def __call__(self, stage: str, line: str) -> None:
92
+ if self._open != stage:
93
+ title = self._titles.get(stage, stage.upper())
94
+ self._echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
95
+ self._open = stage
96
+ self._echo(f" │ {line}")
97
+
98
+
99
+ def echo_noop(name: str, reason: str) -> None:
100
+ """A metric with nothing to do — a single ``•`` line."""
101
+ click.echo(f" • {name}: {reason}")
102
+
103
+
104
+ def echo_error(name: str, message: str) -> None:
105
+ """A per-metric failure — a red ``✗`` line on stderr."""
106
+ click.echo(click.style(f" ✗ {name}: {message}", fg="red"), err=True)
107
+
108
+
109
+ def echo_done(summary: str) -> None:
110
+ """The closing ``Done. …`` summary (cyan, bold), preceded by a blank line."""
111
+ click.echo()
112
+ click.echo(click.style(f"Done. {summary}", fg="cyan", bold=True))
@@ -21,10 +21,12 @@ same windowed detectors and `detector_id` identity). The fastest path is the
21
21
  > **You can also run this engine *inside* `dtk tune`** — its **Autotune** mode runs
22
22
  > the same search **server-side** (using the incidents you've marked as ground
23
23
  > truth), re-seeds the knobs with the winner, and lets you **Apply** in place. That's
24
- > the same computation as this command; the difference is it edits the metric YAML in
25
- > place (advisory no run record / `__tuned_<id>.yml` / persisted detections) rather
26
- > than emitting a new tuned file. Reach for `dtk autotune` when you want the audited
27
- > run + tuned file + persisted winner detections.
24
+ > the same computation as this command, with two differences: it tunes on the
25
+ > **window currently shown** in the cockpit (the **Points shown** trim the series
26
+ > you see and score) rather than the full capped history, and it edits the metric YAML
27
+ > in place (advisory — no run record / `__tuned_<id>.yml` / persisted detections)
28
+ > rather than emitting a new tuned file. Reach for `dtk autotune` when you want the
29
+ > audited run + tuned file + persisted winner detections.
28
30
 
29
31
  ## What it searches
30
32
 
@@ -109,10 +109,12 @@ anomalies** — loop a cloud of anomaly dots, each consecutive run, gaps bridged
109
109
  `consecutive_anomalies`, becomes one span — or **Threshold capture** every span past
110
110
  a horizontal line, each widened to a full interval so the alert lands inside; the
111
111
  painted window saves as `capture_windows`), and **Autotune** (**Run autotune**
112
- launches the **real** `dtk autotune` engine **server-side** over the metric's full
113
- history, using your marked incidents as ground truth, then **re-seeds every knob**
114
- with the winner and shows the score + decision log; the band leads like Tune). The
115
- Autotune mode is **advisory** it computes + re-seeds only and persists nothing (no
112
+ launches the **real** `dtk autotune` engine **server-side** over the **window
113
+ currently shown** (the **Points shown** trim the same series you see and score, not
114
+ the full history), using your marked incidents as ground truth, then **re-seeds every
115
+ knob** with the winner and shows the score + decision log; the band leads like Tune;
116
+ each run also streams a structured `LABELS → … → RESULT` log to the terminal, like
117
+ `dtk run`). The Autotune mode is **advisory** — it computes + re-seeds only and persists nothing (no
116
118
  run record / `__tuned_<id>.yml` / detections, so `dtk tune` stays lock-free); review
117
119
  the band and **Apply** to write it back. It honours the metric's `autotune:` block,
118
120
  is supervised when incidents are marked (also picks `consecutive_anomalies`) else
@@ -31,7 +31,14 @@ from detectkit.autotune.labels import (
31
31
  parse_incident_labels,
32
32
  )
33
33
  from detectkit.autotune.runner import build_settings, cap_history, resolve_scoring
34
- from detectkit.cli._output import echo_done, echo_error, echo_noop
34
+ from detectkit.cli._output import (
35
+ AUTOTUNE_STAGE_TITLES,
36
+ StageLogRenderer,
37
+ echo_block,
38
+ echo_done,
39
+ echo_error,
40
+ echo_noop,
41
+ )
35
42
  from detectkit.cli.commands.run import find_project_root, parse_date, select_metrics
36
43
  from detectkit.config.metric_config import AutoTuneConfig, MetricConfig
37
44
  from detectkit.config.profile import ProfilesConfig
@@ -41,28 +48,6 @@ from detectkit.detectors.base import DetectionResult
41
48
  from detectkit.detectors.factory import DetectorFactory
42
49
  from detectkit.utils.json_utils import json_dumps_sorted
43
50
 
44
- _STAGE_TITLES = {
45
- "labels": "LABELS",
46
- "seasonality": "SEASONALITY",
47
- "detector_select": "DETECTOR SELECT",
48
- "grid_search": "GRID SEARCH",
49
- "window": "WINDOW",
50
- }
51
-
52
-
53
- class _StageRenderer:
54
- """Streams engine progress as the run-log tree (header per stage + │ lines)."""
55
-
56
- def __init__(self) -> None:
57
- self._open: str | None = None
58
-
59
- def __call__(self, stage: str, line: str) -> None:
60
- if self._open != stage:
61
- title = _STAGE_TITLES.get(stage, stage.upper())
62
- click.echo(click.style(f" ┌─ {title}", fg="cyan", bold=True))
63
- self._open = stage
64
- click.echo(f" │ {line}")
65
-
66
51
 
67
52
  def _results_to_batch(results: list[DetectionResult]) -> dict[str, np.ndarray]:
68
53
  """Convert detect() output into the ``save_detections`` batch shape."""
@@ -358,7 +343,7 @@ def _tune_one(
358
343
  ground_truth=ground_truth,
359
344
  interval_seconds=interval_seconds,
360
345
  settings=settings,
361
- on_stage=_StageRenderer(),
346
+ on_stage=StageLogRenderer(titles=AUTOTUNE_STAGE_TITLES),
362
347
  )
363
348
 
364
349
  run_id = compute_run_id(result)
@@ -434,9 +419,7 @@ def _finalize(
434
419
 
435
420
  if dry_run:
436
421
  children.append("dry-run: no config written, no detections persisted")
437
- click.echo(click.style(" ┌─ RESULT", fg="cyan", bold=True))
438
- for i, child in enumerate(children):
439
- click.echo(f" {'└─' if i == len(children) - 1 else '│ '} {child}")
422
+ echo_block("RESULT", children)
440
423
  return
441
424
 
442
425
  # Persist the run record (the audit trail).
@@ -518,9 +501,7 @@ def _finalize(
518
501
  f"persisted winner, pruned {pruned} superseded run(s)"
519
502
  )
520
503
  children.append(f"Re-run with: dtk run --select {name}__tuned_{run_id}")
521
- click.echo(click.style(" ┌─ RESULT", fg="cyan", bold=True))
522
- for i, child in enumerate(children):
523
- click.echo(f" {'└─' if i == len(children) - 1 else '│ '} {child}")
504
+ echo_block("RESULT", children)
524
505
 
525
506
 
526
507
  def _prune_prior_winners(