Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion scripts/validation/validate-string-versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,12 @@ def __init__(self, file_path: str):
"version_compatibility", # Compatibility strings
"execution_id", # Some execution IDs may need to be strings for compatibility
"version_str", # Parameter names for parsing functions
# EXTERNAL_SYSTEMS (3 fields)
# EXTERNAL_SYSTEMS (5 fields)
"external_id", # External system identifiers (not ONEX-managed)
"certificate_id", # X.509 certificate IDs
"service_id", # Consul service identifiers (external system constraint)
"consul_service_id", # Consul service identifiers (prefixed variant)
"network_id", # Network identifiers (VPC, subnet names - external systems)
# DISTRIBUTED_TRACING (4 unique fields - OpenTelemetry standard)
"trace_id", # OpenTelemetry trace identifier
"span_id", # OpenTelemetry span identifier
Expand Down
1 change: 1 addition & 0 deletions src/omnibase_core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"metadata",
"nodes",
"projection",
"registration",
"results",
"state",
"validation",
Expand Down
98 changes: 98 additions & 0 deletions src/omnibase_core/models/registration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 OmniNode Team
"""Registration domain models for ONEX node registration workflows.

This module provides pure data models for node registration operations.
These models follow the ONEX "Intent -> Effect" pattern where Reducers
compute registration payloads deterministically, and Effects perform
actual registration operations.

Models:
ModelRegistrationPayload:
Typed payload for registration intents. Contains all information
needed to register a node to both Consul and PostgreSQL. Emitted
by Reducers, consumed by Effects.

ModelDualRegistrationOutcome:
Domain-level outcome of dual registration. Captures the result
of registering to both Consul and PostgreSQL. Returned by Effects,
aggregated by Orchestrators.

Design Principles:
- **Pure Domain Models**: No I/O dependencies, no infrastructure concerns
- **Immutable**: All models are frozen (thread-safe after creation)
- **Typed**: Strong typing with validation constraints
- **Serializable**: Full JSON serialization support

Data Flow:
```
┌──────────────────────────────────────────────────────────────────┐
│ Registration Workflow Flow │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Introspection Reducer Effect Orchestrator │
│ │ │ │ │ │
│ │ process │ │ │ │
│ │────────────>│ │ │ │
│ │ │ Payload │ │ │
│ │ │─────────────────>│ │ │
│ │ │ │ Outcome │ │
│ │ │ │──────────────>│ │
│ │ │ │ │ agg │
│ │
└──────────────────────────────────────────────────────────────────┘
```

Usage:
>>> from omnibase_core.models.registration import (
... ModelRegistrationPayload,
... ModelDualRegistrationOutcome,
... )
>>> from uuid import uuid4
>>> from pydantic import BaseModel
>>>
>>> # Create a registration payload
>>> class NodeRecord(BaseModel):
... node_id: str
... status: str
>>>
>>> payload = ModelRegistrationPayload(
... node_id=uuid4(),
... deployment_id=uuid4(),
... environment="production",
... network_id="vpc-main",
... consul_service_id="node-123",
... consul_service_name="onex-compute",
... consul_tags=["env:prod"],
... consul_health_check=None,
... postgres_record=NodeRecord(node_id="123", status="active"),
... )
>>>
>>> # Create a registration outcome
>>> outcome = ModelDualRegistrationOutcome(
... node_id=uuid4(),
... status="success",
... postgres_applied=True,
... consul_applied=True,
... correlation_id=uuid4(),
... )

See Also:
omnibase_core.models.intents: Core infrastructure intents
omnibase_core.models.reducer.model_intent: Extension intents
omnibase_core.nodes.NodeReducer: Emits registration payloads
omnibase_core.nodes.NodeEffect: Consumes payloads, returns outcomes
omnibase_core.nodes.NodeOrchestrator: Aggregates outcomes
"""

from omnibase_core.models.registration.model_dual_registration_outcome import (
ModelDualRegistrationOutcome,
)
from omnibase_core.models.registration.model_registration_payload import (
ModelRegistrationPayload,
)

__all__ = [
"ModelRegistrationPayload",
"ModelDualRegistrationOutcome",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 OmniNode Team
"""Domain-level outcome of dual registration.

This module provides ModelDualRegistrationOutcome, a pure domain model representing
the logical result of registering a node to both Consul and PostgreSQL. This is a
PURE domain model - it does NOT include infrastructure concerns like timing,
retries, or telemetry.

Design Pattern:
ModelDualRegistrationOutcome is the return type from Effect nodes that perform
dual registration. It captures the outcome of both registration operations
in a single, immutable model.

The model follows these principles:
- **Domain Purity**: Only captures domain-level outcomes, no infra concerns
- **Atomic Result**: Single model represents outcome of dual operation
- **Error Transparency**: Clear error fields when operations fail
- **Correlation**: Links to originating request via correlation_id

Outcome States:
The `status` field represents the overall outcome:
- "success": Both Postgres and Consul registration succeeded
- "partial": One succeeded, one failed (check individual flags)
- "failed": Both registration attempts failed

Data Flow:
```
┌──────────────────────────────────────────────────────────────────┐
│ Registration Outcome Flow │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Effect Node Orchestrator │
│ │ │ │
│ │ register to Postgres │ │
│ │ register to Consul │ │
│ │ │ │
│ │ create DualRegistrationOutcome │ │
│ │───────────────────────────────────────────>│ │
│ │ │ aggregate │
│ │ │ outcomes │
│ │
└──────────────────────────────────────────────────────────────────┘
```

Thread Safety:
ModelDualRegistrationOutcome is immutable (frozen=True) after creation,
making it thread-safe for concurrent read access.

Example:
>>> from uuid import uuid4
>>> from omnibase_core.models.registration import ModelDualRegistrationOutcome
>>>
>>> # Successful dual registration
>>> outcome = ModelDualRegistrationOutcome(
... node_id=uuid4(),
... status="success",
... postgres_applied=True,
... consul_applied=True,
... correlation_id=uuid4(),
... )
>>>
>>> # Partial failure (Postgres succeeded, Consul failed)
>>> partial = ModelDualRegistrationOutcome(
... node_id=uuid4(),
... status="partial",
... postgres_applied=True,
... consul_applied=False,
... consul_error="Connection timeout",
... correlation_id=uuid4(),
... )

See Also:
omnibase_core.models.registration.ModelRegistrationPayload: Input payload
omnibase_core.nodes.NodeEffect: Effect nodes that produce this outcome
omnibase_core.nodes.NodeOrchestrator: Orchestrator that aggregates outcomes
"""

from __future__ import annotations

from typing import Literal
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, model_validator


class ModelDualRegistrationOutcome(BaseModel):
"""Domain-level outcome of dual registration.

This is a PURE domain model representing the logical result of
registering a node to both Consul and PostgreSQL. It does NOT
include infrastructure concerns (timing, retries, telemetry).

This model is used by:
- Effect nodes (return this after performing registration)
- Orchestrator nodes (aggregate multiple outcomes)

Attributes:
node_id: The node that was registered (or attempted).
status: Overall outcome status ("success", "partial", "failed").
postgres_applied: Whether PostgreSQL registration succeeded.
consul_applied: Whether Consul registration succeeded.
postgres_error: Error message if PostgreSQL registration failed.
consul_error: Error message if Consul registration failed.
correlation_id: Correlation ID for distributed tracing.

Example:
>>> from uuid import uuid4
>>>
>>> # Complete success
>>> success = ModelDualRegistrationOutcome(
... node_id=uuid4(),
... status="success",
... postgres_applied=True,
... consul_applied=True,
... correlation_id=uuid4(),
... )
>>>
>>> # Complete failure
>>> failure = ModelDualRegistrationOutcome(
... node_id=uuid4(),
... status="failed",
... postgres_applied=False,
... consul_applied=False,
... postgres_error="Database connection refused",
... consul_error="Service unavailable",
... correlation_id=uuid4(),
... )
"""

model_config = ConfigDict(
frozen=True,
extra="forbid",
from_attributes=True,
validate_assignment=True,
)

# ---- Identity ----
node_id: UUID = Field(
...,
description="The node that was registered (or attempted).",
)

# ---- Overall Status ----
status: Literal["success", "partial", "failed"] = Field(
...,
description=(
"Overall outcome status. 'success' means both operations succeeded, "
"'partial' means one succeeded and one failed, 'failed' means both failed."
),
)

# ---- Individual Operation Results ----
postgres_applied: bool = Field(
...,
description="Whether PostgreSQL registration succeeded.",
)
consul_applied: bool = Field(
...,
description="Whether Consul registration succeeded.",
)

# ---- Error Information ----
postgres_error: str | None = Field(
default=None,
description="Error message if PostgreSQL registration failed (max 2000 characters).",
max_length=2000,
)
consul_error: str | None = Field(
default=None,
description="Error message if Consul registration failed (max 2000 characters).",
max_length=2000,
)

# ---- Tracing ----
correlation_id: UUID = Field(
...,
description=(
"Correlation ID for distributed tracing. Links this outcome "
"to the originating request."
),
)

@model_validator(mode="after")
def validate_status_consistency(self) -> ModelDualRegistrationOutcome:
"""Validate that status field matches the applied flags.

This validator enforces domain invariants for the three possible status values:

1. status="success": Requires BOTH postgres_applied=True AND consul_applied=True
- Complete success means both operations succeeded
- Cannot have success status if either operation failed

2. status="failed": Requires BOTH postgres_applied=False AND consul_applied=False
- Complete failure means both operations failed
- Cannot have failed status if either operation succeeded

3. status="partial": Requires EXACTLY ONE operation to succeed (XOR condition)
- Partial means one succeeded and one failed
- Cannot have partial if both succeeded or both failed

These invariants ensure the model cannot be constructed in an inconsistent state,
preventing bugs where status doesn't match the actual operation outcomes.

Returns:
Self after validation passes.

Raises:
ValueError: If status doesn't match the applied flags.

Example:
>>> from uuid import uuid4
>>>
>>> # Valid: status="success" with both operations succeeded
>>> ModelDualRegistrationOutcome(
... node_id=uuid4(),
... status="success",
... postgres_applied=True,
... consul_applied=True,
... correlation_id=uuid4(),
... )
>>>
>>> # Invalid: status="success" but Consul failed
>>> ModelDualRegistrationOutcome(
... node_id=uuid4(),
... status="success", # Will raise ValueError
... postgres_applied=True,
... consul_applied=False,
... correlation_id=uuid4(),
... )
Traceback (most recent call last):
...
ValueError: status='success' requires both postgres_applied and consul_applied to be True
"""
both_succeeded = self.postgres_applied and self.consul_applied
both_failed = not self.postgres_applied and not self.consul_applied

if self.status == "success" and not both_succeeded:
raise ValueError(
"status='success' requires both postgres_applied and consul_applied to be True"
)
if self.status == "failed" and not both_failed:
raise ValueError(
"status='failed' requires both postgres_applied and consul_applied to be False"
)
if self.status == "partial" and (both_succeeded or both_failed):
raise ValueError(
"status='partial' requires exactly one operation to succeed"
)

return self


__all__ = ["ModelDualRegistrationOutcome"]
Loading