0017. The template is the source of truth, and a golden diff proves it¶
Status: Accepted Date: 2026-09-12
Context¶
Phase A built examples/reference-service/ as ordinary Python so that no
one ever debugged Jinja and Python at the same time (ADR 0002, the
original specification's section 3.3). Phase B turns that tree into the
template body, {{cookiecutter.project_slug}}/. From then on two trees
describe one service, and the question is which one a contributor edits and
what stops the other from drifting.
Decision¶
The template body is the only source of truth from the first M7 pull
request. examples/reference-service/ is rendered from it by
scripts/regen.py with the fixed answers in tests/reference-answers.yaml,
under PYFR_REGEN so the render has no side effects, and is committed as
output. just regen-check — the golden CI job and tests/test_golden.py —
fails when the render and the committed example differ by a byte.
One file is outside the comparison: uv.lock. A lock is resolver output
for one dependency set, and pruning produces eight; it cannot be a Jinja
document. The example's lock is kept and checked against its regenerated
pyproject.toml by uv lock --check.
Dependabot edits the rendered example because it cannot parse Jinja. just
adopt copies each changed line back into the template file that renders
it; a change that is not a line-for-line replacement is refused and made by
hand.
Alternatives considered¶
- Keep the example hand-maintained and add the golden diff only at the end of M7. Rejected: leaves four pull requests in which the trees can disagree with nothing enforcing agreement.
- Derive the template from the example mechanically. Rejected: cannot
express pruning or the
{% raw %}guards.
Consequences¶
- Contributors edit
{{cookiecutter.project_slug}}/and runjust regen; a hand edit to the example fails the build with the file named. - Every pull request that touches the template shows its effect on the example as an ordinary file diff.
- The example's
git blamerestarts at this decision; the history of its files continues in the template body, wheregit mvcarried it. just adoptserves three round trips, not one: Dependabot's pin changes, and the generated files (openapi.json,.env.example, the configuration reference) whose generators run in the example and whose replaced lines are carried back into the template the same way; and the root workflows' action pins, which Dependabot can bump only there, are copied into the template's workflows.ruff,mypyandimport-lintercannot run on the template body. They run on the example on every push, and — from PR 5 — on three sampled renders on merge.- The documentation follows the same rule: pages about the service live in
the template body and are rendered into the example's
docs/; PyFr's own site links to that render as the documentation every generated project ships.