Architecture · Python vs static

What runs as Python, and what's static.

For every meaningful piece of the system, which side it lives on — and why it lives there.

The short version: the live product is a static, zero-backend site. Nothing Python runs when you use it. The math your browser executes is JavaScript generated from the Python; Python is the single source, the build-time data source, and the thing the tests hold the JavaScript honest against. Tap any row to see why it sits where it does.

Living document — regenerated as the system changes.

The five places a piece can live:
Python · canonical Generated Runs in browser Static HTML Not live yet Test guard
1 · Python — the source of truth
engine.py · agents.py · provenance.py · scenarios.py · validate.py · feature layer (waterfall / envelope / sensitivity)
2 · The bridge — build_site.py
Reads the Python truth (agent moves, provenance, scenario list) and the verbatim JS engine (reference/engine.js), and emits the static files below.
3 · Generated & committed to the repo
engine.js · full-engine.js · data.js · explore.html · full.html · model.html
4 · Runs in your browser — zero backend
/exploreengine.js  ·  /full + /modelfull-engine.js + data.js. All math is client-side; nothing is uploaded.
Parity tests lock the Python and the JavaScript together — same numbers, scenario by scenario. That lockstep is what lets the browser run JS while Python stays the reference.
The flows — how NCUE actually works
Seven flows, all sharing one engine. Read just the arrow labels to follow each one; the node colours match the chips above, so the diagrams cross-reference the flagged edges below.
Flow 1
Source of truth
ncue/engine.pythe single canonical enginegenerated bytools/py2js_engine.pythe transpiler — readable JS outemitsengine.js · full-engine.jschecked by✓ generated JS == Python (scenarios + 1000s of random inputs)served tothe live site/explore · /full · /model
One Python source, transpiled to JS and proven equal by fuzz. (The old hand-written JS is frozen in oracle/ as a witness.)
Flow 2
Build + deploy
Developer push to main (git)triggersGitHub Actions (CI)runspytest — 37 parity testsa red test blocks the deployif green, runsbuild_site.pyregenerates engine.js, data.js, full/model.htmldeployswrangler pages deploy → Cloudflare Pageslive atncue-dev.pages.dev
Every push is tested against Python before it can ship.
Flow 3
User runtime — /full and /model
User opens /full or /modelservesstatic HTML + inline CSSbootsfull-engine.js + data.js boot in the browseruser moves a slider / picks a scenarioengine recomputes (run → recalc)renders inlinedashboards refresh inlineKPIs · waterfall · FAR envelope · sensitivityNo backend — nothing leaves your machine
Everything in the tool computes live in your browser; no server, nothing uploaded.
Flow 4
Agents — hand it to the team
User clicks ‘Hand it to the team’runswindow.NCUE.optimize — tested orchestratorthe parity-guarded port (test_site_parity.py)(decision B)✓ decision B replaced the inline composer — edges 2 & 3 closedcomposes the best validated path fromthe five Agents (from data.js)Program · Cost · Revenue · Operations · Capitaleach move checked byvalidators bind hereDSCR ≥ 1.20 · ~$95M cap — a breach is penalisedengine recomputesengine recomputesproducesaudit trail + citation chips + dashboards refresh
Composed by the tested, validator-aware orchestrator: a move that breaks a guard is penalised, and ‘go’ needs zero flags. (Decision B closed edges 2 & 3.)
Flow 5
Provenance
An assumption (e.g. $273/SF hard cost)stored inprovenance.pysource · author · confidence · citationexported at build intodata.jsthe store, frozen into a static filecarried on every Move, rendered asconfidence-coloured citation chipsunder each agent move — tap for the source
Every assumption the Agents use carries its source, visible as a citation chip.
Flow 6
MCP server
External Claude / MCP clientcalls an MCP toolserver/mcp_server.pywrapsengine + provenance toolsget_provenance · sources_for_changes · coveragereturnsstructured response
How an outside assistant runs the model and reads sourced assumptions straight from the canonical Python.
Flow 7
Real agents (dormant)
User → orchestratorwould callllm_agent.pygrounded bygrounding.pyper-domain comps & rulesasksAnthropic APIreturns a proposalengine validates the proposalthenresponse — proposed inputs only
What ‘turning on real agents’ means — off today; needs a host + an API key (decision D).
The engine & friends
The core math and the agent logic. Most of this exists twice on purpose — Python as the reference, JavaScript as what actually runs — held identical by tests.
The engine engine.pyPython · single source
The deterministic pro forma — the single thing every surface depends on, and now a single source. ncue/engine.py is canonical; site/engine.js / full-engine.js are generated from it at build time by tools/py2js_engine.py (a small, readable transpiler), so the browser still runs plain JS with no backend, but nobody hand-maintains a second copy. The generator is proven faithful by test_generator_parity.py (generated JS vs Python over the scenarios + thousands of random inputs). The old hand-written JS is frozen in oracle/engine.js as an independent witness. (Decision C closed edge 1.)
Validators validate.pyPython + JS twin
The hard floors — DSCR ≥ 1.20, the ~$95M facility cap, FAR flags. Python canonical, with a JavaScript twin inside ncue-runtime.js and inline checks in the tool UI. Guarded by test_site_parity.py.
Scenarios scenarios.pyPython + JS twin
The nine presets (base, Building, Cost, Revenue, Public, Affordable, Finance, Full Stack, Live). The engine twins both carry the scenario inputs; data.js additionally exports the scenario names for labels. The scenario bar you click on /full and /model is reading the JS engine's copy.
The five Agents agents.pyPython → data.js
Program, Cost, Revenue, Operations, Capital Stack — each a vetted move-set + rationale. Authored in Python, baked into data.js at build. The browser reads the move-sets from data.js; the toggles on /model apply exactly those values. So the agent moves trace to Python, they just aren't computed live — they're static facts today (heuristics), not yet reasoned.
Provenance provenance.pyPython → data.js
Per-assumption source / author / confidence / citation. Canonical in Python, exported into data.js. The citation chips you see when an Agent is toggled on, and the "Sources & confidence" panel, render straight from that exported data.
Feature layer waterfall · envelope · sensitivityInline JS + Python twin
The upside/IRR waterfall, the program/FAR metrics, and the cap × hard-cost sensitivity grid. These run live as inline JavaScript inside the tool UI (they're tightly coupled to the DOM that draws the tables and charts). Python mirrors the math in waterfall.py / envelope.py / sensitivity.py, and test_features_parity.py runs that JavaScript through Node and asserts it equals the Python — number for number.
Orchestrator orchestrator.pyPython + JS twin · live on /model
Composes the path that makes a deal pencil. It exists in Python (orchestrator.py) and as a JavaScript twin in ncue-runtime.js, both parity-tested. As of decision B, "Hand it to the team" on /model runs the JS twin (window.NCUE.optimize) — not the old inline composer. It's validator-aware: a move that breaches the DSCR floor or the facility cap is penalised, and "penciled" requires zero flags. So the guards now actually bind on the live tool. (This closed edges 2 & 3.)
Grounding grounding.pyPython only · dormant
Per-Agent market context for reasoning. Python-only, and only used by the LLM path — which isn't hosted yet. Not exported to the site, not in the browser. It wakes up when the Agents go online.
LLM seam llm_agent.pyPython only · dormant
Where a Claude model would reason about how much to move each assumption, grounded in comps — still only ever returning proposed inputs. Off by default, clamped, with graceful fallback to the heuristics. Nothing live calls it; it needs a host + an API key.
The static site — what each page actually does
Every page is served from Cloudflare Pages with no server. The difference is whether a page computes (loads an engine) or just presents.
Landing, How-it-works, Pricing, Roadmap, this pageStatic HTML
Hand-authored content pages. No engine, no compute. Any numbers in them (prices, example figures) are written into the HTML as prose — they don't recalculate. /agents is just a redirect to /how-it-works.
Explore /exploreBrowser · engine.js
The lightweight slider viewer. Generated from ncue/viewer.py but rewired to compute entirely in the browser via engine.js (the /api/run server path in viewer.py is for local dev only — it's never deployed). Zero backend.
Full detail /fullBrowser · full-engine.js + data.js
The complete tool. Generated from full.template.html; the engine is supplied by full-engine.js and the provenance/agent data by data.js. Every dashboard, the sensitivity grid, the waterfall, export — all inline JavaScript, all client-side.
The model /modelBrowser · full-engine.js + data.js
The same page as /full, generated from the same template, with the five-Agent control layer added on top. The Agents drive the page's own engine (P / run / recalc), so /model and /full can never disagree. The agent move-sets and citations come from data.js; the engine still does all the math.
The generated bridge files
Built by build_site.py and committed to the repo. These are the seam between Python truth and the browser.
engine.js & full-engine.jsGenerated from engine.py
The engine, generated from ncue/engine.py (decision C) and packaged two ways: engine.js as a namespaced module (window.NCUE_ENGINE, used by /explore + /model) and full-engine.js as bare globals (run, BASE, SCN… that the full tool's inline UI references by name). Same math, two wrappers, one source.
data.jsGenerated from Python
The Python truth, frozen into a static file: the five Agents' move-sets, the full provenance store, and the scenario names. This is how Python facts reach the browser without a backend. test_site_parity.py loads it and checks it against Python.
ncue-runtime.jsHand-written JS twin · live on /model
The hand-maintained JavaScript twin of the validators + agents + orchestrator, parity-tested against Python. As of decision B, /model loads it and routes "Hand it to the team" and the live gap bar through its validator-aware orchestrator — so it's no longer orphaned. It also remains the front-runner for the "online" path.
Python-only services
Real Python that runs — just not as part of the public website.
MCP server server/mcp_server.pyPython · live for MCP consumers
Wraps the engine + provenance as MCP tools, so an assistant (or any MCP client) can run the model and read sourced assumptions directly from the canonical Python. Independent of the website.
Backend API server/api.pyPython · scaffold, unhosted
A FastAPI app (/optimize, /run, /health) that runs the orchestrator with the optional LLM agents. It's the seam for taking the Agents online — built and tested, but not hosted. Needs a container host + an API key.
CLI & viewer cli.py · viewer.pyPython only
cli.py runs scenarios from the terminal. viewer.py is a local Python dev server and the template that build_site.py turns into the static /explore page. On the live site only the static output exists.
The tests — the lockstep
These are the reason the split is safe. They're what guarantees the JavaScript the browser runs equals the Python reference.
37 parity & unit testsPython
test_parity.py — the engine (Python) vs the frozen JS oracle. test_features_parity.py — the waterfall/envelope/sensitivity inline JS vs Python, run through Node. test_site_parity.py — the browser twins (engine.js + ncue-runtime.js) vs Python, same KPIs and same orchestrator path. test_agents.py + test_provenance.py — Python units. CI runs all of them before every deploy; a red test blocks the ship.
Doesn't fit the clean Python-or-static split
The honest edges — things that are duplicated, dormant, or drifting, that a yes/no decision could tidy.

1 · One engine source — closed (decision C)

The engine used to live twice: a hand-written reference/engine.js (what shipped) and ncue/engine.py (what the tests checked), kept in sync by hand. Now engine.py is the only source and the JavaScript is generated from it (tools/py2js_engine.py), so there is one place to change the math. Rounding-convention note: the generated engine adopts Python's banker's rounding (round half to even), where the old hand-written JS used half-up. The only field affected is the displayed affordable-unit count, and only on the rare input where it lands exactly on a half — every scenario and 3,000 random inputs are otherwise bit-identical, and nothing downstream uses the rounded integer.

2 · Orchestrator unified — closed (decision B)

There used to be three: orchestrator.py, ncue-runtime.js, and a small inline composer on /model — and it was the inline one that ran, so the tested logic and the DSCR/facility guards weren't actually binding on the live tool. Decision B routed /model through the tested twin (window.NCUE.optimize): the validators now bind live (a breaching move is penalised; "go" needs zero flags), and there's one composition logic, parity-guarded against Python.

3 · ncue-runtime.js no longer orphaned — closed (decision B)

It now powers /model's "Hand it to the team" and gap bar, so it's a live, tested part of the site — not a file carried only for the test suite.

4 · Feature math is still duplicated — next to close

The waterfall / envelope / sensitivity math is still hand-written twice — inline JS in the tool UI and Python in the feature layer — kept equal only by the test. The engine itself is now single-source (edge 1); extending the same generator to the feature layer is the planned next step, after which this closes too.

5 · Generated files are committed

engine.js, full-engine.js, data.js, and the generated HTML are checked into the repo. The parity test catches stale numbers, but someone editing the site without rebuilding could still commit a drifted file. CI rebuilds on deploy, which mostly contains this.

6 · Dormant Python (the "online" stack)

llm_agent.py, grounding.py, and server/api.py are built and tested but unhosted — the Agents are heuristic everywhere until a host + key turn them on. (Decision D.)

7 · Marketing numbers are hardcoded

Prices and example figures on the content pages are written into the HTML as prose. They don't recalculate, and they aren't covered by any test.

8 · Secrets live in the working folder

github-token.txt and cloudflare-token.txt are gitignored and re-supplied per session, but they persist in the mounted folder between sessions. Safe (never committed), worth knowing they're there.