Problem
Keelson partitions protobuf types by transport:
interfaces/*.proto — RPC request/response types (package-less, global namespace)
messages/payloads/*.proto — pub/sub payload types (package keelson)
subjects.yaml maps a subject to a type, and the SDK resolves it from a descriptor set built only from messages/payloads/ (sdks/python/keelson/__init__.py loads payloads/protobuf_file_descriptor_set.bin).
But a Coordinate, Waypoint, MissionItem, Route is a domain noun — neither inherently RPC nor pub/sub. Partitioning domain nouns by transport forces the same concept to be modelled twice, and makes a type defined for one transport unusable on the other.
This is already biting (not hypothetical)
Two Waypoint messages now exist, modelling the same thing with different coordinate types:
interfaces/VehicleMission.proto: Waypoint / Loiter built on Coordinate { double latitude_deg; double longitude_deg } (in VehicleCommon.proto).
messages/payloads/Route.proto: Waypoint / Leg built on foxglove.LocationFix.
And because the subject type pool is payloads-only, you cannot publish an interface type today: e.g. there's no way to add current_mission_item: keelson.MissionItem to subjects.yaml, even though the RPC layer already defines a clean MissionItem. The only options are duplicate the type into payloads or don't publish it.
Concrete motivating case: mission_current_seq publishes a bare TimestampedInt index that's only interpretable by re-indexing into a Mission downloaded via the VehicleMission RPC. A natural alternative — publishing the resolved MissionItem directly — is blocked by the transport split.
Proposal: factor out a third bucket, don't merge the two
The right dividing line is domain noun vs transport wrapper, not interface vs subject:
| Bucket |
Examples |
Shareable? |
| Domain types |
Coordinate, Waypoint, MissionItem, Route, Weather, VesselType |
Yes — referenced by both RPC files and subjects.yaml |
| RPC wrappers |
MissionUploadResponse, SetCurrentWaypointRequest (carry CommandResult / request-response semantics) |
No — RPC-only, never published |
| Pub/sub primitives |
TimestampedFloat, … |
pub/sub (already in the pool) |
interfaces/ keeps the request/response shapes, but their payload fields become shared domain types from the keelson pool.
Migration sketch
- Namespace. Put shared domain types in
package keelson (interfaces are currently package-less). Decide their home: extend messages/payloads/ or add messages/domain/.
- Consolidate the existing duplicates first (smallest concrete win): one
Coordinate (or standardise on foxglove.LocationFix), one Waypoint. Reconcile VehicleMission.Waypoint ↔ Route.Waypoint before they diverge further.
- Interfaces import domain types instead of redefining them (
VehicleMission imports keelson.MissionItem / keelson.Coordinate).
- Extend the SDK subject descriptor set to include the shared domain types (today payloads-only) — this is the change that makes them subject-referenceable. Mirror in the JS SDK generator.
Cautions / open questions
- Coupling is real but correct. A shared type means a field add touches both the RPC contract and any subject publishing it — RPC and pub/sub then version together. Acceptable, but more coordination.
- Keep the discipline. Request/response wrappers must stay un-publishable; "domain noun yes, transport wrapper no" is easy to erode.
- A good RPC type isn't automatically a good message.
MissionItem has no identity but its list index, so publishing one in isolation loses the context (its position in a Mission) that makes it meaningful. Sharing the type is fine; the publishing design must still carry context (index, mission id, …).
- Scope / sequencing. This is a foundational change to the
messages/ ↔ interfaces/ boundary. It should land after the in-flight 5.1.0V-trial split PRs settle, and start from the Coordinate/Waypoint consolidation rather than a big-bang reorg.
- Open question: do we want a single canonical coordinate type repo-wide (
foxglove.LocationFix vs a keelson Coordinate), given foxglove types carry visualization-oriented fields (covariance, frame_id) that are noise for a route waypoint?
Why now
The Route/Voyage/Weather payloads (PRs #141/#142) roughly doubled the domain-type surface and immediately collided with the vehicle RPC types (the two Waypoints). Left alone, the duplication compounds with every new domain area. Good moment to decide the direction even if the implementation is deferred.
🤖 Generated with Claude Code
Problem
Keelson partitions protobuf types by transport:
interfaces/*.proto— RPC request/response types (package-less, global namespace)messages/payloads/*.proto— pub/sub payload types (package keelson)subjects.yamlmaps a subject to a type, and the SDK resolves it from a descriptor set built only frommessages/payloads/(sdks/python/keelson/__init__.pyloadspayloads/protobuf_file_descriptor_set.bin).But a
Coordinate,Waypoint,MissionItem,Routeis a domain noun — neither inherently RPC nor pub/sub. Partitioning domain nouns by transport forces the same concept to be modelled twice, and makes a type defined for one transport unusable on the other.This is already biting (not hypothetical)
Two
Waypointmessages now exist, modelling the same thing with different coordinate types:interfaces/VehicleMission.proto:Waypoint/Loiterbuilt onCoordinate { double latitude_deg; double longitude_deg }(inVehicleCommon.proto).messages/payloads/Route.proto:Waypoint/Legbuilt onfoxglove.LocationFix.And because the subject type pool is payloads-only, you cannot publish an interface type today: e.g. there's no way to add
current_mission_item: keelson.MissionItemtosubjects.yaml, even though the RPC layer already defines a cleanMissionItem. The only options are duplicate the type into payloads or don't publish it.Concrete motivating case:
mission_current_seqpublishes a bareTimestampedIntindex that's only interpretable by re-indexing into aMissiondownloaded via theVehicleMissionRPC. A natural alternative — publishing the resolvedMissionItemdirectly — is blocked by the transport split.Proposal: factor out a third bucket, don't merge the two
The right dividing line is domain noun vs transport wrapper, not interface vs subject:
Coordinate,Waypoint,MissionItem,Route,Weather,VesselTypesubjects.yamlMissionUploadResponse,SetCurrentWaypointRequest(carryCommandResult/ request-response semantics)TimestampedFloat, …interfaces/keeps the request/response shapes, but their payload fields become shared domain types from thekeelsonpool.Migration sketch
package keelson(interfaces are currently package-less). Decide their home: extendmessages/payloads/or addmessages/domain/.Coordinate(or standardise onfoxglove.LocationFix), oneWaypoint. ReconcileVehicleMission.Waypoint↔Route.Waypointbefore they diverge further.VehicleMissionimportskeelson.MissionItem/keelson.Coordinate).Cautions / open questions
MissionItemhas no identity but its list index, so publishing one in isolation loses the context (its position in aMission) that makes it meaningful. Sharing the type is fine; the publishing design must still carry context (index, mission id, …).messages/↔interfaces/boundary. It should land after the in-flight5.1.0V-trialsplit PRs settle, and start from theCoordinate/Waypointconsolidation rather than a big-bang reorg.foxglove.LocationFixvs a keelsonCoordinate), given foxglove types carry visualization-oriented fields (covariance, frame_id) that are noise for a route waypoint?Why now
The Route/Voyage/Weather payloads (PRs #141/#142) roughly doubled the domain-type surface and immediately collided with the vehicle RPC types (the two
Waypoints). Left alone, the duplication compounds with every new domain area. Good moment to decide the direction even if the implementation is deferred.🤖 Generated with Claude Code