152 lines
4.2 KiB
Markdown
152 lines
4.2 KiB
Markdown
# Design: enterprise-ports
|
||
|
||
## Module layout
|
||
|
||
```
|
||
packages/geospatial/logistics/ports/
|
||
package.json
|
||
tsconfig.lib.json
|
||
vite.config.ts
|
||
vitest.config.ts
|
||
README.md
|
||
src/
|
||
index.ts
|
||
constants/index.ts # EARTH_RADIUS_NM, DEFAULT_SPEED_KNOTS, VESSEL_CLASS_SPEEDS
|
||
types/
|
||
port.types.ts
|
||
port.types.md
|
||
error-codes/
|
||
error-codes.ts
|
||
error-codes.md
|
||
error-codes.test.ts
|
||
index.ts
|
||
schemas/
|
||
schemas.ts
|
||
schemas.md
|
||
schemas.test.ts
|
||
index.ts
|
||
otel/
|
||
otel.ts
|
||
otel.md
|
||
otel.test.ts
|
||
index.ts
|
||
data/
|
||
sea-ports-asia/
|
||
sea-ports-europe/
|
||
sea-ports-americas/
|
||
sea-ports-africa-mena/
|
||
sea-ports-oceania/
|
||
sea-ports/
|
||
airports/
|
||
rail-terminals/
|
||
all-ports/
|
||
shipping-lanes/
|
||
distance/
|
||
distance.ts # great-circle wrapper, nm/km conversions
|
||
distance.md
|
||
distance.test.ts
|
||
index.ts
|
||
find-port/
|
||
find-port.ts
|
||
find-port.md
|
||
find-port.test.ts
|
||
index.ts
|
||
nearest-port/
|
||
nearest-port.ts
|
||
nearest-port.md
|
||
nearest-port.test.ts
|
||
index.ts
|
||
spatial-index/
|
||
spatial-index.ts
|
||
spatial-index.md
|
||
spatial-index.test.ts
|
||
index.ts
|
||
lane-lookup/
|
||
lane-lookup.ts
|
||
lane-lookup.md
|
||
lane-lookup.test.ts
|
||
index.ts
|
||
lane-distance/
|
||
lane-distance.ts
|
||
lane-distance.md
|
||
lane-distance.test.ts
|
||
index.ts
|
||
lane-progress/
|
||
lane-progress.ts
|
||
lane-progress.md
|
||
lane-progress.test.ts
|
||
index.ts
|
||
eta/
|
||
eta.ts
|
||
eta.md
|
||
eta.test.ts
|
||
index.ts
|
||
property.test.ts
|
||
```
|
||
|
||
## Distance helper
|
||
|
||
`distanceNm(a, b)` and `distanceKm(a, b)` wrap
|
||
`@suaveplan/geo`'s `greatCircleDistance` against a WGS84-radius
|
||
spherical earth (semi-major axis 6_371_000 m) via `defineEllipsoid`. The
|
||
genesis `greatCircleDistance` returns metres by default; this package
|
||
converts via the geo `convertDistance` utility. We bind a single
|
||
`EARTH_SPHERE` ellipsoid once at module load — no allocation per call.
|
||
|
||
## Error mapping
|
||
|
||
Each main-branch `new PortError("msg", "CODE")` call maps to a typed
|
||
subclass:
|
||
|
||
| main code | genesis class |
|
||
|---|---|
|
||
| `NO_PORTS` | `PortsNoPortsError` |
|
||
| `INVALID_RADIUS` | `PortsInvalidRadiusError` |
|
||
| `INSUFFICIENT_WAYPOINTS` | `PortsInsufficientWaypointsError` |
|
||
| `INVALID_SPEED` | `PortsInvalidSpeedError` |
|
||
| `MISSING_SPEED` | `PortsMissingSpeedError` |
|
||
| _(new)_ | `PortsPortNotFoundError` — strict variant of `findPort` |
|
||
| _(new)_ | `PortsLaneNotFoundError` — strict variant of `getShippingLane` |
|
||
| _(new)_ | `PortsInvalidLaneError` — Zod schema rejected |
|
||
|
||
All extend `PortsError extends SuaveplanError<PortsCode>` with i18n
|
||
message keys.
|
||
|
||
## Spatial index
|
||
|
||
`PortSpatialIndex` keys ports by `floor(latitude / 5)` and
|
||
`floor(longitude / 5)` into a `Map<string, Port[]>`. Query for a
|
||
coordinate fetches the 3×3 cell neighbourhood and runs exact great-circle
|
||
distance on the candidate set. For ~120 curated ports this is overkill in
|
||
absolute terms but keeps the public API stable as the dataset grows.
|
||
|
||
## OTel instruments
|
||
|
||
Module-scope meter `@suaveplan/ports@0.1.0`:
|
||
|
||
- Counter `ports.lookup_total` — labels `kind=findPort|byCountry|byType|search`.
|
||
- Counter `ports.lane_lookup_total` — labels `mode=sea|rail|air|all`.
|
||
- Counter `ports.eta_total` — labels `confidence=high|medium|low`.
|
||
- Histogram `ports.find_nearest_duration_ms`.
|
||
- Histogram `ports.lane_distance_duration_ms`.
|
||
- Histogram `ports.eta_duration_ms`.
|
||
|
||
`refreshPortsMeters()` re-resolves every instrument against the current
|
||
global `MeterProvider` after tests wire in-memory exporters.
|
||
|
||
## Clock injection
|
||
|
||
`estimateETA(coord, lane, speedKnotsOverride?, opts?)`:
|
||
`opts = { departureTime?: Date; clock?: Clock }`. If `departureTime` is
|
||
omitted and `clock` is supplied → `new Date(clock.now())`; else `new
|
||
Date()`. Tests pass a `FakeClock` from `@suaveplan/testing` for
|
||
determinism.
|
||
|
||
## Data shape stability
|
||
|
||
The migrated dataset preserves every record from the main branch
|
||
(coordinates, throughputs, vessel limits, IATA/UIC codes) — adding any
|
||
new fields would break consumer expectations. The data is split into
|
||
per-region files exactly mirroring the main layout so future curated
|
||
updates can be diffed cleanly.
|