Reference · How it is built

Architecture

Every component in the running system, what it is allowed to do, and the path a single question takes from a browser to a fact and back.

Runtime  3 containers · 1 host Tools  13 typed operations Store  514,000 facts
Request path

The model cannot reach the database

This is the structural claim the whole design rests on, and it is easier to see than to describe: there is no edge between the language model and the data.

Browser static HTML · no build step · 34 KB HTTPS · the only public port Caddy TLS · CSP · :80 → :443 redirect private network · :8000 FastAPI access gate · per-IP and spend budgets server-sent events back to the browser one question Agent loop plans tool calls · composes prose holds no portfolio knowledge Claude Haiku 4.5 Anthropic API · prompt caching sees tool schemas, not tables 13 tool schemas tool + arguments no path no SQL, no direct read validated arguments only Tools + semantic layer metric rules · basis conversion coverage and exclusion accounting SELECT only Postgres 16 · pgvector
The model is a planner, not a reader. It receives the question and the tool schemas, and returns the name of a tool with arguments. Everything that touches data happens below it, in code, on a connection whose database role can only SELECT. Removing the crossed edge is not a policy — there is no code that could make that call.
Tool surface

Fourteen operations, and nothing else

Each takes a validated argument schema and returns the same envelope: the result, its basis, how many entities matched, how many are represented, and every exclusion with a reason. An argument value that does not exist raises an error listing the valid ones.

Find out what exists

The agent cannot invent an identifier. It has to look one up, and ambiguity comes back as ambiguity rather than a guess.

  • list_dimensions
  • resolve_entity
  • traverse
  • list_documents

Ask for numbers

Screening returns distributions and rankings across the portfolio; drill-down returns rows and refuses above fifty entities rather than truncating.

  • get_metric
  • get_timeseries
  • compare_vintages

Ask about execution and risk

Schedule, rig demand and the risk register, with unassigned work surfaced rather than netted away.

  • get_drilling_schedule
  • get_rig_demand
  • get_risks

Ask about precedent and provenance

Analogue projects with their post-investment reviews, document retrieval always scoped to a resolved entity set, a one-page card assembled for a single project, and a call that retraces any figure to its source facts.

  • find_similar_projects
  • search_documents
  • get_project_card
  • explain
One question

The answer passes a gate before anyone sees it

The loop runs until the model stops asking for tools. What happens next is the part that is unusual: the draft is checked in code, against the envelopes it was built from.

turn 1 get_metric turn 2 get_timeseries turn 3 draft answer Review 8 deterministic checks, in code pass User fail — exactly one revision Revise, with the defects named not "review your work" — the specific failing checks are handed back re-check, but never revise twice delivered with any open issue named
The reviewer is code, not a second opinion from the model. It compares the draft against the tool envelopes: a total whose coverage was not disclosed, a figure with no traceable source, a refusal the answer talked its way around. One revision is allowed — a model that cannot fix a named defect in one pass produces churn, not correction — and whatever still fails is reported alongside the answer rather than hidden.
Fact store

Last year's plan is still there

Facts are written once and never updated. Each carries the plan vintage it belongs to, which is the only reason “what changed between two plans, and why” is answerable at all.

BP2023 161,348 facts closed · immutable BP2024 160,495 facts closed · immutable BP2025 160,726 facts current plan one partition per plan vintage compare_vintages → scope in, scope out, maturity, phasing, scale, rate, equity, macro — eight components, reconciled append only — no UPDATE, no DELETE, anywhere in the query layer
The constraint that pays for itself later. Overwriting last year's plan is the cheap decision at the time and the one that makes the most valuable question permanently unanswerable. Because the rows survive, a delta between two plans can be decomposed into eight named components that sum back to the total.
Deployment

What is actually running

Three containers on one virtual machine. Only one of them is reachable from the internet.

Caddy
:80 and :443 public — automatic TLS, content security policy, reverse proxy. The only process bound to a public interface.
App
:8000 private — FastAPI under uvicorn, single worker, non-root. Holds the only API key. explain() resolves against an in-process log, which is why the worker count is fixed rather than incidental.
Database
:5432 private — Postgres 16 with pgvector and pg_trgm. The app connects as a role that can only SELECT; CREATE is revoked from PUBLIC.
Front end
Hand-written HTML, CSS and JavaScript with no build step and no framework. Charts are hand-drawn SVG. Assets are served under content-hashed URLs so a deploy cannot be served stale.
Model
Claude Haiku 4.5 over the Anthropic API. Roughly 97% of input tokens are served from prompt cache; a typical question costs one to eight US cents.

What is in the store

TableRowsWhat it holds
fact_timeseries482,569Annual profiles, partitioned by vintage
fact_scalar31,024NPV, IRR, reserves, one value per project
risk_revision4,511Risk scores over time, latest wins
document_chunk3,043Section-split text with embeddings
conformance2,990Estimate quality per project per measure group
well1,796Producers and injectors, with spud dates
risk1,509Register entries across the portfolio
document702Plans, reviews, notes, registers