02-sops

newsletter output invariants

2026-04-19·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

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

General 06-reference/ filing contract

This section covers ALL direct-Write files in ~/rdco-vault/06-reference/, regardless of source (X essays, Anthropic docs, vendor explainers, synthesis catalogs, research briefs, book notes, or any other ad-hoc write). The newsletter-specific invariants above (I1–I13) apply only to files produced by /process-newsletter. The general contract below applies universally.

CLAUDE.md guardrail: Extending this contract into a CLAUDE.md hard rule (pre-write check before any 06-reference/ Write) is an open architecture decision pending founder approval. Until then, this section is the canonical reference; sub-agent dispatches should explicitly cite it in their prompts.

Required frontmatter fields

Field Constraint
date ISO-8601 date (YYYY-MM-DD). Never omit.
type One of: reference, research-brief, book-note, synthesis. Use reference for newsletter and X/social sources; research-brief for deep-research outputs; book-note for book chapter notes; synthesis for cross-source synthesis.
content_type Short label describing the original source shape: e.g. newsletter, x-essay, vendor-doc, arxiv-paper, book-chapter, synthesis-catalog.
tags Non-empty list with ≥ 2 entries.

Newsletter-specific fields (source, author, newsletter_format, sponsored) are required when content_type: newsletter and already covered by I3–I6. For non-newsletter files, sponsored: bool + sponsor_entity are required whenever the source has an undisclosed or self-promotional financial interest.

Required body headings

Every 06-reference/ file MUST contain:

  1. ## Why this is in the vault — one or two sentences: what makes this worth the retrieval cost? A file without a stated reason is unauditable.
  2. ## Mapping against Ray Data Co (or ## Mapping against RDCO) — the load-bearing section per vault canon. Without a mapping argument, the file didn't justify being filed.
  3. ## Related — ≥ 2 wikilinks in [[basename]] format. Full ~/ paths ([[~/rdco-vault/...]]) are not valid wikilinks and will break vault navigation. Use [[basename]] only.

Sponsored-content handling: when sponsored: true or when a self-promotional financial interest is present, add a ## ⚠️ Sponsorship or ## ⚠️ Structural Bias section before the core-argument section — same rule as I11 for newsletters, now universal.

Applies to

Link to newsletter-specific section

The newsletter-specific invariants (I1–I13) above enforce a subset of this contract for /process-newsletter outputs and add source/sender-matching checks not applicable to other sources. The general contract is the floor; I1–I13 add a stricter ceiling for newsletter files.


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.

Changelog

Date Change
2026-04-19 Initial SOP — 13 invariants for /process-newsletter newsletter outputs
2026-04-19 Extended newsletter_format allowed values to add business-history and founder-interview; raised frontmatter-block scan window from 30 to 80 lines
2026-06-11 Added General 06-reference/ filing contract section covering all direct-Write files in 06-reference/ regardless of source. Defines required frontmatter fields (date, type, content_type, tags), required body headings (## Why this is in the vault, ## Mapping against Ray Data Co, ## Related ≥2 [[basename]] wikilinks), and sponsored-content handling for non-newsletter files. CLAUDE.md guardrail pending founder approval.