midi-harbor/specs/001-service-and-clients/data-model.md
James Coleman f059eaef65 docs(specs): drop the spec names used before the renumbering
- The specs index no longer carries a table mapping the three original spec names to the renumbered ones, and spec headers no longer record a feature branch or "first written as" name.
- A task that pointed at research under the old 001 directory now points at its current path, so every reference resolves to a spec that exists.
- The constitution's 1.4.1 amendment still records that the specs were split and renumbered, without listing the retired names.
2026-09-29 14:35:33 -05:00

4.1 KiB
Raw Blame History

Data Model: Midi Harbor

Feature: 001-service-and-clients | Date: 2026-09-20

This document defines the domain entities, their fields, relationships, validation rules, and state transitions. It is the vocabulary shared by the daemon, the IPC contract, the CLI, and the GUI. Types live in midi-harbor-core unless noted.


Each endpoint kind, peers, routes, connection state, counters, events, the configuration and capabilities are described in the spec they belong to.


1. Identity

Three different notions of identity must not be confused.

Identity Scope Stability Purpose
EndpointId Midi Harbor's config Permanent once assigned What routes reference
PlatformHandle One daemon run Lost on restart or replug What the OS API needs
DeviceFingerprint Physical hardware Survives unplug/replug Re-binds hardware to its EndpointId

EndpointId: an opaque UUID assigned at creation and never reused. Renaming an endpoint does not change it (FR-004). This is the only identifier persisted in routes.

DeviceFingerprint: a composite key for physical hardware (FR-015e, R-013), carrying a confidence level:

DeviceFingerprint {
    unique_id:     Option<i32>      // CoreMIDI kMIDIPropertyUniqueID
    usb_serial:    Option<String>   // Linux, from sysfs, when reported
    manufacturer:  Option<String>
    model:         Option<String>
    name:          String           // always present, never sufficient alone
    topology_path: Option<String>   // stable per physical socket, not across sockets
}

MatchConfidence is Exact (unique id or USB serial matched), Probable (manufacturer + model + name matched), or Ambiguous (name only, or several candidates matched). Only Exact and Probable auto-rebind routes; Ambiguous surfaces a choice to the user rather than guessing (R-013, FR-015g).


2. Endpoint

The central entity. Everything MIDI flows to or from is an Endpoint (FR-015b).

Endpoint {
    id:            EndpointId
    name:          String           // user-facing, editable
    kind:          EndpointKind
    enabled:       bool             // user intent, persisted
    state:         ConnectionState  // runtime, not persisted
    direction:     Direction        // Input | Output | Bidirectional
    counters:      TrafficCounters  // runtime
    created_at:    Timestamp
}

EndpointKind is a tagged union carrying kind-specific data:

  • VirtualPort(VirtualPort)
  • NetworkSession(NetworkSession)
  • PhysicalDevice(PhysicalDevice)
  • BluetoothDevice(BluetoothDevice)

Validation:

  • name is 1–128 characters, no control characters, trimmed of surrounding whitespace.
  • name must be unique among endpoints of the same kind (FR-005). Virtual ports and network ports share one namespace, because other applications see a network port as a port of its name (FR-015h). Names may otherwise collide across kinds, since the platform namespaces them separately.
  • enabled is user intent and is independent of state. A disabled endpoint is never connected; an enabled endpoint may still be Disconnected or Retrying.

3. Endpoint kinds


Entity relationships

Configuration 1───* Endpoint
Endpoint      1───1 EndpointKind  (VirtualPort | PhysicalDevice | NetworkSession | BluetoothDevice)
Endpoint      1───1 ConnectionState
Endpoint      1───1 TrafficCounters
Endpoint      1───* Event
Route         *───1 Endpoint  (as source)
Route         *───1 Endpoint  (as destination)
NetworkSession 0───1 Peer
PhysicalDevice 1───1 DeviceFingerprint

At runtime, routes reference endpoints by EndpointId, never by platform handle — which is why replugging and rebooting leave routes intact.

In the stored file, routes reference endpoints by name, so the document can be edited by hand (R-011). Renaming an endpoint through the daemon rewrites every route naming it in the same operation, so connections survive renames as FR-004 requires. A name matching nothing leaves the route visible and marked broken (FR-035) rather than silently dropped.