hexser 0.5.0

Zero-boilerplate hexagonal architecture with graph-based introspection
Documentation
# SNAPP_CONTRACT — hexser

- **Snapp tier:** XS (cross-shared substrate — `hexser` IS the Port abstraction that PRD-272
  §1.4 mandates for every cross-Snapp call site; every other crate's boundary is expressed in
  terms of the traits this crate defines).

- **PRD-272 status:** conforming as of 2026-07-21, with two exceptions tracked and landing under
  the same PRD (not yet merged at the time this contract was authored):
  - §1.5 / §3.G: `HexGraph::current()` (`graph/hex_graph.rs`) still caches behind
    `std::sync::OnceLock<Arc<HexGraph>>`. CLAIM-B1 replaces it with
    `std::sync::LazyLock<arc_swap::ArcSwap<HexGraph>>` plus `HexGraph::install()` /
    `HexGraph::rebuild_current()` as the sole write site. `container/container.rs`'s
    `tokio::sync::RwLock<HashMap<String, ServiceEntry>>` (feature `container`) is likewise
    slated (CLAIM-B2) to become `arc_swap::ArcSwap<indexmap::IndexMap<String, ServiceEntry>>`
    with CAS-retry writes and wait-free reads; its per-entry `tokio::sync::OnceCell` singleton
    cache is a documented carve-out (per-entry init, not a map-wide lock) and is not affected.
  - §3.H: the graph adjacency indices (`outgoing`/`incoming` in `GraphInner`), and other
    iterated maps (event-bus handler registry, CloudEvents envelope `extensions`) are mid-flight
    HashMap → `indexmap::IndexMap` conversions under the same PRD (CLAIM-A1/A2/A3). Graph
    *nodes* already use `BTreeMap` (deterministic).
  - `arc_swap` and `indexmap` are already core (non-optional) dependencies of this crate
    (`Cargo.toml`), added ahead of the above landing.

- **Boundary ports (trait Ports exposed to other crates/Snapps):**
  - `crate::ports::repository::{Repository, QueryRepository}` — save-side and generic
    filter/sort/paginate read-side persistence ports (`ports/repository.rs`).
  - `crate::ports::events::{EventPublisher, EventSubscriber, EventCodec, EventRouter}`    CloudEvents v1.0-compliant transport-agnostic event ports, plus the
    `CloudEventsEnvelope<T>` wire type (`ports/events/mod.rs`, `ports/events/*.rs`).
  - `crate::ports::mcp_server::McpServer` (feature `mcp`) — `initialize`, `list_resources`,
    `read_resource`, `refresh_project`, `handle_request`; the Model Context Protocol JSON-RPC
    boundary adapters implement against (`ports/mcp_server.rs`).
  - `crate::ports::{InputPort, OutputPort, Query, UseCase}` — the remaining Phase-1 port
    traits (input/output boundary and CQRS query shape).
  - Graph introspection surface: `crate::graph::HexGraph` (`HexGraph::current()`,
    `nodes_by_layer`, `nodes_by_role`, `edges_from`/`edges_to`, DOT/Mermaid/JSON export under
    `visualization`) — not a trait Port, but the read surface every other Snapp/tool queries to
    introspect this process's architecture.

- **Wire format at the boundary:**
  - JSON at the **external** AI/MCP boundary: `AIContext`/`ContextBuilder` and `AgentPack`
    (feature `ai`) serialize via `serde_json` (`ai::ai_context::AIContext::to_json`,
    `ai::agent_pack::AgentPack::to_json`); the MCP server (feature `mcp`) speaks JSON-RPC
    (`domain::mcp::{JsonRpcRequest, JsonRpcResponse}`) per protocol mandate.
  - In-process trait dispatch internally — `Repository`/`QueryRepository`/`EventPublisher`/
    `EventSubscriber`/`McpServer` calls are direct Rust trait calls within a process, not
    serialized messages.
  - Postcard (PRD-272 §1.3, internal cross-Snapp message wire) explicitly does **not** apply
    here: the AIContext/AgentPack export and the MCP JSON-RPC surface are an **external** wire
    consumed by AI agents and MCP clients outside the Rust process/workspace, not an internal
    cross-Snapp message between Rust crates. JSON is the correct choice for both (human/AI
    readability for the former, protocol mandate for the latter).

- **Shared-read hot state (ArcSwap surfaces):** the `HexGraph` architecture graph via
  `HexGraph::current()` is the self-programming substrate — read on every hot path (MCP
  `read_resource`/`list_resources` via `ProjectRegistry::from_current_graph`, every AI-context
  export) and hot-swapped when the program rewrites itself (`hexser/refresh`). Landing state
  (CLAIM-B1, this PRD): `current()` backed by
  `std::sync::LazyLock<arc_swap::ArcSwap<HexGraph>>`, a wait-free RCU surface per N_BOOK §1.5,
  read via `.load_full()` (unchanged `Arc<HexGraph>` return type) and hot-swapped by the sole
  write site `HexGraph::install(graph)` (with `HexGraph::rebuild_current()` rebuilding from the
  registry and installing). At the time of authoring, `current()` is still `OnceLock`-cached
  (write-once); this section describes the intended/landing state per this PRD.

- **Locks:** ArcSwap for the graph (landing, see above). The dyn `container` feature (feature
  `container`) uses (landing) `arc_swap::ArcSwap` for its services map plus a per-entry
  `tokio::sync::OnceCell` singleton cache — the latter is a documented carve-out (per-entry
  initialization, not a map-wide lock, never held across provider execution). No hot-path
  `RwLock`/`Mutex`/`OnceLock` outside these two, and the `container` `RwLock` is being removed
  under this PRD. The `RefCell`-based `adapters/in_memory_event_bus.rs` remains the documented
  single-threaded test/dev adapter (exempt as a non-hot-path, non-shared-state, single-threaded
  utility) — see N_BOOK Phase E (actor-grade path) for the multi-threaded successor.

- **Deterministic iteration:** `BTreeMap` for graph nodes (already landed); `indexmap::IndexMap`
  for graph adjacency (`outgoing`/`incoming`), `GraphMetadata::attributes`, the event-bus
  `handlers` registry, and `CloudEventsEnvelope::extensions` (all landing under this PRD's
  CLAIM-A1/A2/A3). No `HashMap`/`HashSet` on iterated (export/serialize/report-order-sensitive)
  paths once landed.

- **N_BOOK grounding:** §1.5 (wait-free RCU — the `HexGraph::current()` ArcSwap surface), §10
  (Snapp boundary independence — every `ports::*` trait is a Port other Snapps/crates code
  against, never a concrete adapter type), §23 (Tier 2 SOLE write site — `HexGraph::install()`
  is the only place the live architecture graph is replaced).

_Authored 2026-07-21 under PRD-272 §1.1 (HEXSER-272-CONFORMANCE)._