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.
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.
- 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.
enqueue(op)capture to outboxOne 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 outboxOps 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 drainThe drain reads pending ops in seq order. This is the only place ordering is decided, and it uses seq rather than any timestamp.
src/prototypes/traverse/lib/outbox.tssrc/prototypes/traverse/lib/useOutbox.tsA 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.
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.
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.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.
Client-generated capture id. Also the server idempotency key: a batch replayed after a failed upload must not create a second boundary.
onboardingsowinggerminationpest_diseasepre_harvestpost_harvestTwo 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.
(jsonb_typeof(geometry -> 'ring') = 'array' and jsonb_typeof(geometry -> 'sigmaM') = 'array')(area_acres >= 0)(perimeter_m >= 0)(vertex_count >= 3)(mean_sigma_m >= 0)devicesimulatedfixtureDenormalised out of geometry so honesty is queryable, not just inspectable. A dashboard must be able to filter simulated rows out.
l1_singlel1_l5_dual(rev >= 1)Monotonic per (plot_id, stage). The client sends the rev it composed against; a mismatch is how a stale write is caught.
acceptedsupersededconflictedreconciledThe queue op that produced this row. Unique, so replaying the write-ahead log is idempotent end to end.
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.
create index captures_plot_idx on traverse.captures(plot_id);
create index captures_club_stage_idx on traverse.captures(club_id, stage);
create index captures_technician_idx on traverse.captures(technician_id);
create index captures_received_idx on traverse.captures(received_at desc);
Geographic anchor. Carries the projection origin the whole area shares.
15 to 25 farmers visited as ONE trip. The unit of routing, of sync, and of reconciliation.
The contracting party. Personal data lives here and nowhere else.
One contracted holding. Holds the contract area and the reconciled estimate, separately.
One observation of one boundary at one stage. Never the truth, always evidence.
- 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_adjacencyis 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.
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.
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.
| Stage | Row state | Area, ac | vs contract |
|---|---|---|---|
| Onboarding | accepted | 9.117 | -9.7% |
| Sowing | no row | - | - |
| Germination | no row | - | - |
| Pest / Disease | accepted | 8.720 | -13.7% |
| Pre-Harvest | accepted, low-confidence fix | 9.105 | -9.8% |
| Post-Harvest | no row | - | - |
| Contract | plots.contracted_area_acres | 10.100 | - |
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.
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.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.
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.
0.5 acre plot, sigma 5.0 m, 72 fixes at 2.5 m spacing, decorrelating over 7 m.
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.
Sigma 5.0 m to 2.10 m. Area error is linear in sigma. Handset refresh, no external hardware, no new training.
60% of the perimeter is shared with a neighbour and therefore observed twice. Variance halves on that portion: sqrt(1 - f/2).
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.
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.
| Stage | Decision it feeds | Grade | k | Band % | Band, ac | Value at risk |
|---|---|---|---|---|---|---|
| Onboarding | contract-baseline | contract | 2.00 | 25.18 | 0.126 | 7,555 |
| Sowing | input-allocation | operational | 2.00 | 25.18 | 0.126 | 7,555 |
| Germination | crop-presence | indicative | 3.00 | 30.00 | 0.150 | 9,000 |
| Pest / disease | advisory | indicative | 3.00 | 30.00 | 0.150 | 9,000 |
| Pre-harvest | forecast | operational | 2.00 | 25.18 | 0.126 | 7,555 |
| Post-harvest | settlement | settlement | 1.96 | 10.08 | 0.050 | 3,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.
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.
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.
non-closureThe 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.
self-intersectionA 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.
implausible-capture-speedA 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.
sparse-verticesA 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-captureTwo 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.
device-downgradeA 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.
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-exceeds-contractInputs 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.
stage-area-jumpOne 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-device-toleranceSeparates 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.
settlement-vs-contractThe 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 | Family | Fired | Rate |
|---|---|---|---|
| Sown area exceeds contract | agreement | 0 | 0.0% |
| Area jumped between stages | agreement | 0 | 0.0% |
| Outside the band this handset can hold | agreement | 6 | 3.0% |
| Reconciled area disagrees with the contract | agreement | 0 | 0.0% |
| Walk did not close | measurement | 0 | 0.0% |
| Boundary folds across itself | measurement | 23 | 11.5% |
| Too fast to have been walked | measurement | 0 | 0.0% |
| Too few fixes for a walk | measurement | 0 | 0.0% |
| Duplicate capture for one stage | measurement | 0 | 0.0% |
| Evidence quality went backwards | measurement | 0 | 0.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.
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.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.
| Planted scenario | Count | Value at risk | Responsible detector |
|---|---|---|---|
| Sown area over contract | 12 | 2,29,434 | sown-exceeds-contract |
| Two technicians, one stage | 9 | - | duplicate-capture |
| Pre-harvest walked on the wrong field | 6 | 5,95,560 | stage-area-jump |
| One claimant's edge pushed into the neighbour | 4 | 21,304 | Club topology check, not a per-plot rule |
| Stages never captured | 31 | - | Stage coverage, not a rule - an absent row is not an anomalous one |
| Captures still on a handset | 3 | - | Queue age, not a rule - the capture is fine, the backlog is not |
| Total | 65 | 8,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.
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.
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
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.- #118
capture.createPLT-0042:pre_harvestsynced
- #64
capture.createPLT-0042:pre_harvestconflict - #65
capture.createPLT-0043:pre_harvestsynced
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.
| What the server said | The 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.opId | conflict 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 all | pending attempts increments and the op is resent. This is what makes a partial or truncated response safe. |
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.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 uniquesync_log_op_uqcreate unique index sync_log_op_uq on traverse.sync_log(op_id);
sync_log_device_seq_uqcreate 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.
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.
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.
| Handset | GNSS | Constellations | Sigma, m | Error on 0.5 ac | Cost | Role |
|---|---|---|---|---|---|---|
| Entry Android (2020 class) DEV-A - Android 11 - Ramulu M. | L1 | GPS, GLONASS | 5.6 | 14.4% | 8,500 | oldest handset in the fleet |
| Mid Android (2021 class) DEV-B - Android 12 - Srikanth B. | L1 | GPS, GLONASS, NavIC | 4.9 | 12.3% | 10,500 | fleet workhorse |
| Mid Android (2023 class) DEV-C - Android 14 - Anusha P. | L1+L5 | GPS, Galileo, NavIC | 2.4 | 5.6% | 13,900 | only dual-frequency handset in the fleet this season |
| Mid Android (2024 class) DEV-D - Android 15 | L1+L5 | GPS, Galileo, BeiDou, NavIC | 2.1 | 4.9% | 15,200 | refresh 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.
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 band | Plots | Mean shared edge | Error today | After refresh | Value at risk today | Removed |
|---|---|---|---|---|---|---|
| 0.5 to 1 ac | 55 | 70% | 3.38% | 1.42% | 1,13,838 | 66,026 |
| 1 to 2 ac | 52 | 70% | 2.08% | 0.88% | 1,35,492 | 78,585 |
| 2 to 5 ac | 57 | 69% | 1.22% | 0.51% | 1,61,867 | 93,883 |
| 5 to 10 ac | 15 | 50% | 0.66% | 0.28% | 49,773 | 28,868 |
| Whole book | 179 | 71% | - | - | 4,60,970 | 2,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.
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.
The smallest set that makes a settlement number defensible.
The capture is the product. Walk or corner-tap, per-vertex sigma recorded, stage band shown before the technician walks away.
Zero connectivity is the normal case, not the edge case. Every write is an append to a durable local log before anything else happens.
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.
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.
The database is the only place that sees both writers. Both captures survive and a person decides.
A flag a supervisor cannot price is a flag a supervisor learns to ignore.
Money moves on this number. It ships with its interval and its evidence or it does not ship.
An area that cannot be traced to the instrument that produced it cannot be defended in a settlement meeting.
Wanted, understood, and waiting on a named trigger.
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'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.
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.
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.
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.
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.
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.
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.
Not a phasing decision. These are out because building them would make the product worse.
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%.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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 costsNo 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 itA 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.sqlat render time. Byte-count assertion: in sync with the committed source.
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.
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.
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.
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 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 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.
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.