Files
2026-05-27 23:29:53 +01:00

5.4 KiB

Tasks: enterprise-geo

Change ID: enterprise-geo Package: @suaveplan/geo Target path: universal/geo/geo Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/retry


0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Scaffold

  • Create universal/geo/geo/ with src/, tests/, src/__benchmarks__/ and all feature subdirectories per the layout in proposal.md.
  • Write package.json at version 0.1.0 with "type": "module", "sideEffects": false, correct exports map, dependencies, and devDependencies.
  • Write tsconfig.json extending @suaveplan/typescript-config/library.json.
  • Write vitest.config.ts using createTestConfig() from @suaveplan/testing/config.
  • Add lint, typecheck, test, test:coverage, build, docs:check scripts.

2. Types + errors

  • src/types/common.ts — LatLng, LatLngAlt, BoundingBox, GeocodeAdapter (interface), GeocodeResult, AddressComponents, GeocodeOptions, ReverseGeocodeOptions, CacheOptions, SearchOptions.
  • src/types/common.md (≥200 words, both examples, API reference).
  • src/error-codes.ts — GeoErrorCode (6 codes), metadata, register, eager call.
  • src/constants/index.ts — EARTH_RADIUS_M = 6_371_000, DEG_TO_RAD = Math.PI / 180.
  • src/types/schemas.ts + schemas.md — Zod schemas.

3. Core modules

  • src/distance/haversine/ — haversineDistance(a, b): number: validates coordinates (throws GEO_INVALID_COORDINATE if out of range), applies Haversine formula. bearing(from, to), midpoint(a, b), destination(from, bearing, distanceM). Tests: London→Paris distance ≈138 000 m ±100 m, same point = 0, antipodal ~20 000 000 m, bearing N/E/S/W, midpoint symmetric.
  • src/distance/vincenty/ — vincentyDistance(a, b): number: Vincenty direct formula for more accurate long-distance calculation; degrades to Haversine for antipodal inputs. Tests: same reference cities as haversine; compare to Haversine — should differ by < 1% for ≤500 km, potentially more for longer.
  • src/geometry/bounding-box/ — boundingBoxFromPoints, expandBoundingBox, boundingBoxesIntersect, pointInBoundingBox. Tests: single point, two-point expansion, crossing antimeridian (handled by clamping), intersect/disjoint, point on edge.
  • src/geometry/polygon/ — polygonArea(ring), pointInPolygon(point, ring). Tests: unit square area = 1 m² (approx), large polygon, point inside/outside/on edge (treated as inside), concave polygon.
  • src/geometry/geojson/ — toGeoJsonPoint, toGeoJsonLineString, toGeoJsonPolygon, fromGeoJsonPoint. Tests: round-trip, valid GeoJSON output.
  • src/clustering/grid-cluster/ — gridCluster(points, cellSizeMetres): buckets points by grid cell; returns array of clusters (each cluster = array of points). Tests: all same cell → 1 cluster, scattered → N clusters, cell size covers all → 1 cluster.
  • src/clustering/kmeans-cluster/ — kMeansCluster(points, k, maxIterations): standard k-means with random initialisation (seeded via injectable RNG for tests), Haversine distance metric. Tests: k=1 returns all in one cluster, k=N with N distinct points returns N clusters, convergence within maxIterations.
  • src/geocoding/geocode-adapter/ — GeocodeAdapter interface + GeocodeResult type + Zod schemas.
  • src/geocoding/cached-geocode/ — createCachedGeocodeAdapter(inner, options): LRU cache (Map with insertion-order eviction, size limit). Tests: cache hit on second call, cache miss calls inner, TTL expiry triggers re-fetch.

4. OTel instrumentation

  • Span geo.geocode with attribute geo.provider; counter geo.geocode_total; histogram geo.geocode_duration_ms.

5. Property-based tests (tests/property.test.ts)

  • Distance symmetry: haversineDistance(a, b) === haversineDistance(b, a) for arbitrary valid coordinates.
  • Bounding-box expansion monotonicity: expandBoundingBox(box, d) ⊇ expandBoundingBox(box, d-1) for all d > 0.
  • Point-in-polygon consistency: pointInPolygon(p, ring) is consistent with winding direction for arbitrary convex polygons.

6. Benchmarks (src/__benchmarks__/geo.bench.ts)

  • haversineDistance 1 000 000 calls.
  • gridCluster for 10 000 random points.
  • pointInPolygon for a 100-vertex polygon 100 000 calls.

7. Docs + verify

  • One .md per implementation file (≥200 words, both examples, API reference). Root README.md.
  • bunx turbo lint typecheck test --filter=@suaveplan/geo — green.
  • bunx turbo test:coverage --filter=@suaveplan/geo — 100%.
  • bun scripts/publish.ts --package @suaveplan/geo.
  • Archive: openspec/archive/2026-04-24-enterprise-geo/.