# 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 into `seaPorts`; plus `airports` and `railTerminals`. `allPorts` combines every category. - Curated `shippingLanes` waypoint 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) 1. **Typed error registry** — `PORTS_CODES` via `defineCodes("PORTS", ["NO_PORTS", "INVALID_RADIUS", "INSUFFICIENT_WAYPOINTS", "INVALID_SPEED", "MISSING_SPEED", "INVALID_LANE", "PORT_NOT_FOUND", "LANE_NOT_FOUND"])`. Each code surfaces a dedicated `PortsError` subclass extending `SuaveplanError`. 2. **Zod schemas** for `Port`, `ShippingLane`, `SphericalCoord`, `LaneProgress`, `ETAEstimate`, `NearestPortResult` exposed under `./schemas/schemas` for boundary validation by API consumers. 3. **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. 4. **Vessel-class speed defaults** — `VESSEL_CLASS_SPEEDS` constants for `ulcv`, `panamax`, `feeder`, `coastal`, `freighter-air`, `rail-freight` used as fallbacks when neither the lane nor caller specifies a speed. 5. **OTel instrumentation** — counters `ports.lookup_total{kind}`, `ports.lane_lookup_total{mode}`, `ports.eta_total{confidence}`; histograms `ports.find_nearest_duration_ms`, `ports.lane_distance_duration_ms`, `ports.eta_duration_ms`. `refreshPortsMeters()` for test isolation. 6. **Spatial index** — `PortSpatialIndex` bucketed by 5-degree lat/lng cells, providing O(neighbours) `findNearest`/`findInRadius` instead of scanning all ~120 ports. Used internally by the public functions. 7. **Property-based tests** — fast-check invariants for ETA monotonicity (speed↑ ⇒ ETA earlier), distance triangle inequality on great-circle segments, and radius-containment consistency (`findPortsInRadius` never returns a port whose haversine distance exceeds `radiusKm`). ### 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 `ports` capability spec. - **Affected code**: new package at `packages/geospatial/logistics/ports/`. - **Downstream**: unblocks `port-tracking`, `logistics`, `customs`, `procurement`, `warehouse-ops`, and their `api-plugin-*` companions. - **Manifest**: `MANIFEST.md` regenerated; `STATUS.md` flips `–` → `✅`. - **Category mapping**: adds plugin `logistics` to super-category `geospatial` in `scripts/folder-reorg/assignment.yml` (`logistics` was previously a plugin without a category assignment). The 18 closed super-categories are unchanged.