detectkit 0.19.0__tar.gz → 0.20.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 (125) hide show
  1. {detectkit-0.19.0/detectkit.egg-info → detectkit-0.20.0}/PKG-INFO +1 -1
  2. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/CLAUDE.section.md +11 -0
  4. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/rules/autotune.md +14 -5
  5. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +25 -7
  6. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +9 -0
  7. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/autotune.py +14 -1
  8. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/init.py +64 -2
  9. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/config/metric_config.py +43 -1
  10. {detectkit-0.19.0 → detectkit-0.20.0/detectkit.egg-info}/PKG-INFO +1 -1
  11. {detectkit-0.19.0 → detectkit-0.20.0}/LICENSE +0 -0
  12. {detectkit-0.19.0 → detectkit-0.20.0}/MANIFEST.in +0 -0
  13. {detectkit-0.19.0 → detectkit-0.20.0}/README.md +0 -0
  14. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/__init__.py +0 -0
  15. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/__init__.py +0 -0
  16. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/base.py +0 -0
  17. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/branding.py +0 -0
  18. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/email.py +0 -0
  19. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/factory.py +0 -0
  20. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/mattermost.py +0 -0
  21. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/slack.py +0 -0
  22. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/telegram.py +0 -0
  23. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/channels/webhook.py +0 -0
  24. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  25. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  26. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  27. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  28. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  29. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  30. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  31. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  32. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/__init__.py +0 -0
  33. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/_base.py +0 -0
  34. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/_types.py +0 -0
  35. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/autotuner.py +0 -0
  36. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/config_emitter.py +0 -0
  37. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/crossval.py +0 -0
  38. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/detector_select.py +0 -0
  39. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/distribution.py +0 -0
  40. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/grid_search.py +0 -0
  41. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/html_labeler.py +0 -0
  42. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/labels.py +0 -0
  43. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/result.py +0 -0
  44. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/scoring.py +0 -0
  45. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/seasonality_search.py +0 -0
  46. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/settings.py +0 -0
  47. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/autotune/window_select.py +0 -0
  48. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/__init__.py +0 -0
  49. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/_output.py +0 -0
  50. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  51. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  52. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  53. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  54. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  55. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  56. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  57. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  58. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/__init__.py +0 -0
  59. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/clean.py +0 -0
  60. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/init_claude.py +0 -0
  61. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/run.py +0 -0
  62. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/test_alert.py +0 -0
  63. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/commands/unlock.py +0 -0
  64. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/cli/main.py +0 -0
  65. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/config/__init__.py +0 -0
  66. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/config/profile.py +0 -0
  67. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/config/project_config.py +0 -0
  68. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/config/validator.py +0 -0
  69. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/core/__init__.py +0 -0
  70. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/core/interval.py +0 -0
  71. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/core/models.py +0 -0
  72. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/__init__.py +0 -0
  73. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/_sql_manager.py +0 -0
  74. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/clickhouse_manager.py +0 -0
  75. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/__init__.py +0 -0
  76. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  77. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  78. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_base.py +0 -0
  79. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  80. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_detections.py +0 -0
  81. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  82. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  83. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_schema.py +0 -0
  84. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  85. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/internal_tables/manager.py +0 -0
  86. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/manager.py +0 -0
  87. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/mysql_manager.py +0 -0
  88. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/postgres_manager.py +0 -0
  89. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/database/tables.py +0 -0
  90. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/__init__.py +0 -0
  91. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/base.py +0 -0
  92. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/factory.py +0 -0
  93. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/seasonality.py +0 -0
  94. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/statistical/__init__.py +0 -0
  95. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  96. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/statistical/iqr.py +0 -0
  97. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/statistical/mad.py +0 -0
  98. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  99. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/detectors/statistical/zscore.py +0 -0
  100. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/loaders/__init__.py +0 -0
  101. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/loaders/metric_loader.py +0 -0
  102. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/loaders/query_template.py +0 -0
  103. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/__init__.py +0 -0
  104. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/error_dispatch.py +0 -0
  105. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  106. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  107. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  108. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  109. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  110. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  111. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  112. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/utils/__init__.py +0 -0
  113. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/utils/datetime_utils.py +0 -0
  114. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/utils/env_interpolation.py +0 -0
  115. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/utils/json_utils.py +0 -0
  116. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit/utils/stats.py +0 -0
  117. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit.egg-info/SOURCES.txt +0 -0
  118. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit.egg-info/dependency_links.txt +0 -0
  119. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit.egg-info/entry_points.txt +0 -0
  120. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit.egg-info/requires.txt +0 -0
  121. {detectkit-0.19.0 → detectkit-0.20.0}/detectkit.egg-info/top_level.txt +0 -0
  122. {detectkit-0.19.0 → detectkit-0.20.0}/pyproject.toml +0 -0
  123. {detectkit-0.19.0 → detectkit-0.20.0}/requirements.txt +0 -0
  124. {detectkit-0.19.0 → detectkit-0.20.0}/setup.cfg +0 -0
  125. {detectkit-0.19.0 → detectkit-0.20.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.19.0
3
+ Version: 0.20.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.19.0"
7
+ __version__ = "0.20.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -11,6 +11,17 @@ configure alerting and channels, run the pipeline, and debug why an alert did
11
11
  (or didn't) fire. Stay numpy/SQL/YAML-first and follow the project's existing
12
12
  conventions.
13
13
 
14
+ **Database access for _you_ (recommended, not required).** detectkit itself
15
+ connects to the database directly via its drivers — it **never** needs an MCP to
16
+ run. But you assist far better with **read access to the same database** (e.g. a
17
+ database MCP for the project's ClickHouse / PostgreSQL / MySQL): you can inspect
18
+ a metric's series, find real incidents to label for `dtk autotune`, sanity-check
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.
24
+
14
25
  ### Where to look (read the matching file before answering)
15
26
 
16
27
  The full, authoritative reference lives in `.claude/rules/detectkit/`. These
@@ -35,9 +35,12 @@ dtk autotune --select <sel> [--incidents FILE] [--label] [--scoring METRIC] \
35
35
  [--from DATE] [--to DATE] [--profile NAME] [--force] [--dry-run]
36
36
  ```
37
37
 
38
- - `--incidents FILE` — a labels file (below) → **supervised** tuning.
39
- - `--label` emit a self-contained HTML chart of the series to mark incidents
40
- visually; it exports a labels file. Generate-and-exit (no DB writes).
38
+ - `--incidents FILE` — a labels file (below) → **supervised** tuning. With no
39
+ labels file, an interactive terminal prompts to enter incidents inline;
40
+ declining (or running non-interactively) tunes **unsupervised**.
41
+ - `--label` — write a self-contained HTML chart of the series to
42
+ `metrics/<name>__labeler.html`; the user marks incidents in a browser and its
43
+ **Export** button downloads a labels file. Generate-and-exit (no DB writes).
41
44
  - `--scoring` — `mcc` (default), `f1`, `f_beta`, `balanced_accuracy`, `roc_auc`,
42
45
  `pr_auc`. MCC uses the whole confusion matrix and suits rare anomalies.
43
46
  - `--dry-run` — run the search but persist nothing and write no config.
@@ -76,14 +79,20 @@ autotune:
76
79
  detector_types: [mad, zscore] # restrict candidates (subset of mad/zscore/iqr)
77
80
  scoring_metric: mcc # default optimization target
78
81
  beta: 1.0 # only for scoring_metric: f_beta
79
- labels_file: incidents/orders.yml
82
+ labels_file: incidents/orders.yml # external labels file, OR inline (below)
83
+ # incidents: # inline labels — mutually exclusive with labels_file
84
+ # - {start: "2026-05-02 14:00:00", end: "2026-05-02 16:30:00", label: outage}
85
+ # - {at: "2026-05-11 09:05:00", label: deploy spike}
86
+ # incidents_timezone: UTC # interprets the naive times above (default UTC)
80
87
  seasonality_candidates: [hour, day_of_week]
81
88
  fixed_params: {window_size: 4320} # pin hyperparameters (excluded from search)
82
89
  folds: 5
83
90
  max_history: 50000 # cap training points
84
91
  ```
85
92
 
86
- Precedence: `--scoring`/`--incidents` flags override the block.
93
+ Label precedence (highest first): `--incidents` flag `labels_file` → inline
94
+ `incidents` → interactive prompt → none. `labels_file` and `incidents` are
95
+ mutually exclusive. `--scoring` likewise overrides `scoring_metric`.
87
96
 
88
97
  ## The annotated config
89
98
 
@@ -57,10 +57,13 @@ If there are no extra signals, the built-ins are enough — continue.
57
57
  ## Step 2 — Gather incident history → write the labels file
58
58
 
59
59
  Supervised tuning needs known incidents. The labels file is the contract; you
60
- fill it from the user's plain-language description. Resolve each incident to
61
- **UTC** and classify it as an interval (`{start, end}`) for a sustained incident
62
- or a point (`{at}`) for a spike. Read the resolved times back to confirm, then
63
- write `incidents/<metric>.yml` (create `incidents/` beside `metrics/`):
60
+ fill it from the user's plain-language description. (If you have read access to
61
+ the database e.g. a database MCP query the metric's series yourself to spot
62
+ candidate incidents and propose them for confirmation, rather than relying only
63
+ on memory.) Resolve each incident to **UTC** and classify it as an interval
64
+ (`{start, end}`) for a sustained incident or a point (`{at}`) for a spike. Read
65
+ the resolved times back to confirm, then write `incidents/<metric>.yml` (the
66
+ `dtk init` scaffold already created `incidents/` beside `metrics/`):
64
67
 
65
68
  ```yaml
66
69
  # incidents/<metric>.yml — known anomalies for supervised autotuning.
@@ -76,9 +79,24 @@ incidents:
76
79
  label: deploy spike
77
80
  ```
78
81
 
79
- The same schema works as JSON. If the user can't enumerate incidents, say so and
80
- go to Step 3 unsupervised or offer `dtk autotune --select <name> --label` to
81
- get a clickable HTML chart they mark visually, which exports this exact file.
82
+ The same schema works as JSON. **Inline alternative:** for just one or two
83
+ incidents, declare the same entries directly under the metric's `autotune:` block
84
+ (`incidents:` + optional `incidents_timezone:`) instead of a separate file
85
+ mutually exclusive with `labels_file`, and `--incidents` still overrides it:
86
+
87
+ ```yaml
88
+ autotune:
89
+ incidents:
90
+ - {start: "2026-05-02 14:00:00", end: "2026-05-02 16:30:00", label: outage}
91
+ - {at: "2026-05-11 09:05:00", label: deploy spike}
92
+ incidents_timezone: UTC # optional; default UTC
93
+ ```
94
+
95
+ If the user can't enumerate incidents, say so and go to Step 3 unsupervised — or
96
+ offer `dtk autotune --select <name> --label`, which writes a clickable HTML chart
97
+ to `metrics/<name>__labeler.html`; they mark incidents in a browser and its
98
+ Export button downloads a labels file in this exact format to feed back via
99
+ `--incidents`.
82
100
 
83
101
  ## Step 3 — Run autotune
84
102
 
@@ -38,6 +38,15 @@ the nearest ancestor that has it). If there is none, ask for a project name and
38
38
  run `dtk init <name>`, then work inside that folder. If several projects sit
39
39
  side by side, ask which one to set up.
40
40
 
41
+ > **Recommended (optional): give _yourself_ read access to the database.**
42
+ > detectkit connects to the DB directly via its drivers and never needs an MCP
43
+ > to run — the `profiles.yml` connection you set up below is all the *tool*
44
+ > needs. But you'll help much better with read access to the same database (e.g.
45
+ > a database MCP): you can inspect metric series, find incidents to label for
46
+ > `dtk autotune`, sanity-check queries, and confirm detections yourself. Suggest
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`.
49
+
41
50
  ## Step 1 — Pick the database backend
42
51
 
43
52
  **ClickHouse, PostgreSQL and MySQL are all fully supported.** Ask which one the
@@ -101,7 +101,11 @@ def _resolve_labels(
101
101
  autotune_cfg: AutoTuneConfig,
102
102
  project_root: Path,
103
103
  ) -> tuple[IncidentLabels, str]:
104
- """Resolve labels by precedence: --incidents > config.labels_file > interactive > none."""
104
+ """Resolve labels by precedence.
105
+
106
+ ``--incidents`` flag > config ``labels_file`` > config inline ``incidents`` >
107
+ interactive prompt > none (unsupervised).
108
+ """
105
109
  path = incidents_path or autotune_cfg.labels_file
106
110
  if path:
107
111
  file_path = Path(path)
@@ -112,6 +116,15 @@ def _resolve_labels(
112
116
  )
113
117
  return labels, f"file {file_path}"
114
118
 
119
+ if autotune_cfg.incidents:
120
+ labels = parse_incident_labels(
121
+ {"incidents": autotune_cfg.incidents, "timezone": autotune_cfg.incidents_timezone},
122
+ interval_seconds=interval_seconds,
123
+ metric_name=metric_name,
124
+ )
125
+ n = len(autotune_cfg.incidents)
126
+ return labels, f"inline config ({n} incident{'s' if n != 1 else ''})"
127
+
115
128
  if sys.stdin.isatty():
116
129
  return _prompt_labels(interval_seconds=interval_seconds), "interactive"
117
130
 
@@ -130,6 +130,48 @@ _BUCKET_SQL = {
130
130
  ),
131
131
  }
132
132
 
133
+ # Example incidents (labels) file for supervised `dtk autotune`. Lives in
134
+ # incidents/ beside metrics/; pointed at via `--incidents` or a metric's
135
+ # `autotune.labels_file`. See docs/guides/autotuning.md.
136
+ _EXAMPLE_INCIDENTS = """# Example incidents (labels) file for supervised `dtk autotune`.
137
+ #
138
+ # Tell autotune WHICH points were real incidents so it can pick the detector,
139
+ # threshold, seasonality and alert window that catch them while keeping false
140
+ # positives down. Hand it to autotune with:
141
+ #
142
+ # dtk autotune --select example_cpu_usage --incidents incidents/example_cpu_usage.yml
143
+ #
144
+ # ...or reference it from the metric's `autotune.labels_file:` (see
145
+ # metrics/example_cpu_usage.yml). Without any labels, autotune falls back to an
146
+ # unsupervised objective (low false-positive rate + stable cross-fold separation).
147
+ #
148
+ # Format:
149
+ # - YAML or JSON. ALL times are UTC unless `timezone:` says otherwise.
150
+ # - Each incident is EITHER an interval ({start, end}) for a sustained problem
151
+ # OR a point ({at}) for a single spike — never both keys. `end` is inclusive.
152
+ # - `label` is optional free text (documentation only).
153
+ #
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.
157
+
158
+ # Optional — must match the metric `name:` it labels (autotune refuses a mismatch).
159
+ metric: example_cpu_usage
160
+
161
+ # Optional — interprets the naive times below (defaults to UTC).
162
+ timezone: UTC
163
+
164
+ incidents:
165
+ # Interval incident (a sustained problem):
166
+ - start: "2026-05-02 14:00:00"
167
+ end: "2026-05-02 16:30:00"
168
+ label: example sustained spike # optional, free text
169
+
170
+ # Point incident (a single anomalous timestamp):
171
+ - at: "2026-05-11 09:05:00"
172
+ label: example one-off spike
173
+ """
174
+
133
175
  # Alert-channel section (backend-independent); appended after the profiles.
134
176
  _ALERT_CHANNELS = """
135
177
  # Alert channels (referenced by name from a metric's alerting.channels)
@@ -190,7 +232,9 @@ def run_init(project_name: str, target_dir: str, db_type: str = "clickhouse"):
190
232
  ├── detectkit_project.yml
191
233
  ├── profiles.yml
192
234
  ├── metrics/
193
- │ └── .gitkeep
235
+ │ └── example_cpu_usage.yml
236
+ ├── incidents/
237
+ │ └── example_cpu_usage.yml # labels for supervised `dtk autotune`
194
238
  └── sql/
195
239
  └── .gitkeep
196
240
  """
@@ -216,12 +260,16 @@ def run_init(project_name: str, target_dir: str, db_type: str = "clickhouse"):
216
260
 
217
261
  # Create subdirectories
218
262
  (target_path / "metrics").mkdir(exist_ok=True)
263
+ (target_path / "incidents").mkdir(exist_ok=True)
219
264
  (target_path / "sql").mkdir(exist_ok=True)
220
265
 
221
- # Create .gitkeep files
266
+ # Create .gitkeep files (incidents/ is kept by its example file below)
222
267
  (target_path / "metrics" / ".gitkeep").touch()
223
268
  (target_path / "sql" / ".gitkeep").touch()
224
269
 
270
+ # Example incidents (labels) file for supervised `dtk autotune`
271
+ (target_path / "incidents" / "example_cpu_usage.yml").write_text(_EXAMPLE_INCIDENTS)
272
+
225
273
  # Create detectkit_project.yml
226
274
  project_config = f"""# detectkit project configuration
227
275
  name: {project_name_clean}
@@ -337,6 +385,19 @@ alerting:
337
385
  no_data_alert: false # alert when the latest interval has no data
338
386
  # alert_cooldown: "2h" # recommended: suppress repeats of a persisting anomaly
339
387
 
388
+ # Auto-tuning (optional) — `dtk autotune --select example_cpu_usage` picks the
389
+ # detector config for you. Supply known incidents to tune supervised; either
390
+ # point at a labels file OR declare them inline (the two are mutually exclusive).
391
+ # autotune:
392
+ # enabled: true
393
+ # # (a) external labels file (see incidents/example_cpu_usage.yml):
394
+ # labels_file: incidents/example_cpu_usage.yml
395
+ # # (b) — or — inline incidents:
396
+ # # incidents:
397
+ # # - {start: "2026-05-02 14:00:00", end: "2026-05-02 16:30:00", label: outage}
398
+ # # - {at: "2026-05-11 09:05:00", label: deploy spike}
399
+ # # incidents_timezone: UTC # interprets the naive times above (default UTC)
400
+
340
401
  # Tags for selection
341
402
  tags:
342
403
  - critical
@@ -368,6 +429,7 @@ detectkit monitoring project.
368
429
  - `detectkit_project.yml` - Project configuration
369
430
  - `profiles.yml` - Database connection profiles
370
431
  - `metrics/` - Metric definitions (YAML files)
432
+ - `incidents/` - Labeled incidents for supervised `dtk autotune` (optional)
371
433
  - `sql/` - SQL query files (optional)
372
434
 
373
435
  ## Commands
@@ -320,7 +320,11 @@ class AutoTuneConfig(BaseModel):
320
320
  detector_types: [mad, zscore] # restrict candidates
321
321
  scoring_metric: mcc # optimization target
322
322
  beta: 1.0 # only for scoring_metric: f_beta
323
- labels_file: incidents/orders.yml
323
+ labels_file: incidents/orders.yml # external labels file, OR inline:
324
+ incidents: # inline labels (mutually exclusive)
325
+ - {start: "2026-05-02 14:00:00", end: "2026-05-02 16:30:00"}
326
+ - {at: "2026-05-11 09:05:00"}
327
+ incidents_timezone: UTC # interprets the naive times above
324
328
  seasonality_candidates: [hour, day_of_week]
325
329
  fixed_params: # pinned, not searched
326
330
  window_size: 4320
@@ -344,6 +348,16 @@ class AutoTuneConfig(BaseModel):
344
348
  labels_file: str | None = Field(
345
349
  default=None, description="Project-relative path to a canonical labels file"
346
350
  )
351
+ incidents: list[dict[str, Any]] | None = Field(
352
+ default=None,
353
+ description="Inline labeled incidents (alternative to labels_file). Each entry is "
354
+ "{start, end} for a sustained incident or {at} for a single point; an optional "
355
+ "'label' is free-text documentation.",
356
+ )
357
+ incidents_timezone: str | None = Field(
358
+ default=None,
359
+ description="Timezone that interprets the naive times in `incidents` (default UTC)",
360
+ )
347
361
  seasonality_candidates: list[str] | None = Field(
348
362
  default=None, description="Restrict the seasonality columns the search may use"
349
363
  )
@@ -422,6 +436,34 @@ class AutoTuneConfig(BaseModel):
422
436
  raise ValueError("max_history must be at least 1")
423
437
  return v
424
438
 
439
+ @model_validator(mode="after")
440
+ def validate_inline_incidents(self) -> "AutoTuneConfig":
441
+ """Validate inline incidents: not alongside labels_file, and well-formed.
442
+
443
+ Reuses the canonical labels parser so an inline block is validated by the
444
+ same rules as a labels file (timestamp formats, interval-vs-point shape,
445
+ timezone), failing fast at config load.
446
+ """
447
+ if self.labels_file and self.incidents:
448
+ raise ValueError(
449
+ "Set either 'labels_file' or 'incidents', not both "
450
+ "(inline incidents and an external labels file are mutually exclusive)"
451
+ )
452
+ if self.incidents_timezone and not self.incidents:
453
+ raise ValueError("incidents_timezone has no effect without 'incidents'")
454
+ if self.incidents is not None:
455
+ # Local import to avoid a config <-> autotune import cycle at module load.
456
+ from detectkit.autotune.labels import parse_incident_labels
457
+
458
+ try:
459
+ parse_incident_labels(
460
+ {"incidents": self.incidents, "timezone": self.incidents_timezone},
461
+ interval_seconds=1,
462
+ )
463
+ except Exception as exc: # ValueError, bad timezone (KeyError), etc.
464
+ raise ValueError(f"invalid 'incidents': {exc}") from exc
465
+ return self
466
+
425
467
 
426
468
  class MetricConfig(BaseModel):
427
469
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.19.0
3
+ Version: 0.20.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes