My CV exists at two disclosure levels. The full version names system sizes and an internal tool; a cleared version does not. Both come from one source file, because two hand-maintained copies of the same document diverge the first week and you find out at the worst moment.
The interesting part is not the templating. It is that a redaction pipeline has a failure mode that is silent by construction: if the thing you meant to remove is still there, nothing errors. The document just quietly says more than you intended, to whoever you sent it to.
Make the redactions assert
Each redaction is a pattern and a replacement, and every one of them must match:
for label, old, new in REDACTIONS:
if old not in out:
sys.exit(f"build failed: redaction '{label}' no longer matches")
out = out.replace(old, new, 1)
This inverts the usual instinct. A templating engine is forgiving: a substitution that matches nothing is a no-op. Here a no-op is the bug. Reword the sentence containing the figure and the build stops with the name of the redaction that no longer applies, instead of shipping a "cleared" document that still carries it.
There is a final sweep too: a list of tokens that must not survive anywhere in the output, checked after all substitutions. Belt and braces, but the braces cost four lines.
display:none is not redaction
The CV shows a phone number in print and hides it on screen. The obvious implementation is a class:
.pdf-only { display: none; }
@media print { .pdf-only { display: inline; } }
This is wrong for a public page, and I shipped it before catching it. The digits are still in the HTML. View Source shows them. Every scraper reads them. And because the print stylesheet un-hides the element, a visitor who hits Ctrl+P renders the number in full.
Hiding is a presentation concern. Redaction is a content concern. The public build now removes the elements from the markup:
public, removed = STRIP_TEL.subn("", public)
if removed != 2:
sys.exit(f"expected to strip 2 .tel elements, stripped {removed}")
if PHONE_DIGITS in public:
sys.exit("phone number still present in the public page source")
Note the count check. A regex that silently matches once instead of twice would leave half the contact line intact, and the second assertion would not necessarily catch it if the surviving element were the separator rather than the number. Assert the shape, then assert the outcome.
Assert things you already know
The landing page has never contained a phone number. The build checks it anyway, on every run. That check has never fired and probably never will.
It stays because "there is no phone number in this file" is a claim about a file that people edit. I know it holds today. The assertion is what makes it hold in six months, when the edit is made by someone who never read this note. Possibly me.
The general shape
- If the failure mode of your pipeline is silence, add an assertion whose failure is loud.
- A substitution that matches nothing is not a no-op. It is an unreported error.
- Hiding data in the presentation layer does not remove it from the payload.
- Assertions about invariants you are confident in are the cheap ones. They are also the ones that catch the edit you did not make.
Update, October 2026
The figures and the internal tool were cleared for publication, so the redaction list is empty and the two editions now differ only by the phone number. The machinery stays in place for the next detail that is not cleared yet. The build gained a second check: it fails if any edition names an internal repository, ticket or host, and it runs that check on the rendered text as well as the source, because a name written as an HTML entity reads the same to a visitor and differently to a regex.