Technical Overview
10 September 2026. The engineering intent, what we built and why it has this shape, the technical goal, and the roadmap to it.
The commercial companion is business-case.md. For working reference rather than direction: architecture.md (ownership boundaries), workflows.md (hop-by-hop paths), when-to-stop.md (the stopping rules), connectors.md (adding a county), gap-audit.md (what is open, with measurements).
1. Engineering intent
The system automates a judgement task on legal evidence. That single fact determines the architecture, because it changes what a bug costs.
In most systems a wrong answer is visible: something crashes, a number looks odd, a page fails to load. Here a wrong answer is a well-formed search note containing the wrong instruments, and it is indistinguishable from a right one until someone re-does the work by hand. So the architecture is built around three rules that all say the same thing in different places.
Rule 1 — Fetching never judges
A collector returns everything it found, each item carrying provenance describing how it was found. A separate stage decides relevance.
This is enforced by a test, not by convention, because the predecessor violated it: the old search derived a keyword signature from its own results and stamped every survivor as useful inside the fetching code — invisible downstream, unexplained, and impossible to test. The rule exists to make relevance decisions reviewable.
collector ──▶ "here is everything, and how I found each one"
│
▼
classifier ──▶ "these concern the parcel, and here is why for each"
│
▼
reporter ──▶ renders. Decides nothing.
One-way dependencies, enforced: orchestrator → collector (fetches, never
judges), orchestrator → classifier (judges, never fetches), orchestrator → reporter (renders, never decides).
Rule 2 — Degrade when you produce less; stop when you produce something untrustworthy
A missing tax bill makes a thinner note, and everything else on it is still true — that is a degradation. A missing legal description makes the instruments that follow not fewer but unfiltered, and no reader can tell — that is a stop.
The distinction is not "did the stage fail." It is "does the run still hold enough evidence for the next stage's output to mean anything."
Rule 3 — Every failure must be legible
The characteristic failure mode of this system is silence. A missing model key reads as a large review queue. A missing parser key reads as "this card is a scan." A missing cache reads as a slightly larger bill. Each of those is a correct-looking result produced by a broken configuration, and each now identifies itself.
2. What we built
2.1 Shape
Four processes, three stores.
| Process | Port | Runtime | Owns |
|---|---|---|---|
frontend/ | 5173 | React + Vite | customer and admin app — where people work |
backend/ | 4000 | Node + Express | orders, auth, employees, credentials, allocation, uploaded documents |
pipeline-backend/orchestrator/ | 4200 | Python | the title-search pipeline, and every browser it drives |
pipeline-ui/ | 5174 | React + Vite | operator's view of the pipeline |
| Store | Holds |
|---|---|
| MariaDB | orders, pick/done tokens, employees, clients |
| MongoDB | observations, runs, credentials, properties, documents |
| Redis | county-login lock, document job queue, SSE pub/sub |
Roughly 29,700 lines of Python in the pipeline, 8,900 of backend JS, 9,100 of frontend JS/JSX. 1,111 Python tests pass.
There used to be a fifth process doing four unrelated jobs, only one of which was the title search. Splitting it produced two rules worth keeping: county passwords live in exactly one process, and shared state lives in Redis, not in a process's memory — the latter learned when two instances each believed they held the county login lock and could log into the same county at once, which is how an account gets locked out.
2.2 The pipeline
POST :4200/orders/{orderNumber}/run
│
┌─────────┴──────────────────────────────────────────────────────┐
│ PLAN which sources this county has, the scoping window, │
│ and whether this order can be judged at all │
│ │ │
│ COLLECT recorder index · assessor card · tax bill │
│ one browser session per county, chain walk included │
│ │ │
│ EXTRACT read the parcel's legal identity off the card │
│ │ │
│ CLASSIFY passes 1–3 deterministic, pass 4 the model │
│ │ │
│ REVIEW park the run if a human is genuinely needed │
│ │ │
│ GENERATE search note · title report · chain · package PDF │
└─────────┬──────────────────────────────────────────────────────┘
│ an event at every stage transition
▼
POST :4000/api/internal/order-events ──▶ orders.search_note
Two deliberate properties: the webhook carries links, not the note (a missed delivery is recoverable — the deliverable stays on disk and is fetchable by order number forever), and everything keys on the customer-facing order number, never the database id.
Stage status is one of pending · done · degraded · failed · blocked · skipped · waiting, and failure policy is per stage — collect retries three times because
county portals time out routinely; classify does not, because the model call
already retries internally and a stage-level retry would multiply rather than
add.
2.3 The searching, which is the actual product
The grantor-grantee walk. A title search's primary method is walking the ownership chain backwards: find the deed that vested title in the current owner, take its grantor, search them, repeat. This was the largest missing capability and it is built, bounded — limited depth, a back-to date on full searches, institutional grantors never seeded, and the walk abandons a hop mid-flight rather than letting an overflow land in the result set.
Building it required settling a question the predecessor had got backwards: on an index row, whose role is recorded? We resolved it by fetching one instrument from both sides and comparing. The answer — the role belongs to the row's own subject, not the counterparty — meant every conveyance had been recorded backwards, a live bug found by asking a question rather than by a crash.
Relevance signals, strongest first:
| Signal | Strength |
|---|---|
| The assessor card names the instrument outright | decisive |
| The card's deed book/page | strong, can be stale |
| The recorder's own parcel reference | strong — the county's own answer |
| Grantor-grantee linkage | strong |
| Full legal-description match | strong positive, worthless negative |
| Tract name without acreage | ambiguous both ways |
| A name hit alone, when the order names a parcel | weak, correctly demoted |
| The model | last resort |
The evidence gate. When a run has no way to tell this parcel's instruments from anyone else's, it stops rather than keeping everything. It asks that question at plan time — before a browser opens — and again before classifying, because discovering it after a live session costs a shared login, a rate limit and minutes.
2.4 County access, which is where the real engineering went
Four collectors behind one interface:
| Collector | Serves | Mechanism |
|---|---|---|
sra.py | Virginia circuit courts | JSON API from inside a browser session, subscription login |
acclaim.py | Brevard County FL | public search with CSV export, plus per-instrument document fetch |
journeys.py | counties with a step-list config | declarative steps under connectors/ |
driver.py | mapped Florida portals | selector-driven scraper |
Connector configs are bucketed by evidence, not quality: verified/ means
somebody watched it work against the live site; unverified/ means it was
generated or guessed. The bucket reaches the plan, so a thin result from an
unwatched connector reads as "not checked" rather than "nothing recorded."
Three counties are verified today.
The Brevard block is the clearest example of what this work actually involves. Its assessor sits behind Cloudflare, and we measured both halves rather than guessing:
| From | The site answers | Meaning |
|---|---|---|
| India | 403 Attention Required! | a refusal — the country is blocked |
| US exit node | 403 Just a moment… | the solvable challenge |
And from a US address, a headless browser cleared nothing in 45 seconds — neither the headless shell nor full Chromium — while a headed browser on a real display cleared it instantly. The county now works, and the requirement is explicit: a US network path and a machine with a real display session.
2.5 The measurement discipline
Fourteen analyst-written notes are ground truth. Scoring targets the rendered note — what a customer receives — not internal classifier state, because the note applies its own scoping on top.
The standing rule is that no accuracy claim is made from memory. Several plausible improvements have been built, measured, found not to pay, and reverted with the measurement recorded — including relaxing the strictest relevance predicate, which scored worse and, in failing, exposed that the note's date window is coupled to how many instruments classification kept. That finding was worth more than the change would have been.
Best current result on the hardest order: 54 of 69 correct with zero false positives. Precision holding at zero as volume grows is the strongest signal we have.
3. Key decisions, and what each one cost
| Decision | Bought | Cost |
|---|---|---|
| Collector never judges | reviewable relevance decisions; a whole class of invisible bug removed | more moving parts; provenance must be threaded everywhere |
| Deterministic passes first, model last | cheap, reproducible, explainable answers for most instruments | the rules can only affirm; see §4 |
| Stop rather than guess | no plausible-but-wrong notes | more runs need a person |
| Score the rendered note | measures what customers get | slower feedback than unit tests |
| One browser session per county | a shared account logs in once, not per hop | the orchestrator is single-instance |
| File-backed run state | a crash loses the stage in flight and nothing before it | no multi-instance without rework |
| Two-bucket connectors | coverage claims stay honest | 25 configs that cannot be counted as coverage |
The one that matters most
The deterministic layer can say "keep" or decline to answer. It cannot say "exclude." That verdict is assigned in exactly one place, inside the model pass. Anything the rules cannot affirm becomes a review item by default.
Combined with the policy of halting on a non-empty review queue, this makes the model load-bearing for completion, not merely for accuracy. It is a defensible design — a person is better than a guess — but it is the direct technical cause of the commercial position in business-case.md §4.3, and it is the single most valuable thing to change.
4. The technical goal
A run that reaches a confident answer on its own, and escalates only genuine ambiguity — on any county we have onboarded, from any host we deploy to, without a person watching it.
Four properties, each currently partial:
| Property | Today | Target |
|---|---|---|
| Decisive | rules affirm; only the model excludes | rules can rule out as well as in, with recorded reasons |
| Unattended | more than half of runs park for a person | a minority park, and only on real ambiguity |
| Portable | one county needs a specific host with a display | requirements explicit and reproducible |
| Horizontal | single instance, in-memory work queue | multiple workers, durable queue |
5. Technical roadmap
Near term — make the answer decisive
The highest-leverage engineering on the roadmap, because it converts directly into the commercial lever.
- Give the deterministic passes the ability to exclude. Today a rule can only confirm. Deriving an equally rigorous "this instrument is on a different parcel" verdict — from a conflicting parcel reference, a conflicting platted lot, a date outside the scope — removes work from the review queue without involving the model at all.
- Decouple the note's date window from the keep count. Measured: keeping more instruments moves the chain start forward and narrows the note, dropping correct instruments that were classified as kept. This coupling blocks recall work, because any recall gain partly cancels itself.
- Run the production path end to end. The corpus harness calls the collector directly — it never builds a plan and never runs the review stage. The plan and collect stages are covered by tests and stubs but have not been exercised against a real county through the real entry point. This is the check that has not been done, and it should precede further accuracy work.
Mid term — make counties repeatable and the platform durable
- A connector verification pipeline. Onboarding a county should be a fixed procedure — subscribe, build the step list, watch it work, promote it, measure it — rather than bespoke work each time. The 25 unverified configs are the backlog this creates.
- A durable work queue. Redis already holds the county-login lock; the work queue should live there too, so a restart does not drop queued orders and the orchestrator can eventually run more than one instance.
- Close the order-management gaps. The Node half handles customers and money and is thinly covered — a handful of test files against ~63 source files, with document routes ungated and a role that grants more than its name suggests. This is not glamorous and it is the part with real exposure.
- Collapse the duplicated domain logic. Roughly 1,400 lines of overlapping classification and chain-building exist in both JS and Python, held in sync by a single hand-written mirror test. Retiring the JS side is blocked on validating the title report.
Longer term — breadth
- Validate the title report against a real one, then treat it as a product.
- The remaining search types. Two-owner and full searches are defined but exercised only in part; two-owner is exactly one turn of the chain walk.
- Coverage beyond the recorder — tax, HOA and court records — once the core is decisive and unattended. Widening a system that still needs a person on half its runs multiplies the wrong thing.
Sequencing rule
Items 1–3 before anything in the mid-term list, and nothing from "breadth" before the core is unattended. The measured history of this project is that breadth added to an undecided core produces thin results that look thorough, which is the one outcome the architecture exists to prevent.
6. What engineering effort looks like here
Three shapes of work, with different cost profiles worth planning around:
Judgement work (items 1–2) is slow and measurement-bound. Every change is scored against fourteen reference notes, and roughly half of promising ideas do not pay. Budget for experiments that get reverted; the measurement is the deliverable even when the code is not.
County work (item 4) is investigative rather than algorithmic. Sites change, block automation, and require watching before they can be trusted. It is predictable in shape and unpredictable in duration — Brevard needed a network diagnosis and a display server, not a code change.
Platform work (items 5–7) is ordinary engineering with known techniques and predictable estimates. It is the least interesting and the most schedulable.
Compute is not a constraint at any point on this roadmap: an order costs under two cents to run, and nothing planned here changes that.