detectkit 0.47.0__tar.gz → 0.48.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.47.0/detectkit.egg-info → detectkit-0.48.0}/PKG-INFO +1 -1
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/__init__.py +1 -1
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/rules/cli.md +13 -3
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +11 -3
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/run.py +48 -47
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/tune.py +6 -1
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/config/validator.py +40 -4
- detectkit-0.48.0/detectkit/tuning/assets/tune.js +187 -0
- detectkit-0.48.0/detectkit/tuning/config_writer.py +253 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/tuning/payload.py +81 -7
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/tuning/server.py +47 -5
- {detectkit-0.47.0 → detectkit-0.48.0/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.47.0/detectkit/tuning/assets/tune.js +0 -187
- detectkit-0.47.0/detectkit/tuning/config_writer.py +0 -138
- {detectkit-0.47.0 → detectkit-0.48.0}/LICENSE +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/MANIFEST.in +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/README.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/grid_search.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/core/models.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/statistical/_windowed.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/pyproject.toml +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/requirements.txt +0 -0
- {detectkit-0.47.0 → detectkit-0.48.0}/setup.cfg +0 -0
- {detectkit-0.47.0 → detectkit-0.48.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.48.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -97,7 +97,14 @@ anomalies**) and the **y = 0** toggle — stay visible in every mode. The chart
|
|
|
97
97
|
**zoomable** (scroll/drag + navigator strip) with a **"Points shown"** trim slider.
|
|
98
98
|
Clicking **Apply** writes the chosen
|
|
99
99
|
config back into the metric YAML **in place** (autotune, by contrast, writes a new
|
|
100
|
-
`__tuned_<id>.yml` and never edits the original).
|
|
100
|
+
`__tuned_<id>.yml` and never edits the original). Apply **merges** — it rewrites
|
|
101
|
+
only the detector(s) you tuned and keeps every **other** detector unchanged, so a
|
|
102
|
+
`manual_bounds` floor alongside a `mad` detector (and a `min_detectors: 2` alert)
|
|
103
|
+
survives a retune. If a metric configures **more than one detector**, a **Tuning
|
|
104
|
+
detector** picker in the Tune rail lets you choose which one to tune (the cockpit
|
|
105
|
+
shows one band at a time); switching re-seeds every knob from that detector, and
|
|
106
|
+
non-tunable detectors (`prophet`/`timesfm`) plus the ones you didn't touch are
|
|
107
|
+
listed as "preserved on Apply". Reads the metric's loaded
|
|
101
108
|
datapoints (run `dtk run --steps load` first if empty); the selector must resolve
|
|
102
109
|
to a single metric.
|
|
103
110
|
|
|
@@ -155,8 +162,11 @@ relative to zero.
|
|
|
155
162
|
|
|
156
163
|
Safe write-back: the config is validated before anything is written, the previous
|
|
157
164
|
YAML is archived under `metrics/.history/<metric>/`, and only then is the metric
|
|
158
|
-
overwritten
|
|
159
|
-
|
|
165
|
+
overwritten (the merge keeps your other detectors verbatim; the re-emitted header
|
|
166
|
+
names what was updated vs preserved). The `.history/` archive is **not** discovered
|
|
167
|
+
as a live metric, so a tuned metric never collides with its own snapshots as a
|
|
168
|
+
"duplicate metric name". Takes **no pipeline lock** (it only edits a config file);
|
|
169
|
+
re-run `dtk run` afterward to recompute detections under the new config.
|
|
160
170
|
`--no-serve` writes a static read-only preview HTML instead (no write-back —
|
|
161
171
|
**Save incidents** downloads the labels file); `--from` / `--to` bound the window;
|
|
162
172
|
`--no-open` prints the URL without opening a browser.
|
|
@@ -146,9 +146,17 @@ false-alert rate exceeds it (tuning-only; labeling stays optional).
|
|
|
146
146
|
When the user is happy, they click **Apply to metric**. detectkit then, in order:
|
|
147
147
|
**validates** the config (a bad/untunable config is rejected and **nothing is
|
|
148
148
|
written**), **archives** the current YAML verbatim under
|
|
149
|
-
`metrics/.history/<metric>/`, and **re-emits** the metric in place
|
|
150
|
-
detector (and
|
|
151
|
-
one). Applying ends the session; saving incidents
|
|
149
|
+
`metrics/.history/<metric>/`, and **re-emits** the metric in place, **merging** the
|
|
150
|
+
tuned detector(s) back in (and updating the first alerting block's
|
|
151
|
+
`consecutive_anomalies` if it has one). Applying ends the session; saving incidents
|
|
152
|
+
does not. **Merge, not replace:** only the detector(s) the user tuned are
|
|
153
|
+
rewritten; every other detector is kept **verbatim** — so a metric with a
|
|
154
|
+
`manual_bounds` floor alongside a `mad` detector (and a `min_detectors: 2` alert)
|
|
155
|
+
keeps both. When a metric has more than one detector, the Tune rail shows a
|
|
156
|
+
**Tuning detector** picker to choose which one to tune. If the user asks to tune a
|
|
157
|
+
metric that has several detectors, reassure them the others are preserved. (The
|
|
158
|
+
`.history/` archive is not discovered as a live metric, so it never causes a
|
|
159
|
+
"duplicate metric name" error.)
|
|
152
160
|
|
|
153
161
|
## Step 5 — Recompute under the new config
|
|
154
162
|
|
|
@@ -12,7 +12,11 @@ import click
|
|
|
12
12
|
from detectkit.config.metric_config import MetricConfig
|
|
13
13
|
from detectkit.config.profile import ProfilesConfig
|
|
14
14
|
from detectkit.config.project_config import ProjectConfig
|
|
15
|
-
from detectkit.config.validator import
|
|
15
|
+
from detectkit.config.validator import (
|
|
16
|
+
discover_metric_files,
|
|
17
|
+
is_discoverable_metric_file,
|
|
18
|
+
validate_metric_uniqueness,
|
|
19
|
+
)
|
|
16
20
|
from detectkit.database.internal_tables import InternalTablesManager
|
|
17
21
|
from detectkit.orchestration.error_dispatch import dispatch_project_error_alert
|
|
18
22
|
from detectkit.orchestration.task_manager import PipelineStep, TaskManager
|
|
@@ -433,17 +437,16 @@ def select_metrics(selector: str, project_root: Path) -> list[tuple[Path, Metric
|
|
|
433
437
|
if selector == "*":
|
|
434
438
|
# "all metrics" — search recursively so nested metrics are included
|
|
435
439
|
# (mirrors validate_project_metrics); a plain glob of "metrics/*"
|
|
436
|
-
# would only see the top level.
|
|
437
|
-
metric_paths =
|
|
440
|
+
# would only see the top level. Excludes the metrics/.history/ archive.
|
|
441
|
+
metric_paths = discover_metric_files(metrics_dir)
|
|
438
442
|
else:
|
|
439
443
|
pattern = selector if selector.startswith("metrics/") else f"metrics/{selector}"
|
|
440
444
|
# Keep only metric files: a bare glob also matches the `.gitkeep`
|
|
441
|
-
# stub created by `dtk init`, any other non-YAML files, and
|
|
442
|
-
#
|
|
445
|
+
# stub created by `dtk init`, any other non-YAML files, directories, and
|
|
446
|
+
# the metrics/.history/ archive dtk tune writes (whose snapshots keep the
|
|
447
|
+
# original name:) — all of which would crash the parser or collide on name.
|
|
443
448
|
metric_paths = [
|
|
444
|
-
p
|
|
445
|
-
for p in project_root.glob(pattern)
|
|
446
|
-
if p.is_file() and p.suffix in (".yml", ".yaml")
|
|
449
|
+
p for p in project_root.glob(pattern) if is_discoverable_metric_file(p, metrics_dir)
|
|
447
450
|
]
|
|
448
451
|
# Metric name selector
|
|
449
452
|
else:
|
|
@@ -485,26 +488,25 @@ def find_metrics_by_tag(metrics_dir: Path, tag: str) -> list[Path]:
|
|
|
485
488
|
|
|
486
489
|
matching_metrics = []
|
|
487
490
|
|
|
488
|
-
#
|
|
489
|
-
for
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
click.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
continue
|
|
491
|
+
# Live metric files only (excludes the metrics/.history/ archive).
|
|
492
|
+
for metric_file in discover_metric_files(metrics_dir):
|
|
493
|
+
try:
|
|
494
|
+
with open(metric_file) as f:
|
|
495
|
+
config = yaml.safe_load(f)
|
|
496
|
+
|
|
497
|
+
if config and "tags" in config:
|
|
498
|
+
if tag in config["tags"]:
|
|
499
|
+
matching_metrics.append(metric_file)
|
|
500
|
+
except Exception as e:
|
|
501
|
+
# Warn about unparseable files but continue searching
|
|
502
|
+
click.echo(
|
|
503
|
+
click.style(
|
|
504
|
+
f"Warning: Skipping {metric_file.relative_to(metrics_dir.parent)}: {e}",
|
|
505
|
+
fg="yellow",
|
|
506
|
+
),
|
|
507
|
+
err=True,
|
|
508
|
+
)
|
|
509
|
+
continue
|
|
508
510
|
|
|
509
511
|
return matching_metrics
|
|
510
512
|
|
|
@@ -522,25 +524,24 @@ def find_metric_by_name(metrics_dir: Path, name: str) -> Path | None:
|
|
|
522
524
|
"""
|
|
523
525
|
import yaml
|
|
524
526
|
|
|
525
|
-
#
|
|
526
|
-
for
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
click.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
continue
|
|
527
|
+
# Live metric files only (excludes the metrics/.history/ archive).
|
|
528
|
+
for metric_file in discover_metric_files(metrics_dir):
|
|
529
|
+
try:
|
|
530
|
+
with open(metric_file) as f:
|
|
531
|
+
config = yaml.safe_load(f)
|
|
532
|
+
|
|
533
|
+
if config and config.get("name") == name:
|
|
534
|
+
return metric_file
|
|
535
|
+
except Exception as e:
|
|
536
|
+
# Warn about unparseable files but continue searching
|
|
537
|
+
click.echo(
|
|
538
|
+
click.style(
|
|
539
|
+
f"Warning: Skipping {metric_file.relative_to(metrics_dir.parent)}: {e}",
|
|
540
|
+
fg="yellow",
|
|
541
|
+
),
|
|
542
|
+
err=True,
|
|
543
|
+
)
|
|
544
|
+
continue
|
|
544
545
|
|
|
545
546
|
return None
|
|
546
547
|
|
|
@@ -153,6 +153,11 @@ def run_tune(
|
|
|
153
153
|
return False
|
|
154
154
|
|
|
155
155
|
click.echo(f" Archived previous config: {applied.archived.relative_to(project_root)}")
|
|
156
|
-
|
|
156
|
+
updated = ", ".join(applied.updated) or "detector"
|
|
157
|
+
echo_done(
|
|
158
|
+
f"{name}: applied tuned detector(s) [{updated}] → {applied.saved.relative_to(project_root)}"
|
|
159
|
+
)
|
|
160
|
+
if applied.preserved:
|
|
161
|
+
click.echo(f" Preserved the metric's other detector(s): {', '.join(applied.preserved)}")
|
|
157
162
|
click.echo(f" Re-run `dtk run --select {name}` to recompute detections under the new config.")
|
|
158
163
|
return True
|
|
@@ -10,6 +10,44 @@ from pathlib import Path
|
|
|
10
10
|
from detectkit.config.metric_config import MetricConfig
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def is_discoverable_metric_file(path: Path, metrics_dir: Path) -> bool:
|
|
14
|
+
"""True for a *live* metric YAML — excludes the ``metrics/.history/`` archive.
|
|
15
|
+
|
|
16
|
+
``dtk tune`` archives the previous metric config under
|
|
17
|
+
``metrics/.history/<metric>/`` before writing the tuned version in place. Those
|
|
18
|
+
snapshots keep the original ``name:``, so discovering them as live metrics would
|
|
19
|
+
(a) flag every tuned metric as a duplicate-name conflict and (b) run stale
|
|
20
|
+
configs. Python's ``pathlib`` glob traverses hidden directories (unlike shell
|
|
21
|
+
globbing), so they must be filtered out explicitly. Any hidden path component
|
|
22
|
+
(a leading dot) *under* ``metrics/`` is skipped — this covers ``.history`` and
|
|
23
|
+
any editor/VCS scratch dir a user drops there, while a project rooted under a
|
|
24
|
+
dot-directory (checked only below ``metrics/``) is unaffected.
|
|
25
|
+
"""
|
|
26
|
+
if not path.is_file() or path.suffix not in (".yml", ".yaml"):
|
|
27
|
+
return False
|
|
28
|
+
try:
|
|
29
|
+
parts = path.relative_to(metrics_dir).parts
|
|
30
|
+
except ValueError:
|
|
31
|
+
parts = path.parts
|
|
32
|
+
return not any(part.startswith(".") for part in parts)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def discover_metric_files(metrics_dir: Path) -> list[Path]:
|
|
36
|
+
"""All live metric YAMLs under ``metrics/`` (recursive), excluding the ``.history`` archive.
|
|
37
|
+
|
|
38
|
+
The single discovery seam shared by project validation and CLI metric selection,
|
|
39
|
+
so the ``.history`` exclusion can't drift between them. Returns a sorted list for
|
|
40
|
+
deterministic ordering.
|
|
41
|
+
"""
|
|
42
|
+
files = [
|
|
43
|
+
p
|
|
44
|
+
for pattern in ("**/*.yml", "**/*.yaml")
|
|
45
|
+
for p in metrics_dir.glob(pattern)
|
|
46
|
+
if is_discoverable_metric_file(p, metrics_dir)
|
|
47
|
+
]
|
|
48
|
+
return sorted(set(files))
|
|
49
|
+
|
|
50
|
+
|
|
13
51
|
def validate_metric_uniqueness(metric_paths: list[Path]) -> list[tuple[Path, MetricConfig]]:
|
|
14
52
|
"""
|
|
15
53
|
Load all metrics and validate that metric names are unique.
|
|
@@ -106,10 +144,8 @@ def validate_project_metrics(project_root: Path) -> list[tuple[Path, MetricConfi
|
|
|
106
144
|
f" your_metric.yml\n"
|
|
107
145
|
)
|
|
108
146
|
|
|
109
|
-
# Find all metric files recursively
|
|
110
|
-
metric_paths =
|
|
111
|
-
for pattern in ["**/*.yml", "**/*.yaml"]:
|
|
112
|
-
metric_paths.extend(metrics_dir.glob(pattern))
|
|
147
|
+
# Find all metric files recursively (excluding the metrics/.history/ archive).
|
|
148
|
+
metric_paths = discover_metric_files(metrics_dir)
|
|
113
149
|
|
|
114
150
|
if not metric_paths:
|
|
115
151
|
raise ValueError(
|