How the app is put together, what it talks to, and the path a report takes
from a form to something an investor can be sent. For what it does and why,
see About; for how to do a particular job, see
How to.
The shape of it
A FastAPI app rendering its own pages, with SQLite behind it. It is
layered so that the rules can be read without a database: what a report
is, what may happen to it and who may do it are plain Python with no
session in sight, and the stores are the only code that knows SQLite
exists.
Each layer only calls the one below it. Nothing in “the rules” imports
a store, which is what lets the same rules be tested without a
database and read without one.
Where it runs
One Linux VPS in New Zealand, and nothing else. nginx takes the request
and hands it to the app over the loopback interface, so the app itself
is not reachable from outside the machine; the database is a file on the
same disk. The only traffic that leaves is outbound, and only when a
report is being researched or a message is being sent.
The app runs as its own service account that cannot log in, and
systemd restarts it if it falls over. Both outbound calls are
optional: with no API key the research step does nothing and says
so, and with no mail server the outbox holds messages rather than
failing whatever was trying to send one.
How a change reaches the server
Pushing to master is the deploy. The build agent runs on
the VPS itself, so there is no SSH hop to configure and nothing holding
a key to the box. The tests are the gate: they run in a throwaway
virtual environment, and only if they pass does anything get copied.
Each step is a place it can stop safely. A failing test copies
nothing; a failing migration leaves the previous version serving,
which is the right way round, because a migration that did not apply
is a service that should not be answering. The privileged half runs
from a root-owned script the deploy cannot overwrite, and it refuses
to run at all if it differs from the one in the commit being
deployed.
What is on the box
Four kinds of state, and only one of them survives a restart in a way
that matters.
The API key and the mail password are read from the environment at
the point they are used. They are not settings, they are never
written to the database, and no screen in the app can show them. The
documents held in memory are why a restart loses a report that was
still being generated — the first thing to replace if this ever runs
on more than one machine.
Where the figures and the words come from
The split down the middle of this diagram is the whole design. Figures
come from a data source and are never asked of a model. Words are
either written by a person or drafted by a model that has searched an
approved list of sites, and a drafted block carries a footnote to what
it was drafted from. Both halves meet in one report object, and both
output formats are rendered from that — so the Word file and the PDF
cannot drift apart.
The model is only ever asked what the market said about a holding,
never what the fund returned.
The research step is configured on this server.
2 funds can be
reported on today.
How a report gets made
A report that researches half a dozen holdings takes minutes, which is
longer than a browser or a proxy will hold a request open. So the form
queues a job and the page polls it. Validation happens before the job
is queued, on purpose: a typo in a date comes back immediately instead
of as a job that dies a minute later.
The job id is a random token and there is no route that lists jobs —
with no sign-in yet, an enumerable id would be an open filing
cabinet. Jobs live in the process, so a restart loses anything in
flight; that is the first thing to replace when this needs to run on
more than one machine.
Which template a report is built with
Three places could decide the layout, and they are tried in this order.
The last two are what makes an unattended run possible: nobody is at a
form to choose, so the fund has to carry the choice, and a fund that has
chosen nothing still has somewhere to fall back to.
Editing a template changes how a document is laid out. It cannot make
an unresearched figure appear, remove a source, drop the notices or
delete the footer — normalise() puts those back — and
template prose is never sent to a model.
Template
Where it comes from
For
Fund report (short) opens on this
Built in
Fund reports
Standard fund report
Built in
Fund reports and subject notes
A fund report with nothing named falls back to
Standard fund report.
What can happen to a report
This diagram is generated from the same rules the review screen
enforces, so it cannot describe a lifecycle the app does not have.
Nothing is ever deleted: archiving retains the report, and every version
is an immutable snapshot.
An administrator may make any of these moves; the table below says
who else can.
Archive: any status → Archived, Administrator.
Who can do what
Move
From and to
Viewer
Analyst
Approver
Administrator
Send for review
Draft or Changes requested → In review
—
yes
—
yes
Request changes
In review → Changes requested
—
—
yes
yes
Withdraw
In review → Draft
—
yes
—
yes
Approve
In review → Approved
—
—
yes
yes
Publish
Approved → Published
—
—
yes
yes
Revoke approval
Approved → Draft
—
—
yes
yes
Archive
any status → Archived
—
—
—
yes
What stops a report being approved
Two of these are switches. The third is not, and is the one the rest of
the design hangs off.
Blocker
Can it be switched off?
The approver last edited the report
Yes — on here
Model-written text that cites no source
Yes — on here
An open-web or supplied source nobody has signed off
No. Deliberately not configurable.
Where this is going
Reports are generated by someone at a form. The point of giving a fund
its own setup is that a run does not need anybody there — a schedule
reads the fund, not a form. Two pieces of that exist and two do not, and
the dashed boxes below are the ones that do not.
A schedule can create work. It cannot sign it off: the provenance
gate and the two-person rule do not know whether a run had somebody
watching, and that is the point.
Generating report
This can take a couple of minutes when AI drafting is enabled, since each holding is researched live.