detectkit 0.54.0__tar.gz → 0.55.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 (157) hide show
  1. {detectkit-0.54.0/detectkit.egg-info → detectkit-0.55.0}/PKG-INFO +1 -1
  2. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/rules/cli.md +32 -9
  4. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/rules/overview.md +3 -1
  5. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +8 -6
  6. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/ui.py +4 -1
  7. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/semantic/__init__.py +2 -0
  8. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/semantic/osi_model.py +29 -13
  9. detectkit-0.55.0/detectkit/ui/assets/ui.js +515 -0
  10. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/ui/metric_files.py +15 -0
  11. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/ui/server.py +230 -5
  12. {detectkit-0.54.0 → detectkit-0.55.0/detectkit.egg-info}/PKG-INFO +1 -1
  13. detectkit-0.54.0/detectkit/ui/assets/ui.js +0 -323
  14. {detectkit-0.54.0 → detectkit-0.55.0}/LICENSE +0 -0
  15. {detectkit-0.54.0 → detectkit-0.55.0}/MANIFEST.in +0 -0
  16. {detectkit-0.54.0 → detectkit-0.55.0}/README.md +0 -0
  17. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/__init__.py +0 -0
  18. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/__init__.py +0 -0
  19. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/base.py +0 -0
  20. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/branding.py +0 -0
  21. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/email.py +0 -0
  22. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/factory.py +0 -0
  23. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/mattermost.py +0 -0
  24. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/slack.py +0 -0
  25. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/telegram.py +0 -0
  26. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/channels/webhook.py +0 -0
  27. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  28. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  29. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  30. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  31. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  32. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  33. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  34. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  35. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  36. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/__init__.py +0 -0
  37. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/_base.py +0 -0
  38. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/_types.py +0 -0
  39. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/autotuner.py +0 -0
  40. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/axis_spec.py +0 -0
  41. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/config_emitter.py +0 -0
  42. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/crossval.py +0 -0
  43. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/detector_select.py +0 -0
  44. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/distribution.py +0 -0
  45. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/grid_search.py +0 -0
  46. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/labels.py +0 -0
  47. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/result.py +0 -0
  48. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/runner.py +0 -0
  49. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/scoring.py +0 -0
  50. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/seasonality_search.py +0 -0
  51. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/settings.py +0 -0
  52. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/autotune/window_select.py +0 -0
  53. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/__init__.py +0 -0
  54. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/_output.py +0 -0
  55. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  56. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  57. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  58. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  59. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  60. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  61. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  62. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  63. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  64. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  65. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/__init__.py +0 -0
  66. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/autotune.py +0 -0
  67. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/clean.py +0 -0
  68. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/init.py +0 -0
  69. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/init_claude.py +0 -0
  70. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/osi.py +0 -0
  71. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/run.py +0 -0
  72. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/test_alert.py +0 -0
  73. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/tune.py +0 -0
  74. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/commands/unlock.py +0 -0
  75. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/cli/main.py +0 -0
  76. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/config/__init__.py +0 -0
  77. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/config/metric_config.py +0 -0
  78. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/config/metric_io.py +0 -0
  79. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/config/profile.py +0 -0
  80. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/config/project_config.py +0 -0
  81. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/config/validator.py +0 -0
  82. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/core/__init__.py +0 -0
  83. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/core/interval.py +0 -0
  84. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/core/models.py +0 -0
  85. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/__init__.py +0 -0
  86. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/_sql_manager.py +0 -0
  87. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/clickhouse_manager.py +0 -0
  88. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/__init__.py +0 -0
  89. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  90. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  91. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_base.py +0 -0
  92. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  93. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_detections.py +0 -0
  94. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  95. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  96. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_schema.py +0 -0
  97. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  98. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/internal_tables/manager.py +0 -0
  99. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/manager.py +0 -0
  100. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/mysql_manager.py +0 -0
  101. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/postgres_manager.py +0 -0
  102. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/database/tables.py +0 -0
  103. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/__init__.py +0 -0
  104. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/base.py +0 -0
  105. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/factory.py +0 -0
  106. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/seasonality.py +0 -0
  107. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/statistical/__init__.py +0 -0
  108. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  109. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/statistical/autoreg.py +0 -0
  110. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/statistical/iqr.py +0 -0
  111. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/statistical/mad.py +0 -0
  112. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  113. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/detectors/statistical/zscore.py +0 -0
  114. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/loaders/__init__.py +0 -0
  115. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/loaders/metric_loader.py +0 -0
  116. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/loaders/query_template.py +0 -0
  117. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/__init__.py +0 -0
  118. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/error_dispatch.py +0 -0
  119. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  120. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  121. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  122. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  123. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  124. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  125. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  126. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/reporting/__init__.py +0 -0
  127. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/reporting/assets/report.js +0 -0
  128. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/reporting/builder.py +0 -0
  129. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/reporting/html_report.py +0 -0
  130. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/semantic/errors.py +0 -0
  131. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/semantic/exporter.py +0 -0
  132. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/semantic/importer.py +0 -0
  133. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/semantic/query_gen.py +0 -0
  134. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/tuning/__init__.py +0 -0
  135. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/tuning/assets/tune.js +0 -0
  136. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/tuning/config_writer.py +0 -0
  137. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/tuning/html.py +0 -0
  138. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/tuning/payload.py +0 -0
  139. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/tuning/server.py +0 -0
  140. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/ui/__init__.py +0 -0
  141. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/ui/html.py +0 -0
  142. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/ui/jobs.py +0 -0
  143. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/ui/overview.py +0 -0
  144. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/utils/__init__.py +0 -0
  145. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/utils/datetime_utils.py +0 -0
  146. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/utils/env_interpolation.py +0 -0
  147. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/utils/json_utils.py +0 -0
  148. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit/utils/stats.py +0 -0
  149. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit.egg-info/SOURCES.txt +0 -0
  150. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit.egg-info/dependency_links.txt +0 -0
  151. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit.egg-info/entry_points.txt +0 -0
  152. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit.egg-info/requires.txt +0 -0
  153. {detectkit-0.54.0 → detectkit-0.55.0}/detectkit.egg-info/top_level.txt +0 -0
  154. {detectkit-0.54.0 → detectkit-0.55.0}/pyproject.toml +0 -0
  155. {detectkit-0.54.0 → detectkit-0.55.0}/requirements.txt +0 -0
  156. {detectkit-0.54.0 → detectkit-0.55.0}/setup.cfg +0 -0
  157. {detectkit-0.54.0 → detectkit-0.55.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.54.0
3
+ Version: 0.55.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.54.0"
7
+ __version__ = "0.55.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -224,20 +224,39 @@ exactly as if typed into a terminal.
224
224
 
225
225
  ### Managing metrics from the UI
226
226
 
227
- The cockpit header has a **New metric** button an editor overlay seeded
228
- with a starter YAML template, with an optional subfolder under `metrics/`;
229
- saving writes `metrics/[<folder>/]<name>.yml`. Every metric row also has an
230
- **Edit** action that opens its raw YAML in the same editor. **Save** validates
227
+ The cockpit header has a **New metric** button, and every metric row an
228
+ **Edit** action; both open an editor overlay with **two tabs sharing one
229
+ draft**: **Builder** a structured form over the whole config (basics,
230
+ schedule/loading, seasonality, minimal detector rows with type + 1-2 key
231
+ params — fine-tuning belongs in `dtk tune` —, alerting with a channel
232
+ multi-select seeded from `profiles.yml` channel names, `ai_context`; SQL in
233
+ a syntax-highlighted pane, `query_file` paths read-only; a **From OSI**
234
+ sub-tab compiles a pasted OSI semantic-model metric through the same code
235
+ path as `dtk osi import`) — and **YAML**, the raw text, kept for whole-config
236
+ pastes. The last-edited tab wins, never silently: leaving an edited YAML tab
237
+ validates server-side first and blocks the switch on error; leaving an
238
+ edited Builder re-emits the YAML. A debounced live-validation chip re-checks
239
+ the draft while typing. Keys the form doesn't model (`autotune:`, custom
240
+ templates, unknown detector types/params, a multi-entry alerting list)
241
+ round-trip verbatim, listed under "Preserved fields". Create writes
242
+ `metrics/[<folder>/]<name>.yml`; after a create, a **next-steps strip**
243
+ offers **Load & detect** (`dtk run --steps load,detect` for just that metric
244
+ — no alert step, so an untuned config can't spam a channel) and then **Open
245
+ tune** on the loaded series. **Save** validates
231
246
  server-side **before any write**: YAML syntax → full `MetricConfig` → a deep
232
247
  detector-params check (each factory-known detector is actually constructed) —
233
248
  an invalid config lands in the editor's error pane with nothing written. A
234
249
  successful save **archives the previous file verbatim** to
235
250
  `metrics/.history/<metric>/` (the same archive `dtk tune`'s Apply writes,
236
- excluded from metric discovery) and only then overwrites in place, so the
237
- text typed is what lands on disk, comments intact (normalized only to end
238
- with a newline). A save is refused when the file changed on disk after the
239
- editor was opened (a `dtk tune` Apply or another session saved first) —
240
- reopen the metric rather than overwrite it. Renaming via `name:`
251
+ excluded from metric discovery) and only then overwrites in place. A
252
+ YAML-tab save writes the text typed, comments intact (normalized only to end
253
+ with a newline); a Builder save **re-emits the YAML deterministically,
254
+ dropping hand-written comments** (the archive keeps the previous file; edit
255
+ comment-heavy files from the YAML tab). A save is refused when the file
256
+ changed on disk after the editor was opened (a `dtk tune` Apply or another
257
+ session saved first) — reopen the metric rather than overwrite it. A file
258
+ that doesn't parse opens YAML-only, with the parse error on the disabled
259
+ Builder tab. Renaming via `name:`
241
260
  is allowed — uniqueness is enforced project-wide — and rows under the old name
242
261
  stay in the `_dtk_*` tables until `dtk clean`. **Delete** lives in the edit
243
262
  overlay behind an explicit confirmation step, and the server additionally
@@ -320,6 +339,10 @@ dtk osi import model.osi.yml -m total_sales -i 1h -o metrics/ # scaffold a metr
320
339
  dtk osi export -o semantic/detectkit.osi.yml # publish back to OSI
321
340
  ```
322
341
 
342
+ The `dtk ui` metric Builder's **From OSI** sub-tab does the import
343
+ interactively (paste a model, pick a metric/target, Compile) through the
344
+ same code path — see "Managing metrics from the UI" above.
345
+
323
346
  ## Common workflows
324
347
 
325
348
  ```bash
@@ -106,7 +106,9 @@ same reports in a browser overlay plus an overview table (alert frequency,
106
106
  freshness — netting out any configured `loading_delay` so a deliberately
107
107
  delayed metric doesn't read as stale — quality when incidents are labeled), a
108
108
  panel that drives `dtk run` / `dtk autotune` / `dtk unlock`, and an in-browser
109
- editor to create, edit and delete metric YAML files. See `cli.md`.
109
+ editor to create, edit and delete metric YAML files a structured **Builder**
110
+ form (with OSI import and a post-create load-then-tune flow) next to a raw
111
+ **YAML** tab. See `cli.md`.
110
112
 
111
113
  ## Glossary
112
114
 
@@ -16,12 +16,14 @@ need detail on any field, read the matching file under
16
16
  `project.md`); this skill is the procedure, those are the reference.
17
17
 
18
18
  `dtk ui` also has a browser editor for metric YAMLs (a **New metric** button
19
- and, per row, an **Edit** action), validated the same way before it writes.
20
- Prefer this skill when scaffolding from scratch it gathers the query,
21
- detector and alerting choices and explains each one in context; prefer the UI
22
- for a quick manual edit while you're already watching the cockpit (nudging a
23
- threshold, adding a channel) it's a raw-YAML editor with no procedure behind
24
- it.
19
+ and, per row, an **Edit** action), validated the same way before it writes:
20
+ a structured **Builder** form (highlighted SQL pane, channel picker, an OSI
21
+ import sub-tab, a post-create load-then-tune strip) next to a raw **YAML**
22
+ tab. Prefer this skill when the user is working with you in the terminal
23
+ it gathers the query, detector and alerting choices and explains each one in
24
+ context; point them at the UI when they'd rather click through the same flow
25
+ themselves, or for a quick manual edit while already watching the cockpit
26
+ (nudging a threshold, adding a channel).
25
27
 
26
28
  ## Step 0 — Confirm you're in a detectkit project
27
29
 
@@ -18,7 +18,7 @@ from detectkit.config.profile import ProfilesConfig
18
18
  from detectkit.config.project_config import ProjectConfig
19
19
  from detectkit.database.internal_tables import InternalTablesManager
20
20
  from detectkit.ui.overview import ALL_WINDOW_PRESETS
21
- from detectkit.ui.server import serve_ui
21
+ from detectkit.ui.server import build_form_meta, serve_ui
22
22
 
23
23
 
24
24
  def run_ui(
@@ -82,6 +82,8 @@ def run_ui(
82
82
  click.echo(click.style(f"detectkit UI: {project_config.name}", fg="cyan", bold=True))
83
83
  click.echo(f" {len(metrics)} metric(s) covered by --select {select} (window: {window})")
84
84
 
85
+ form_meta = build_form_meta(profiles_config)
86
+
85
87
  try:
86
88
  serve_ui(
87
89
  project_config=project_config,
@@ -92,6 +94,7 @@ def run_ui(
92
94
  profile=profile,
93
95
  echo=click.echo,
94
96
  open_browser=not no_open,
97
+ form_meta=form_meta,
95
98
  )
96
99
  except KeyboardInterrupt:
97
100
  pass
@@ -24,6 +24,7 @@ from detectkit.semantic.osi_model import (
24
24
  DETECTKIT_VENDOR,
25
25
  OsiSemanticModel,
26
26
  load_osi_models,
27
+ parse_osi_models,
27
28
  )
28
29
 
29
30
  __all__ = [
@@ -38,4 +39,5 @@ __all__ = [
38
39
  "export_models",
39
40
  "import_osi_metric",
40
41
  "load_osi_models",
42
+ "parse_osi_models",
41
43
  ]
@@ -171,21 +171,25 @@ def normalize_ai_context(raw: Any) -> dict[str, Any] | None:
171
171
  return None
172
172
 
173
173
 
174
- def load_osi_models(path: Path) -> list[OsiSemanticModel]:
175
- """Parse an OSI YAML file into a list of :class:`OsiSemanticModel`.
176
-
177
- Accepts the canonical ``semantic_model:`` root (a list, per the OSI examples)
178
- as well as a single mapping or a bare list, for robustness. Raises
179
- :class:`OsiParseError` on a missing/empty/malformed file.
174
+ def parse_osi_models(text: str, *, source: str = "<input>") -> list[OsiSemanticModel]:
175
+ """Parse OSI YAML *text* into a list of :class:`OsiSemanticModel`.
176
+
177
+ The text seam behind :func:`load_osi_models`: it takes the raw YAML string
178
+ directly, so a caller with no file on disk e.g. the ``dtk ui`` Builder's
179
+ "From OSI" paste box, which hands the browser's textarea contents straight
180
+ to the server — can parse a model without writing a temp file first.
181
+ Accepts the canonical ``semantic_model:`` root (a list, per the OSI
182
+ examples) as well as a single mapping or a bare list, for robustness.
183
+ Raises :class:`OsiParseError` on empty/malformed text; *source* names the
184
+ origin (a file path, or a caller-chosen marker like ``"<pasted OSI
185
+ model>"``) so error messages stay actionable without a real path.
180
186
  """
181
- if not path.exists():
182
- raise OsiParseError(f"OSI model file not found: {path}")
183
187
  try:
184
- raw = yaml.safe_load(path.read_text())
188
+ raw = yaml.safe_load(text)
185
189
  except yaml.YAMLError as exc:
186
- raise OsiParseError(f"invalid YAML in {path}: {exc}") from exc
190
+ raise OsiParseError(f"invalid YAML in {source}: {exc}") from exc
187
191
  if not raw:
188
- raise OsiParseError(f"empty OSI model file: {path}")
192
+ raise OsiParseError(f"empty OSI model file: {source}")
189
193
 
190
194
  node: Any = raw.get("semantic_model", raw) if isinstance(raw, dict) else raw
191
195
  items = node if isinstance(node, list) else [node]
@@ -196,12 +200,24 @@ def load_osi_models(path: Path) -> list[OsiSemanticModel]:
196
200
  try:
197
201
  models.append(OsiSemanticModel.model_validate(item))
198
202
  except Exception as exc: # pydantic ValidationError, etc.
199
- raise OsiParseError(f"could not parse a semantic_model in {path}: {exc}") from exc
203
+ raise OsiParseError(f"could not parse a semantic_model in {source}: {exc}") from exc
200
204
  if not models:
201
- raise OsiParseError(f"no semantic_model found in {path}")
205
+ raise OsiParseError(f"no semantic_model found in {source}")
202
206
  return models
203
207
 
204
208
 
209
+ def load_osi_models(path: Path) -> list[OsiSemanticModel]:
210
+ """Parse an OSI YAML file into a list of :class:`OsiSemanticModel`.
211
+
212
+ Existence-checks *path*, reads it, then delegates to :func:`parse_osi_models`
213
+ with ``source=str(path)`` — the same parsing/error-message behavior as
214
+ before the text/path split, just re-homed onto the text seam.
215
+ """
216
+ if not path.exists():
217
+ raise OsiParseError(f"OSI model file not found: {path}")
218
+ return parse_osi_models(path.read_text(), source=str(path))
219
+
220
+
205
221
  def find_metric(
206
222
  models: list[OsiSemanticModel], metric_name: str
207
223
  ) -> tuple[OsiSemanticModel, OsiMetric]: