# Handle System Invariants

This document describes the invariants that the handle system must maintain. These invariants are enforced by tests to prevent regressions.

## Conceptual Model

A Handle is a **lazy typed reference** - it carries type information via `(plugin, method)` but defers data retrieval until resolution.

```
Handle[T] ≈ Lazy<T>
  where T = Schema(plugin, method).resolve_type

resolve : Handle[T] → T
```

The schema lookup is **type reflection** - asking "what type will I get when I unwrap this?" Resolution is the **forcing operation** that materializes the data.

## Invariant 1: Roundtrip Preservation

**Property**: Parsing a displayed handle yields the original handle.

```
∀ h : Handle. parse(display(h)) = h
```

**Implementation**: `Handle::from_str(handle.to_string()) == handle`

**Test locations**: `src/types.rs`
- `invariant_handle_roundtrip_with_meta`
- `invariant_handle_roundtrip_no_meta`
- `invariant_handle_roundtrip_single_meta`

**Why it matters**: Handles are serialized to strings for storage (Arbor nodes), transmission (RPC), and display (CLI). The string representation must be lossless.

## Invariant 2: Structural Equality

**Property**: Handles with identical components are equal; handles differing in any component are not equal.

```
h1 = h2  ⟺  h1.plugin = h2.plugin ∧
            h1.version = h2.version ∧
            h1.method = h2.method ∧
            h1.meta = h2.meta
```

**Test locations**: `src/types.rs`
- `invariant_handle_equality_reflexive`
- `invariant_handle_equality_symmetric`
- `invariant_handle_inequality_different_plugin`
- `invariant_handle_inequality_different_version`
- `invariant_handle_inequality_different_method`
- `invariant_handle_inequality_different_meta`
- `invariant_handle_inequality_meta_vs_no_meta`

**Why it matters**: Equality is used for deduplication, caching, and identity checks. Incorrect equality could cause data corruption.

## Invariant 3: Parse Error Clarity

**Property**: Invalid handle strings fail with descriptive errors, not panics.

```
parse("invalid") = Err(message) where message.contains(hint)
```

**Test locations**: `src/types.rs`
- `invariant_parse_error_missing_at`
- `invariant_parse_error_missing_double_colon`
- `invariant_parse_error_empty_string`

**Why it matters**: User-provided handle strings (from CLI, configs, RPC) may be malformed. Clear errors enable debugging.

## Invariant 4: Meta Order Preservation

**Property**: Meta fields maintain insertion order exactly.

```
handle.push_meta("a").push_meta("b").push_meta("c")
  → handle.meta = ["a", "b", "c"]
```

**Test locations**: `src/types.rs`
- `invariant_meta_order_preserved`
- `invariant_meta_empty_string_allowed`
- `invariant_meta_with_meta_replaces`

**Why it matters**: Meta field semantics depend on position (e.g., `meta[0]` = message ID, `meta[1]` = role, `meta[2]` = name). Order changes would break resolution.

## Invariant 5: JSON Serialization Roundtrip

**Property**: JSON serialization is lossless and handles edge cases gracefully.

```
∀ h : Handle. from_json(to_json(h)) = h
```

**Additional properties**:
- Empty meta is omitted from JSON (not serialized as `[]`)
- Missing meta in JSON deserializes to empty `Vec`

**Test locations**: `src/types.rs`
- `invariant_json_roundtrip`
- `invariant_json_empty_meta_omitted`
- `invariant_json_deserialize_missing_meta`

**Why it matters**: Handles are stored in databases as JSON and transmitted via JSON-RPC. Compact serialization reduces storage; graceful deserialization handles schema evolution.

## Invariant 6: Creation-Resolution Format Agreement

**Property**: The meta format produced by handle creation must match the format expected by resolution.

```
let handle = message_to_handle(msg, name)
let identifier = handle.meta.join(":")
resolve_message_handle(identifier) succeeds
```

**Test locations**: `src/activations/cone/storage.rs`
- `invariant_handle_meta_format_matches_resolver`
- `invariant_handle_meta_roles`
- `invariant_handle_plugin_method_fixed`
- `invariant_handle_meta_has_three_parts`

**Why it matters**: This invariant was violated before the fix. Creation stored `["msg-uuid", "role", "name"]` but resolution expected `"msg-uuid:role:name"`. The fix joins meta parts on the resolution side.

**Historical bug**:
```rust
// Before: resolution only used meta[0]
let msg_id = handle.meta.first()?;
storage.resolve_message_handle(&msg_id)  // Failed: "msg-uuid" != "msg-uuid:role:name"

// After: resolution joins all meta parts
let identifier = handle.meta.join(":");
storage.resolve_message_handle(&identifier)  // Works
```

## Invariant 7: Plugin Routing

**Property**: `resolve_handle` dispatches to the plugin named in `handle.plugin`.

```
plexus.resolve_handle(Handle { plugin: "cone", ... })
  → cone.resolve_handle(...)

plexus.resolve_handle(Handle { plugin: "unknown", ... })
  → Err(ActivationNotFound("unknown"))
```

**Test locations**: `src/plexus/plexus.rs`
- `invariant_resolve_handle_unknown_plugin`
- `invariant_resolve_handle_unsupported`
- `invariant_resolve_handle_routes_by_plugin_name`
- `invariant_handle_plugin_determines_routing`

**Why it matters**: Correct routing is the foundation of the handle system. A handle for plugin A must never resolve using plugin B's logic.

## Invariant Summary Table

| Invariant | Location | Critical For |
|-----------|----------|--------------|
| Roundtrip | `types.rs` | Storage, transmission |
| Equality | `types.rs` | Identity, caching |
| Parse errors | `types.rs` | User experience |
| Meta order | `types.rs` | Field semantics |
| JSON roundtrip | `types.rs` | Persistence |
| Format agreement | `cone/storage.rs` | Resolution correctness |
| Plugin routing | `plexus/plexus.rs` | Dispatch correctness |

## Adding New Invariants

When adding handle functionality, consider:

1. **Does it affect serialization?** Add roundtrip tests.
2. **Does it affect equality?** Add comparison tests.
3. **Does it affect resolution?** Add format agreement tests.
4. **Does it affect routing?** Add dispatch tests.

Name tests with `invariant_` prefix to distinguish from unit tests.
