02-sops

newsletter output invariants

Sat Apr 18 2026 20:00:00 GMT-0400 (Eastern Daylight Time) ·sop

/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

IDNameWhat it checks
I1frontmatter-block-existsFile 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)
I2frontmatter-valid-yamlThe frontmatter block parses cleanly with yaml.safe_load() and yields a mapping
I3required-fields-presentFrontmatter contains: date, type, source, author, newsletter_format, sponsored, tags (source_url is recommended-not-required)
I4type-equals-referenceThe type field equals exactly "reference" (vault convention for newsletter notes)
I5newsletter-format-allowednewsletter_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)
I6sponsored-is-booleansponsored parses as a Python bool, not a string like "true"
I7tags-non-empty-min-2tags is a non-empty list (or non-empty comma-separated string) with at least 2 entries
I8mapping-section-presentBody 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.
I9why-section-presentBody contains a ## Why this is in the vault heading
I10filename-sender-matches-sourceThe filename’s leading sender slug appears (loose substring / normalized form) in the source: frontmatter field
I11sponsorship-section-when-sponsoredWhen sponsored: true, body has a ## Sponsorship (or ## ⚠️ Sponsorship) section per the skill’s bias-disclosure rule
I12curation-section-when-curation-or-hybridWhen newsletter_format is curation or hybrid, body has a ## Curation section subsection per the skill’s curation-link-labeling rule
I13no-duplicate-source-dateNo 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:

  1. Single concern. One invariant = one structural property. If you find yourself writing or between two unrelated checks, split them.
  2. 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.
  3. Named I<N>. Sequential, never reused. If you retire an invariant, leave its ID dead and don’t recycle it.
  4. Document in this SOP. Add a row to the invariants table above with the same name as the script’s INVARIANTS dict entry.
  5. Add to the script. Implement in ~/.claude/scripts/audit-newsletter-outputs.py, return (invariant_id, message) tuples on failure, empty on pass.
  6. 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:

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.