← All commits
AI · claude-opus-4-7radosukala90cdb82Our-One/our-one

P.9: Comparisons Draft-with-Opus drafter

+495 / 87 files

Files changed

Diff

Lines reveal in sequence as you scroll. First 20 lines per file shown — expand for the rest.

modifiedsrc/app/hall/admin/comparisons/[id]/edit/page.tsx+9 / 0tsx
@@ -23,6 +23,7 @@ interface Props {
aiNotePublished?: string;
aiNoteDiscarded?: string;
operatorResponseSaved?: string;
+ opusDrafted?: string;
}>;
}
 
@@ -38,6 +39,7 @@ export default async function EditComparisonPage({ params, searchParams }: Props
aiNotePublished,
aiNoteDiscarded,
operatorResponseSaved,
+ opusDrafted,
} = await searchParams;
 
const db = getDb();
@@ -106,6 +108,13 @@ export default async function EditComparisonPage({ params, searchParams }: Props
Operator response saved.
</p>
)}
new filesrc/app/hall/admin/comparisons/draft-with-opus/page.tsx+125 / 0tsx
@@ -0,0 +1,125 @@
+import Link from "next/link";
+import { getDb } from "@/shared/db/client";
+import { incumbents } from "@/shared/db/schema/incumbents";
+import { listFeatures } from "@/shared/features/queries";
+import { draftComparisonWithOpusAction } from "@/shared/comparisons/actions";
+ 
+export const metadata = { title: "Draft a Comparison with Opus" };
+export const dynamic = "force-dynamic";
+ 
+export default async function DraftWithOpusPage() {
+ const db = getDb();
+ const [incumbentRows, featureRows] = await Promise.all([
+ db.select().from(incumbents).orderBy(incumbents.displayName),
+ listFeatures({ limit: 500 }).catch(() => []),
+ ]);
+ 
+ return (
+ <div className="px-6 py-12 md:py-16">
+ <div className="mx-auto max-w-[44rem]">
modifiedsrc/app/hall/admin/comparisons/new/page.tsx+10 / 0tsx
@@ -33,6 +33,16 @@ export default async function NewComparisonPage() {
citations are in place — the publish action validates that ≥2
citations have non-empty quotes.
</p>
+ <p className="mt-3 font-sans text-xs text-stone-500">
+ Faster path:{" "}
+ <Link
+ href="/hall/admin/comparisons/draft-with-opus"
+ className="font-medium text-stone-900 underline decoration-stone-300 underline-offset-4 hover:decoration-stone-900"
+ >
+ draft the four prose sections with Opus
+ </Link>
+ {" "}— Claude returns a typed draft in ~1020 sec, you add citations and edit.
+ </p>
 
{incumbentRows.length === 0 ? (
<div className="mt-8 rounded-xl border border-amber-200 bg-amber-50 px-6 py-5">
modifiedsrc/app/hall/admin/comparisons/page.tsx+15 / 7tsx
@@ -37,16 +37,24 @@ export default async function AdminComparisonsIndexPage() {
return (
<div className="px-6 py-12 md:py-16">
<div className="mx-auto max-w-[64rem]">
<div className="flex items-baseline justify-between">
+ <div className="flex flex-wrap items-baseline justify-between gap-3">
<h1 className="font-serif text-3xl font-bold tracking-tight text-stone-900 md:text-4xl">
Comparisons
</h1>
<Link
href="/hall/admin/comparisons/new"
className="rounded-md bg-stone-900 px-4 py-2 font-sans text-sm font-medium text-[#FDFBF7] hover:bg-stone-700"
>
New Comparison →
</Link>
+ <div className="flex items-center gap-2">
+ <Link
+ href="/hall/admin/comparisons/draft-with-opus"
+ className="rounded-md border border-stone-900 bg-white px-4 py-2 font-sans text-sm font-medium text-stone-900 hover:bg-stone-100"
+ >
modifiedsrc/app/hall/admin/layout.tsx+1 / 0tsx
@@ -32,6 +32,7 @@ export default async function AdminLayout({
<span className="text-stone-300">·</span>
<Link href="/hall/admin/comparisons" className="text-stone-700 hover:text-stone-900">Comparisons</Link>
<Link href="/hall/admin/comparisons/new" className="text-stone-700 hover:text-stone-900">New comparison</Link>
+ <Link href="/hall/admin/comparisons/draft-with-opus" className="text-stone-700 hover:text-stone-900">Draft w/ Opus</Link>
<span className="text-stone-300">·</span>
<Link href="/hall/admin/questions" className="text-stone-700 hover:text-stone-900">Questions</Link>
<Link href="/hall/admin/questions/new" className="text-stone-700 hover:text-stone-900">New question</Link>
modifiedsrc/shared/comparisons/actions.ts+109 / 1typescript
@@ -29,7 +29,11 @@ import {
findComparisonBySlug,
listFeaturesForComparison,
} from "./queries";
import { findFeatureBySlug } from "@/shared/features/queries";
+import {
+ findFeatureBySlug,
+ listCommitmentTiesForFeature,
+} from "@/shared/features/queries";
+import { draftComparisonWithOpus } from "./opus-drafter";
 
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
const URL_RE = /^https?:\/\//i;
@@ -447,6 +451,110 @@ export async function createMemberDraftComparisonAction(
redirect(`/features/${primaryFeatureSlug}?draftSubmitted=1`);
}
 
+/**
+ * Operator-triggered Opus draft. Inserts a Comparison row in status=draft
+ * with the four prose sections filled in by Claude and citations left
new filesrc/shared/comparisons/opus-drafter.ts+226 / 0typescript
@@ -0,0 +1,226 @@
+import Anthropic from "@anthropic-ai/sdk";
+import { COMMITMENTS } from "@/shared/constitution";
+ 
+const MODEL = "claude-opus-4-7";
+const MAX_TOKENS = 3000;
+ 
+const SYSTEM_PROMPT = `You are drafting a Comparison for Our.one — a public, cited contrast against a named incumbent.
+ 
+Our.one is a parent company for user-community-owned products in the AI age. Its Constitution names eleven commitments that bind the company. The Comparisons system is how Our.one publicly contrasts itself with named incumbents (TikTok, Instagram, LinkedIn, Discord, etc.). Each Comparison has four prose sections (summaryMd, competitorBehaviorMd, ourBehaviorMd, whyDifferentMd) plus citations and a slug.
+ 
+You draft the four prose sections. The operator adds citations and edits before publishing. The publish validator demands ≥2 cited verbatim quotes from the incumbent's own sources before the Comparison can go public — citations are explicitly NOT your job, because they require verifying quotes against live URLs at the moment of publish.
+ 
+The eleven Constitution commitments:
+${COMMITMENTS.map((c) => `${c.n}. ${c.text}`).join("\n")}
+ 
+Editorial principles you must follow:
+ 
+1. Cited contrast, never emotional escalation. "TikTok engineers the For-You feed for watch time" is sourced. "TikTok is destroying your kids' brains" is outrage-bait — reject the latter framing.
+2. Paraphrase the incumbent's actual position. The "their way" section should read as something the incumbent's own engineering team might describe. If the only honest read is mild, say so — the operator picks a different Elephant if the contrast is weak.