omnibase_infra 0.2.1__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.
Files changed (675) hide show
  1. omnibase_infra/__init__.py +101 -0
  2. omnibase_infra/cli/__init__.py +1 -0
  3. omnibase_infra/cli/commands.py +216 -0
  4. omnibase_infra/clients/__init__.py +0 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
  7. omnibase_infra/decorators/__init__.py +29 -0
  8. omnibase_infra/decorators/allow_any.py +109 -0
  9. omnibase_infra/dlq/__init__.py +90 -0
  10. omnibase_infra/dlq/constants_dlq.py +57 -0
  11. omnibase_infra/dlq/models/__init__.py +26 -0
  12. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  13. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  14. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  15. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  16. omnibase_infra/enums/__init__.py +123 -0
  17. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  18. omnibase_infra/enums/enum_backend_type.py +27 -0
  19. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  20. omnibase_infra/enums/enum_capture_state.py +88 -0
  21. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  22. omnibase_infra/enums/enum_circuit_state.py +51 -0
  23. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  24. omnibase_infra/enums/enum_contract_type.py +84 -0
  25. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  26. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  27. omnibase_infra/enums/enum_environment.py +46 -0
  28. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  29. omnibase_infra/enums/enum_handler_error_type.py +101 -0
  30. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  31. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  32. omnibase_infra/enums/enum_handler_type.py +77 -0
  33. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  34. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  35. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  36. omnibase_infra/enums/enum_message_category.py +213 -0
  37. omnibase_infra/enums/enum_node_archetype.py +74 -0
  38. omnibase_infra/enums/enum_node_output_type.py +185 -0
  39. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  40. omnibase_infra/enums/enum_policy_type.py +32 -0
  41. omnibase_infra/enums/enum_registration_state.py +261 -0
  42. omnibase_infra/enums/enum_registration_status.py +33 -0
  43. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  44. omnibase_infra/enums/enum_response_status.py +26 -0
  45. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  46. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  47. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  48. omnibase_infra/enums/enum_topic_standard.py +42 -0
  49. omnibase_infra/enums/enum_validation_severity.py +78 -0
  50. omnibase_infra/errors/__init__.py +156 -0
  51. omnibase_infra/errors/error_architecture_violation.py +152 -0
  52. omnibase_infra/errors/error_chain_propagation.py +188 -0
  53. omnibase_infra/errors/error_compute_registry.py +92 -0
  54. omnibase_infra/errors/error_consul.py +132 -0
  55. omnibase_infra/errors/error_container_wiring.py +243 -0
  56. omnibase_infra/errors/error_event_bus_registry.py +102 -0
  57. omnibase_infra/errors/error_infra.py +608 -0
  58. omnibase_infra/errors/error_message_type_registry.py +101 -0
  59. omnibase_infra/errors/error_policy_registry.py +112 -0
  60. omnibase_infra/errors/error_vault.py +123 -0
  61. omnibase_infra/event_bus/__init__.py +72 -0
  62. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
  63. omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
  64. omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
  65. omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
  66. omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
  67. omnibase_infra/event_bus/models/__init__.py +29 -0
  68. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  69. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
  70. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  71. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  72. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  73. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  74. omnibase_infra/event_bus/topic_constants.py +376 -0
  75. omnibase_infra/handlers/__init__.py +75 -0
  76. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  77. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  78. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  79. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  80. omnibase_infra/handlers/handler_consul.py +787 -0
  81. omnibase_infra/handlers/handler_db.py +1039 -0
  82. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  83. omnibase_infra/handlers/handler_graph.py +1154 -0
  84. omnibase_infra/handlers/handler_http.py +920 -0
  85. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  86. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  87. omnibase_infra/handlers/handler_mcp.py +748 -0
  88. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  89. omnibase_infra/handlers/handler_vault.py +422 -0
  90. omnibase_infra/handlers/mcp/__init__.py +19 -0
  91. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  92. omnibase_infra/handlers/mcp/protocols.py +178 -0
  93. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  94. omnibase_infra/handlers/mixins/__init__.py +42 -0
  95. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  96. omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
  97. omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
  98. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  99. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  100. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  101. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  102. omnibase_infra/handlers/models/__init__.py +286 -0
  103. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  104. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  105. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  106. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  107. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  108. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  109. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  110. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  111. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  112. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  113. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  114. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  115. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  116. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  117. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  118. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  119. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  120. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  121. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  122. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  123. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  124. omnibase_infra/handlers/models/http/__init__.py +50 -0
  125. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  126. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  127. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  128. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  129. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  130. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  131. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  132. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  133. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  134. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  135. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  136. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  137. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  138. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  139. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  140. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  141. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  142. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  143. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  144. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  145. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  146. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  147. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  148. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  149. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  150. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  151. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  152. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  153. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  154. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  155. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  156. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  157. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  158. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  159. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  160. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  161. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  162. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  163. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  164. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  165. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  166. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  167. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  168. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  169. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  170. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  171. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  172. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  173. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  174. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  175. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  176. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  177. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  178. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  179. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  180. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  181. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  182. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  183. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  184. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  185. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  186. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  187. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  188. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  189. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  190. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  191. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
  192. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  193. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  194. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  195. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  196. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  197. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
  198. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  199. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  200. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  201. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  202. omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
  203. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  204. omnibase_infra/idempotency/__init__.py +94 -0
  205. omnibase_infra/idempotency/models/__init__.py +43 -0
  206. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  207. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  208. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  209. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  210. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  211. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  212. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  213. omnibase_infra/idempotency/store_inmemory.py +265 -0
  214. omnibase_infra/idempotency/store_postgres.py +923 -0
  215. omnibase_infra/infrastructure/__init__.py +0 -0
  216. omnibase_infra/mixins/__init__.py +71 -0
  217. omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
  218. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  219. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  220. omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
  221. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  222. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  223. omnibase_infra/models/__init__.py +136 -0
  224. omnibase_infra/models/corpus/__init__.py +17 -0
  225. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  226. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  227. omnibase_infra/models/discovery/__init__.py +42 -0
  228. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  229. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  230. omnibase_infra/models/discovery/model_introspection_config.py +311 -0
  231. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  232. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  233. omnibase_infra/models/dispatch/__init__.py +147 -0
  234. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  235. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  236. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  237. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  238. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  239. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  240. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  241. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  242. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  243. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  244. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  245. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  246. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  247. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  248. omnibase_infra/models/errors/__init__.py +45 -0
  249. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  250. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  251. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  252. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  253. omnibase_infra/models/handlers/__init__.py +37 -0
  254. omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
  255. omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
  256. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  257. omnibase_infra/models/health/__init__.py +9 -0
  258. omnibase_infra/models/health/model_health_check_result.py +40 -0
  259. omnibase_infra/models/lifecycle/__init__.py +39 -0
  260. omnibase_infra/models/logging/__init__.py +51 -0
  261. omnibase_infra/models/logging/model_log_context.py +756 -0
  262. omnibase_infra/models/model_retry_error_classification.py +78 -0
  263. omnibase_infra/models/projection/__init__.py +43 -0
  264. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  265. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  266. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  267. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  268. omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
  269. omnibase_infra/models/projectors/__init__.py +41 -0
  270. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  271. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  272. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  273. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  274. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  275. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  276. omnibase_infra/models/registration/__init__.py +59 -0
  277. omnibase_infra/models/registration/commands/__init__.py +15 -0
  278. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  279. omnibase_infra/models/registration/events/__init__.py +56 -0
  280. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  281. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  282. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  283. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  284. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  285. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  286. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  287. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  288. omnibase_infra/models/registration/model_node_capabilities.py +179 -0
  289. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  290. omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
  291. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  292. omnibase_infra/models/registration/model_node_registration.py +162 -0
  293. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  294. omnibase_infra/models/registry/__init__.py +29 -0
  295. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  296. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  297. omnibase_infra/models/resilience/__init__.py +9 -0
  298. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  299. omnibase_infra/models/routing/__init__.py +25 -0
  300. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  301. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  302. omnibase_infra/models/runtime/__init__.py +40 -0
  303. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  304. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  305. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  306. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  307. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  308. omnibase_infra/models/runtime/model_handler_contract.py +280 -0
  309. omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
  310. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  311. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  312. omnibase_infra/models/security/__init__.py +50 -0
  313. omnibase_infra/models/security/classification_levels.py +99 -0
  314. omnibase_infra/models/security/model_environment_policy.py +145 -0
  315. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  316. omnibase_infra/models/security/model_security_error.py +81 -0
  317. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  318. omnibase_infra/models/security/model_security_warning.py +67 -0
  319. omnibase_infra/models/snapshot/__init__.py +27 -0
  320. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  321. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  322. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  323. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  324. omnibase_infra/models/types/__init__.py +71 -0
  325. omnibase_infra/models/validation/__init__.py +89 -0
  326. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  327. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  328. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  329. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  330. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  331. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  332. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  333. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  334. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  335. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  336. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  337. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  338. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  339. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  340. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  341. omnibase_infra/nodes/__init__.py +48 -0
  342. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  343. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  344. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
  345. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  346. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  347. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  348. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  349. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  350. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  351. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  352. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  353. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  354. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  355. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  356. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  357. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  358. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  359. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
  360. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  361. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  362. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  363. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  364. omnibase_infra/nodes/effects/README.md +358 -0
  365. omnibase_infra/nodes/effects/__init__.py +26 -0
  366. omnibase_infra/nodes/effects/contract.yaml +172 -0
  367. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  368. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  369. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  370. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  371. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  372. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  373. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  374. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  375. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  376. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  377. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  378. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  379. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
  380. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  381. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  382. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  383. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  384. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  385. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  386. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
  387. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  388. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  389. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  390. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  391. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  392. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  393. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  394. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  395. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  396. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  397. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  398. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  399. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  400. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  401. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  402. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  403. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  404. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  405. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  406. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  407. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  408. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
  409. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
  410. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
  411. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  412. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  413. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  414. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  415. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  416. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  417. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  418. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  419. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
  420. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  421. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  422. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  423. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  424. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  425. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  426. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  427. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  428. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  429. omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
  430. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  431. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  432. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  433. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
  434. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  435. omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
  436. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  437. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  438. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  439. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
  440. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  441. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  442. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  443. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  444. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  445. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  446. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  447. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  448. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  449. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  450. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  451. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  452. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  453. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  454. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  455. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  456. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  457. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  458. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  459. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  460. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  461. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  462. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  463. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  464. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  465. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
  466. omnibase_infra/nodes/reducers/__init__.py +30 -0
  467. omnibase_infra/nodes/reducers/models/__init__.py +32 -0
  468. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
  469. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  470. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  471. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  472. omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
  473. omnibase_infra/observability/__init__.py +143 -0
  474. omnibase_infra/observability/constants_metrics.py +91 -0
  475. omnibase_infra/observability/factory_observability_sink.py +525 -0
  476. omnibase_infra/observability/handlers/__init__.py +118 -0
  477. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  478. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  479. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  480. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  481. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  482. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  483. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  484. omnibase_infra/observability/hooks/__init__.py +74 -0
  485. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  486. omnibase_infra/observability/models/__init__.py +30 -0
  487. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  488. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  489. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  490. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  491. omnibase_infra/observability/sinks/__init__.py +69 -0
  492. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  493. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  494. omnibase_infra/plugins/__init__.py +27 -0
  495. omnibase_infra/plugins/examples/__init__.py +28 -0
  496. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  497. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  498. omnibase_infra/plugins/models/__init__.py +21 -0
  499. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  500. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  501. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  502. omnibase_infra/plugins/plugin_compute_base.py +435 -0
  503. omnibase_infra/projectors/__init__.py +30 -0
  504. omnibase_infra/projectors/contracts/__init__.py +63 -0
  505. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  506. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  507. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  508. omnibase_infra/protocols/__init__.py +99 -0
  509. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  510. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  511. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  512. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  513. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  514. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  515. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  516. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  517. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  518. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  519. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  520. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  521. omnibase_infra/runtime/__init__.py +296 -0
  522. omnibase_infra/runtime/binding_config_resolver.py +2706 -0
  523. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  524. omnibase_infra/runtime/contract_handler_discovery.py +582 -0
  525. omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
  526. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  527. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  528. omnibase_infra/runtime/enums/__init__.py +18 -0
  529. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  530. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  531. omnibase_infra/runtime/envelope_validator.py +179 -0
  532. omnibase_infra/runtime/handler_contract_source.py +669 -0
  533. omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
  534. omnibase_infra/runtime/handler_registry.py +321 -0
  535. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  536. omnibase_infra/runtime/kernel.py +40 -0
  537. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  538. omnibase_infra/runtime/mixin_semver_cache.py +378 -0
  539. omnibase_infra/runtime/mixins/__init__.py +17 -0
  540. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
  541. omnibase_infra/runtime/models/__init__.py +192 -0
  542. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  543. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  544. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  545. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  546. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  547. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  548. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  549. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  550. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  551. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  552. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  553. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  554. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  555. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  556. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  557. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  558. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  559. omnibase_infra/runtime/models/model_health_check_result.py +228 -0
  560. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  561. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  562. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  563. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  564. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  565. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  566. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  567. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  568. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  569. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  570. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  571. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  572. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  573. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  574. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
  575. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  576. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  577. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  578. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  579. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  580. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  581. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  582. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  583. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  584. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  585. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  586. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  587. omnibase_infra/runtime/projector_shell.py +1102 -0
  588. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  589. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  590. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  591. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  592. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  593. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  594. omnibase_infra/runtime/protocol_policy.py +366 -0
  595. omnibase_infra/runtime/protocols/__init__.py +27 -0
  596. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  597. omnibase_infra/runtime/registry/__init__.py +93 -0
  598. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  599. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  600. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  601. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  602. omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
  603. omnibase_infra/runtime/registry_compute.py +1143 -0
  604. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  605. omnibase_infra/runtime/registry_policy.py +1502 -0
  606. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  607. omnibase_infra/runtime/secret_resolver.py +2110 -0
  608. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  609. omnibase_infra/runtime/service_kernel.py +1573 -0
  610. omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
  611. omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
  612. omnibase_infra/runtime/util_container_wiring.py +1123 -0
  613. omnibase_infra/runtime/util_validation.py +314 -0
  614. omnibase_infra/runtime/util_version.py +98 -0
  615. omnibase_infra/runtime/util_wiring.py +566 -0
  616. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  617. omnibase_infra/services/__init__.py +68 -0
  618. omnibase_infra/services/corpus_capture.py +678 -0
  619. omnibase_infra/services/service_capability_query.py +945 -0
  620. omnibase_infra/services/service_health.py +897 -0
  621. omnibase_infra/services/service_node_selector.py +530 -0
  622. omnibase_infra/services/service_timeout_emitter.py +682 -0
  623. omnibase_infra/services/service_timeout_scanner.py +390 -0
  624. omnibase_infra/services/snapshot/__init__.py +31 -0
  625. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  626. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  627. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  628. omnibase_infra/shared/__init__.py +8 -0
  629. omnibase_infra/testing/__init__.py +10 -0
  630. omnibase_infra/testing/utils.py +23 -0
  631. omnibase_infra/types/__init__.py +48 -0
  632. omnibase_infra/types/type_cache_info.py +49 -0
  633. omnibase_infra/types/type_dsn.py +173 -0
  634. omnibase_infra/types/type_infra_aliases.py +60 -0
  635. omnibase_infra/types/typed_dict/__init__.py +21 -0
  636. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  637. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  638. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  639. omnibase_infra/utils/__init__.py +89 -0
  640. omnibase_infra/utils/correlation.py +208 -0
  641. omnibase_infra/utils/util_datetime.py +372 -0
  642. omnibase_infra/utils/util_dsn_validation.py +333 -0
  643. omnibase_infra/utils/util_env_parsing.py +264 -0
  644. omnibase_infra/utils/util_error_sanitization.py +457 -0
  645. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  646. omnibase_infra/utils/util_semver.py +233 -0
  647. omnibase_infra/validation/__init__.py +307 -0
  648. omnibase_infra/validation/enums/__init__.py +11 -0
  649. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  650. omnibase_infra/validation/infra_validators.py +1486 -0
  651. omnibase_infra/validation/linter_contract.py +907 -0
  652. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  653. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  654. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  655. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  656. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  657. omnibase_infra/validation/models/__init__.py +15 -0
  658. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  659. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  660. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  661. omnibase_infra/validation/validation_exemptions.yaml +1710 -0
  662. omnibase_infra/validation/validator_any_type.py +715 -0
  663. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  664. omnibase_infra/validation/validator_execution_shape.py +465 -0
  665. omnibase_infra/validation/validator_localhandler.py +261 -0
  666. omnibase_infra/validation/validator_registration_security.py +410 -0
  667. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  668. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  669. omnibase_infra/validation/validator_security.py +410 -0
  670. omnibase_infra/validation/validator_topic_category.py +1152 -0
  671. omnibase_infra-0.2.1.dist-info/METADATA +197 -0
  672. omnibase_infra-0.2.1.dist-info/RECORD +675 -0
  673. omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
  674. omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
  675. omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1279 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """PostgreSQL Snapshot Store for Production Persistence.
4
+
5
+ This module provides a PostgreSQL implementation of ProtocolSnapshotStore
6
+ for production snapshot persistence. The store uses asyncpg for async
7
+ database operations and supports:
8
+
9
+ - Idempotent saves via content_hash deduplication
10
+ - Subject-based filtering and sequence ordering
11
+ - Atomic sequence number generation
12
+ - Parent reference tracking for lineage/fork scenarios
13
+
14
+ Table Schema:
15
+ The store expects a `snapshots` table with the following schema. Use
16
+ the `ensure_schema()` method to create it automatically.
17
+
18
+ .. code-block:: sql
19
+
20
+ CREATE TABLE IF NOT EXISTS snapshots (
21
+ id UUID PRIMARY KEY,
22
+ subject_type VARCHAR(255) NOT NULL,
23
+ subject_id UUID NOT NULL,
24
+ data JSONB NOT NULL,
25
+ sequence_number INTEGER NOT NULL,
26
+ version INTEGER DEFAULT 1,
27
+ content_hash VARCHAR(128),
28
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
29
+ parent_id UUID REFERENCES snapshots(id),
30
+
31
+ CONSTRAINT snapshots_subject_sequence_unique
32
+ UNIQUE (subject_type, subject_id, sequence_number)
33
+ );
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_snapshots_subject
36
+ ON snapshots (subject_type, subject_id, sequence_number DESC);
37
+
38
+ -- UNIQUE partial index enables atomic ON CONFLICT upserts
39
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshots_content_hash
40
+ ON snapshots (content_hash) WHERE content_hash IS NOT NULL;
41
+
42
+ Connection Pooling:
43
+ The store requires an asyncpg connection pool to be injected at
44
+ construction time. This allows the pool to be shared across multiple
45
+ stores and services, with lifecycle managed by the application.
46
+
47
+ .. code-block:: python
48
+
49
+ import asyncpg
50
+ from omnibase_infra.services.snapshot import StoreSnapshotPostgres
51
+
52
+ # Create pool (managed by application)
53
+ pool = await asyncpg.create_pool(dsn="postgresql://...")
54
+
55
+ # Inject pool into store
56
+ store = StoreSnapshotPostgres(pool=pool)
57
+ await store.ensure_schema()
58
+
59
+ # Use store
60
+ snapshot_id = await store.save(snapshot)
61
+
62
+ Error Handling:
63
+ All operations wrap database exceptions in ONEX error types:
64
+ - InfraConnectionError: Connection failures, pool exhaustion
65
+ - InfraTimeoutError: Query timeouts (from asyncpg.QueryCanceledError)
66
+
67
+ Security:
68
+ - All queries use parameterized statements (no SQL injection)
69
+ - DSN/credentials are never logged or exposed in errors
70
+ - Connection pool credentials managed externally
71
+
72
+ Related Tickets:
73
+ - OMN-1246: ServiceSnapshot Infrastructure Primitive
74
+ """
75
+
76
+ from __future__ import annotations
77
+
78
+ import json
79
+ import logging
80
+ from datetime import UTC, datetime, timedelta
81
+ from uuid import UUID
82
+
83
+ import asyncpg
84
+ import asyncpg.exceptions
85
+
86
+ from omnibase_infra.enums import EnumInfraTransportType
87
+ from omnibase_infra.errors import (
88
+ InfraConnectionError,
89
+ ModelInfraErrorContext,
90
+ ProtocolConfigurationError,
91
+ )
92
+ from omnibase_infra.models.snapshot import ModelSnapshot, ModelSubjectRef
93
+
94
+ logger = logging.getLogger(__name__)
95
+
96
+
97
+ class StoreSnapshotPostgres:
98
+ """PostgreSQL implementation of ProtocolSnapshotStore.
99
+
100
+ Provides production-grade snapshot persistence using asyncpg with:
101
+ - Content-hash based idempotency for duplicate detection
102
+ - Atomic sequence number generation using database MAX() + 1
103
+ - JSONB storage for snapshot data payloads
104
+ - Composite indexes for efficient subject-based queries
105
+
106
+ Connection Management:
107
+ The pool is injected at construction time and NOT managed by
108
+ this class. The application is responsible for pool lifecycle
109
+ (creation, health checks, shutdown).
110
+
111
+ Concurrency:
112
+ Database-level constraints ensure sequence uniqueness. For
113
+ high-concurrency scenarios, consider using database sequences
114
+ or advisory locks.
115
+
116
+ Example:
117
+ >>> import asyncpg
118
+ >>> from omnibase_infra.services.snapshot import StoreSnapshotPostgres
119
+ >>> from omnibase_infra.models.snapshot import ModelSnapshot, ModelSubjectRef
120
+ >>>
121
+ >>> # Create pool and store
122
+ >>> pool = await asyncpg.create_pool(dsn="postgresql://...")
123
+ >>> store = StoreSnapshotPostgres(pool=pool)
124
+ >>> await store.ensure_schema()
125
+ >>>
126
+ >>> # Save a snapshot
127
+ >>> subject = ModelSubjectRef(subject_type="agent", subject_id=uuid4())
128
+ >>> snapshot = ModelSnapshot(
129
+ ... subject=subject,
130
+ ... data={"status": "active"},
131
+ ... sequence_number=1,
132
+ ... )
133
+ >>> saved_id = await store.save(snapshot)
134
+ """
135
+
136
+ def __init__(self, pool: asyncpg.Pool) -> None:
137
+ """Initialize the PostgreSQL snapshot store.
138
+
139
+ Args:
140
+ pool: asyncpg connection pool. The pool must be created and
141
+ configured by the caller. The store does not manage pool
142
+ lifecycle (creation, shutdown).
143
+
144
+ Note:
145
+ Call ensure_schema() after construction to create the
146
+ required table and indexes if they don't exist.
147
+ """
148
+ self._pool = pool
149
+
150
+ async def save(self, snapshot: ModelSnapshot) -> UUID:
151
+ """Persist a snapshot with content-hash based idempotency.
152
+
153
+ If a snapshot with the same content_hash already exists,
154
+ returns the existing snapshot's ID instead of creating a
155
+ duplicate. This enables safe retries without data duplication.
156
+
157
+ Race Condition Handling:
158
+ This method uses INSERT ON CONFLICT with a unique partial index
159
+ on content_hash to achieve atomic idempotency. The database-level
160
+ unique constraint eliminates TOCTOU race conditions that would
161
+ occur with separate SELECT-then-INSERT patterns.
162
+
163
+ Conflict scenarios:
164
+ - Same content_hash (any sequence): Returns existing ID via ON CONFLICT
165
+ - Same sequence, different content_hash: Raises UniqueViolationError
166
+ - No conflicts: Normal insert
167
+
168
+ Args:
169
+ snapshot: The snapshot to persist.
170
+
171
+ Returns:
172
+ UUID of the saved or existing snapshot.
173
+
174
+ Raises:
175
+ InfraConnectionError: If database connection fails or
176
+ query execution fails.
177
+
178
+ Note:
179
+ Requires ensure_schema() to have created the unique partial index
180
+ on content_hash. See ensure_schema() for details.
181
+ """
182
+ # Serialize data to JSON for JSONB storage (done outside try for clarity)
183
+ data_json = json.dumps(snapshot.data, sort_keys=True)
184
+
185
+ try:
186
+ async with self._pool.acquire() as conn:
187
+ if snapshot.content_hash:
188
+ # Atomic upsert using ON CONFLICT on the unique content_hash index.
189
+ # This eliminates the TOCTOU race condition by letting the database
190
+ # handle the check-and-insert atomically:
191
+ # - If content_hash exists: DO UPDATE (no-op) returns existing row
192
+ # - If content_hash is new: INSERT returns new row
193
+ # - If sequence conflicts: Raises UniqueViolationError (handled below)
194
+ #
195
+ # The DO UPDATE SET id = snapshots.id is a no-op that enables
196
+ # RETURNING to return the existing row's id.
197
+ result = await conn.fetchval(
198
+ """
199
+ INSERT INTO snapshots (
200
+ id, subject_type, subject_id, data, sequence_number,
201
+ version, content_hash, created_at, parent_id
202
+ ) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
203
+ ON CONFLICT (content_hash) WHERE content_hash IS NOT NULL
204
+ DO UPDATE SET id = snapshots.id
205
+ RETURNING id
206
+ """,
207
+ snapshot.id,
208
+ snapshot.subject.subject_type,
209
+ snapshot.subject.subject_id,
210
+ data_json,
211
+ snapshot.sequence_number,
212
+ snapshot.version,
213
+ snapshot.content_hash,
214
+ snapshot.created_at,
215
+ snapshot.parent_id,
216
+ )
217
+
218
+ if result:
219
+ result_id = UUID(str(result))
220
+ if result_id != snapshot.id:
221
+ logger.debug(
222
+ "Duplicate snapshot detected via content_hash, "
223
+ "returning existing ID",
224
+ extra={
225
+ "existing_id": str(result_id),
226
+ "content_hash": snapshot.content_hash[:16] + "...",
227
+ },
228
+ )
229
+ else:
230
+ logger.debug(
231
+ "Snapshot saved",
232
+ extra={
233
+ "snapshot_id": str(snapshot.id),
234
+ "subject_type": snapshot.subject.subject_type,
235
+ "sequence_number": snapshot.sequence_number,
236
+ },
237
+ )
238
+ return result_id
239
+
240
+ # Result should never be None with DO UPDATE, but handle defensively
241
+ context = ModelInfraErrorContext(
242
+ transport_type=EnumInfraTransportType.DATABASE,
243
+ operation="save_snapshot",
244
+ target_name="snapshots",
245
+ )
246
+ raise InfraConnectionError(
247
+ "Unexpected NULL result from upsert",
248
+ context=context,
249
+ )
250
+
251
+ # No content_hash - insert directly with conflict handling
252
+ result = await conn.fetchval(
253
+ """
254
+ INSERT INTO snapshots (
255
+ id, subject_type, subject_id, data, sequence_number,
256
+ version, content_hash, created_at, parent_id
257
+ ) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
258
+ ON CONFLICT (subject_type, subject_id, sequence_number)
259
+ DO NOTHING
260
+ RETURNING id
261
+ """,
262
+ snapshot.id,
263
+ snapshot.subject.subject_type,
264
+ snapshot.subject.subject_id,
265
+ data_json,
266
+ snapshot.sequence_number,
267
+ snapshot.version,
268
+ snapshot.content_hash,
269
+ snapshot.created_at,
270
+ snapshot.parent_id,
271
+ )
272
+
273
+ if result:
274
+ logger.debug(
275
+ "Snapshot saved",
276
+ extra={
277
+ "snapshot_id": str(snapshot.id),
278
+ "subject_type": snapshot.subject.subject_type,
279
+ "sequence_number": snapshot.sequence_number,
280
+ },
281
+ )
282
+ return UUID(str(result))
283
+
284
+ # Sequence conflict - return error
285
+ context = ModelInfraErrorContext(
286
+ transport_type=EnumInfraTransportType.DATABASE,
287
+ operation="save_snapshot",
288
+ target_name="snapshots",
289
+ )
290
+ raise InfraConnectionError(
291
+ f"Sequence conflict: sequence_number {snapshot.sequence_number} "
292
+ f"already exists for subject "
293
+ f"({snapshot.subject.subject_type}, {snapshot.subject.subject_id})",
294
+ context=context,
295
+ )
296
+
297
+ except asyncpg.exceptions.UniqueViolationError as e:
298
+ # UniqueViolationError occurs when:
299
+ # 1. Sequence constraint violated (same subject + sequence, different content)
300
+ # 2. Rare race on content_hash unique index (concurrent identical inserts)
301
+ #
302
+ # For case 2, check if content_hash exists and return it for idempotency.
303
+ if snapshot.content_hash:
304
+ try:
305
+ async with self._pool.acquire() as conn:
306
+ existing = await conn.fetchval(
307
+ "SELECT id FROM snapshots WHERE content_hash = $1",
308
+ snapshot.content_hash,
309
+ )
310
+ if existing:
311
+ existing_id = UUID(str(existing))
312
+ logger.debug(
313
+ "Race condition resolved: returning existing ID "
314
+ "after UniqueViolationError",
315
+ extra={
316
+ "existing_id": str(existing_id),
317
+ "content_hash": snapshot.content_hash[:16] + "...",
318
+ },
319
+ )
320
+ return existing_id
321
+ except Exception:
322
+ pass # Fall through to re-raise original error
323
+
324
+ # Sequence conflict with different content - this is a real conflict
325
+ context = ModelInfraErrorContext(
326
+ transport_type=EnumInfraTransportType.DATABASE,
327
+ operation="save_snapshot",
328
+ target_name="snapshots",
329
+ )
330
+ raise InfraConnectionError(
331
+ f"Unique constraint violation during save for subject "
332
+ f"({snapshot.subject.subject_type}, {snapshot.subject.subject_id}): "
333
+ f"{e.constraint_name or 'unknown constraint'}",
334
+ context=context,
335
+ ) from e
336
+
337
+ except InfraConnectionError:
338
+ # Re-raise our own errors without wrapping
339
+ raise
340
+
341
+ except Exception as e:
342
+ context = ModelInfraErrorContext(
343
+ transport_type=EnumInfraTransportType.DATABASE,
344
+ operation="save_snapshot",
345
+ target_name="snapshots",
346
+ )
347
+ raise InfraConnectionError(
348
+ f"Failed to save snapshot: {type(e).__name__}",
349
+ context=context,
350
+ ) from e
351
+
352
+ async def load(self, snapshot_id: UUID) -> ModelSnapshot | None:
353
+ """Load a snapshot by ID.
354
+
355
+ Args:
356
+ snapshot_id: The unique identifier of the snapshot.
357
+
358
+ Returns:
359
+ The snapshot if found, None otherwise.
360
+
361
+ Raises:
362
+ InfraConnectionError: If database connection fails.
363
+ """
364
+ try:
365
+ async with self._pool.acquire() as conn:
366
+ row = await conn.fetchrow(
367
+ "SELECT * FROM snapshots WHERE id = $1",
368
+ snapshot_id,
369
+ )
370
+ if row is None:
371
+ return None
372
+ return self._row_to_model(row)
373
+ except Exception as e:
374
+ context = ModelInfraErrorContext(
375
+ transport_type=EnumInfraTransportType.DATABASE,
376
+ operation="load_snapshot",
377
+ target_name="snapshots",
378
+ )
379
+ raise InfraConnectionError(
380
+ f"Failed to load snapshot: {type(e).__name__}",
381
+ context=context,
382
+ ) from e
383
+
384
+ async def load_many(self, snapshot_ids: list[UUID]) -> dict[UUID, ModelSnapshot]:
385
+ """Load multiple snapshots by ID in a single query.
386
+
387
+ Uses a batch query with ANY() for efficient multi-row fetch,
388
+ avoiding N+1 query patterns when loading multiple snapshots.
389
+
390
+ Args:
391
+ snapshot_ids: List of snapshot UUIDs to load.
392
+
393
+ Returns:
394
+ Dictionary mapping snapshot ID to ModelSnapshot for found
395
+ snapshots. Missing IDs are not included in the result.
396
+
397
+ Raises:
398
+ InfraConnectionError: If database connection fails.
399
+
400
+ Example:
401
+ >>> snapshots = await store.load_many([id1, id2, id3])
402
+ >>> for sid, snap in snapshots.items():
403
+ ... print(f"{sid}: seq={snap.sequence_number}")
404
+ """
405
+ if not snapshot_ids:
406
+ return {}
407
+
408
+ try:
409
+ async with self._pool.acquire() as conn:
410
+ rows = await conn.fetch(
411
+ "SELECT * FROM snapshots WHERE id = ANY($1::uuid[])",
412
+ snapshot_ids,
413
+ )
414
+ return {row["id"]: self._row_to_model(row) for row in rows}
415
+ except Exception as e:
416
+ context = ModelInfraErrorContext(
417
+ transport_type=EnumInfraTransportType.DATABASE,
418
+ operation="load_many_snapshots",
419
+ target_name="snapshots",
420
+ )
421
+ raise InfraConnectionError(
422
+ f"Failed to load snapshots: {type(e).__name__}",
423
+ context=context,
424
+ ) from e
425
+
426
+ async def load_latest(
427
+ self,
428
+ subject: ModelSubjectRef | None = None,
429
+ ) -> ModelSnapshot | None:
430
+ """Load the most recent snapshot by sequence_number.
431
+
432
+ Retrieves the snapshot with the highest sequence_number,
433
+ optionally filtered by subject. "Most recent" is determined
434
+ by sequence_number (not created_at) for consistent ordering.
435
+
436
+ Args:
437
+ subject: Optional filter by subject reference.
438
+
439
+ - If provided: Returns the latest snapshot for that specific
440
+ subject (highest sequence_number within that subject).
441
+ - If None: Returns the globally latest snapshot across ALL
442
+ subjects (highest sequence_number in the entire store).
443
+
444
+ Returns:
445
+ The most recent snapshot matching criteria, or None if no
446
+ snapshots exist.
447
+
448
+ Raises:
449
+ InfraConnectionError: If database connection fails.
450
+
451
+ Note:
452
+ When ``subject=None``, "globally latest" means the snapshot with
453
+ the highest sequence_number across all subjects. Since sequence
454
+ numbers are per-subject (each subject starts at 1), this may NOT
455
+ correspond to the most recently created snapshot by wall-clock
456
+ time. Use ``query(after=timestamp)`` if you need time-based
457
+ ordering across subjects.
458
+
459
+ Examples:
460
+ >>> # Get latest for a specific subject
461
+ >>> subject = ModelSubjectRef(
462
+ ... subject_type="node_registration",
463
+ ... subject_id=node_uuid,
464
+ ... )
465
+ >>> latest = await store.load_latest(subject=subject)
466
+ >>> # Returns snapshot with highest sequence_number for this subject
467
+
468
+ >>> # Get globally latest across ALL subjects
469
+ >>> global_latest = await store.load_latest(subject=None)
470
+ >>> # Returns snapshot with highest sequence_number in entire store
471
+ >>> # Note: This is NOT necessarily the most recent by created_at
472
+ """
473
+ try:
474
+ async with self._pool.acquire() as conn:
475
+ if subject:
476
+ row = await conn.fetchrow(
477
+ """
478
+ SELECT * FROM snapshots
479
+ WHERE subject_type = $1 AND subject_id = $2
480
+ ORDER BY sequence_number DESC LIMIT 1
481
+ """,
482
+ subject.subject_type,
483
+ subject.subject_id,
484
+ )
485
+ else:
486
+ row = await conn.fetchrow(
487
+ "SELECT * FROM snapshots ORDER BY sequence_number DESC LIMIT 1"
488
+ )
489
+ if row is None:
490
+ return None
491
+ return self._row_to_model(row)
492
+ except Exception as e:
493
+ context = ModelInfraErrorContext(
494
+ transport_type=EnumInfraTransportType.DATABASE,
495
+ operation="load_latest_snapshot",
496
+ target_name="snapshots",
497
+ )
498
+ raise InfraConnectionError(
499
+ f"Failed to load latest snapshot: {type(e).__name__}",
500
+ context=context,
501
+ ) from e
502
+
503
+ async def load_latest_many(
504
+ self,
505
+ subjects: list[ModelSubjectRef],
506
+ ) -> dict[tuple[str, UUID], ModelSnapshot]:
507
+ """Load the latest snapshot for multiple subjects in a single query.
508
+
509
+ Uses a window function to efficiently fetch the latest snapshot per
510
+ subject in one database round-trip, avoiding N+1 query patterns.
511
+
512
+ Args:
513
+ subjects: List of subject references to load latest snapshots for.
514
+
515
+ Returns:
516
+ Dictionary mapping (subject_type, subject_id) tuple to the latest
517
+ ModelSnapshot for that subject. Subjects with no snapshots are
518
+ not included in the result.
519
+
520
+ Raises:
521
+ InfraConnectionError: If database connection fails.
522
+
523
+ Example:
524
+ >>> subjects = [
525
+ ... ModelSubjectRef(subject_type="agent", subject_id=agent_id),
526
+ ... ModelSubjectRef(subject_type="workflow", subject_id=wf_id),
527
+ ... ]
528
+ >>> latest = await store.load_latest_many(subjects)
529
+ >>> for (stype, sid), snap in latest.items():
530
+ ... print(f"{stype}/{sid}: seq={snap.sequence_number}")
531
+ """
532
+ if not subjects:
533
+ return {}
534
+
535
+ try:
536
+ async with self._pool.acquire() as conn:
537
+ # Build arrays for subject_type and subject_id to match
538
+ subject_types = [s.subject_type for s in subjects]
539
+ subject_ids = [s.subject_id for s in subjects]
540
+
541
+ # Use window function to get latest per subject in one query
542
+ # The query uses a CTE to rank snapshots per subject, then
543
+ # filters to only keep the top-ranked (latest) per subject.
544
+ rows = await conn.fetch(
545
+ """
546
+ WITH ranked AS (
547
+ SELECT *,
548
+ ROW_NUMBER() OVER (
549
+ PARTITION BY subject_type, subject_id
550
+ ORDER BY sequence_number DESC
551
+ ) as rn
552
+ FROM snapshots
553
+ WHERE (subject_type, subject_id) IN (
554
+ SELECT * FROM UNNEST($1::text[], $2::uuid[])
555
+ )
556
+ )
557
+ SELECT * FROM ranked WHERE rn = 1
558
+ """,
559
+ subject_types,
560
+ subject_ids,
561
+ )
562
+
563
+ return {
564
+ (row["subject_type"], row["subject_id"]): self._row_to_model(row)
565
+ for row in rows
566
+ }
567
+ except Exception as e:
568
+ context = ModelInfraErrorContext(
569
+ transport_type=EnumInfraTransportType.DATABASE,
570
+ operation="load_latest_many_snapshots",
571
+ target_name="snapshots",
572
+ )
573
+ raise InfraConnectionError(
574
+ f"Failed to load latest snapshots: {type(e).__name__}",
575
+ context=context,
576
+ ) from e
577
+
578
+ async def query(
579
+ self,
580
+ subject: ModelSubjectRef | None = None,
581
+ limit: int = 50,
582
+ after: datetime | None = None,
583
+ ) -> list[ModelSnapshot]:
584
+ """Query snapshots with optional filtering.
585
+
586
+ Returns snapshots ordered by sequence_number descending
587
+ (most recent first).
588
+
589
+ Args:
590
+ subject: Optional filter by subject reference.
591
+ limit: Maximum results to return (default 50).
592
+ after: Only return snapshots created after this time.
593
+
594
+ Returns:
595
+ List of snapshots ordered by sequence_number descending.
596
+
597
+ Raises:
598
+ InfraConnectionError: If database connection fails.
599
+ """
600
+ try:
601
+ async with self._pool.acquire() as conn:
602
+ # Build dynamic query with parameterized conditions
603
+ conditions: list[str] = []
604
+ params: list[object] = []
605
+
606
+ if subject:
607
+ conditions.append(f"subject_type = ${len(params) + 1}")
608
+ params.append(subject.subject_type)
609
+ conditions.append(f"subject_id = ${len(params) + 1}")
610
+ params.append(subject.subject_id)
611
+
612
+ if after:
613
+ conditions.append(f"created_at > ${len(params) + 1}")
614
+ params.append(after)
615
+
616
+ where_clause = " AND ".join(conditions) if conditions else "TRUE"
617
+ params.append(limit)
618
+
619
+ # S608: This is NOT SQL injection - where_clause contains only
620
+ # safe static column names with parameterized value placeholders
621
+ # ($1, $2, etc). All user-supplied values go through params.
622
+ query = f"""
623
+ SELECT * FROM snapshots
624
+ WHERE {where_clause}
625
+ ORDER BY sequence_number DESC
626
+ LIMIT ${len(params)}
627
+ """ # noqa: S608
628
+
629
+ rows = await conn.fetch(query, *params)
630
+ return [self._row_to_model(row) for row in rows]
631
+ except Exception as e:
632
+ context = ModelInfraErrorContext(
633
+ transport_type=EnumInfraTransportType.DATABASE,
634
+ operation="query_snapshots",
635
+ target_name="snapshots",
636
+ )
637
+ raise InfraConnectionError(
638
+ f"Failed to query snapshots: {type(e).__name__}",
639
+ context=context,
640
+ ) from e
641
+
642
+ async def delete(self, snapshot_id: UUID) -> bool:
643
+ """Delete a snapshot by ID.
644
+
645
+ Args:
646
+ snapshot_id: The unique identifier of the snapshot to delete.
647
+
648
+ Returns:
649
+ True if the snapshot was deleted, False if not found.
650
+
651
+ Raises:
652
+ InfraConnectionError: If database connection fails.
653
+ """
654
+ try:
655
+ async with self._pool.acquire() as conn:
656
+ result = await conn.execute(
657
+ "DELETE FROM snapshots WHERE id = $1",
658
+ snapshot_id,
659
+ )
660
+ # asyncpg returns "DELETE N" where N is rows affected
661
+ deleted: bool = str(result) == "DELETE 1"
662
+ if deleted:
663
+ logger.debug(
664
+ "Snapshot deleted",
665
+ extra={"snapshot_id": str(snapshot_id)},
666
+ )
667
+ return deleted
668
+ except Exception as e:
669
+ context = ModelInfraErrorContext(
670
+ transport_type=EnumInfraTransportType.DATABASE,
671
+ operation="delete_snapshot",
672
+ target_name="snapshots",
673
+ )
674
+ raise InfraConnectionError(
675
+ f"Failed to delete snapshot: {type(e).__name__}",
676
+ context=context,
677
+ ) from e
678
+
679
+ async def get_next_sequence_number(self, subject: ModelSubjectRef) -> int:
680
+ """Get the next sequence number for a subject with advisory lock.
681
+
682
+ Uses PostgreSQL advisory locks to ensure atomic sequence allocation.
683
+ The lock is held only during the MAX() query, allowing concurrent
684
+ access to different subjects while preventing race conditions for
685
+ the same subject.
686
+
687
+ Advisory Lock Strategy:
688
+ Uses pg_advisory_xact_lock() with a hash of (subject_type, subject_id)
689
+ to create a subject-specific lock. This ensures:
690
+ - Concurrent calls for the SAME subject serialize
691
+ - Concurrent calls for DIFFERENT subjects proceed in parallel
692
+ - Lock is automatically released at transaction end
693
+
694
+ Args:
695
+ subject: The subject reference for sequence generation.
696
+
697
+ Returns:
698
+ The next sequence number (starts at 1 for new subjects).
699
+
700
+ Raises:
701
+ InfraConnectionError: If database connection fails.
702
+
703
+ Note:
704
+ For atomic allocate-and-save operations, prefer save_with_auto_sequence()
705
+ which combines sequence allocation and insert in a single transaction.
706
+
707
+ Concurrency Guarantees:
708
+ - No duplicate sequence numbers for the same subject
709
+ - No gaps in sequence numbers (unless deletes occur)
710
+ - Monotonically increasing per subject
711
+ """
712
+ try:
713
+ async with self._pool.acquire() as conn:
714
+ # Use a transaction to hold the advisory lock during the query.
715
+ # The lock key is derived from a hash of subject identifiers.
716
+ # pg_advisory_xact_lock takes a bigint, so we use hashtext()
717
+ # on the concatenated subject identifiers.
718
+ async with conn.transaction():
719
+ # Acquire advisory lock for this specific subject.
720
+ # hashtext() returns a stable 32-bit hash; we cast to bigint.
721
+ # This serializes concurrent calls for the same subject.
722
+ # Note: subject_id must be converted to str() for text concatenation
723
+ # in the hashtext() call - asyncpg requires string type for || operator.
724
+ await conn.execute(
725
+ """
726
+ SELECT pg_advisory_xact_lock(
727
+ hashtext($1 || '::' || $2)::bigint
728
+ )
729
+ """,
730
+ subject.subject_type,
731
+ str(subject.subject_id),
732
+ )
733
+
734
+ # Now safely get the next sequence number while holding the lock
735
+ result = await conn.fetchval(
736
+ """
737
+ SELECT COALESCE(MAX(sequence_number), 0) + 1
738
+ FROM snapshots
739
+ WHERE subject_type = $1 AND subject_id = $2
740
+ """,
741
+ subject.subject_type,
742
+ subject.subject_id,
743
+ )
744
+ # Lock released automatically when transaction ends
745
+ return int(result) if result else 1
746
+ except Exception as e:
747
+ context = ModelInfraErrorContext(
748
+ transport_type=EnumInfraTransportType.DATABASE,
749
+ operation="get_sequence_number",
750
+ target_name="snapshots",
751
+ )
752
+ raise InfraConnectionError(
753
+ f"Failed to get sequence number: {type(e).__name__}",
754
+ context=context,
755
+ ) from e
756
+
757
+ async def save_with_auto_sequence(
758
+ self,
759
+ subject: ModelSubjectRef,
760
+ data: dict[str, object],
761
+ *,
762
+ version: int = 1,
763
+ content_hash: str | None = None,
764
+ parent_id: UUID | None = None,
765
+ ) -> tuple[UUID, int]:
766
+ """Atomically allocate sequence number and save snapshot.
767
+
768
+ This method combines sequence allocation and insert into a single
769
+ atomic transaction, eliminating the TOCTOU race condition that
770
+ exists when calling get_next_sequence_number() followed by save()
771
+ separately.
772
+
773
+ Atomicity Guarantees:
774
+ 1. Advisory lock prevents concurrent sequence allocation for same subject
775
+ 2. Sequence allocation and INSERT occur in same transaction
776
+ 3. If INSERT fails, no sequence number is "consumed"
777
+ 4. Content-hash deduplication still applies (returns existing if duplicate)
778
+
779
+ Race Condition Handling:
780
+ - Same content_hash: Returns existing snapshot ID and sequence number
781
+ - Concurrent saves for same subject: Serialized via advisory lock
782
+ - Database constraint violations: Wrapped as InfraConnectionError
783
+
784
+ Args:
785
+ subject: The subject reference for the snapshot.
786
+ data: The snapshot payload as a JSON-compatible dictionary.
787
+ version: Version number for the snapshot (default 1).
788
+ content_hash: Optional content hash for deduplication. If provided
789
+ and a snapshot with this hash already exists, returns the
790
+ existing snapshot's ID and sequence number.
791
+ parent_id: Optional parent snapshot ID for lineage tracking.
792
+
793
+ Returns:
794
+ Tuple of (snapshot_id, sequence_number) for the saved or existing snapshot.
795
+
796
+ Raises:
797
+ InfraConnectionError: If database connection fails or
798
+ constraint violation occurs.
799
+
800
+ Example:
801
+ >>> subject = ModelSubjectRef(subject_type="agent", subject_id=uuid4())
802
+ >>> snapshot_id, seq_num = await store.save_with_auto_sequence(
803
+ ... subject=subject,
804
+ ... data={"status": "active"},
805
+ ... content_hash="sha256:abc123...",
806
+ ... )
807
+ >>> print(f"Saved snapshot {snapshot_id} with sequence {seq_num}")
808
+ """
809
+ from uuid import uuid4
810
+
811
+ snapshot_id = uuid4()
812
+ data_json = json.dumps(data, sort_keys=True)
813
+ created_at = datetime.now(UTC)
814
+
815
+ try:
816
+ async with self._pool.acquire() as conn:
817
+ async with conn.transaction():
818
+ # Step 1: Check for existing content_hash FIRST (before locking).
819
+ # This avoids unnecessary lock acquisition for duplicates and
820
+ # eliminates the race condition between check and insert by
821
+ # performing the check within the same transaction that will
822
+ # do the insert. The transaction isolation ensures consistency.
823
+ if content_hash:
824
+ existing = await conn.fetchrow(
825
+ """
826
+ SELECT id, sequence_number FROM snapshots
827
+ WHERE content_hash = $1
828
+ """,
829
+ content_hash,
830
+ )
831
+ if existing:
832
+ existing_id = UUID(str(existing["id"]))
833
+ existing_seq = int(existing["sequence_number"])
834
+ logger.debug(
835
+ "Duplicate snapshot detected via content_hash, "
836
+ "returning existing ID",
837
+ extra={
838
+ "existing_id": str(existing_id),
839
+ "content_hash": content_hash[:16] + "...",
840
+ },
841
+ )
842
+ return existing_id, existing_seq
843
+
844
+ # Step 2: Acquire advisory lock for this subject.
845
+ # This serializes concurrent saves for the same subject,
846
+ # preventing race conditions in sequence number allocation.
847
+ # The lock is held until the transaction commits/rollbacks.
848
+ # Note: subject_id must be converted to str() for text concatenation
849
+ # in the hashtext() call - asyncpg requires string type for || operator.
850
+ await conn.execute(
851
+ """
852
+ SELECT pg_advisory_xact_lock(
853
+ hashtext($1 || '::' || $2)::bigint
854
+ )
855
+ """,
856
+ subject.subject_type,
857
+ str(subject.subject_id),
858
+ )
859
+
860
+ # Step 3: Allocate next sequence number while holding lock.
861
+ # The advisory lock ensures no concurrent transaction can
862
+ # read the same MAX value for this subject.
863
+ sequence_number = await conn.fetchval(
864
+ """
865
+ SELECT COALESCE(MAX(sequence_number), 0) + 1
866
+ FROM snapshots
867
+ WHERE subject_type = $1 AND subject_id = $2
868
+ """,
869
+ subject.subject_type,
870
+ subject.subject_id,
871
+ )
872
+ sequence_number = int(sequence_number) if sequence_number else 1
873
+
874
+ # Step 4: Insert with ON CONFLICT for content_hash idempotency.
875
+ # Even though we checked above, a concurrent transaction might
876
+ # have committed between our check and lock acquisition (before
877
+ # the lock was acquired). The ON CONFLICT handles this edge case.
878
+ if content_hash:
879
+ result = await conn.fetchrow(
880
+ """
881
+ INSERT INTO snapshots (
882
+ id, subject_type, subject_id, data, sequence_number,
883
+ version, content_hash, created_at, parent_id
884
+ ) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
885
+ ON CONFLICT (content_hash) WHERE content_hash IS NOT NULL
886
+ DO UPDATE SET id = snapshots.id
887
+ RETURNING id, sequence_number
888
+ """,
889
+ snapshot_id,
890
+ subject.subject_type,
891
+ subject.subject_id,
892
+ data_json,
893
+ sequence_number,
894
+ version,
895
+ content_hash,
896
+ created_at,
897
+ parent_id,
898
+ )
899
+ else:
900
+ # No content_hash - insert directly
901
+ result = await conn.fetchrow(
902
+ """
903
+ INSERT INTO snapshots (
904
+ id, subject_type, subject_id, data, sequence_number,
905
+ version, content_hash, created_at, parent_id
906
+ ) VALUES ($1, $2, $3, $4::jsonb, $5, $6, $7, $8, $9)
907
+ RETURNING id, sequence_number
908
+ """,
909
+ snapshot_id,
910
+ subject.subject_type,
911
+ subject.subject_id,
912
+ data_json,
913
+ sequence_number,
914
+ version,
915
+ content_hash,
916
+ created_at,
917
+ parent_id,
918
+ )
919
+
920
+ if result:
921
+ result_id = UUID(str(result["id"]))
922
+ result_seq = int(result["sequence_number"])
923
+
924
+ if result_id != snapshot_id:
925
+ # Existing snapshot returned via ON CONFLICT
926
+ logger.debug(
927
+ "Duplicate snapshot detected during insert, "
928
+ "returning existing ID",
929
+ extra={
930
+ "existing_id": str(result_id),
931
+ "sequence_number": result_seq,
932
+ },
933
+ )
934
+ else:
935
+ logger.debug(
936
+ "Snapshot saved atomically",
937
+ extra={
938
+ "snapshot_id": str(snapshot_id),
939
+ "subject_type": subject.subject_type,
940
+ "sequence_number": result_seq,
941
+ },
942
+ )
943
+ return result_id, result_seq
944
+
945
+ # Should never reach here with valid INSERT
946
+ context = ModelInfraErrorContext(
947
+ transport_type=EnumInfraTransportType.DATABASE,
948
+ operation="save_with_auto_sequence",
949
+ target_name="snapshots",
950
+ )
951
+ raise InfraConnectionError(
952
+ "Unexpected NULL result from atomic insert",
953
+ context=context,
954
+ )
955
+
956
+ except asyncpg.exceptions.UniqueViolationError as e:
957
+ # This can occur if:
958
+ # 1. Very rare race on content_hash between check and insert
959
+ # 2. Sequence constraint violated (shouldn't happen with advisory lock)
960
+ #
961
+ # For content_hash conflicts, try to return the existing row.
962
+ # Use the same connection pool but a new connection to avoid
963
+ # transaction state issues.
964
+ if content_hash:
965
+ try:
966
+ async with self._pool.acquire() as recovery_conn:
967
+ existing = await recovery_conn.fetchrow(
968
+ "SELECT id, sequence_number FROM snapshots "
969
+ "WHERE content_hash = $1",
970
+ content_hash,
971
+ )
972
+ if existing:
973
+ existing_id = UUID(str(existing["id"]))
974
+ existing_seq = int(existing["sequence_number"])
975
+ logger.debug(
976
+ "Race condition resolved: returning existing ID "
977
+ "after UniqueViolationError",
978
+ extra={
979
+ "existing_id": str(existing_id),
980
+ "content_hash": content_hash[:16] + "...",
981
+ },
982
+ )
983
+ return existing_id, existing_seq
984
+ except Exception:
985
+ pass # Fall through to re-raise original error
986
+
987
+ context = ModelInfraErrorContext(
988
+ transport_type=EnumInfraTransportType.DATABASE,
989
+ operation="save_with_auto_sequence",
990
+ target_name="snapshots",
991
+ )
992
+ raise InfraConnectionError(
993
+ f"Unique constraint violation: {e.constraint_name or 'unknown'}",
994
+ context=context,
995
+ ) from e
996
+
997
+ except InfraConnectionError:
998
+ raise
999
+
1000
+ except Exception as e:
1001
+ context = ModelInfraErrorContext(
1002
+ transport_type=EnumInfraTransportType.DATABASE,
1003
+ operation="save_with_auto_sequence",
1004
+ target_name="snapshots",
1005
+ )
1006
+ raise InfraConnectionError(
1007
+ f"Failed to save snapshot atomically: {type(e).__name__}",
1008
+ context=context,
1009
+ ) from e
1010
+
1011
+ async def cleanup_expired(
1012
+ self,
1013
+ *,
1014
+ max_age_seconds: int | None = None,
1015
+ keep_latest_n: int | None = None,
1016
+ subject: ModelSubjectRef | None = None,
1017
+ ) -> int:
1018
+ """Remove expired snapshots based on retention policy.
1019
+
1020
+ Supports multiple retention strategies:
1021
+ - Time-based: Delete snapshots older than max_age_seconds
1022
+ - Count-based: Keep only the N most recent per subject
1023
+ - Subject-scoped: Apply policy only to a specific subject
1024
+
1025
+ When both max_age_seconds and keep_latest_n are provided, snapshots
1026
+ must satisfy BOTH conditions to be deleted (i.e., be older than
1027
+ max_age AND not in the latest N).
1028
+
1029
+ Args:
1030
+ max_age_seconds: Delete snapshots older than this many seconds.
1031
+ keep_latest_n: Always retain the N most recent per subject.
1032
+ subject: If provided, apply cleanup only to this subject.
1033
+
1034
+ Returns:
1035
+ Number of snapshots deleted.
1036
+
1037
+ Raises:
1038
+ ProtocolConfigurationError: If keep_latest_n is provided but < 1.
1039
+ InfraConnectionError: If database connection fails.
1040
+
1041
+ Note:
1042
+ For keep_latest_n, this uses a window function to identify
1043
+ snapshots outside the retention window per subject. This is
1044
+ efficient for moderate numbers of subjects but may require
1045
+ batching for very large datasets.
1046
+ """
1047
+ if keep_latest_n is not None and keep_latest_n < 1:
1048
+ raise ProtocolConfigurationError(
1049
+ "keep_latest_n must be >= 1",
1050
+ keep_latest_n=keep_latest_n,
1051
+ )
1052
+
1053
+ # If neither policy is specified, no-op
1054
+ if max_age_seconds is None and keep_latest_n is None:
1055
+ return 0
1056
+
1057
+ try:
1058
+ async with self._pool.acquire() as conn:
1059
+ # Build conditions for deletion
1060
+ conditions: list[str] = []
1061
+ params: list[object] = []
1062
+
1063
+ # Subject filter (applies to all strategies)
1064
+ if subject is not None:
1065
+ conditions.append(f"subject_type = ${len(params) + 1}")
1066
+ params.append(subject.subject_type)
1067
+ conditions.append(f"subject_id = ${len(params) + 1}")
1068
+ params.append(subject.subject_id)
1069
+
1070
+ subject_filter = " AND ".join(conditions) if conditions else "TRUE"
1071
+
1072
+ # Strategy 1: Age-based only (simpler query)
1073
+ if max_age_seconds is not None and keep_latest_n is None:
1074
+ cutoff_time = datetime.now(UTC) - timedelta(seconds=max_age_seconds)
1075
+ params.append(cutoff_time)
1076
+
1077
+ # S608: Safe - subject_filter contains only parameterized placeholders
1078
+ delete_query = f"""
1079
+ DELETE FROM snapshots
1080
+ WHERE {subject_filter}
1081
+ AND created_at < ${len(params)}
1082
+ """ # noqa: S608
1083
+
1084
+ result = await conn.execute(delete_query, *params)
1085
+ # asyncpg returns "DELETE N"
1086
+ deleted_str = str(result).replace("DELETE ", "")
1087
+ return int(deleted_str) if deleted_str.isdigit() else 0
1088
+
1089
+ # Strategy 2: Keep latest N only
1090
+ if keep_latest_n is not None and max_age_seconds is None:
1091
+ params.append(keep_latest_n)
1092
+
1093
+ # Use window function to rank snapshots per subject
1094
+ # S608: Safe - subject_filter contains only parameterized placeholders
1095
+ delete_query = f"""
1096
+ DELETE FROM snapshots
1097
+ WHERE id IN (
1098
+ SELECT id FROM (
1099
+ SELECT id,
1100
+ ROW_NUMBER() OVER (
1101
+ PARTITION BY subject_type, subject_id
1102
+ ORDER BY sequence_number DESC
1103
+ ) as rn
1104
+ FROM snapshots
1105
+ WHERE {subject_filter}
1106
+ ) ranked
1107
+ WHERE rn > ${len(params)}
1108
+ )
1109
+ """ # noqa: S608
1110
+
1111
+ result = await conn.execute(delete_query, *params)
1112
+ deleted_str = str(result).replace("DELETE ", "")
1113
+ return int(deleted_str) if deleted_str.isdigit() else 0
1114
+
1115
+ # Strategy 3: Combined (both age and count)
1116
+ # Delete if: older than max_age AND NOT in latest N
1117
+ # NOTE: max_age_seconds is validated non-None by strategy check above,
1118
+ # but mypy cannot narrow the Optional[float] type through control flow.
1119
+ cutoff_time = datetime.now(UTC) - timedelta(
1120
+ seconds=max_age_seconds # type: ignore[arg-type] # NOTE: control flow narrowing limitation
1121
+ )
1122
+ params.append(cutoff_time)
1123
+ cutoff_param_idx = len(params)
1124
+
1125
+ params.append(keep_latest_n)
1126
+ keep_n_param_idx = len(params)
1127
+
1128
+ # S608: Safe - subject_filter contains only parameterized placeholders
1129
+ delete_query = f"""
1130
+ DELETE FROM snapshots
1131
+ WHERE id IN (
1132
+ SELECT id FROM (
1133
+ SELECT id,
1134
+ created_at,
1135
+ ROW_NUMBER() OVER (
1136
+ PARTITION BY subject_type, subject_id
1137
+ ORDER BY sequence_number DESC
1138
+ ) as rn
1139
+ FROM snapshots
1140
+ WHERE {subject_filter}
1141
+ ) ranked
1142
+ WHERE rn > ${keep_n_param_idx}
1143
+ AND created_at < ${cutoff_param_idx}
1144
+ )
1145
+ """ # noqa: S608
1146
+
1147
+ result = await conn.execute(delete_query, *params)
1148
+ deleted_str = str(result).replace("DELETE ", "")
1149
+ return int(deleted_str) if deleted_str.isdigit() else 0
1150
+
1151
+ except ProtocolConfigurationError:
1152
+ # Re-raise configuration validation errors
1153
+ raise
1154
+ except Exception as e:
1155
+ context = ModelInfraErrorContext(
1156
+ transport_type=EnumInfraTransportType.DATABASE,
1157
+ operation="cleanup_expired",
1158
+ target_name="snapshots",
1159
+ )
1160
+ raise InfraConnectionError(
1161
+ f"Failed to cleanup expired snapshots: {type(e).__name__}",
1162
+ context=context,
1163
+ ) from e
1164
+
1165
+ def _row_to_model(self, row: asyncpg.Record) -> ModelSnapshot:
1166
+ """Convert a database row to a ModelSnapshot.
1167
+
1168
+ Args:
1169
+ row: asyncpg Record from a SELECT query.
1170
+
1171
+ Returns:
1172
+ ModelSnapshot instance populated from the row.
1173
+ """
1174
+ # asyncpg returns JSONB as dict automatically
1175
+ data = row["data"]
1176
+ if isinstance(data, str):
1177
+ data = json.loads(data)
1178
+
1179
+ return ModelSnapshot(
1180
+ id=row["id"],
1181
+ subject=ModelSubjectRef(
1182
+ subject_type=row["subject_type"],
1183
+ subject_id=row["subject_id"],
1184
+ ),
1185
+ data=data,
1186
+ sequence_number=row["sequence_number"],
1187
+ version=row["version"],
1188
+ content_hash=row["content_hash"],
1189
+ created_at=row["created_at"],
1190
+ parent_id=row["parent_id"],
1191
+ )
1192
+
1193
+ async def ensure_schema(self) -> None:
1194
+ """Create the snapshots table and indexes if they don't exist.
1195
+
1196
+ This method is idempotent and safe to call on every startup.
1197
+ Uses IF NOT EXISTS clauses to avoid errors on existing objects.
1198
+
1199
+ Schema Design:
1200
+ The content_hash column has a UNIQUE partial index to enable
1201
+ atomic idempotency checks via INSERT ON CONFLICT. This eliminates
1202
+ TOCTOU race conditions that would occur with separate SELECT-then-
1203
+ INSERT patterns.
1204
+
1205
+ Constraints:
1206
+ - Primary key on id (UUID)
1207
+ - Unique constraint on (subject_type, subject_id, sequence_number)
1208
+ - Unique partial index on content_hash WHERE content_hash IS NOT NULL
1209
+
1210
+ Raises:
1211
+ InfraConnectionError: If schema creation fails.
1212
+
1213
+ Note:
1214
+ This method uses multi-statement execution via transaction.
1215
+ Each statement is executed separately to work within asyncpg's
1216
+ single-statement limitation.
1217
+
1218
+ Migration Note:
1219
+ If upgrading from a schema with non-unique idx_snapshots_content_hash,
1220
+ the old index will be dropped and replaced with a unique index.
1221
+ Ensure no duplicate content_hash values exist before migration.
1222
+ """
1223
+ try:
1224
+ async with self._pool.acquire() as conn:
1225
+ # Create table
1226
+ await conn.execute("""
1227
+ CREATE TABLE IF NOT EXISTS snapshots (
1228
+ id UUID PRIMARY KEY,
1229
+ subject_type VARCHAR(255) NOT NULL,
1230
+ subject_id UUID NOT NULL,
1231
+ data JSONB NOT NULL,
1232
+ sequence_number INTEGER NOT NULL,
1233
+ version INTEGER DEFAULT 1,
1234
+ content_hash VARCHAR(128),
1235
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1236
+ parent_id UUID REFERENCES snapshots(id),
1237
+
1238
+ CONSTRAINT snapshots_subject_sequence_unique
1239
+ UNIQUE (subject_type, subject_id, sequence_number)
1240
+ )
1241
+ """)
1242
+
1243
+ # Create subject index
1244
+ await conn.execute("""
1245
+ CREATE INDEX IF NOT EXISTS idx_snapshots_subject
1246
+ ON snapshots (subject_type, subject_id, sequence_number DESC)
1247
+ """)
1248
+
1249
+ # Drop old non-unique content_hash index if it exists (for migration).
1250
+ # This is safe because CREATE UNIQUE INDEX IF NOT EXISTS will fail
1251
+ # if a non-unique index with the same name exists.
1252
+ await conn.execute("""
1253
+ DROP INDEX IF EXISTS idx_snapshots_content_hash
1254
+ """)
1255
+
1256
+ # Create UNIQUE partial index on content_hash.
1257
+ # This enables atomic ON CONFLICT upserts and prevents duplicate
1258
+ # content_hash entries, eliminating TOCTOU race conditions.
1259
+ await conn.execute("""
1260
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_snapshots_content_hash
1261
+ ON snapshots (content_hash) WHERE content_hash IS NOT NULL
1262
+ """)
1263
+
1264
+ logger.info(
1265
+ "Snapshot schema ensured (table and indexes created/verified)"
1266
+ )
1267
+ except Exception as e:
1268
+ context = ModelInfraErrorContext(
1269
+ transport_type=EnumInfraTransportType.DATABASE,
1270
+ operation="ensure_schema",
1271
+ target_name="snapshots",
1272
+ )
1273
+ raise InfraConnectionError(
1274
+ f"Failed to ensure schema: {type(e).__name__}",
1275
+ context=context,
1276
+ ) from e
1277
+
1278
+
1279
+ __all__: list[str] = ["StoreSnapshotPostgres"]