detectkit 0.22.0__tar.gz → 0.23.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.
- {detectkit-0.22.0/detectkit.egg-info → detectkit-0.23.0}/PKG-INFO +1 -1
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/__init__.py +1 -1
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/html_labeler.py +113 -19
- detectkit-0.23.0/detectkit/autotune/label_server.py +166 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/rules/autotune.md +23 -18
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +17 -13
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/autotune.py +56 -19
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/main.py +16 -2
- {detectkit-0.22.0 → detectkit-0.23.0/detectkit.egg-info}/PKG-INFO +1 -1
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit.egg-info/SOURCES.txt +1 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/LICENSE +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/MANIFEST.in +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/README.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/core/models.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/pyproject.toml +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/requirements.txt +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/setup.cfg +0 -0
- {detectkit-0.22.0 → detectkit-0.23.0}/setup.py +0 -0
|
@@ -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.
|
|
7
|
+
__version__ = "0.23.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -68,8 +68,16 @@ _TEMPLATE = """<!doctype html>
|
|
|
68
68
|
button.primary:hover { background: var(--clay-700); }
|
|
69
69
|
button.ghost { background: transparent; color: var(--term-text); border: 1px solid var(--term-border); }
|
|
70
70
|
button.ghost:hover { border-color: var(--faint); color: var(--paper); }
|
|
71
|
+
input.setname { background: var(--term-surface); color: var(--paper); border: 1px solid var(--term-border);
|
|
72
|
+
border-radius: 7px; padding: 9px 11px; font-family: var(--ui); font-size: 13px; min-width: 200px; }
|
|
73
|
+
input.setname::placeholder { color: var(--muted); }
|
|
74
|
+
input.setname:focus { outline: none; border-color: var(--clay); }
|
|
71
75
|
.summary { margin-left: auto; color: var(--faint); font-size: 12.5px; font-family: var(--mono); }
|
|
72
76
|
.summary b { color: var(--clay); font-weight: 600; }
|
|
77
|
+
.savemsg { margin: 4px 2px 0; font-size: 13px; display: none; }
|
|
78
|
+
.savemsg.ok { display: block; color: var(--accent-green, #2e9e73); }
|
|
79
|
+
.savemsg.err { display: block; color: var(--anomaly); }
|
|
80
|
+
.savemsg.info { display: block; color: var(--faint); }
|
|
73
81
|
canvas#c { width: 100%; height: clamp(300px, 44vh, 500px); display:block; touch-action: none;
|
|
74
82
|
background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 10px; cursor: crosshair; }
|
|
75
83
|
.zoombar { display:flex; align-items:center; gap:8px; margin: 10px 0 6px; }
|
|
@@ -103,18 +111,21 @@ _TEMPLATE = """<!doctype html>
|
|
|
103
111
|
<b>Export</b>. Save the file into <code class="k">incidents/__METRIC__/</code> and run
|
|
104
112
|
<code class="k">dtk autotune --select __METRIC__ --incidents incidents/__METRIC__/</code></p>
|
|
105
113
|
<div class="toolbar">
|
|
114
|
+
<input id="setname" class="setname" type="text" placeholder="name this set (optional)" />
|
|
106
115
|
<button id="export" class="primary">Export labels</button>
|
|
107
116
|
<button id="clear" class="ghost">Clear all</button>
|
|
108
117
|
<span id="summary" class="summary"></span>
|
|
109
118
|
</div>
|
|
119
|
+
<div id="savemsg" class="savemsg"></div>
|
|
110
120
|
<canvas id="c" aria-label="metric series — drag to mark an incident, scroll to zoom"></canvas>
|
|
111
121
|
<div class="zoombar">
|
|
112
122
|
<button id="zreset" class="ghost">Reset zoom</button>
|
|
113
123
|
<span id="range" class="rangelbl"></span>
|
|
114
124
|
</div>
|
|
115
125
|
<canvas id="ov" aria-label="navigator — drag the window to pan, its edges to stretch the view"></canvas>
|
|
116
|
-
<div class="navhint">
|
|
117
|
-
|
|
126
|
+
<div class="navhint">Drag on an empty area to mark an incident · drag an existing incident's edges to
|
|
127
|
+
adjust it, or its middle to move it · scroll to zoom, double-click to reset · drag the navigator
|
|
128
|
+
window below to pan, its edges to stretch / squeeze the view.</div>
|
|
118
129
|
<div id="empty" class="empty">No incidents marked yet — drag across a span on the chart above.</div>
|
|
119
130
|
<ul id="list"></ul>
|
|
120
131
|
<footer>All times UTC · self-contained, nothing leaves your browser · re-label any time —
|
|
@@ -123,6 +134,10 @@ _TEMPLATE = """<!doctype html>
|
|
|
123
134
|
</div>
|
|
124
135
|
<script>
|
|
125
136
|
const DATA = __PAYLOAD__;
|
|
137
|
+
// When served by `dtk autotune --label` (local server), this is the save endpoint
|
|
138
|
+
// and Export POSTs straight into incidents/<metric>/. As a static file it is null,
|
|
139
|
+
// and Export falls back to a browser download.
|
|
140
|
+
const SAVE_URL = __SAVE_URL__;
|
|
126
141
|
const pts = DATA.points.map(p => ({ts: Date.parse(p.t.replace(' ','T')+'Z'), v: p.v}));
|
|
127
142
|
const N = pts.length;
|
|
128
143
|
const vraw = pts.filter(p => p.v !== null).map(p => p.v);
|
|
@@ -218,8 +233,12 @@ function draw() {
|
|
|
218
233
|
ctx.save(); ctx.beginPath(); ctx.rect(M.l*dpr, M.t*dpr, plotW(), plotH()); ctx.clip();
|
|
219
234
|
incidents.forEach(iv => { const x0=px(iv.a), x1=px(iv.b);
|
|
220
235
|
ctx.fillStyle='rgba(214,50,50,0.20)'; ctx.fillRect(x0, M.t*dpr, x1-x0, plotH());
|
|
221
|
-
ctx.strokeStyle='rgba(214,50,50,0.55)'; ctx.lineWidth=1*dpr; ctx.strokeRect(x0, M.t*dpr, x1-x0, plotH());
|
|
222
|
-
|
|
236
|
+
ctx.strokeStyle='rgba(214,50,50,0.55)'; ctx.lineWidth=1*dpr; ctx.strokeRect(x0, M.t*dpr, x1-x0, plotH());
|
|
237
|
+
// draggable edge handles
|
|
238
|
+
ctx.fillStyle='rgba(214,50,50,0.95)';
|
|
239
|
+
ctx.fillRect(x0-1.5*dpr, M.t*dpr, 3*dpr, plotH());
|
|
240
|
+
ctx.fillRect(x1-1.5*dpr, M.t*dpr, 3*dpr, plotH()); });
|
|
241
|
+
if (dragging && dragging.mode==='new') { const x0=px(dragging.a), x1=px(dragging.b);
|
|
223
242
|
ctx.fillStyle='rgba(240,173,78,0.28)'; ctx.fillRect(Math.min(x0,x1), M.t*dpr, Math.abs(x1-x0), plotH()); }
|
|
224
243
|
drawSeries(ctx, px, py, viewMin, viewMax, M.l*dpr, plotW(), '#d15b36', 1.5);
|
|
225
244
|
ctx.restore();
|
|
@@ -272,9 +291,45 @@ const ovEdgeCss = ts => { const r=ov.getBoundingClientRect();
|
|
|
272
291
|
c.addEventListener('wheel', e => { e.preventDefault(); const t=tsAt(e.clientX);
|
|
273
292
|
let s=clamp(vspan()*Math.pow(1.0015, e.deltaY), minSpan, fullSpan);
|
|
274
293
|
const f=(t-viewMin)/(vspan()||1); setView(t-f*s, t-f*s+s); }, {passive:false});
|
|
275
|
-
|
|
294
|
+
// Hit-test an existing incident edge / body in CSS px (for editing vs creating).
|
|
295
|
+
const EDGE_PX = 6;
|
|
296
|
+
const minStep = () => Math.max(step, 1);
|
|
297
|
+
const pxCss = ts => { const r=c.getBoundingClientRect();
|
|
298
|
+
return M.l + (ts-viewMin)/(vspan()||1)*(r.width-(M.l+M.r)); };
|
|
299
|
+
function hitIncident(clientX) {
|
|
300
|
+
const x = clientX - c.getBoundingClientRect().left;
|
|
301
|
+
for (let i=0;i<incidents.length;i++) { const xa=pxCss(incidents[i].a), xb=pxCss(incidents[i].b);
|
|
302
|
+
if (Math.abs(x-xa)<=EDGE_PX) return {i, edge:'a'};
|
|
303
|
+
if (Math.abs(x-xb)<=EDGE_PX) return {i, edge:'b'}; }
|
|
304
|
+
for (let i=0;i<incidents.length;i++) { const xa=pxCss(incidents[i].a), xb=pxCss(incidents[i].b);
|
|
305
|
+
if (x>xa+EDGE_PX && x<xb-EDGE_PX) return {i, edge:'move'}; }
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
c.addEventListener('mousedown', e => {
|
|
309
|
+
const hit = hitIncident(e.clientX), t = tsAt(e.clientX);
|
|
310
|
+
if (hit && hit.edge==='move') { const iv=incidents[hit.i];
|
|
311
|
+
dragging={mode:'move', i:hit.i, grab:t, a0:iv.a, b0:iv.b, sx:e.clientX, cx:e.clientX}; }
|
|
312
|
+
else if (hit) dragging={mode:'edge', i:hit.i, edge:hit.edge, sx:e.clientX, cx:e.clientX};
|
|
313
|
+
else dragging={mode:'new', a:t, b:t, sx:e.clientX, cx:e.clientX};
|
|
314
|
+
});
|
|
276
315
|
c.addEventListener('mousemove', e => { if (ovAct) return;
|
|
277
|
-
if (dragging) {
|
|
316
|
+
if (dragging) {
|
|
317
|
+
dragging.cx=e.clientX; const t=tsAt(e.clientX);
|
|
318
|
+
if (dragging.mode==='new') { dragging.b=t; }
|
|
319
|
+
else if (dragging.mode==='edge') { const iv=incidents[dragging.i]; if (!iv) return;
|
|
320
|
+
if (dragging.edge==='a') iv.a=clamp(Math.min(t, iv.b-minStep()), tmin, tmax);
|
|
321
|
+
else iv.b=clamp(Math.max(t, iv.a+minStep()), tmin, tmax); }
|
|
322
|
+
else if (dragging.mode==='move') { const iv=incidents[dragging.i]; if (!iv) return;
|
|
323
|
+
let na=dragging.a0+(t-dragging.grab), nb=dragging.b0+(t-dragging.grab);
|
|
324
|
+
if (na<tmin) { nb+=tmin-na; na=tmin; } if (nb>tmax) { na-=nb-tmax; nb=tmax; }
|
|
325
|
+
iv.a=clamp(na,tmin,tmax); iv.b=clamp(nb,tmin,tmax); }
|
|
326
|
+
draw();
|
|
327
|
+
} else {
|
|
328
|
+
const hit=hitIncident(e.clientX);
|
|
329
|
+
c.style.cursor = hit ? (hit.edge==='move' ? 'grab' : 'ew-resize') : 'crosshair';
|
|
330
|
+
hover={ts:tsAt(e.clientX)}; draw();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
278
333
|
c.addEventListener('mouseleave', () => { if (!dragging) { hover=null; draw(); } });
|
|
279
334
|
|
|
280
335
|
ov.addEventListener('mousedown', e => { e.preventDefault(); ov.style.cursor='grabbing';
|
|
@@ -297,10 +352,13 @@ window.addEventListener('mousemove', e => { if (!ovAct) return; const t=ovTsAtCs
|
|
|
297
352
|
window.addEventListener('mouseup', () => {
|
|
298
353
|
if (ovAct) { ovAct=null; return; }
|
|
299
354
|
if (!dragging) return;
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
355
|
+
if (dragging.mode==='new') {
|
|
356
|
+
if (Math.abs(dragging.cx-dragging.sx) > 4) {
|
|
357
|
+
const a=clamp(Math.min(dragging.a,dragging.b),tmin,tmax), b=clamp(Math.max(dragging.a,dragging.b),tmin,tmax);
|
|
358
|
+
incidents.push({a, b, label:''});
|
|
359
|
+
}
|
|
360
|
+
} else { const iv=incidents[dragging.i]; // edge/move: keep start <= end
|
|
361
|
+
if (iv && iv.a>iv.b) { const t=iv.a; iv.a=iv.b; iv.b=t; } }
|
|
304
362
|
dragging=null; render();
|
|
305
363
|
});
|
|
306
364
|
|
|
@@ -326,17 +384,40 @@ function render() {
|
|
|
326
384
|
drawAll();
|
|
327
385
|
}
|
|
328
386
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
387
|
+
const slug = s => (String(s).toLowerCase().replace(/[^a-z0-9_-]+/g,'-').replace(/^-+|-+$/g,'') || '__METRIC__');
|
|
388
|
+
const setMsg = (text, cls) => { const el=document.getElementById('savemsg');
|
|
389
|
+
el.textContent=text; el.className='savemsg '+cls; };
|
|
390
|
+
const buildYaml = () => {
|
|
333
391
|
let y='metric: __METRIC__\\ntimezone: UTC\\nincidents:\\n';
|
|
334
392
|
const sorted=incidents.slice().sort((p,q)=>p.a-q.a);
|
|
335
393
|
if (!sorted.length) y+=' []\\n';
|
|
336
394
|
sorted.forEach(iv => { y+=' - {start: "'+fmtTs(iv.a)+'", end: "'+fmtTs(iv.b)+'"'
|
|
337
395
|
+ (iv.label && iv.label.trim() ? ', label: '+yamlStr(iv.label.trim()) : '') + '}\\n'; });
|
|
338
|
-
|
|
339
|
-
|
|
396
|
+
return y;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const exportBtn = document.getElementById('export');
|
|
400
|
+
if (SAVE_URL) exportBtn.textContent = 'Save & tune';
|
|
401
|
+
exportBtn.onclick = () => {
|
|
402
|
+
const y = buildYaml();
|
|
403
|
+
const name = document.getElementById('setname').value;
|
|
404
|
+
if (SAVE_URL) {
|
|
405
|
+
setMsg('Saving…', 'info'); exportBtn.disabled = true;
|
|
406
|
+
fetch(SAVE_URL, {method:'POST', headers:{'Content-Type':'application/json'},
|
|
407
|
+
body: JSON.stringify({name: name, yaml: y})})
|
|
408
|
+
.then(r => r.ok ? r.json() : r.text().then(t => { throw new Error(t || ('HTTP '+r.status)); }))
|
|
409
|
+
.then(res => setMsg('Saved to ' + res.saved + ' — autotune is now running in your terminal. '
|
|
410
|
+
+ 'You can close this tab.', 'ok'))
|
|
411
|
+
.catch(e => { exportBtn.disabled = false; setMsg('Save failed: ' + e.message, 'err'); });
|
|
412
|
+
} else {
|
|
413
|
+
const d=new Date();
|
|
414
|
+
const stamp=d.getUTCFullYear()+pad2(d.getUTCMonth()+1)+pad2(d.getUTCDate())+'T'
|
|
415
|
+
+pad2(d.getUTCHours())+pad2(d.getUTCMinutes())+pad2(d.getUTCSeconds())+'Z';
|
|
416
|
+
const base = name.trim() ? slug(name) : '__METRIC__';
|
|
417
|
+
const blob=new Blob([y], {type:'text/yaml'}); const a=document.createElement('a');
|
|
418
|
+
a.href=URL.createObjectURL(blob); a.download=base+'-'+stamp+'.yml'; a.click();
|
|
419
|
+
setMsg('Downloaded ' + base + '-' + stamp + '.yml — move it into incidents/__METRIC__/ and re-run.', 'info');
|
|
420
|
+
}
|
|
340
421
|
};
|
|
341
422
|
|
|
342
423
|
function drawAll() { draw(); drawOverview();
|
|
@@ -350,8 +431,17 @@ window.addEventListener('resize', fit); fit(); render();
|
|
|
350
431
|
"""
|
|
351
432
|
|
|
352
433
|
|
|
353
|
-
def render_labeler_html(
|
|
354
|
-
|
|
434
|
+
def render_labeler_html(
|
|
435
|
+
metric_name: str, data: dict[str, np.ndarray], *, save_url: str | None = None
|
|
436
|
+
) -> str:
|
|
437
|
+
"""Return a self-contained HTML labeler page for *metric_name*'s series.
|
|
438
|
+
|
|
439
|
+
With ``save_url`` (set by ``dtk autotune --label``'s local server) the page's
|
|
440
|
+
Export button POSTs the labels straight to that endpoint; without it (a static
|
|
441
|
+
file) Export falls back to a browser download.
|
|
442
|
+
"""
|
|
443
|
+
import json
|
|
444
|
+
|
|
355
445
|
timestamps = data["timestamp"]
|
|
356
446
|
values = data["value"]
|
|
357
447
|
points = []
|
|
@@ -359,4 +449,8 @@ def render_labeler_html(metric_name: str, data: dict[str, np.ndarray]) -> str:
|
|
|
359
449
|
v = values[i]
|
|
360
450
|
points.append({"t": _ts_to_str(timestamps[i]), "v": None if np.isnan(v) else float(v)})
|
|
361
451
|
payload = json_dumps_sorted({"metric": metric_name, "points": points})
|
|
362
|
-
return
|
|
452
|
+
return (
|
|
453
|
+
_TEMPLATE.replace("__PAYLOAD__", payload)
|
|
454
|
+
.replace("__SAVE_URL__", json.dumps(save_url))
|
|
455
|
+
.replace("__METRIC__", metric_name)
|
|
456
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Local browser labeler server for ``dtk autotune --label``.
|
|
2
|
+
|
|
3
|
+
A pure-stdlib localhost server: it serves the interactive labeler page and, when
|
|
4
|
+
the user clicks **Save & tune**, validates the labels, writes a versioned file
|
|
5
|
+
into ``incidents/<metric>/`` and stops — so the command can continue straight
|
|
6
|
+
into the tuning run. Bound to 127.0.0.1 with a one-shot token; nothing is exposed
|
|
7
|
+
off the machine, and nothing is written until the user explicitly saves.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import secrets
|
|
15
|
+
import threading
|
|
16
|
+
import webbrowser
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, cast
|
|
22
|
+
from urllib.parse import parse_qs, urlparse
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from detectkit.autotune.html_labeler import render_labeler_html
|
|
27
|
+
from detectkit.autotune.labels import parse_incident_labels
|
|
28
|
+
|
|
29
|
+
_NAME_RE = re.compile(r"[^a-z0-9_-]+")
|
|
30
|
+
_MAX_BODY = 5_000_000 # generous cap on the posted labels payload
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _sanitize(name: str) -> str:
|
|
34
|
+
"""Filesystem-safe slug for a label-set name; falls back to ``incidents``."""
|
|
35
|
+
slug = _NAME_RE.sub("-", name.strip().lower()).strip("-")
|
|
36
|
+
return slug or "incidents"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _stamp() -> str:
|
|
40
|
+
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _LabelServer(ThreadingHTTPServer):
|
|
44
|
+
"""Localhost server holding the per-run state the handler reads/writes."""
|
|
45
|
+
|
|
46
|
+
# Don't block process/interpreter exit on in-flight request threads (we stop
|
|
47
|
+
# after a single save anyway); also avoids coverage's thread-tracing hanging
|
|
48
|
+
# at exit on lingering handler threads.
|
|
49
|
+
daemon_threads = True
|
|
50
|
+
|
|
51
|
+
def __init__(self, address: tuple[str, int], handler: type[BaseHTTPRequestHandler]) -> None:
|
|
52
|
+
super().__init__(address, handler)
|
|
53
|
+
self.token: str = ""
|
|
54
|
+
self.html: str = ""
|
|
55
|
+
self.metric: str = ""
|
|
56
|
+
self.incidents_dir: Path = Path(".")
|
|
57
|
+
self.interval_seconds: int = 1
|
|
58
|
+
self.saved_path: Path | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
62
|
+
def log_message(self, *args: Any) -> None: # silence default stderr logging
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
def _srv(self) -> _LabelServer:
|
|
66
|
+
return cast(_LabelServer, self.server)
|
|
67
|
+
|
|
68
|
+
def do_GET(self) -> None:
|
|
69
|
+
body = self._srv().html.encode("utf-8")
|
|
70
|
+
self.send_response(200)
|
|
71
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
72
|
+
self.send_header("Content-Length", str(len(body)))
|
|
73
|
+
self.end_headers()
|
|
74
|
+
self.wfile.write(body)
|
|
75
|
+
|
|
76
|
+
def do_POST(self) -> None:
|
|
77
|
+
srv = self._srv()
|
|
78
|
+
if parse_qs(urlparse(self.path).query).get("token", [""])[0] != srv.token:
|
|
79
|
+
self.send_error(403, "bad token")
|
|
80
|
+
return
|
|
81
|
+
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
82
|
+
if length <= 0 or length > _MAX_BODY:
|
|
83
|
+
self.send_error(413, "empty or too large")
|
|
84
|
+
return
|
|
85
|
+
try:
|
|
86
|
+
import yaml as _yaml
|
|
87
|
+
|
|
88
|
+
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
|
89
|
+
yaml_text = str(payload.get("yaml", ""))
|
|
90
|
+
set_name = _sanitize(str(payload.get("name", "")))
|
|
91
|
+
raw = _yaml.safe_load(yaml_text)
|
|
92
|
+
# validate against the canonical schema before writing anything
|
|
93
|
+
parse_incident_labels(
|
|
94
|
+
raw, interval_seconds=srv.interval_seconds, metric_name=srv.metric
|
|
95
|
+
)
|
|
96
|
+
srv.incidents_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
out = srv.incidents_dir / f"{set_name}-{_stamp()}.yml"
|
|
98
|
+
out.write_text(yaml_text, encoding="utf-8")
|
|
99
|
+
srv.saved_path = out
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
self.send_error(400, f"invalid labels: {exc}")
|
|
102
|
+
return
|
|
103
|
+
resp = json.dumps({"saved": str(out)}).encode("utf-8")
|
|
104
|
+
self.send_response(200)
|
|
105
|
+
self.send_header("Content-Type", "application/json")
|
|
106
|
+
self.send_header("Content-Length", str(len(resp)))
|
|
107
|
+
self.end_headers()
|
|
108
|
+
self.wfile.write(resp)
|
|
109
|
+
# stop serving (from this worker thread) so the command can continue
|
|
110
|
+
threading.Thread(target=srv.shutdown, daemon=True).start()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_label_server(
|
|
114
|
+
*,
|
|
115
|
+
metric_name: str,
|
|
116
|
+
data: dict[str, np.ndarray],
|
|
117
|
+
incidents_dir: Path,
|
|
118
|
+
interval_seconds: int,
|
|
119
|
+
) -> tuple[_LabelServer, str]:
|
|
120
|
+
"""Construct (without running) the labeler server; return ``(server, page_url)``."""
|
|
121
|
+
server = _LabelServer(("127.0.0.1", 0), _Handler)
|
|
122
|
+
token = secrets.token_urlsafe(16)
|
|
123
|
+
port = int(server.server_address[1])
|
|
124
|
+
server.token = token
|
|
125
|
+
server.metric = metric_name
|
|
126
|
+
server.incidents_dir = incidents_dir
|
|
127
|
+
server.interval_seconds = interval_seconds
|
|
128
|
+
server.html = render_labeler_html(
|
|
129
|
+
metric_name, data, save_url=f"http://127.0.0.1:{port}/save?token={token}"
|
|
130
|
+
)
|
|
131
|
+
return server, f"http://127.0.0.1:{port}/?token={token}"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def serve_labeler(
|
|
135
|
+
*,
|
|
136
|
+
metric_name: str,
|
|
137
|
+
data: dict[str, np.ndarray],
|
|
138
|
+
incidents_dir: Path,
|
|
139
|
+
interval_seconds: int,
|
|
140
|
+
open_browser: bool = True,
|
|
141
|
+
echo: Callable[[str], None] = print,
|
|
142
|
+
on_ready: Callable[[str], None] | None = None,
|
|
143
|
+
) -> Path | None:
|
|
144
|
+
"""Serve the labeler until the user saves (returns the file) or cancels (None)."""
|
|
145
|
+
server, url = build_label_server(
|
|
146
|
+
metric_name=metric_name,
|
|
147
|
+
data=data,
|
|
148
|
+
incidents_dir=incidents_dir,
|
|
149
|
+
interval_seconds=interval_seconds,
|
|
150
|
+
)
|
|
151
|
+
if on_ready is not None:
|
|
152
|
+
on_ready(url)
|
|
153
|
+
echo(f" Labeler: {url}")
|
|
154
|
+
echo(" Mark incidents in the browser, then click Save & tune (Ctrl-C to cancel).")
|
|
155
|
+
if open_browser:
|
|
156
|
+
try:
|
|
157
|
+
webbrowser.open(url)
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
try:
|
|
161
|
+
server.serve_forever(poll_interval=0.3)
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
return None
|
|
164
|
+
finally:
|
|
165
|
+
server.server_close()
|
|
166
|
+
return server.saved_path
|
|
@@ -47,13 +47,16 @@ dtk autotune --select <sel> [--incidents FILE] [--label] [--scoring METRIC] \
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
- `--incidents FILE|DIR` — a labels file (below) → **supervised** tuning. May be a
|
|
50
|
-
**directory** (e.g. `incidents/<name>/`)
|
|
51
|
-
|
|
52
|
-
inline; declining (or running
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
**directory** (e.g. `incidents/<name>/`): interactive runs prompt to pick a
|
|
51
|
+
version (default newest), non-interactive use the newest. With nothing given, an
|
|
52
|
+
interactive terminal prompts to enter incidents inline; declining (or running
|
|
53
|
+
non-interactively) tunes **unsupervised**.
|
|
54
|
+
- `--label` — open the interactive labeler (zoom/pan, edit incident edges,
|
|
55
|
+
per-incident descriptions, named sets). **Default:** a local 127.0.0.1 server +
|
|
56
|
+
browser; **Save & tune** writes `incidents/<name>/<set>-<UTC>.yml` and the run
|
|
57
|
+
**continues into tuning on it**. `--no-serve` writes a static
|
|
58
|
+
`metrics/<name>__labeler.html` (Export downloads the file) and exits; `--no-open`
|
|
59
|
+
prints the URL instead of launching a browser.
|
|
57
60
|
- `--scoring` — `mcc` (default), `f1`, `f_beta`, `balanced_accuracy`, `roc_auc`,
|
|
58
61
|
`pr_auc`. MCC uses the whole confusion matrix and suits rare anomalies.
|
|
59
62
|
- `--dry-run` — run the search but persist nothing and write no config.
|
|
@@ -82,17 +85,19 @@ incidents:
|
|
|
82
85
|
When labels would help, **offer the interactive HTML labeler before asking the
|
|
83
86
|
user to recall timestamps** — it is the easiest, most reliable path:
|
|
84
87
|
|
|
85
|
-
1. Run `dtk autotune --select <name> --label
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
4.
|
|
95
|
-
|
|
88
|
+
1. Run `dtk autotune --select <name> --label`. It starts a local labeler server
|
|
89
|
+
and opens the browser (use `--no-open` on a remote box and share the printed
|
|
90
|
+
127.0.0.1 URL; the user port-forwards or runs it locally).
|
|
91
|
+
2. The user marks incidents on the chart (scroll to zoom, drag the navigator to
|
|
92
|
+
move, click-drag to mark, drag an incident's edges to adjust, add a
|
|
93
|
+
description, optionally name the set), then clicks **Save & tune**.
|
|
94
|
+
3. That writes `incidents/<name>/<set>-<UTC>.yml` automatically (versioned —
|
|
95
|
+
re-labeling never overwrites) and the **same command continues into the tuning
|
|
96
|
+
run** on it. No manual file moving.
|
|
97
|
+
4. To re-tune later on saved sets, point `--incidents` at the folder
|
|
98
|
+
(`incidents/<name>/`) — interactive runs let the user pick a version. The
|
|
99
|
+
static `--no-serve` path still exists (Export downloads a file you move into
|
|
100
|
+
`incidents/<name>/`).
|
|
96
101
|
|
|
97
102
|
Prefer this whenever the user can *recognise* incidents on a chart but doesn't
|
|
98
103
|
have exact times. If they already know the times (or you found them via a DB
|
{detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md
RENAMED
|
@@ -86,21 +86,25 @@ recall timestamps:
|
|
|
86
86
|
dtk autotune --select <name> --label
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
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:
|
|
91
92
|
|
|
92
|
-
1.
|
|
93
|
-
it's self-contained (inline chart + data, no server, no internet).
|
|
94
|
-
2. Navigate a long/dense series: **scroll to zoom**, double-click to reset, drag
|
|
93
|
+
1. Navigate a long/dense series: **scroll to zoom**, double-click to reset, drag
|
|
95
94
|
the **navigator strip** to move the view (window = pan, edges = stretch).
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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* / *Clear all* fix
|
|
98
|
+
mistakes.
|
|
99
|
+
3. Click **Save & tune**. The server writes `incidents/<name>/<set>-<UTC>.yml`
|
|
100
|
+
automatically (versioned — re-labeling never overwrites) and the **same command
|
|
101
|
+
continues into the tuning run on it**. No manual file moving.
|
|
102
|
+
|
|
103
|
+
(`--no-serve` is the offline fallback: it writes a static
|
|
104
|
+
`metrics/<name>__labeler.html` whose Export downloads a versioned file you then
|
|
105
|
+
move into `incidents/<name>/` and pass via `--incidents`.) To re-tune later on a
|
|
106
|
+
saved set, point `--incidents` at `incidents/<name>/` (interactive runs let the
|
|
107
|
+
user pick a version).
|
|
104
108
|
|
|
105
109
|
Prefer this whenever the user can *recognise* incidents on a chart but doesn't
|
|
106
110
|
have exact timestamps — it is far easier than dictating times, and they label
|
|
@@ -27,6 +27,7 @@ from detectkit.autotune import (
|
|
|
27
27
|
render_labeler_html,
|
|
28
28
|
run_autotune_engine,
|
|
29
29
|
)
|
|
30
|
+
from detectkit.autotune.label_server import serve_labeler
|
|
30
31
|
from detectkit.autotune.labels import GroundTruth, IncidentLabels, parse_incident_labels
|
|
31
32
|
from detectkit.cli._output import echo_done, echo_error, echo_noop
|
|
32
33
|
from detectkit.cli.commands.run import find_project_root, parse_date, select_metrics
|
|
@@ -96,20 +97,29 @@ def _resolve_scoring(scoring_override: str | None, autotune_cfg: AutoTuneConfig)
|
|
|
96
97
|
_LABELS_GLOBS = ("*.yml", "*.yaml", "*.json")
|
|
97
98
|
|
|
98
99
|
|
|
99
|
-
def
|
|
100
|
-
"""
|
|
100
|
+
def _labels_files(directory: Path) -> list[Path]:
|
|
101
|
+
"""All labels files in *directory*, oldest→newest.
|
|
101
102
|
|
|
102
|
-
The labeler
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
every labeling round on disk while always tuning on the latest one.
|
|
103
|
+
The labeler saves versioned, ISO-stamped names (``<set>-<UTC>.yml``) which sort
|
|
104
|
+
chronologically, so name order is chronological (tie-broken by mtime). This
|
|
105
|
+
lets ``incidents/<metric>/`` keep every labeling round on disk.
|
|
106
106
|
"""
|
|
107
107
|
files: list[Path] = []
|
|
108
108
|
for pattern in _LABELS_GLOBS:
|
|
109
109
|
files.extend(directory.glob(pattern))
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
return sorted(files, key=lambda p: (p.name, p.stat().st_mtime))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _pick_labels_file(files: list[Path]) -> tuple[Path, str]:
|
|
114
|
+
"""Choose one labels file from a versioned set; prompt when interactive."""
|
|
115
|
+
newest = files[-1]
|
|
116
|
+
if len(files) == 1 or not sys.stdin.isatty():
|
|
117
|
+
return newest, "newest"
|
|
118
|
+
click.echo(" Saved label sets:")
|
|
119
|
+
for i, f in enumerate(files, 1):
|
|
120
|
+
click.echo(f" {i}) {f.name}" + (" (newest)" if f is newest else ""))
|
|
121
|
+
idx = click.prompt(f" Choose a set [1-{len(files)}]", default=len(files), type=int)
|
|
122
|
+
return files[min(max(idx, 1), len(files)) - 1], "chosen"
|
|
113
123
|
|
|
114
124
|
|
|
115
125
|
def _resolve_labels(
|
|
@@ -132,15 +142,16 @@ def _resolve_labels(
|
|
|
132
142
|
if not file_path.is_absolute():
|
|
133
143
|
file_path = project_root / file_path
|
|
134
144
|
if file_path.is_dir():
|
|
135
|
-
|
|
136
|
-
if
|
|
145
|
+
files = _labels_files(file_path)
|
|
146
|
+
if not files:
|
|
137
147
|
raise FileNotFoundError(
|
|
138
148
|
f"No labels files (*.yml / *.yaml / *.json) found in {file_path}"
|
|
139
149
|
)
|
|
150
|
+
chosen, how = _pick_labels_file(files)
|
|
140
151
|
labels = parse_labels_file(
|
|
141
152
|
chosen, interval_seconds=interval_seconds, metric_name=metric_name
|
|
142
153
|
)
|
|
143
|
-
return labels, f"file {chosen} (
|
|
154
|
+
return labels, f"file {chosen} ({how} in {file_path}/)"
|
|
144
155
|
labels = parse_labels_file(
|
|
145
156
|
file_path, interval_seconds=interval_seconds, metric_name=metric_name
|
|
146
157
|
)
|
|
@@ -248,6 +259,8 @@ def run_autotune(
|
|
|
248
259
|
select: str,
|
|
249
260
|
incidents_path: str | None,
|
|
250
261
|
label: bool,
|
|
262
|
+
no_serve: bool = False,
|
|
263
|
+
no_open: bool = False,
|
|
251
264
|
scoring_override: str | None,
|
|
252
265
|
from_date: str | None,
|
|
253
266
|
to_date: str | None,
|
|
@@ -283,6 +296,8 @@ def run_autotune(
|
|
|
283
296
|
internal_manager=internal_manager,
|
|
284
297
|
incidents_path=incidents_path,
|
|
285
298
|
label=label,
|
|
299
|
+
no_serve=no_serve,
|
|
300
|
+
no_open=no_open,
|
|
286
301
|
scoring_override=scoring_override,
|
|
287
302
|
from_dt=from_dt,
|
|
288
303
|
to_dt=to_dt,
|
|
@@ -303,6 +318,8 @@ def _tune_one(
|
|
|
303
318
|
internal_manager: InternalTablesManager,
|
|
304
319
|
incidents_path: str | None,
|
|
305
320
|
label: bool,
|
|
321
|
+
no_serve: bool = False,
|
|
322
|
+
no_open: bool = False,
|
|
306
323
|
scoring_override: str | None,
|
|
307
324
|
from_dt: datetime | None,
|
|
308
325
|
to_dt: datetime | None,
|
|
@@ -324,15 +341,35 @@ def _tune_one(
|
|
|
324
341
|
echo_noop(name, "no datapoints — run `dtk run --select " + name + " --steps load` first")
|
|
325
342
|
return False
|
|
326
343
|
|
|
327
|
-
# --label:
|
|
344
|
+
# --label: open the interactive labeler. Default — a local server that saves the
|
|
345
|
+
# marked incidents straight into incidents/<name>/ and then falls through to
|
|
346
|
+
# tuning on them. --no-serve instead writes a static HTML file and exits (you
|
|
347
|
+
# move the downloaded export in yourself); --no-open skips launching a browser.
|
|
328
348
|
if label:
|
|
329
|
-
html = render_labeler_html(name, data)
|
|
330
|
-
out = project_root / "metrics" / f"{metric_path.stem}__labeler.html"
|
|
331
|
-
out.write_text(html, encoding="utf-8")
|
|
332
349
|
click.echo(click.style(f"Processing metric: {name}", fg="cyan", bold=True))
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
350
|
+
if no_serve:
|
|
351
|
+
html = render_labeler_html(name, data)
|
|
352
|
+
out = project_root / "metrics" / f"{metric_path.stem}__labeler.html"
|
|
353
|
+
out.write_text(html, encoding="utf-8")
|
|
354
|
+
click.echo(f" Wrote labeler: {out.relative_to(project_root)}")
|
|
355
|
+
click.echo(
|
|
356
|
+
f" Open it, mark incidents, Export, save into incidents/{name}/, "
|
|
357
|
+
f"then re-run with --incidents incidents/{name}/"
|
|
358
|
+
)
|
|
359
|
+
return True
|
|
360
|
+
saved = serve_labeler(
|
|
361
|
+
metric_name=name,
|
|
362
|
+
data=data,
|
|
363
|
+
incidents_dir=project_root / "incidents" / name,
|
|
364
|
+
interval_seconds=interval_seconds,
|
|
365
|
+
open_browser=not no_open,
|
|
366
|
+
echo=click.echo,
|
|
367
|
+
)
|
|
368
|
+
if saved is None:
|
|
369
|
+
echo_noop(name, "labeling cancelled — no labels saved")
|
|
370
|
+
return False
|
|
371
|
+
click.echo(f" Saved labels: {saved.relative_to(project_root)}")
|
|
372
|
+
incidents_path = str(saved) # continue into supervised tuning on this set
|
|
336
373
|
|
|
337
374
|
click.echo(click.style(f"Tuning metric: {name}", fg="cyan", bold=True))
|
|
338
375
|
click.echo(f" Config file: {metric_path.relative_to(project_root)}")
|
|
@@ -205,7 +205,17 @@ def run(
|
|
|
205
205
|
@click.option(
|
|
206
206
|
"--label",
|
|
207
207
|
is_flag=True,
|
|
208
|
-
help="
|
|
208
|
+
help="Open the interactive labeler (local server) to mark incidents, then tune on them",
|
|
209
|
+
)
|
|
210
|
+
@click.option(
|
|
211
|
+
"--no-serve",
|
|
212
|
+
is_flag=True,
|
|
213
|
+
help="With --label, write a static HTML labeler file and exit instead of serving",
|
|
214
|
+
)
|
|
215
|
+
@click.option(
|
|
216
|
+
"--no-open",
|
|
217
|
+
is_flag=True,
|
|
218
|
+
help="With --label, don't auto-open the browser (just print the local URL)",
|
|
209
219
|
)
|
|
210
220
|
@click.option(
|
|
211
221
|
"--scoring",
|
|
@@ -240,6 +250,8 @@ def autotune(
|
|
|
240
250
|
select: str,
|
|
241
251
|
incidents_path: str,
|
|
242
252
|
label: bool,
|
|
253
|
+
no_serve: bool,
|
|
254
|
+
no_open: bool,
|
|
243
255
|
scoring_override: str,
|
|
244
256
|
from_date: str,
|
|
245
257
|
to_date: str,
|
|
@@ -266,7 +278,7 @@ def autotune(
|
|
|
266
278
|
# Unsupervised (no labels)
|
|
267
279
|
dtk autotune --select checkout_errors
|
|
268
280
|
|
|
269
|
-
#
|
|
281
|
+
# Mark incidents in a browser, save into incidents/<m>/, and tune on them
|
|
270
282
|
dtk autotune --select checkout_errors --label
|
|
271
283
|
|
|
272
284
|
# Search only, change nothing
|
|
@@ -278,6 +290,8 @@ def autotune(
|
|
|
278
290
|
select=select,
|
|
279
291
|
incidents_path=incidents_path,
|
|
280
292
|
label=label,
|
|
293
|
+
no_serve=no_serve,
|
|
294
|
+
no_open=no_open,
|
|
281
295
|
scoring_override=scoring_override,
|
|
282
296
|
from_date=from_date,
|
|
283
297
|
to_date=to_date,
|
|
@@ -39,6 +39,7 @@ detectkit/autotune/detector_select.py
|
|
|
39
39
|
detectkit/autotune/distribution.py
|
|
40
40
|
detectkit/autotune/grid_search.py
|
|
41
41
|
detectkit/autotune/html_labeler.py
|
|
42
|
+
detectkit/autotune/label_server.py
|
|
42
43
|
detectkit/autotune/labels.py
|
|
43
44
|
detectkit/autotune/result.py
|
|
44
45
|
detectkit/autotune/scoring.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md
RENAMED
|
File without changes
|
{detectkit-0.22.0 → detectkit-0.23.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|