Disclaimer: Independent product concept by Kaushal Khodifad. Not a live commercial product.return to portfolio
Disclaimer: Independent product concept by Kaushal Khodifad.
Traverse - technical write-up

The parts, the schema, and the arithmetic that has to hold.

Traverse exists because a 15 to 25 percent gap between contracted and actual area is mostly measurement physics, not careless field work. That reframing has consequences all the way down: the error model decides the acceptance bands, the bands decide the anomaly rules, the offline queue decides the conflict semantics, and the handset's GNSS class turns out to be a column on every row rather than a procurement footnote. This page is the build.

Nothing below is a screenshot of a number. Every figure is computed at render time by the engine in src/lib/traverse, aggregated from the seeded fixture world, or parsed out of the committed migration. Where a figure is an assumption or a study input rather than engine output, it says so on the line where it appears.

7pure engine modules, zero runtime dependencies
8 / 116schema tables and columns, parsed live
6lifecycle stages, each with its own band
10anomaly rules, every threshold derived
7.0xarea-error reduction, no external hardware
1 - System architecture

Five parts, one of them on both sides of the network

The interesting property of this system is not any single component. It is that the boundary between the phone and the server runs through the middle of the product, that the boundary is usually closed, and that the thing which decides whether a number is acceptable is the one module that lives on both sides of it.

HANDSET - WORKS WITH NO NETWORK AT ALLSERVER - SERVICE ROLE ONLYnetwork boundary - available twice a weekenqueue(op)pending, seq ascPOST /api/traverse/syncinsert, guardedaccepted | 23505SyncResponsestatus updateCapture1
Walk the boundary, record the uncertainty with it
Outbox2
Durable op log, ordered by seq and only by seq
Drain3
Send in seq order, roll back whole on failure
Sync API4
Validate, probe, apply, answer per op
Postgres5
The conflict detector is a constraint, not code
Engine - src/lib/traversegeo - accuracy - reconcile - topology - anomaly - route - selftestPure, deterministic, zero dependencies. The same module computes the band the phone shows and the band the server applies.same tolerance, offlinesame tolerance, on the server
2 / 6
Outbox - write-ahead logOn the handset, durable
What it owns
  • Appends one op per capture, amendment, note or conflict decision. Nothing is mutated in place.
  • Allocates a monotonic, gap-free seq inside the same IndexedDB transaction as the write, so two tabs cannot collide.
  • Derives the conflict scope key as plotId:stage. Two technicians sharing one is exactly the case the system exists to catch.
  • Restores an op left inflight by a page closed mid-upload back to pending on the next init.
On the wire
enqueue(op)capture to outbox

One capture becomes one capture.create op carrying the ring, the origin, the per-vertex sigma array and the fix source. The enqueue is idempotent when the caller supplies the id, so a retried save cannot double-write.

status updatedrain returns to outbox

Ops move to synced, conflict or failed. An op the server did not mention returns to pending and is resent, which is what makes a partial response safe.

pending, seq ascoutbox to drain

The drain reads pending ops in seq order. This is the only place ordering is decided, and it uses seq rather than any timestamp.

The guarantee it makes downstream
Ordering never depends on a device clock. Field handsets drift and are sometimes set by hand, so seq is the only ordering authority.
How it fails, said plainly
If the browser refuses IndexedDB (private mode, quota) the queue silently falls back to memory and reports storage: memory. The UI is then required to say the queue is session-only.
Where it lives
src/prototypes/traverse/lib/outbox.tssrc/prototypes/traverse/lib/useOutbox.ts
On the handset, durable
2 - The data model

A capture is an observation, never a fact

A plot never stores the boundary. It stores what the contract says and what the evidence says, in separate columns, because the gap between those two is the dispute and collapsing them into one field is how a dispute becomes invisible. Everything below is parsed out of the committed migration at render time, comments included.

8tables
116columns
44CHECK constraints
19indexes, 2 partial
8tables with RLS on
0policies, by design

Parsed from the migrationParsed at render time from src/lib/traverse/sql/0001_traverse_schema.sql - 507 lines, 25.0 KB, 37% of lines are comments. Byte-count assertion against the committed source: in sync.

Zero policies is the access model, not an omission
RLS on with no policies means every non-bypassing role reads zero rows and writes nothing, so there is no policy surface to get wrong later. Grants are revoked from anon and authenticated as well, so the failure mode is a permission error rather than an empty result that reads like missing data. The parser above counts 8 tables with RLS enabled and 0 policies created anywhere in the file.
traverse.capturesline 222 - 23 columns - 0 table constraints - 5 indexes

geometry is JSONB, matching CaptureGeometry in src/prototypes/traverse/lib/types.ts exactly: { ring: [{x,y}...], origin: {lat,lon}, sigmaM: [number...], fixSource: 'device'|'simulated'|'fixture', deviceClass: 'l1_single'|'l1_l5_dual', walkSeconds?: number } Coordinates are Club-local metres. There is no PostGIS dependency and no map library anywhere in this study: a ring of a few dozen local points is a JSON array, and every area, perimeter and adjustment is plain arithmetic.

Parsed from the migrationThe migration's own comment block for this table, carried through verbatim.

Show
idtext primary keyprimary key

Client-generated capture id. Also the server idempotency key: a batch replayed after a failed upload must not create a second boundary.

plot_idtext not null references traversenot nullreferences plots
club_idtext not null references traversenot nullreferences clubs
farmer_idtext not null references traversenot nullreferences farmers
stagetext not null checknot null
Vocabularyonboardingsowinggerminationpest_diseasepre_harvestpost_harvest
technician_idtext not null references traversenot nullreferences technicians
device_idtext not nullnot null
captured_attimestamptz not nullnot null

Two clocks, kept apart. Field handsets drift and are sometimes set by hand; ordering and audit use received_at, the field narrative uses captured_at, and neither one pretends to be the other.

received_attimestamptz not null default nownot null
geometryjsonb not null checknot null
Check(jsonb_typeof(geometry -> 'ring') = 'array' and jsonb_typeof(geometry -> 'sigmaM') = 'array')
area_acresnumeric(7,4)not null
Check(area_acres >= 0)
perimeter_mnumeric(9,2)
Check(perimeter_m >= 0)
vertex_countint not null checknot null
Check(vertex_count >= 3)
mean_sigma_mnumeric(5,2)not null
Check(mean_sigma_m >= 0)
fix_sourcetext not null checknot null
Vocabularydevicesimulatedfixture

Denormalised out of geometry so honesty is queryable, not just inspectable. A dashboard must be able to filter simulated rows out.

device_classtext not null checknot null
Vocabularyl1_singlel1_l5_dual
crop_codetext
notetext
revbigint not null defaultnot null
Check(rev >= 1)

Monotonic per (plot_id, stage). The client sends the rev it composed against; a mismatch is how a stale write is caught.

statustext not null defaultnot null
Vocabularyacceptedsupersededconflictedreconciled
superseded_bytext references traversereferences captures
op_idtext unique

The queue op that produced this row. Unique, so replaying the write-ahead log is idempotent end to end.

created_attimestamptz not null default nownot null
Indexes
captures_active_stage_uquniquepartial
create unique index captures_active_stage_uq on traverse.captures(plot_id, stage) where status = 'accepted';

THE CONFLICT DETECTOR, expressed as a constraint rather than as code. At most one ACCEPTED capture per plot + stage. A second technician writing the same plot and stage cannot quietly win: the insert fails and the server turns that failure into a conflict row. An amendment by the same technician supersedes the previous capture in the same transaction, which frees the slot.

captures_plot_idx
create index captures_plot_idx on traverse.captures(plot_id);
captures_club_stage_idx
create index captures_club_stage_idx on traverse.captures(club_id, stage);
captures_technician_idx
create index captures_technician_idx on traverse.captures(technician_id);
captures_received_idx
create index captures_received_idx on traverse.captures(received_at desc);
The hierarchy, and why the Club is the unit954 captures in this world
3Village
root of the projection

Geographic anchor. Carries the projection origin the whole area shares.

8Club
2.7 per village

15 to 25 farmers visited as ONE trip. The unit of routing, of sync, and of reconciliation.

160Farmer
20.0 per club

The contracting party. Personal data lives here and nowhere else.

179Plot
1.1 per farmer

One contracted holding. Holds the contract area and the reconciled estimate, separately.

954Capture
5.3 per plot

One observation of one boundary at one stage. Never the truth, always evidence.

Three separate reasons the Club is the aggregation boundary
  • Projection. Every capture in a Club is stored in metres east and north of one origin on clubs.origin_lat / origin_lon. Rings are only directly comparable inside that frame, which is what makes shared-edge topology arithmetic rather than geodesy.
  • Topology. A shared edge is only worth modelling between plots that abut, and plots abut inside a Club. plot_adjacency is Club-scoped for exactly that reason.
  • Operations. A Club is one trip and one sync. Ops for a whole Club arrive together, so reconciliation has a natural batch and the technician sees the result of a trip, not of a plot.
21farmers in the club
27plots
57.1 accontracted acres
52shared-edge pairs
3,649 mshared boundary
70%mean shared share of a plot's perimeter

Synthetic fixtureAggregated at render time over Kothapalle North, which holds 148 captures across those 27 plots. The shared-boundary share is the part of a plot's perimeter that a neighbour also walks, which is the part reconciliation gets for free.

Six stages of history, stored so comparison is cheapPLT-0001 - Ilaiah L.

There is no temporal table, no event log to replay and no history side-table. One capture row is one observation of one boundary at one stage, and the partial unique index captures_active_stage_uq keeps at most one of them ACCEPTED per plot and stage. Comparing stage 4 with stage 5 is therefore a single index scan on captures_plot_idx that returns at most six rows, with the geometry already in them.

History that is superseded is not deleted: an amendment marks the previous row superseded, links it with superseded_by and increments rev. A losing side of a conflict is kept as conflicted. The full arrival record lives in sync_log, one row per op with its outcome whether it was applied or not, so "the phone says it synced" is auditable without making the captures table carry two jobs.

OnboardingSowingGerminationPest / DiseasePre-HarvestPost-Harvest
StageRow stateArea, acvs contract
Onboardingaccepted9.117-9.7%
Sowingno row--
Germinationno row--
Pest / Diseaseaccepted8.720-13.7%
Pre-Harvestaccepted, low-confidence fix9.105-9.8%
Post-Harvestno row--
Contractplots.contracted_area_acres10.100-
What the spread between stages is, and is not
This plot has 3 of 6 stages captured and a spread of 0.397 acres between the widest and narrowest of them. One boundary did not change size six times. That spread IS the measurement error, which is why the product reconciles the stages instead of picking one of them.
How big a boundary actually isthe JSONB versus PostGIS question, as a number
10median vertices in a ring
54most vertices on any plot
152 Bmedian ring as JSON
33 KBall 179 boundaries together

Computed liveMeasured by serialising every fixture ring at the two-decimal-metre precision the wire contract sends. Range 61 B to 788 B, p90 327 B.

Why JSONB is the right call at this stage
A boundary is a few hundred bytes with a median of 10 vertices. Every operation the product performs on it - area, perimeter, closure, intersection, shared-edge extraction, the least-squares adjustment - already exists as pure TypeScript in src/lib/traverse, because it has to run on a handset with no network. Moving the authoritative geometry into the database would mean the number the technician is shown in the field is computed by different code from the number the settlement uses.
What would change it
A genuine cross-club spatial query, a containment or nearest-neighbour query that has to run in SQL, or plot counts past roughly 100k where scanning JSONB stops being free. The migration would then be additive: add a geometry column, backfill it from the JSONB, and keep the JSONB as the field-of-record so the offline path is untouched.
jsonb columns in the schema: 5no PostGIS extensionno map libraryno projection dependency
3 - Accuracy and validation

Bands that move with the plot and the instrument

A threshold that does not move with plot size flags small farmers first, and a threshold that does not move with the handset flags the technician who was issued the older phone. Both are ways of turning physics into a disciplinary matter. Every band here is the stage's sigma multiple applied to the error one capture of this plot on this handset can actually achieve.

Set the plot and the instrumentevery number below recomputes
0.5 to 10 acres is the real range in this contract book.
Handset
Capture method
Plot shape
12.59%one-sigma area error, one capture
255 m2standard deviation of the area
9.0effective independent fixes, of 72 recorded
3,777disputed value on this one plot, one capture

Computed liveareaErrorEstimate(0.5, 5, 72, "walk") from src/lib/traverse/accuracy.ts. Walked track, one fix every 2.5 m, error decorrelating over 7 m.

Why a small plot is the hard caseerror against plot size
0%6%13%19%25%0.5 ac2 ac5 ac10 ac
single-freq, sigma 5 m dual-freq, sigma 2.1 m
The curve is geometry, not effort
Area error scales with perimeter times sigma over area, so it rises as plots shrink. A technician walking a half-acre plot is not doing worse work than one walking a five-acre plot; they are fighting a larger number for the same care. Any acceptance band that does not move with plot size therefore flags the small farmers first.
The reduction ladder12.59% to 1.81%
A shared edge is observed by both neighbours, so its variance halves. This world's median plot shares 70.6% of its perimeter.
One walk, single-frequency handset12.59%1.0x

0.5 acre plot, sigma 5.0 m, 72 fixes at 2.5 m spacing, decorrelating over 7 m.

Reconcile 6 stage captures5.14%2.4x

6 independent looks at one boundary on 6 different days. Random error falls as 1/sqrt(N), so 2.45x. Assumes no shared path offset.

Dual-frequency L1+L5 handset2.16%5.8x

Sigma 5.0 m to 2.10 m. Area error is linear in sigma. Handset refresh, no external hardware, no new training.

Club shared-edge constraint1.81%7.0x

60% of the perimeter is shared with a neighbour and therefore observed twice. Variance halves on that portion: sqrt(1 - f/2).

7.0xtotal reduction, zero external hardware
3,777 to 542disputed value on a 0.5 ac plot

Computed liveEvery rung assumes the captures are of the same physical boundary. A plot that genuinely changed, for example a sub-let strip, is a data event, not a measurement error, and the anomaly rules must separate the two.

Per-stage acceptance bands0.50 ac - single-freq - sigma 5 m

Each band is the stage's sigma multiple times the error one capture of THIS plot on THIS handset can actually achieve. The multiple is a product decision justified by the decision the stage feeds; the achievable error is physics. Nothing here is a configured percentage.

Only post-harvest is judged against the reconciled estimate. Drag it down to one and watch the settlement band, and only that band, widen as evidence is taken away.
StageDecision it feedsGradekBand %Band, acValue at risk
Onboardingcontract-baselinecontract2.0025.180.1267,555
Sowinginput-allocationoperational2.0025.180.1267,555
Germinationcrop-presenceindicative3.0030.000.1509,000
Pest / diseaseadvisoryindicative3.0030.000.1509,000
Pre-harvestforecastoperational2.0025.180.1267,555
Post-harvestsettlementsettlement1.9610.080.0503,023

Onboarding. Contract baseline from a single walk. The band cannot be tighter than one walk's physics, so it is 2 sigma of exactly that, accepting 95% of honest captures.

Sowing. Sown extent issues physical inputs. 2 sigma keeps issue moving, because a false hold costs a return trip worth more than the input variance it catches.

Germination. The question is whether the crop emerged, which is temporal and needs no boundary resolution. The band is deliberately loose so it never generates a dispute.

Pest / disease. Affected extent drives spray volume at a small fraction of crop value. Indicative precision is the correct precision here.

Pre-harvest. Feeds yield forecast and procurement planning. An error here is a planning error, not a payment error, so 2 sigma is right.

Post-harvest. Settlement grade. Money moves on this number, so the band is the 95% interval, and it is taken on the reconciled estimate from every stage rather than on one walk. That is what makes it the tightest band on the plot.

Computed livetoleranceForStage(stage, 0.5, "single-freq"). Bands are clamped at a floor of 0.01 acre, about the value of the visit it would take to settle the argument, and a ceiling of 30%, past which a band stops being a band. Error decorrelates over 7 m, the model's one empirical parameter.

Study input, not engine outputThe rupee column is the band applied at the study's benchmark gross value of INR 60,000 per acre. That benchmark is carried as a study input rather than as engine output; the fixture's own per-crop economics put cotton at INR 62,050 an acre, so it sits inside this world's range, and the device case further down uses each plot band's own crop value instead of the flat figure.

4 - Anomaly rules

Ten rules, two families, and a bench that runs them

The measurement family asks whether a capture is internally sound and needs no second opinion, so it can run on the handset before any sync. The agreement family asks whether a capture disagrees with the contract, another stage or the reconciled estimate, so it needs the evidence assembled first. The bench below runs the real rule functions against captures the engine simulates from a known-good boundary, which is the only way to answer how often a rule flags honest work.

Measurement family6 rules

Is this capture internally sound. These run with no ground truth and no second opinion, on the capture alone, which is why they can run on the handset before any sync.

Walk did not closenon-closure

The technician physically returned to the corner they started on, so the gap is pure receiver error and is the one quality signal available with no ground truth.

Threshold 3.5 times sqrt(2) sigma, the Rayleigh spread expected when the start and end fixes are fully decorrelated. About one honest capture in 450.

Boundary folds across itselfself-intersection

A fold cancels part of the plot against the rest, so the number looks plausible and is wrong.

Threshold A fold cutting off at least 2% of the plot, between segments more than 20 m, or a tenth of the perimeter, apart along the walk. Short-range or trivial crossings are ordinary jitter and are ignored.

Too fast to have been walkedimplausible-capture-speed

A boundary recorded at vehicle speed was not walked, so the vertices are not on the boundary.

Threshold 2.5 m/s, comfortably above a brisk walk on a bund.

Too few fixes for a walksparse-vertices

A walk logged as four points is a sketch. It may be right, but it carries none of the evidence a walk is supposed to carry.

Threshold Fewer than 8 vertices per 100 m of boundary.

Duplicate capture for one stageduplicate-capture

Two captures of one stage usually means a failed sync and a re-walk. Counting both twice inflates the evidence and the confidence with it.

Threshold More than one capture of the same plot and stage.

Evidence quality went backwardsdevice-downgrade

A plot captured on a dual-frequency handset and then on a single-frequency one gets a wider settlement band for no field reason.

Threshold Any later capture on a lower device class than an earlier one.

Agreement family4 rules

Does this capture disagree with something else - the contract, another stage, or the reconciled estimate. These need a reference, so they run after the evidence is assembled.

Sown area exceeds contractsown-exceeds-contract

Inputs are issued per acre, so an over-stated sown area issues seed and fertiliser that the contract never bought.

Threshold Contracted area plus the sowing-stage band for this plot size and handset.

Area jumped between stagesstage-area-jump

One boundary should not change size mid-season. A real jump means a sub-let strip, a re-survey or the wrong plot.

Threshold Combined band of the two stages, added in quadrature because the two errors are independent.

Outside the band this handset can holdoutside-device-tolerance

Separates a capture that disagrees with the record from one that is simply imprecise.

Threshold The stage band from accuracy.ts, computed for ONE capture, since that is what is being judged. Plot size and handset class are already inside it.

Reconciled area disagrees with the contractsettlement-vs-contract

The one finding that decides what a farmer is paid. It runs on the reconciled estimate and quotes an interval, so a difference inside measurement spread is never raised as a difference.

Threshold The 95% interval of the reconciled area, which narrows as stages accumulate.

Rule bench - how often does this flag honest work200 simulated captures of a known-good boundary
Handset
seeds 41000 to 41199
13.5%honest captures that raised any flag
27of 200 simulated captures
23.8 mwidest closure gap in the run
34.5%worst area error in the run
A free validation the bench gives away
The closed-form model predicts a one-sigma area error of 12.59% for this plot and handset. The RMS of the 200 simulated walks just run is 12.73%, a 1.1% disagreement. Those are two independent routes to the same number - a covariance sum in closed form, and a seeded Monte Carlo of the walk - so agreement here is the model checking itself in front of you. Mean absolute error over the same runs is 10.39%, which is the expected sqrt(2/pi) of the one-sigma figure rather than a third answer.
RuleFamilyFiredRate
Sown area exceeds contractagreement00.0%
Area jumped between stagesagreement00.0%
Outside the band this handset can holdagreement63.0%
Reconciled area disagrees with the contractagreement00.0%
Walk did not closemeasurement00.0%
Boundary folds across itselfmeasurement2311.5%
Too fast to have been walkedmeasurement00.0%
Too few fixes for a walkmeasurement00.0%
Duplicate capture for one stagemeasurement00.0%
Evidence quality went backwardsmeasurement00.0%

Computed liveEach row is runAnomalyRules from src/lib/traverse/anomaly.ts applied to a capture produced by simulateGpsWalk over a square plot whose true area is also the contracted area. The boundary is correct by construction, so every flag here is a false positive by definition. Captures are SIMULATED and labelled as such on the record.

Read this table the right way round
A rule that never fires on honest work is not automatically a good rule, and a rule that fires often here is not automatically bad: outside-device-tolerance is supposed to fire at roughly its stated confidence level, because that is what a band at that confidence means. What this bench is for is catching the rule whose rate does not match the threshold it claims, and watching what happens to every rate when the plot shrinks or the handset changes.
What this bench found, and the engine change it caused

This bench earned its keep. implausible-capture-speed used to fire on 18.5% of honest single-frequency walks of a 0.5-acre plot, and the mechanism was clear once the rate was on screen. The rule divided the perimeter of the RECORDED ring by the walk duration. At sigma 5 m with a fix every 2.5 m every step picks up jitter in both directions, so the recorded polyline is substantially longer than the boundary the technician actually walked. The numerator was noise-inflated, the denominator was not, and the quotient crossed 2.5 m/s without anybody getting into a vehicle. The rate collapsing on dual-frequency was the giveaway: that is the signature of a noise artefact, not of behaviour.

ruleImplausibleCaptureSpeed in src/lib/traverse/anomaly.ts now measures pace on a Douglas-Peucker-simplified track at roughly one sigma for the capture's own handset, using simplifyRing() from geo.ts. Simplification removes jitter while keeping every real corner, because a genuine turn displaces the track by many metres and noise displaces it by about one sigma, so the rule reads pace instead of reading jitter. The rate in the table above is the post-fix rate, computed live: change the handset and plot size and watch what it does now.

The point is not the patch. A rule that blamed eighteen honest technicians in every hundred was making exactly the mistake this whole study exists to argue against - reading a property of the measurement as a property of the person - and it was sitting inside our own rule set until a bench that runs on correct-by-construction boundaries put a number on it.

What this world actually contains, and what finds it65 planted records
Planted scenarioCountValue at riskResponsible detector
Sown area over contract122,29,434sown-exceeds-contract
Two technicians, one stage9-duplicate-capture
Pre-harvest walked on the wrong field65,95,560stage-area-jump
One claimant's edge pushed into the neighbour421,304Club topology check, not a per-plot rule
Stages never captured31-Stage coverage, not a rule - an absent row is not an anomalous one
Captures still on a handset3-Queue age, not a rule - the capture is fine, the backlog is not
Total658,46,298

Synthetic fixtureCounts and rupee figures are the seeded world's own planted scenarios. Three of the six are deliberately not anomaly rules: an overlap is a relationship between two plots and only the Club topology check sees it, a missing stage is an absent row rather than a wrong one, and a stale queue is a logistics problem with a perfectly good capture inside it. Filing all three as anomalies would put a supervisor in front of a list they cannot act on.

5 - Conflict semantics and idempotency

Two phones that cannot see each other, and a database that can

Every hard case in this system comes from the same fact: two technicians can capture the same plot and stage on the same afternoon with no way to know the other exists. The detector therefore has to live where both writes eventually arrive, the loser has to survive, and the decision has to be recordable with no signal.

Two technicians, one plot, one stage, no networkstep 4 of 8
Technician BSyncs from home that evening

The insert violates the partial unique index. That 23505 is not returned as an error; it is read as the signal it is, and the server fetches the sitting capture to build both sides of a conflict.

Mechanism 23505 on captures_active_stage_uq, converted not raised

The rule that makes all of this work
At most one accepted capture per plot and stage, enforced by captures_active_stage_uq. The database is the only component that sees both writers, so the detector belongs there and not in the route handler, where a race can open between the read and the write.
Technician A - outbox
  • #118capture.createPLT-0042:pre_harvestsynced
Technician B - outbox
  • #64capture.createPLT-0042:pre_harvestconflict
  • #65capture.createPLT-0043:pre_harvestsynced
Server - captures and conflicts
  • CAP-A-118pre_harvest rev 1accepted

Computed liveA deterministic replay of the contract in src/prototypes/traverse/lib and src/app/api/traverse/sync. No request is made and no row is written; the live queue lives on the Offline tab.

Every op gets its own answerone batch, per-op outcomes
What the server saidThe op becomes
listed in accepted[]synced

serverRev becomes the op's new baseRev, so the next amendment knows what it was composed against.

matched in conflicts[] on mine.opIdconflict

conflictId is attached and the op's scope stops draining until a decision syncs.

listed in rejected[]failed

The reason is stored on the op. It stays in the log; a rejected write is evidence, not rubbish.

not mentioned at allpending

attempts increments and the op is resent. This is what makes a partial or truncated response safe.

An upload that does not complete is not a failure
source: offline rolls every op in the batch back to pending and increments attempts. Nothing is half applied and nothing is lost. In this product that path is the common one, so the surface has to render it as ordinary rather than as an error.
Idempotency, in three keysparsed from the migration

A batch that times out after the server committed it will be sent again. That is not an edge case on a queue with a 43-day tail, it is a weekly event. Three uniqueness constraints make the replay return the same acceptance instead of writing a second boundary.

captures.op_idtext unique
sync_log_op_uq
create unique index sync_log_op_uq on traverse.sync_log(op_id);
sync_log_device_seq_uq
create unique index sync_log_device_seq_uq on traverse.sync_log(device_id, seq);

Parsed from the migrationFound by the parser as unique constraints on op identity. The unique (device_id, seq) pair is what makes a GAP in a device's sequence visible instead of silent.

What that costs in practice99 sync sessions in this world
43captures still on handsets, 4.5% of all
88 hmedian time from capture to server
43.1 doldest op still queued
14sessions that carried a conflict
10.5 MBtotal payload moved

Synthetic fixtureAggregated from the seeded world's sync sessions. The tail matters more than the median: a design that only works when the median op syncs in a day would strand the 43 captures currently sitting on handsets, and the disputed value attached to the plots they belong to is 28,98,068 across the book.

6 - Device strategy

A handset refresh, not a second device to carry

Dual-frequency L1+L5 reached mid-tier Android from 2022, which roughly halves horizontal sigma and therefore roughly halves area error. The argument for it over an external receiver is not that a phone is more accurate. It is that reconciling six stage captures already removes most of what the extra hardware would buy, and it does so without a second thing to charge, pair, train on and replace.

The fleet, as a data attribute3 models in service, 3 technicians
HandsetGNSSConstellationsSigma, mError on 0.5 acCostRole
Entry Android (2020 class)
DEV-A - Android 11 - Ramulu M.
L1GPS, GLONASS5.614.4%8,500oldest handset in the fleet
Mid Android (2021 class)
DEV-B - Android 12 - Srikanth B.
L1GPS, GLONASS, NavIC4.912.3%10,500fleet workhorse
Mid Android (2023 class)
DEV-C - Android 14 - Anusha P.
L1+L5GPS, Galileo, NavIC2.45.6%13,900only dual-frequency handset in the fleet this season
Mid Android (2024 class)
DEV-D - Android 15
L1+L5GPS, Galileo, BeiDou, NavIC2.14.9%15,200refresh candidate, not yet issued to any technicianrefresh candidate

Computed liveThe error column is expectedAreaErrorPct(0.5, sigma, 72) from the engine. Handset sigma, price and role come from the synthetic fixture catalogue.

Study input, not engine outputDual-frequency L1+L5 GNSS on mid-tier Android reaches roughly 1.75 to 3 m horizontal accuracy, against 5 to 8 m for single-frequency L1. Carried as a study input. The simulator uses 2.1 to 2.4 m and 4.9 to 5.6 m, inside these bands. The engine's own constants are SIGMA_BY_DEVICE = single-freq 5 m, dual-freq 2.1 m.

Why device class is a column on every capture, and not a constant
Sigma is the largest single term in the error of the number a farmer is paid on, and this fleet is mixed: 2 single-frequency handsets and 1 dual-frequency one in service. Measured across this world's 936 captures, mean absolute area error is 10.08% on single-frequency and 5.66% on dual-frequency - the same technicians, the same fields, the same care. Carrying the class on the row is what lets every band, interval and anomaly threshold move with the instrument, and what lets an area be traced back to the instrument in a settlement meeting.
Handset refresh against an external receiveraccuracy computed, prices declared
Refresh target
This world runs 3 technicians on one technician-equivalent book. Scale it to your own.
Assumption. No source is claimed - set it to your own quote.
Assumption. Everything derived from it is marked.
45,600refresh 3 handsets to 2.1 m
1,35,0003 external receivers, your price
2,67,363disputed value moved out of the band per season
17%refresh capex as a share of one season's disputed value

Assumption you setThe two receiver figures are yours. Capex on both sides is unit price times fleet size and nothing else - no freight, duty, spares, insurance or recovery-from-leavers is modelled, and all of those fall on the external option rather than on a phone the operator was replacing anyway.

Computed liveRead the last tile carefully: disputed value at risk is the contract value sitting inside the measurement band, not cash the operator recovers. Narrowing the band does not pay that money into anyone's account. What it buys is that the argument gets smaller, fewer settlements need a second visit to resolve, and the share that does end up conceded is conceded over a smaller number. Calling this a payback period would be the kind of arithmetic this study is supposed to be arguing against.

Plot bandPlotsMean shared edgeError todayAfter refreshValue at risk todayRemoved
0.5 to 1 ac5570%3.38%1.42%1,13,83866,026
1 to 2 ac5270%2.08%0.88%1,35,49278,585
2 to 5 ac5769%1.22%0.51%1,61,86793,883
5 to 10 ac1550%0.66%0.28%49,77328,868
Whole book17971%--4,60,9702,67,363

Computed livePer plot-size band, each using that band's own mean plot size, mean shared-boundary share and mean stage coverage from the fixture book, run through errorReductionLadder twice - once with the fleet sigma on both rungs, once with the refresh target on the device rung. Rupees use each band's own mean gross value per acre from the crop mix, not a flat benchmark.

Devices a technician carries and charges
PhoneOne. The phone they already carry for the app, the call and the photo.
ReceiverTwo, and the second one is useless on its own.
New training
PhoneNone. The capture screen does not change.
ReceiverPairing, antenna placement, battery discipline, and what to do when it will not connect.
Failure in the field
PhoneA degraded fix is still a capture at the phone's own sigma, recorded honestly.
ReceiverA flat or unpaired receiver is either no capture at all, or a silent fall back to the phone with the wrong sigma on the record.
Asset register
PhoneAlready on a handset refresh cycle that exists for other reasons.
ReceiverA second asset class to buy, track, repair, insure and recover from leavers.
Best achievable accuracy
PhoneBounded by what a phone antenna can do.
ReceiverBetter, and with corrections better again.
What it fixes
PhoneRoughly halves sigma, which is roughly halves the area error.
ReceiverPushes sigma lower still - but the reconciliation mechanism already removes most of what is left.
The honest half of the argument
An external receiver at 0.8 m is genuinely more accurate than any phone: on a 0.5 acre plot it would carry about 1.83% single-capture area error against 4.88% for the refresh target. The argument against it is not that it is worse. It is that reconciling six stage captures with the Club topology already removes most of what the extra hardware would buy, and it does so without a second device to carry, charge, pair, train on, track and replace. If the reconciliation mechanism did not exist, the receiver would be the right answer.
7 - Scope

What ships, what waits for a named trigger, what is never built

Deferring something without stating the observable event that pulls it forward is not a scoping decision. Each deferred item below carries its trigger, and several carry the measurement from this book that shows the trigger has not fired yet.

Filter by theme
Ships in v18

The smallest set that makes a settlement number defensible.

Six-stage capture on the handset the technician already carriescapture

The capture is the product. Walk or corner-tap, per-vertex sigma recorded, stage band shown before the technician walks away.

Offline-first write-ahead outboxoffline

Zero connectivity is the normal case, not the edge case. Every write is an append to a durable local log before anything else happens.

Club-scoped least-squares reconciliation with shared-edge constraintsaccuracy

This is the error-reduction mechanism the whole thesis rests on. Six captures of one boundary plus a neighbour's captures of the shared edge are one small geodetic network, not six opinions.

Per-stage tolerance bands derived from plot size and device classaccuracy

A band that does not move with the plot and the handset is a band that flags physics as misconduct. Every threshold is computed, never configured.

Conflict detection in the database, with a two-sided resolution flowoffline

The database is the only place that sees both writers. Both captures survive and a person decides.

Ten anomaly rules, each with a derived threshold and a rupee figuresettlement

A flag a supervisor cannot price is a flag a supervisor learns to ignore.

Settlement pack: reconciled area, its 95% interval, and the captures behind itsettlement

Money moves on this number. It ships with its interval and its evidence or it does not ship.

Device register: every capture traceable to a handset and its catalogued sigmaplatform

An area that cannot be traced to the instrument that produced it cannot be defended in a settlement meeting.

Deliberately deferred8

Wanted, understood, and waiting on a named trigger.

PostGIS geometry columns and spatial indexesplatform

Every geometric operation this product needs is already pure TypeScript that runs offline on the phone. Moving the authoritative area into the database would put it somewhere the field cannot reach.

Trigger A genuine cross-club spatial query, a containment or nearest-neighbour query that has to run in SQL, or plot counts past roughly 100k where a sequential scan over JSONB stops being free.

Measured now: 179 plots and 324 shared-edge pairs across 8 clubs. Nothing here needs a spatial index.

Satellite temporal layer for emergence, stress and senescenceaccuracy

Satellite's real job here is temporal, and that needs no boundary resolution at all. It is deferred because it answers a different question from the one v1 has to answer first.

Trigger Once reconciled boundaries are trusted, a 5 to 10 day revisit can answer did the crop emerge, die or recover without a visit.

RTK or NTRIP corrections over cellularaccuracy

Sub-metre is not what a contract-farming settlement needs, and correction streams need the network the field does not have.

Trigger A customer whose use is land-record grade rather than contract grade.

Crop-level sub-polygons inside one plotcapture

Intercropping is real but it is a minority of plots, and a per-crop boundary multiplies the capture burden by the number of crops on the plot.

Trigger When the intercropped share of plots is large enough AND settlement moves to per-crop rather than per-plot. The fixture world's own intercropped share is shown beside this item.

Measured now: 18 of 179 plots carry more than one crop, 10.1%. Settlement in this book is per plot, so the trigger has not fired.

Photo and voice evidence attached to a capturecapture

Useful for triage, but it is bytes on a queue that already has a 38-day tail, and it does not make a single area number better.

Trigger When anomaly triage is limited by missing field context rather than by geometry.

Measured now: the queue already carries a 43-day tail and 43 unsynced captures on 10.5 MB of geometry alone.

Farmer-facing dispute portalsettlement

Opening a dispute channel before the measurement is trustworthy invites disputes the system cannot answer.

Trigger After the anomaly rules are shown, on real data, to raise fewer false disputes than the eye estimate did.

On-device reconciliation across a whole Clubaccuracy

Reconciliation is server-side in v1 because a Club's worth of geometry arrives together at sync anyway.

Trigger When a technician needs the reconciled number in the field, before a sync round trip, to settle an argument on the spot.

Multi-tenant organisation model, SSO and role hierarchyplatform

One operator, one contract book. Building a tenancy model before there is a second tenant is building a guess.

Trigger The second customer.

Measured now: 3 technicians, one contract book, one operator.

Non-goals8

Not a phasing decision. These are out because building them would make the product worse.

Verifying a boundary from 10 m satellite imageryaccuracy

Not a phasing decision, a physics one. At this plot size a 10 m pixel cannot separate one contracted holding from its neighbour, so using it to verify a boundary manufactures disputes that are not there. The separability figures are computed on this study's own geometry, below.

At 10 m this plot carries a time series (12 clean pixels) but not a boundary (12.8% area error, 40% of the area in mixed pixels). Measured on this study's own geometry: of 69 sub-half-hectare plots, 1 is separable at 10 m against 69 at 3 m, retaining 61.5% of area as unmixed pixels against 87.2%.

Determining land ownership or titlesettlement

Traverse measures the cultivated extent a contract is settled against. It is not a land record, it does not resolve title, and it must never be presented as either.

External GPS receivers, mapping-grade handhelds, or any second deviceplatform

A dual-frequency handset refresh beats external hardware on cost, on training, on breakage and on the number of things a technician has to remember to charge. The cost case is worked below.

A map library, a basemap or offline tile cachesplatform

There is no basemap worth showing at this resolution, tiles cannot be cached for a village round without a data budget nobody has approved, and what a technician needs to see is six rings and a neighbour's edge, not a photograph.

Automatic conflict resolutionoffline

A conflict is two people's work. The system's job is to preserve both sides, quantify the gap and put a person in front of it.

Forcing a technician onto a computed routeoffline

The route planner proposes an order and shows what it saves. Weather, a farmer who is out, and a road that is closed all beat the optimiser, and a tool that argues with the field gets switched off.

Yield predictionsettlement

A different product with a different data requirement. Traverse produces the area a yield model would need, which is a reason to keep them separate rather than to merge them.

Scoring technicians on their area errorcapture

The load-bearing non-goal. Most of the variance is the receiver, so a technician leaderboard on area error would rank people by the age of the handset they were issued. The device register exists to make that visible, not to make it personal.

Measured now: mean absolute area error is 10.08% on single-frequency handsets and 5.66% on dual-frequency ones, for the same people doing the same work.

The non-goal the rest of the product is built around
Most of the variance is the receiver, not the person holding it. A technician leaderboard on area error would therefore rank people by the age of the handset they were issued, and it would do it with a number that looks objective. The device register exists so that fact is visible to whoever sets the budget, not so it is visible to whoever sets the appraisal.

Synthetic fixtureThe measurement lines under individual items are computed from the seeded book at render time, so a trigger either has fired or has not - it is not a matter of opinion.

8 - Decision log

The calls, the alternatives, and what each one cost

Including the ones that are easiest to attack. Six entries carry a proof computed on this page rather than an assertion, because the two most load-bearing decisions here - hand-rolled SVG instead of a map library, and a plain local projection instead of a geodesy dependency - are exactly the two a reviewer should be sceptical about.

Decision log12 calls, 9 with a live proof
  • Why

    Every geometric operation this product performs - area, perimeter, closure, intersection, shared-edge extraction, the least-squares adjustment - is already pure TypeScript in src/lib/traverse, because it has to run on a handset with no network. Putting the authoritative geometry engine in the database would mean the phone computes one area and the server computes another, and the one the technician is shown in the field would not be the one the settlement uses. Rings here are small, the workload is per plot and per club, and there is no query in v1 that a spatial index would make possible.

    Rejected
    • PostGIS geometry(Polygon, 32644) with GiST indexes
    • GeoJSON in a text column with a check constraint
    • A separate vertices table, one row per point
    What it costs
    No spatial SQL. Anything geometric is a read plus a computation in application code, and a future cross-club query will be a sequential scan.
    What would change it
    A real cross-club spatial query, a containment or nearest query that has to run in SQL, or plot counts past roughly 100k. At that point the migration is additive: add a geometry column, backfill from the JSONB, keep the JSONB as the field-of-record.

    The parser found 8 tables, 116 columns, 44 CHECK constraints, 18 foreign keys, 19 indexes of which 5 are unique and 2 are partial, 5 JSONB columns, RLS enabled on 8 tables and 0 policies anywhere in the file.

    Parsed from the migrationRead from src/lib/traverse/sql/0001_traverse_schema.sql at render time. Byte-count assertion: in sync with the committed source.

9 - Limits

What this is not, stated before anyone has to ask

A technical write-up that only describes what works is a sales document. These are the boundaries of what the numbers on this page can support.

The world on this page is synthetic

Every farmer, club, plot, capture and rupee figure comes from a seeded generator. The district and state are real only as a geographic anchor; the villages and the people are not. Nothing here is a measurement of a real contract book.

The GNSS is simulated, and says so on every row

Captures are generated by an AR(1) error chain along the walk, tuned to the device sigma. That is a model of receiver error, not a recording of one, and the fix source column carries that on every record.

Five figures are study inputs, not engine output

Sentinel separability, the dual-frequency accuracy bands, 2022 handset availability, the 15 to 25 percent variance band, and the benchmark value per acre are carried as inputs with a note saying what still needs citing. They are listed explicitly so no surface can render them as something this engine derived.

The migration has not been applied

The schema is committed for review. Until it is applied against a project with a service-role key, every API route answers with source: fixture and a note naming the reason, and nothing is persisted anywhere.

The reconciliation figures in the fixture are an inverse-variance reference

The fixture world carries a reference reconciliation so it is internally coherent. The production least-squares engine with shared-edge constraints lives in src/lib/traverse and is what the Reconciliation surface runs; the two are labelled separately on purpose.

Disclaimer carried by the fixture world

SYNTHETIC DATA. Every company, village, club, farmer, plot boundary, capture, contract, rate and settlement in this file is fabricated for a product study. No real person, holding or agreement is represented. District and state names are real Indian administrative names used only to anchor the belt; village centroids are approximate synthetic points inside it and do not correspond to any actual settlement.

World clock 2026-03-14. 954 captures across 179 plots and 8 clubs, carrying 28,98,068 of disputed value on 24 plots.

The surfaces that exercise all of this live on the other tabs: the capture flow, the live queue and conflict resolution, and the reconciliation itself.

↑ Independent product concept by Kaushal Khodifad. Traverse is an independent product concept by Kaushal Khodifad; it is not a real company or a commercial product. It explores the lifecycle farm-plot capture for contract farming space. Not a live commercial product. Data is illustrative.

Return to portfolioOpen the live demo