Skip to content

Commit fa55196

Browse files
Copilotfermga
andcommitted
Phase 1 & 2: Create modular preconditions and postconditions for ZHIR
Co-authored-by: fermga <203334638+fermga@users.noreply.github.com>
1 parent b99ff49 commit fa55196

File tree

5 files changed

+914
-95
lines changed

5 files changed

+914
-95
lines changed

src/tnfr/operators/definitions.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3484,10 +3484,11 @@ class Mutation(Operator):
34843484
glyph: ClassVar[Glyph] = Glyph.ZHIR
34853485

34863486
def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
3487-
"""Apply ZHIR with bifurcation potential detection.
3487+
"""Apply ZHIR with bifurcation potential detection and postcondition verification.
34883488
34893489
Detects when ∂²EPI/∂t² > τ (bifurcation threshold) and sets telemetry flags
3490-
to enable validation of grammar U4a.
3490+
to enable validation of grammar U4a. Also verifies postconditions to ensure
3491+
operator contract fulfillment.
34913492
34923493
Parameters
34933494
----------
@@ -3499,8 +3500,20 @@ def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
34993500
Additional parameters including:
35003501
- tau: Bifurcation threshold (default from graph config or 0.5)
35013502
- validate_preconditions: Enable precondition checks (default True)
3503+
- validate_postconditions: Enable postcondition checks (default False)
35023504
- collect_metrics: Enable metrics collection (default False)
35033505
"""
3506+
# Capture state before mutation for postcondition verification
3507+
validate_postconditions = kw.get("validate_postconditions", False) or G.graph.get(
3508+
"VALIDATE_OPERATOR_POSTCONDITIONS", False
3509+
)
3510+
3511+
state_before = None
3512+
if validate_postconditions:
3513+
state_before = self._capture_state(G, node)
3514+
# Also capture epi_kind if tracked
3515+
state_before["epi_kind"] = G.nodes[node].get("epi_kind")
3516+
35043517
# Compute structural acceleration before base operator
35053518
d2_epi = self._compute_epi_acceleration(G, node)
35063519

@@ -3521,6 +3534,10 @@ def __call__(self, G: TNFRGraph, node: Any, **kw: Any) -> None:
35213534
# Detect bifurcation potential if acceleration exceeds threshold
35223535
if d2_epi > tau:
35233536
self._detect_bifurcation_potential(G, node, d2_epi=d2_epi, tau=tau)
3537+
3538+
# Verify postconditions if enabled
3539+
if validate_postconditions and state_before is not None:
3540+
self._verify_postconditions(G, node, state_before)
35243541

35253542
def _compute_epi_acceleration(self, G: TNFRGraph, node: Any) -> float:
35263543
"""Calculate ∂²EPI/∂t² from node's EPI history.
@@ -3613,6 +3630,44 @@ def _validate_preconditions(self, G: TNFRGraph, node: Any) -> None:
36133630

36143631
validate_mutation(G, node)
36153632

3633+
def _verify_postconditions(
3634+
self, G: TNFRGraph, node: Any, state_before: dict[str, Any]
3635+
) -> None:
3636+
"""Verify ZHIR-specific postconditions.
3637+
3638+
Ensures that ZHIR fulfilled its contract:
3639+
1. Phase was transformed (θ changed)
3640+
2. Identity preserved (epi_kind maintained)
3641+
3. Bifurcation handled (if detected)
3642+
3643+
Parameters
3644+
----------
3645+
G : TNFRGraph
3646+
Graph containing the node
3647+
node : Any
3648+
Node that was mutated
3649+
state_before : dict
3650+
Node state before operator application, containing:
3651+
- theta: Phase value before mutation
3652+
- epi_kind: Identity before mutation (if tracked)
3653+
"""
3654+
from .postconditions.mutation import (
3655+
verify_phase_transformed,
3656+
verify_identity_preserved,
3657+
verify_bifurcation_handled,
3658+
)
3659+
3660+
# Verify phase transformation
3661+
verify_phase_transformed(G, node, state_before["theta"])
3662+
3663+
# Verify identity preservation (if tracked)
3664+
epi_kind_before = state_before.get("epi_kind")
3665+
if epi_kind_before is not None:
3666+
verify_identity_preserved(G, node, epi_kind_before)
3667+
3668+
# Verify bifurcation handling
3669+
verify_bifurcation_handled(G, node)
3670+
36163671
def _collect_metrics(
36173672
self, G: TNFRGraph, node: Any, state_before: dict[str, Any]
36183673
) -> dict[str, Any]:
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Postcondition validators for TNFR structural operators.
2+
3+
Each operator has specific guarantees that must be verified after execution
4+
to ensure TNFR structural invariants are maintained. This package provides
5+
postcondition validators for operators that need strict verification.
6+
7+
Postconditions ensure that operators fulfill their contracts and maintain
8+
canonical TNFR physics.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import TYPE_CHECKING
14+
15+
if TYPE_CHECKING:
16+
from ...types import NodeId, TNFRGraph
17+
18+
__all__ = [
19+
"OperatorContractViolation",
20+
]
21+
22+
23+
class OperatorContractViolation(Exception):
24+
"""Raised when an operator's postconditions are violated."""
25+
26+
def __init__(self, operator: str, reason: str) -> None:
27+
"""Initialize contract violation error.
28+
29+
Parameters
30+
----------
31+
operator : str
32+
Name of the operator that violated its contract
33+
reason : str
34+
Description of why the contract was violated
35+
"""
36+
self.operator = operator
37+
self.reason = reason
38+
super().__init__(f"{operator} contract violation: {reason}")
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""Postcondition validators for ZHIR (Mutation) operator.
2+
3+
Implements verification of mutation postconditions including phase transformation,
4+
identity preservation, and bifurcation handling.
5+
6+
These postconditions ensure that ZHIR fulfills its contract and maintains TNFR
7+
structural invariants.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from typing import TYPE_CHECKING
13+
14+
if TYPE_CHECKING:
15+
from ...types import NodeId, TNFRGraph
16+
17+
from ...alias import get_attr
18+
from ...constants.aliases import ALIAS_THETA
19+
from . import OperatorContractViolation
20+
21+
__all__ = [
22+
"verify_phase_transformed",
23+
"verify_identity_preserved",
24+
"verify_bifurcation_handled",
25+
]
26+
27+
28+
def verify_phase_transformed(
29+
G: TNFRGraph, node: NodeId, theta_before: float
30+
) -> None:
31+
"""Verify that phase was actually transformed by ZHIR.
32+
33+
ZHIR's primary contract is phase transformation (θ → θ'). This verifies
34+
that the phase actually changed, fulfilling the operator's purpose.
35+
36+
Parameters
37+
----------
38+
G : TNFRGraph
39+
Graph containing the node
40+
node : NodeId
41+
Node to verify
42+
theta_before : float
43+
Phase value before ZHIR application
44+
45+
Raises
46+
------
47+
OperatorContractViolation
48+
If phase was not transformed (theta unchanged)
49+
50+
Notes
51+
-----
52+
A small tolerance (1e-6) is used to account for floating-point precision.
53+
If theta changes by less than this tolerance, it's considered unchanged.
54+
55+
This check ensures that ZHIR actually performs its structural transformation
56+
rather than being a no-op.
57+
58+
Examples
59+
--------
60+
>>> from tnfr.structural import create_nfr
61+
>>> from tnfr.operators import Mutation
62+
>>> G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.0)
63+
>>> theta_before = G.nodes[node]["theta"]
64+
>>> Mutation()(G, node)
65+
>>> verify_phase_transformed(G, node, theta_before) # Should pass
66+
"""
67+
theta_after = float(get_attr(G.nodes[node], ALIAS_THETA, 0.0))
68+
69+
# Check if phase actually changed (with small tolerance for floating-point)
70+
if abs(theta_after - theta_before) < 1e-6:
71+
raise OperatorContractViolation(
72+
"Mutation",
73+
f"Phase was not transformed (θ before={theta_before:.6f}, "
74+
f"θ after={theta_after:.6f}, diff={abs(theta_after - theta_before):.9f}). "
75+
f"ZHIR must transform phase to fulfill its contract."
76+
)
77+
78+
79+
def verify_identity_preserved(
80+
G: TNFRGraph, node: NodeId, epi_kind_before: str | None
81+
) -> None:
82+
"""Verify that structural identity (epi_kind) was preserved through mutation.
83+
84+
ZHIR transforms phase/regime while preserving structural identity. A cell
85+
remains a cell, a concept remains a concept - only the operational mode changes.
86+
This is a fundamental TNFR invariant: transformations preserve coherence.
87+
88+
Parameters
89+
----------
90+
G : TNFRGraph
91+
Graph containing the node
92+
node : NodeId
93+
Node to verify
94+
epi_kind_before : str or None
95+
Identity (epi_kind) before ZHIR application
96+
97+
Raises
98+
------
99+
OperatorContractViolation
100+
If identity changed during mutation
101+
102+
Notes
103+
-----
104+
If epi_kind_before is None (identity not tracked), this check is skipped.
105+
This allows flexibility for simple nodes while enforcing identity preservation
106+
when it's explicitly tracked.
107+
108+
Identity preservation is distinct from EPI preservation - EPI may change
109+
slightly during mutation (structural adjustments), but the fundamental type
110+
(epi_kind) must remain constant.
111+
112+
Examples
113+
--------
114+
>>> from tnfr.structural import create_nfr
115+
>>> from tnfr.operators import Mutation
116+
>>> G, node = create_nfr("test", epi=0.5, vf=1.0)
117+
>>> G.nodes[node]["epi_kind"] = "stem_cell"
118+
>>> epi_kind_before = G.nodes[node]["epi_kind"]
119+
>>> Mutation()(G, node)
120+
>>> # After mutation, epi_kind should still be "stem_cell"
121+
>>> verify_identity_preserved(G, node, epi_kind_before) # Should pass
122+
"""
123+
# Skip check if identity was not tracked
124+
if epi_kind_before is None:
125+
return
126+
127+
epi_kind_after = G.nodes[node].get("epi_kind")
128+
129+
if epi_kind_after != epi_kind_before:
130+
raise OperatorContractViolation(
131+
"Mutation",
132+
f"Structural identity changed during mutation: "
133+
f"{epi_kind_before}{epi_kind_after}. "
134+
f"ZHIR must preserve epi_kind while transforming phase."
135+
)
136+
137+
138+
def verify_bifurcation_handled(G: TNFRGraph, node: NodeId) -> None:
139+
"""Verify that bifurcation was handled if triggered during mutation.
140+
141+
When ZHIR detects bifurcation potential (∂²EPI/∂t² > τ), it must either:
142+
1. Create a variant node (if bifurcation mode = "variant_creation")
143+
2. Set detection flag (if bifurcation mode = "detection")
144+
145+
This ensures that bifurcation events are properly tracked and controlled,
146+
preventing uncontrolled structural fragmentation.
147+
148+
Parameters
149+
----------
150+
G : TNFRGraph
151+
Graph containing the node
152+
node : NodeId
153+
Node to verify
154+
155+
Raises
156+
------
157+
OperatorContractViolation
158+
If bifurcation was triggered but not handled according to configured mode
159+
160+
Notes
161+
-----
162+
**Bifurcation Modes**:
163+
164+
- "detection" (default): Only flag bifurcation potential, no variant creation
165+
- "variant_creation": Create new node as bifurcation variant
166+
167+
In "variant_creation" mode, the function verifies that a bifurcation event
168+
was recorded in G.graph["zhir_bifurcation_events"].
169+
170+
Grammar rule U4a requires bifurcation handlers (THOL or IL) after ZHIR
171+
when bifurcation is detected.
172+
173+
Examples
174+
--------
175+
>>> from tnfr.structural import create_nfr
176+
>>> from tnfr.operators import Mutation
177+
>>> G, node = create_nfr("test", epi=0.5, vf=1.0)
178+
>>> G.graph["ZHIR_BIFURCATION_MODE"] = "detection"
179+
>>> Mutation()(G, node)
180+
>>> # If bifurcation detected, flag should be set
181+
>>> verify_bifurcation_handled(G, node) # Should pass
182+
"""
183+
# Check if bifurcation was detected
184+
bifurcation_potential = G.nodes[node].get("_zhir_bifurcation_potential", False)
185+
186+
if not bifurcation_potential:
187+
# No bifurcation detected, nothing to verify
188+
return
189+
190+
# Bifurcation was detected - verify it was handled
191+
mode = G.graph.get("ZHIR_BIFURCATION_MODE", "detection")
192+
193+
if mode == "variant_creation":
194+
# In variant creation mode, verify variant was actually created
195+
events = G.graph.get("zhir_bifurcation_events", [])
196+
197+
# Check if this node has a recorded bifurcation event
198+
node_has_event = any(
199+
event.get("parent_node") == node
200+
for event in events
201+
)
202+
203+
if not node_has_event:
204+
raise OperatorContractViolation(
205+
"Mutation",
206+
f"Bifurcation potential detected (∂²EPI/∂t² > τ) but variant "
207+
f"was not created. Mode={mode} requires variant creation. "
208+
f"Check _spawn_mutation_variant() implementation."
209+
)
210+
211+
elif mode == "detection":
212+
# In detection mode, just verify the flag is set (already checked above)
213+
# No variant creation required, flag is sufficient
214+
pass
215+
216+
else:
217+
# Unknown mode - log warning but don't raise error
218+
import warnings
219+
warnings.warn(
220+
f"Unknown ZHIR_BIFURCATION_MODE: {mode}. "
221+
f"Expected 'detection' or 'variant_creation'. "
222+
f"Bifurcation handling could not be fully verified.",
223+
stacklevel=2,
224+
)

0 commit comments

Comments
 (0)