Last active
May 12, 2026 02:01
-
-
Save douglaz/c7d31f5b5c8ddb96eaeb3c22a3ce6211 to your computer and use it in GitHub Desktop.
news-lens spec explainer (rev 4) — Wiki-grounded news commentary fork of news-tagger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en" class="bg-slate-950 text-slate-100"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>news-lens — spec rev 4</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: 'Inter', system-ui, sans-serif; } | |
| .mono, code, pre { font-family: 'JetBrains Mono', ui-monospace, monospace; } | |
| .arrow { position: relative; } | |
| .arrow::after { | |
| content: ''; | |
| position: absolute; | |
| left: 50%; | |
| bottom: -1.75rem; | |
| transform: translateX(-50%); | |
| width: 2px; | |
| height: 1.5rem; | |
| background: linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184 / 0.3)); | |
| } | |
| .arrow::before { | |
| content: ''; | |
| position: absolute; | |
| left: 50%; | |
| bottom: -1.85rem; | |
| transform: translateX(-50%); | |
| border-left: 6px solid transparent; | |
| border-right: 6px solid transparent; | |
| border-top: 8px solid rgb(148 163 184 / 0.3); | |
| } | |
| .strike { text-decoration: line-through; text-decoration-color: rgb(244 63 94); } | |
| </style> | |
| </head> | |
| <body class="antialiased selection:bg-amber-400 selection:text-slate-950"> | |
| <!-- =============== HERO =============== --> | |
| <header class="border-b border-slate-800/80 bg-gradient-to-b from-slate-900 to-slate-950"> | |
| <div class="max-w-6xl mx-auto px-6 py-16 sm:py-24"> | |
| <div class="flex flex-wrap items-center gap-3 text-xs uppercase tracking-widest text-slate-400 mb-6"> | |
| <span class="px-2 py-1 rounded bg-amber-500/10 text-amber-300 border border-amber-500/30">SPEC rev 4</span> | |
| <span class="px-2 py-1 rounded bg-slate-800 border border-slate-700">draft / pre-implementation</span> | |
| <span class="px-2 py-1 rounded bg-rose-500/10 text-rose-300 border border-rose-500/30">hard fork of news-tagger</span> | |
| </div> | |
| <h1 class="text-5xl sm:text-7xl font-bold tracking-tight"> | |
| <span class="mono">news-lens</span> | |
| </h1> | |
| <p class="mt-6 text-xl sm:text-2xl text-slate-300 max-w-3xl leading-relaxed"> | |
| Wiki-grounded news commentary from a defined editorial perspective — initially Austrian-libertarian, captured in | |
| <code class="text-amber-300">~/wiki/topics/libertarian/</code>. | |
| </p> | |
| <p class="mt-4 text-base text-slate-400 max-w-3xl"> | |
| One Rust fetch loop. One harness call per post. Wiki is the system of record. KISS. | |
| </p> | |
| </div> | |
| </header> | |
| <!-- =============== ONE-LINER PITCH =============== --> | |
| <section class="max-w-6xl mx-auto px-6 py-16 grid sm:grid-cols-2 gap-6"> | |
| <div class="rounded-lg bg-slate-900/60 border border-slate-800 p-6"> | |
| <h3 class="text-sm font-semibold text-emerald-400 uppercase tracking-wider">Goals</h3> | |
| <ul class="mt-4 space-y-3 text-slate-300"> | |
| <li class="flex gap-3"><span class="text-emerald-500 mono">·</span> Given a news item, produce commentary written from a defined editorial perspective.</li> | |
| <li class="flex gap-3"><span class="text-emerald-500 mono">·</span> Ground commentary in the wiki: cite specific articles, reuse framings, stay consistent.</li> | |
| <li class="flex gap-3"><span class="text-emerald-500 mono">·</span> Build a persistent archive of contextualized news.</li> | |
| <li class="flex gap-3"><span class="text-emerald-500 mono">·</span><span>Reuse <span class="mono">news-tagger</span>'s plumbing where it still earns its place.</span></li> | |
| </ul> | |
| </div> | |
| <div class="rounded-lg bg-slate-900/60 border border-slate-800 p-6"> | |
| <h3 class="text-sm font-semibold text-rose-400 uppercase tracking-wider">Non-goals</h3> | |
| <ul class="mt-4 space-y-3 text-slate-300"> | |
| <li class="flex gap-3"><span class="text-rose-500 mono">·</span><span>Multi-label classification into a fixed taxonomy — that's <span class="mono">news-tagger</span>'s job.</span></li> | |
| <li class="flex gap-3"><span class="text-rose-500 mono">·</span> Editorial perspectives outside what the connected wiki contains.</li> | |
| <li class="flex gap-3"><span class="text-rose-500 mono">·</span> A general-purpose RAG framework.</li> | |
| <li class="flex gap-3"><span class="text-rose-500 mono">·</span> Multi-perspective in one run, translation, images, embeddings, curate TUI.</li> | |
| </ul> | |
| </div> | |
| </section> | |
| <!-- =============== ARCHITECTURE DIAGRAM =============== --> | |
| <section class="border-y border-slate-800 bg-slate-900/30"> | |
| <div class="max-w-6xl mx-auto px-6 py-16 sm:py-20"> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">Architecture — single agent</h2> | |
| <p class="text-slate-400 mb-12">One harness invocation per post, period. Strictly serial.</p> | |
| <div class="grid lg:grid-cols-12 gap-6"> | |
| <!-- Pipeline column --> | |
| <div class="lg:col-span-7 space-y-7"> | |
| <!-- Step 1 --> | |
| <div class="arrow rounded-lg border border-slate-700 bg-slate-900 p-5"> | |
| <div class="flex items-start gap-4"> | |
| <div class="flex-shrink-0 w-10 h-10 rounded-full bg-indigo-500/20 border border-indigo-500/40 flex items-center justify-center font-bold text-indigo-300">1</div> | |
| <div> | |
| <h4 class="font-semibold text-slate-100">Fetch loop (Rust)</h4> | |
| <p class="text-sm text-slate-400 mt-1">Pull next new post from X / Nostr / JSONL fixture. One at a time. No mutex, no parallel tasks.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 2 --> | |
| <div class="arrow rounded-lg border border-amber-500/40 bg-amber-500/[0.04] p-5"> | |
| <div class="flex items-start gap-4"> | |
| <div class="flex-shrink-0 w-10 h-10 rounded-full bg-amber-500/20 border border-amber-500/50 flex items-center justify-center font-bold text-amber-300">2</div> | |
| <div class="flex-1"> | |
| <h4 class="font-semibold text-amber-200">Shell out to harness <span class="text-xs text-slate-400 font-normal">— the single agentic call</span></h4> | |
| <p class="text-sm text-slate-400 mt-1 mb-3"><code class="text-amber-300 mono">claude --print</code> / <code class="text-amber-300 mono">codex</code> / <code class="text-amber-300 mono">opencode</code> with <code class="text-amber-300 mono">prompts/process-post.md</code>.</p> | |
| <ol class="mt-2 space-y-1.5 text-sm text-slate-300 list-decimal list-inside marker:text-amber-400"> | |
| <li>Read the lens.</li> | |
| <li>File the post as <code class="text-amber-300 mono">raw/news/<slug>.md</code> via <code class="text-amber-300 mono">/wiki:ingest</code>.</li> | |
| <li>Decide if newsworthy enough to comment.</li> | |
| <li class="text-slate-400">If yes: | |
| <ul class="ml-6 mt-1 space-y-1 list-disc marker:text-slate-600 text-slate-400"> | |
| <li>Read 5–12 relevant articles from <code class="mono">wiki/{concepts,topics,references}/</code>.</li> | |
| <li>Draft commentary with <code class="mono">[[wikilinks]]</code> citations.</li> | |
| <li>Write <code class="mono">wiki/theses/<slug>.md</code>.</li> | |
| </ul> | |
| </li> | |
| <li>Run <code class="text-amber-300 mono">/wiki:lint --fix</code>.</li> | |
| <li>Print final-line JSON.</li> | |
| </ol> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 3 --> | |
| <div class="arrow rounded-lg border border-slate-700 bg-slate-900 p-5"> | |
| <div class="flex items-start gap-4"> | |
| <div class="flex-shrink-0 w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/40 flex items-center justify-center font-bold text-emerald-300">3</div> | |
| <div> | |
| <h4 class="font-semibold text-slate-100">Render & publish <span class="text-xs text-slate-400 font-normal">— only if a thesis was produced</span></h4> | |
| <p class="text-sm text-slate-400 mt-1">Pure-Rust template renders the reply from <code class="mono">one_liner</code>. Publish via existing X/Nostr publisher. Subject to <code class="mono">--dry-run</code> and <code class="mono">--require-approval</code>.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 4 --> | |
| <div class="rounded-lg border border-slate-700 bg-slate-900 p-5"> | |
| <div class="flex items-start gap-4"> | |
| <div class="flex-shrink-0 w-10 h-10 rounded-full bg-slate-700 border border-slate-600 flex items-center justify-center font-bold text-slate-300">4</div> | |
| <div> | |
| <h4 class="font-semibold text-slate-100">Record</h4> | |
| <p class="text-sm text-slate-400 mt-1">Insert into <code class="mono">processed_posts</code>: post_id, lens_id, stance, raw_path, thesis_slug, platform IDs. Loop.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right column: source of truth --> | |
| <div class="lg:col-span-5 space-y-6"> | |
| <div class="rounded-lg border border-slate-700 bg-slate-950 p-5"> | |
| <h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider">Split source of truth</h4> | |
| <div class="mt-4 space-y-4"> | |
| <div class="border-l-2 border-amber-500 pl-4"> | |
| <div class="text-xs uppercase tracking-wider text-amber-400 font-semibold">Filesystem (the wiki)</div> | |
| <div class="mt-1 text-sm text-slate-300">raw news, theses, indexes, log.md, See Also backlinks</div> | |
| </div> | |
| <div class="border-l-2 border-sky-500 pl-4"> | |
| <div class="text-xs uppercase tracking-wider text-sky-400 font-semibold">state.sqlite</div> | |
| <div class="mt-1 text-sm text-slate-300">fetch cursor, X / Nostr post IDs, what news-lens did per post</div> | |
| </div> | |
| </div> | |
| <p class="mt-4 text-xs text-slate-500 italic">"Has post X been processed?" is a PK lookup. "What did we publish for thesis Y?" is a slug filter.</p> | |
| </div> | |
| <div class="rounded-lg border border-slate-700 bg-slate-950 p-5"> | |
| <h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider">The lens</h4> | |
| <p class="mt-3 text-sm text-slate-300">A short markdown file separate from the wiki, declaring voice, register, and the stance vocabulary.</p> | |
| <ul class="mt-4 space-y-2 text-sm"> | |
| <li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-300 text-xs font-semibold mono">ENDORSE</span><span class="text-slate-400">news reports facts that confirm a wiki claim.</span></li> | |
| <li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-rose-500/15 text-rose-300 text-xs font-semibold mono">CRITIQUE</span><span class="text-slate-400">news's framing assumes premises the wiki rejects.</span></li> | |
| <li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-sky-500/15 text-sky-300 text-xs font-semibold mono">CONTEXT</span><span class="text-slate-400">news is descriptive, wiki adds frame.</span></li> | |
| <li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">DECLINE</span><span class="text-slate-400">no wiki article speaks to the substance.</span></li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- =============== KEEP / DROP =============== --> | |
| <section class="max-w-6xl mx-auto px-6 py-16"> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">What stays, what goes</h2> | |
| <p class="text-slate-400 mb-10">Roughly ≈ 2000 LOC deleted. The harness owns provider selection — news-lens does not.</p> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <!-- KEEP --> | |
| <div class="rounded-lg border border-emerald-500/30 bg-emerald-500/[0.03]"> | |
| <div class="px-5 py-3 border-b border-emerald-500/30 flex items-center justify-between"> | |
| <h3 class="font-semibold text-emerald-300">Keep</h3> | |
| <span class="text-xs text-emerald-400/70 mono">§3</span> | |
| </div> | |
| <div class="divide-y divide-emerald-500/10 text-sm"> | |
| <div class="px-5 py-3"><code class="mono text-emerald-300">SourcePost</code> — the post type</div> | |
| <div class="px-5 py-3"><code class="mono text-emerald-300">PostSource</code> port + Jsonl / X / Stub sources</div> | |
| <div class="px-5 py-3"><code class="mono text-emerald-300">Publisher</code> port + X / Nostr / outbox</div> | |
| <div class="px-5 py-3"><code class="mono text-emerald-300">StateStore</code> port + sqlite / memory (simpler schema)</div> | |
| <div class="px-5 py-3"><code class="mono text-emerald-300">Clock</code>, <code class="mono text-emerald-300">RateLimiter</code>, ignore-pattern compilation</div> | |
| <div class="px-5 py-3"><code class="mono text-emerald-300">AppConfig</code> infra, env-var loading, doctor</div> | |
| <div class="px-5 py-3">clap subcommand scaffolding (replace bodies)</div> | |
| </div> | |
| </div> | |
| <!-- DROP --> | |
| <div class="rounded-lg border border-rose-500/30 bg-rose-500/[0.03]"> | |
| <div class="px-5 py-3 border-b border-rose-500/30 flex items-center justify-between"> | |
| <h3 class="font-semibold text-rose-300">Drop</h3> | |
| <span class="text-xs text-rose-400/70 mono">§4</span> | |
| </div> | |
| <div class="divide-y divide-rose-500/10 text-sm"> | |
| <div class="px-5 py-3"><code class="mono text-rose-300">TagDefinition</code>, <code class="mono text-rose-300">Taxonomy</code>, <code class="mono text-rose-300">taxonomy_hash</code></div> | |
| <div class="px-5 py-3"><code class="mono text-rose-300">ClassifyOutput</code>, <code class="mono text-rose-300">TagMatch</code>, classify use case</div> | |
| <div class="px-5 py-3"><code class="mono text-rose-300">render.rs</code> — agent renders prose</div> | |
| <div class="px-5 py-3"><code class="mono text-rose-300">curate.rs</code> TUI</div> | |
| <div class="px-5 py-3"><code class="mono text-rose-300">policy.forbidden_patterns</code> — lives in the lens now</div> | |
| <div class="px-5 py-3 bg-rose-500/[0.06]"><code class="mono text-rose-300">adapters/llm/{anthropic,openai,gemini,ollama,opencode,openai_compat,claude_code,codex,local_command}.rs</code> <span class="text-rose-400/70 text-xs"> ~2000 LOC</span></div> | |
| <div class="px-5 py-3"><code class="mono text-rose-300">FsWikiWriter</code> — agent calls <code class="mono">/wiki:ingest</code> directly</div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- =============== AGENT RETURN CONTRACT =============== --> | |
| <section class="border-y border-slate-800 bg-slate-900/30"> | |
| <div class="max-w-6xl mx-auto px-6 py-16 grid lg:grid-cols-2 gap-10 items-start"> | |
| <div> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">Agent return contract</h2> | |
| <p class="text-slate-400 mb-6">The agent prints exactly one JSON object as its final stdout line. news-lens parses, validates, then publishes — never the other way around.</p> | |
| <h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3">Validation before publish</h4> | |
| <ul class="space-y-2 text-sm text-slate-300"> | |
| <li class="flex gap-3"><span class="mono text-amber-400">·</span><span><code class="mono">stance</code> is one of <code class="mono text-amber-300">endorse | critique | contextualize | decline | failed</code></span></li> | |
| <li class="flex gap-3"><span class="mono text-amber-400">·</span><span><code class="mono">raw_path</code> exists on disk</span></li> | |
| <li class="flex gap-3"><span class="mono text-amber-400">·</span><span>if not <code class="mono">decline</code>: <code class="mono">thesis_path</code>, <code class="mono">thesis_slug</code>, <code class="mono">one_liner</code> present, path exists</span></li> | |
| <li class="flex gap-3"><span class="mono text-amber-400">·</span><span><code class="mono">one_liner.len() ≤ 240</code> (truncate gently if not)</span></li> | |
| </ul> | |
| <h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mt-8 mb-3">Failure mode</h4> | |
| <p class="text-sm text-slate-300">Malformed JSON, missing path, timeout, any harness error → record <code class="mono text-rose-300">stance='failed'</code>, move on. No retries, no quarantine. Re-run by hand via <code class="mono">news-lens process --post <id></code>.</p> | |
| </div> | |
| <pre class="rounded-lg border border-slate-700 bg-slate-950 p-5 text-sm overflow-x-auto mono leading-relaxed"><span class="text-slate-500">// agent prints exactly this on the final stdout line</span> | |
| { | |
| <span class="text-amber-300">"stance"</span>: <span class="text-emerald-300">"critique"</span>, | |
| <span class="text-amber-300">"raw_path"</span>: <span class="text-emerald-300">"raw/news/2026-05-07-argentina-rent-decontrol.md"</span>, | |
| <span class="text-amber-300">"raw_slug"</span>: <span class="text-emerald-300">"2026-05-07-argentina-rent-decontrol"</span>, | |
| <span class="text-amber-300">"thesis_path"</span>: <span class="text-emerald-300">"wiki/theses/on-argentina-rent-decontrol.md"</span>, | |
| <span class="text-amber-300">"thesis_slug"</span>: <span class="text-emerald-300">"on-argentina-rent-decontrol"</span>, | |
| <span class="text-amber-300">"one_liner"</span>: <span class="text-emerald-300">"The cap simply cuts the supply they were</span> | |
| <span class="text-emerald-300"> going to consume — see [[state-power-and-intervention]]."</span> | |
| }</pre> | |
| </div> | |
| </section> | |
| <!-- =============== STATE SCHEMA =============== --> | |
| <section class="max-w-6xl mx-auto px-6 py-16 grid lg:grid-cols-2 gap-10 items-start"> | |
| <div> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">State schema</h2> | |
| <p class="text-slate-400 mb-6">Two tables. Intentionally tiny. Reconciliation with the wiki is a separate <code class="mono">wiki status</code> op — never on the hot path.</p> | |
| <div class="grid grid-cols-2 gap-4 text-sm"> | |
| <div class="rounded border border-slate-800 bg-slate-900 p-4"> | |
| <div class="text-xs font-semibold text-slate-400 uppercase tracking-wider">No</div> | |
| <ul class="mt-2 space-y-1 text-slate-400"> | |
| <li>cost_usd column</li> | |
| <li>taxonomy_hash</li> | |
| <li>retry_count</li> | |
| <li>quarantine flag</li> | |
| </ul> | |
| </div> | |
| <div class="rounded border border-slate-800 bg-slate-900 p-4"> | |
| <div class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Yes</div> | |
| <ul class="mt-2 space-y-1 text-slate-300"> | |
| <li>processed_posts</li> | |
| <li>account_state</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <pre class="rounded-lg border border-slate-700 bg-slate-950 p-5 text-sm overflow-x-auto mono leading-relaxed"><span class="text-sky-300">CREATE TABLE</span> processed_posts ( | |
| post_id <span class="text-amber-300">TEXT PRIMARY KEY</span>, | |
| lens_id <span class="text-amber-300">TEXT NOT NULL</span>, | |
| processed_at <span class="text-amber-300">TEXT NOT NULL</span>, | |
| stance <span class="text-amber-300">TEXT NOT NULL</span>, <span class="text-slate-500">-- endorse|critique|contextualize|decline|failed</span> | |
| raw_path <span class="text-amber-300">TEXT</span>, <span class="text-slate-500">-- relative to wiki root</span> | |
| thesis_slug <span class="text-amber-300">TEXT</span>, <span class="text-slate-500">-- nullable when stance=decline|failed</span> | |
| x_post_id <span class="text-amber-300">TEXT</span>, | |
| nostr_event_id <span class="text-amber-300">TEXT</span> | |
| ); | |
| <span class="text-sky-300">CREATE TABLE</span> account_state ( | |
| account <span class="text-amber-300">TEXT PRIMARY KEY</span>, | |
| since_id <span class="text-amber-300">TEXT</span>, | |
| updated_at <span class="text-amber-300">TEXT NOT NULL</span> | |
| );</pre> | |
| </section> | |
| <!-- =============== LOCKED DECISIONS =============== --> | |
| <section class="border-y border-slate-800 bg-slate-900/30"> | |
| <div class="max-w-6xl mx-auto px-6 py-16"> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">Locked decisions</h2> | |
| <p class="text-slate-400 mb-10">Settled. The rest of the spec elaborates them. Struck-through items were locked earlier, then superseded.</p> | |
| <div class="grid md:grid-cols-2 gap-x-8 gap-y-3 text-sm"> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">1.</span><span>Hard fork of <code class="mono">news-tagger</code>, not a mode added to it.</span></div> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">2.</span><span>Single-wiki for v1. One topic-wiki path, one lens.</span></div> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">3.</span><span>The wiki is the system of record. news-lens is a contributor.</span></div> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">4.</span><span>Single agentic call per post. One harness invocation does ingest + commentary + lint.</span></div> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">5.</span><span>No upstream skill changes. Hand-crafted prompt orchestrates existing skills.</span></div> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">6.</span><span>Block-then-publish. Replies only after agent confirms thesis slug.</span></div> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">7.</span><span>Split source of truth: filesystem for wiki facts, <code class="mono">state.sqlite</code> for platform facts.</span></div> | |
| <div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">8.</span><span class="font-semibold text-amber-200">KISS for v1: smallest viable answer for every open question.</span></div> | |
| <div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">9.</span><span class="strike">Three LLM roles with separate config blocks.</span></div> | |
| <div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">10.</span><span class="strike">Three-pass CLI (ingest / comment / publish).</span></div> | |
| <div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">11.</span><span class="strike">Outbox staging directory + slug-claim mechanism.</span></div> | |
| <div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">12.</span><span class="strike">Per-wiki mutex / concurrency control.</span></div> | |
| <div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">13.</span><span class="strike">Per-call cost tracking + daily budget breaker.</span></div> | |
| <div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">14.</span><span class="strike">Quarantine + auto-retry on agent failure.</span></div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- =============== PHASED ROLLOUT =============== --> | |
| <section class="max-w-6xl mx-auto px-6 py-16"> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">Phased rollout</h2> | |
| <p class="text-slate-400 mb-10">Each phase is independently shippable. We do not graduate until the previous one earns trust.</p> | |
| <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| <div class="rounded-lg border border-slate-700 bg-slate-900 p-5"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="px-2 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">PHASE 0</span> | |
| <span class="text-xs text-slate-500">done</span> | |
| </div> | |
| <h4 class="font-semibold">Spec lock-in</h4> | |
| <p class="mt-2 text-sm text-slate-400">This doc. Rev 4.</p> | |
| </div> | |
| <div class="rounded-lg border border-indigo-500/40 bg-indigo-500/[0.05] p-5"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="px-2 py-0.5 rounded bg-indigo-500/20 text-indigo-300 text-xs font-semibold mono">PHASE 1</span> | |
| <span class="text-xs text-indigo-400">smoke</span> | |
| </div> | |
| <h4 class="font-semibold">One-shot agent</h4> | |
| <p class="mt-2 text-sm text-slate-400"><code class="mono text-indigo-300">process --post --text "..."</code> against a real wiki. No publishing, no DB, no fetching.</p> | |
| <p class="mt-3 text-xs text-slate-500 italic">Goal: see whether the prompt produces clean lint output and useful commentary.</p> | |
| </div> | |
| <div class="rounded-lg border border-slate-700 bg-slate-900 p-5"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="px-2 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">PHASE 2</span> | |
| <span class="text-xs text-slate-500">fixture</span> | |
| </div> | |
| <h4 class="font-semibold">Fetch + state</h4> | |
| <p class="mt-2 text-sm text-slate-400"><code class="mono">process --jsonl <fixture></code> processes a batch. Records to state DB. Still no publishing.</p> | |
| </div> | |
| <div class="rounded-lg border border-slate-700 bg-slate-900 p-5"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="px-2 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">PHASE 3</span> | |
| <span class="text-xs text-slate-500">live read</span> | |
| </div> | |
| <h4 class="font-semibold">Run loop, dry-run</h4> | |
| <p class="mt-2 text-sm text-slate-400"><code class="mono">run --dry-run</code> polls X, processes posts, writes to wiki, does not publish. Eyeball for a week.</p> | |
| </div> | |
| <div class="rounded-lg border border-emerald-500/40 bg-emerald-500/[0.05] p-5"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-300 text-xs font-semibold mono">PHASE 4</span> | |
| <span class="text-xs text-emerald-400">approval</span> | |
| </div> | |
| <h4 class="font-semibold">Publish</h4> | |
| <p class="mt-2 text-sm text-slate-400">X / Nostr in <code class="mono text-emerald-300">--require-approval</code> mode (existing outbox). Every commentary reviewed before going out. Stay here a week.</p> | |
| </div> | |
| <div class="rounded-lg border border-slate-800 bg-slate-950/60 p-5"> | |
| <div class="flex items-center justify-between mb-3"> | |
| <span class="px-2 py-0.5 rounded bg-slate-800 text-slate-400 text-xs font-semibold mono">PHASE 5</span> | |
| <span class="text-xs text-slate-600">re-evaluate</span> | |
| </div> | |
| <h4 class="font-semibold text-slate-300">Split out what measurement demands</h4> | |
| <ul class="mt-2 text-sm text-slate-400 space-y-1"> | |
| <li>· Cost too high → cheap prefilter back</li> | |
| <li>· Bad retrieval → Rust-side retrieval back</li> | |
| <li>· JSON drift → direct API with <code class="mono">response_format</code></li> | |
| </ul> | |
| <p class="mt-3 text-xs text-slate-500 italic">None requires rearchitecture. Each is a localized split.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- =============== KISS DEFERRED TABLE =============== --> | |
| <section class="border-y border-slate-800 bg-slate-900/30"> | |
| <div class="max-w-6xl mx-auto px-6 py-16"> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">Deferred decisions</h2> | |
| <p class="text-slate-400 mb-10">Every question below has a v1 default that picks the smallest viable answer. Each "no" in v1 saves real code — and becomes a localized addition when measurement justifies it.</p> | |
| <div class="overflow-x-auto rounded-lg border border-slate-800"> | |
| <table class="w-full text-sm"> | |
| <thead class="bg-slate-900 border-b border-slate-800 text-slate-400"> | |
| <tr> | |
| <th class="text-left font-semibold px-4 py-3 w-12">#</th> | |
| <th class="text-left font-semibold px-4 py-3">Question</th> | |
| <th class="text-left font-semibold px-4 py-3">v1 default</th> | |
| <th class="text-left font-semibold px-4 py-3">Revisit trigger</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-800 text-slate-300"> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">1</td> | |
| <td class="px-4 py-3 font-medium">News dedup</td> | |
| <td class="px-4 py-3 text-slate-400">No dedup. Process every post.</td> | |
| <td class="px-4 py-3 text-slate-400"><code class="mono">wiki status</code> reports noticeable duplication.</td> | |
| </tr> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">2</td> | |
| <td class="px-4 py-3 font-medium">What counts as news worth fetching</td> | |
| <td class="px-4 py-3 text-slate-400">Existing pre-fetch filters. Agent <span class="mono">Decline</span> handles the rest.</td> | |
| <td class="px-4 py-3 text-slate-400">Decline rate > ≈80% → add <code class="mono">min_text_chars</code>.</td> | |
| </tr> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">3</td> | |
| <td class="px-4 py-3 font-medium">Lens versioning</td> | |
| <td class="px-4 py-3 text-slate-400">No hash, no <code class="mono">--force</code>. Manual <code class="mono">DELETE</code> + rerun.</td> | |
| <td class="px-4 py-3 text-slate-400">Doing it more than once a month.</td> | |
| </tr> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">4</td> | |
| <td class="px-4 py-3 font-medium">Harness failure modes</td> | |
| <td class="px-4 py-3 text-slate-400">No retries, no quarantine. <code class="mono">stance='failed'</code>, move on.</td> | |
| <td class="px-4 py-3 text-slate-400">Transient errors drop posts you wanted.</td> | |
| </tr> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">5</td> | |
| <td class="px-4 py-3 font-medium">Cost runaway</td> | |
| <td class="px-4 py-3 text-slate-400"><code class="mono">timeout_secs</code> only. Watch provider dashboard.</td> | |
| <td class="px-4 py-3 text-slate-400">First surprise bill → add <code class="mono">max_posts_per_day</code>.</td> | |
| </tr> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">6</td> | |
| <td class="px-4 py-3 font-medium">Concurrency</td> | |
| <td class="px-4 py-3 text-slate-400">Strictly serial.</td> | |
| <td class="px-4 py-3 text-slate-400">Throughput becomes the bottleneck.</td> | |
| </tr> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">7</td> | |
| <td class="px-4 py-3 font-medium">Thesis re-edit on developing stories</td> | |
| <td class="px-4 py-3 text-slate-400">Never. New news → new thesis with cross-links.</td> | |
| <td class="px-4 py-3 text-slate-400">Story-chain repetition is visibly noisy.</td> | |
| </tr> | |
| <tr class="hover:bg-slate-900/50"> | |
| <td class="px-4 py-3 mono text-slate-500">8</td> | |
| <td class="px-4 py-3 font-medium">Agent retrieval discipline</td> | |
| <td class="px-4 py-3 text-slate-400">Trust the prompt's "read 5–12 articles" line.</td> | |
| <td class="px-4 py-3 text-slate-400"><code class="mono">timeout_secs</code> keeps hitting.</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- =============== CLI SURFACE =============== --> | |
| <section class="max-w-6xl mx-auto px-6 py-16"> | |
| <h2 class="text-3xl font-bold tracking-tight mb-2">CLI surface</h2> | |
| <p class="text-slate-400 mb-10"><code class="mono">fetch</code>, <code class="mono">classify</code>, <code class="mono">curate</code> are dropped. Standard flags: <code class="mono">--dry-run</code>, <code class="mono">--require-approval</code>, <code class="mono">--config <path></code>.</p> | |
| <div class="rounded-lg border border-slate-800 overflow-hidden"> | |
| <table class="w-full text-sm"> | |
| <thead class="bg-slate-900 border-b border-slate-800 text-slate-400"> | |
| <tr> | |
| <th class="text-left font-semibold px-4 py-3">Command</th> | |
| <th class="text-left font-semibold px-4 py-3">Behavior</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-800 text-slate-300"> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens run</td><td class="px-4 py-3 text-slate-400">Fetch new posts → agent call + optional publish → loop.</td></tr> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens run --once</td><td class="px-4 py-3 text-slate-400">One poll cycle.</td></tr> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens process --post <id-or-text></td><td class="px-4 py-3 text-slate-400">Process a single post (fetched-by-id or ad-hoc text).</td></tr> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens process --jsonl <path></td><td class="px-4 py-3 text-slate-400">Process posts from a fixture file.</td></tr> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens wiki status</td><td class="px-4 py-3 text-slate-400">Wiki path, raw news count, theses count, uncommented news count.</td></tr> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens lens list | show <id></td><td class="px-4 py-3 text-slate-400">Inspect lens files.</td></tr> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens doctor</td><td class="px-4 py-3 text-slate-400">Existing doctor + wiki readable, lens parseable, harness reachable.</td></tr> | |
| <tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens config init</td><td class="px-4 py-3 text-slate-400">Generate config skeleton.</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| <!-- =============== FOOTER =============== --> | |
| <footer class="border-t border-slate-800 bg-slate-950"> | |
| <div class="max-w-6xl mx-auto px-6 py-12 grid sm:grid-cols-2 gap-8 items-start"> | |
| <div> | |
| <h3 class="font-semibold text-slate-200">Next steps</h3> | |
| <ol class="mt-4 space-y-2 text-sm text-slate-400 list-decimal list-inside marker:text-amber-400"> | |
| <li>Stand up <code class="mono">news-lens</code> repo from a fork of <code class="mono">news-tagger@master</code>.</li> | |
| <li>Delete §4 components.</li> | |
| <li>Add the harness subprocess adapter.</li> | |
| <li>Write <code class="mono">prompts/process-post.md</code> and iterate against a stub wiki.</li> | |
| <li>Implement <code class="mono">process --post --text "..."</code> end-to-end. (= Phase 1.)</li> | |
| <li>Wire post sources, state store, publishers back in. (= Phases 2–4.)</li> | |
| </ol> | |
| </div> | |
| <div class="text-sm text-slate-500"> | |
| <div class="font-semibold text-slate-300 mb-2">Working name</div> | |
| <p><span class="mono text-slate-200">news-lens</span>. Bikeshed later.</p> | |
| <div class="mt-6 font-semibold text-slate-300 mb-2">Estimated effort</div> | |
| <p>Phase 1: a couple of days. The codebase shrinks dramatically. Phases 2–4 are where the prompt engineering lives.</p> | |
| </div> | |
| </div> | |
| <div class="border-t border-slate-800/80 bg-slate-950/80"> | |
| <div class="max-w-6xl mx-auto px-6 py-4 text-xs text-slate-600 flex flex-wrap items-center justify-between gap-2"> | |
| <span>SPEC-libertarian.md — rev 4 — 368 lines</span> | |
| <span class="mono">draft / pre-implementation</span> | |
| </div> | |
| </div> | |
| </footer> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment