I've cobbled together a comprehensive example of some of my thinking right now.
Highlights:
- Execution types are their own entity, as we seem to agree.
- Collapsing "operations" and "notifications" into a single category: "events".
- Because interface types encapsulate a modality it is appropriate that they also contain state (attributes). This, to me, makes more sense than mapping to attributes on the node, because those attributes are not part of the interface definition and there can be no guarantee that they are of the correct types.
In the example below I show two kinds of modalities -- "phase-by-phase" and "async" -- as well as various execution types.
execution_types:
 Command:
  description:
   Executes a command in a sandboxed area managed by the orchestrator.
   This could be on one of the orchestrator's hosts, in a container, a dedicated
   VM, etc., or it could be on the user's local machine. Can optionally copy over
   artifacts before execution.
 Remote:
  description:
   Abstract base type for execution on remote hosts.
  properties:
   address: {}
   authentication: {}
   authorization: {}
 SSH:
  derived_from: Remote
  description:
   Executes a command on a remote host using SSH. Can optionally copy over
   artifacts before execution.
 GRPC:
  derived_from: Remote
  description:
   Calls a gRPC function on a remote host.
interface_types:
 Phases:
  description:
   Base type for interfaces with a phase-by-phase modality. The expectation
   is that only one event will happen at any given time.
  events:
   failed:
    description:
     Optional event for the failure of any of the other events.
    inputs:
     phase:
      type: string
  state: # attribute definitions
   phase:
    type: string
 Lifecycle:
  derived_from: Phases
  events: # collapses operations and notifications
   create:
    type: Command # default execution type, node templates can change it
   created: {}
   configure:
    type: Command
   configured: {}
   start:
    type: Command
   started: {}
   stop:
    type: Command
   stopped: {}
   delete:
    type: Command
   deleted: {}
   failed: # refinement
    inputs:
     phase:
      type: string
      constraints:
      - valid_values: [ creating, configuring, starting, stopping, deleting ]
  state:
   phase: # refinement
    type: string
    constraints:
    - valid_values: [ creating, configuring, starting, stopping, deleting ]
 Async:
  description:
   Base type for a group of asynchronous events. The expectation is that
   events can be triggered at any time, in sequence or simultaneously.
  events:
   subscribe:
    description:
     Optional event to register handlers for the other events. Should be triggered only once.
   unsubscribe: {}
   poll:
    description:
     Optional event to poll for state and trigger the other events as appropriate.
     Should be triggered once in a while.
    inputs:
     frequency:
      type: scalar-unit.frequency
 Provisioning:
  derived_from: Async
  events:
   activated: {}
   standby: {}
   cleanup: {}
  state:
   active:
    type: boolean
 Health:
  derived_from: Async
  events:
   failed: {}
   recovered: {}
  state:
   healthy:
    type: boolean
node_types:
 Server:
  interfaces:
   provisioning:
    type: Provisioning
   hardware-health:
    type: Health
   os-health:
    type: Health
topology_template:
 node_templates:
  server:
   type: Server
   interfaces:
    hardware-health:
     events:
      register:
       type: SSH
       implementation:
        command: [ python, install-health-agent.py ]
      failed:
       type: Command
       implementation:
        command: [ failed.sh ]
    os-health:
     events:
      poll:
       type: GRPC
       implementation:
        rpc: OS.CheckHealth