DEV Community

Turfano: when the AI helps you admit the first version was wrong

What Turfano is

Turfano is a faithful .NET port of TurfJS — the most widely used geospatial analysis library in JavaScript — with its own GeoJSON types and zero external dependencies in the core package.

using Turfano.GeoJson;

var origin = new Position(-122.4194, 37.7749);
var destination = new Position(-122.4094, 37.7849);

var distance = Geo.Distance(origin, destination); // Turfano.Units.Length
Console.WriteLine($"{distance.Kilometers:F3} km");

var union = Geo.Union(square, overlappingSquare); // Turfano.GeoJson.Geometry?
Enter fullscreen mode Exit fullscreen mode

That's today. Because last October's version was a completely different project — and the difference is the story worth telling.

October 2025: 63% parity, built on top of NetTopologySuite

Before it was called Turfano, the project was called DotTerritory: a wrapper that translated TurfJS's functions using NetTopologySuite (NTS) for geometry types and UnitsNet for quantities. An efficiency report generated by another AI (Devin, October 2025) documents the state of the art back then: 72 of the 114 functions in the @turf index implemented — 63% coverage.

That wasn't nothing. But it was a wrapper — and a wrapper means inheriting the semantics of whatever you're wrapping, not the semantics of whatever you're porting.

The problem that only shows up when you compare against the real source

NTS is an excellent topology engine. But it resolves the same geometric operations with subtly different rules than the original TurfJS: polygon boundary handling, whether the centroid includes the closing vertex, whether nearestPointOnLine is planar or geodesic. Swapping the implementation underneath (re-typing logic that already existed on top of NTS) isn't safe — that's documented, in plain terms, in the spec for the feature that pulls the NTS engine out of the overlay path (specs/009-nts-engine-exit):

When porting functions (parity waves), re-typing existing logic is not safe: validating against the real @turf caught bugs in the NTS-based code (RhumbDestination with the wrong q; nearestPointOnLine planar vs. geodesic; centroid including the closing vertex; Boolean* predicates using NTS boundary semantics instead of Turf's). Always validate each function against the real @turf.

That's the lesson logged — in the project's own lessons-learned file — right after a simpler, earlier one: avoid cryptic acronyms in names (no D2R, no Rtan; prefer RadiansPerDegree, RightTangent — except well-known domain terms, like BBox, lon/lat, and the geodesic notation phi/lambda).

The rewrite: port the real algorithm, not someone else's translation

The decision was to throw away most of the existing surface and port the actual algorithms, validating every single function against the real TurfJS TypeScript source (vendored under reference/, run with Bun):

  • Overlay (Union/Difference/Intersect/Dissolve): a port of the Martinez–Rueda engine via polyclip-ts — the very source @turf itself runs — instead of delegating to NTS.
  • Isolines/isobands: marching squares faithful to TurfJS.
  • Clustering: k-means with deterministic centroids (the first k points), for reproducibility — no hidden randomness.
  • Buffer: the one operation that stays NTS-bound on purpose, isolated in an optional satellite package.

The result, across two identity rewrites: the package became Turfano (it was DotTerritory) and the main class became Turf (it was Territory, to match TurfJS's own naming — Turf.Area(), Turf.Distance()).

Today: 100% of the @turf index covered by the Geo facade, 295+ tests, 1.0.0-rc.1, zero external dependencies in the core. NTS didn't disappear — it became the Turfano.NetTopologySuite satellite package, for anyone who needs Buffer or already lives in that ecosystem (a public ToNts/FromNts bridge, with a packed-sequence boundary — zero materialized Coordinate objects).

Two bonus finds while re-reading the repo to write this

The lessons-learned file also records that running a subagent editing files in the same working tree while the orchestrator was doing checkout/commit in parallel caused a git race — the documented fix is to isolate that subagent in its own git worktree. The AI documented its own operational mistake so it wouldn't repeat it.

And the project's constitution file (the spec-kit governance "manifesto," .specify/memory/constitution.md) still has the example placeholders — [PRINCIPLE_1_NAME], [GOVERNANCE_RULES] — never filled in. The AI is rigorous enough to validate every ported function against the source; filling out the "why this project exists" template, not so much.

The fast convergence — and what it doesn't replace

The idea of "fast convergence between different systems" (getting a JS ecosystem and a .NET ecosystem to agree, function by function, on the same result) only worked here because every port was checked against the real source — never against the intuition of "it should be similar to what we already had." AI massively speeds up the work of porting the 42 functions that were missing. It doesn't speed up the part where you realize the 63%-covered foundation you already built had bugs too subtle to trust without re-checking — that still takes someone willing to throw away what "mostly worked."

A note on how this was built

Turfano was built with OpenCode running GLM-5.2 — a different tool-and-model pairing than the one used for MySheet (Claude Opus/Sonnet). Two different AIs, two different spec-kit workflows, and the same pattern underneath: a history only becomes a tellable story once someone — human or AI — bothers to write down the why, not just the what.


Turfano is open source. Full code, complete @turf index coverage, and the specs for every parity wave: github.com/danfma/Turfano.

Top comments (0)