"""
Assemble the validation study corpus (v2, fixes applied after v1 failures).
Per DESIGN.md ยง3 target: 12 documents.
v2 design adjustments (documented in manifest):
- FOMC extraction uses targeted selector (col-xs-12 col-sm-8 div after
share-links marker) rather than unreliable id="article" div.
- Wikipedia fetch uses MediaWiki action API with prop=extracts
(plain-text endpoint returned 404).
- Stratum A revised: Altman essay and FOMC statement are PUBLIC
DOCUMENTS, not AI outputs. Renamed Stratum A to "human-authored
published documents" to be accurate; moved AI-generation-as-in-
distribution work into Stratum B (6 generations instead of 4).
Stratum C remains Wikipedia-encyclopedic.
- Worked-example extraction removed; those documents are meta-
analyses and the AI output inside them is described not quoted
verbatim.
Writes:
corpus/XX_slug.txt
corpus/manifest.json (records stratum revisions explicitly)
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import subprocess
import sys
import urllib.error
import urllib.request
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
STUDY_DIR = REPO_ROOT / "fvs_eval" / "validation_study"
CORPUS_DIR = STUDY_DIR / "corpus"
CORPUS_DIR.mkdir(exist_ok=True)
# ============================================================================
# Stratum A: public human-authored documents (fetched from source)
# ============================================================================
def _strip_html(html: str) -> str:
html = re.sub(r"", "", html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(r"", "", html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(r"(p|div|br|h[1-6]|li)>", "\n", html, flags=re.IGNORECASE)
html = re.sub(r"<(br|hr)[^>]*/?>", "\n", html, flags=re.IGNORECASE)
text = re.sub(r"<[^>]+>", "", html)
for old, new in [
("&", "&"), ("<", "<"), (">", ">"),
(""", '"'), ("'", "'"), ("'", "'"),
(" ", " "), ("'", "'"), ("—", "-"), ("–", "-"),
]:
text = text.replace(old, new)
lines = [ln.strip() for ln in text.split("\n")]
lines = [ln for ln in lines if ln]
return "\n\n".join(lines)
def _altman_essay_text() -> str:
url = "https://ia.samaltman.com/"
result = subprocess.run(
["curl", "-sSL", "--max-time", "30", url],
capture_output=True, text=True, timeout=45,
)
return _strip_html(result.stdout)
def _fomc_march_2026_text() -> str:
"""FOMC Mar 2026 press release. Extract only the main
content block.
The page structure (verified on live fetch): the main content is in
a series of
...
tags inside a
that appears AFTER the
share-buttons block. We isolate that block by finding the first
tag after the "Implementation Note" link is preceded in the
page; more robust extraction: take the 4-6
tags between the
article-share block and the "Last Update" footer.
"""
url = "https://www.federalreserve.gov/newsevents/pressreleases/monetary20260318a.htm"
result = subprocess.run(
["curl", "-sSL", "--max-time", "30", url],
capture_output=True, text=True, timeout=45,
)
html = result.stdout
# Narrow to the col-xs-12 col-sm-8 col-md-8 that carries the press
# release body. There are two such divs on the page; the second (by
# source order) holds the body.
divs = re.findall(
r'
(.*?)
',
html, flags=re.DOTALL,
)
if len(divs) < 1:
raise RuntimeError("FOMC: could not isolate press-release div")
# Largest div (by length) is typically the body.
body = max(divs, key=len)
# Cut off anything after "Implementation Note" link section
impl = body.find("Implementation Note")
if impl > 500:
body = body[:impl]
text = _strip_html(body)
# Sanity filter: remove navigation fragments that sometimes leak
text = re.sub(r"(?im)^\s*(share|print|email|last update|home|press releases?)\s*$", "", text)
text = re.sub(r"\n{3,}", "\n\n", text).strip()
return text
# ============================================================================
# Stratum B: freshly generated AI analyses (pre-declared prompts)
# ============================================================================
STRATUM_B_PROMPTS = [
{
"slug": "b01_nvidia_investment",
"topic": "business / investment",
"prompt": "Analyze the investment case for a major technology company entering the AI accelerator market in 2026. Write a considered written analysis, approximately 400-500 words.",
},
{
"slug": "b02_automation_policy",
"topic": "policy / governance",
"prompt": "Describe how a national government might approach long-term structural unemployment caused by automation. Write a considered written analysis, approximately 400-500 words.",
},
{
"slug": "b03_social_media_adolescents",
"topic": "social / behavioral",
"prompt": "Discuss the relationship between social media use and adolescent mental health. Write a considered written analysis, approximately 400-500 words.",
},
{
"slug": "b04_llm_customer_support",
"topic": "technical",
"prompt": "Analyze whether a company should deploy large language models in customer support. Write a considered written analysis, approximately 400-500 words.",
},
{
"slug": "b05_remote_work_productivity",
"topic": "organizational",
"prompt": "Discuss how a mid-sized company should evaluate remote work policies. Write a considered written analysis, approximately 400-500 words.",
},
{
"slug": "b06_quantum_computing_outlook",
"topic": "scientific / research",
"prompt": "Describe the current state and near-term outlook of quantum computing research. Write a considered written analysis, approximately 400-500 words.",
},
]
def _call_anthropic(prompt: str, max_tokens: int = 1200) -> dict:
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
raise RuntimeError("ANTHROPIC_API_KEY not set")
system_prompt = (
"You are a helpful assistant. Respond to the user's analytical "
"prompt with a considered written analysis."
)
candidates = [
"claude-opus-4-7",
"claude-sonnet-4-6",
"claude-haiku-4-5-20251001",
]
last_err = None
for cand in candidates:
payload = {
"model": cand,
"max_tokens": max_tokens,
"system": system_prompt,
"temperature": 0.7,
"messages": [{"role": "user", "content": prompt}],
}
req = urllib.request.Request(
"https://api.anthropic.com/v1/messages",
method="POST",
data=json.dumps(payload).encode("utf-8"),
headers={
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
)
try:
with urllib.request.urlopen(req, timeout=90) as resp:
data = json.loads(resp.read())
text = "".join(
block.get("text", "") for block in data.get("content", [])
if block.get("type") == "text"
)
return {
"text": text,
"model": data.get("model"),
"requested_model": cand,
"stop_reason": data.get("stop_reason"),
"input_tokens": data.get("usage", {}).get("input_tokens"),
"output_tokens": data.get("usage", {}).get("output_tokens"),
}
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace") if e.fp else str(e)
last_err = f"{e.code} {e.reason}: {body[:400]}"
continue
except Exception as e:
last_err = str(e)
continue
raise RuntimeError(f"All candidates failed; last: {last_err}")
# ============================================================================
# Stratum C: Wikipedia articles via action API
# ============================================================================
STRATUM_C_URLS = [
{
"slug": "c01_wikipedia_semaglutide",
"title": "Semaglutide",
"description": "Wikipedia article on semaglutide (medical topic).",
},
{
"slug": "c02_wikipedia_eu_ai_act",
"title": "Artificial_Intelligence_Act",
"description": "Wikipedia article on the EU AI Act (policy topic).",
},
{
"slug": "c03_wikipedia_quantum_supremacy",
"title": "Quantum_supremacy",
"description": "Wikipedia article on quantum supremacy (technical topic).",
},
{
"slug": "c04_wikipedia_ubi",
"title": "Universal_basic_income",
"description": "Wikipedia article on universal basic income (contested topic).",
},
]
def _fetch_wikipedia_extract(title: str, max_chars: int = 6000) -> str:
"""Fetch Wikipedia plain-text extract via MediaWiki action API.
The /api/rest_v1/page/plain endpoint returns 404 (deprecated); the
action API's prop=extracts with explaintext=1 is the current
supported path and returns the intro + lead sections in plain text.
"""
import urllib.parse
api = (
"https://en.wikipedia.org/w/api.php?"
"action=query&prop=extracts&explaintext=1&redirects=1&format=json&titles="
+ urllib.parse.quote(title)
)
req = urllib.request.Request(
api,
headers={"User-Agent": "FrameCheck-ValidationStudy/1.0 (research)"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read())
pages = data.get("query", {}).get("pages", {})
if not pages:
raise RuntimeError(f"Wikipedia: no pages returned for {title}")
# Pages is keyed by pageid; take the first non-missing.
for pid, page in pages.items():
if page.get("missing") is not None:
raise RuntimeError(f"Wikipedia: title not found: {title}")
extract = page.get("extract", "")
if not extract:
continue
if len(extract) > max_chars:
cut = extract[:max_chars]
last_para = cut.rfind("\n\n")
if last_para > max_chars // 2:
extract = cut[:last_para]
else:
extract = cut
return extract.strip()
raise RuntimeError(f"Wikipedia: no extract content for {title}")
# ============================================================================
# Corpus assembly
# ============================================================================
def _sha256(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
def _word_count(s: str) -> int:
return len(s.split())
def _now_iso() -> str:
return subprocess.check_output(["date", "-u", "+%Y-%m-%dT%H:%M:%SZ"]).decode().strip()
def assemble_corpus():
manifest = []
print("== Stratum A: public human-authored documents ==")
for slug, fetcher, title, notes in [
("a01_altman_intelligence_age", _altman_essay_text,
"The Intelligence Age (Sam Altman, Sept 2024)",
"AI-company founder essay (human-authored)."),
("a02_fomc_march_2026", _fomc_march_2026_text,
"FOMC Press Release, March 2026",
"Policy document; central bank rate decision."),
]:
try:
text = fetcher()
if _word_count(text) < 150:
raise RuntimeError(f"too short ({_word_count(text)} words)")
(CORPUS_DIR / f"{slug}.txt").write_text(text, encoding="utf-8")
manifest.append({
"doc_id": slug,
"stratum": "A",
"title": title,
"source": "https://ia.samaltman.com/" if "altman" in slug
else "https://www.federalreserve.gov/newsevents/pressreleases/monetary20260318a.htm",
"fetched_at": _now_iso(),
"word_count": _word_count(text),
"sha256": _sha256(text),
"notes": notes,
})
print(f" {slug}: {_word_count(text)} words")
except Exception as e:
print(f" {slug}: FAILED - {e}")
print("\n== Stratum B: freshly generated AI analyses ==")
for spec in STRATUM_B_PROMPTS:
try:
result = _call_anthropic(spec["prompt"])
(CORPUS_DIR / f"{spec['slug']}.txt").write_text(result["text"], encoding="utf-8")
manifest.append({
"doc_id": spec["slug"],
"stratum": "B",
"title": spec["topic"],
"source": f"anthropic-api::{result['model']}",
"fetched_at": _now_iso(),
"word_count": _word_count(result["text"]),
"sha256": _sha256(result["text"]),
"notes": f"Generated via Anthropic API. Model requested={result['requested_model']}, served={result['model']}. Prompt: {spec['prompt']}",
"stop_reason": result.get("stop_reason"),
"input_tokens": result.get("input_tokens"),
"output_tokens": result.get("output_tokens"),
})
print(f" {spec['slug']}: {_word_count(result['text'])} words (model={result['model']})")
except Exception as e:
print(f" {spec['slug']}: FAILED - {e}")
print("\n== Stratum C: Wikipedia articles ==")
for spec in STRATUM_C_URLS:
try:
text = _fetch_wikipedia_extract(spec["title"])
if _word_count(text) < 200:
raise RuntimeError(f"too short ({_word_count(text)} words)")
(CORPUS_DIR / f"{spec['slug']}.txt").write_text(text, encoding="utf-8")
manifest.append({
"doc_id": spec["slug"],
"stratum": "C",
"title": spec["description"],
"source": f"https://en.wikipedia.org/wiki/{spec['title']}",
"fetched_at": _now_iso(),
"word_count": _word_count(text),
"sha256": _sha256(text),
"notes": "Fetched via MediaWiki action API (prop=extracts, explaintext=1); truncated at ~6000 chars on paragraph boundary.",
})
print(f" {spec['slug']}: {_word_count(text)} words")
except Exception as e:
print(f" {spec['slug']}: FAILED - {e}")
manifest_path = CORPUS_DIR / "manifest.json"
with open(manifest_path, "w", encoding="utf-8") as f:
json.dump({
"version": 2,
"assembled_at": _now_iso(),
"total_documents": len(manifest),
"target": 12,
"design_notes": (
"v2 corpus after v1 extraction failures. See script "
"docstring for revisions. Stratum A reduced to 2 "
"documents (Altman + FOMC); Stratum B expanded to 6 "
"fresh AI generations; Stratum C is 4 Wikipedia "
"articles. Total target 12, actual varies on fetch "
"success."
),
"documents": manifest,
}, f, indent=2)
print(f"\nManifest: {manifest_path}")
print(f"Total documents: {len(manifest)}")
return manifest
if __name__ == "__main__":
assemble_corpus()