diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 00000000..04ffa28b --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000 + } + ] +} diff --git a/.env.example b/.env.example index dd37f023..257fb8af 100644 --- a/.env.example +++ b/.env.example @@ -53,6 +53,11 @@ YJS_COMPACTION_PENDING_THRESHOLD=500 YJS_COMPACTION_IDLE_MS=300000 YJS_COMPACTION_MAX_DOCS=20 +# Feature previews +# Show the in-progress Verified track page in production. Defaults to on in local dev, +# off in production. Set to "true" on a Vercel preview deploy to view /verified there. +NEXT_PUBLIC_VERIFIED_PREVIEW=false + # Security defaults SECURITY_RATE_LIMIT=true SECURITY_SUSPICIOUS_DETECTION=true diff --git a/app/(site)/home-client.tsx b/app/(site)/home-client.tsx index 13b9fc2e..6e368d08 100644 --- a/app/(site)/home-client.tsx +++ b/app/(site)/home-client.tsx @@ -4,7 +4,6 @@ import { gsap } from "gsap"; import { Observer } from "gsap/Observer"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import { TextPlugin } from "gsap/TextPlugin"; -import { ChevronDown } from "lucide-react"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; @@ -21,10 +20,7 @@ if (typeof window !== "undefined") { export default function HomePage() { const containerRef = useRef(null); - const heroRef = useRef(null); - const pinContainerRef = useRef(null); const paragraphRef = useRef(null); - const scrollIndicatorRef = useRef(null); const textScrollRef = useRef(null); const videoRef = useRef(null); const [videoLoaded, setVideoLoaded] = useState(false); @@ -45,7 +41,7 @@ export default function HomePage() { const lines = text.split(//i); // Words that should have gradient effect - const gradientWords = ["interactives", "CHI", "2026."]; + const gradientWords = ["Verified", "evidence."]; element.innerHTML = lines .map((line, lineIndex) => { @@ -93,9 +89,6 @@ export default function HomePage() { return progress; }); - // Add 0.2 snap breakpoint for first screen - snapPoints.push(0.18); - // Add end point if last section doesn't reach bottom if (snapPoints[snapPoints.length - 1] < 0.99) { snapPoints.push(1); @@ -197,7 +190,7 @@ export default function HomePage() { // Ensure initial position is set for all words gsap.set(words, { - x: window.innerWidth - 25, + x: 0, }); // Make paragraph visible after wrapping and initial position is set @@ -208,56 +201,6 @@ export default function HomePage() { }); } - // Scroll indicator bounce animation - if (scrollIndicatorRef.current) { - // Create bouncing animation - gsap.to(".scroll-arrow-container", { - y: 15, - duration: 1, - ease: "power1.inOut", - repeat: -1, - yoyo: true, - }); - - // Fade out on scroll - gsap.to(scrollIndicatorRef.current, { - opacity: 0, - scrollTrigger: { - trigger: pinContainerRef.current, - start: "top top", - end: "+=50", - scrub: true, - }, - }); - } - - // Main text animation with snap point at the end - gsap.to(words, { - x: 0, - stagger: 0.02, - ease: "power4.inOut", - scrollTrigger: { - trigger: pinContainerRef.current, - start: "top top", - end: "bottom bottom", - scrub: true, - pin: heroRef.current, - }, - }); - - // Add snap point for the hero section - ScrollTrigger.create({ - trigger: pinContainerRef.current, - start: "top top", - end: "bottom bottom", - snap: { - snapTo: [0, 0.95, 1], // Snap at start, near end, and end - duration: { min: 0.2, max: 0.5 }, - delay: 0.1, - ease: "power1.inOut", - }, - }); - // Scroll-triggered animations gsap.utils.toArray(".scroll-section").forEach((section: any) => { gsap.from(section, { @@ -691,28 +634,12 @@ export default function HomePage() { - {/* Text Scrolling Hero Section */} -
+ {/* Hero Section */} +
- {/* Scroll Indicator */} -
-
- Scroll - -
-
- {/* Content Container */}
{/* Header */} @@ -747,13 +674,13 @@ export default function HomePage() { className="paragraph" style={{ fontFamily: "var(--font-roboto-flex), sans-serif", - fontSize: "5vw", - lineHeight: 1.0, - letterSpacing: "-0.05em", + fontSize: "clamp(42px, 5vw, 74px)", + lineHeight: 0.95, + letterSpacing: "0", fontWeight: 500, }} dangerouslySetInnerHTML={{ - __html: `The research behind
interactives was published at
CHI 2026.`, + __html: `New track!
Verified papers
with executable evidence.`, }} />
+

+ New Track announcement +

- Read the paper to learn about the technology and story behind the platform. + A new interactives publication track for papers whose core claims can be checked + by running code and tests.

- +
diff --git a/app/(site)/verified/page.tsx b/app/(site)/verified/page.tsx new file mode 100644 index 00000000..5e21e6f0 --- /dev/null +++ b/app/(site)/verified/page.tsx @@ -0,0 +1,655 @@ +import { ArrowRight } from "lucide-react"; +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import type { CSSProperties, ReactNode } from "react"; + +import Footer from "@/components/site/Footer"; +import SiteHeaderLight from "@/components/site/SiteHeaderLight"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export const metadata: Metadata = { + title: "Verified", + description: + "Verified is a new interactives publication track for papers whose core research claims can be checked by running code and tests.", + openGraph: { + title: "Verified - interactives", + description: + "A publication track for testable research claims: declare the claim, run the evidence, publish the verification record.", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Verified - interactives", + description: "Executable evidence for scoped research claims.", + }, + robots: { index: false, follow: false }, +}; + +function previewEnabled() { + if (process.env.NEXT_PUBLIC_VERIFIED_PREVIEW === "true") return true; + return process.env.NODE_ENV !== "production"; +} + +const CONTRACT_ITEMS = [ + { + label: "01", + title: "Author-agnostic", + body: "The source of the manuscript is not the publication gate. The claim is.", + }, + { + label: "02", + title: "Claim-scoped", + body: "A badge never means the whole paper is true. It means these declared claims were checked under stated conditions.", + }, + { + label: "03", + title: "Evidence-first", + body: "The prose tells the story. The executable package carries the evidence.", + }, +]; + +const PROCESS_STEPS = [ + ["Declare", "Turn contributions into explicit, testable claims."], + ["Package", "Attach tests, fixtures, seeds, references, and a pinned environment."], + ["Run", "Execute in a network-isolated sandbox with fixed CPU, memory, and time budgets."], + ["Assess", "Check whether the tests faithfully support the claim, not merely whether they pass."], + ["Publish", "Attach a transparent verification record to claims that pass both gates."], +] as const; + +const GATES = [ + { + label: "Gate 1", + name: "Execution", + question: "Did it run?", + body: "The submitted package executes in a pinned, deterministic, network-isolated environment. This gate is objective, repeatable, and produces the raw run record.", + outputs: ["exit code", "logs", "resource usage", "re-run result"], + }, + { + label: "Gate 2", + name: "Faithfulness", + question: "Did the evidence match the claim?", + body: "Passing tests are not enough. The second gate checks for tautological tests, cherry-picked fixtures, overfit answers, and prose that claims more than the evidence supports.", + outputs: ["claim coverage", "adequacy notes", "scope limits", "verdict"], + }, +]; + +const RECEIPT_ROWS = [ + ["Paper", "Sorting with partial comparisons"], + ["Claim", "C1"], + ["Execution", "Passed"], + ["Faithfulness", "Passed"], + ["Environment", "python:3.12-slim"], + ["Network", "Disabled"], + ["Budget", "2 vCPU / 512 MB / 90 s"], + ["Compute", "0.8 CPU·min / $0.04"], + ["Record", "Published with claim"], +] as const; + +const SCOPE_GOOD = [ + "Deterministic algorithmic correctness", + "CPU-only benchmarks on pinned data", + "Exact output reproduction", + "Machine-checkable proofs or assertions", + "Reproducible data pipelines for figures/tables", +]; + +const SCOPE_NOT_YET = [ + "Human-subjects or field claims", + 'Novelty claims, e.g. "first to..."', + "Qualitative or design contributions", + "Non-mechanizable theory", + "GPU-heavy or nondeterministic ML claims", +]; + +const POLICY_ITEMS = [ + ["Billed", "Sandbox execution and verification work."], + ["Not sold", "Acceptance, badge, or publication outcome."], + ["Public record", "What ran, what passed, and what was out of scope."], +] as const; + +type WindowTone = "plain" | "blue" | "strong"; + +function SentenceLines({ text }: { text: string }) { + const sentences = text.split(". "); + + return sentences.map((sentence, index) => ( + + {index < sentences.length - 1 ? `${sentence}.` : sentence} + + )); +} + +function ScreenSection({ + children, + className, + id, + style, +}: { + children: ReactNode; + className?: string; + id?: string; + style?: CSSProperties; +}) { + return ( +
+
+ {children} +
+
+ ); +} + +function SectionIntro({ + eyebrow, + title, + body, + className, +}: { + eyebrow: string; + title: string; + body?: string; + className?: string; +}) { + return ( +
+

{eyebrow}

+

+ +

+ {body ? ( +

{body}

+ ) : null} +
+ ); +} + +function StatusPill({ children, tone = "plain" }: { children: ReactNode; tone?: WindowTone }) { + const toneClass = { + plain: "border-gray-400 bg-[#F7F7F4] text-gray-800", + blue: "border-brand-600 bg-brand-50 text-brand-700", + strong: "border-gray-950 bg-gray-950 text-[#F1F1F1]", + }[tone]; + + return ( + + {children} + + ); +} + +function WindowFrame({ + title, + children, + className, + tone = "plain", + eyebrow, +}: { + title: string; + children: ReactNode; + className?: string; + tone?: WindowTone; + eyebrow?: string; +}) { + const shadowClass = { + plain: "shadow-[10px_10px_0_0_rgba(17,24,39,0.11)]", + blue: "shadow-[12px_12px_0_0_rgba(37,99,235,0.18)]", + strong: "shadow-[12px_12px_0_0_rgba(17,24,39,0.16)]", + }[tone]; + + return ( +
+
+ {title} + {eyebrow ? ( + {eyebrow} + ) : null} +
+ {children} +
+ ); +} + +function ManifestPanel() { + const rows = [ + ["claim", '"sortpartial returns a sorted array under the stated comparison budget"'], + ["evidence", "tests/claim_01.py"], + ["env", "python:3.12-slim"], + ["network", "disabled"], + ["budget", "2 vCPU / 512 MB / 90 s"], + ["gate.execution", "passed"], + ["gate.faithfulness", "passed"], + ["record", "ready_to_publish"], + ] as const; + + return ( + +
+
+ {rows.map(([label, value]) => ( +
+
{label}:
+
{value}
+
+ ))} +
+
+ Execution passed + Faithfulness passed + Compute 0.8 CPU·min +
+
+
+ ); +} + +function PaperClaim() { + return ( +
+
+ Claim C1 + paper excerpt +
+

+ + sortpartial + {" "} + returns a sorted array and stays under the stated comparison budget on the pinned workload. +

+
+ ); +} + +function ContractPanel() { + return ( + +
+ {CONTRACT_ITEMS.map(item => ( +
+

{item.label}

+

+ {item.title} +

+

{item.body}

+
+ ))} +
+
+ ); +} + +function ProcessPanel() { + return ( + +
    + {PROCESS_STEPS.map(([label, body], index) => ( +
  1. +
    + + {String(index + 1).padStart(2, "0")} + + {index < PROCESS_STEPS.length - 1 ? ( + -> + ) : null} +
    +

    + {label} +

    +

    {body}

    +
  2. + ))} +
+
+ ); +} + +function GatePanel({ + gate, + highlighted = false, +}: { + gate: (typeof GATES)[number]; + highlighted?: boolean; +}) { + return ( + +
+
+ {gate.label} + {gate.name} +
+

+ {gate.question} +

+

{gate.body}

+
+

Outputs

+
    + {gate.outputs.map(output => ( +
  • + {output} +
  • + ))} +
+
+
+
+ ); +} + +function ReceiptPanel() { + return ( + +
+ {RECEIPT_ROWS.map(([label, value]) => ( +
+
{label}
+
+ {value} +
+
+ ))} +
+
+ ); +} + +function ScopeMatrix() { + return ( + +
+
+
+ Good v1 candidates +
+
    + {SCOPE_GOOD.map(item => ( +
  • + {item} +
  • + ))} +
+
+
+
+ Not v1 +
+
    + {SCOPE_NOT_YET.map(item => ( +
  • + {item} +
  • + ))} +
+
+
+
+ ); +} + +function PolicyLedger() { + return ( + +
+ {POLICY_ITEMS.map(([label, body]) => ( +
+

{label}

+

{body}

+
+ ))} +
+
+ ); +} + +export default function VerifiedPage() { + if (!previewEnabled()) notFound(); + + return ( +
+ + +
+ +
+
+

+ Verified publication track +

+

+ +

+

+ Verified is an interactives track for papers whose core contribution is testable. + Authors declare claims, attach executable evidence, and receive a public + verification record when both gates pass. +

+

+ Who wrote the paper is not the gate. The record shows what was checked, how it ran, + what passed, and what remains outside the claim. +

+ +
+ +
+ +
+
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+                  {`def test_matches_oracle():
+    for seed in range(10_000):
+        xs = random_ints(seed, n=1_000)
+        assert sortpartial(xs) == sorted(xs)
+
+def test_comparison_budget():
+    ops = count_comparisons(sortpartial, n=100_000)
+    assert ops <= 3 * 100_000 * log2(100_000)`}
+                
+
+ +
+
+
+ + +
+
+ +
+ +
+
+ +
+
+ + +
+
+

Executable publishing

+

+ +

+
+ +
+

+ The badge is scoped to the claim and the environment. It says what was checked, + how it ran, and which parts remain outside the record. +

+
+ Claim scoped + Evidence executed + Record attached +
+ +
+
+
+
+
+ +
+
+ ); +} diff --git a/components/site/SiteHeaderLight.tsx b/components/site/SiteHeaderLight.tsx index 5cd20962..c97d77ec 100644 --- a/components/site/SiteHeaderLight.tsx +++ b/components/site/SiteHeaderLight.tsx @@ -2,11 +2,21 @@ import Link from "next/link"; import SiteMenu from "@/components/site/SiteMenu"; +import { cn } from "@/lib/utils"; -export default function SiteHeaderLight() { +type SiteHeaderLightProps = { + fullWidth?: boolean; +}; + +export default function SiteHeaderLight({ fullWidth = false }: SiteHeaderLightProps) { return (
-
+
{/* Logo */}

= [ { href: "/", label: "Home" }, { href: "/about", label: "About" }, { href: "/papers/preprint", label: "Preprints" }, + { href: "/verified", label: "Verified Track" }, { href: "/docs", label: "Docs" }, { href: "/submit", label: "Submit" }, ]; diff --git a/docs/verified-track.md b/docs/verified-track.md new file mode 100644 index 00000000..5cddc991 --- /dev/null +++ b/docs/verified-track.md @@ -0,0 +1,136 @@ +# Verified Track — Design & Deployment Notes + +> Status: **Draft** · Branch: `feat/verified-track` · Target deploy: production (Vercel + Supabase) + +A new publication track where acceptance is gated on **machine-verifiable claims** rather +than (or in addition to) human peer review. Authors declare their contributions as explicit +**claims**, submit **tests**, and the platform runs those tests in an isolated environment and +checks that they actually substantiate the declared claims. Verified → published. Compute is +metered and billed. + +## 1. Framing (this is load-bearing) + +**Author-agnostic, claim-verified.** We are not selling *publication*; we are selling +*verification*. Who/what wrote the paper (human or AI) is irrelevant — the gate checks whether +each claim is reproducibly true. "Published on the verified track" signals **"this contribution +was machine-checked"**, nothing more, nothing less. + +This framing is what separates the idea from a pay-to-publish paper mill. Keep it central in all +copy and UX. + +## 2. The core risk: "tests pass" ≠ "claim is true" + +A test only proves *the assertion the author wrote*, on *the data/seed the author chose*. Failure +modes to defend against: + +- cherry-picked data / random seed +- tautological tests (implementation checked against itself) +- hardcoded / overfit answers +- prose claim broader than what the test covers + +Therefore verification is **two gates**, not one: + +1. **Cheap gate** — tests execute and pass in a pinned, sandboxed, deterministic environment. +2. **Faithfulness gate** — the tests are *adequate and faithful* to the declared claim. This is + the hard part and the source of the badge's value. Mechanism is an **open decision** (§7): + LLM judge, enforced test spec (held-out data, fixed seed, baseline comparison, no network), + human spot-check, or a combination. + +If gate 2 is weak, the track gets gamed within weeks and the badge is worthless. + +## 3. Scope of verifiable claims (v1 should be narrow) + +Works well: algorithmic correctness, benchmark/perf numbers, "this code produces this output", +machine-checkable math, data-pipeline reproducibility. + +Works poorly: human-subjects/empirical claims, novelty ("first to…"), qualitative contributions, +non-mechanizable theory. + +The track must **state explicitly** what it can and cannot verify. v1 recommendation: accept only +**deterministic, CPU-only, single-process, assertion-based** claims; defer GPU/ML/statistical +claims. + +## 4. Architecture — two tiers, deployed differently + +``` +┌─────────────────────────── Vercel (existing app) ───────────────────────────┐ +│ Next.js: track UI, claim/test submission, billing records, │ +│ thin enqueue + status API (NO untrusted code runs here) │ +└───────────────┬──────────────────────────────────────────────────────────────┘ + │ enqueue job / poll status (Upstash Redis as queue + metering) +┌───────────────▼─────────────── Runner tier (OFF Vercel) ─────────────────────┐ +│ Isolated execution: container / microVM (Firecracker/gVisor) OR managed │ +│ sandbox (E2B / Modal). Pinned image, no network, CPU/mem/time caps. │ +│ Runs gate 1; emits logs + result. Faithfulness gate (§2.2) runs here or in │ +│ a separate judge step. │ +└───────────────────────────────────────────────────────────────────────────────┘ +``` + +**Why the split:** Vercel serverless is short-lived and cannot safely isolate untrusted code. +The only async infra today is **Upstash Redis** (rate-limit + AI quota) — reusable as the job +queue and the metering store, but it is *not* an execution environment. There is no queue/worker/ +sandbox in the repo today; the runner tier must be provisioned separately. + +## 5. Data model additions (PROPOSED — not final) + +Additive only. Mirrors the existing Prisma + manual-SQL pattern (see §6). + +- `PaperType` += `VERIFIED` (new enum value). +- `PaperStatus` += `VERIFICATION_PENDING`, `VERIFICATION_RUNNING`, `VERIFICATION_PASSED`, + `VERIFICATION_FAILED` (new enum values). +- **`Claim`** — `paperId` → 1:N declared contributions (text + structured assertion descriptor). +- **`TestArtifact`** — submitted tests + environment spec (pinned image, entrypoint, resource + budget), linked to a paper and to the claims it targets. +- **`VerificationRun`** — one execution attempt: status, logs ref, compute cost, exit result, + per-claim pass/fail, faithfulness verdict. +- Billing: reuse the existing Redis daily-quota pattern (`quota:{id}:{date}`) for metering; + consider a Postgres usage-record table for auditable billing. + +## 6. Production deployment playbook (CRITICAL — read before any schema change) + +This repo does **not** use Prisma Migrate. `prisma/` has only `schema.prisma` + `seed.ts`; there +is **no `prisma/migrations/`**. The build runs only `next build` (+ `prisma generate` via +`postinstall`) and **never touches the database**. Schema changes are applied **by hand**. + +To add a model/enum and ship it to production: + +1. Edit `prisma/schema.prisma` (additive only) → run `npx prisma generate` locally. +2. Hand-write an **idempotent** SQL file in `supabase/migrations/YYYYMMDD_verified_track.sql`: + - tables: `CREATE TABLE IF NOT EXISTS "PascalCaseName" (...)` — names **must match Prisma** + exactly (models here have no `@@map`, so Prisma expects quoted PascalCase, e.g. + `"VerificationRun"`). Follow the `20251229_yjs_doc_events.sql` precedent. + - enums: `CREATE TYPE "EnumName" AS ENUM (...)`, or `ALTER TYPE ... ADD VALUE IF NOT EXISTS` + for adding values to an existing enum. + - RLS: if the table is reachable from the browser via the anon/authenticated key, hand-write + `ENABLE ROW LEVEL SECURITY` + policies (Prisma cannot express these). +3. **Run the SQL on Supabase FIRST** (before merging code). Order matters: build the additive + tables, *then* merge code. Old code ignores the new tables, so this is safe. +4. Local self-check (CI does **not** build): `npm run lint && npm run typecheck && npm run test:ci + && npm run build`. +5. Push branch → Vercel preview + GitHub "Quality" CI (`quality.yml`: lint/typecheck/test only — + no build, no deploy). ⚠️ Preview deploys reuse prod env incl. `DATABASE_URL` — preview writes + hit the production DB unless a separate Supabase project is configured for preview. +6. Open PR → `master`. Merge → Vercel auto-deploys to production via its Git integration (there is + no deploy GitHub Action). +7. Post-deploy: add any new env vars to **both** `.env.example` and the Vercel project settings; + keep the feature behind a flag until ready. + +Safety rules for prod: additive-only migrations, `IF NOT EXISTS` everywhere, never `DROP`/`ALTER` +existing columns, confirm Supabase PITR/backups before the first migration, ship behind a flag. + +## 7. Open decisions (lock these before writing real schema) + +1. **Faithfulness gate (§2.2):** LLM judge / enforced test spec / human spot-check / combination? + This determines whether the badge is worth anything. +2. **Runner compute (§4):** self-host an isolated tier (Firecracker/gVisor on a provisioned host) + vs managed sandbox (E2B / Modal)? Determines the runner architecture. +3. **v1 claim scope (§3):** start with deterministic CPU-only assertion tests, or support ML/ + benchmarks from the start? Determines MVP size. + +## 8. Branch slicing + +- **Branch 1 (`feat/verified-track`, Vercel-safe):** schema + migration SQL, track UI, claim/test + submission form, thin enqueue/status API (execution stubbed), billing/usage data structures. + Deploys cleanly through the existing Vercel + Supabase flow, end-to-end with a stubbed runner. +- **Branch 2 (separate infra):** the real isolated execution tier + the faithfulness gate. Lives + off Vercel; its deployment is a separate track from the web app.