detectkit 0.46.0__tar.gz → 0.47.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 (145) hide show
  1. {detectkit-0.46.0/detectkit.egg-info → detectkit-0.47.0}/PKG-INFO +5 -1
  2. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/base.py +17 -0
  4. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_base.py +5 -0
  5. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_decision.py +2 -0
  6. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_recovery.py +1 -0
  7. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/CLAUDE.section.md +1 -0
  8. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/alerting.md +1 -0
  9. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/cli.md +40 -0
  10. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/metrics.md +16 -0
  11. detectkit-0.47.0/detectkit/cli/commands/osi.py +245 -0
  12. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/main.py +8 -0
  13. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/metric_config.py +76 -1
  14. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_alert_step.py +1 -0
  15. detectkit-0.47.0/detectkit/semantic/__init__.py +41 -0
  16. detectkit-0.47.0/detectkit/semantic/errors.py +31 -0
  17. detectkit-0.47.0/detectkit/semantic/exporter.py +102 -0
  18. detectkit-0.47.0/detectkit/semantic/importer.py +281 -0
  19. detectkit-0.47.0/detectkit/semantic/osi_model.py +221 -0
  20. detectkit-0.47.0/detectkit/semantic/query_gen.py +229 -0
  21. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/payload.py +20 -0
  22. {detectkit-0.46.0 → detectkit-0.47.0/detectkit.egg-info}/PKG-INFO +5 -1
  23. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/SOURCES.txt +7 -0
  24. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/requires.txt +5 -0
  25. {detectkit-0.46.0 → detectkit-0.47.0}/pyproject.toml +7 -0
  26. {detectkit-0.46.0 → detectkit-0.47.0}/LICENSE +0 -0
  27. {detectkit-0.46.0 → detectkit-0.47.0}/MANIFEST.in +0 -0
  28. {detectkit-0.46.0 → detectkit-0.47.0}/README.md +0 -0
  29. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/__init__.py +0 -0
  30. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/__init__.py +0 -0
  31. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/branding.py +0 -0
  32. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/email.py +0 -0
  33. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/factory.py +0 -0
  34. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/mattermost.py +0 -0
  35. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/slack.py +0 -0
  36. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/telegram.py +0 -0
  37. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/channels/webhook.py +0 -0
  38. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  39. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  40. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  41. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
  42. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  43. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  44. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/__init__.py +0 -0
  45. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/_base.py +0 -0
  46. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/_types.py +0 -0
  47. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/autotuner.py +0 -0
  48. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/config_emitter.py +0 -0
  49. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/crossval.py +0 -0
  50. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/detector_select.py +0 -0
  51. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/distribution.py +0 -0
  52. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/grid_search.py +0 -0
  53. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/labels.py +0 -0
  54. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/result.py +0 -0
  55. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/runner.py +0 -0
  56. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/scoring.py +0 -0
  57. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/seasonality_search.py +0 -0
  58. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/settings.py +0 -0
  59. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/autotune/window_select.py +0 -0
  60. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/__init__.py +0 -0
  61. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/_output.py +0 -0
  62. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/autotune.md +0 -0
  63. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  64. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  65. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  66. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
  67. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  68. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  69. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  70. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +0 -0
  71. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/__init__.py +0 -0
  72. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/autotune.py +0 -0
  73. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/clean.py +0 -0
  74. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/init.py +0 -0
  75. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/init_claude.py +0 -0
  76. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/run.py +0 -0
  77. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/test_alert.py +0 -0
  78. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/tune.py +0 -0
  79. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/cli/commands/unlock.py +0 -0
  80. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/__init__.py +0 -0
  81. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/profile.py +0 -0
  82. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/project_config.py +0 -0
  83. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/config/validator.py +0 -0
  84. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/core/__init__.py +0 -0
  85. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/core/interval.py +0 -0
  86. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/core/models.py +0 -0
  87. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/__init__.py +0 -0
  88. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/_sql_manager.py +0 -0
  89. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/clickhouse_manager.py +0 -0
  90. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/__init__.py +0 -0
  91. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  92. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  93. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_base.py +0 -0
  94. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  95. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_detections.py +0 -0
  96. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  97. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  98. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_schema.py +0 -0
  99. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  100. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/internal_tables/manager.py +0 -0
  101. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/manager.py +0 -0
  102. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/mysql_manager.py +0 -0
  103. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/postgres_manager.py +0 -0
  104. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/database/tables.py +0 -0
  105. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/__init__.py +0 -0
  106. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/base.py +0 -0
  107. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/factory.py +0 -0
  108. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/seasonality.py +0 -0
  109. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/__init__.py +0 -0
  110. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  111. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/iqr.py +0 -0
  112. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/mad.py +0 -0
  113. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  114. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/detectors/statistical/zscore.py +0 -0
  115. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/loaders/__init__.py +0 -0
  116. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/loaders/metric_loader.py +0 -0
  117. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/loaders/query_template.py +0 -0
  118. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/__init__.py +0 -0
  119. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/error_dispatch.py +0 -0
  120. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  121. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  122. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  123. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  124. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  125. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  126. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/__init__.py +0 -0
  127. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/assets/report.js +0 -0
  128. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/builder.py +0 -0
  129. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/reporting/html_report.py +0 -0
  130. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/__init__.py +0 -0
  131. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/assets/tune.js +0 -0
  132. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/config_writer.py +0 -0
  133. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/html.py +0 -0
  134. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/tuning/server.py +0 -0
  135. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/__init__.py +0 -0
  136. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/datetime_utils.py +0 -0
  137. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/env_interpolation.py +0 -0
  138. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/json_utils.py +0 -0
  139. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit/utils/stats.py +0 -0
  140. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/dependency_links.txt +0 -0
  141. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/entry_points.txt +0 -0
  142. {detectkit-0.46.0 → detectkit-0.47.0}/detectkit.egg-info/top_level.txt +0 -0
  143. {detectkit-0.46.0 → detectkit-0.47.0}/requirements.txt +0 -0
  144. {detectkit-0.46.0 → detectkit-0.47.0}/setup.cfg +0 -0
  145. {detectkit-0.46.0 → detectkit-0.47.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.46.0
3
+ Version: 0.47.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -47,12 +47,15 @@ Requires-Dist: timesfm>=0.1.0; extra == "timesfm"
47
47
  Provides-Extra: advanced-detectors
48
48
  Requires-Dist: prophet>=1.1.0; extra == "advanced-detectors"
49
49
  Requires-Dist: timesfm>=0.1.0; extra == "advanced-detectors"
50
+ Provides-Extra: osi
51
+ Requires-Dist: sqlglot>=20.0; extra == "osi"
50
52
  Provides-Extra: all
51
53
  Requires-Dist: clickhouse-driver>=0.2.0; extra == "all"
52
54
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
53
55
  Requires-Dist: pymysql>=1.0.0; extra == "all"
54
56
  Requires-Dist: prophet>=1.1.0; extra == "all"
55
57
  Requires-Dist: timesfm>=0.1.0; extra == "all"
58
+ Requires-Dist: sqlglot>=20.0; extra == "all"
56
59
  Provides-Extra: dev
57
60
  Requires-Dist: pytest>=7.0; extra == "dev"
58
61
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -61,6 +64,7 @@ Requires-Dist: requests-mock>=1.12; extra == "dev"
61
64
  Requires-Dist: black>=23.0; extra == "dev"
62
65
  Requires-Dist: mypy>=1.0; extra == "dev"
63
66
  Requires-Dist: ruff>=0.1.0; extra == "dev"
67
+ Requires-Dist: sqlglot>=20.0; extra == "dev"
64
68
  Provides-Extra: integration
65
69
  Requires-Dist: testcontainers[clickhouse,mysql,postgres]>=4.0; extra == "integration"
66
70
  Requires-Dist: psycopg2-binary>=2.9.0; extra == "integration"
@@ -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.46.0"
7
+ __version__ = "0.47.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -72,6 +72,13 @@ class AlertData:
72
72
  error_message: str | None = None
73
73
  description: str | None = None
74
74
  mentions: list[str] = field(default_factory=list)
75
+ # OSI-compatible AI grounding: the metric's alternative names. Exposed to
76
+ # message templates as the OPT-IN ``{synonyms}`` / ``{synonyms_line}``
77
+ # variables (via :meth:`build_context`) so a custom template can surface an
78
+ # "Also known as: …" line — but the DEFAULT rendering deliberately does NOT
79
+ # show it, so existing alerts are byte-identical. Stamped by the orchestrator
80
+ # from ``MetricConfig.ai_context.synonyms``; defaults to empty.
81
+ ai_synonyms: list[str] = field(default_factory=list)
75
82
  project_name: str | None = None
76
83
  # Optional actionable links surfaced in the message. ``dashboard_url`` is the
77
84
  # headline link (rendered natively as a clickable title/link on
@@ -416,6 +423,14 @@ class BaseAlertChannel(ABC):
416
423
  # Format description line (empty string if no description)
417
424
  description_line = f"{alert_data.description}\n" if alert_data.description else ""
418
425
 
426
+ # OSI ai_context synonyms — the metric's alternative names. Provided as
427
+ # OPT-IN template variables only (``{synonyms}`` / ``{synonyms_line}``);
428
+ # the default templates do NOT reference them, so a metric without
429
+ # ai_context — and every existing alert — renders unchanged. A custom
430
+ # template can drop ``{synonyms_line}`` in to show "Also known as: …".
431
+ synonyms = ", ".join(alert_data.ai_synonyms) if alert_data.ai_synonyms else ""
432
+ synonyms_line = f"Also known as: {synonyms}\n" if synonyms else ""
433
+
419
434
  # Format mentions
420
435
  mentions_str = self.format_mentions(alert_data.mentions)
421
436
  mentions_line = f"\n{mentions_str}" if mentions_str else ""
@@ -482,6 +497,8 @@ class BaseAlertChannel(ABC):
482
497
  "error_message": alert_data.error_message or "",
483
498
  "description": alert_data.description or "",
484
499
  "description_line": description_line,
500
+ "synonyms": synonyms,
501
+ "synonyms_line": synonyms_line,
485
502
  "dashboard_url": dashboard_url,
486
503
  "dashboard_line": dashboard_line,
487
504
  "help_url": help_url,
@@ -36,6 +36,7 @@ class _OrchestratorBase:
36
36
  links: dict[str, str] | None = None,
37
37
  project_name: str | None = None,
38
38
  help_url: str | None = None,
39
+ ai_synonyms: list[str] | None = None,
39
40
  ):
40
41
  self.metric_name = metric_name
41
42
  self.interval = interval
@@ -57,6 +58,10 @@ class _OrchestratorBase:
57
58
  # defaulting to the official docs; None when opted out). Stamped onto every
58
59
  # AlertData so channels render a guide link for non-operator stakeholders.
59
60
  self.help_url = help_url
61
+ # OSI ai_context synonyms (the metric's alternative names). Stamped onto
62
+ # every AlertData so channels can render an "Also known as" identity line.
63
+ # Empty list when the metric has no ai_context — renders unchanged.
64
+ self.ai_synonyms = ai_synonyms or []
60
65
 
61
66
  @staticmethod
62
67
  def _group_by_timestamp(
@@ -280,6 +280,7 @@ class _DecisionMixin(_OrchestratorBase):
280
280
  consecutive_count=consecutive_count,
281
281
  description=self.description,
282
282
  mentions=self.mentions,
283
+ ai_synonyms=self.ai_synonyms,
283
284
  dashboard_url=self.dashboard_url,
284
285
  links=self.links,
285
286
  project_name=self.project_name,
@@ -345,6 +346,7 @@ class _DecisionMixin(_OrchestratorBase):
345
346
  is_no_data=True,
346
347
  description=self.description,
347
348
  mentions=self.mentions,
349
+ ai_synonyms=self.ai_synonyms,
348
350
  dashboard_url=self.dashboard_url,
349
351
  links=self.links,
350
352
  project_name=self.project_name,
@@ -188,6 +188,7 @@ class _RecoveryMixin(_OrchestratorBase):
188
188
  is_recovery=True,
189
189
  description=self.description,
190
190
  mentions=self.mentions,
191
+ ai_synonyms=self.ai_synonyms,
191
192
  dashboard_url=self.dashboard_url,
192
193
  links=self.links,
193
194
  project_name=self.project_name,
@@ -40,6 +40,7 @@ version — **read the relevant one on demand** instead of guessing:
40
40
  | Alert rules (quorum/direction/consecutive), cooldown, recovery, templates | `.claude/rules/detectkit/alerting.md` |
41
41
  | Auto-tuning a detector (`dtk autotune`), labels file, the `autotune:` block, `_dtk_autotune_runs` | `.claude/rules/detectkit/autotune.md` |
42
42
  | Manually/interactively tuning a detector in the browser and writing it back in place (`dtk tune`) | `.claude/rules/detectkit/cli.md` |
43
+ | OSI (Open Semantic Interchange) interop: import an OSI metric into a native metric / export back (`dtk osi`) | `.claude/rules/detectkit/cli.md` |
43
44
 
44
45
  ### Skills
45
46
 
@@ -297,6 +297,7 @@ referenced by path). Key variables:
297
297
  | Variable | Meaning |
298
298
  |---|---|
299
299
  | `{metric_name}`, `{description}` / `{description_line}` | identity |
300
+ | `{synonyms}` / `{synonyms_line}` | alt names from `ai_context.synonyms` (opt-in; not in default templates — add `{synonyms_line}` for an "Also known as: …" line) |
300
301
  | `{project_name}` / `{project_name_prefix}` | project label (`"[name] "` prefix, or `""`) |
301
302
  | `{timestamp}`, `{timezone}` | when (display tz via `alerting.timezone`, default UTC) |
302
303
  | `{value}` / `{value_display}` | metric value (`value_display` is NaN-safe) |
@@ -15,6 +15,8 @@ Run all commands from a project directory (the one containing
15
15
  | `dtk test-alert <metric>` | Send a mock alert to the metric's channels |
16
16
  | `dtk unlock --select <sel>` | Clear a stuck pipeline lock |
17
17
  | `dtk clean --select <sel>` | Prune internal data that no longer matches the config |
18
+ | `dtk osi import <model>` | Scaffold a native metric from an OSI semantic-model metric (see "OSI interop") |
19
+ | `dtk osi export` | Publish metrics into an OSI fragment (config in `custom_extensions[detectkit]`) |
18
20
  | `dtk --version` | Show installed detectkit version |
19
21
 
20
22
  ## Selectors (`--select` / `-s`)
@@ -193,6 +195,44 @@ actually delete.
193
195
  unless `-y/--yes`; refuses to run if the project defines no metrics or configs
194
196
  fail to parse (so a wrong directory can't wipe valid data).
195
197
 
198
+ ## OSI interop (`dtk osi`)
199
+
200
+ `dtk osi` converts between [Open Semantic Interchange](https://github.com/open-semantic-interchange/OSI)
201
+ (OSI) semantic models and native detectkit metrics. It's a **separate, additive**
202
+ layer: it never runs the pipeline, takes no lock, and the converter package
203
+ (`detectkit/semantic/`) is not imported by load/detect/alert — so it can't affect
204
+ a running project. OSI is treated as an *interchange* format (define the KPI once,
205
+ consume in BI + AI), not an execution engine. OSI adoption is still early, so the
206
+ broadly-useful piece today is `ai_context` (see the metrics rule) — the converters
207
+ are a forward bridge for teams that already run a governed semantic layer.
208
+
209
+ - `dtk osi import <model.osi.yml> --metric <name> --interval <grain>` — the
210
+ "enhanced init": resolve one OSI metric and **scaffold a normal native metric**
211
+ (SQL `query`, interval, a starter detector, the metric's `ai_context`). Review
212
+ it, then commit like any hand-written metric. Targets:
213
+ - `--target clickhouse` (default) — a direct `toStartOfInterval(...) GROUP BY`
214
+ query from the dataset's physical `source` (ANSI→ClickHouse via **sqlglot**,
215
+ the optional `[osi]` extra: `pip install 'detectkit[osi]'`).
216
+ - `--target cube --cube <name> --time-field <dim>` — a Cube **SQL-API**
217
+ `MEASURE(...)` query, so the metric runs through Cube and the alert matches a
218
+ Cube dashboard's number by construction. Point its `profile` at a Postgres
219
+ connection on Cube's SQL port.
220
+ - Only provably per-bucket-additive measures compile (`SUM`/`COUNT`/`COUNT(DISTINCT)`/
221
+ `AVG`/`MIN`/`MAX` + ratios like `SUM(x)/NULLIF(COUNT(DISTINCT y),0)`). Window
222
+ functions / non-aggregates / unknown aggregates are **refused** with a message
223
+ to use `query_file:` — never a plausible-but-wrong series.
224
+ - `dtk osi compile <model> --metric <name> --interval <g>` prints just the SQL
225
+ (for review). `dtk osi export [--select <sel>]` writes an OSI fragment carrying
226
+ a lossless snapshot of the config in `custom_extensions[detectkit]` + the
227
+ `ai_context` — a **one-way carrier** (`import` does not reconstruct from it; the
228
+ metric YAML stays the source of truth).
229
+
230
+ ```bash
231
+ dtk osi compile model.osi.yml -m total_sales -i 1h # preview the SQL
232
+ dtk osi import model.osi.yml -m total_sales -i 1h -o metrics/ # scaffold a metric
233
+ dtk osi export -o semantic/detectkit.osi.yml # publish back to OSI
234
+ ```
235
+
196
236
  ## Common workflows
197
237
 
198
238
  ```bash
@@ -13,6 +13,9 @@ description: API p95 latency # optional, shown in alerts
13
13
  profile: prod # optional, overrides project default_profile
14
14
  enabled: true # optional, false → skipped by `dtk run`
15
15
  tags: [critical, api] # optional, used by `--select tag:<t>`
16
+ ai_context: # optional — OSI-compatible grounding (descriptive only)
17
+ instructions: "p95 API latency, ms; user-facing"
18
+ synonyms: ["api latency", "response time"]
16
19
 
17
20
  interval: 5min # required — point spacing on the time grid
18
21
 
@@ -63,6 +66,19 @@ false_alert_budget: 0.3 # optional — `dtk tune` target false-alert rate
63
66
  project-wide default; unset → project, then a built-in `0.5`. Tuning-only — it never
64
67
  affects the load/detect/alert pipeline.
65
68
 
69
+ `ai_context` is **OSI-compatible grounding** you can add to any metric with **no
70
+ OSI model needed** — the metric's business meaning (`instructions`), alternative
71
+ names (`synonyms`) and example values (`examples`), mirroring the
72
+ [Open Semantic Interchange](https://github.com/open-semantic-interchange/OSI)
73
+ `ai_context` shape so a metric's meaning is portable to/from an OSI semantic model.
74
+ Accepts a bare string (→ `instructions`) or the full mapping. It is **purely
75
+ descriptive**: it never affects load/detect/alert or the detector id, and it does
76
+ **not** change any default-rendered alert. The `synonyms` are exposed to alert
77
+ templates as the **opt-in** `{synonyms}` / `{synonyms_line}` variables (a custom
78
+ `template` can add an "Also known as: …" line), and the whole block is carried in
79
+ the `dtk tune` cockpit payload as read-only grounding. Omit it and everything
80
+ renders as before.
81
+
66
82
  ## `interval` (required)
67
83
 
68
84
  Point spacing. String (`"30s"`, `"1min"`, `"5min"`, `"10min"`, `"1hour"`,
@@ -0,0 +1,245 @@
1
+ """``dtk osi`` — Open Semantic Interchange interop (import / export / compile).
2
+
3
+ A self-contained, additive command group. It never runs the pipeline, takes no
4
+ lock, and touches no internal table:
5
+
6
+ - ``dtk osi import`` scaffolds a native detectkit metric from an OSI model metric
7
+ (review the output, then commit it like any hand-written metric).
8
+ - ``dtk osi export`` publishes detectkit metrics into an OSI fragment (a one-way
9
+ carrier: the full config rides in ``custom_extensions[detectkit]``; ``import``
10
+ does not reconstruct from it).
11
+ - ``dtk osi compile`` prints just the SQL an import would generate (for review).
12
+
13
+ Needs the optional ``[osi]`` extra (sqlglot) only for the ClickHouse target.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import click
23
+ import yaml
24
+
25
+
26
+ @click.group()
27
+ def osi() -> None:
28
+ """Open Semantic Interchange (OSI) interop: import / export / compile metrics."""
29
+
30
+
31
+ def _split_csv(value: str | None) -> list[str] | None:
32
+ if not value:
33
+ return None
34
+ items = [v.strip() for v in value.split(",") if v.strip()]
35
+ return items or None
36
+
37
+
38
+ def _dump_metric_yaml(body: dict[str, Any], header: str) -> str:
39
+ """Render a metric body as YAML with a ``#`` provenance header (mirrors config_emitter)."""
40
+ text = yaml.safe_dump(body, sort_keys=False, allow_unicode=True, width=100)
41
+ return header + text
42
+
43
+
44
+ def _import_header(result: Any, model_file: Path) -> str:
45
+ lines = [
46
+ "# Generated by `dtk osi import` — review before committing.",
47
+ f"# Source OSI model: {model_file}",
48
+ f"# Metric: {result.metric_name} · target: {result.target} · "
49
+ f"sql-fingerprint: {result.fingerprint}",
50
+ "# detectkit resumes from the last datapoint, so if you re-import and this",
51
+ "# fingerprint changes, backfill/clean to avoid mixing definitions.",
52
+ ]
53
+ for w in result.warnings:
54
+ lines.append(f"# NOTE: {w}")
55
+ return "\n".join(lines) + "\n"
56
+
57
+
58
+ @osi.command(name="import")
59
+ @click.argument("model_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
60
+ @click.option("--metric", "-m", required=True, help="OSI metric name to import")
61
+ @click.option("--interval", "-i", required=True, help="Series grain (e.g. 1h, 10min, 3600)")
62
+ @click.option(
63
+ "--target",
64
+ type=click.Choice(["clickhouse", "cube"]),
65
+ default="clickhouse",
66
+ show_default=True,
67
+ help="clickhouse = direct SQL from dataset.source; cube = Cube SQL-API (MEASURE)",
68
+ )
69
+ @click.option("--dataset", help="OSI dataset to build FROM (default: the model's only dataset)")
70
+ @click.option("--time-field", help="Time grain column / dimension (default: a field with is_time)")
71
+ @click.option("--where", help="Extra SQL predicate (ANSI) added to the WHERE clause")
72
+ @click.option("--cube", help="Cube name (cube target)")
73
+ @click.option("--cube-measure", help="Cube measure name (cube target; default: the metric name)")
74
+ @click.option("--time-dimension", help="Cube time dimension (cube target)")
75
+ @click.option("--seasonality", help="Comma-separated time-derived seasonality columns")
76
+ @click.option("--detector", default="mad", show_default=True, help="Starter detector type")
77
+ @click.option(
78
+ "--out",
79
+ "-o",
80
+ type=click.Path(path_type=Path),
81
+ help="Write the metric YAML here (a dir → <name>.yml). Default: print to stdout.",
82
+ )
83
+ @click.option("--force", is_flag=True, help="Overwrite an existing --out file")
84
+ def import_(
85
+ model_file: Path,
86
+ metric: str,
87
+ interval: str,
88
+ target: str,
89
+ dataset: str | None,
90
+ time_field: str | None,
91
+ where: str | None,
92
+ cube: str | None,
93
+ cube_measure: str | None,
94
+ time_dimension: str | None,
95
+ seasonality: str | None,
96
+ detector: str,
97
+ out: Path | None,
98
+ force: bool,
99
+ ) -> None:
100
+ """Scaffold a native detectkit metric from an OSI MODEL_FILE metric."""
101
+ from detectkit.semantic import OsiError, import_osi_metric, load_osi_models
102
+
103
+ try:
104
+ models = load_osi_models(model_file)
105
+ result = import_osi_metric(
106
+ models=models,
107
+ metric_name=metric,
108
+ interval=interval,
109
+ target=target,
110
+ dataset=dataset,
111
+ time_field=time_field,
112
+ where=where,
113
+ cube=cube,
114
+ cube_measure=cube_measure,
115
+ time_dimension=time_dimension,
116
+ seasonality=_split_csv(seasonality),
117
+ detector_type=detector,
118
+ )
119
+ except OsiError as exc:
120
+ raise click.ClickException(str(exc)) from exc
121
+
122
+ rendered = _dump_metric_yaml(result.metric, _import_header(result, model_file))
123
+
124
+ if out is None:
125
+ click.echo(rendered)
126
+ click.echo("# (printed only — pass --out to write a metric file)", err=True)
127
+ return
128
+
129
+ dest = out / f"{result.metric_name}.yml" if out.is_dir() else out
130
+ if dest.exists() and not force:
131
+ raise click.ClickException(f"{dest} exists — pass --force to overwrite")
132
+ dest.parent.mkdir(parents=True, exist_ok=True)
133
+ dest.write_text(rendered)
134
+ click.echo(f"Wrote {dest}")
135
+ for w in result.warnings:
136
+ click.echo(f" note: {w}")
137
+
138
+
139
+ @osi.command()
140
+ @click.argument("model_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
141
+ @click.option("--metric", "-m", required=True, help="OSI metric name")
142
+ @click.option("--interval", "-i", required=True, help="Series grain (e.g. 1h, 10min, 3600)")
143
+ @click.option("--target", type=click.Choice(["clickhouse", "cube"]), default="clickhouse")
144
+ @click.option("--dataset", help="OSI dataset to build FROM")
145
+ @click.option("--time-field", help="Time grain column / dimension")
146
+ @click.option("--where", help="Extra SQL predicate (ANSI)")
147
+ @click.option("--cube", help="Cube name (cube target)")
148
+ @click.option("--cube-measure", help="Cube measure name (cube target)")
149
+ @click.option("--time-dimension", help="Cube time dimension (cube target)")
150
+ def compile( # noqa: A001 - mirrors the `dtk osi compile` verb
151
+ model_file: Path,
152
+ metric: str,
153
+ interval: str,
154
+ target: str,
155
+ dataset: str | None,
156
+ time_field: str | None,
157
+ where: str | None,
158
+ cube: str | None,
159
+ cube_measure: str | None,
160
+ time_dimension: str | None,
161
+ ) -> None:
162
+ """Print ONLY the SQL an import would generate (no file written)."""
163
+ from detectkit.semantic import OsiError, import_osi_metric, load_osi_models
164
+
165
+ try:
166
+ models = load_osi_models(model_file)
167
+ result = import_osi_metric(
168
+ models=models,
169
+ metric_name=metric,
170
+ interval=interval,
171
+ target=target,
172
+ dataset=dataset,
173
+ time_field=time_field,
174
+ where=where,
175
+ cube=cube,
176
+ cube_measure=cube_measure,
177
+ time_dimension=time_dimension,
178
+ )
179
+ except OsiError as exc:
180
+ raise click.ClickException(str(exc)) from exc
181
+ click.echo(result.sql)
182
+
183
+
184
+ @osi.command()
185
+ @click.option(
186
+ "--select", "-s", "selector", help="Metric selector (name / path / tag:<t>); default: all"
187
+ )
188
+ @click.option("--model-name", default="detectkit_export", show_default=True, help="OSI model name")
189
+ @click.option(
190
+ "--out",
191
+ "-o",
192
+ type=click.Path(path_type=Path),
193
+ help="Write the OSI YAML here. Default: print to stdout.",
194
+ )
195
+ @click.option("--force", is_flag=True, help="Overwrite an existing --out file")
196
+ def export(selector: str | None, model_name: str, out: Path | None, force: bool) -> None:
197
+ """Export native detectkit metrics into an OSI fragment (config in custom_extensions)."""
198
+ from detectkit.cli.commands.run import find_project_root, select_metrics
199
+ from detectkit.config.validator import validate_project_metrics
200
+ from detectkit.semantic import export_models
201
+
202
+ root = find_project_root()
203
+ if root is None:
204
+ raise click.ClickException(
205
+ "not inside a detectkit project (no detectkit_project.yml found)"
206
+ )
207
+
208
+ try:
209
+ pairs = select_metrics(selector, root) if selector else validate_project_metrics(root)
210
+ except Exception as exc:
211
+ raise click.ClickException(str(exc)) from exc
212
+ configs = [cfg for _, cfg in pairs]
213
+ if not configs:
214
+ raise click.ClickException("no metrics selected")
215
+
216
+ doc = export_models(configs, model_name=model_name)
217
+ rendered = yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=100)
218
+ header = (
219
+ f"# Generated by `dtk osi export` — {len(configs)} metric(s).\n"
220
+ "# The portable measure is a placeholder; the exact detectkit definition\n"
221
+ "# (detectors, alerting, query) rides in custom_extensions[detectkit].\n"
222
+ "# One-way carrier: `dtk osi import` does not reconstruct from this block —\n"
223
+ "# keep your metric YAML as the source of truth.\n"
224
+ )
225
+ rendered = header + rendered
226
+
227
+ if out is None:
228
+ click.echo(rendered)
229
+ return
230
+ if out.exists() and not force:
231
+ raise click.ClickException(f"{out} exists — pass --force to overwrite")
232
+ out.parent.mkdir(parents=True, exist_ok=True)
233
+ out.write_text(rendered)
234
+ click.echo(f"Wrote {out} ({len(configs)} metric(s))", err=False)
235
+ click.echo(
236
+ " note: one-way carrier — `dtk osi import` does not reconstruct from "
237
+ "custom_extensions[detectkit]; keep your metric YAML as the source of truth.",
238
+ err=True,
239
+ )
240
+
241
+
242
+ def run_osi() -> None:
243
+ """Entry shim (kept symmetric with other commands; the group is wired in main)."""
244
+ osi(standalone_mode=True) # pragma: no cover
245
+ sys.exit(0) # pragma: no cover
@@ -515,5 +515,13 @@ def clean(select: str, orphaned_metrics: bool, execute: bool, yes: bool, profile
515
515
  )
516
516
 
517
517
 
518
+ # OSI (Open Semantic Interchange) interop — a self-contained command group
519
+ # (`dtk osi import` / `export` / `compile`). Isolated from the load/detect/alert
520
+ # pipeline; its module only imports click/yaml at load time (sqlglot stays lazy).
521
+ from detectkit.cli.commands.osi import osi as _osi_group # noqa: E402
522
+
523
+ cli.add_command(_osi_group)
524
+
525
+
518
526
  if __name__ == "__main__":
519
527
  cli()
@@ -507,6 +507,59 @@ class AutoTuneConfig(BaseModel):
507
507
  return self
508
508
 
509
509
 
510
+ class AiContextConfig(BaseModel):
511
+ """OSI-compatible AI grounding for a metric (purely descriptive).
512
+
513
+ Mirrors the OSI ``ai_context`` shape (``instructions`` / ``synonyms`` /
514
+ ``examples``) verbatim, so a metric's business meaning, alternative names and
515
+ example values are portable to and from an OSI semantic model (Open Semantic
516
+ Interchange). It is surfaced to humans and agents — synonyms ride into alerts
517
+ as an "Also known as" line, the whole block is baked into the ``dtk tune``
518
+ cockpit payload, and it is shipped to the assistant via ``dtk init-claude`` —
519
+ but it **never** affects the load/detect/alert pipeline or the detector id. A
520
+ metric with no ``ai_context`` behaves exactly as before.
521
+
522
+ Accepts either the full mapping or a bare string (lifted to ``instructions``):
523
+
524
+ ```yaml
525
+ ai_context: "Revenue recognized at order completion, net of refunds."
526
+ # or
527
+ ai_context:
528
+ instructions: "Revenue recognized at order completion, net of refunds."
529
+ synonyms: ["total revenue", "gross sales"]
530
+ examples: ["12030.50", "9821.00"]
531
+ ```
532
+ """
533
+
534
+ instructions: str | None = Field(
535
+ default=None,
536
+ description="Business meaning / how to interpret the metric (grounds humans + LLMs)",
537
+ )
538
+ synonyms: list[str] = Field(
539
+ default_factory=list,
540
+ description="Alternative names for the metric (grounding + the alert 'Also known as' line)",
541
+ )
542
+ examples: list[str] = Field(
543
+ default_factory=list,
544
+ description="Illustrative example values or phrasings (grounding only)",
545
+ )
546
+
547
+ @field_validator("synonyms", "examples")
548
+ @classmethod
549
+ def _clean_str_list(cls, v: list[str]) -> list[str]:
550
+ """Drop blank/whitespace-only entries (trimming each), preserving order and
551
+ de-duplicating, so a stray empty YAML entry can't leak a bare comma into the
552
+ rendered "Also known as" line."""
553
+ seen: set[str] = set()
554
+ cleaned: list[str] = []
555
+ for item in v:
556
+ s = str(item).strip()
557
+ if s and s not in seen:
558
+ seen.add(s)
559
+ cleaned.append(s)
560
+ return cleaned
561
+
562
+
510
563
  class MetricConfig(BaseModel):
511
564
  """
512
565
  Configuration for a single metric.
@@ -602,7 +655,7 @@ class MetricConfig(BaseModel):
602
655
  """Normalize alerting to list. Accepts single dict/AlertConfig (backward compat) or list."""
603
656
  if v is None:
604
657
  return None
605
- if isinstance(v, (dict, AlertConfig)):
658
+ if isinstance(v, dict | AlertConfig):
606
659
  return [v]
607
660
  return v
608
661
 
@@ -612,6 +665,19 @@ class MetricConfig(BaseModel):
612
665
  autotune: AutoTuneConfig | None = Field(
613
666
  default=None, description="Optional auto-tuning constraints (for `dtk autotune`)"
614
667
  )
668
+ # OSI-compatible AI grounding (instructions/synonyms/examples). Descriptive
669
+ # only: surfaced in alerts (synonyms → "Also known as"), the `dtk tune`
670
+ # cockpit and assistant grounding, but never touches load/detect/alert or the
671
+ # detector id. Mirrors OSI's `ai_context` so a metric's meaning is portable to
672
+ # and from an OSI semantic model. Accepts a bare string (→ instructions).
673
+ ai_context: AiContextConfig | None = Field(
674
+ default=None,
675
+ description=(
676
+ "Optional OSI-compatible AI grounding (instructions/synonyms/examples). "
677
+ "Descriptive only — surfaced in alerts, the tune cockpit and assistant "
678
+ "grounding; never affects load/detect/alert or the detector id."
679
+ ),
680
+ )
615
681
  # Per-metric false-alert-rate (FDR) budget for manual tuning, overriding the
616
682
  # project-wide `false_alert_budget`. The `dtk tune` cockpit flags — non
617
683
  # intrusively — when the share of fired alerts that don't overlap a real
@@ -637,6 +703,15 @@ class MetricConfig(BaseModel):
637
703
  raise ValueError("false_alert_budget must be a fraction in (0, 1] (e.g. 0.3 = 30%)")
638
704
  return v
639
705
 
706
+ @field_validator("ai_context", mode="before")
707
+ @classmethod
708
+ def _accept_ai_context_string(cls, v: Any) -> Any:
709
+ """Allow ``ai_context: "free text"`` as shorthand for ``{instructions: ...}``,
710
+ mirroring OSI's ``ai_context`` which is itself either a string or a struct."""
711
+ if isinstance(v, str):
712
+ return {"instructions": v}
713
+ return v
714
+
640
715
  # Parsed interval (computed from string/int)
641
716
  _interval: Interval | None = None
642
717
 
@@ -68,6 +68,7 @@ class _AlertStepMixin(_TaskManagerBase):
68
68
  alert_config=alerting_config,
69
69
  description=config.description,
70
70
  mentions=alerting_config.mentions,
71
+ ai_synonyms=(config.ai_context.synonyms if config.ai_context else None),
71
72
  dashboard_url=alerting_config.dashboard_url,
72
73
  links=alerting_config.links,
73
74
  project_name=getattr(self.project_config, "name", None),
@@ -0,0 +1,41 @@
1
+ """OSI (Open Semantic Interchange) interop for detectkit.
2
+
3
+ An **isolated, additive** layer: nothing in the load/detect/alert pipeline imports
4
+ it, so it can never affect a running project. It powers the ``dtk osi`` command
5
+ group only (import an OSI metric into a native detectkit metric; export detectkit
6
+ metrics into an OSI fragment). sqlglot is an optional dependency (the ``[osi]``
7
+ extra), imported lazily inside :mod:`detectkit.semantic.query_gen`.
8
+
9
+ See ``.claude/rules/architecture.md`` ("Manual semantic interop") and
10
+ ``project_osi_detectkit_integration`` for the design.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from detectkit.semantic.errors import (
16
+ OsiDependencyMissing,
17
+ OsiError,
18
+ OsiParseError,
19
+ OsiUnsupportedMetric,
20
+ )
21
+ from detectkit.semantic.exporter import export_metric_to_osi, export_models
22
+ from detectkit.semantic.importer import ImportResult, import_osi_metric
23
+ from detectkit.semantic.osi_model import (
24
+ DETECTKIT_VENDOR,
25
+ OsiSemanticModel,
26
+ load_osi_models,
27
+ )
28
+
29
+ __all__ = [
30
+ "DETECTKIT_VENDOR",
31
+ "ImportResult",
32
+ "OsiDependencyMissing",
33
+ "OsiError",
34
+ "OsiParseError",
35
+ "OsiSemanticModel",
36
+ "OsiUnsupportedMetric",
37
+ "export_metric_to_osi",
38
+ "export_models",
39
+ "import_osi_metric",
40
+ "load_osi_models",
41
+ ]