Decision 0007
0007 — The application may render research findings, under four constraints
Last changed 2026-08-24 docs/decisions/0007-application-may-render-research.md
Date: 2026-08-12. Status: accepted. Amends the third bullet of decision 0006’s “What this does not change”.
Context
Every decision record so far has answered a research question. This one answers a product question, because the owner has asked for one: turn the client into a resource hub that carries what the repository has measured, so an investor can act on it.
The standing rule was that no number from research/ may appear in the shipped
application as a finding. It was written when the client was a marketing shell
claiming real-time data, optimality and professional validation — none of which
anything here supports. Against copy like that, a blanket ban was the only cheap
defence, and it was the right call.
It is now the wrong call, for three reasons.
- The ban protects the research and abandons the reader. The shipped copy makes false claims because it has no evidence to draw on. Cutting it off from the evidence guarantees that whatever fills the space is worse than the evidence would have been.
- The largest measured result is the one an application is best placed to deliver. The ~109 bp/yr contractual budget is bought by fund choice, fund wrapper, account placement and lot method (structural and tax-aware edges). All four are arithmetic on the reader’s own inputs — a bracket, an account mix, a horizon. A Markdown page can state the general result. Only software can compute the reader’s. Not trading is not one of them, and constraint 3 below is what stops it becoming one: the behaviour gap is measured against the average investor, so it may be displayed but never added to this figure (edge decomposition §2.4).
- The asset-location ranking is the worked example. The recommendation page already requires that it “must be computed, not asserted”, because the familiar rule is right for bonds by a factor of four and wrong for emerging-market equity at two of the four US dividend rates. That instruction is addressed to an application. It presumes one.
The risk the ban guarded against is real and does not go away: a number lifted out of
research/ loses its status, its interval, its as of date and its counter-evidence,
and what survives the journey is a confident-sounding figure with none of the machinery
that made it honest. So the answer is not to lift the ban. It is to make the machinery
travel with the number.
Decision
The application may render any finding from research/ or docs/research/, provided
the finding arrives with its evidence intact. Four constraints, each enforced by the
type system rather than by prose.
- One canonical content layer. Every fact the application can display lives in
src/content/, typed, and is imported from there. A number hardcoded in a route or a component is a defect. This makes the set of claims the application makes enumerable, reviewable, and diffable against the research pages in one place. - Status, date and source travel with the number. Every displayed figure carries
its status in the closed vocabulary, its
as ofdate where the source has one, its confidence interval where one was measured, and a link to the page that owns it. A loading without its interval is a misquote. A fee without its date is a misquote. - The certainty class governs the language, and the benchmark governs the sum. A
risk premium may never be described as an edge. Lines measured against different
benchmarks — a cheap index, the average investor, the reader’s own counterfactual —
may never be added together, which is the rule
studies/outperformance_horizon.pyalready enforces in code and which the application must enforce in its interface. - Client calculations mirror the research workspace and are tested against it.
Presentation math in
src/lib/is a port of a study module, not a new implementation, and its tests run against fixtures generated by that module (research/src/portfolio_edge/reporting/client_fixtures.py→src/lib/fixtures/research-ground-truth.json). This is the client’s version of the repository’s standing requirement that a fixture be computed independently of the implementation under test.
No optimiser ships. The root AGENTS.md rule that optimisation math belongs in
research/ stands unchanged. What is permitted here is closed-form arithmetic that the
research workspace has already run and tested — a normal CDF, a shelter-priority
ranking, a break-even rate. Anything that searches a weight space belongs in
research/ and gets a frozen specification and a ledger entry.
What this does not change
- No sleeve is promoted. Decision 0004 stands in full.
The application must display
exploratoryasexploratoryandunresolvedasunresolved, andunresolvedis not a promotion. - Nothing may be claimed to beat an index. Against a cheap index the honest budget is ~5.4 bp against 313 bp of tracking error. The application states that as plainly as the recommendation page does, and in the same place as the 109 bp figure, never apart from it.
- The free-price-source ban holds. Decision 0002 is untouched. The application ships no price data, calls no market API, and stores no keys. It renders what has been measured; it does not measure.
docs/research/remains canonical. The content layer is a rendering of those pages and is subordinate to them. Where the two disagree, the page wins and the content layer is the thing that gets fixed.
Alternatives considered
Keep the ban and ship a client that links to the Markdown. Rejected. It is the status quo with extra steps, and it forfeits the compute-the-reader’s-own-answer case that motivates the change. It also leaves the false copy in place, since a link is not a correction.
Allow findings but skip the content layer, writing numbers into routes. Rejected. The claims the application makes then become unenumerable, and the first stale figure is undetectable until a reader acts on it. The content layer costs one indirection and buys a reviewable claim surface.
Allow findings but forbid calculators. Rejected, and it is the option that would have been most defensible a month ago. It fails on the asset-location result specifically: the ranking inverts between two live US dividend rates, so a page that states one ranking is wrong for a large share of its readers, and a page that states all four is unreadable. The arithmetic has to be run per reader or the finding cannot be delivered at all.
Ship the research workspace’s Python to the browser. Rejected. It would need Pyodide, a multi-megabyte payload and a build path nobody here maintains, to avoid porting about two hundred lines of closed-form arithmetic that is already covered by generated fixtures.
Consequences
- The client acquires a test runner and a
src/lib/of ported arithmetic. Both are new surface area, and the fixture file couples the client to the research workspace: a study module that changes its numbers breaks a client test, which is the intended behaviour and not a nuisance to route around. - Every status change in any experiment now obliges a check of
src/content/as well as the recommendation page. The coupling is the same one decision 0006 accepted, extended one layer outward. - The shipped copy claiming real-time data, optimality and professional validation is deleted rather than softened. It was flagged as a product decision to raise; this is the record of it being raised and taken.
- The root
AGENTS.mdtrap list andREADME.md’s closing line both assert the ban and must be updated in the same change that lands this. - Supersede this record rather than amending it if the application ever acquires a live data source, since constraint 4 and the second bullet above both assume it has none.