Index/Case study
Radar Autonomy
An agentic operating system with a product on top. The product decides where autonomous fleets should put their depots. The operating system underneath is what makes the product’s answers checkable — a governed control plane that routes machine work, bounds what any agent can see, and keeps a person on the irreversible verbs. I designed and built both.
Lines of TypeScript across 574 tracked files
GeoJSON features published across 28 datasets
Automated data-integrity checks standing in for a test suite
Cron pipelines that fetch, validate and commit data unattended
A research problem wearing real-estate clothes.
Autonomous fleets need ground. Not abstractly — specific parcels, close enough to where demand actually is, with enough grid capacity to charge a fleet overnight, zoned to permit the use, with a yard geometry that a vehicle can actually circulate through, and with a path to site control that does not take three years.
Each of those is a different public-records problem with a different source, a different update cadence, and a different failure mode. Assembled by hand, the answer arrives as a spreadsheet and a recommendation — and six months later, when someone asks why this parcel, the reasoning has evaporated. The number survives; the evidence behind it does not.
Radar exists to make that chain unbreakable. Not a map with scores on it. A traceable path from a public source, through a documented derivation, to a gate that a site either cleared or failed — with the record that decided it still attached at the end.
Two halves, and the second one is the interesting one.
The visible half is a geospatial workbench: markets, layers, candidate parcels, gates, underwriting, exports. Everything below in this case study describes it.
The half nobody sees is an operating system for the machine work that keeps it honest — a separate repository holding no application code at all, only registries, policies, schemas and adversarial evaluations. A deterministic router decides which specialist handles a task and exactly what it may read, before any model reasons about anything. Specialists launch with no tools and no inherited history. Four verbs stay with a human.
I built it in that order for a reason. The product got good fast and then started drifting — numbers that were right on Monday, unsourced by Friday, with no way to tell which agent or which run had changed them. The operating system is what stopped that, and it is the part of this project I would rebuild first somewhere else.
Source to decision, without a gap.
Brief
What the operator actually needs
Demand
Where the trips are
Candidates
Parcels and facilities
Gates
Pass, fail, or unresolved
Underwriting
Weighted, versioned
Dossier
Frozen and hashed
Monitoring
Sources age; so does the answer
Unknown is a value. It never scores.
The single most consequential design decision in the system is that a missing input is not a zero, not an average, and not a hidden default. It is null with a reason code, and it drops the record into a review band instead of quietly producing a number that looks like knowledge.
Every value in the system carries an evidence mode, and each mode carries the confidence weight it deserves. Stale utility data is nulled rather than shown. A pipeline build date is never promoted into a claim about source freshness. A future-dated source is flagged invalid, not fresh.
Evidence modes and their confidence weight
- Measured
- Read directly from the source record. Full weight — 1.00
- Modeled
- Derived through a documented model. 0.65
- Directional
- Indicative only; will not carry a decision. 0.35
- Demo
- Illustrative. Contributes nothing — 0.00
- Unavailable
- Source could not be resolved. Contributes nothing — 0.00
Unresolved gates contribute zero confidence and zero suitability, and the weight they took with them is reported back as an explicit audit figure rather than being silently redistributed across the gates that did resolve.
Freshness, enforced
Grid hosting-capacity data is only allowed to populate a parcel when the record was measured at the nearest line section and is inside its 45-day service-level window. Outside it, the field goes null and the method becomes an unavailable-source clock with a human-readable reason.
No double-counting
The land-use component deliberately excludes every permit-derived signal, because permit activity is already scored in a separate readiness component. Weights re-normalise over whichever components actually have data, so the composite can never reward the same permit rows twice.
Screening is not approval.
Nine weighted gates, summing to a hundred points, resolve a site into one of four states. A fatal failure forces a screen-out no matter how well the rest scores. An unresolved fatal gate forces a hold. Confidence below seventy caps you at conditional advance.
Candidate ranking then sorts by decision state first — then fatal failures, then material failures, then unresolved weight, and only then by score. That ordering is the point: it is structurally impossible for a high-scoring screened-out site to outrank a viable one.
Site identity itself is a gate. It is not satisfied by an address string matching; it requires the parcel match to be a containing point, exactly one matched parcel feature whose identifier equals the assessor parcel number, and the official address point to agree with the site coordinate to within 1e-8 degrees.
The nine gates
- Legal use · 16
- Does the zoning actually permit the operation
- Utility · 16
- Is there grid capacity to charge the fleet
- Fleet capacity · 14
- Does the site hold the vehicle count
- Site control · 14
- Is there a realistic path to control it
- Budget · 10
- Does the basis survive the operator’s envelope
- Identity · 8
- Is this provably the parcel we think it is
- Yard geometry · 8
- Can vehicles actually circulate
- Access · 7
- Routing, restrictions, deadhead
- Environmental · 7
- Screens that can kill a deal late
Network timing must not decide what you see.
The workbench renders more than a hundred map layers — 109 typed layer specifications over a set of shared sources — and several of them are multi-megabyte GeoJSON assets that land whenever the network delivers them. In the naive implementation, whichever asset arrives last paints on top, so the same site can look different on two loads.
So the stack is reconciled rather than appended. Every layer is declared once in one ordered canonical array; new layers insert relative to the next already-mounted canonical identifier; and the entire stack is re-sorted whenever an async asset arrives. Broad context layers are additionally pinned beneath the basemap’s own road and place labels, so geography never buries its own names.
Two details I am fond of. First, geographic scope is a compile-time contract: layer and mode availability live in tables closed with a TypeScript satisfies clause, so adding a layer without declaring which markets it is valid in is a build failure — and switching markets projects the user’s visibility rather than mutating it, so one city can never render or cite another city’s evidence. Second, the basemap has a three-tier fallback ending in an inline offline style, because a blocked CDN should degrade to a working dark map with every bundled layer still attached, not to a dead black rectangle.
What one candidate site actually carries.
San Francisco parcels individually screened for grid power and deadhead distance
Distinct public-record enrichment feeds joined onto every candidate site
Candidates that survive screening and carry full underwriting
Those thirty-nine feeds are not a data dump. Each one runs the same house pattern end to end — a fetch or derive script, JSON keyed by site identifier, a typed facts interface, a dedicated panel component, and a row in both the coverage ledger and the methodology ledger — which is why there are forty facts panels in the product and no orphaned datasets behind them. Grid hosting capacity, zoning, census demographics, occupational wage, fire and EMS response, pavement condition, curb parking, truck restrictions, sensitive receptors, environmental screens.
Grounded, or it refuses.
There is a research agent inside the workbench. It runs a read-only tool loop over the product’s own data — workbench context, candidate search, site briefs, operator facilities, market summaries, data limitations — plus live intel and hosted web search, bounded to eight tool rounds and force-finalised after five data rounds.
The rule it lives under is the same rule the rest of the system lives under: a reply is checked for real tool evidence, and without it the agent falls back to a safe ungrounded response rather than improvising. Citations are derived mechanically from the tool calls that actually happened, not written by the model.
It is also budgeted like something that runs in production — capped body size, per-principal and per-IP rate limits, and a token-cost ceiling whose units are deliberately decoupled from billing prices, so renaming a model can never silently disable the guardrail. Limiter keys are hashes; no raw user identifier, access token, or IP is retained.
It keeps itself current.
Four scheduled pipelines run without anyone present. They pull from public sources, validate what comes back, snapshot the raw response into an append-only archive with a diff log, and commit the result. Two machine identities appear in the commit history alongside mine, which is the honest way to describe a repository that partly maintains itself.
Above them sits a scheduled analyst agent that runs daily in an isolated worktree, sweeps ten defined work lanes, ships one change end to end, and promotes its own release through a written gate: clean detached main, exactly one commit, checks run against committed bytes only, re-fetch before push, never force-push, poll for the exact commit hash, and confirm the deployment health endpoint uncached.
Its memory is deliberately not conversational. It writes a staged backlog, a skill registry with dated evolution decisions, and a run note per day. Six of the ten recorded skill decisions are deliberate no change or defer entries, with written reasoning about context cost — which I consider the strongest evidence the thing is actually thinking rather than accumulating.
What is checked, and by whom
- Test suite
- There is no npm test and no CI gate. 44 domain check scripts are the suite, and local validation is the only thing standing between a change and production.
- Source rights
- Every source carries a rights decision; an expired or revoked one fails the build rather than degrading quietly.
- Temporal integrity
- Evidence cannot claim a validity window it does not have. Inverted or future-dated records are rejected.
- Entity resolution
- A mapped facility clue is never allowed to be restated as a physical facility.
- Staleness
- A monitored registry of source artefacts with per-source age limits and a due-ratio threshold.
- Publication
- Datasets are hash-addressed behind a manifest pointer that is replaced last, so the app can never advertise a half-built pair.
What the agents are not allowed to do.
Alongside the product sits a small second repository that does nothing but govern how machine work is delegated. It holds no application code — it is registries, policies, schemas and evaluations, and it is machine-checked on every change.
Routing happens before any model reasoning. A deterministic router classifies the task, selects one specialist, and resolves exactly which context items that specialist may read — returning a three-way split of readable items, metadata-only items, and blocked items, each blocked one carrying a named machine-readable reason. Specialists then launch as no-tool subagents in a fresh, non-resumed context that can see nothing but a signed packet.
The packet is where the design earns its keep. It embeds a revision hash computed over every registry, so editing any of them invalidates every outstanding packet. And validation refuses to trust the packet’s own claims: it re-runs the router live and byte-compares the result. A packet whose two revision strings match each other but are stale is rejected. A packet that quietly appends one extra source grant on a current revision is also rejected — because the freshly derived route does not contain it.
Above all of it is a four-verb human boundary — send, spend, sign, release — declared once, derived mechanically, and enforced in code rather than requested in prose. Approvals bind to a hash of the exact payload, so a changed recipient or amount invalidates the approval. A producer can never review its own work.
Pinned routing cases, plus adversarial registry mutations that must fail closed
Adversarial safety evaluations — 20 of them required rejections
Machine-enforced schemas across the control plane
Specialist agents, each with no tools and no inherited context
Rigour is a feature you can ship.
The instinct on a project this size is to defer the discipline — get the map working, make the numbers look right, add provenance later. That order does not work. Provenance is not a layer you can retrofit; it is a property of how each value was constructed, and once a value has been through three derivations without carrying its source, the information is gone.
The second lesson is that constraints written in prose do not hold. Every rule in this system that actually survived contact with a deadline is one that fails a build, fails a check, or fails a schema. The rules that lived in documentation got broken — including by me.
Both of those transfer directly to diligence work. The question I ask of a target’s AI claims now is not whether the model is good. It is: what happens here when an input is missing, and who finds out?