/process-newsletter Output Invariants
Why this exists
Kingsbury’s “Future of Everything Is Lies” critique landed a real punch: if the verification layer is itself an LLM, you’ve just stacked another correlated failure mode on top of the one you were trying to catch. Two LLMs that share training distributions, reinforcement signals, and prompt-injection susceptibility don’t independently verify each other — they fail together, quietly, and in ways the user can’t audit. RDCO took that critique seriously. This audit script is our concrete answer: a pure-Python deterministic test suite for /process-newsletter outputs that makes ZERO LLM calls and cannot hallucinate a pass. It can fail in known mechanical ways (regex misses an unusual heading variant, an edge-case YAML that parses but doesn’t match intent), but those failures are inspectable, reproducible, and patchable. The audit doesn’t replace LLM judgment — it provides a structural floor underneath it. If an LLM-generated newsletter note doesn’t even have its required frontmatter fields or the load-bearing “Mapping against Ray Data Co” section, no amount of LLM-on-LLM review will save it; the structural check catches it cheaply, deterministically, and forever.
The 13 invariants
| ID | Name | What it checks |
|---|---|---|
| I1 | frontmatter-block-exists | File starts with --- on line 1, closes with --- somewhere in first 80 lines (raised from 30 on 2026-04-19 — richly cross-linked notes legitimately exceed 30 lines of frontmatter when tags + related links are dense) |
| I2 | frontmatter-valid-yaml | The frontmatter block parses cleanly with yaml.safe_load() and yields a mapping |
| I3 | required-fields-present | Frontmatter contains: date, type, source, author, newsletter_format, sponsored, tags (source_url is recommended-not-required) |
| I4 | type-equals-reference | The type field equals exactly "reference" (vault convention for newsletter notes) |
| I5 | newsletter-format-allowed | newsletter_format is one of: business-history, curation, founder-interview, guest-post, hybrid, thought-leadership (extended 2026-04-19 to add business-history for Acquired-shape company deep dives and founder-interview for Dwarkesh/Lex/Tim Ferriss-shape long-form interviews) |
| I6 | sponsored-is-boolean | sponsored parses as a Python bool, not a string like "true" |
| I7 | tags-non-empty-min-2 | tags is a non-empty list (or non-empty comma-separated string) with at least 2 entries |
| I8 | mapping-section-present | Body contains a ## Mapping against Ray Data Co (or ## Mapping against RDCO) heading. This is the load-bearing section per the skill — without a mapping, the note didn’t justify being filed. |
| I9 | why-section-present | Body contains a ## Why this is in the vault heading |
| I10 | filename-sender-matches-source | The filename’s leading sender slug appears (loose substring / normalized form) in the source: frontmatter field |
| I11 | sponsorship-section-when-sponsored | When sponsored: true, body has a ## Sponsorship (or ## ⚠️ Sponsorship) section per the skill’s bias-disclosure rule |
| I12 | curation-section-when-curation-or-hybrid | When newsletter_format is curation or hybrid, body has a ## Curation section subsection per the skill’s curation-link-labeling rule |
| I13 | no-duplicate-source-date | No two audited files share the same (source, date, topic-slug) tuple — bulk backfill of N distinct articles on one day is fine; identical sender+date+slug means a re-file |
How to extend
When adding a new invariant, follow this pattern:
- Single concern. One invariant = one structural property. If you find yourself writing
orbetween two unrelated checks, split them. - Deterministic. No LLM calls. No network. No probabilistic logic. Stdlib + PyYAML only. If a check requires judgment, it doesn’t belong here — it belongs in an LLM-driven review skill.
- Named
I<N>. Sequential, never reused. If you retire an invariant, leave its ID dead and don’t recycle it. - Document in this SOP. Add a row to the invariants table above with the same name as the script’s
INVARIANTSdict entry. - Add to the script. Implement in
~/.claude/scripts/audit-newsletter-outputs.py, return(invariant_id, message)tuples on failure, empty on pass. - Dogfood-test against the existing corpus before committing. Run with
--since 2026-01-01(or the full audit window) and inspect the violation distribution. If the new invariant generates >50% violation rate against existing files, either the invariant is wrong or the corpus has a real hygiene problem worth surfacing — be honest about which.
Failure handling
The audit is a post-condition check, not a gating check. When /process-newsletter finishes a batch or watch run, it runs the audit, includes the violation summary in the run report, and surfaces failing files for founder review. It does NOT auto-block, auto-retry, or auto-fix. The pattern is discover → audit → decide, not gate → block → error. The founder owns the decision of whether a violation matters enough to re-process the offending file. This keeps the audit cheap and honest: it never gets in the way, and it never silently “corrects” something it doesn’t understand. If the audit becomes a gating check in the future (e.g. wired into a pre-commit hook on the vault repo), the --strict flag exists for that — but the default mode is observe-and-report.
Limitations (be honest)
These invariants do NOT catch:
- Semantic correctness of the Mapping section. I8 fires if the heading is missing. It cannot tell you whether the mapping argument is good, honest, or load-bearing. A note with
## Mapping against Ray Data Co\n\nN/Apasses I8. - Accuracy of sponsor disclosure. I11 fires if the
## Sponsorshipheading is missing whensponsored: true. It cannot verify that the sponsor was correctly identified, that an undisclosed sponsor wasn’t missed, or that the bias note is calibrated. - Whether the citation is actually right. Nothing here checks
source_urlresolves, that the author byline matches the real author, or that quoted material is accurate. - Whether the topic-slug is well-chosen. I10 only checks the sender prefix. The trailing topic-slug could be lazy, generic, or misleading.
- Whether the article should have been filed at all. Skip-triage (Step 2.5 of the skill) is judgment territory; we can’t catch a file that shouldn’t exist.
- Cross-document consistency. If two notes contradict each other, or if a note contradicts something already in the vault, the audit doesn’t notice.
These remain LLM-judgment territory. Future versions could add semantic invariants that cross-reference against the QMD index or the typed knowledge graph at ~/.claude/state/graph.duckdb (e.g. “does this note’s claimed author exist in the vault’s author authority list?”), but that re-introduces LLM contamination if the index/graph were themselves built by LLM-driven processes. Keep that semantic layer separate, name it differently (e.g. semantic-audit-newsletter-outputs.py), and never confuse the deterministic structural audit with the probabilistic semantic audit. The whole value of this script is that it doesn’t pretend to do something it can’t.