detectkit 0.55.2__tar.gz → 0.56.1__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 (157) hide show
  1. {detectkit-0.55.2/detectkit.egg-info → detectkit-0.56.1}/PKG-INFO +1 -1
  2. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/__init__.py +1 -1
  3. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/CLAUDE.section.md +2 -1
  4. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/rules/cli.md +16 -6
  5. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/rules/metrics.md +2 -1
  6. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +10 -3
  7. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/clean.py +4 -11
  8. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/factory.py +23 -0
  9. detectkit-0.56.1/detectkit/ui/assets/ui.js +538 -0
  10. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/ui/jobs.py +6 -5
  11. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/ui/overview.py +14 -11
  12. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/ui/server.py +111 -6
  13. {detectkit-0.55.2 → detectkit-0.56.1/detectkit.egg-info}/PKG-INFO +1 -1
  14. detectkit-0.55.2/detectkit/ui/assets/ui.js +0 -518
  15. {detectkit-0.55.2 → detectkit-0.56.1}/LICENSE +0 -0
  16. {detectkit-0.55.2 → detectkit-0.56.1}/MANIFEST.in +0 -0
  17. {detectkit-0.55.2 → detectkit-0.56.1}/README.md +0 -0
  18. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/__init__.py +0 -0
  19. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/__init__.py +0 -0
  20. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/base.py +0 -0
  21. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/branding.py +0 -0
  22. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/email.py +0 -0
  23. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/factory.py +0 -0
  24. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/mattermost.py +0 -0
  25. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/slack.py +0 -0
  26. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/telegram.py +0 -0
  27. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/channels/webhook.py +0 -0
  28. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/__init__.py +0 -0
  29. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/_base.py +0 -0
  30. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  31. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/_decision.py +0 -0
  32. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  33. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  34. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/_replay.py +0 -0
  35. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/_types.py +0 -0
  36. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  37. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/__init__.py +0 -0
  38. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/_base.py +0 -0
  39. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/_types.py +0 -0
  40. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/autotuner.py +0 -0
  41. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/axis_spec.py +0 -0
  42. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/config_emitter.py +0 -0
  43. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/crossval.py +0 -0
  44. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/detector_select.py +0 -0
  45. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/distribution.py +0 -0
  46. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/grid_search.py +0 -0
  47. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/labels.py +0 -0
  48. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/result.py +0 -0
  49. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/runner.py +0 -0
  50. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/scoring.py +0 -0
  51. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/seasonality_search.py +0 -0
  52. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/settings.py +0 -0
  53. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/autotune/window_select.py +0 -0
  54. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/__init__.py +0 -0
  55. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/_output.py +0 -0
  56. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  57. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  58. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  59. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  60. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/rules/project.md +0 -0
  61. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  62. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  63. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  64. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  65. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/__init__.py +0 -0
  66. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/autotune.py +0 -0
  67. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/init.py +0 -0
  68. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/init_claude.py +0 -0
  69. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/osi.py +0 -0
  70. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/run.py +0 -0
  71. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/test_alert.py +0 -0
  72. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/tune.py +0 -0
  73. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/ui.py +0 -0
  74. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/commands/unlock.py +0 -0
  75. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/cli/main.py +0 -0
  76. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/config/__init__.py +0 -0
  77. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/config/metric_config.py +0 -0
  78. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/config/metric_io.py +0 -0
  79. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/config/profile.py +0 -0
  80. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/config/project_config.py +0 -0
  81. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/config/validator.py +0 -0
  82. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/core/__init__.py +0 -0
  83. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/core/interval.py +0 -0
  84. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/core/models.py +0 -0
  85. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/__init__.py +0 -0
  86. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/_sql_manager.py +0 -0
  87. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/clickhouse_manager.py +0 -0
  88. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/__init__.py +0 -0
  89. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_alert_states.py +0 -0
  90. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  91. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_base.py +0 -0
  92. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_datapoints.py +0 -0
  93. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_detections.py +0 -0
  94. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_maintenance.py +0 -0
  95. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_metrics.py +0 -0
  96. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_schema.py +0 -0
  97. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/_tasks.py +0 -0
  98. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/internal_tables/manager.py +0 -0
  99. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/manager.py +0 -0
  100. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/mysql_manager.py +0 -0
  101. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/postgres_manager.py +0 -0
  102. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/database/tables.py +0 -0
  103. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/__init__.py +0 -0
  104. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/base.py +0 -0
  105. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/seasonality.py +0 -0
  106. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/statistical/__init__.py +0 -0
  107. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/statistical/_windowed.py +0 -0
  108. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/statistical/autoreg.py +0 -0
  109. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/statistical/iqr.py +0 -0
  110. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/statistical/mad.py +0 -0
  111. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  112. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/detectors/statistical/zscore.py +0 -0
  113. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/loaders/__init__.py +0 -0
  114. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/loaders/metric_loader.py +0 -0
  115. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/loaders/query_template.py +0 -0
  116. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/__init__.py +0 -0
  117. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/error_dispatch.py +0 -0
  118. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/task_manager/__init__.py +0 -0
  119. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  120. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/task_manager/_base.py +0 -0
  121. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  122. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  123. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/task_manager/_types.py +0 -0
  124. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/orchestration/task_manager/manager.py +0 -0
  125. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/reporting/__init__.py +0 -0
  126. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/reporting/assets/report.js +0 -0
  127. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/reporting/builder.py +0 -0
  128. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/reporting/html_report.py +0 -0
  129. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/semantic/__init__.py +0 -0
  130. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/semantic/errors.py +0 -0
  131. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/semantic/exporter.py +0 -0
  132. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/semantic/importer.py +0 -0
  133. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/semantic/osi_model.py +0 -0
  134. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/semantic/query_gen.py +0 -0
  135. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/tuning/__init__.py +0 -0
  136. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/tuning/assets/tune.js +0 -0
  137. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/tuning/config_writer.py +0 -0
  138. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/tuning/html.py +0 -0
  139. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/tuning/payload.py +0 -0
  140. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/tuning/server.py +0 -0
  141. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/ui/__init__.py +0 -0
  142. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/ui/html.py +0 -0
  143. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/ui/metric_files.py +0 -0
  144. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/utils/__init__.py +0 -0
  145. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/utils/datetime_utils.py +0 -0
  146. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/utils/env_interpolation.py +0 -0
  147. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/utils/json_utils.py +0 -0
  148. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit/utils/stats.py +0 -0
  149. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit.egg-info/SOURCES.txt +0 -0
  150. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit.egg-info/dependency_links.txt +0 -0
  151. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit.egg-info/entry_points.txt +0 -0
  152. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit.egg-info/requires.txt +0 -0
  153. {detectkit-0.55.2 → detectkit-0.56.1}/detectkit.egg-info/top_level.txt +0 -0
  154. {detectkit-0.55.2 → detectkit-0.56.1}/pyproject.toml +0 -0
  155. {detectkit-0.55.2 → detectkit-0.56.1}/requirements.txt +0 -0
  156. {detectkit-0.55.2 → detectkit-0.56.1}/setup.cfg +0 -0
  157. {detectkit-0.55.2 → detectkit-0.56.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.55.2
3
+ Version: 0.56.1
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.55.2"
7
+ __version__ = "0.56.1"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -84,7 +84,8 @@ version — **read the relevant one on demand** instead of guessing:
84
84
  database key, not the filename. Keep filename and `name` in sync.
85
85
  - **Changing a detector parameter changes the detector's identity** and
86
86
  recomputes its detections from scratch; the old rows are orphaned. After
87
- retuning a live metric, run `dtk clean --select <metric>` to prune them.
87
+ retuning a live metric, run `dtk clean --select <metric>` to prune them (or,
88
+ from `dtk ui`, open the metric and click **Clean stale** in its detail view).
88
89
  - **`alert_cooldown` defaults to `null`** = a persisting anomaly re-alerts on
89
90
  *every* `dtk run`. Always set a cooldown for production metrics.
90
91
  - The pipeline is **idempotent**: it resumes from the last saved timestamp.
@@ -12,7 +12,7 @@ Run all commands from a project directory (the one containing
12
12
  | `dtk run --select <sel>` | Run the load → detect → alert pipeline |
13
13
  | `dtk autotune --select <sel>` | Auto-configure a metric's detector (see `autotune.md`) |
14
14
  | `dtk tune --select <sel>` | Interactively tune a detector on real data, write it back in place |
15
- | `dtk ui` | Project-wide live overview + browser pipeline control panel + create/edit/delete metric YAMLs |
15
+ | `dtk ui` | Project-wide live overview + browser pipeline control panel + create/edit/delete metric YAMLs + per-metric Clean stale |
16
16
  | `dtk test-alert <metric>` | Send a mock alert to the metric's channels |
17
17
  | `dtk unlock --select <sel>` | Clear a stuck pipeline lock |
18
18
  | `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
@@ -211,13 +211,21 @@ dtk ui [-s/--select "*"] [--window 24h|7d|30d|90d|all] [--profile NAME] [--no-op
211
211
  - `--no-open` — print the URL instead of opening a browser tab.
212
212
 
213
213
  Clicking a metric's **Open** shows the existing self-contained HTML report
214
- (the same one `--report` writes) in an overlay. A **pipeline panel** drives
214
+ (the same one `--report` writes) in an overlay, whose header also has a
215
+ **Clean stale** button next to **Tune**: it previews a `dtk clean --select
216
+ <metric>` dry-run (superseded detector generations + row counts, stale
217
+ alert-state ids), and — if anything is stale — an amber confirm strip offers
218
+ **Delete stale data**, which spawns the real `dtk clean --select <metric>
219
+ --execute` as a job (`clean` kind) sharing the run/autotune/unlock gate below;
220
+ on success the report reloads and the overview's stale chip clears. This is
221
+ the drift-mode `dtk clean --select` from inside the UI; `--orphaned-metrics`
222
+ (renamed/deleted metrics) stays CLI-only. A **pipeline panel** drives
215
223
  the real CLI as subprocesses — `dtk run` (select/steps/from/to/force/
216
224
  full-refresh), `dtk autotune`, `dtk unlock` — streaming their terminal
217
- output live; **only one of these three runs at a time** (they'd contend for
218
- the same pipeline lock anyway). **Tune** launches `dtk tune --select
225
+ output live; **only one `run`/`autotune`/`unlock`/`clean` job runs at a
226
+ time** (they'd contend for the same pipeline lock anyway). **Tune** launches `dtk tune --select
219
227
  <metric>` for a metric and opens its cockpit in a new tab — unlike
220
- run/autotune/unlock, **multiple tune jobs run concurrently**, since each is
228
+ run/autotune/unlock/clean, **multiple tune jobs run concurrently**, since each is
221
229
  its own isolated, lock-free session. The `dtk ui` server itself takes **no
222
230
  pipeline lock** and never mutates anything — every spawned command behaves
223
231
  exactly as if typed into a terminal.
@@ -290,7 +298,9 @@ the next normal run recovers on its own.)
290
298
 
291
299
  Editing metrics over time leaves stale rows in the internal tables. `dtk clean`
292
300
  removes that drift. **Both modes dry-run by default** — pass `--execute` to
293
- actually delete.
301
+ actually delete. Drift mode is also one click away, per metric, from `dtk
302
+ ui`'s detail overlay (**Clean stale**, next to Tune); `--orphaned-metrics`
303
+ stays CLI-only.
294
304
 
295
305
  - **Drift mode** — `dtk clean --select <sel>`: for each still-existing metric,
296
306
  deletes `_dtk_detections` rows for `detector_id`s the config no longer
@@ -149,7 +149,8 @@ columns and bakes the best grouping into the tuned config (see `autotune.md`).
149
149
  `detector_id` and recomputes that detector's detections; the old rows are
150
150
  orphaned in `_dtk_detections`. **Changing/removing an alerting block** orphans
151
151
  its `_dtk_alert_states` row. Prune both with `dtk clean --select <m>`
152
- (preview, then `--execute`).
152
+ (preview, then `--execute`) — or, from `dtk ui`, the metric's detail view's
153
+ **Clean stale** button.
153
154
  - **Renaming/deleting a metric** orphans all its rows under the old name; purge
154
155
  with `dtk clean --orphaned-metrics`.
155
156
  - Datapoints are keyed only by `(metric, timestamp)` — a parameter edit never
@@ -33,8 +33,10 @@ for me, I won't look").
33
33
 
34
34
  Your job is to **set up the sandbox, open it, and guide the user through it** —
35
35
  they drive the chart; you prepare it, explain each control in plain language, and
36
- handle the follow-up (`dtk run` / `dtk clean`). This skill is the procedure; for
37
- field detail read the matching file under `.claude/rules/detectkit/` (`cli.md`
36
+ handle the follow-up (`dtk run` / `dtk clean`, or point at `dtk ui`'s per-metric
37
+ **Clean stale** button as the interactive equivalent). This skill is the
38
+ procedure; for field detail read the matching file under
39
+ `.claude/rules/detectkit/` (`cli.md`
38
40
  has the full `dtk tune` reference, plus `detectors.md`, `autotune.md`,
39
41
  `alerting.md`).
40
42
 
@@ -194,6 +196,10 @@ dtk clean --select <name> --execute # prune the orphaned old detector row
194
196
  dtk test-alert <name> # if alerting is configured
195
197
  ```
196
198
 
199
+ (`dtk ui`'s metric detail overlay has the same cleanup as a **Clean stale**
200
+ button, if the user is already there and would rather not switch to a
201
+ terminal.)
202
+
197
203
  ## When to hand off instead
198
204
 
199
205
  - **Fully automatic, no browser** (CI, "just pick one for me") → the
@@ -211,4 +217,5 @@ dtk test-alert <name> # if alerting is configured
211
217
  - [ ] You explained the relevant mode(s) — including that **Autotune is built in**
212
218
  and runs over the window shown — and which controls stay visible everywhere.
213
219
  - [ ] After **Apply**, you ran `dtk run` to recompute and `dtk clean --execute`
214
- to prune the orphaned old detector rows (and `dtk test-alert` if alerting).
220
+ (or `dtk ui`'s **Clean stale** button) to prune the orphaned old detector
221
+ rows (and `dtk test-alert` if alerting).
@@ -264,17 +264,10 @@ def _valid_detector_ids(config: MetricConfig) -> set[str]:
264
264
  injection) so the computed ``detector_id`` matches what the pipeline
265
265
  writes — anything in the DB not in this set is stale.
266
266
  """
267
- ids: set[str] = set()
268
- for detector_config in config.detectors or []:
269
- params = detector_config.get_algorithm_params()
270
- seasonality_components = detector_config.get_seasonality_components()
271
- if seasonality_components is not None:
272
- params["seasonality_components"] = seasonality_components
273
- detector = DetectorFactory.create_from_config(
274
- {"type": detector_config.type, "params": params}
275
- )
276
- ids.add(detector.get_detector_id())
277
- return ids
267
+ return {
268
+ DetectorFactory.detector_id_for_config(detector_config)
269
+ for detector_config in config.detectors or []
270
+ }
278
271
 
279
272
 
280
273
  def _valid_alert_config_ids(config: MetricConfig) -> set[str]:
@@ -2,6 +2,8 @@
2
2
  Detector factory for creating detector instances from configuration.
3
3
  """
4
4
 
5
+ from typing import TYPE_CHECKING
6
+
5
7
  from detectkit.detectors.base import BaseDetector
6
8
  from detectkit.detectors.statistical.autoreg import AutoregDetector
7
9
  from detectkit.detectors.statistical.iqr import IQRDetector
@@ -9,6 +11,9 @@ from detectkit.detectors.statistical.mad import MADDetector
9
11
  from detectkit.detectors.statistical.manual_bounds import ManualBoundsDetector
10
12
  from detectkit.detectors.statistical.zscore import ZScoreDetector
11
13
 
14
+ if TYPE_CHECKING:
15
+ from detectkit.config.metric_config import DetectorConfig
16
+
12
17
 
13
18
  class DetectorFactory:
14
19
  """
@@ -119,6 +124,24 @@ class DetectorFactory:
119
124
  detectors.append(detector)
120
125
  return detectors
121
126
 
127
+ @classmethod
128
+ def detector_id_for_config(cls, detector_config: "DetectorConfig") -> str:
129
+ """The ``detector_id`` the detect step would run this configured detector under.
130
+
131
+ Mirrors the DETECT step's derivation exactly (``get_algorithm_params``
132
+ + the same seasonality injection → :meth:`create_from_config` →
133
+ ``get_detector_id``), so callers that only need the id — the overview's
134
+ current-config filter, ``dtk clean``'s drift diff, the UI's stale-data
135
+ preview — all agree with what the pipeline writes. Raises (like
136
+ :meth:`create`) on a config the factory rejects.
137
+ """
138
+ params = detector_config.get_algorithm_params()
139
+ seasonality_components = detector_config.get_seasonality_components()
140
+ if seasonality_components is not None:
141
+ params["seasonality_components"] = seasonality_components
142
+ detector = cls.create_from_config({"type": detector_config.type, "params": params})
143
+ return detector.get_detector_id()
144
+
122
145
  @classmethod
123
146
  def list_available_types(cls) -> list[str]:
124
147
  """