Skip to content

Commit 0dd5579

Browse files
authored
Merge pull request #2885 from fermga/copilot/refactor-zhir-architecture
Refactor ZHIR to modular architecture aligned with IL/OZ/THOL
2 parents 42abca8 + 5420de2 commit 0dd5579

File tree

5 files changed

+926
-95
lines changed

5 files changed

+926
-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: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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+
**Special Case**: If epi_kind is used to track operator glyphs (common pattern),
109+
the check is skipped since this is operational metadata, not structural identity.
110+
To enable strict identity checking, use a separate attribute (e.g., "structural_type"
111+
or "node_type") for identity tracking.
112+
113+
Identity preservation is distinct from EPI preservation - EPI may change
114+
slightly during mutation (structural adjustments), but the fundamental type
115+
(epi_kind) must remain constant.
116+
117+
Examples
118+
--------
119+
>>> from tnfr.structural import create_nfr
120+
>>> from tnfr.operators import Mutation
121+
>>> G, node = create_nfr("test", epi=0.5, vf=1.0)
122+
>>> G.nodes[node]["structural_type"] = "stem_cell" # Use separate attribute
123+
>>> epi_kind_before = G.nodes[node]["structural_type"]
124+
>>> Mutation()(G, node)
125+
>>> # After mutation, structural_type should still be "stem_cell"
126+
>>> # verify_identity_preserved(G, node, epi_kind_before) # Would check structural_type
127+
"""
128+
# Skip check if identity was not tracked
129+
if epi_kind_before is None:
130+
return
131+
132+
epi_kind_after = G.nodes[node].get("epi_kind")
133+
134+
# Skip if epi_kind appears to be tracking operator glyphs (common pattern)
135+
# Operator glyphs are short codes like "IL", "OZ", "ZHIR"
136+
if epi_kind_after in ["IL", "EN", "AL", "OZ", "RA", "UM", "SHA", "VAL", "NUL", "THOL", "ZHIR", "NAV", "REMESH"]:
137+
# epi_kind is being used for operator tracking, not identity
138+
# This is acceptable operational metadata, skip identity check
139+
return
140+
141+
if epi_kind_after != epi_kind_before:
142+
raise OperatorContractViolation(
143+
"Mutation",
144+
f"Structural identity changed during mutation: "
145+
f"{epi_kind_before}{epi_kind_after}. "
146+
f"ZHIR must preserve epi_kind while transforming phase."
147+
)
148+
149+
150+
def verify_bifurcation_handled(G: TNFRGraph, node: NodeId) -> None:
151+
"""Verify that bifurcation was handled if triggered during mutation.
152+
153+
When ZHIR detects bifurcation potential (∂²EPI/∂t² > τ), it must either:
154+
1. Create a variant node (if bifurcation mode = "variant_creation")
155+
2. Set detection flag (if bifurcation mode = "detection")
156+
157+
This ensures that bifurcation events are properly tracked and controlled,
158+
preventing uncontrolled structural fragmentation.
159+
160+
Parameters
161+
----------
162+
G : TNFRGraph
163+
Graph containing the node
164+
node : NodeId
165+
Node to verify
166+
167+
Raises
168+
------
169+
OperatorContractViolation
170+
If bifurcation was triggered but not handled according to configured mode
171+
172+
Notes
173+
-----
174+
**Bifurcation Modes**:
175+
176+
- "detection" (default): Only flag bifurcation potential, no variant creation
177+
- "variant_creation": Create new node as bifurcation variant
178+
179+
In "variant_creation" mode, the function verifies that a bifurcation event
180+
was recorded in G.graph["zhir_bifurcation_events"].
181+
182+
Grammar rule U4a requires bifurcation handlers (THOL or IL) after ZHIR
183+
when bifurcation is detected.
184+
185+
Examples
186+
--------
187+
>>> from tnfr.structural import create_nfr
188+
>>> from tnfr.operators import Mutation
189+
>>> G, node = create_nfr("test", epi=0.5, vf=1.0)
190+
>>> G.graph["ZHIR_BIFURCATION_MODE"] = "detection"
191+
>>> Mutation()(G, node)
192+
>>> # If bifurcation detected, flag should be set
193+
>>> verify_bifurcation_handled(G, node) # Should pass
194+
"""
195+
# Check if bifurcation was detected
196+
bifurcation_potential = G.nodes[node].get("_zhir_bifurcation_potential", False)
197+
198+
if not bifurcation_potential:
199+
# No bifurcation detected, nothing to verify
200+
return
201+
202+
# Bifurcation was detected - verify it was handled
203+
mode = G.graph.get("ZHIR_BIFURCATION_MODE", "detection")
204+
205+
if mode == "variant_creation":
206+
# In variant creation mode, verify variant was actually created
207+
events = G.graph.get("zhir_bifurcation_events", [])
208+
209+
# Check if this node has a recorded bifurcation event
210+
node_has_event = any(
211+
event.get("parent_node") == node
212+
for event in events
213+
)
214+
215+
if not node_has_event:
216+
raise OperatorContractViolation(
217+
"Mutation",
218+
f"Bifurcation potential detected (∂²EPI/∂t² > τ) but variant "
219+
f"was not created. Mode={mode} requires variant creation. "
220+
f"Check _spawn_mutation_variant() implementation."
221+
)
222+
223+
elif mode == "detection":
224+
# In detection mode, just verify the flag is set (already checked above)
225+
# No variant creation required, flag is sufficient
226+
pass
227+
228+
else:
229+
# Unknown mode - log warning but don't raise error
230+
import warnings
231+
warnings.warn(
232+
f"Unknown ZHIR_BIFURCATION_MODE: {mode}. "
233+
f"Expected 'detection' or 'variant_creation'. "
234+
f"Bifurcation handling could not be fully verified.",
235+
stacklevel=2,
236+
)

0 commit comments

Comments
 (0)