5.4 KiB
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 statusreports nothing) and no in-flight worktrees with uncommitted work (git worktree listthengit statusin 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.mdmatchesfind packages -maxdepth 3 -name package.jsoncount. Drift here means a prior change archived without regenerating the manifest.
1. Scaffold
- Create
universal/geo/geo/withsrc/,tests/,src/__benchmarks__/and all feature subdirectories per the layout inproposal.md. - Write
package.jsonat version0.1.0with"type": "module","sideEffects": false, correctexportsmap,dependencies, anddevDependencies. - Write
tsconfig.jsonextending@suaveplan/typescript-config/library.json. - Write
vitest.config.tsusingcreateTestConfig()from@suaveplan/testing/config. - Add
lint,typecheck,test,test:coverage,build,docs:checkscripts.
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 (throwsGEO_INVALID_COORDINATEif 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/—GeocodeAdapterinterface +GeocodeResulttype + 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.geocodewith attributegeo.provider; countergeo.geocode_total; histogramgeo.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)
haversineDistance1 000 000 calls.gridClusterfor 10 000 random points.pointInPolygonfor a 100-vertex polygon 100 000 calls.
7. Docs + verify
- One
.mdper implementation file (≥200 words, both examples, API reference). RootREADME.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/.