detectkit 0.41.0__tar.gz → 0.42.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.41.0/detectkit.egg-info → detectkit-0.42.0}/PKG-INFO +1 -1
  2. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/__init__.py +0 -2
  4. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/CLAUDE.section.md +6 -4
  5. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/autotune.md +35 -47
  6. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/cli.md +5 -2
  7. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +32 -33
  8. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +2 -1
  9. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/autotune.py +24 -141
  10. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/init.py +3 -2
  11. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/main.py +6 -28
  12. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/reporting/html_report.py +6 -5
  13. {detectkit-0.41.0 → detectkit-0.42.0/detectkit.egg-info}/PKG-INFO +1 -1
  14. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit.egg-info/SOURCES.txt +0 -2
  15. detectkit-0.41.0/detectkit/autotune/html_labeler.py +0 -969
  16. detectkit-0.41.0/detectkit/autotune/label_server.py +0 -177
  17. {detectkit-0.41.0 → detectkit-0.42.0}/LICENSE +0 -0
  18. {detectkit-0.41.0 → detectkit-0.42.0}/MANIFEST.in +0 -0
  19. {detectkit-0.41.0 → detectkit-0.42.0}/README.md +0 -0
  20. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/__init__.py +0 -0
  21. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/__init__.py +0 -0
  22. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/base.py +0 -0
  23. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/branding.py +0 -0
  24. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/email.py +0 -0
  25. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/factory.py +0 -0
  26. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/mattermost.py +0 -0
  27. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/slack.py +0 -0
  28. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/telegram.py +0 -0
  29. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/channels/webhook.py +0 -0
  30. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  31. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  32. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  33. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  34. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  35. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  36. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  37. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  38. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  39. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/_base.py +0 -0
  40. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/_types.py +0 -0
  41. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/autotuner.py +0 -0
  42. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/config_emitter.py +0 -0
  43. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/crossval.py +0 -0
  44. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/detector_select.py +0 -0
  45. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/distribution.py +0 -0
  46. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/grid_search.py +0 -0
  47. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/labels.py +0 -0
  48. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/result.py +0 -0
  49. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/scoring.py +0 -0
  50. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/seasonality_search.py +0 -0
  51. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/settings.py +0 -0
  52. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/autotune/window_select.py +0 -0
  53. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/__init__.py +0 -0
  54. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/_output.py +0 -0
  55. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  56. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  57. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  58. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  59. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  60. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  61. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  62. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/__init__.py +0 -0
  63. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/clean.py +0 -0
  64. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/init_claude.py +0 -0
  65. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/run.py +0 -0
  66. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/test_alert.py +0 -0
  67. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/tune.py +0 -0
  68. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/cli/commands/unlock.py +0 -0
  69. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/config/__init__.py +0 -0
  70. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/config/metric_config.py +0 -0
  71. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/config/profile.py +0 -0
  72. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/config/project_config.py +0 -0
  73. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/config/validator.py +0 -0
  74. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/core/__init__.py +0 -0
  75. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/core/interval.py +0 -0
  76. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/core/models.py +0 -0
  77. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/__init__.py +0 -0
  78. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/_sql_manager.py +0 -0
  79. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/clickhouse_manager.py +0 -0
  80. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/__init__.py +0 -0
  81. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  82. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  83. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_base.py +0 -0
  84. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  85. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_detections.py +0 -0
  86. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  87. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  88. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_schema.py +0 -0
  89. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  90. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/internal_tables/manager.py +0 -0
  91. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/manager.py +0 -0
  92. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/mysql_manager.py +0 -0
  93. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/postgres_manager.py +0 -0
  94. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/database/tables.py +0 -0
  95. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/__init__.py +0 -0
  96. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/base.py +0 -0
  97. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/factory.py +0 -0
  98. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/seasonality.py +0 -0
  99. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/statistical/__init__.py +0 -0
  100. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  101. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/statistical/iqr.py +0 -0
  102. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/statistical/mad.py +0 -0
  103. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  104. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/detectors/statistical/zscore.py +0 -0
  105. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/loaders/__init__.py +0 -0
  106. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/loaders/metric_loader.py +0 -0
  107. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/loaders/query_template.py +0 -0
  108. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/__init__.py +0 -0
  109. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/error_dispatch.py +0 -0
  110. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  111. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  112. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  113. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  114. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  115. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  116. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  117. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/reporting/__init__.py +0 -0
  118. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/reporting/assets/report.js +0 -0
  119. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/reporting/builder.py +0 -0
  120. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/tuning/__init__.py +0 -0
  121. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/tuning/assets/tune.js +0 -0
  122. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/tuning/config_writer.py +0 -0
  123. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/tuning/html.py +0 -0
  124. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/tuning/payload.py +0 -0
  125. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/tuning/server.py +0 -0
  126. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/utils/__init__.py +0 -0
  127. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/utils/datetime_utils.py +0 -0
  128. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/utils/env_interpolation.py +0 -0
  129. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/utils/json_utils.py +0 -0
  130. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit/utils/stats.py +0 -0
  131. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit.egg-info/dependency_links.txt +0 -0
  132. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit.egg-info/entry_points.txt +0 -0
  133. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit.egg-info/requires.txt +0 -0
  134. {detectkit-0.41.0 → detectkit-0.42.0}/detectkit.egg-info/top_level.txt +0 -0
  135. {detectkit-0.41.0 → detectkit-0.42.0}/pyproject.toml +0 -0
  136. {detectkit-0.41.0 → detectkit-0.42.0}/requirements.txt +0 -0
  137. {detectkit-0.41.0 → detectkit-0.42.0}/setup.cfg +0 -0
  138. {detectkit-0.41.0 → detectkit-0.42.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.41.0
3
+ Version: 0.42.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.41.0"
7
+ __version__ = "0.42.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -15,7 +15,6 @@ from detectkit.autotune._base import AutoTuneError, _AutoTuneBase
15
15
  from detectkit.autotune._types import ScoringMetric, TuneMode
16
16
  from detectkit.autotune.autotuner import AutoTuner, run_autotune_engine
17
17
  from detectkit.autotune.config_emitter import compute_run_id, emit_tuned_config
18
- from detectkit.autotune.html_labeler import render_labeler_html
19
18
  from detectkit.autotune.labels import (
20
19
  GroundTruth,
21
20
  IncidentLabels,
@@ -39,6 +38,5 @@ __all__ = [
39
38
  "emit_tuned_config",
40
39
  "parse_incident_labels",
41
40
  "parse_labels_file",
42
- "render_labeler_html",
43
41
  "run_autotune_engine",
44
42
  ]
@@ -17,10 +17,12 @@ run. But you assist far better with **read access to the same database** (e.g. a
17
17
  database MCP for the project's ClickHouse / PostgreSQL / MySQL): you can inspect
18
18
  a metric's series, find real incidents to label for `dtk autotune`, sanity-check
19
19
  a metric query before running it, and confirm detections — instead of asking the
20
- user to run every query by hand. Without it, fall back to
21
- `dtk autotune --select <metric> --label` (a visual labeler) and to asking the
22
- user. This access is optional and separate from the `profiles.yml` connection
23
- detectkit uses for the pipeline.
20
+ user to run every query by hand. Without it, fall back to marking incidents
21
+ visually in `dtk tune --select <metric>` (**Label** mode to drag spans / Threshold
22
+ capture / Lasso the anomaly cloud, or **Review** mode to confirm fired alerts as
23
+ incidents), then **Save incidents** — which `dtk autotune` auto-discovers in
24
+ `incidents/<metric>/` — and to asking the user. This access is optional and
25
+ separate from the `profiles.yml` connection detectkit uses for the pipeline.
24
26
 
25
27
  ### Where to look (read the matching file before answering)
26
28
 
@@ -64,28 +64,24 @@ ratios to choose.
64
64
  ## Command
65
65
 
66
66
  ```bash
67
- dtk autotune --select <sel> [--incidents FILE] [--label] [--scoring METRIC] \
67
+ dtk autotune --select <sel> [--incidents FILE] [--scoring METRIC] \
68
68
  [--from DATE] [--to DATE] [--profile NAME] [--force] [--dry-run] [--report]
69
69
  ```
70
70
 
71
71
  - `--incidents FILE|DIR` — a labels file (below) → **supervised** tuning. May be a
72
72
  **directory** (e.g. `incidents/<name>/`): interactive runs prompt to pick a
73
- version (default newest), non-interactive use the newest. With nothing given, an
73
+ version (default newest), non-interactive use the newest. **Omit it and `dtk
74
+ autotune` auto-discovers the newest labels in `incidents/<metric>/`** (the store
75
+ `dtk tune`'s **Save incidents** writes) — so after labeling in `dtk tune` you run
76
+ `dtk autotune --select <metric>` with no flag. With no labels anywhere, an
74
77
  interactive terminal prompts to enter incidents inline; declining (or running
75
78
  non-interactively) tunes **unsupervised**.
76
- - `--label` open the interactive labeler (zoom/pan, edit incident edges,
77
- per-incident descriptions, named sets). **Default:** a local 127.0.0.1 server +
78
- browser; **Save & tune** writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml`
79
- (named after the metric, optional set name as a suffix) and the run
80
- **continues into tuning on it**. It **seeds from the metric's newest saved set**
81
- (or from `--incidents FILE|DIR` when given), so re-running `--label` continues
82
- editing in place. Beyond click-drag marking it offers **Threshold capture**
83
- (grab every span above/below a horizontal line in one gesture), **Lasso capture**
84
- (loop around a cloud of points; each grid-adjacent run, gaps bridged, becomes one
85
- incident span), **on-chart delete** (each band's ✕, or select + Delete key), and
86
- **Import file…** (load a labels file you pick). `--no-serve` writes a static
87
- `metrics/<name>__labeler.html` (Export downloads the file) and exits; `--no-open`
88
- prints the URL instead of launching a browser.
79
+ - **To label incidents**, use `dtk tune --select <metric>`: switch to **Label**
80
+ mode (drag spans, **Threshold capture** every span past a horizontal line,
81
+ **Lasso** the anomaly cloud) or **Review** mode (confirm fired alerts as
82
+ incidents), then **Save incidents**. That writes versioned files into
83
+ `incidents/<metric>/` the **same store this command reads** so the labels feed
84
+ the next supervised tune with no `--incidents` flag (see `cli.md`).
89
85
  - `--scoring` — `mcc` (default), `f1`, `f_beta`, `balanced_accuracy`, `roc_auc`,
90
86
  `pr_auc`. MCC uses the whole confusion matrix and suits rare anomalies.
91
87
  - `--dry-run` — run the search but persist nothing and write no config.
@@ -115,45 +111,35 @@ incidents:
115
111
  - {at: "2026-05-11 09:05:00"} # point
116
112
  ```
117
113
 
118
- ### Getting labels — offer the interactive labeler first
114
+ ### Getting labels — label in `dtk tune` first
119
115
 
120
- When labels would help, **offer the interactive HTML labeler before asking the
116
+ When labels would help, **offer to mark incidents in `dtk tune` before asking the
121
117
  user to recall timestamps** — it is the easiest, most reliable path:
122
118
 
123
- 1. Run `dtk autotune --select <name> --label`. It starts a local labeler server
124
- and opens the browser (use `--no-open` on a remote box and share the printed
125
- 127.0.0.1 URL; the user port-forwards or runs it locally).
126
- 2. The user marks incidents on the chart (scroll to zoom, drag the navigator to
127
- move, click-drag to mark, drag an incident's edges to adjust, add a
128
- description, optionally name the set), then clicks **Save & tune**. For many
129
- clear outliers, **Threshold capture** grabs every span past a horizontal line
130
- at once (above/below, with an optional gap-bridge); it captures within the
131
- current view by default, and dragging across the chart limits it to a time
132
- window (different boundary per period) the painted window is **saved with the
133
- set and restored on reopen** (a `capture_windows:` block; metadata only). For an
134
- irregular cloud, **Lasso capture** loops around the points freehand and turns
135
- each grid-adjacent run (small gaps bridged) into one incident span. Each
136
- band's (or select + Delete)
137
- removes one, and **focus** on a list row jumps the chart to it.
138
- 3. That writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml` automatically
139
- (named after the metric, optional set name as a suffix; versioned —
140
- re-labeling never overwrites) and the **same command continues into the tuning
141
- run** on it. No manual file moving.
142
- 4. **Editing over time:** re-running `--label` seeds the page from the metric's
143
- newest saved set, so labeling can grow across sessions. To re-tune later on
144
- saved sets, point `--incidents` at the folder (`incidents/<name>/`) —
145
- interactive runs let the user pick a version, and `--label --incidents <file>`
146
- opens that exact set for editing. The static `--no-serve` path still exists
147
- (Export downloads a file you move into `incidents/<name>/`; its **Import
148
- file…** button loads one back in).
119
+ 1. Run `dtk tune --select <name>`. It opens a localhost browser view of the real
120
+ series (use `--no-open` on a remote box and share the printed 127.0.0.1 URL).
121
+ 2. Mark the real incidents one of two ways:
122
+ - **Label** mode drag a span over each incident, or **Threshold capture**
123
+ every span past a horizontal line in one gesture, or **Lasso anomalies** to
124
+ loop a cloud of anomaly dots into per-streak incident spans.
125
+ - **Review** mode the fired alerts lead; click each alert marker to confirm it
126
+ **valid** (which marks it as a ground-truth incident) or **false alarm**.
127
+ **Confirm all unreviewed valid** does the lot a clean metric whose alerts
128
+ are all good is validated in a few clicks without hand-drawing spans.
129
+ 3. Click **Save incidents**. It writes a versioned
130
+ `incidents/<metric>/<…>.yml` automatically.
131
+ 4. Run `dtk autotune --select <name>` with **no `--incidents` flag** — it
132
+ auto-discovers the newest file in `incidents/<metric>/` and tunes supervised on
133
+ it. (You can still pass `--incidents <file-or-dir>` to pick a specific set.)
149
134
 
150
135
  Prefer this whenever the user can *recognise* incidents on a chart but doesn't
151
136
  have exact times. If they already know the times (or you found them via a DB
152
137
  MCP), write the labels file / inline `incidents:` directly instead. If there are
153
138
  no known incidents, run unsupervised — the baseline below is good on its own.
154
139
 
155
- With no labels (no `--incidents`, no config `labels_file`, no interactive
156
- entry), tuning falls back to an **unsupervised** objective that blends three
140
+ With no labels (no `--incidents`, no config `labels_file`, none auto-discovered
141
+ in `incidents/<metric>/`, no interactive entry), tuning falls back to an
142
+ **unsupervised** objective that blends three
157
143
  band-fit / flag-budget terms — `0.4·budget + 0.3·sharpness + 0.3·separation`: a
158
144
  smooth one-sided flag-rate budget (no hard cliff), sharpness (a tight,
159
145
  well-calibrated confidence interval), and separation (flagged points sitting
@@ -192,8 +178,10 @@ differs from `seasonality_candidates`, which only narrows the *set of columns*
192
178
  the search may consider but still runs the search and may choose none.
193
179
 
194
180
  Label precedence (highest first): `--incidents` flag → `labels_file` → inline
195
- `incidents` → interactive prompt none. `labels_file` and `incidents` are
196
- mutually exclusive. `--scoring` likewise overrides `scoring_metric`.
181
+ `incidents` → auto-discovered `incidents/<metric>/` (the newest file, e.g. from
182
+ `dtk tune`'s **Save incidents**) → interactive prompt → none (unsupervised).
183
+ `labels_file` and `incidents` are mutually exclusive. `--scoring` likewise
184
+ overrides `scoring_metric`.
197
185
 
198
186
  ## The annotated config
199
187
 
@@ -67,8 +67,11 @@ Automatically chooses a metric's seasonality, detector type, hyperparameters and
67
67
  history window, then writes an annotated `metrics/<name>__tuned_<id>.yml`. Reads
68
68
  the metric's loaded datapoints (run `dtk run --steps load` first if empty), never
69
69
  edits the original, never alerts. `--incidents FILE` enables supervised tuning
70
- against labeled incidents; without it, an unsupervised objective is used.
71
- `--dry-run` searches without writing. `--report [PATH]` writes the same
70
+ against labeled incidents; omit it and autotune **auto-discovers** the newest
71
+ labels in `incidents/<metric>/` (the store `dtk tune`'s **Save incidents** writes
72
+ — label there in Label/Review mode, then just run `dtk autotune`); with no labels
73
+ anywhere, an unsupervised objective is used. `--dry-run` searches without writing.
74
+ `--report [PATH]` writes the same
72
75
  self-contained HTML report as `dtk run` for the tuned winner (default
73
76
  `reports/<metric>__tuned_<id>.html`; `<dir>` or a `.html` file also accepted).
74
77
  Full reference: `autotune.md`.
@@ -79,44 +79,39 @@ generalize is rejected.
79
79
  Labels turn tuning from a good unsupervised default into a config optimised
80
80
  against *your* real incidents (it then optimises MCC and also tunes the alert
81
81
  window `consecutive_anomalies`). **The easiest, most reliable way to produce them
82
- is the interactive HTML labeler — offer this first**, before asking the user to
82
+ is to mark incidents in `dtk tune` — offer this first**, before asking the user to
83
83
  recall timestamps:
84
84
 
85
85
  ```bash
86
- dtk autotune --select <name> --label
86
+ dtk tune --select <name>
87
87
  ```
88
88
 
89
- By default this starts a **local labeler server** (127.0.0.1) and opens the
90
- browser; on a remote machine add `--no-open` and share the printed URL. Walk the
91
- user through it:
89
+ This opens a localhost browser view of the metric's **real** series; on a remote
90
+ machine add `--no-open` and share the printed URL. Walk the user through it:
92
91
 
93
92
  1. Navigate a long/dense series: **scroll to zoom**, double-click to reset, drag
94
- the **navigator strip** to move the view (window = pan, edges = stretch).
95
- 2. **Click-drag across the chart** to mark each incident (red band + a row below
96
- with an optional **description**). Adjust one by dragging its **edges**, or its
97
- **middle** to move it; optionally **name the set**. Remove one by clicking its
98
- **✕** on the chart (or selecting it and pressing **Delete**) no hunting in
99
- the list; **focus** on a list row jumps the chart to that incident; *Clear all*
100
- resets.
101
- 3. When many outliers are obvious, use **Threshold capture**: set a horizontal
102
- line (hover the chart or type a value), pick **above/below**, optionally bridge
103
- small gaps, and **Add N spans** marks them all at once then tidy with the ✕.
104
- It captures within the current view by default; **drag across the chart** to
105
- limit it to a time window (so a metric that behaves differently across periods
106
- can take a different boundary in each). **↺ whole view** clears the window.
107
- 4. Click **Save & tune**. The server writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml`
108
- automatically (named after the metric, with the optional set name as a suffix;
109
- versioned — re-labeling never overwrites) and the **same command
110
- continues into the tuning run on it**. No manual file moving.
111
-
112
- **Editing an existing set:** re-running `--label` **seeds the page from the
113
- metric's newest saved set** (or from `--label --incidents <file-or-dir>`), so
114
- labeling can grow across sessions — mark a few more, save a new version. (`--no-serve`
115
- is the offline fallback: it writes a static `metrics/<name>__labeler.html` whose
116
- Export downloads a versioned file you then move into `incidents/<name>/` and pass
117
- via `--incidents`; its **Import file…** button loads a saved set back in.) To
118
- re-tune later on a saved set, point `--incidents` at `incidents/<name>/`
119
- (interactive runs let the user pick a version).
93
+ the **navigator strip** to move the view.
94
+ 2. Mark the real incidents in one of two modes:
95
+ - **Label** mode **drag a span** over each incident (edges to resize, middle
96
+ to move, ✕/Delete to remove). When many outliers are obvious, **Threshold
97
+ capture** grabs every span past a horizontal line in one gesture (set the
98
+ line, pick the side, optionally bridge gaps); **Lasso anomalies** loops a
99
+ freeform cloud of anomaly dots into one incident span per grid-adjacent run.
100
+ - **Review** mode the fired alerts lead; **click each alert marker** to cycle
101
+ its verdict un-reviewed **valid** (green) **false alarm** (slate).
102
+ **Confirming an alert valid marks it as a ground-truth incident**, and
103
+ **Confirm all unreviewed valid** does the lot so a clean metric whose alerts
104
+ are all good is validated in a few clicks without hand-drawing spans.
105
+ 3. Click **Save incidents**. It writes `incidents/<metric>/<…>.yml` automatically
106
+ (versioned — saving never overwrites). Saving doesn't end the session, so you
107
+ can keep tuning; you can also click **Apply** to write the detector config back.
108
+
109
+ **Then run autotune with no `--incidents` flag** `dtk autotune --select <name>`
110
+ **auto-discovers the newest file in `incidents/<metric>/`** (the same store
111
+ `dtk tune` saves to) and tunes supervised on it. To re-tune on a specific set,
112
+ pass `--incidents <file-or-dir>` (point it at `incidents/<name>/` and interactive
113
+ runs let the user pick a version). Labeling grows across sessions reopen
114
+ `dtk tune`, mark a few more, **Save incidents** again, re-run autotune.
120
115
 
121
116
  Prefer this whenever the user can *recognise* incidents on a chart but doesn't
122
117
  have exact timestamps — it is far easier than dictating times, and they label
@@ -158,10 +153,14 @@ is good on its own; it just can't learn *which* spikes you specifically care abo
158
153
  ## Step 3 — Run autotune
159
154
 
160
155
  ```bash
161
- # Supervised (recommended when you have incidents):
156
+ # Supervised labeled in `dtk tune`: just run it, autotune auto-discovers
157
+ # the newest file in incidents/<name>/ (no --incidents flag needed):
158
+ dtk autotune --select <name>
159
+
160
+ # Supervised — point at a specific labels file or set explicitly:
162
161
  dtk autotune --select <name> --incidents incidents/<name>.yml
163
162
 
164
- # Unsupervised (no labels — tunes on data statistics):
163
+ # Unsupervised (no labels anywhere — tunes on data statistics):
165
164
  dtk autotune --select <name>
166
165
  ```
167
166
 
@@ -45,7 +45,8 @@ side by side, ask which one to set up.
45
45
  > a database MCP): you can inspect metric series, find incidents to label for
46
46
  > `dtk autotune`, sanity-check queries, and confirm detections yourself. Suggest
47
47
  > the user connect one if they haven't; if they can't, you can still do
48
- > everything by asking them to run queries and by using `dtk autotune --label`.
48
+ > everything by asking them to run queries and by marking incidents visually in
49
+ > `dtk tune` (Label/Review modes), which `dtk autotune` auto-discovers.
49
50
 
50
51
  ## Step 1 — Pick the database backend
51
52
 
@@ -24,16 +24,11 @@ from detectkit.autotune import (
24
24
  compute_run_id,
25
25
  emit_tuned_config,
26
26
  parse_labels_file,
27
- render_labeler_html,
28
27
  run_autotune_engine,
29
28
  )
30
- from detectkit.autotune.label_server import serve_labeler
31
29
  from detectkit.autotune.labels import (
32
30
  GroundTruth,
33
31
  IncidentLabels,
34
- incidents_to_display,
35
- load_capture_windows,
36
- load_incidents_for_display,
37
32
  parse_incident_labels,
38
33
  )
39
34
  from detectkit.cli._output import echo_done, echo_error, echo_noop
@@ -107,9 +102,10 @@ _LABELS_GLOBS = ("*.yml", "*.yaml", "*.json")
107
102
  def _labels_files(directory: Path) -> list[Path]:
108
103
  """All labels files in *directory*, oldest→newest.
109
104
 
110
- The labeler saves versioned, ISO-stamped names (``<metric>[-<set>]-<UTC>.yml``)
111
- which sort chronologically, so name order is chronological (tie-broken by mtime).
112
- This lets ``incidents/<metric>/`` keep every labeling round on disk.
105
+ ``dtk tune`` (Label mode) saves versioned, ISO-stamped names
106
+ (``<metric>[-<set>]-<UTC>.yml``) which sort chronologically, so name order is
107
+ chronological (tie-broken by mtime). This lets ``incidents/<metric>/`` keep
108
+ every labeling round on disk.
113
109
  """
114
110
  files: list[Path] = []
115
111
  for pattern in _LABELS_GLOBS:
@@ -117,64 +113,6 @@ def _labels_files(directory: Path) -> list[Path]:
117
113
  return sorted(files, key=lambda p: (p.name, p.stat().st_mtime))
118
114
 
119
115
 
120
- def _resolve_preload_incidents(
121
- *,
122
- metric: str,
123
- interval_seconds: int,
124
- incidents_path: str | None,
125
- autotune_cfg: AutoTuneConfig,
126
- project_root: Path,
127
- ) -> tuple[list[dict[str, str]], Path | None]:
128
- """Existing incidents to seed the labeler with (edit-in-place flow).
129
-
130
- Tries, in order: the ``--incidents`` flag, the config ``labels_file``, then the
131
- default versioned dir ``incidents/<metric>/`` (where ``--label`` saves). A
132
- directory resolves to its newest file. Best-effort — a missing/invalid file
133
- just yields an empty seed (and a warning), never aborting the labeling flow.
134
- """
135
- candidates: list[Path] = []
136
- explicit = incidents_path or autotune_cfg.labels_file
137
- if explicit:
138
- p = Path(explicit)
139
- candidates.append(p if p.is_absolute() else project_root / p)
140
- candidates.append(project_root / "incidents" / metric)
141
- for cand in candidates:
142
- try:
143
- if cand.is_dir():
144
- files = _labels_files(cand)
145
- if not files:
146
- continue
147
- target = files[-1]
148
- elif cand.exists():
149
- target = cand
150
- else:
151
- continue
152
- disp = load_incidents_for_display(
153
- target, interval_seconds=interval_seconds, metric_name=metric
154
- )
155
- return disp, target
156
- except Exception as exc: # noqa: BLE001 — preload is a best-effort convenience
157
- click.echo(click.style(f" Could not preload {cand}: {exc}", fg="yellow"))
158
- return [], None
159
-
160
- # No file on disk — fall back to inline config incidents (mirrors the
161
- # precedence in _resolve_labels) so an inline-labeled metric also seeds.
162
- if autotune_cfg.incidents:
163
- try:
164
- labels = parse_incident_labels(
165
- {
166
- "incidents": autotune_cfg.incidents,
167
- "timezone": autotune_cfg.incidents_timezone,
168
- },
169
- interval_seconds=interval_seconds,
170
- metric_name=metric,
171
- )
172
- return incidents_to_display(labels), None
173
- except Exception as exc: # noqa: BLE001 — best-effort convenience
174
- click.echo(click.style(f" Could not preload inline incidents: {exc}", fg="yellow"))
175
- return [], None
176
-
177
-
178
116
  def _pick_labels_file(files: list[Path]) -> tuple[Path, str]:
179
117
  """Choose one labels file from a versioned set; prompt when interactive."""
180
118
  newest = files[-1]
@@ -198,8 +136,9 @@ def _resolve_labels(
198
136
  """Resolve labels by precedence.
199
137
 
200
138
  ``--incidents`` flag > config ``labels_file`` > config inline ``incidents`` >
201
- interactive prompt > none (unsupervised). A directory may be given for the
202
- flag or ``labels_file``; its newest versioned file is used.
139
+ auto-discovered ``incidents/<metric>/`` (where ``dtk tune`` Label mode saves) >
140
+ interactive prompt > none (unsupervised). A directory may be given for the flag
141
+ or ``labels_file``; its newest versioned file is used.
203
142
  """
204
143
  path = incidents_path or autotune_cfg.labels_file
205
144
  if path:
@@ -231,6 +170,23 @@ def _resolve_labels(
231
170
  n = len(autotune_cfg.incidents)
232
171
  return labels, f"inline config ({n} incident{'s' if n != 1 else ''})"
233
172
 
173
+ # Auto-discover the metric's saved labels — `dtk tune` (Label mode) writes
174
+ # versioned files into incidents/<metric>/, so a labeling round there feeds the
175
+ # next autotune with no extra flags (newest used; interactive runs may pick).
176
+ default_dir = project_root / "incidents" / metric_name
177
+ if default_dir.is_dir():
178
+ files = _labels_files(default_dir)
179
+ if files:
180
+ chosen, how = _pick_labels_file(files)
181
+ labels = parse_labels_file(
182
+ chosen, interval_seconds=interval_seconds, metric_name=metric_name
183
+ )
184
+ try:
185
+ where: object = default_dir.relative_to(project_root)
186
+ except ValueError:
187
+ where = default_dir
188
+ return labels, f"file {chosen} (auto-discovered, {how} in {where}/)"
189
+
234
190
  if sys.stdin.isatty():
235
191
  return _prompt_labels(interval_seconds=interval_seconds), "interactive"
236
192
 
@@ -324,9 +280,6 @@ def run_autotune(
324
280
  *,
325
281
  select: str,
326
282
  incidents_path: str | None,
327
- label: bool,
328
- no_serve: bool = False,
329
- no_open: bool = False,
330
283
  scoring_override: str | None,
331
284
  from_date: str | None,
332
285
  to_date: str | None,
@@ -363,9 +316,6 @@ def run_autotune(
363
316
  project_root=project_root,
364
317
  internal_manager=internal_manager,
365
318
  incidents_path=incidents_path,
366
- label=label,
367
- no_serve=no_serve,
368
- no_open=no_open,
369
319
  scoring_override=scoring_override,
370
320
  from_dt=from_dt,
371
321
  to_dt=to_dt,
@@ -387,9 +337,6 @@ def _tune_one(
387
337
  project_root: Path,
388
338
  internal_manager: InternalTablesManager,
389
339
  incidents_path: str | None,
390
- label: bool,
391
- no_serve: bool = False,
392
- no_open: bool = False,
393
340
  scoring_override: str | None,
394
341
  from_dt: datetime | None,
395
342
  to_dt: datetime | None,
@@ -413,70 +360,6 @@ def _tune_one(
413
360
  echo_noop(name, "no datapoints — run `dtk run --select " + name + " --steps load` first")
414
361
  return False
415
362
 
416
- # --label: open the interactive labeler. Default — a local server that saves the
417
- # marked incidents straight into incidents/<name>/ and then falls through to
418
- # tuning on them. --no-serve instead writes a static HTML file and exits (you
419
- # move the downloaded export in yourself); --no-open skips launching a browser.
420
- if label:
421
- click.echo(click.style(f"Processing metric: {name}", fg="cyan", bold=True))
422
- preload, preload_src = _resolve_preload_incidents(
423
- metric=name,
424
- interval_seconds=interval_seconds,
425
- incidents_path=incidents_path,
426
- autotune_cfg=autotune_cfg,
427
- project_root=project_root,
428
- )
429
- if preload:
430
- if preload_src is None:
431
- where: object = "inline config"
432
- else:
433
- try:
434
- where = preload_src.relative_to(project_root)
435
- except ValueError: # an absolute path outside the project tree
436
- where = preload_src
437
- click.echo(f" Editing {len(preload)} existing incident(s) from {where}")
438
- # Restore the painted threshold-capture window from the same seed file
439
- # (best-effort — a missing/old file just yields no window).
440
- capture_windows: list[dict[str, str]] = []
441
- if preload_src is not None:
442
- try:
443
- capture_windows = load_capture_windows(
444
- preload_src, interval_seconds=interval_seconds, metric_name=name
445
- )
446
- except Exception: # noqa: BLE001 — restoring the scope is a convenience
447
- capture_windows = []
448
- if no_serve:
449
- html = render_labeler_html(
450
- name,
451
- data,
452
- interval_seconds=interval_seconds,
453
- incidents=preload,
454
- capture_windows=capture_windows,
455
- )
456
- out = project_root / "metrics" / f"{metric_path.stem}__labeler.html"
457
- out.write_text(html, encoding="utf-8")
458
- click.echo(f" Wrote labeler: {out.relative_to(project_root)}")
459
- click.echo(
460
- f" Open it, mark incidents, Export, save into incidents/{name}/, "
461
- f"then re-run with --incidents incidents/{name}/"
462
- )
463
- return True
464
- saved = serve_labeler(
465
- metric_name=name,
466
- data=data,
467
- incidents_dir=project_root / "incidents" / name,
468
- interval_seconds=interval_seconds,
469
- open_browser=not no_open,
470
- echo=click.echo,
471
- preload=preload,
472
- capture_windows=capture_windows,
473
- )
474
- if saved is None:
475
- echo_noop(name, "labeling cancelled — no labels saved")
476
- return False
477
- click.echo(f" Saved labels: {saved.relative_to(project_root)}")
478
- incidents_path = str(saved) # continue into supervised tuning on this set
479
-
480
363
  click.echo(click.style(f"Tuning metric: {name}", fg="cyan", bold=True))
481
364
  click.echo(f" Config file: {metric_path.relative_to(project_root)}")
482
365
 
@@ -152,8 +152,9 @@ _EXAMPLE_INCIDENTS = """# Example incidents (labels) file for supervised `dtk au
152
152
  # - `label` is optional free text (documentation only).
153
153
  #
154
154
  # Tip: can't list incidents from memory? Run
155
- # dtk autotune --select example_cpu_usage --label
156
- # to get a clickable HTML chart; mark incidents in a browser and export this file.
155
+ # dtk tune --select example_cpu_usage
156
+ # and use Label mode to mark incidents in a browser; Save incidents writes them
157
+ # into incidents/example_cpu_usage/, which `dtk autotune` auto-discovers.
157
158
 
158
159
  # Optional — must match the metric `name:` it labels (autotune refuses a mismatch).
159
160
  metric: example_cpu_usage
@@ -214,22 +214,7 @@ def run(
214
214
  "--incidents",
215
215
  "incidents_path",
216
216
  type=click.Path(),
217
- help="Canonical labels file marking real incidents (enables supervised tuning)",
218
- )
219
- @click.option(
220
- "--label",
221
- is_flag=True,
222
- help="Open the interactive labeler (local server) to mark incidents, then tune on them",
223
- )
224
- @click.option(
225
- "--no-serve",
226
- is_flag=True,
227
- help="With --label, write a static HTML labeler file and exit instead of serving",
228
- )
229
- @click.option(
230
- "--no-open",
231
- is_flag=True,
232
- help="With --label, don't auto-open the browser (just print the local URL)",
217
+ help="Canonical labels file (or incidents/<metric>/ dir) marking real incidents",
233
218
  )
234
219
  @click.option(
235
220
  "--scoring",
@@ -275,9 +260,6 @@ def run(
275
260
  def autotune(
276
261
  select: str,
277
262
  incidents_path: str,
278
- label: bool,
279
- no_serve: bool,
280
- no_open: bool,
281
263
  scoring_override: str,
282
264
  from_date: str,
283
265
  to_date: str,
@@ -295,19 +277,18 @@ def autotune(
295
277
  tuned config named <metric>__tuned_<id>. Each run is recorded in
296
278
  _dtk_autotune_runs.
297
279
 
298
- Mark real incidents for supervised tuning via --incidents (a labels file);
299
- without it, tuning falls back to an unsupervised objective.
280
+ Mark real incidents for supervised tuning via --incidents (a labels file or
281
+ the incidents/<metric>/ dir); without it, tuning falls back to an unsupervised
282
+ objective. Incidents marked in `dtk tune` (Label mode) are auto-discovered from
283
+ incidents/<metric>/, so no flag is needed after labeling there.
300
284
 
301
285
  Examples:
302
286
  # Supervised tuning against labeled incidents
303
287
  dtk autotune --select checkout_errors --incidents incidents/checkout.yml
304
288
 
305
- # Unsupervised (no labels)
289
+ # Unsupervised (no labels), or auto-pick incidents/checkout_errors/ if present
306
290
  dtk autotune --select checkout_errors
307
291
 
308
- # Mark incidents in a browser, save into incidents/<m>/, and tune on them
309
- dtk autotune --select checkout_errors --label
310
-
311
292
  # Search only, change nothing
312
293
  dtk autotune --select checkout_errors --dry-run
313
294
  """
@@ -316,9 +297,6 @@ def autotune(
316
297
  run_autotune(
317
298
  select=select,
318
299
  incidents_path=incidents_path,
319
- label=label,
320
- no_serve=no_serve,
321
- no_open=no_open,
322
300
  scoring_override=scoring_override,
323
301
  from_date=from_date,
324
302
  to_date=to_date,
@@ -1,10 +1,11 @@
1
1
  """Render a report payload into a single self-contained HTML file.
2
2
 
3
- Same delivery model as ``autotune/html_labeler.py``: one HTML document with the
4
- renderer JS inlined (the pre-built ``assets/report.js`` bundle — one source
5
- shared with the website landing demo) and the data baked in as a JS literal. No
6
- CDN, no network, nothing leaves the browser. Placeholders are substituted with
7
- ``str.replace`` (NOT ``.format``) so literal ``{}`` in the JS/CSS survive.
3
+ The self-contained inline-bundle delivery model (shared with the ``dtk tune``
4
+ page): one HTML document with the renderer JS inlined (the pre-built
5
+ ``assets/report.js`` bundle — one source shared with the website landing demo)
6
+ and the data baked in as a JS literal. No CDN, no network, nothing leaves the
7
+ browser. Placeholders are substituted with ``str.replace`` (NOT ``.format``) so
8
+ literal ``{}`` in the JS/CSS survive.
8
9
  """
9
10
 
10
11
  from __future__ import annotations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.41.0
3
+ Version: 0.42.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT