detectkit 0.26.0__tar.gz → 0.27.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 (126) hide show
  1. {detectkit-0.26.0/detectkit.egg-info → detectkit-0.27.0}/PKG-INFO +1 -1
  2. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/config_emitter.py +1 -0
  4. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/grid_search.py +25 -1
  5. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/html_labeler.py +21 -6
  6. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/window_select.py +44 -0
  7. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/rules/autotune.md +9 -1
  8. {detectkit-0.26.0 → detectkit-0.27.0/detectkit.egg-info}/PKG-INFO +1 -1
  9. {detectkit-0.26.0 → detectkit-0.27.0}/LICENSE +0 -0
  10. {detectkit-0.26.0 → detectkit-0.27.0}/MANIFEST.in +0 -0
  11. {detectkit-0.26.0 → detectkit-0.27.0}/README.md +0 -0
  12. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/__init__.py +0 -0
  13. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/__init__.py +0 -0
  14. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/base.py +0 -0
  15. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/branding.py +0 -0
  16. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/email.py +0 -0
  17. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/factory.py +0 -0
  18. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/mattermost.py +0 -0
  19. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/slack.py +0 -0
  20. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/telegram.py +0 -0
  21. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/channels/webhook.py +0 -0
  22. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  23. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  24. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  25. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  26. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  27. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  28. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  29. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  30. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/__init__.py +0 -0
  31. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/_base.py +0 -0
  32. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/_types.py +0 -0
  33. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/autotuner.py +0 -0
  34. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/crossval.py +0 -0
  35. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/detector_select.py +0 -0
  36. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/distribution.py +0 -0
  37. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/label_server.py +0 -0
  38. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/labels.py +0 -0
  39. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/result.py +0 -0
  40. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/scoring.py +0 -0
  41. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/seasonality_search.py +0 -0
  42. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/autotune/settings.py +0 -0
  43. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/__init__.py +0 -0
  44. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/_output.py +0 -0
  45. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  46. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  47. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  48. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  49. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  50. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  51. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  52. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  53. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  54. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  55. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  56. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/__init__.py +0 -0
  57. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/autotune.py +0 -0
  58. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/clean.py +0 -0
  59. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/init.py +0 -0
  60. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/init_claude.py +0 -0
  61. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/run.py +0 -0
  62. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/test_alert.py +0 -0
  63. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/commands/unlock.py +0 -0
  64. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/cli/main.py +0 -0
  65. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/config/__init__.py +0 -0
  66. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/config/metric_config.py +0 -0
  67. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/config/profile.py +0 -0
  68. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/config/project_config.py +0 -0
  69. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/config/validator.py +0 -0
  70. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/core/__init__.py +0 -0
  71. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/core/interval.py +0 -0
  72. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/core/models.py +0 -0
  73. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/__init__.py +0 -0
  74. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/_sql_manager.py +0 -0
  75. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/clickhouse_manager.py +0 -0
  76. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/__init__.py +0 -0
  77. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  78. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  79. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_base.py +0 -0
  80. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  81. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_detections.py +0 -0
  82. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  83. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  84. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_schema.py +0 -0
  85. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  86. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/internal_tables/manager.py +0 -0
  87. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/manager.py +0 -0
  88. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/mysql_manager.py +0 -0
  89. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/postgres_manager.py +0 -0
  90. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/database/tables.py +0 -0
  91. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/__init__.py +0 -0
  92. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/base.py +0 -0
  93. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/factory.py +0 -0
  94. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/seasonality.py +0 -0
  95. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/statistical/__init__.py +0 -0
  96. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  97. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/statistical/iqr.py +0 -0
  98. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/statistical/mad.py +0 -0
  99. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  100. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/detectors/statistical/zscore.py +0 -0
  101. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/loaders/__init__.py +0 -0
  102. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/loaders/metric_loader.py +0 -0
  103. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/loaders/query_template.py +0 -0
  104. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/__init__.py +0 -0
  105. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/error_dispatch.py +0 -0
  106. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  107. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  108. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  109. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  110. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  111. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  112. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  113. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/utils/__init__.py +0 -0
  114. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/utils/datetime_utils.py +0 -0
  115. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/utils/env_interpolation.py +0 -0
  116. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/utils/json_utils.py +0 -0
  117. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit/utils/stats.py +0 -0
  118. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit.egg-info/SOURCES.txt +0 -0
  119. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit.egg-info/dependency_links.txt +0 -0
  120. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit.egg-info/entry_points.txt +0 -0
  121. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit.egg-info/requires.txt +0 -0
  122. {detectkit-0.26.0 → detectkit-0.27.0}/detectkit.egg-info/top_level.txt +0 -0
  123. {detectkit-0.26.0 → detectkit-0.27.0}/pyproject.toml +0 -0
  124. {detectkit-0.26.0 → detectkit-0.27.0}/requirements.txt +0 -0
  125. {detectkit-0.26.0 → detectkit-0.27.0}/setup.cfg +0 -0
  126. {detectkit-0.26.0 → detectkit-0.27.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.26.0
3
+ Version: 0.27.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.26.0"
7
+ __version__ = "0.27.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -23,6 +23,7 @@ _RULE = "# " + "─" * 61
23
23
  _STAGE_LABELS = {
24
24
  "seasonality": "SEASONALITY",
25
25
  "detector_select": "DETECTOR",
26
+ "regime": "REGIME",
26
27
  "grid_search": "GRID SEARCH",
27
28
  "window": "WINDOW",
28
29
  }
@@ -15,7 +15,12 @@ from typing import Any
15
15
 
16
16
  from detectkit.autotune._base import _AutoTuneBase
17
17
  from detectkit.autotune._types import CandidateEval
18
- from detectkit.autotune.window_select import min_samples_for, select_window, trend_present
18
+ from detectkit.autotune.window_select import (
19
+ detect_level_shift,
20
+ min_samples_for,
21
+ select_window,
22
+ trend_present,
23
+ )
19
24
  from detectkit.detectors.factory import DetectorFactory
20
25
 
21
26
 
@@ -39,6 +44,25 @@ def grid_search(
39
44
  base["seasonality_components"] = seasonality
40
45
 
41
46
  has_trend = trend_present(tuner)
47
+ if not has_trend:
48
+ # The trend gate is a single midpoint-median test, so it silently misses a
49
+ # level shift that sits off-center (both halves straddle it) or one big
50
+ # enough to inflate the global MAD it is measured against. When that
51
+ # happens the engine treats the series as stationary — prefers the largest
52
+ # window, skips detrend — and the baseline quietly averages two regimes.
53
+ # Surface it so the user can narrow the window and re-tune; advisory only.
54
+ found, sigmas, frac = detect_level_shift(tuner)
55
+ if found:
56
+ tuner.log(
57
+ "regime",
58
+ f"series reads stationary, but a large level shift (~{sigmas:.1f}σ "
59
+ f"within-regime) sits ~{round(frac * 100)}% into the training window — "
60
+ "the midpoint trend test misses an off-center shift, so the baseline "
61
+ "may average two regimes. If the earlier regime is stale, re-tune with "
62
+ "`--from <date after the shift>` (or set `autotune.max_history`).",
63
+ shift_sigmas=round(sigmas, 2),
64
+ shift_fraction=round(frac, 3),
65
+ )
42
66
  eps = tuner.settings.min_improvement
43
67
  best_overall: CandidateEval | None = None
44
68
 
@@ -121,6 +121,9 @@ _TEMPLATE = """<!doctype html>
121
121
  .thbar input.num { width: 84px; font-family: var(--mono); }
122
122
  .thbar input:focus, .thbar select:focus { outline: none; border-color: var(--nodata); }
123
123
  .thbar button { padding: 7px 13px; }
124
+ .thbar .thscope { color: var(--faint); font-size: 12px; white-space: nowrap; }
125
+ .thbar .thscope.hint { font-style: italic; }
126
+ .thbar .thscope b { color: var(--nodata); font-weight: 600; font-style: normal; }
124
127
  canvas#c { width: 100%; height: clamp(300px, 44vh, 500px); display:block; touch-action: none;
125
128
  background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: crosshair; }
126
129
  .zoombar { display:flex; align-items:center; gap:8px; margin: 10px 0 6px; }
@@ -177,6 +180,7 @@ _TEMPLATE = """<!doctype html>
177
180
  <label>bridge gaps ≤
178
181
  <input id="thgap" class="num" type="number" min="0" step="1" value="0" /> intervals
179
182
  </label>
183
+ <span id="thscope" class="thscope"></span>
180
184
  <button id="thwin" class="ghost" style="display:none" title="capture across the whole current view again">↺ whole view</button>
181
185
  <button id="thadd" class="primary" disabled>Add 0 spans</button>
182
186
  <button id="thdone" class="ghost">Done</button>
@@ -330,7 +334,7 @@ function thRuns() { const val=thEff(); if (val===null) return [];
330
334
  if (s!==null) runs.push([s,e]);
331
335
  return runs; }
332
336
  function thCount() { const n=thRuns().length;
333
- thaddEl.textContent = 'Add '+n+' span'+(n===1?'':'s'); thaddEl.disabled = n===0; }
337
+ thaddEl.textContent = 'Add '+n+' span'+(n===1?'':'s'); thaddEl.disabled = n===0; updateThWin(); }
334
338
  // Add a captured span, merging it into any overlapping incidents (a single span
335
339
  // can bridge several) into one band that keeps the first one's label.
336
340
  function addCaptured(a,b) {
@@ -414,10 +418,13 @@ function draw() {
414
418
  // Readout while picking a threshold: the line value, side, and how many spans
415
419
  // would be captured.
416
420
  function drawThLabel() {
417
- const val=thEff(); if (val===null) return;
418
- const n=thRuns().length, win=capRange(), narrow = !!(thDragWin || capWin);
419
- const text='line '+fmtVal(val)+' · '+thdirEl.value+' · '+n+' span'+(n===1?'':'s')
420
- + (narrow ? (' · '+fmtDur(win[1]-win[0])+' window') : '');
421
+ const val=thEff(), win=capRange(), narrow = !!(thDragWin || capWin);
422
+ let text;
423
+ if (val===null) { // no line yet — prompt how to use the mode
424
+ text = 'drag the chart to pick a period · hover or type a value to set the line';
425
+ } else { const n=thRuns().length;
426
+ text='line '+fmtVal(val)+' · '+thdirEl.value+' · '+n+' span'+(n===1?'':'s')
427
+ + (narrow ? (' · '+fmtDur(win[1]-win[0])+' window') : ''); }
421
428
  ctx.font=(11*dpr)+'px ui-monospace, monospace';
422
429
  const tw=ctx.measureText(text).width, bw=tw+14*dpr, bh=22*dpr, bx=M.l*dpr+6*dpr, by=M.t*dpr+2;
423
430
  ctx.fillStyle='rgba(27,25,22,0.96)'; ctx.strokeStyle='#f0ad4e'; ctx.lineWidth=1*dpr;
@@ -615,7 +622,15 @@ const thbtnEl=document.getElementById('thbtn'), thbarEl=document.getElementById(
615
622
  const thvalEl=document.getElementById('thval'), thdirEl=document.getElementById('thdir');
616
623
  const thgapEl=document.getElementById('thgap'), thaddEl=document.getElementById('thadd');
617
624
  const thdoneEl=document.getElementById('thdone'), thwinEl=document.getElementById('thwin');
618
- function updateThWin() { thwinEl.style.display = capWin ? '' : 'none'; }
625
+ const thscopeEl=document.getElementById('thscope');
626
+ // Always-visible scope readout so the time-window control is discoverable
627
+ // (the ✕/↺ reset only appears once a window exists).
628
+ function updateThWin() {
629
+ thwinEl.style.display = capWin ? '' : 'none';
630
+ if (!thscopeEl) return;
631
+ const w = thDragWin || capWin;
632
+ if (w) { const r=capRange(); thscopeEl.innerHTML = 'period: <b>'+fmtDur(r[1]-r[0])+'</b>'; thscopeEl.className='thscope'; }
633
+ else { thscopeEl.textContent = 'period: current view — drag the chart to limit it'; thscopeEl.className='thscope hint'; } }
619
634
  thwinEl.onclick = () => { capWin=null; updateThWin(); thCount(); draw(); };
620
635
  thbtnEl.onclick = () => toggleTh();
621
636
  thdoneEl.onclick = () => toggleTh(false);
@@ -55,6 +55,50 @@ def trend_present(tuner: _AutoTuneBase) -> bool:
55
55
  return abs(med_second - med_first) > 2.0 * 1.4826 * mad
56
56
 
57
57
 
58
+ # A level shift is "large" when the two regimes' centers differ by at least this
59
+ # many within-regime robust sigmas. Measured against the *within-segment* scale
60
+ # (not the global MAD) so a big step can't self-mask by inflating the yardstick.
61
+ _SHIFT_SIGMA_BAR = 3.0
62
+ _SHIFT_MIN_POINTS = 32 # too few points to meaningfully talk about two regimes
63
+ _SHIFT_MIN_SIDE_FRAC = 0.1 # each candidate segment must hold ≥10% of the points
64
+ _SHIFT_SCAN_SPLITS = 24 # coarse grid of candidate split points to scan
65
+
66
+
67
+ def detect_level_shift(tuner: _AutoTuneBase) -> tuple[bool, float, float]:
68
+ """Scan for the strongest single level shift anywhere in the series.
69
+
70
+ Complements :func:`trend_present`, which only compares the two *halves'*
71
+ medians against the *global* MAD and so misses a shift that (a) sits
72
+ off-center — both halves then straddle it — or (b) self-masks by inflating the
73
+ global MAD it is measured against. This scans candidate split points across
74
+ the series and scores each step against the **within-segment** robust scale,
75
+ which a true step does not inflate (a smooth ramp, by contrast, keeps a large
76
+ within-segment spread and so does not register). Returns ``(found,
77
+ magnitude_sigmas, location_fraction)``; ``found`` is ``True`` only when the
78
+ strongest step clears :data:`_SHIFT_SIGMA_BAR` within-regime sigmas.
79
+ """
80
+ v = np.asarray(tuner.data["value"], dtype=float)
81
+ v = v[~np.isnan(v)]
82
+ n = int(v.size)
83
+ min_side = max(4, int(n * _SHIFT_MIN_SIDE_FRAC))
84
+ if n < _SHIFT_MIN_POINTS or n - 2 * min_side < 1:
85
+ return (False, 0.0, 0.0)
86
+ step = max(1, (n - 2 * min_side) // _SHIFT_SCAN_SPLITS)
87
+ best_sigmas = 0.0
88
+ best_frac = 0.0
89
+ for s in range(min_side, n - min_side + 1, step):
90
+ med_l = float(np.median(v[:s]))
91
+ med_r = float(np.median(v[s:]))
92
+ delta = abs(med_r - med_l)
93
+ if delta <= 0:
94
+ continue
95
+ within = float(np.median(np.abs(np.concatenate([v[:s] - med_l, v[s:] - med_r]))))
96
+ sigmas = delta / (1.4826 * within) if within > 0 else 99.0
97
+ if sigmas > best_sigmas:
98
+ best_sigmas, best_frac = sigmas, s / n
99
+ return (best_sigmas >= _SHIFT_SIGMA_BAR, min(best_sigmas, 99.0), best_frac)
100
+
101
+
58
102
  def select_window(
59
103
  tuner: _AutoTuneBase,
60
104
  detector_type: str,
@@ -33,7 +33,15 @@ same windowed detectors and `detector_id` identity). The fastest path is the
33
33
  series prefers the **larger** `window_size` ("more history is better"), a
34
34
  trending / regime-shifting one the **smaller**; sets `loading_start_time` to
35
35
  cover the lead-in (and pins the detector's `start_time` to it, so the first
36
- `dtk run` detects across all loaded history).
36
+ `dtk run` detects across all loaded history). The trend gate is a midpoint
37
+ test, so it can miss a level shift that sits off-center or self-masks by
38
+ inflating the global MAD; a backstop scan then logs a **`REGIME`** advisory in
39
+ the decision log (and streams it) when the series reads stationary yet a large
40
+ (≥3σ within-regime) level shift is present — surface it to the user and suggest
41
+ re-tuning with `--from <date after the shift>` (or `autotune.max_history`) if
42
+ the earlier regime is stale. Advisory only; it changes no chosen parameters,
43
+ and it detects level shifts, not variance/shape changes (label incidents for
44
+ those).
37
45
  5. **Alert window** (supervised only) — sweeps `consecutive_anomalies` on the
38
46
  labeled incidents.
39
47
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.26.0
3
+ Version: 0.27.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