About this app
It turns fund data into an investor-facing wholesale fund report, and keeps a record of where every sentence in that report came from. For how to use it, see How to.
What it does
A report is assembled in three passes. The quantitative content (returns, attribution, positioning) is read from a data source. The narrative content is either written by a person or drafted by an AI model that has searched an approved list of sites. Both are then rendered to Word and PDF from the same underlying report object, so the two formats cannot drift apart.
Nothing here invents a number. The model is only ever asked what the market said about a holding, never what the fund returned.
Research a subject is the same machinery pointed at something we hold no data on — a manager, a competitor, a company someone has asked about. You give it a name and whatever you already know, and it researches recent news and market reaction and writes the note from what it finds. Because there is no data source behind a subject like that, the note carries no returns, attribution or positioning at all, and says so on its face rather than filling the tables with estimates. What you typed in goes into the document as your words, under its own heading, marked as supplied with the request and unverified.
Where the words came from
This is the part the rest of the design hangs off. Every block of text in a report carries an origin: written by a person, drafted by a model, or drafted by a model and then edited. Any block a model wrote carries a footnote to the source it was based on.
| Marking | Means |
|---|---|
| No tag | Written by a person. |
AI-DRAFTED | Written by the model, unedited. |
AI-EDITED | Drafted by the model, then changed by a person. |
[4] | Refers to entry 4 in the Sources list at the end of the report. |
| Red flag on a source | Found outside the approved site list. A person has to sign it off before publication. |
A model-written passage with no source attached is treated as a defect:
the approval step refuses the report until
it is fixed. The
generate endpoint also reports the count in an
X-Provenance-Problems response header, so an automated
caller can tell a clean report from one that could not be approved
without opening the document.
Getting from draft to published
Generating a document is only the first step. From Past reports a report goes into the review flow:
| State | What can happen next |
|---|---|
| Draft | Submitted for review, or deleted. |
| In review | Approved, or sent back to draft. |
| Approved | Published, or approval revoked (back to draft). |
| Published | Superseded by a new version. Published versions are never edited in place. |
Each transition is appended to an audit trail, each version is stored as an immutable snapshot, and whoever last edited a report cannot be the person who approves it.
What is real, and what is a placeholder
| Part | Status |
|---|---|
| Fund figures | Placeholder. 2 funds on the generate form: the demo ones have fixed numbers that do not vary by reporting period, and the rest were typed in on the Funds tab in Admin. |
| Report rendering | Real. Word and PDF are both produced from the report object, independently. |
| Provenance and citations | Real. Enforced in the model, checked at approval, shown in the document. |
| AI research | Real, and searches the live web. Enabled on this server. |
| Approved source list | Real. 15 sites, editable without a deploy. |
| Lifecycle and audit trail | Real. Every report can be sent to review, edited, approved and published from Reports. |
How it can be expanded
Point it at real fund data
Everything reads fund figures through one interface,
FundDataSource. Adding a class that satisfies it and
setting FUNDREPORT_FUND_DATA_SOURCE switches the whole app
over, with no renderer, lifecycle, or web code changes needed. That is
also the point at which a reporting period stops being a label and
becomes a query, and the "figures don't vary by period" notice
disappears on its own.
Change what research is allowed to read
The approved sites live in a JSON file that is read at request time, so
the list can change without a deploy. Ticking boxes on the generate
screen narrows that list for a single report; it can never widen it, so
a crafted request cannot add a site nobody approved. Turning on
FUNDREPORT_ALLOW_OPEN_WEB_RESEARCH lets the model cite
anything it finds, and every such citation is flagged for review.
Sign people in
The review flow records who did what, but who you are is a chip in the top bar rather than a login. Every admin route already depends on one function that answers "who is this", so switching sign-in on is a change to that function, not a hunt through the routes.
Add sections to the report
A report is a plain data structure. A new section means adding a field to it and a block to each of the two renderers. Keeping the renderers independent, rather than converting Word to PDF or the reverse, costs a little duplication and buys two documents that are each properly typeset.
Still open
Whether the published document is a statutory disclosure or marketing material is deliberately undecided. The two carry different obligations, and that decision changes wording, mandatory sections, and who signs off, so nothing in the design assumes an answer yet.
