kiri-cli 1.0.0b3__py3-none-any.whl

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.

Potentially problematic release.


This version of kiri-cli might be problematic. Click here for more details.

Files changed (743) hide show
  1. kiri_adapters/__init__.py +193 -0
  2. kiri_adapters/backends/__init__.py +0 -0
  3. kiri_adapters/backends/databricks.manifest.yml +55 -0
  4. kiri_adapters/backends/databricks.py +2980 -0
  5. kiri_adapters/backends/fabric_dw.manifest.yml +17 -0
  6. kiri_adapters/backends/fabric_dw.py +307 -0
  7. kiri_adapters/backends/fabric_lakehouse.manifest.yml +19 -0
  8. kiri_adapters/backends/fabric_lakehouse.py +365 -0
  9. kiri_adapters/backends/iceberg.manifest.yml +17 -0
  10. kiri_adapters/backends/iceberg.py +451 -0
  11. kiri_adapters/backends/localduckdb.manifest.yml +43 -0
  12. kiri_adapters/backends/localduckdb.py +1295 -0
  13. kiri_adapters/backends/mssql.manifest.yml +17 -0
  14. kiri_adapters/backends/mssql.py +366 -0
  15. kiri_adapters/backends/postgres.manifest.yml +17 -0
  16. kiri_adapters/backends/postgres.py +434 -0
  17. kiri_adapters/backends/trino.manifest.yml +17 -0
  18. kiri_adapters/backends/trino.py +542 -0
  19. kiri_adapters/base.py +892 -0
  20. kiri_adapters/doctor_checks.py +81 -0
  21. kiri_adapters/dv_planner.py +1205 -0
  22. kiri_adapters/dv_planner_bridge.py +265 -0
  23. kiri_adapters/dv_planner_pit.py +265 -0
  24. kiri_adapters/dv_planner_ref.py +403 -0
  25. kiri_adapters/dv_planner_sat_variants.py +668 -0
  26. kiri_adapters/framework.py +112 -0
  27. kiri_adapters/incident/__init__.py +30 -0
  28. kiri_adapters/incident/_http.py +121 -0
  29. kiri_adapters/incident/base.py +89 -0
  30. kiri_adapters/incident/jira.py +365 -0
  31. kiri_adapters/incident/servicenow.py +314 -0
  32. kiri_adapters/incident/zendesk.py +285 -0
  33. kiri_adapters/manifest.py +466 -0
  34. kiri_adapters/orchestration/__init__.py +30 -0
  35. kiri_adapters/orchestration/airflow.py +117 -0
  36. kiri_adapters/orchestration/artifact_emitter.py +99 -0
  37. kiri_adapters/orchestration/autoloader_emitter.py +141 -0
  38. kiri_adapters/orchestration/azure_data_factory.py +137 -0
  39. kiri_adapters/orchestration/base.py +626 -0
  40. kiri_adapters/orchestration/databricks_workflows.py +1768 -0
  41. kiri_adapters/orchestration/k8s_cron.py +555 -0
  42. kiri_adapters/orchestration/lifecycle.py +222 -0
  43. kiri_adapters/orchestration/pipeline_compat.py +145 -0
  44. kiri_adapters/orchestration/runtime_python.py +188 -0
  45. kiri_adapters/paths.py +38 -0
  46. kiri_adapters/protocols/__init__.py +64 -0
  47. kiri_adapters/protocols/ai_service/__init__.py +7 -0
  48. kiri_adapters/protocols/ai_service/conformance.py +120 -0
  49. kiri_adapters/protocols/base.py +220 -0
  50. kiri_adapters/protocols/catalog/__init__.py +7 -0
  51. kiri_adapters/protocols/catalog/conformance.py +138 -0
  52. kiri_adapters/protocols/framework/__init__.py +7 -0
  53. kiri_adapters/protocols/framework/conformance.py +98 -0
  54. kiri_adapters/protocols/incident/__init__.py +7 -0
  55. kiri_adapters/protocols/incident/conformance.py +64 -0
  56. kiri_adapters/protocols/orchestration/__init__.py +7 -0
  57. kiri_adapters/protocols/orchestration/conformance.py +84 -0
  58. kiri_adapters/protocols/platform/__init__.py +7 -0
  59. kiri_adapters/protocols/platform/conformance.py +120 -0
  60. kiri_adapters/protocols/sql_dialect/__init__.py +7 -0
  61. kiri_adapters/protocols/sql_dialect/conformance.py +91 -0
  62. kiri_adapters/protocols/vault/__init__.py +7 -0
  63. kiri_adapters/protocols/vault/conformance.py +65 -0
  64. kiri_adapters/py.typed +0 -0
  65. kiri_adapters/registry.py +104 -0
  66. kiri_adapters/semantic/__init__.py +60 -0
  67. kiri_adapters/semantic/apply.py +274 -0
  68. kiri_adapters/semantic/base.py +530 -0
  69. kiri_adapters/semantic/cube.py +280 -0
  70. kiri_adapters/semantic/dbt_semantic.py +92 -0
  71. kiri_adapters/semantic/metric_views.py +249 -0
  72. kiri_adapters/semantic/metricflow.py +345 -0
  73. kiri_adapters/semantic/snowflake_views.py +75 -0
  74. kiri_adapters/sql_dialect.py +1592 -0
  75. kiri_adapters/tsql_to_spark.py +653 -0
  76. kiri_ai_gateway/__init__.py +147 -0
  77. kiri_ai_gateway/audit.py +334 -0
  78. kiri_ai_gateway/batch.py +293 -0
  79. kiri_ai_gateway/config.py +165 -0
  80. kiri_ai_gateway/gateway.py +642 -0
  81. kiri_ai_gateway/placeholder_guard.py +145 -0
  82. kiri_ai_gateway/prices/__init__.py +77 -0
  83. kiri_ai_gateway/prices/prices.yaml +64 -0
  84. kiri_ai_gateway/prompts/__init__.py +54 -0
  85. kiri_ai_gateway/prompts/airbyte_source_setup.yaml +212 -0
  86. kiri_ai_gateway/prompts/bdv_translation.yaml +90 -0
  87. kiri_ai_gateway/prompts/contract_draft_mvp.yaml +117 -0
  88. kiri_ai_gateway/prompts/describe_table.yaml +19 -0
  89. kiri_ai_gateway/prompts/design_gold_from_question.yaml +130 -0
  90. kiri_ai_gateway/prompts/detect_pii.yaml +59 -0
  91. kiri_ai_gateway/prompts/discover_classify_column.yaml +69 -0
  92. kiri_ai_gateway/prompts/discover_describe_table.yaml +61 -0
  93. kiri_ai_gateway/prompts/dv_quality_summary.yaml +36 -0
  94. kiri_ai_gateway/prompts/incident_assist.yaml +58 -0
  95. kiri_ai_gateway/prompts/infer_source_schema.yaml +31 -0
  96. kiri_ai_gateway/prompts/ingest_backend_chooser.yaml +116 -0
  97. kiri_ai_gateway/prompts/interactive_source_edit.yaml +94 -0
  98. kiri_ai_gateway/prompts/map_needed_entity_to_source.yaml +72 -0
  99. kiri_ai_gateway/prompts/meet_kiri.yaml +127 -0
  100. kiri_ai_gateway/prompts/native_source_setup.yaml +211 -0
  101. kiri_ai_gateway/prompts/source_grouping_reason.yaml +29 -0
  102. kiri_ai_gateway/prompts/suggest_attribute_classification.yaml +42 -0
  103. kiri_ai_gateway/prompts/suggest_data_tests.yaml +71 -0
  104. kiri_ai_gateway/prompts/suggest_gold_dimension.yaml +108 -0
  105. kiri_ai_gateway/prompts/suggest_gold_fact.yaml +107 -0
  106. kiri_ai_gateway/prompts/suggest_gold_model.yaml +70 -0
  107. kiri_ai_gateway/prompts/suggest_semantic_intent.yaml +91 -0
  108. kiri_ai_gateway/prompts/suggest_silver_model.yaml +140 -0
  109. kiri_ai_gateway/providers/__init__.py +7 -0
  110. kiri_ai_gateway/providers/anthropic_provider.py +26 -0
  111. kiri_ai_gateway/providers/gateway_provider.py +188 -0
  112. kiri_ai_gateway/providers/local_provider.py +177 -0
  113. kiri_ai_gateway/py.typed +0 -0
  114. kiri_ai_gateway/service.py +283 -0
  115. kiri_ai_gateway/transport.py +103 -0
  116. kiri_ai_gateway/transports/__init__.py +12 -0
  117. kiri_ai_gateway/transports/anthropic_transport.py +73 -0
  118. kiri_ai_gateway/transports/ollama_transport.py +82 -0
  119. kiri_ai_gateway/transports/openai_compatible_transport.py +125 -0
  120. kiri_ai_gateway/workspace_policy.py +52 -0
  121. kiri_catalog/__init__.py +199 -0
  122. kiri_catalog/annotation_plan.py +183 -0
  123. kiri_catalog/appearances.py +155 -0
  124. kiri_catalog/backends/__init__.py +79 -0
  125. kiri_catalog/backends/base.py +167 -0
  126. kiri_catalog/backends/memory.py +241 -0
  127. kiri_catalog/backends/postgres.py +497 -0
  128. kiri_catalog/backends/redacting.py +195 -0
  129. kiri_catalog/backends/sqlite.py +480 -0
  130. kiri_catalog/context.py +94 -0
  131. kiri_catalog/federation/__init__.py +114 -0
  132. kiri_catalog/federation/api.py +200 -0
  133. kiri_catalog/federation/cache.py +149 -0
  134. kiri_catalog/federation/emit.py +209 -0
  135. kiri_catalog/federation/fs_resolver.py +295 -0
  136. kiri_catalog/federation/http_resolver.py +486 -0
  137. kiri_catalog/federation/metrics.py +123 -0
  138. kiri_catalog/federation/release.py +342 -0
  139. kiri_catalog/federation/resolver.py +332 -0
  140. kiri_catalog/federation/schema.py +460 -0
  141. kiri_catalog/importer.py +668 -0
  142. kiri_catalog/lineage_drift.py +280 -0
  143. kiri_catalog/lineage_graph.py +176 -0
  144. kiri_catalog/lineage_graph_explain.py +96 -0
  145. kiri_catalog/lineage_graph_mermaid.py +230 -0
  146. kiri_catalog/observed_uc_lineage.py +216 -0
  147. kiri_catalog/pii_detect.py +176 -0
  148. kiri_catalog/push.py +122 -0
  149. kiri_catalog/py.typed +0 -0
  150. kiri_catalog/redact.py +101 -0
  151. kiri_catalog/review_actions.py +119 -0
  152. kiri_catalog/schema/__init__.py +70 -0
  153. kiri_catalog/schema/asset.py +142 -0
  154. kiri_catalog/schema/column.py +247 -0
  155. kiri_catalog/schema/enums.py +195 -0
  156. kiri_catalog/schema/job.py +51 -0
  157. kiri_catalog/schema/lineage.py +33 -0
  158. kiri_catalog/schema/policy.py +69 -0
  159. kiri_catalog/schema/primitives.py +90 -0
  160. kiri_catalog/schema/review_state.py +111 -0
  161. kiri_catalog/schema/urn.py +45 -0
  162. kiri_catalog/schema_discovery.py +160 -0
  163. kiri_catalog/sinks/__init__.py +64 -0
  164. kiri_catalog/sinks/base.py +178 -0
  165. kiri_catalog/sinks/horizon.py +195 -0
  166. kiri_catalog/sinks/purview.py +216 -0
  167. kiri_catalog/sinks/unity.py +352 -0
  168. kiri_catalog/sinks/unity_executor.py +543 -0
  169. kiri_catalog/sources/__init__.py +42 -0
  170. kiri_catalog/sources/base.py +104 -0
  171. kiri_catalog/sources/unity.py +465 -0
  172. kiri_catalog/steward_writeback.py +158 -0
  173. kiri_catalog/tag_sql.py +99 -0
  174. kiri_cli/LICENSE +105 -0
  175. kiri_cli/__init__.py +1 -0
  176. kiri_cli/_apply_cli.py +63 -0
  177. kiri_cli/_apply_reporter.py +38 -0
  178. kiri_cli/_closed_menu.py +36 -0
  179. kiri_cli/_fixture_option.py +18 -0
  180. kiri_cli/_layer_detect.py +158 -0
  181. kiri_cli/_project_option.py +79 -0
  182. kiri_cli/adapter_cmd.py +192 -0
  183. kiri_cli/ai_batch_cmd.py +258 -0
  184. kiri_cli/ai_cmd.py +352 -0
  185. kiri_cli/approve_cmd.py +659 -0
  186. kiri_cli/attr_cmd.py +575 -0
  187. kiri_cli/audit_cmd.py +348 -0
  188. kiri_cli/audit_correlate_cmd.py +648 -0
  189. kiri_cli/audit_join.py +437 -0
  190. kiri_cli/audit_join_cmd.py +160 -0
  191. kiri_cli/auto_cmd.py +97 -0
  192. kiri_cli/azure_cmd.py +243 -0
  193. kiri_cli/catalog.py +1293 -0
  194. kiri_cli/cli_reference_live.py +147 -0
  195. kiri_cli/compile_cmd.py +493 -0
  196. kiri_cli/compliance_cmd.py +221 -0
  197. kiri_cli/contract_approval_cmd.py +94 -0
  198. kiri_cli/contract_cmd.py +64 -0
  199. kiri_cli/contract_codeowners_cmd.py +162 -0
  200. kiri_cli/contract_diff.py +111 -0
  201. kiri_cli/contract_emit.py +90 -0
  202. kiri_cli/contract_export.py +192 -0
  203. kiri_cli/contract_lint.py +1505 -0
  204. kiri_cli/contract_merge_cmd.py +264 -0
  205. kiri_cli/contract_new.py +326 -0
  206. kiri_cli/contract_promote.py +216 -0
  207. kiri_cli/contract_scaffold_cmd.py +134 -0
  208. kiri_cli/contract_schema_export.py +69 -0
  209. kiri_cli/contract_show.py +150 -0
  210. kiri_cli/contract_stubs.py +60 -0
  211. kiri_cli/contract_sync_from_live.py +327 -0
  212. kiri_cli/contract_validate.py +154 -0
  213. kiri_cli/contract_verify_live.py +242 -0
  214. kiri_cli/cost_cmd.py +243 -0
  215. kiri_cli/databricks_health.py +1254 -0
  216. kiri_cli/databricks_setup_cmd.py +712 -0
  217. kiri_cli/discover_cmd.py +600 -0
  218. kiri_cli/docs_cmd.py +35 -0
  219. kiri_cli/doctor_cmd.py +145 -0
  220. kiri_cli/dv_cmd.py +814 -0
  221. kiri_cli/dv_conformance_cmd.py +154 -0
  222. kiri_cli/dv_install.py +493 -0
  223. kiri_cli/dv_migrate_cmd.py +183 -0
  224. kiri_cli/dv_test_cmd.py +175 -0
  225. kiri_cli/federation_cmd.py +315 -0
  226. kiri_cli/fk_cmd.py +160 -0
  227. kiri_cli/flows_cmd.py +910 -0
  228. kiri_cli/governance_cmd.py +358 -0
  229. kiri_cli/incident_api_cmd.py +180 -0
  230. kiri_cli/incident_cmd.py +410 -0
  231. kiri_cli/incident_setup_cmd.py +239 -0
  232. kiri_cli/ingest.py +427 -0
  233. kiri_cli/ingest_discover.py +480 -0
  234. kiri_cli/ingest_new_source.py +386 -0
  235. kiri_cli/intent_cmd.py +122 -0
  236. kiri_cli/invariants_cmd.py +118 -0
  237. kiri_cli/kiri_banner.py +161 -0
  238. kiri_cli/kpi_cmd.py +222 -0
  239. kiri_cli/layer_cmd.py +464 -0
  240. kiri_cli/lineage.py +601 -0
  241. kiri_cli/main.py +2472 -0
  242. kiri_cli/maturity_cmd.py +222 -0
  243. kiri_cli/migrate_cmd.py +1347 -0
  244. kiri_cli/onboard_cmd.py +223 -0
  245. kiri_cli/pack_cmd.py +496 -0
  246. kiri_cli/pipeline_cmd.py +127 -0
  247. kiri_cli/pre_classify_cmd.py +274 -0
  248. kiri_cli/project_cmd.py +592 -0
  249. kiri_cli/py.typed +0 -0
  250. kiri_cli/quality_cmd.py +295 -0
  251. kiri_cli/rbac.py +139 -0
  252. kiri_cli/rbac_cmd.py +291 -0
  253. kiri_cli/release.py +850 -0
  254. kiri_cli/release_semver.py +265 -0
  255. kiri_cli/repo_cmd.py +155 -0
  256. kiri_cli/retention_cmd.py +333 -0
  257. kiri_cli/sample_data/__init__.py +12 -0
  258. kiri_cli/sample_data/customers.contract.yml +47 -0
  259. kiri_cli/sample_data/customers.csv +25 -0
  260. kiri_cli/scaffold_cmd.py +721 -0
  261. kiri_cli/schema_cmd.py +185 -0
  262. kiri_cli/semantic_cmd.py +605 -0
  263. kiri_cli/silver_cmd.py +348 -0
  264. kiri_cli/skills_cmd.py +243 -0
  265. kiri_cli/skills_data/README.md +51 -0
  266. kiri_cli/skills_data/add-source/SKILL.md +116 -0
  267. kiri_cli/skills_data/azure-deploy/SKILL.md +261 -0
  268. kiri_cli/skills_data/azure-status/SKILL.md +111 -0
  269. kiri_cli/skills_data/compliance-check/SKILL.md +99 -0
  270. kiri_cli/skills_data/databricks-build-pipeline/SKILL.md +112 -0
  271. kiri_cli/skills_data/databricks-project-setup/SKILL.md +157 -0
  272. kiri_cli/skills_data/debug-apply/SKILL.md +186 -0
  273. kiri_cli/skills_data/draft-gold-model/SKILL.md +203 -0
  274. kiri_cli/skills_data/explain-kpi/SKILL.md +158 -0
  275. kiri_cli/skills_data/incident-coach/SKILL.md +135 -0
  276. kiri_cli/skills_data/incident-setup/SKILL.md +112 -0
  277. kiri_cli/skills_data/maturity-coach/SKILL.md +120 -0
  278. kiri_cli/skills_data/promote-contract/SKILL.md +91 -0
  279. kiri_cli/skills_data/review-attributes/SKILL.md +94 -0
  280. kiri_cli/sla_cmd.py +769 -0
  281. kiri_cli/source_cmd.py +909 -0
  282. kiri_cli/templates/__init__.py +6 -0
  283. kiri_cli/templates/dv4dbt_packages.yml.j2 +11 -0
  284. kiri_cli/upgrade_cmd.py +896 -0
  285. kiri_cli/use_cmd.py +120 -0
  286. kiri_cli/vault_cmd.py +241 -0
  287. kiri_cli/verify_generated.py +241 -0
  288. kiri_cli-1.0.0b3.dist-info/METADATA +212 -0
  289. kiri_cli-1.0.0b3.dist-info/RECORD +743 -0
  290. kiri_cli-1.0.0b3.dist-info/WHEEL +4 -0
  291. kiri_cli-1.0.0b3.dist-info/entry_points.txt +4 -0
  292. kiri_compliance/__init__.py +33 -0
  293. kiri_compliance/audit_coverage.py +354 -0
  294. kiri_compliance/controls.py +763 -0
  295. kiri_compliance/evidence.py +155 -0
  296. kiri_compliance/py.typed +0 -0
  297. kiri_compliance/quality_engine.py +246 -0
  298. kiri_compliance/renderer.py +195 -0
  299. kiri_compliance/report.py +44 -0
  300. kiri_compliance/schema.py +129 -0
  301. kiri_core/__init__.py +185 -0
  302. kiri_core/adapter_surfaces.py +149 -0
  303. kiri_core/ai_query_gate/__init__.py +92 -0
  304. kiri_core/ai_query_gate/audit.py +153 -0
  305. kiri_core/ai_query_gate/evaluator.py +484 -0
  306. kiri_core/ai_query_gate/models.py +162 -0
  307. kiri_core/ai_query_gate/parser.py +436 -0
  308. kiri_core/ai_query_gate/resolver.py +274 -0
  309. kiri_core/ai_query_gate/store.py +234 -0
  310. kiri_core/ai_query_gate/store_postgres.py +409 -0
  311. kiri_core/api_keys.py +243 -0
  312. kiri_core/apply/__init__.py +27 -0
  313. kiri_core/apply/conformance.py +99 -0
  314. kiri_core/apply/incident_router.py +176 -0
  315. kiri_core/apply/models.py +140 -0
  316. kiri_core/apply/reporter.py +46 -0
  317. kiri_core/apply/runner.py +3083 -0
  318. kiri_core/apply_log_store.py +155 -0
  319. kiri_core/approval_flow/__init__.py +133 -0
  320. kiri_core/approval_flow/integration.py +156 -0
  321. kiri_core/approval_flow/models.py +443 -0
  322. kiri_core/approval_flow/notifier.py +263 -0
  323. kiri_core/approval_flow/orchestrator.py +660 -0
  324. kiri_core/approval_flow/reconcile_signoff.py +53 -0
  325. kiri_core/approval_flow/store.py +220 -0
  326. kiri_core/approval_flow/store_jsonl.py +217 -0
  327. kiri_core/approval_flow/store_postgres.py +580 -0
  328. kiri_core/approval_policy.py +116 -0
  329. kiri_core/asgi_trace.py +160 -0
  330. kiri_core/compliance.py +802 -0
  331. kiri_core/cutover_approval.py +187 -0
  332. kiri_core/discovery/__init__.py +109 -0
  333. kiri_core/discovery/ai/__init__.py +37 -0
  334. kiri_core/discovery/ai/_payload.py +115 -0
  335. kiri_core/discovery/ai/budget.py +83 -0
  336. kiri_core/discovery/ai/classify.py +223 -0
  337. kiri_core/discovery/ai/describe.py +200 -0
  338. kiri_core/discovery/ai/errors.py +49 -0
  339. kiri_core/discovery/discover.py +648 -0
  340. kiri_core/discovery/heuristics/__init__.py +42 -0
  341. kiri_core/discovery/heuristics/classification.py +174 -0
  342. kiri_core/discovery/heuristics/medallion.py +157 -0
  343. kiri_core/discovery/heuristics/pii.py +92 -0
  344. kiri_core/discovery/introspector.py +636 -0
  345. kiri_core/discovery/introspector_databricks.py +396 -0
  346. kiri_core/discovery/introspector_postgres.py +394 -0
  347. kiri_core/discovery/introspector_snowflake.py +383 -0
  348. kiri_core/discovery/reporting/__init__.py +17 -0
  349. kiri_core/discovery/reporting/discovery_report.py +190 -0
  350. kiri_core/discovery/scaffold/__init__.py +41 -0
  351. kiri_core/discovery/scaffold/contract_writer.py +632 -0
  352. kiri_core/discovery/scaffold/provenance_overlay.py +143 -0
  353. kiri_core/discovery/scaffold/source_writer.py +211 -0
  354. kiri_core/discovery/scaffold/todo_marker.py +98 -0
  355. kiri_core/domain_policy.py +122 -0
  356. kiri_core/goal_coverage.py +82 -0
  357. kiri_core/gold_registry.py +211 -0
  358. kiri_core/health.py +192 -0
  359. kiri_core/incident.py +204 -0
  360. kiri_core/incident_builders.py +256 -0
  361. kiri_core/incident_log.py +418 -0
  362. kiri_core/intent/__init__.py +46 -0
  363. kiri_core/intent/agent_runner.py +82 -0
  364. kiri_core/intent/project_dv.py +261 -0
  365. kiri_core/intent/propose.py +132 -0
  366. kiri_core/intent/semantic_intent.py +446 -0
  367. kiri_core/invariants.py +301 -0
  368. kiri_core/kpi.py +373 -0
  369. kiri_core/latency.py +150 -0
  370. kiri_core/lifecycle/__init__.py +555 -0
  371. kiri_core/lifecycle/ai_index.py +453 -0
  372. kiri_core/lifecycle/auto_mode.py +172 -0
  373. kiri_core/lifecycle/bootstrap.py +316 -0
  374. kiri_core/lifecycle/cli_reference.py +1064 -0
  375. kiri_core/lifecycle/commit.py +258 -0
  376. kiri_core/lifecycle/compile_dispatch.py +296 -0
  377. kiri_core/lifecycle/compile_range.py +665 -0
  378. kiri_core/lifecycle/contract_inventory.py +354 -0
  379. kiri_core/lifecycle/contracts.py +250 -0
  380. kiri_core/lifecycle/doctor.py +412 -0
  381. kiri_core/lifecycle/git_remote.py +948 -0
  382. kiri_core/lifecycle/identity.py +147 -0
  383. kiri_core/lifecycle/ingestion_modes.py +215 -0
  384. kiri_core/lifecycle/layer_contract.py +452 -0
  385. kiri_core/lifecycle/layer_contract_migrate.py +969 -0
  386. kiri_core/lifecycle/layer_contract_parser.py +436 -0
  387. kiri_core/lifecycle/layer_contract_validator.py +2291 -0
  388. kiri_core/lifecycle/layout.py +298 -0
  389. kiri_core/lifecycle/onboarding.py +306 -0
  390. kiri_core/lifecycle/openlineage_emit.py +153 -0
  391. kiri_core/lifecycle/patch.py +231 -0
  392. kiri_core/lifecycle/plan.py +241 -0
  393. kiri_core/lifecycle/probe.py +204 -0
  394. kiri_core/lifecycle/project_context.py +334 -0
  395. kiri_core/lifecycle/project_status.py +532 -0
  396. kiri_core/lifecycle/scaffold.py +455 -0
  397. kiri_core/lifecycle/source_grouping.py +725 -0
  398. kiri_core/lifecycle/source_introspection.py +488 -0
  399. kiri_core/lifecycle/source_inventory.py +561 -0
  400. kiri_core/lifecycle/source_patch.py +353 -0
  401. kiri_core/lifecycle/sources.py +470 -0
  402. kiri_core/lifecycle/sync_from_live.py +727 -0
  403. kiri_core/lifecycle/validate.py +116 -0
  404. kiri_core/lifecycle/verify_annotations.py +391 -0
  405. kiri_core/lifecycle/verify_live.py +389 -0
  406. kiri_core/lifecycle/verify_metric_views.py +321 -0
  407. kiri_core/lifecycle/watermark.py +371 -0
  408. kiri_core/lifecycle/watermark_apply.py +656 -0
  409. kiri_core/lineage.py +418 -0
  410. kiri_core/loader.py +377 -0
  411. kiri_core/logging.py +374 -0
  412. kiri_core/maturity.py +592 -0
  413. kiri_core/migration_layer_policy.py +285 -0
  414. kiri_core/names.py +163 -0
  415. kiri_core/observed_lineage.py +62 -0
  416. kiri_core/odcs/__init__.py +15 -0
  417. kiri_core/otel_bootstrap.py +326 -0
  418. kiri_core/pipeline_ir.py +332 -0
  419. kiri_core/pipeline_loader.py +82 -0
  420. kiri_core/planning.py +110 -0
  421. kiri_core/project.py +1576 -0
  422. kiri_core/project_docs.py +199 -0
  423. kiri_core/py.typed +0 -0
  424. kiri_core/rate_limit.py +456 -0
  425. kiri_core/release_state/__init__.py +37 -0
  426. kiri_core/release_state/models.py +108 -0
  427. kiri_core/release_state/store.py +634 -0
  428. kiri_core/resilience/__init__.py +288 -0
  429. kiri_core/resilience/retry.py +173 -0
  430. kiri_core/runtime.py +40 -0
  431. kiri_core/schema/__init__.py +62 -0
  432. kiri_core/schema/column.py +140 -0
  433. kiri_core/schema/ingestion.py +665 -0
  434. kiri_core/schema/model.py +44 -0
  435. kiri_core/schema/reporting_goal.py +160 -0
  436. kiri_core/schema/source.py +188 -0
  437. kiri_core/silver/__init__.py +148 -0
  438. kiri_core/silver/apply_runner.py +830 -0
  439. kiri_core/silver/backfill.py +280 -0
  440. kiri_core/silver/dv_backend.py +426 -0
  441. kiri_core/silver/dv_graph.py +932 -0
  442. kiri_core/silver/dv_graph_mermaid.py +443 -0
  443. kiri_core/silver/dv_graph_payload.py +79 -0
  444. kiri_core/silver/dv_graph_views.py +416 -0
  445. kiri_core/silver/graph_projector.py +566 -0
  446. kiri_core/silver/hash_canonicaliser.py +172 -0
  447. kiri_core/silver/hash_policy.py +195 -0
  448. kiri_core/silver/load_ts_policy.py +202 -0
  449. kiri_core/silver/schema_evolution.py +410 -0
  450. kiri_core/silver/zone_rules.py +81 -0
  451. kiri_core/sizing.py +134 -0
  452. kiri_core/storage_paths.py +130 -0
  453. kiri_core/technique_validator.py +700 -0
  454. kiri_core/trace.py +409 -0
  455. kiri_core/types.py +41 -0
  456. kiri_core/urn.py +161 -0
  457. kiri_dbt_bridge/__init__.py +22 -0
  458. kiri_dbt_bridge/_schema_resolve.py +38 -0
  459. kiri_dbt_bridge/bridge.py +94 -0
  460. kiri_dbt_bridge/dbt.manifest.yml +26 -0
  461. kiri_dbt_bridge/dv4dbt/__init__.py +143 -0
  462. kiri_dbt_bridge/dv4dbt/backend.py +204 -0
  463. kiri_dbt_bridge/dv4dbt/bridge_generator.py +289 -0
  464. kiri_dbt_bridge/dv4dbt/compat.py +232 -0
  465. kiri_dbt_bridge/dv4dbt/hub_generator.py +282 -0
  466. kiri_dbt_bridge/dv4dbt/link_generator.py +306 -0
  467. kiri_dbt_bridge/dv4dbt/manifest.py +203 -0
  468. kiri_dbt_bridge/dv4dbt/pit_generator.py +318 -0
  469. kiri_dbt_bridge/dv4dbt/policy_translation.py +249 -0
  470. kiri_dbt_bridge/dv4dbt/project_writer.py +588 -0
  471. kiri_dbt_bridge/dv4dbt/ref_generator.py +404 -0
  472. kiri_dbt_bridge/dv4dbt/render.py +180 -0
  473. kiri_dbt_bridge/dv4dbt/sat_generator.py +480 -0
  474. kiri_dbt_bridge/dv4dbt/sat_variant_generator.py +529 -0
  475. kiri_dbt_bridge/dv4dbt/templates/__init__.py +16 -0
  476. kiri_dbt_bridge/dv4dbt/templates/bridge.sql.j2 +9 -0
  477. kiri_dbt_bridge/dv4dbt/templates/dbt_project.yml.j2 +37 -0
  478. kiri_dbt_bridge/dv4dbt/templates/hub.sql.j2 +9 -0
  479. kiri_dbt_bridge/dv4dbt/templates/link.sql.j2 +9 -0
  480. kiri_dbt_bridge/dv4dbt/templates/packages.yml.j2 +10 -0
  481. kiri_dbt_bridge/dv4dbt/templates/pit.sql.j2 +9 -0
  482. kiri_dbt_bridge/dv4dbt/templates/ref_historised.sql.j2 +9 -0
  483. kiri_dbt_bridge/dv4dbt/templates/ref_non_historised.sql.j2 +9 -0
  484. kiri_dbt_bridge/dv4dbt/templates/sat.sql.j2 +9 -0
  485. kiri_dbt_bridge/dv4dbt/templates/sat_effectivity.sql.j2 +9 -0
  486. kiri_dbt_bridge/dv4dbt/templates/sat_multi_active.sql.j2 +9 -0
  487. kiri_dbt_bridge/dv4dbt/templates/sat_non_historised.sql.j2 +9 -0
  488. kiri_dbt_bridge/dv4dbt/templates/sat_record_tracking.sql.j2 +9 -0
  489. kiri_dbt_bridge/dv4dbt/templates/sat_virtual_end_dated.sql.j2 +9 -0
  490. kiri_dbt_bridge/dv4dbt/templates/sources.yml.j2 +39 -0
  491. kiri_dbt_bridge/framework_adapter.py +310 -0
  492. kiri_dbt_bridge/generate.py +372 -0
  493. kiri_dbt_bridge/gold/__init__.py +21 -0
  494. kiri_dbt_bridge/gold/_sql_emit.py +128 -0
  495. kiri_dbt_bridge/gold/assemble.py +90 -0
  496. kiri_dbt_bridge/gold/canonical_columns.py +168 -0
  497. kiri_dbt_bridge/gold/computed_expressions.py +179 -0
  498. kiri_dbt_bridge/gold/dialects/__init__.py +69 -0
  499. kiri_dbt_bridge/gold/dialects/databricks.py +150 -0
  500. kiri_dbt_bridge/gold/dialects/duckdb.py +120 -0
  501. kiri_dbt_bridge/gold/dialects/trino.py +136 -0
  502. kiri_dbt_bridge/gold/dim_generator.py +688 -0
  503. kiri_dbt_bridge/gold/dimension_refs.py +255 -0
  504. kiri_dbt_bridge/gold/dispatch.py +69 -0
  505. kiri_dbt_bridge/gold/fact_generator.py +534 -0
  506. kiri_dbt_bridge/gold/flat_generator.py +144 -0
  507. kiri_dbt_bridge/gold/lineage.py +484 -0
  508. kiri_dbt_bridge/gold/localized_twins.py +218 -0
  509. kiri_dbt_bridge/gold/measures.py +49 -0
  510. kiri_dbt_bridge/gold/role_playing.py +75 -0
  511. kiri_dbt_bridge/gold/surrogate_key.py +114 -0
  512. kiri_dbt_bridge/macros/kiri_scd1_dim.sql +57 -0
  513. kiri_dbt_bridge/macros/kiri_scd2_dim.sql +188 -0
  514. kiri_dbt_bridge/macros/kiri_sha256.sql +32 -0
  515. kiri_dbt_bridge/macros/kiri_string_cast.sql +37 -0
  516. kiri_dbt_bridge/macros/kiri_surrogate_key.sql +43 -0
  517. kiri_dbt_bridge/profiles.py +162 -0
  518. kiri_dbt_bridge/py.typed +0 -0
  519. kiri_dbt_bridge/safe_ident.py +44 -0
  520. kiri_dbt_bridge/silver_conformance.py +188 -0
  521. kiri_dbt_bridge/silver_kimball/__init__.py +27 -0
  522. kiri_dbt_bridge/silver_kimball/assemble.py +174 -0
  523. kiri_dbt_bridge/silver_kimball/dim_generator.py +602 -0
  524. kiri_dbt_bridge/silver_kimball/dispatch.py +86 -0
  525. kiri_dbt_bridge/silver_kimball/fact_generator.py +320 -0
  526. kiri_dbt_bridge/silver_kimball/flat_generator.py +156 -0
  527. kiri_dbt_bridge/silver_kimball/lineage.py +464 -0
  528. kiri_dbt_bridge/silver_kimball/watermark_window.py +199 -0
  529. kiri_dbt_bridge/silver_kimball/zone.py +68 -0
  530. kiri_dbt_bridge/silver_scd2_curated/__init__.py +46 -0
  531. kiri_dbt_bridge/silver_scd2_curated/assemble.py +213 -0
  532. kiri_dbt_bridge/silver_scd2_curated/curated_generator.py +351 -0
  533. kiri_dbt_bridge/silver_scd2_curated/standardized_generator.py +261 -0
  534. kiri_dbt_bridge/silver_scd2_curated/standardized_plan.py +254 -0
  535. kiri_iceberg/__init__.py +11 -0
  536. kiri_iceberg/catalog.py +128 -0
  537. kiri_iceberg/profile.py +131 -0
  538. kiri_iceberg/py.typed +0 -0
  539. kiri_ingestion/__init__.py +63 -0
  540. kiri_ingestion/airbyte_client.py +428 -0
  541. kiri_ingestion/database_fetcher.py +224 -0
  542. kiri_ingestion/layout.py +71 -0
  543. kiri_ingestion/mssql_introspect.py +565 -0
  544. kiri_ingestion/py.typed +0 -0
  545. kiri_ingestion/rest_fetcher.py +472 -0
  546. kiri_ingestion/runner.py +341 -0
  547. kiri_ingestion/schema_drift.py +222 -0
  548. kiri_ingestion/storage.py +621 -0
  549. kiri_mcp/__init__.py +5 -0
  550. kiri_mcp/audit.py +99 -0
  551. kiri_mcp/audit_decorators.py +252 -0
  552. kiri_mcp/main.py +22 -0
  553. kiri_mcp/py.typed +0 -0
  554. kiri_mcp/rbac.py +110 -0
  555. kiri_mcp/server.py +766 -0
  556. kiri_mcp/skills.py +297 -0
  557. kiri_mcp/tools/__init__.py +70 -0
  558. kiri_mcp/tools/_shared.py +58 -0
  559. kiri_mcp/tools/adapter.py +150 -0
  560. kiri_mcp/tools/cli_reference.py +59 -0
  561. kiri_mcp/tools/compliance.py +119 -0
  562. kiri_mcp/tools/contract.py +374 -0
  563. kiri_mcp/tools/dv.py +62 -0
  564. kiri_mcp/tools/federation.py +152 -0
  565. kiri_mcp/tools/intent.py +71 -0
  566. kiri_mcp/tools/lineage.py +204 -0
  567. kiri_mcp/tools/lineage_drift.py +75 -0
  568. kiri_mcp/tools/plan.py +66 -0
  569. kiri_mcp/tools/project.py +311 -0
  570. kiri_migration/__init__.py +56 -0
  571. kiri_migration/name_preservation.py +271 -0
  572. kiri_migration/predeploy.py +311 -0
  573. kiri_migration/py.typed +0 -0
  574. kiri_migration/reconcile.py +1891 -0
  575. kiri_migration/scaffold/__init__.py +90 -0
  576. kiri_migration/scaffold/_audit_columns.py +74 -0
  577. kiri_migration/scaffold/_classify.py +100 -0
  578. kiri_migration/scaffold/_typemap.py +71 -0
  579. kiri_migration/scaffold/_yaml.py +33 -0
  580. kiri_migration/scaffold/bronze.py +279 -0
  581. kiri_migration/scaffold/compile_targets/__init__.py +271 -0
  582. kiri_migration/scaffold/compile_targets/_common.py +193 -0
  583. kiri_migration/scaffold/compile_targets/copy_into.py +33 -0
  584. kiri_migration/scaffold/compile_targets/dlt.py +125 -0
  585. kiri_migration/scaffold/compile_targets/dlt_with_append_flows.py +163 -0
  586. kiri_migration/scaffold/compile_targets/dlt_with_apply_changes.py +192 -0
  587. kiri_migration/scaffold/compile_targets/dlt_with_expectations.py +276 -0
  588. kiri_migration/scaffold/compile_targets/layer_dispatch.py +258 -0
  589. kiri_migration/scaffold/compile_targets/sql_full_snapshot.py +385 -0
  590. kiri_migration/scaffold/compile_targets/streaming_table.py +37 -0
  591. kiri_migration/scaffold/mart.py +234 -0
  592. kiri_migration/scaffold/plans/__init__.py +49 -0
  593. kiri_migration/scaffold/plans/_registry.py +308 -0
  594. kiri_migration/scaffold/plans/minimal.py +550 -0
  595. kiri_migration/scaffold/plans/plan_b_layer.py +640 -0
  596. kiri_migration/scaffold/silver_from_marts.py +256 -0
  597. kiri_migration/scaffold/silver_pattern.py +451 -0
  598. kiri_migration/sp_translator.py +398 -0
  599. kiri_odcs/LICENSE +204 -0
  600. kiri_odcs/__init__.py +234 -0
  601. kiri_odcs/ai_trust_tier.py +475 -0
  602. kiri_odcs/canonical.py +263 -0
  603. kiri_odcs/diff.py +63 -0
  604. kiri_odcs/errors.py +51 -0
  605. kiri_odcs/invariant_sql.py +136 -0
  606. kiri_odcs/kiri_bridge.py +71 -0
  607. kiri_odcs/kiri_ext.py +3166 -0
  608. kiri_odcs/kiri_fk.py +447 -0
  609. kiri_odcs/kiri_pit.py +121 -0
  610. kiri_odcs/kiri_ref.py +57 -0
  611. kiri_odcs/openlineage_adapter.py +105 -0
  612. kiri_odcs/parser.py +1213 -0
  613. kiri_odcs/pii_guardrails.py +159 -0
  614. kiri_odcs/py.typed +0 -0
  615. kiri_odcs/schema_export.py +223 -0
  616. kiri_openlineage/__init__.py +58 -0
  617. kiri_openlineage/builder.py +146 -0
  618. kiri_openlineage/cost.py +190 -0
  619. kiri_openlineage/emitter.py +165 -0
  620. kiri_openlineage/py.typed +0 -0
  621. kiri_openlineage/schema.py +121 -0
  622. kiri_packs/__init__.py +67 -0
  623. kiri_packs/installer.py +500 -0
  624. kiri_packs/manifest.py +134 -0
  625. kiri_packs/publisher.py +605 -0
  626. kiri_packs/py.typed +0 -0
  627. kiri_packs/validator.py +295 -0
  628. kiri_quality/__init__.py +108 -0
  629. kiri_quality/ai_audit_correlator.py +293 -0
  630. kiri_quality/ai_audit_correlator_apply.py +526 -0
  631. kiri_quality/conformance_evidence.py +272 -0
  632. kiri_quality/dbt_test_collector.py +214 -0
  633. kiri_quality/dq_codegen.py +300 -0
  634. kiri_quality/dv_checks.py +393 -0
  635. kiri_quality/dv_column_lineage.py +279 -0
  636. kiri_quality/dv_column_lineage_apply.py +856 -0
  637. kiri_quality/dv_conformance.py +841 -0
  638. kiri_quality/dv_fixture_runner.py +478 -0
  639. kiri_quality/dv_invariants.py +986 -0
  640. kiri_quality/dv_kiri.py +97 -0
  641. kiri_quality/dv_markdown.py +169 -0
  642. kiri_quality/dv_observed_uc_reader.py +187 -0
  643. kiri_quality/dv_runner.py +389 -0
  644. kiri_quality/evidence_status.py +337 -0
  645. kiri_quality/fk_checks.py +800 -0
  646. kiri_quality/fk_ddl.py +402 -0
  647. kiri_quality/fk_pit_contract.py +168 -0
  648. kiri_quality/fk_runner.py +950 -0
  649. kiri_quality/kernel.py +376 -0
  650. kiri_quality/pii.py +60 -0
  651. kiri_quality/pre_classify/__init__.py +55 -0
  652. kiri_quality/pre_classify/engine.py +346 -0
  653. kiri_quality/pre_classify/locale_packs/en.yaml +117 -0
  654. kiri_quality/pre_classify/locale_packs/sv.yaml +73 -0
  655. kiri_quality/pre_classify/merge.py +297 -0
  656. kiri_quality/pre_classify/model.py +163 -0
  657. kiri_quality/py.typed +0 -0
  658. kiri_quality/reject_lane.py +261 -0
  659. kiri_quality/rule_runner.py +565 -0
  660. kiri_quality/schema_evolution_check.py +88 -0
  661. kiri_quality/silver_quality.py +494 -0
  662. kiri_quality/silver_quality_markdown.py +187 -0
  663. kiri_quality/uc_security_compile.py +951 -0
  664. kiri_quality/uc_security_compile_apply.py +1186 -0
  665. kiri_rbac/__init__.py +87 -0
  666. kiri_rbac/access.py +371 -0
  667. kiri_rbac/loader.py +104 -0
  668. kiri_rbac/pr_gate.py +213 -0
  669. kiri_rbac/providers/__init__.py +29 -0
  670. kiri_rbac/providers/_jwt.py +121 -0
  671. kiri_rbac/providers/azure_ad.py +93 -0
  672. kiri_rbac/providers/base.py +107 -0
  673. kiri_rbac/providers/factory.py +48 -0
  674. kiri_rbac/providers/github.py +134 -0
  675. kiri_rbac/providers/oidc_generic.py +99 -0
  676. kiri_rbac/py.typed +0 -0
  677. kiri_rbac/resolver.py +118 -0
  678. kiri_rbac/schema.py +219 -0
  679. kiri_schema_registry/__init__.py +22 -0
  680. kiri_schema_registry/avro.py +127 -0
  681. kiri_schema_registry/base.py +71 -0
  682. kiri_schema_registry/confluent.py +184 -0
  683. kiri_schema_registry/glue.py +178 -0
  684. kiri_schema_registry/py.typed +0 -0
  685. kiri_slack_bot/__init__.py +24 -0
  686. kiri_slack_bot/handler.py +248 -0
  687. kiri_slack_bot/py.typed +0 -0
  688. kiri_slack_bot/queries.py +90 -0
  689. kiri_slack_bot/signature.py +94 -0
  690. kiri_vault/__init__.py +59 -0
  691. kiri_vault/audit.py +64 -0
  692. kiri_vault/aws_vault.py +123 -0
  693. kiri_vault/azure_vault.py +236 -0
  694. kiri_vault/boundaries.py +219 -0
  695. kiri_vault/client.py +54 -0
  696. kiri_vault/databricks_vault.py +292 -0
  697. kiri_vault/env_vault.py +77 -0
  698. kiri_vault/py.typed +0 -0
  699. kiri_vault/ref.py +84 -0
  700. kirimana_automation/__init__.py +0 -0
  701. kirimana_automation/business_calendar.py +179 -0
  702. kirimana_automation/dag_snapshot.py +252 -0
  703. kirimana_automation/db/__init__.py +5 -0
  704. kirimana_automation/db/migrate.py +571 -0
  705. kirimana_automation/db/migrations/0001_release_state.down.duckdb.sql +29 -0
  706. kirimana_automation/db/migrations/0001_release_state.down.postgres.sql +37 -0
  707. kirimana_automation/db/migrations/0001_release_state.duckdb.sql +170 -0
  708. kirimana_automation/db/migrations/0001_release_state.postgres.sql +176 -0
  709. kirimana_automation/db/migrations/0002_rate_limit_buckets.down.duckdb.sql +9 -0
  710. kirimana_automation/db/migrations/0002_rate_limit_buckets.down.postgres.sql +9 -0
  711. kirimana_automation/db/migrations/0002_rate_limit_buckets.duckdb.sql +14 -0
  712. kirimana_automation/db/migrations/0002_rate_limit_buckets.postgres.sql +18 -0
  713. kirimana_automation/db/migrations/0003_flow_runs.down.duckdb.sql +18 -0
  714. kirimana_automation/db/migrations/0003_flow_runs.down.postgres.sql +19 -0
  715. kirimana_automation/db/migrations/0003_flow_runs.duckdb.sql +85 -0
  716. kirimana_automation/db/migrations/0003_flow_runs.postgres.sql +102 -0
  717. kirimana_automation/db/migrations/0004_flow_dag_snapshots.down.duckdb.sql +11 -0
  718. kirimana_automation/db/migrations/0004_flow_dag_snapshots.down.postgres.sql +11 -0
  719. kirimana_automation/db/migrations/0004_flow_dag_snapshots.duckdb.sql +15 -0
  720. kirimana_automation/db/migrations/0004_flow_dag_snapshots.postgres.sql +31 -0
  721. kirimana_automation/db/migrations/0005_calendar_skip.down.duckdb.sql +109 -0
  722. kirimana_automation/db/migrations/0005_calendar_skip.down.postgres.sql +42 -0
  723. kirimana_automation/db/migrations/0005_calendar_skip.duckdb.sql +113 -0
  724. kirimana_automation/db/migrations/0005_calendar_skip.postgres.sql +42 -0
  725. kirimana_automation/db/migrations/0006_flow_apply_log.down.duckdb.sql +9 -0
  726. kirimana_automation/db/migrations/0006_flow_apply_log.down.postgres.sql +11 -0
  727. kirimana_automation/db/migrations/0006_flow_apply_log.duckdb.sql +45 -0
  728. kirimana_automation/db/migrations/0006_flow_apply_log.postgres.sql +49 -0
  729. kirimana_automation/db/migrations/0007_flow_task_error_redacted.down.duckdb.sql +14 -0
  730. kirimana_automation/db/migrations/0007_flow_task_error_redacted.down.postgres.sql +2 -0
  731. kirimana_automation/db/migrations/0007_flow_task_error_redacted.duckdb.sql +7 -0
  732. kirimana_automation/db/migrations/0007_flow_task_error_redacted.postgres.sql +11 -0
  733. kirimana_automation/db/migrations/README.md +84 -0
  734. kirimana_automation/flow_loader.py +220 -0
  735. kirimana_automation/freeze.py +133 -0
  736. kirimana_automation/kiri_recommender.py +313 -0
  737. kirimana_automation/main.py +195 -0
  738. kirimana_automation/py.typed +0 -0
  739. kirimana_automation/run_history.py +593 -0
  740. kirimana_automation/run_history_analysis.py +607 -0
  741. kirimana_automation/schedule.py +343 -0
  742. kirimana_automation/schedule_validator.py +572 -0
  743. kirimana_automation/workflow_compiler.py +1288 -0
kiri_cli/sla_cmd.py ADDED
@@ -0,0 +1,769 @@
1
+ """``kiri sla check`` — runtime SLA enforcement (#161 / ADR 0036).
2
+
3
+ Reads each contract's ``kiri.sla.*`` block (#160), checks declared
4
+ freshness + quality thresholds against observed signals, and routes
5
+ breaches via the prefix-dispatcher (slack: / teams: / local:).
6
+
7
+ Two pure layers + one I/O layer:
8
+
9
+ - :func:`evaluate_freshness` / :func:`evaluate_quality` — pure
10
+ functions that compare a SlaMeta to a single observed value and
11
+ return a :class:`SlaCheckResult`. Pure = directly unit-testable.
12
+ - :func:`build_breach_message` — turns a breached report into the
13
+ payload shape webhooks accept. Also pure.
14
+ - :func:`dispatch` — the HTTP layer. Mockable through a callable
15
+ parameter so tests don't speak HTTP.
16
+
17
+ The CLI command (``sla_check_cmd``) glues them together: load
18
+ project, fetch signals via the platform adapter, evaluate, dispatch.
19
+
20
+ Why route is a free-form prefix descriptor rather than a typed enum
21
+ of providers: each provider has its own auth shape (Slack uses an
22
+ incoming webhook URL from env, PagerDuty needs a routing key, etc).
23
+ The descriptor stays in the contract; the env wiring lives in the
24
+ deployment. Adding a new provider = one prefix branch in
25
+ :func:`dispatch` + one env var documented in ADR 0036.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import json
31
+ import os
32
+ import re
33
+ from collections.abc import Callable, Mapping
34
+ from datetime import UTC, datetime, timedelta
35
+ from pathlib import Path
36
+ from typing import Annotated, Any, Literal
37
+ from urllib.request import Request, urlopen
38
+
39
+ import typer
40
+ from kiri_adapters import get_adapter
41
+ from kiri_adapters.base import ProjectManifest
42
+ from kiri_core import Project, load_project
43
+ from kiri_core.odcs import CanonicalContract, SlaMeta
44
+ from pydantic import BaseModel, ConfigDict
45
+ from rich.console import Console
46
+ from rich.table import Table
47
+
48
+ from kiri_cli._project_option import resolve_project
49
+
50
+ console = Console()
51
+
52
+
53
+ # Match SlaMeta.freshness shape — same regex, kept local so we can
54
+ # tokenise the digit + unit cleanly without re-parsing.
55
+ _DURATION_RE = re.compile(r"^(\d+)([smhdw])$", re.IGNORECASE)
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Result models
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ CheckStatus = Literal["ok", "breach", "skipped", "unmeasured"]
64
+ CheckKind = Literal["freshness", "quality"]
65
+
66
+
67
+ class SlaCheckResult(BaseModel):
68
+ """One SLA check outcome on one contract.
69
+
70
+ ``status="skipped"`` means the check itself doesn't apply (e.g. the
71
+ contract didn't declare a threshold) — operators read it as "no
72
+ SLA set, nothing to evaluate".
73
+
74
+ ``status="unmeasured"`` means the check applies but the observed
75
+ signal isn't wired through yet (e.g. catalog quality signal not
76
+ plumbed into ``sla check``). Operators read it as "we owe you a
77
+ real answer here" — distinct from "passed".
78
+ """
79
+
80
+ model_config = ConfigDict(extra="forbid", frozen=True)
81
+
82
+ contract_name: str
83
+ kind: CheckKind
84
+ status: CheckStatus
85
+ threshold: str | None = None # human-readable, e.g. "4h" or "0.99"
86
+ observed: str | None = None
87
+ message: str = ""
88
+
89
+
90
+ class SlaContractReport(BaseModel):
91
+ """Aggregate per-contract report — every check + the route used."""
92
+
93
+ model_config = ConfigDict(extra="forbid", frozen=True)
94
+
95
+ contract_name: str
96
+ results: list[SlaCheckResult]
97
+ route: str | None = None
98
+ severity: str | None = None
99
+
100
+ @property
101
+ def has_breach(self) -> bool:
102
+ return any(r.status == "breach" for r in self.results)
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Pure evaluators
107
+ # ---------------------------------------------------------------------------
108
+
109
+
110
+ def _parse_duration(spec: str) -> timedelta:
111
+ """Convert a freshness spec like ``4h`` to a :class:`timedelta`."""
112
+ match = _DURATION_RE.match(spec)
113
+ if match is None: # pragma: no cover — gated by SlaMeta validator
114
+ raise ValueError(f"invalid duration: {spec!r}")
115
+ n, unit = int(match.group(1)), match.group(2).lower()
116
+ multipliers: dict[str, timedelta] = {
117
+ "s": timedelta(seconds=1),
118
+ "m": timedelta(minutes=1),
119
+ "h": timedelta(hours=1),
120
+ "d": timedelta(days=1),
121
+ "w": timedelta(weeks=1),
122
+ }
123
+ return n * multipliers[unit]
124
+
125
+
126
+ def evaluate_freshness(
127
+ *,
128
+ contract_name: str,
129
+ sla: SlaMeta,
130
+ manifest: ProjectManifest | None,
131
+ now: datetime | None = None,
132
+ ) -> SlaCheckResult:
133
+ """Compare ``sla.freshness`` to the latest apply timestamp.
134
+
135
+ ``now`` is injectable for deterministic tests. ``manifest`` is
136
+ fetched once by the caller for the active ``(project, target)`` and
137
+ is None when no apply has stamped the manifest yet.
138
+
139
+ Returns ``status="skipped"`` when the contract didn't declare
140
+ ``kiri.sla.freshness`` — silent skip beats noisy "no SLA set" rows
141
+ in the report.
142
+ """
143
+ if sla.freshness is None:
144
+ return SlaCheckResult(
145
+ contract_name=contract_name,
146
+ kind="freshness",
147
+ status="skipped",
148
+ message="no kiri.sla.freshness declared",
149
+ )
150
+ latest_apply_iso = manifest.release_ts if manifest else None
151
+ if latest_apply_iso is None:
152
+ return SlaCheckResult(
153
+ contract_name=contract_name,
154
+ kind="freshness",
155
+ status="skipped",
156
+ threshold=sla.freshness,
157
+ message="no apply has stamped the manifest yet",
158
+ )
159
+
160
+ threshold = _parse_duration(sla.freshness)
161
+ try:
162
+ latest = datetime.fromisoformat(latest_apply_iso.replace("Z", "+00:00"))
163
+ except ValueError:
164
+ return SlaCheckResult(
165
+ contract_name=contract_name,
166
+ kind="freshness",
167
+ status="skipped",
168
+ threshold=sla.freshness,
169
+ message=f"can't parse manifest release_ts: {latest_apply_iso!r}",
170
+ )
171
+ if latest.tzinfo is None:
172
+ latest = latest.replace(tzinfo=UTC)
173
+
174
+ current = now if now is not None else datetime.now(UTC)
175
+ age = current - latest
176
+ age_str = _format_timedelta(age)
177
+
178
+ if age <= threshold:
179
+ return SlaCheckResult(
180
+ contract_name=contract_name,
181
+ kind="freshness",
182
+ status="ok",
183
+ threshold=sla.freshness,
184
+ observed=age_str,
185
+ )
186
+ return SlaCheckResult(
187
+ contract_name=contract_name,
188
+ kind="freshness",
189
+ status="breach",
190
+ threshold=sla.freshness,
191
+ observed=age_str,
192
+ message=(f"latest apply was {age_str} ago, exceeds {sla.freshness}"),
193
+ )
194
+
195
+
196
+ def evaluate_quality(
197
+ *,
198
+ contract_name: str,
199
+ sla: SlaMeta,
200
+ latest_pass_rate: float | None,
201
+ ) -> SlaCheckResult:
202
+ """Compare ``sla.quality_threshold`` to the latest GE pass-rate.
203
+
204
+ The catalog wiring that surfaces ``latest_pass_rate`` lives in a
205
+ separate task (see TODO at ``sla_check_cmd`` and follow-up issue
206
+ referenced in CODEX-TECH-DEBT-V1 slice F / finding F11). When the
207
+ threshold is declared but the signal isn't wired through, this
208
+ function emits ``status="unmeasured"`` — explicitly different from
209
+ "skipped" (= no threshold to evaluate). Operators see at a glance
210
+ that the check is owed a real answer, not silently passing.
211
+ """
212
+ if sla.quality_threshold is None:
213
+ return SlaCheckResult(
214
+ contract_name=contract_name,
215
+ kind="quality",
216
+ status="skipped",
217
+ message="no kiri.sla.quality_threshold declared",
218
+ )
219
+ if latest_pass_rate is None:
220
+ return SlaCheckResult(
221
+ contract_name=contract_name,
222
+ kind="quality",
223
+ status="unmeasured",
224
+ threshold=f"{sla.quality_threshold:.2f}",
225
+ message=(
226
+ "quality signal not wired — GE pass-rate is not yet "
227
+ "plumbed into `sla check` (CODEX-TECH-DEBT-V1 F11)"
228
+ ),
229
+ )
230
+ threshold = sla.quality_threshold
231
+ if latest_pass_rate >= threshold:
232
+ return SlaCheckResult(
233
+ contract_name=contract_name,
234
+ kind="quality",
235
+ status="ok",
236
+ threshold=f"{threshold:.2f}",
237
+ observed=f"{latest_pass_rate:.2f}",
238
+ )
239
+ return SlaCheckResult(
240
+ contract_name=contract_name,
241
+ kind="quality",
242
+ status="breach",
243
+ threshold=f"{threshold:.2f}",
244
+ observed=f"{latest_pass_rate:.2f}",
245
+ message=(f"pass-rate {latest_pass_rate:.2f} below threshold {threshold:.2f}"),
246
+ )
247
+
248
+
249
+ def _format_timedelta(td: timedelta) -> str:
250
+ """Compact human-readable timedelta — '3d 4h' / '15m' / '12h'."""
251
+ total_minutes = int(td.total_seconds() // 60)
252
+ if total_minutes < 1:
253
+ return f"{int(td.total_seconds())}s"
254
+ if total_minutes < 60:
255
+ return f"{total_minutes}m"
256
+ days, hours = divmod(total_minutes // 60, 24)
257
+ if days:
258
+ return f"{days}d {hours}h" if hours else f"{days}d"
259
+ return f"{hours}h"
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Breach message + dispatcher
264
+ # ---------------------------------------------------------------------------
265
+
266
+
267
+ def build_breach_message(report: SlaContractReport) -> dict[str, Any]:
268
+ """Render a breached report as a webhook-friendly payload.
269
+
270
+ Same shape for every transport (Slack/Teams/local) — we keep the
271
+ payload structured (not pre-formatted markdown) so each
272
+ transport's renderer can choose its own visual treatment. For
273
+ Slack/Teams that's a Block Kit / Adaptive Card; for local stdout
274
+ it's just JSON dumped.
275
+ """
276
+ breaches = [r for r in report.results if r.status == "breach"]
277
+ return {
278
+ "contract": report.contract_name,
279
+ "severity": report.severity or "medium",
280
+ "breaches": [
281
+ {
282
+ "kind": b.kind,
283
+ "threshold": b.threshold,
284
+ "observed": b.observed,
285
+ "message": b.message,
286
+ }
287
+ for b in breaches
288
+ ],
289
+ }
290
+
291
+
292
+ HttpPoster = Callable[[str, str, Mapping[str, str] | None], int]
293
+ """Callable that POSTs ``body`` to ``url`` with optional extra headers and
294
+ returns the status code.
295
+
296
+ Injected so tests don't speak HTTP. The default implementation uses
297
+ ``urllib.request.urlopen`` — no extra dependency, pure stdlib.
298
+
299
+ Sprint 1.7 (2026-05-23): the third positional ``headers`` argument is
300
+ required by the OpsGenie route, which authenticates via an
301
+ ``Authorization: GenieKey <api_key>`` header rather than a webhook URL
302
+ that embeds the secret. Slack / Teams / PagerDuty paths pass
303
+ ``headers=None``. Tests inject deterministic posters that mirror the
304
+ same three-positional signature.
305
+ """
306
+
307
+
308
+ def _default_http_post(
309
+ url: str,
310
+ body: str,
311
+ headers: Mapping[str, str] | None = None,
312
+ ) -> int:
313
+ """Default poster: stdlib urlopen with JSON content-type plus any
314
+ operator-supplied headers (e.g. OpsGenie ``Authorization: GenieKey``).
315
+ """
316
+ request_headers = {"Content-Type": "application/json"}
317
+ if headers:
318
+ request_headers.update(headers)
319
+ req = Request( # noqa: S310 — URL is operator-supplied via env
320
+ url,
321
+ data=body.encode("utf-8"),
322
+ headers=request_headers,
323
+ method="POST",
324
+ )
325
+ with urlopen(req, timeout=10) as resp: # noqa: S310
326
+ return int(resp.status)
327
+
328
+
329
+ def dispatch(
330
+ route: str,
331
+ payload: dict[str, Any],
332
+ *,
333
+ poster: HttpPoster | None = None,
334
+ dry_run: bool = False,
335
+ ) -> str:
336
+ """Send a breach notification via the route's prefix dispatcher.
337
+
338
+ Supported prefixes:
339
+
340
+ - ``local:`` — print JSON to stdout (no HTTP). Useful in CI / dev.
341
+ - ``slack:`` — POST to ``$SLACK_WEBHOOK_URL`` (operator configures
342
+ the incoming webhook URL out-of-band).
343
+ - ``teams:`` — POST to ``$TEAMS_WEBHOOK_URL``.
344
+
345
+ Sprint 1.7 (2026-05-23) closes the deferred PagerDuty + Opsgenie
346
+ wiring. The route prefix selects the transport, the suffix is the
347
+ instance identifier (PagerDuty Events v2 service-routing key,
348
+ Opsgenie team alias). The secret itself MUST come from an
349
+ environment variable, never the CLI argument, so the routing key
350
+ is never logged to shell history.
351
+
352
+ ``dry_run=True`` short-circuits transport and returns a "(dry-run)"
353
+ marker — same control-flow path as a real send but without I/O.
354
+ """
355
+ poster = poster or _default_http_post
356
+ body = json.dumps(payload, indent=2)
357
+
358
+ if dry_run:
359
+ return f"(dry-run) would route to {route}: {payload['contract']}"
360
+
361
+ if route.startswith("local:"):
362
+ print(body)
363
+ return f"local: printed payload for {payload['contract']}"
364
+
365
+ if route.startswith("slack:"):
366
+ url = os.environ.get("SLACK_WEBHOOK_URL")
367
+ if not url:
368
+ return f"slack: SLACK_WEBHOOK_URL not set — skipped {payload['contract']}"
369
+ # Slack incoming webhooks accept simple {"text": "..."} bodies.
370
+ text = _format_for_slack(payload)
371
+ status = poster(url, json.dumps({"text": text}), None)
372
+ return f"slack: HTTP {status} for {payload['contract']}"
373
+
374
+ if route.startswith("teams:"):
375
+ url = os.environ.get("TEAMS_WEBHOOK_URL")
376
+ if not url:
377
+ return f"teams: TEAMS_WEBHOOK_URL not set — skipped {payload['contract']}"
378
+ text = _format_for_teams(payload)
379
+ status = poster(url, json.dumps({"text": text}), None)
380
+ return f"teams: HTTP {status} for {payload['contract']}"
381
+
382
+ if route.startswith("pagerduty:"):
383
+ # PagerDuty Events API v2 (https://events.pagerduty.com/v2/enqueue).
384
+ # The routing key is a 32-char service-integration token; we
385
+ # require it via PAGERDUTY_ROUTING_KEY rather than splicing it
386
+ # into the route argument so it never lands in shell history or
387
+ # in a git-tracked kiri.yml.
388
+ routing_key = os.environ.get("PAGERDUTY_ROUTING_KEY")
389
+ if not routing_key:
390
+ return f"pagerduty: PAGERDUTY_ROUTING_KEY not set — skipped {payload['contract']}"
391
+ event_body = json.dumps(_format_for_pagerduty(payload, routing_key))
392
+ status = poster("https://events.pagerduty.com/v2/enqueue", event_body, None)
393
+ return f"pagerduty: HTTP {status} for {payload['contract']}"
394
+
395
+ if route.startswith("opsgenie:"):
396
+ # Opsgenie Alerts API
397
+ # (https://api.opsgenie.com/v2/alerts). Auth header is
398
+ # ``Authorization: GenieKey <api_key>``; the key comes from
399
+ # OPSGENIE_API_KEY for the same reason PagerDuty's routing key
400
+ # does not live on the CLI.
401
+ api_key = os.environ.get("OPSGENIE_API_KEY")
402
+ if not api_key:
403
+ return f"opsgenie: OPSGENIE_API_KEY not set — skipped {payload['contract']}"
404
+ alert_body = json.dumps(_format_for_opsgenie(payload))
405
+ status = poster(
406
+ "https://api.opsgenie.com/v2/alerts",
407
+ alert_body,
408
+ {"Authorization": f"GenieKey {api_key}"},
409
+ )
410
+ return f"opsgenie: HTTP {status} for {payload['contract']}"
411
+
412
+ return f"unknown route prefix: {route!r} (skipped {payload['contract']})"
413
+
414
+
415
+ def _format_for_slack(payload: dict[str, Any]) -> str:
416
+ """Plain-text Slack message — one line per breach, severity tag."""
417
+ lines = [
418
+ f":rotating_light: *SLA breach* on `{payload['contract']}` (severity {payload['severity']})"
419
+ ]
420
+ for b in payload["breaches"]:
421
+ lines.append(
422
+ f" • {b['kind']}: {b['message']} "
423
+ f"(threshold {b['threshold']}, observed {b['observed']})"
424
+ )
425
+ return "\n".join(lines)
426
+
427
+
428
+ def _format_for_teams(payload: dict[str, Any]) -> str:
429
+ """Plain-text Teams message — same shape, different emoji conventions."""
430
+ lines = [f"⚠️ SLA breach on **{payload['contract']}** (severity {payload['severity']})"]
431
+ for b in payload["breaches"]:
432
+ lines.append(
433
+ f"- {b['kind']}: {b['message']} (threshold {b['threshold']}, observed {b['observed']})"
434
+ )
435
+ return "\n".join(lines)
436
+
437
+
438
+ _PAGERDUTY_SEVERITY_MAP: dict[str, str] = {
439
+ "critical": "critical",
440
+ "high": "error",
441
+ "warning": "warning",
442
+ "low": "info",
443
+ }
444
+
445
+
446
+ def _format_for_pagerduty(payload: dict[str, Any], routing_key: str) -> dict[str, Any]:
447
+ """Build a PagerDuty Events API v2 ``trigger`` event.
448
+
449
+ The ``dedup_key`` keys on contract identity so re-firing the same
450
+ breach updates the same incident instead of creating duplicates.
451
+ The full breach payload rides in ``custom_details`` for
452
+ on-call responders.
453
+ """
454
+
455
+ severity = _PAGERDUTY_SEVERITY_MAP.get(str(payload.get("severity", "")).lower(), "warning")
456
+ summaries = [
457
+ f"{b['kind']}: {b['message']} (threshold {b['threshold']}, observed {b['observed']})"
458
+ for b in payload["breaches"]
459
+ ]
460
+ summary = (
461
+ f"SLA breach on {payload['contract']} — {len(summaries)} "
462
+ f"breach{'es' if len(summaries) != 1 else ''}"
463
+ )
464
+ return {
465
+ "routing_key": routing_key,
466
+ "event_action": "trigger",
467
+ "dedup_key": f"kirimana:sla:{payload['contract']}",
468
+ "payload": {
469
+ "summary": summary,
470
+ "severity": severity,
471
+ "source": "kirimana",
472
+ "component": payload["contract"],
473
+ "group": "sla",
474
+ "class": "sla_breach",
475
+ "custom_details": {
476
+ "contract": payload["contract"],
477
+ "severity_input": payload.get("severity"),
478
+ "breaches": payload["breaches"],
479
+ "details": summaries,
480
+ },
481
+ },
482
+ }
483
+
484
+
485
+ _OPSGENIE_PRIORITY_MAP: dict[str, str] = {
486
+ "critical": "P1",
487
+ "high": "P2",
488
+ "warning": "P3",
489
+ "low": "P4",
490
+ }
491
+
492
+
493
+ def _format_for_opsgenie(payload: dict[str, Any]) -> dict[str, Any]:
494
+ """Build an Opsgenie alert payload.
495
+
496
+ ``alias`` keys on contract identity (same dedup semantics as the
497
+ PagerDuty ``dedup_key``); ``priority`` derives from the SLA
498
+ severity via the closed-menu mapping above. The full breach
499
+ payload rides in ``details`` so on-call responders can drill in
500
+ without leaving Opsgenie.
501
+ """
502
+
503
+ priority = _OPSGENIE_PRIORITY_MAP.get(str(payload.get("severity", "")).lower(), "P3")
504
+ message = (
505
+ f"SLA breach on {payload['contract']} ({len(payload['breaches'])} "
506
+ f"breach{'es' if len(payload['breaches']) != 1 else ''})"
507
+ )
508
+ description_lines = [
509
+ f"{b['kind']}: {b['message']} (threshold {b['threshold']}, observed {b['observed']})"
510
+ for b in payload["breaches"]
511
+ ]
512
+ return {
513
+ "message": message,
514
+ "alias": f"kirimana:sla:{payload['contract']}",
515
+ "priority": priority,
516
+ "source": "kirimana",
517
+ "tags": ["kirimana", "sla", payload["contract"]],
518
+ "description": "\n".join(description_lines),
519
+ "details": {
520
+ "contract": payload["contract"],
521
+ "severity_input": str(payload.get("severity") or ""),
522
+ "breach_count": str(len(payload["breaches"])),
523
+ },
524
+ }
525
+
526
+
527
+ # ---------------------------------------------------------------------------
528
+ # CLI command
529
+ # ---------------------------------------------------------------------------
530
+
531
+
532
+ def _evaluate_contract(
533
+ contract: CanonicalContract,
534
+ *,
535
+ manifest: ProjectManifest | None,
536
+ latest_pass_rate: float | None,
537
+ now: datetime | None = None,
538
+ ) -> SlaContractReport:
539
+ """Run both checks on one contract."""
540
+ sla = contract.kiri.sla
541
+ name = contract.contract.name
542
+ results = [
543
+ evaluate_freshness(
544
+ contract_name=name,
545
+ sla=sla,
546
+ manifest=manifest,
547
+ now=now,
548
+ ),
549
+ evaluate_quality(
550
+ contract_name=name,
551
+ sla=sla,
552
+ latest_pass_rate=latest_pass_rate,
553
+ ),
554
+ ]
555
+ route = sla.on_breach.route if sla.on_breach else None
556
+ severity = sla.on_breach.severity.value if sla.on_breach else None
557
+ return SlaContractReport(
558
+ contract_name=name,
559
+ results=results,
560
+ route=route,
561
+ severity=severity,
562
+ )
563
+
564
+
565
+ _STATUS_GLYPHS = {
566
+ "ok": "[green]✓[/green]",
567
+ "breach": "[red]✗[/red]",
568
+ "skipped": "[dim]·[/dim]",
569
+ "unmeasured": "[yellow]?[/yellow]",
570
+ }
571
+
572
+
573
+ def _render_report(reports: list[SlaContractReport]) -> None:
574
+ table = Table(show_header=True, header_style="bold")
575
+ table.add_column("Contract")
576
+ table.add_column("Check")
577
+ table.add_column("", width=2)
578
+ table.add_column("Threshold")
579
+ table.add_column("Observed")
580
+ table.add_column("Detail", overflow="fold")
581
+ for rep in reports:
582
+ for r in rep.results:
583
+ table.add_row(
584
+ rep.contract_name,
585
+ r.kind,
586
+ _STATUS_GLYPHS[r.status],
587
+ r.threshold or "—",
588
+ r.observed or "—",
589
+ r.message,
590
+ )
591
+ console.print(table)
592
+
593
+
594
+ def sla_check_cmd(
595
+ project: Annotated[
596
+ Path | None,
597
+ typer.Option("--project", "-p", help="Project root."),
598
+ ] = None,
599
+ target: Annotated[
600
+ str | None,
601
+ typer.Option(
602
+ "--target",
603
+ help="Environment target (default: project's default_target).",
604
+ ),
605
+ ] = None,
606
+ contract: Annotated[
607
+ str | None,
608
+ typer.Option(
609
+ "--contract",
610
+ help="Check only this contract (by name).",
611
+ ),
612
+ ] = None,
613
+ dry_run: Annotated[
614
+ bool,
615
+ typer.Option(
616
+ "--dry-run",
617
+ help="Don't actually dispatch breaches; print what would happen.",
618
+ ),
619
+ ] = False,
620
+ ) -> None:
621
+ """Check SLA declarations across the project + route breaches.
622
+
623
+ Reads each contract's ``kiri.sla.*`` block, fetches the platform's
624
+ manifest for the freshness signal, evaluates, dispatches breaches
625
+ via the configured ``on_breach.route``.
626
+
627
+ Exit codes:
628
+
629
+ - ``0`` — no breaches (every check ok or skipped).
630
+ - ``1`` — at least one breach detected.
631
+ - ``2`` — project failed to load.
632
+ """
633
+ project = resolve_project(project)
634
+ try:
635
+ p = load_project(project)
636
+ except Exception as e:
637
+ console.print(f"[red]✗[/red] failed to load project: {e}")
638
+ raise typer.Exit(code=2) from e
639
+
640
+ chosen_target = target or p.config.default_target
641
+ profile = p.config.profile_for(chosen_target)
642
+ adapter = get_adapter(p.config.adapter, profile)
643
+
644
+ # Filter to contracts that declare any kiri.sla.* — otherwise the
645
+ # report is mostly skipped rows. Operators wanting "everything"
646
+ # can pass --contract.
647
+ declared = (
648
+ [c for c in p.contracts if c.contract.name == contract]
649
+ if contract is not None
650
+ else [
651
+ c
652
+ for c in p.contracts
653
+ if c.kiri.sla.freshness is not None
654
+ or c.kiri.sla.quality_threshold is not None
655
+ or c.kiri.sla.on_breach is not None
656
+ ]
657
+ )
658
+
659
+ if not declared:
660
+ if contract is not None:
661
+ console.print(f"[red]✗[/red] no contract named {contract!r}.")
662
+ raise typer.Exit(code=2)
663
+ console.print("[yellow]![/yellow] no contracts declare kiri.sla.* — nothing to check.")
664
+ return
665
+
666
+ # read_manifest returns None when no apply has stamped the slot yet
667
+ # — fine, evaluate_freshness emits "skipped" with a clear message.
668
+ try:
669
+ manifest = adapter.read_manifest(p.config.name, chosen_target)
670
+ except Exception:
671
+ manifest = None
672
+
673
+ reports: list[SlaContractReport] = []
674
+ for c in declared:
675
+ # Catalog quality signal lands later (CODEX-TECH-DEBT-V1 F11) —
676
+ # pass None so the quality check renders as ``unmeasured`` with
677
+ # a "signal not wired" message rather than a misleading
678
+ # ``skipped`` row. The status taxonomy distinguishes the two:
679
+ # ``skipped`` = check doesn't apply; ``unmeasured`` = check
680
+ # applies but we owe a real answer.
681
+ reports.append(
682
+ _evaluate_contract(
683
+ c,
684
+ manifest=manifest,
685
+ latest_pass_rate=None,
686
+ )
687
+ )
688
+
689
+ # Surface the wiring gap loudly above the table so an operator who
690
+ # only glances at the report can't mistake ``unmeasured`` quality
691
+ # rows for ``ok``.
692
+ if any(
693
+ r.status == "unmeasured" and r.kind == "quality" for rep in reports for r in rep.results
694
+ ):
695
+ console.print(
696
+ "[yellow]![/yellow] quality signal not wired — GE pass-rate "
697
+ "is not yet plumbed into `sla check`. Quality rows below "
698
+ "will show `unmeasured`, not `ok`. "
699
+ "(CODEX-TECH-DEBT-V1 F11)"
700
+ )
701
+
702
+ _render_report(reports)
703
+
704
+ breaches = [r for r in reports if r.has_breach]
705
+ if not breaches:
706
+ console.print("\n[green]✓ no breaches[/green]")
707
+ return
708
+
709
+ console.print(f"\n[bold]{len(breaches)} breach(es) — dispatching:[/bold]")
710
+ for rep in breaches:
711
+ if not rep.route:
712
+ console.print(
713
+ f" [yellow]·[/yellow] {rep.contract_name}: "
714
+ "no on_breach.route declared — logged only"
715
+ )
716
+ else:
717
+ outcome = dispatch(rep.route, build_breach_message(rep), dry_run=dry_run)
718
+ console.print(f" → {outcome}")
719
+ # ADR 0038 #173: also dispatch to ITSM if configured. Peer
720
+ # surface to slack/teams — operators get both a notification
721
+ # and a tracked ticket.
722
+ if not dry_run:
723
+ _route_to_itsm(p, project, rep, chosen_target)
724
+ raise typer.Exit(code=1)
725
+
726
+
727
+ def _route_to_itsm(
728
+ project_obj: Project,
729
+ project_root: Path,
730
+ report: SlaContractReport,
731
+ target_env: str,
732
+ ) -> None:
733
+ """Best-effort fan-out to the ITSM dispatcher (#173).
734
+
735
+ Imported lazily so projects without an ``incidents:`` block don't
736
+ pay the kiri_adapters import cost on every SLA check.
737
+ """
738
+ from kiri_core import incident_from_sla_breach
739
+ from kiri_core.apply.incident_router import route_incident
740
+
741
+ incident = incident_from_sla_breach(report, target_env=target_env)
742
+ result = route_incident(project_obj, project_root, incident)
743
+ if result is not None:
744
+ console.print(
745
+ f" → itsm:{result.backend} ticket={result.ticket_id} ({result.action.value})"
746
+ )
747
+
748
+
749
+ def build_sla_app() -> typer.Typer:
750
+ """Build the ``kiri sla ...`` sub-app."""
751
+ sla_app = typer.Typer(
752
+ name="sla",
753
+ help="Data SLA enforcement — check freshness/quality + route breaches (ADR 0036).",
754
+ no_args_is_help=True,
755
+ )
756
+ sla_app.command("check")(sla_check_cmd)
757
+ return sla_app
758
+
759
+
760
+ __all__ = [
761
+ "SlaCheckResult",
762
+ "SlaContractReport",
763
+ "build_breach_message",
764
+ "build_sla_app",
765
+ "dispatch",
766
+ "evaluate_freshness",
767
+ "evaluate_quality",
768
+ "sla_check_cmd",
769
+ ]