Files
suaveplan/openspec/archive/2026-05-25-enterprise-ports/proposal.md
2026-05-27 23:29:53 +01:00

106 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.