The obvious way to enforce provenance is to build the document from plain values and then run a validator over it that asks “is everything sourced?”.
That design fails in a specific and predictable way. The validator can only check the fields it knows to look at, so the first field somebody adds without updating the validator is unsourced and silent. The check drifts behind the document, and you find out when a client does.
A value and its citation are one type
docpkg/sources.py takes the other approach. A document value is not a string:
@dataclass(frozen=True)
class Sourced:
value: Any
citation: Citation
There is no constructor that produces a value without a citation. So an unsourced value cannot be built, and a field added next year is sourced or it does not render — without anyone remembering to extend a list.
A Citation names three things, and all three are needed:
@dataclass(frozen=True)
class Citation:
source_id: str # which file
record_id: str # which record inside it
field: str # which key of that record
A citation naming only the file is the documentary equivalent of “it was on the internet somewhere”. The source id is checked at construction against a closed registry, so a citation to an invented origin raises immediately:
def test_a_citation_to_an_undeclared_source_is_rejected(self):
with self.assertRaises(UnknownSource):
Citation("some-source-nobody-declared", "REC-1", "value")
Two sources, because one cannot teach this
The document draws on two fixture sources, and the split is the point:
| Source | Authoritative for | Knows nothing about |
|---|---|---|
| Custodian position extract | Holdings, valuations, charges | What the client wants |
| Client fact-find | Objectives, risk profile, capacity for loss | What anything is worth |
With a single origin, “where did this come from?” has only one answer and the citation is decoration. With two, the provenance table is doing real work — and the commonest provenance bug becomes visible. It is not an unsourced figure. It is a figure sourced to a file that happens to contain a similar number for a different reason.
That is why each source records what it is authoritative_for, and why the
rendered document prints those claims:
## Sources
| Source | Authoritative for |
| --- | --- |
| Approved disclosure library | regulated wording, quoted verbatim |
| Client fact-find questionnaire | objectives, risk profile and capacity for loss |
| Custodian position extract | holdings, valuations and charges |
The third surface: text that is quoted, never generated
references/disclosures.json is not a fourth integration. It is a reference
surface, and the distinction matters.
A figure is a value: it can be looked up, compared, re-derived. A regulated paragraph is a sentence, and the only safe operation on a regulated sentence is to quote it verbatim with an identifier attached.
Ask a model to “summarise the risk warning” and it produces something that reads better and means something slightly different, and nobody downstream can tell which words were approved and which were improvised. So the renderer does not accept a risk warning as text. It accepts a reference id, looks it up, and emits the stored wording. The suite checks the output byte-for-byte against the library:
def test_disclosures_are_quoted_verbatim_from_the_library(self):
approved = {d["record_id"]: d["text"] for d in library["disclosures"]}
document = render_markdown(build_fixture_state())
for text in approved.values():
self.assertIn(text, document)
Unsourced renders blank
The fixture deliberately leaves the property holdings unsourced, so the shipped document has real gaps in it. A demo where every field happens to be populated cannot show what the blank rule does, and the blank rule is half the point.
| Property funds | — |
| Property funds weight | — |
An em-dash. Not N/A, not TBC, not 0, not an empty cell. A reader skimming a
column of numbers has to be able to see that something is missing, and a zero
reads as a figure.
The strong form of the test is not that a blank appears. It is that no digit ever appears where a blank belongs:
def test_an_unsourced_field_never_renders_a_plausible_figure(self):
rows = [ln for ln in document.splitlines() if ln.startswith("| Property funds")]
for row in rows:
self.assertIn(BLANK, row)
self.assertNotIn("£", row)
And pointing a field at a record that does not exist gets you the same blank, rather than an error or a guess:
def resolve(citation: Citation) -> Any:
...
return None
None — never a default, never the nearest match. Returning a plausible
substitute here would defeat every guard downstream, because downstream cannot
tell a real value from a helpful one.
Why blank rather than refuse
The asymmetry is deliberate, and it is worth being explicit about which way it runs.
A blank in a client document is embarrassing, and somebody fixes it that afternoon. A plausible wrong number in a client document is a misrepresentation that nobody notices until it matters.
So an unsourced field renders blank and the document still ships with its gaps listed. A field whose citation has stopped agreeing with its record is a different condition entirely, and that one refuses. Chapter 5.
