5.3 KiB
OpenSpec Change: enterprise-ports
- Change ID: enterprise-ports
- Package:
@suaveplan/ports - Category / plugin:
geospatial/logistics - Target path:
packages/geospatial/logistics/ports/ - Status: PROPOSED
- Date: 2026-05-25
- Reference spec:
/mnt/s/web/suaveplan/enterprise/ports.md - Main-branch source:
/mnt/s/web/suaveplan/packages/universal/ports
Why
@suaveplan/ports is the canonical universal-tier global port database
on the main branch. It pairs a curated dataset of sea ports, airports,
and rail terminals (UN/LOCODE addressed) with shipping-lane waypoint
graphs and ETA estimation. The genesis branch starts empty; landing this
package unblocks every logistics/shipping consumer (port-tracking,
logistics, customs, procurement, warehouse-ops, and the matching
api-plugin-* packages) which all transitively depend on port lookups
and lane-based routing.
The reference implementation on main shipped at 95%+ coverage but
without the cross-cutting concerns mandated by the enterprise checklist
in enterprise/ports.md: stable typed error codes via
@suaveplan/error, Zod schemas at the public boundary, OpenTelemetry
instrumentation, injectable Clock, and 100% test coverage. This change
rebuilds the package from scratch on genesis with those additions while
preserving the full main-branch public surface so direct consumers do
not need migration shims.
What changes
Preserved (from main-branch surface)
- Curated port dataset split by region:
seaPortsAsia,seaPortsEurope,seaPortsAmericas,seaPortsAfricaMena,seaPortsOceania, aggregated intoseaPorts; plusairportsandrailTerminals.allPortscombines every category. - Curated
shippingLaneswaypoint graph spanning the major global routes (Asia–Europe via Suez, Asia–Europe via Cape, Transpacific, Transatlantic, rail and air lanes). - Port lookups:
findPort(locode),findPortsByCountry(country, ports?),findPortsByType(type, ports?),searchPorts(query, ports?). - Spatial lookups:
findNearestPort(coord, ports?),findPortsInRadius(coord, radiusKm, ports?). - Lane lookups:
getShippingLane(id),getShippingLanesByMode(mode, lanes?),getAllShippingLanes(). - Lane distance:
estimateLaneDistance(lane),cumulativeDistances(lane)(nautical miles, great-circle). - Lane progress:
findPositionOnLane(coord, lane)→{ progress, remainingDistanceNm, distanceTravelledNm, nearestWaypointIndex }. - ETA:
estimateETA(currentCoord, lane, avgSpeedKnots?, departureTime?)→{ remainingDistanceNm, etaHours, etaDate, confidence }. - Types:
Port,PortType,PortFacility,LaneMode,ShippingLane,LaneProgress,ETAEstimate,NearestPortResult.
Added (genesis enterprise additions)
- Typed error registry —
PORTS_CODESviadefineCodes("PORTS", ["NO_PORTS", "INVALID_RADIUS", "INSUFFICIENT_WAYPOINTS", "INVALID_SPEED", "MISSING_SPEED", "INVALID_LANE", "PORT_NOT_FOUND", "LANE_NOT_FOUND"]). Each code surfaces a dedicatedPortsErrorsubclass extendingSuaveplanError. - Zod schemas for
Port,ShippingLane,SphericalCoord,LaneProgress,ETAEstimate,NearestPortResultexposed under./schemas/schemasfor boundary validation by API consumers. - Injectable
Clock—estimateETA(coord, lane, speed?, opts?)now accepts{ departureTime?, clock? }so callers (jobs, simulation, tests) can supply a deterministic clock.Date.now()is never read at module scope. - Vessel-class speed defaults —
VESSEL_CLASS_SPEEDSconstants forulcv,panamax,feeder,coastal,freighter-air,rail-freightused as fallbacks when neither the lane nor caller specifies a speed. - OTel instrumentation — counters
ports.lookup_total{kind},ports.lane_lookup_total{mode},ports.eta_total{confidence}; histogramsports.find_nearest_duration_ms,ports.lane_distance_duration_ms,ports.eta_duration_ms.refreshPortsMeters()for test isolation. - Spatial index —
PortSpatialIndexbucketed by 5-degree lat/lng cells, providing O(neighbours)findNearest/findInRadiusinstead of scanning all ~120 ports. Used internally by the public functions. - Property-based tests — fast-check invariants for ETA monotonicity
(speed↑ ⇒ ETA earlier), distance triangle inequality on great-circle
segments, and radius-containment consistency (
findPortsInRadiusnever returns a port whose haversine distance exceedsradiusKm).
Removed
Nothing. The previous proposal's PortDatabase class and JSON-backed
"6000-port" dataset are not part of the main-branch surface and were
aspirational; this change ships the actual surface main shipped.
Impact
- Affected specs: new
portscapability spec. - Affected code: new package at
packages/geospatial/logistics/ports/. - Downstream: unblocks
port-tracking,logistics,customs,procurement,warehouse-ops, and theirapi-plugin-*companions. - Manifest:
MANIFEST.mdregenerated;STATUS.mdflips–→✅. - Category mapping: adds plugin
logisticsto super-categorygeospatialinscripts/folder-reorg/assignment.yml(logisticswas previously a plugin without a category assignment). The 18 closed super-categories are unchanged.