omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__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 (139) hide show
  1. omnibase_infra/constants_topic_patterns.py +26 -0
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  4. omnibase_infra/enums/enum_handler_source_mode.py +16 -2
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_binding_resolution.py +128 -0
  7. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
  8. omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
  9. omnibase_infra/event_bus/event_bus_kafka.py +105 -47
  10. omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
  11. omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
  12. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
  13. omnibase_infra/event_bus/testing/__init__.py +26 -0
  14. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  15. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  16. omnibase_infra/handlers/handler_consul.py +2 -0
  17. omnibase_infra/handlers/mixins/__init__.py +5 -0
  18. omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
  19. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  20. omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
  21. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  22. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  23. omnibase_infra/mixins/mixin_node_introspection.py +189 -19
  24. omnibase_infra/models/__init__.py +8 -0
  25. omnibase_infra/models/bindings/__init__.py +59 -0
  26. omnibase_infra/models/bindings/constants.py +144 -0
  27. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  28. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  29. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  30. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  31. omnibase_infra/models/discovery/model_introspection_config.py +25 -17
  32. omnibase_infra/models/dispatch/__init__.py +8 -0
  33. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  34. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  35. omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
  36. omnibase_infra/models/model_node_identity.py +126 -0
  37. omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
  38. omnibase_infra/models/registration/__init__.py +9 -0
  39. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  40. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  41. omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
  42. omnibase_infra/models/runtime/__init__.py +9 -0
  43. omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
  44. omnibase_infra/nodes/__init__.py +9 -0
  45. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  46. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  47. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  48. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  49. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  50. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  51. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  52. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  53. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  54. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  55. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  56. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  57. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  58. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  59. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  60. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  61. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  62. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  63. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  64. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  65. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  66. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  67. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  68. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  69. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  70. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  71. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  72. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  73. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  74. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  75. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  76. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  77. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  78. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  79. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  80. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  81. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  82. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  83. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  84. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  85. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  86. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
  87. omnibase_infra/nodes/reducers/models/__init__.py +7 -2
  88. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
  89. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  90. omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
  91. omnibase_infra/protocols/__init__.py +3 -0
  92. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  93. omnibase_infra/runtime/__init__.py +60 -0
  94. omnibase_infra/runtime/binding_resolver.py +753 -0
  95. omnibase_infra/runtime/constants_security.py +70 -0
  96. omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
  97. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  98. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  99. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  100. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  101. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  102. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  103. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  104. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  105. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  106. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  107. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  108. omnibase_infra/runtime/handler_source_resolver.py +43 -2
  109. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  110. omnibase_infra/runtime/models/__init__.py +13 -0
  111. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  112. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  113. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
  114. omnibase_infra/runtime/models/model_security_config.py +109 -0
  115. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  116. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  117. omnibase_infra/runtime/service_kernel.py +76 -6
  118. omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
  119. omnibase_infra/runtime/service_runtime_host_process.py +770 -20
  120. omnibase_infra/runtime/transition_notification_publisher.py +3 -2
  121. omnibase_infra/runtime/util_wiring.py +206 -62
  122. omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
  123. omnibase_infra/services/session/config_consumer.py +25 -8
  124. omnibase_infra/services/session/config_store.py +2 -2
  125. omnibase_infra/services/session/consumer.py +1 -1
  126. omnibase_infra/topics/__init__.py +45 -0
  127. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  128. omnibase_infra/topics/util_topic_composition.py +95 -0
  129. omnibase_infra/types/typed_dict/__init__.py +9 -1
  130. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  131. omnibase_infra/utils/__init__.py +9 -0
  132. omnibase_infra/utils/util_consumer_group.py +232 -0
  133. omnibase_infra/validation/infra_validators.py +18 -1
  134. omnibase_infra/validation/validation_exemptions.yaml +192 -0
  135. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
  136. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
  137. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
  138. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
  139. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
@@ -13,12 +13,15 @@ Available Submodules:
13
13
  - reducers: Reducer nodes for state aggregation
14
14
  - node_registration_reducer: Declarative FSM-driven registration reducer
15
15
  - node_registration_orchestrator: Registration workflow orchestrator
16
+ - node_ledger_projection_compute: Event ledger projection compute node
16
17
 
17
18
  Available Classes:
18
19
  - NodeRegistrationReducer: Declarative FSM-driven reducer (ONEX pattern)
19
20
  - RegistrationReducer: Pure function reducer implementation
20
21
  - NodeRegistryEffect: Effect node for dual-backend registration execution
21
22
  - NodeRegistrationOrchestrator: Workflow orchestrator for registration
23
+ - NodeLedgerProjectionCompute: Event ledger projection compute node
24
+ - RegistryInfraLedgerProjection: Registry for ledger projection node
22
25
  """
23
26
 
24
27
  from omnibase_infra.nodes.effects import (
@@ -27,6 +30,10 @@ from omnibase_infra.nodes.effects import (
27
30
  ModelRegistryResponse,
28
31
  NodeRegistryEffect,
29
32
  )
33
+ from omnibase_infra.nodes.node_ledger_projection_compute import (
34
+ NodeLedgerProjectionCompute,
35
+ RegistryInfraLedgerProjection,
36
+ )
30
37
  from omnibase_infra.nodes.node_registration_orchestrator import (
31
38
  NodeRegistrationOrchestrator,
32
39
  )
@@ -40,9 +47,11 @@ __all__: list[str] = [
40
47
  "ModelBackendResult",
41
48
  "ModelRegistryRequest",
42
49
  "ModelRegistryResponse",
50
+ "NodeLedgerProjectionCompute",
43
51
  "NodeRegistrationOrchestrator",
44
52
  "NodeRegistrationReducer",
45
53
  "NodeRegistryEffect",
46
54
  "RegistrationReducer",
55
+ "RegistryInfraLedgerProjection",
47
56
  "RegistryInfraNodeRegistrationReducer",
48
57
  ]
@@ -0,0 +1,29 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Contract Registry Reducer Node.
4
+
5
+ This module provides the declarative reducer node for projecting contract
6
+ registration events to PostgreSQL. The reducer is FSM-driven and follows
7
+ the ONEX declarative pattern.
8
+
9
+ Exports:
10
+ NodeContractRegistryReducer: Declarative reducer node shell.
11
+ ContractRegistryReducer: Pure function reducer class.
12
+ ModelContractRegistryState: Immutable state model for the reducer.
13
+ """
14
+
15
+ from omnibase_infra.nodes.contract_registry_reducer.models import (
16
+ ModelContractRegistryState,
17
+ )
18
+ from omnibase_infra.nodes.contract_registry_reducer.node import (
19
+ NodeContractRegistryReducer,
20
+ )
21
+ from omnibase_infra.nodes.contract_registry_reducer.reducer import (
22
+ ContractRegistryReducer,
23
+ )
24
+
25
+ __all__ = [
26
+ "ContractRegistryReducer",
27
+ "ModelContractRegistryState",
28
+ "NodeContractRegistryReducer",
29
+ ]
@@ -0,0 +1,255 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ # ONEX Node Contract - Contract Registry Reducer Node
4
+ #
5
+ # This reducer follows the pure function pattern for contract registry workflows:
6
+ # reduce(state, event) -> ModelReducerOutput[state + intents]
7
+ #
8
+ # The FSM defines state transitions for the contract registry lifecycle:
9
+ # ready -> processing -> ready (simple cycle for event processing)
10
+ #
11
+ # All state transition logic is driven by this contract.
12
+ # =============================================================================
13
+ contract_version:
14
+ major: 1
15
+ minor: 0
16
+ patch: 0
17
+ node_version: "1.0.0"
18
+ name: "contract_registry_reducer"
19
+ node_type: "REDUCER_GENERIC"
20
+ # Enum Usage (CLAUDE.md reference):
21
+ # - node_type uses EnumNodeOutputType.REDUCER for validation
22
+ # - Reducer outputs are PROJECTION type (state + intents) via EnumNodeOutputType
23
+ # - Incoming messages are routed via EnumMessageCategory (EVENT/COMMAND)
24
+ # - PROJECTION exists only in EnumNodeOutputType and is only valid for REDUCER nodes
25
+ # - See: docs/decisions/adr-enum-message-category-vs-node-output-type.md
26
+ description: |
27
+ Reducer node that consumes contract registration events from Kafka
28
+ and materializes them to Postgres for discovery and observability.
29
+
30
+ This reducer tracks:
31
+ - Contract registrations from nodes on startup
32
+ - Contract deregistrations for graceful shutdown
33
+ - Node heartbeats for liveness tracking and last_seen_at updates
34
+ - Runtime ticks for staleness computation (marking stale contracts inactive)
35
+
36
+ The reducer emits intents for PostgreSQL writes via the Effect layer,
37
+ following the pure reducer pattern where state changes and side effects
38
+ are separated.
39
+ # Event Consumption Configuration
40
+ # ================================
41
+ # Topics follow 5-segment naming: {env}.onex.{category}.{domain}.{event-name}.v{version}
42
+ # Categories: evt (external events), int (internal events), cmd (commands)
43
+ consumed_events:
44
+ - topic: "{env}.onex.evt.platform.contract-registered.v1"
45
+ event_type: "ModelContractRegisteredEvent"
46
+ description: "Contract registration from nodes on startup"
47
+ - topic: "{env}.onex.evt.platform.contract-deregistered.v1"
48
+ event_type: "ModelContractDeregisteredEvent"
49
+ description: "Explicit contract deregistration on graceful shutdown"
50
+ - topic: "{env}.onex.evt.platform.node-heartbeat.v1"
51
+ event_type: "ModelNodeHeartbeatEvent"
52
+ description: "Heartbeat for liveness tracking and last_seen_at updates"
53
+ - topic: "{env}.onex.int.platform.runtime-tick.v1"
54
+ event_type: "ModelRuntimeTick"
55
+ internal: true
56
+ description: "Periodic tick for staleness computation - marks contracts as stale/inactive"
57
+ # Input/Output Model Configuration
58
+ # =================================
59
+ input_model:
60
+ name: "ModelReducerInput"
61
+ module: "omnibase_core.models.reducer.model_reducer_input"
62
+ description: "Reducer input containing contract event and trigger."
63
+ output_model:
64
+ name: "ModelReducerOutput"
65
+ module: "omnibase_core.models.reducer.model_reducer_output"
66
+ description: "Reducer output containing new state and PostgreSQL intents."
67
+ # FSM State Machine Definition
68
+ # ============================
69
+ # The contract registry reducer uses a simple FSM for event processing.
70
+ # Unlike the registration reducer which tracks a multi-step workflow,
71
+ # this reducer processes individual events and returns to ready state.
72
+ #
73
+ # State Diagram:
74
+ # +-------+ event received +------------+
75
+ # | ready | -----------------> | processing |
76
+ # +-------+ +------------+
77
+ # ^ |
78
+ # | event processed |
79
+ # +------------------------------+
80
+ #
81
+ state_machine:
82
+ state_machine_name: "contract_registry_fsm"
83
+ initial_state: "ready"
84
+ states:
85
+ - state_name: "ready"
86
+ description: "Ready to process contract events"
87
+ entry_actions: []
88
+ exit_actions: []
89
+ required_data: []
90
+ - state_name: "processing"
91
+ description: "Currently processing a contract event"
92
+ entry_actions: []
93
+ exit_actions: []
94
+ required_data:
95
+ - "event_id"
96
+ - "correlation_id"
97
+ transitions:
98
+ # ready -> processing: On any contract event
99
+ - from_state: "ready"
100
+ to_state: "processing"
101
+ trigger: "contract_event_received"
102
+ description: "Start processing contract registration event"
103
+ conditions:
104
+ - expression: "event_id is_present"
105
+ required: true
106
+ actions:
107
+ - action_name: "process_contract_event"
108
+ action_type: "intent_emission"
109
+ # processing -> ready: After event processed
110
+ - from_state: "processing"
111
+ to_state: "ready"
112
+ trigger: "event_processed"
113
+ description: "Return to ready state after processing"
114
+ actions: []
115
+ persistence_enabled: true
116
+ terminal_states: []
117
+ # Intent Emission Configuration
118
+ # ============================
119
+ # The reducer emits intents for Effect layer execution.
120
+ # These intents are NOT executed by the reducer - they are returned
121
+ # in the ModelReducerOutput for the Runtime to publish.
122
+ intent_emission:
123
+ enabled: true
124
+ # Intent Type Clarification:
125
+ # These intent_type values (e.g., "postgres.upsert_contract") are PAYLOAD intent types,
126
+ # stored in `payload.intent_type` on typed payload models like ModelPayloadUpsertContract.
127
+ # The outer ModelIntent.intent_type is always "extension" for extension-based intents.
128
+ # Effect layer routing uses payload.intent_type to dispatch to the correct handler.
129
+ # See: CLAUDE.md "Intent Model Architecture" for the two-layer intent pattern.
130
+ intents:
131
+ - intent_type: "postgres.upsert_contract"
132
+ target_pattern: "postgres://contracts/{contract_id}"
133
+ payload_model: "ModelPayloadUpsertContract"
134
+ payload_module: "omnibase_infra.nodes.contract_registry_reducer.models.model_payload_upsert_contract"
135
+ description: "Upsert contract record in PostgreSQL"
136
+ - intent_type: "postgres.update_topic"
137
+ target_pattern: "postgres://topics/{topic_suffix}"
138
+ payload_model: "ModelPayloadUpdateTopic"
139
+ payload_module: "omnibase_infra.nodes.contract_registry_reducer.models.model_payload_update_topic"
140
+ description: "Update topic record in PostgreSQL"
141
+ - intent_type: "postgres.mark_stale"
142
+ target_pattern: "postgres://contracts/stale"
143
+ payload_model: "ModelPayloadMarkStale"
144
+ payload_module: "omnibase_infra.nodes.contract_registry_reducer.models.model_payload_mark_stale"
145
+ description: "Mark stale contracts as inactive"
146
+ - intent_type: "postgres.update_heartbeat"
147
+ target_pattern: "postgres://contracts/{contract_id}/heartbeat"
148
+ payload_model: "ModelPayloadUpdateHeartbeat"
149
+ payload_module: "omnibase_infra.nodes.contract_registry_reducer.models.model_payload_update_heartbeat"
150
+ description: "Update last_seen_at timestamp from heartbeat"
151
+ - intent_type: "postgres.deactivate_contract"
152
+ target_pattern: "postgres://contracts/{contract_id}"
153
+ payload_model: "ModelPayloadDeactivateContract"
154
+ payload_module: "omnibase_infra.nodes.contract_registry_reducer.models.model_payload_deactivate_contract"
155
+ description: "Mark contract as inactive on deregistration"
156
+ - intent_type: "postgres.cleanup_topic_references"
157
+ target_pattern: "postgres://topics/cleanup/{contract_id}"
158
+ payload_model: "ModelPayloadCleanupTopicReferences"
159
+ payload_module: "omnibase_infra.nodes.contract_registry_reducer.models.model_payload_cleanup_topic_references"
160
+ description: "Remove contract references from topics on deregistration"
161
+ # State Model Configuration
162
+ # =========================
163
+ # The reducer uses ModelContractRegistryState for immutable state management.
164
+ state_model:
165
+ name: "ModelContractRegistryState"
166
+ module: "omnibase_infra.nodes.contract_registry_reducer.models.model_contract_registry_state"
167
+ description: "Immutable state model for contract registry reducer"
168
+ # Validation Configuration
169
+ # ========================
170
+ # Event validation rules applied before state transitions.
171
+ validation:
172
+ enabled: true
173
+ rules:
174
+ - field: "contract_id"
175
+ required: true
176
+ error_code: "missing_contract_id"
177
+ error_message: "contract_id is required for contract identity"
178
+ - field: "node_id"
179
+ required: true
180
+ error_code: "missing_node_id"
181
+ error_message: "node_id is required to associate contract with source node"
182
+ - field: "node_type"
183
+ required: false
184
+ valid_values:
185
+ - "effect"
186
+ - "compute"
187
+ - "reducer"
188
+ - "orchestrator"
189
+ invalid_error_code: "invalid_node_type"
190
+ error_message: "node_type must be a valid ONEX node archetype"
191
+ # Performance Configuration
192
+ # =========================
193
+ # Performance thresholds for monitoring.
194
+ # Values can be overridden via environment variables.
195
+ performance:
196
+ thresholds:
197
+ reduce_ms:
198
+ default: 300.0
199
+ env_var: "ONEX_PERF_THRESHOLD_CONTRACT_REDUCE_MS"
200
+ description: "Target processing time for reduce() method"
201
+ intent_build_ms:
202
+ default: 50.0
203
+ env_var: "ONEX_PERF_THRESHOLD_CONTRACT_INTENT_BUILD_MS"
204
+ description: "Target processing time for intent building"
205
+ staleness_check_ms:
206
+ default: 100.0
207
+ env_var: "ONEX_PERF_THRESHOLD_STALENESS_CHECK_MS"
208
+ description: "Target processing time for staleness computation on tick"
209
+ # Idempotency Configuration
210
+ # =========================
211
+ # The reducer supports idempotent event processing via Kafka offset tracking.
212
+ idempotency:
213
+ enabled: true
214
+ strategy: "kafka_offset_tracking"
215
+ derivation: "topic_partition_offset"
216
+ tracking_fields:
217
+ - "topic"
218
+ - "partition"
219
+ - "offset"
220
+ description: "Skip duplicate events based on Kafka position (topic:partition -> offset). State tracks last processed offset per (topic, partition) for multi-topic idempotency."
221
+ # Staleness Configuration
222
+ # =======================
223
+ # Configuration for staleness computation on runtime-tick events.
224
+ staleness:
225
+ threshold_seconds: 300 # 5 minutes without heartbeat = stale
226
+ env_var: "ONEX_CONTRACT_STALENESS_THRESHOLD_SECONDS"
227
+ description: "Contracts without heartbeat for this duration are marked stale"
228
+ # Dependencies
229
+ # ============
230
+ dependencies:
231
+ - name: "container"
232
+ type: "ModelONEXContainer"
233
+ module: "omnibase_core.models.container.model_onex_container"
234
+ description: "ONEX dependency injection container"
235
+ # Health Check
236
+ # ============
237
+ health_check:
238
+ enabled: true
239
+ endpoint: "/health"
240
+ interval_seconds: 30
241
+ # Metadata
242
+ # ========
243
+ metadata:
244
+ author: "OmniNode Team"
245
+ created: "2026-01-29"
246
+ updated: "2026-01-29"
247
+ tags:
248
+ - "reducer"
249
+ - "contract-registry"
250
+ - "fsm"
251
+ - "pure-function"
252
+ - "kafka-consumer"
253
+ - "postgres-projector"
254
+ related_tickets:
255
+ - "OMN-1653"
@@ -0,0 +1,38 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Models for the contract registry reducer node.
4
+
5
+ This module exports the state model and intent payloads used by NodeContractRegistryReducer.
6
+ """
7
+
8
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_contract_registry_state import (
9
+ ModelContractRegistryState,
10
+ )
11
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_cleanup_topic_references import (
12
+ ModelPayloadCleanupTopicReferences,
13
+ )
14
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_deactivate_contract import (
15
+ ModelPayloadDeactivateContract,
16
+ )
17
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_mark_stale import (
18
+ ModelPayloadMarkStale,
19
+ )
20
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_update_heartbeat import (
21
+ ModelPayloadUpdateHeartbeat,
22
+ )
23
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_update_topic import (
24
+ ModelPayloadUpdateTopic,
25
+ )
26
+ from omnibase_infra.nodes.contract_registry_reducer.models.model_payload_upsert_contract import (
27
+ ModelPayloadUpsertContract,
28
+ )
29
+
30
+ __all__ = [
31
+ "ModelContractRegistryState",
32
+ "ModelPayloadCleanupTopicReferences",
33
+ "ModelPayloadDeactivateContract",
34
+ "ModelPayloadMarkStale",
35
+ "ModelPayloadUpdateHeartbeat",
36
+ "ModelPayloadUpdateTopic",
37
+ "ModelPayloadUpsertContract",
38
+ ]
@@ -0,0 +1,266 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Immutable state model for contract registry reducer.
4
+
5
+ This module provides ModelContractRegistryState, an immutable state model for the
6
+ contract registry projection reducer. The state follows the pure reducer pattern
7
+ where state is passed in and returned from reduce(), with no internal mutation.
8
+
9
+ Architecture:
10
+ ModelContractRegistryState is designed for projection to PostgreSQL. The reducer
11
+ processes contract registration events and emits intents for persistence. The state
12
+ tracks:
13
+
14
+ - Processed positions per (topic, partition) for multi-topic idempotency
15
+ - Last processed event ID (for correlation/debugging)
16
+ - Staleness tracking (for TTL-based garbage collection)
17
+ - Processing statistics (for observability)
18
+
19
+ State transitions are performed via `with_*` methods that return new instances,
20
+ ensuring the reducer remains pure and deterministic.
21
+
22
+ Idempotency:
23
+ The state uses Kafka-based idempotency (topic, partition, offset) rather than
24
+ event ID-based idempotency. This is more robust for replay scenarios since
25
+ Kafka guarantees ordering within a partition.
26
+
27
+ **Multi-Topic Support**: The reducer consumes from 4 different Kafka topics
28
+ (contract-registered, contract-deregistered, heartbeat, runtime-tick). The state
29
+ tracks the last processed offset **per (topic, partition)** to correctly detect
30
+ duplicates even when events arrive interleaved from different topics.
31
+
32
+ The `is_duplicate_event` method checks if an event was already processed by
33
+ looking up the (topic, partition) key in `processed_positions` and comparing
34
+ offsets.
35
+
36
+ Related:
37
+ - NodeContractRegistryReducer: Declarative reducer that uses this state model
38
+ - OMN-1653: Contract registry reducer implementation
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from datetime import datetime
44
+ from uuid import UUID
45
+
46
+ from pydantic import BaseModel, ConfigDict, Field
47
+
48
+
49
+ class ModelContractRegistryState(BaseModel):
50
+ """Immutable state for contract registry projection.
51
+
52
+ This state tracks processed positions per (topic, partition) for multi-topic
53
+ idempotency and provides statistics for observability. The actual registry
54
+ data lives in PostgreSQL (this reducer projects to it).
55
+
56
+ The state is immutable (frozen=True) to enforce the pure reducer pattern.
57
+ All state transitions create new instances via `with_*` methods.
58
+
59
+ Multi-Topic Idempotency:
60
+ The reducer consumes from 4 different Kafka topics. A naive single-position
61
+ tracker would fail when events arrive interleaved:
62
+
63
+ 1. Process topic A, partition 0, offset 100 -> track (A, 0, 100)
64
+ 2. Process topic B, partition 0, offset 50 -> track (B, 0, 50)
65
+ 3. Replay topic A, partition 0, offset 100 -> NOT detected as duplicate!
66
+
67
+ This model uses `processed_positions` to track the last offset per
68
+ (topic, partition) combination, ensuring correct duplicate detection
69
+ across all consumed topics.
70
+
71
+ Persistence Integration:
72
+ This model is persisted to PostgreSQL by the Projector component:
73
+
74
+ - **Stored**: By Runtime calling Projector.persist() after reduce() returns
75
+ - **Retrieved**: By Orchestrator via ProtocolProjectionReader before reduce()
76
+ - **Idempotency**: Kafka offset tracking enables duplicate detection
77
+
78
+ The reducer does NOT persist state directly - it returns the new state
79
+ in ModelReducerOutput.result. The Runtime handles persistence.
80
+
81
+ Immutability:
82
+ This model uses frozen=True to enforce strict immutability:
83
+
84
+ - All fields are immutable after construction
85
+ - Transition methods (with_*) return NEW instances
86
+ - Original state is never modified
87
+ - Safe for concurrent access and comparison
88
+
89
+ Attributes:
90
+ last_event_id: UUID of last processed event (for correlation/debugging).
91
+ processed_positions: Dict mapping "topic:partition" to last processed offset.
92
+ last_staleness_check_at: Timestamp of last staleness check run.
93
+ contracts_processed: Count of contract registration events processed.
94
+ heartbeats_processed: Count of heartbeat events processed.
95
+ deregistrations_processed: Count of deregistration events processed.
96
+
97
+ Example:
98
+ >>> from uuid import uuid4
99
+ >>> state = ModelContractRegistryState() # Initial state
100
+ >>> state.contracts_processed
101
+ 0
102
+ >>> state = state.with_event_processed(
103
+ ... uuid4(), "contracts", 0, 1
104
+ ... ).with_contract_registered()
105
+ >>> state.contracts_processed
106
+ 1
107
+ >>> # Multi-topic: positions are tracked independently
108
+ >>> state = state.with_event_processed(uuid4(), "heartbeats", 0, 50)
109
+ >>> state.is_duplicate_event("contracts", 0, 1) # Still detected
110
+ True
111
+ """
112
+
113
+ model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
114
+
115
+ # Last processed event ID (for correlation/debugging only, not idempotency)
116
+ last_event_id: UUID | None = Field(
117
+ default=None,
118
+ description="UUID of the last processed event for correlation",
119
+ )
120
+
121
+ # Multi-topic idempotency: track last offset per (topic, partition)
122
+ # Key format: "topic:partition" -> last processed offset
123
+ processed_positions: dict[str, int] = Field(
124
+ default_factory=dict,
125
+ description="Last processed offset per (topic:partition) for multi-topic idempotency",
126
+ )
127
+
128
+ # Staleness tracking
129
+ last_staleness_check_at: datetime | None = Field(
130
+ default=None,
131
+ description="Timestamp of the last staleness check run",
132
+ )
133
+
134
+ # Statistics (for observability)
135
+ contracts_processed: int = Field(
136
+ default=0,
137
+ description="Count of contract registration events processed",
138
+ )
139
+ heartbeats_processed: int = Field(
140
+ default=0,
141
+ description="Count of heartbeat events processed",
142
+ )
143
+ deregistrations_processed: int = Field(
144
+ default=0,
145
+ description="Count of deregistration events processed",
146
+ )
147
+
148
+ @staticmethod
149
+ def _position_key(topic: str, partition: int) -> str:
150
+ """Generate dict key for (topic, partition) combination.
151
+
152
+ Args:
153
+ topic: Kafka topic name.
154
+ partition: Kafka partition number.
155
+
156
+ Returns:
157
+ String key in format "topic:partition".
158
+ """
159
+ return f"{topic}:{partition}"
160
+
161
+ def is_duplicate_event(self, topic: str, partition: int, offset: int) -> bool:
162
+ """Check if event was already processed (Kafka-based idempotency).
163
+
164
+ Uses per-(topic, partition) offset tracking for duplicate detection.
165
+ An event is considered a duplicate if:
166
+ - The (topic, partition) has been processed before
167
+ - The event's offset is <= the last processed offset for that combination
168
+
169
+ This correctly handles multi-topic consumption where events from different
170
+ topics arrive interleaved.
171
+
172
+ Args:
173
+ topic: Kafka topic of the event.
174
+ partition: Kafka partition of the event.
175
+ offset: Kafka offset of the event.
176
+
177
+ Returns:
178
+ True if this event was already processed (is a duplicate).
179
+ """
180
+ key = self._position_key(topic, partition)
181
+ last_offset = self.processed_positions.get(key)
182
+ if last_offset is None:
183
+ return False
184
+ return offset <= last_offset
185
+
186
+ def with_event_processed(
187
+ self,
188
+ event_id: UUID,
189
+ topic: str,
190
+ partition: int,
191
+ offset: int,
192
+ ) -> ModelContractRegistryState:
193
+ """Return new state with event marked as processed.
194
+
195
+ Creates a new immutable state instance with the Kafka offset tracking
196
+ updated for the specific (topic, partition) combination. Statistics are
197
+ preserved; use the specific `with_*` methods to increment them.
198
+
199
+ Multi-Topic Support:
200
+ Each (topic, partition) combination has its own tracked offset.
201
+ This ensures correct idempotency when the reducer consumes from
202
+ multiple topics (contract-registered, contract-deregistered,
203
+ heartbeat, runtime-tick).
204
+
205
+ Args:
206
+ event_id: UUID of the processed event.
207
+ topic: Kafka topic of the event.
208
+ partition: Kafka partition of the event.
209
+ offset: Kafka offset of the event.
210
+
211
+ Returns:
212
+ New ModelContractRegistryState with updated offset tracking.
213
+ """
214
+ key = self._position_key(topic, partition)
215
+ # Create new dict with updated position (immutable pattern)
216
+ new_positions = {**self.processed_positions, key: offset}
217
+ return self.model_copy(
218
+ update={
219
+ "last_event_id": event_id,
220
+ "processed_positions": new_positions,
221
+ }
222
+ )
223
+
224
+ def with_contract_registered(self) -> ModelContractRegistryState:
225
+ """Return new state with contract registration count incremented.
226
+
227
+ Returns:
228
+ New ModelContractRegistryState with contracts_processed + 1.
229
+ """
230
+ return self.model_copy(
231
+ update={"contracts_processed": self.contracts_processed + 1}
232
+ )
233
+
234
+ def with_heartbeat_processed(self) -> ModelContractRegistryState:
235
+ """Return new state with heartbeat count incremented.
236
+
237
+ Returns:
238
+ New ModelContractRegistryState with heartbeats_processed + 1.
239
+ """
240
+ return self.model_copy(
241
+ update={"heartbeats_processed": self.heartbeats_processed + 1}
242
+ )
243
+
244
+ def with_deregistration_processed(self) -> ModelContractRegistryState:
245
+ """Return new state with deregistration count incremented.
246
+
247
+ Returns:
248
+ New ModelContractRegistryState with deregistrations_processed + 1.
249
+ """
250
+ return self.model_copy(
251
+ update={"deregistrations_processed": self.deregistrations_processed + 1}
252
+ )
253
+
254
+ def with_staleness_check(self, check_time: datetime) -> ModelContractRegistryState:
255
+ """Return new state with staleness check timestamp updated.
256
+
257
+ Args:
258
+ check_time: Timestamp of the staleness check.
259
+
260
+ Returns:
261
+ New ModelContractRegistryState with updated staleness check time.
262
+ """
263
+ return self.model_copy(update={"last_staleness_check_at": check_time})
264
+
265
+
266
+ __all__ = ["ModelContractRegistryState"]
@@ -0,0 +1,55 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Cleanup topic references intent payload model.
4
+
5
+ Related:
6
+ - OMN-1653: Contract Registry Reducer implementation
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime
12
+ from typing import Literal
13
+ from uuid import UUID
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+
18
+ class ModelPayloadCleanupTopicReferences(BaseModel):
19
+ """Payload for PostgreSQL cleanup topic references intents.
20
+
21
+ Used when a contract-deregistered event is processed to remove
22
+ the contract_id from all topics.contract_ids JSONB arrays.
23
+
24
+ Note: contract_id is a derived natural key (node_name:major.minor.patch),
25
+ not a UUID. This is intentional per the contract registry design.
26
+
27
+ Attributes:
28
+ intent_type: Routing discriminator. Always "postgres.cleanup_topic_references".
29
+ correlation_id: Correlation ID for distributed tracing.
30
+ contract_id: Contract ID to remove from all topic references.
31
+ node_name: Contract node name (for logging/debugging).
32
+ cleaned_at: Cleanup timestamp.
33
+ """
34
+
35
+ model_config = ConfigDict(frozen=True, extra="forbid", from_attributes=True)
36
+
37
+ intent_type: Literal["postgres.cleanup_topic_references"] = Field(
38
+ default="postgres.cleanup_topic_references",
39
+ description="Routing discriminator for intent dispatch.",
40
+ )
41
+
42
+ correlation_id: UUID = Field(
43
+ ..., description="Correlation ID for distributed tracing."
44
+ )
45
+
46
+ # ONEX_EXCLUDE: pattern_validator - contract_id is a derived natural key (name:version), not UUID
47
+ contract_id: str = Field(..., description="Contract ID to remove from topics.")
48
+
49
+ # ONEX_EXCLUDE: pattern_validator - node_name is the contract name, not an entity reference
50
+ node_name: str = Field(..., description="Contract node name.")
51
+
52
+ cleaned_at: datetime = Field(..., description="Cleanup timestamp.")
53
+
54
+
55
+ __all__ = ["ModelPayloadCleanupTopicReferences"]