From fa70f03eb25913028a9554b83bc66ee56d902a9e Mon Sep 17 00:00:00 2001 From: richard-peng-xia Date: Wed, 13 May 2026 17:33:14 -0400 Subject: [PATCH 01/25] Add UC Berkeley mirror site (port 40015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a full Flask mirror of berkeley.edu as the 16th WebHarbor site. **Site features:** - 8 SQLAlchemy models: College, Department, Program, NewsArticle, Event, ResearchCenter, Faculty, Bookmark (+ User with auth) - 20+ routes: homepage, news, programs, events, research centers, departments, faculty, admissions, about, unified search - 23 Jinja2 templates styled with Berkeley Blue (#003262) / Gold (#FDB515) - 30 benchmark tasks in tasks.jsonl (WebVoyager schema) **Seed data (fully idempotent):** - 14 UC Berkeley colleges/schools (real names) - 83 degree programs (BA/BS/MA/MS/PhD/MBA/JD/MD/MEng) - 121 news articles (2023–2025, 7 categories) - 64 events (upcoming + past, 7 categories) - 25 research centers (BAIR, QB3, MSRI, …) - 82 faculty (Jennifer Doudna, Stuart Russell, Saul Perlmutter, …) - 4 benchmark users: alice/bob/carol/dave (password: test1234) **Infrastructure changes:** - control_server.py: add 'berkeley' to SITES (port 40015) - websyn_start.sh: add 'berkeley' to startup array - Dockerfile: EXPOSE 40015, generate instance_seed DB at build time (no HF assets needed — all data is code-generated via seed_data.py) Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 10 +- control_server.py | 2 +- sites/berkeley/_health.py | 57 + sites/berkeley/app.py | 747 ++++++ sites/berkeley/seed_data.py | 2248 +++++++++++++++++ sites/berkeley/static/css/.gitkeep | 0 sites/berkeley/static/js/.gitkeep | 0 sites/berkeley/tasks.jsonl | 30 + sites/berkeley/templates/404.html | 15 + sites/berkeley/templates/500.html | 14 + sites/berkeley/templates/about.html | 106 + sites/berkeley/templates/academics.html | 69 + sites/berkeley/templates/account.html | 81 + sites/berkeley/templates/admissions.html | 131 + sites/berkeley/templates/base.html | 374 +++ .../berkeley/templates/department_detail.html | 84 + sites/berkeley/templates/departments.html | 42 + sites/berkeley/templates/event_detail.html | 91 + sites/berkeley/templates/events.html | 102 + sites/berkeley/templates/faculty.html | 87 + sites/berkeley/templates/faculty_profile.html | 97 + sites/berkeley/templates/index.html | 164 ++ sites/berkeley/templates/login.html | 39 + sites/berkeley/templates/news.html | 103 + sites/berkeley/templates/news_article.html | 113 + sites/berkeley/templates/program_detail.html | 98 + sites/berkeley/templates/programs.html | 102 + sites/berkeley/templates/register.html | 60 + sites/berkeley/templates/research.html | 72 + sites/berkeley/templates/research_center.html | 82 + sites/berkeley/templates/search.html | 164 ++ websyn_start.sh | 8 +- 32 files changed, 5385 insertions(+), 7 deletions(-) create mode 100644 sites/berkeley/_health.py create mode 100644 sites/berkeley/app.py create mode 100644 sites/berkeley/seed_data.py create mode 100644 sites/berkeley/static/css/.gitkeep create mode 100644 sites/berkeley/static/js/.gitkeep create mode 100644 sites/berkeley/tasks.jsonl create mode 100644 sites/berkeley/templates/404.html create mode 100644 sites/berkeley/templates/500.html create mode 100644 sites/berkeley/templates/about.html create mode 100644 sites/berkeley/templates/academics.html create mode 100644 sites/berkeley/templates/account.html create mode 100644 sites/berkeley/templates/admissions.html create mode 100644 sites/berkeley/templates/base.html create mode 100644 sites/berkeley/templates/department_detail.html create mode 100644 sites/berkeley/templates/departments.html create mode 100644 sites/berkeley/templates/event_detail.html create mode 100644 sites/berkeley/templates/events.html create mode 100644 sites/berkeley/templates/faculty.html create mode 100644 sites/berkeley/templates/faculty_profile.html create mode 100644 sites/berkeley/templates/index.html create mode 100644 sites/berkeley/templates/login.html create mode 100644 sites/berkeley/templates/news.html create mode 100644 sites/berkeley/templates/news_article.html create mode 100644 sites/berkeley/templates/program_detail.html create mode 100644 sites/berkeley/templates/programs.html create mode 100644 sites/berkeley/templates/register.html create mode 100644 sites/berkeley/templates/research.html create mode 100644 sites/berkeley/templates/research_center.html create mode 100644 sites/berkeley/templates/search.html diff --git a/Dockerfile b/Dockerfile index 991e5ab60..73ca1437d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 15 Flask mirror sites + control plane on :8101. +# 16 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -28,11 +28,17 @@ WORKDIR /opt/WebSyn # run scripts/fetch_assets.sh to pull them from Hugging Face first. COPY sites/ /opt/WebSyn/ +# Berkeley: all data is code-generated (no scraped images → no HF asset). +# Build the seed DB once at image-build time so websyn_start.sh can copy it on boot. +RUN cd /opt/WebSyn/berkeley && \ + python3 -c "from app import app" && \ + cp instance/berkeley.db instance_seed/berkeley.db + COPY websyn_start.sh /opt/websyn_start.sh COPY control_server.py /opt/control_server.py COPY site_runner.py /opt/site_runner.py RUN chmod +x /opt/websyn_start.sh -EXPOSE 8101 40000-40014 +EXPOSE 8101 40000-40015 CMD ["/opt/websyn_start.sh"] diff --git a/control_server.py b/control_server.py index c255253c6..7f7daff4c 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', + 'coursera', 'espn', 'berkeley', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/sites/berkeley/_health.py b/sites/berkeley/_health.py new file mode 100644 index 000000000..f404286f9 --- /dev/null +++ b/sites/berkeley/_health.py @@ -0,0 +1,57 @@ +"""UC Berkeley mirror health check.""" +from healthcheck import random_user + + +def run(p): + # 1. Home page renders + p.assert_get('home', '/', must_contain='Berkeley') + + # 2. News list renders + p.assert_get('news list', '/news', must_contain='article') + + # 3. Programs list renders + p.assert_get('programs list', '/programs', must_contain='program') + + # 4. Faculty list renders + p.assert_get('faculty list', '/faculty', must_contain='Professor') + + # 5. Search returns results + p.assert_get('search', '/search?q=computer+science', must_contain='result') + + # 6. Register page renders with CSRF + user = random_user() + html = p.assert_get('register page', '/register', must_contain='csrf_token') + token = p.csrf(html) + if not token: + p.check('register csrf token', False, 'no csrf in register form') + return + + # 7. Submit registration + p.assert_post('register submit', '/register', { + 'csrf_token': token, + 'username': user['first_name'].lower() + user['last_name'].lower(), + 'full_name': user['name'], + 'email': user['email'], + 'password': user['password'], + 'confirm': user['password'], + }, accept_status=(200, 302, 303)) + + # Logout so login form is real + p.get('/logout') + + # 8. Login page renders + html = p.assert_get('login page', '/login', accept_status=(200, 302, 303)) + token = p.csrf(html) if html else '' + + # 9. Submit login + if token: + p.assert_post('login submit', '/login', { + 'csrf_token': token, + 'email': user['email'], + 'password': user['password'], + }, accept_status=(200, 302, 303)) + else: + p.check('login submit', True, 'already authenticated from register') + + # 10. Authenticated account page + p.assert_get('account page', '/account', must_contain=user['first_name']) diff --git a/sites/berkeley/app.py b/sites/berkeley/app.py new file mode 100644 index 000000000..54d02881c --- /dev/null +++ b/sites/berkeley/app.py @@ -0,0 +1,747 @@ +#!/usr/bin/env python3 +"""UC Berkeley mirror — Flask application.""" +import os +import re +from datetime import datetime +from math import ceil + +from flask import (Flask, render_template, request, redirect, url_for, + flash, jsonify, session, abort, g) +from flask_sqlalchemy import SQLAlchemy +from flask_login import (LoginManager, UserMixin, login_user, logout_user, + login_required, current_user) +from flask_wtf import FlaskForm +from flask_wtf.csrf import CSRFProtect +from flask_bcrypt import Bcrypt +from wtforms import StringField, PasswordField, TextAreaField, SelectField +from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'berkeley-mirror-secret-key-2024' +app.config['SQLALCHEMY_DATABASE_URI'] = ( + f"sqlite:///{os.path.join(BASE_DIR, 'instance', 'berkeley.db')}") +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['WTF_CSRF_TIME_LIMIT'] = None + +os.makedirs(os.path.join(BASE_DIR, 'instance'), exist_ok=True) + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +login_manager = LoginManager(app) +login_manager.login_view = 'login' +login_manager.login_message = 'Please sign in to continue.' +login_manager.login_message_category = 'info' +csrf = CSRFProtect(app) + +PER_PAGE = 20 + +# ─── Helpers ────────────────────────────────────────────────────────────────── + +def slugify(text): + if not text: + return '' + s = re.sub(r'[^a-zA-Z0-9\s-]', '', text) + s = re.sub(r'[\s]+', '-', s.strip().lower()) + return s + +# ─── Models ─────────────────────────────────────────────────────────────────── + +class User(db.Model, UserMixin): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False, index=True) + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(255), nullable=False) + full_name = db.Column(db.String(150), nullable=False, default='') + role = db.Column(db.String(30), default='student') + bio = db.Column(db.Text, default='') + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + bookmarks = db.relationship('Bookmark', backref='user', lazy=True, + cascade='all, delete-orphan') + + def set_password(self, pw): + self.password_hash = bcrypt.generate_password_hash(pw).decode('utf-8') + + def check_password(self, pw): + return bcrypt.check_password_hash(self.password_hash, pw) + + +class College(db.Model): + __tablename__ = 'colleges' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) + slug = db.Column(db.String(200), unique=True, nullable=False, index=True) + description = db.Column(db.Text, default='') + dean = db.Column(db.String(150), default='') + founded_year = db.Column(db.Integer, default=1868) + undergrad_count = db.Column(db.Integer, default=1000) + grad_count = db.Column(db.Integer, default=500) + dept_count = db.Column(db.Integer, default=10) + + departments = db.relationship('Department', backref='college', lazy=True) + programs = db.relationship('Program', backref='college', lazy=True) + research_centers = db.relationship('ResearchCenter', backref='college', lazy=True) + + +class Department(db.Model): + __tablename__ = 'departments' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) + slug = db.Column(db.String(200), unique=True, nullable=False, index=True) + college_id = db.Column(db.Integer, db.ForeignKey('colleges.id'), nullable=False) + description = db.Column(db.Text, default='') + chair = db.Column(db.String(150), default='') + phone = db.Column(db.String(30), default='') + location = db.Column(db.String(200), default='') + + faculty = db.relationship('Faculty', backref='department', lazy=True) + programs = db.relationship('Program', backref='department', lazy=True) + + +class Program(db.Model): + __tablename__ = 'programs' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(300), nullable=False) + slug = db.Column(db.String(300), unique=True, nullable=False, index=True) + degree_type = db.Column(db.String(20), default='BA') + college_id = db.Column(db.Integer, db.ForeignKey('colleges.id'), nullable=True) + department_id = db.Column(db.Integer, db.ForeignKey('departments.id'), nullable=True) + description = db.Column(db.Text, default='') + requirements = db.Column(db.Text, default='') + units = db.Column(db.Integer, default=120) + duration_years = db.Column(db.Float, default=4.0) + application_deadline = db.Column(db.String(80), default='') + is_online = db.Column(db.Boolean, default=False) + gre_required = db.Column(db.Boolean, default=False) + + +class NewsArticle(db.Model): + __tablename__ = 'news_articles' + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(300), nullable=False) + slug = db.Column(db.String(300), unique=True, nullable=False, index=True) + category = db.Column(db.String(50), default='Campus Life') + author = db.Column(db.String(150), default='Berkeley News Staff') + published_date = db.Column(db.DateTime, default=datetime.utcnow) + content = db.Column(db.Text, default='') + summary = db.Column(db.Text, default='') + tags = db.Column(db.String(500), default='') + view_count = db.Column(db.Integer, default=0) + featured = db.Column(db.Boolean, default=False) + + +class Event(db.Model): + __tablename__ = 'events' + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(300), nullable=False) + description = db.Column(db.Text, default='') + start_datetime = db.Column(db.DateTime, nullable=False) + end_datetime = db.Column(db.DateTime, nullable=True) + location = db.Column(db.String(300), default='') + building = db.Column(db.String(200), default='') + category = db.Column(db.String(50), default='Lecture') + organizer = db.Column(db.String(200), default='') + registration_required = db.Column(db.Boolean, default=False) + cost = db.Column(db.String(50), default='Free') + url = db.Column(db.String(300), default='') + + +class ResearchCenter(db.Model): + __tablename__ = 'research_centers' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(300), nullable=False) + slug = db.Column(db.String(300), unique=True, nullable=False, index=True) + description = db.Column(db.Text, default='') + director = db.Column(db.String(150), default='') + college_id = db.Column(db.Integer, db.ForeignKey('colleges.id'), nullable=True) + focus_areas = db.Column(db.String(500), default='') + url = db.Column(db.String(300), default='') + founded_year = db.Column(db.Integer, default=2000) + + +class Faculty(db.Model): + __tablename__ = 'faculty' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(150), nullable=False) + slug = db.Column(db.String(200), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), default='Professor') + department_id = db.Column(db.Integer, db.ForeignKey('departments.id'), nullable=True) + email = db.Column(db.String(120), default='') + office = db.Column(db.String(200), default='') + phone = db.Column(db.String(30), default='') + research_interests = db.Column(db.String(500), default='') + bio = db.Column(db.Text, default='') + is_emeritus = db.Column(db.Boolean, default=False) + + +class Bookmark(db.Model): + __tablename__ = 'bookmarks' + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + item_type = db.Column(db.String(50), nullable=False) + item_id = db.Column(db.Integer, nullable=False) + note = db.Column(db.Text, default='') + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +# ─── Forms ──────────────────────────────────────────────────────────────────── + +class LoginForm(FlaskForm): + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired()]) + +class RegisterForm(FlaskForm): + username = StringField('Username', validators=[DataRequired(), Length(3, 80)]) + full_name = StringField('Full Name', validators=[DataRequired(), Length(2, 150)]) + email = StringField('Email', validators=[DataRequired(), Email()]) + password = PasswordField('Password', validators=[DataRequired(), Length(8, 100)]) + confirm = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) + +class ProfileForm(FlaskForm): + full_name = StringField('Full Name', validators=[DataRequired(), Length(2, 150)]) + email = StringField('Email', validators=[DataRequired(), Email()]) + bio = TextAreaField('Bio', validators=[Optional(), Length(max=1000)]) + +class BookmarkForm(FlaskForm): + item_type = StringField('Type', validators=[DataRequired()]) + item_id = StringField('ID', validators=[DataRequired()]) + note = TextAreaField('Note', validators=[Optional(), Length(max=500)]) + +# ─── Login Manager ──────────────────────────────────────────────────────────── + +@login_manager.user_loader +def load_user(user_id): + return db.session.get(User, int(user_id)) + +# ─── Context Processors ─────────────────────────────────────────────────────── + +@app.context_processor +def inject_globals(): + return { + 'now': datetime.utcnow(), + 'colleges': College.query.order_by(College.name).all(), + } + +# ─── Routes ─────────────────────────────────────────────────────────────────── + +@app.route('/') +def index(): + featured_news = NewsArticle.query.filter_by(featured=True).order_by( + NewsArticle.published_date.desc()).limit(6).all() + if len(featured_news) < 3: + featured_news = NewsArticle.query.order_by( + NewsArticle.published_date.desc()).limit(6).all() + upcoming_events = Event.query.filter( + Event.start_datetime >= datetime.utcnow() + ).order_by(Event.start_datetime).limit(4).all() + recent_research = ResearchCenter.query.limit(4).all() + stats = { + 'nobel_laureates': 12, + 'top_10_programs': 50, + 'varsity_sports': 30, + 'national_titles': 105, + 'faculty_count': 1629, + 'undergrad_count': 31800, + 'grad_count': 12000, + 'degree_programs': 350, + } + return render_template('index.html', + featured_news=featured_news, + upcoming_events=upcoming_events, + recent_research=recent_research, + stats=stats) + + +@app.route('/news') +def news(): + q = request.args.get('q', '').strip() + category = request.args.get('category', '') + featured = request.args.get('featured', '') + page = request.args.get('page', 1, type=int) + + query = NewsArticle.query + if q: + query = query.filter( + db.or_( + NewsArticle.title.ilike(f'%{q}%'), + NewsArticle.summary.ilike(f'%{q}%'), + NewsArticle.content.ilike(f'%{q}%'), + NewsArticle.tags.ilike(f'%{q}%'), + )) + if category: + query = query.filter(NewsArticle.category == category) + if featured == '1': + query = query.filter(NewsArticle.featured == True) + + query = query.order_by(NewsArticle.published_date.desc()) + total = query.count() + articles = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + categories = ['Research', 'Campus Life', 'Faculty', 'Student', 'Athletics', + 'Science', 'Arts'] + return render_template('news.html', + articles=articles, + total=total, + page=page, + total_pages=total_pages, + categories=categories, + current_category=category, + q=q, + featured=featured) + + +@app.route('/news/') +def news_article(slug): + article = NewsArticle.query.filter_by(slug=slug).first_or_404() + article.view_count = (article.view_count or 0) + 1 + db.session.commit() + related = NewsArticle.query.filter( + NewsArticle.category == article.category, + NewsArticle.id != article.id + ).order_by(NewsArticle.published_date.desc()).limit(3).all() + return render_template('news_article.html', article=article, related=related) + + +@app.route('/academics') +def academics(): + colleges = College.query.order_by(College.name).all() + total_programs = Program.query.count() + total_depts = Department.query.count() + return render_template('academics.html', + colleges=colleges, + total_programs=total_programs, + total_depts=total_depts) + + +@app.route('/programs') +def programs(): + q = request.args.get('q', '').strip() + college_slug = request.args.get('college', '') + degree = request.args.get('degree', '') + page = request.args.get('page', 1, type=int) + + query = Program.query + if q: + query = query.filter( + db.or_( + Program.name.ilike(f'%{q}%'), + Program.description.ilike(f'%{q}%'), + )) + if college_slug: + col = College.query.filter_by(slug=college_slug).first() + if col: + query = query.filter(Program.college_id == col.id) + if degree: + query = query.filter(Program.degree_type == degree) + + query = query.order_by(Program.name) + total = query.count() + progs = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + all_colleges = College.query.order_by(College.name).all() + degree_types = ['BA', 'BS', 'MA', 'MS', 'PhD', 'MPH', 'MBA', 'MEng', 'JD', 'MD'] + return render_template('programs.html', + programs=progs, + total=total, + page=page, + total_pages=total_pages, + all_colleges=all_colleges, + degree_types=degree_types, + current_college=college_slug, + current_degree=degree, + q=q) + + +@app.route('/programs/') +def program_detail(slug): + program = Program.query.filter_by(slug=slug).first_or_404() + related = Program.query.filter( + Program.college_id == program.college_id, + Program.id != program.id + ).limit(4).all() + return render_template('program_detail.html', program=program, related=related) + + +@app.route('/events') +def events(): + q = request.args.get('q', '').strip() + category = request.args.get('category', '') + date_filter = request.args.get('date', 'upcoming') + page = request.args.get('page', 1, type=int) + now = datetime.utcnow() + + query = Event.query + if q: + query = query.filter( + db.or_( + Event.title.ilike(f'%{q}%'), + Event.description.ilike(f'%{q}%'), + Event.location.ilike(f'%{q}%'), + Event.organizer.ilike(f'%{q}%'), + )) + if category: + query = query.filter(Event.category == category) + if date_filter == 'upcoming': + query = query.filter(Event.start_datetime >= now) + query = query.order_by(Event.start_datetime) + elif date_filter == 'past': + query = query.filter(Event.start_datetime < now) + query = query.order_by(Event.start_datetime.desc()) + elif date_filter == 'today': + today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + today_end = now.replace(hour=23, minute=59, second=59) + query = query.filter(Event.start_datetime.between(today_start, today_end)) + query = query.order_by(Event.start_datetime) + else: + query = query.order_by(Event.start_datetime) + + total = query.count() + evts = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + categories = ['Lecture', 'Sports', 'Arts', 'Career', 'Health', 'Social', 'Virtual'] + return render_template('events.html', + events=evts, + total=total, + page=page, + total_pages=total_pages, + categories=categories, + current_category=category, + date_filter=date_filter, + q=q) + + +@app.route('/events/') +def event_detail(event_id): + event = db.session.get(Event, event_id) + if event is None: + abort(404) + related = Event.query.filter( + Event.category == event.category, + Event.id != event.id, + Event.start_datetime >= datetime.utcnow() + ).order_by(Event.start_datetime).limit(3).all() + return render_template('event_detail.html', event=event, related=related) + + +@app.route('/research') +def research(): + centers = ResearchCenter.query.order_by(ResearchCenter.name).all() + colleges = College.query.order_by(College.name).all() + return render_template('research.html', centers=centers, colleges=colleges) + + +@app.route('/research/') +def research_center(slug): + center = ResearchCenter.query.filter_by(slug=slug).first_or_404() + related = ResearchCenter.query.filter( + ResearchCenter.college_id == center.college_id, + ResearchCenter.id != center.id + ).limit(3).all() + return render_template('research_center.html', center=center, related=related) + + +@app.route('/departments') +def departments(): + colleges = College.query.order_by(College.name).all() + depts_by_college = {} + for college in colleges: + depts_by_college[college] = Department.query.filter_by( + college_id=college.id).order_by(Department.name).all() + return render_template('departments.html', depts_by_college=depts_by_college) + + +@app.route('/departments/') +def department_detail(slug): + dept = Department.query.filter_by(slug=slug).first_or_404() + faculty_list = Faculty.query.filter_by(department_id=dept.id).order_by(Faculty.name).all() + programs = Program.query.filter_by(department_id=dept.id).all() + return render_template('department_detail.html', + dept=dept, + faculty_list=faculty_list, + programs=programs) + + +@app.route('/admissions') +def admissions(): + undergrad_programs = Program.query.filter( + Program.degree_type.in_(['BA', 'BS']) + ).count() + grad_programs = Program.query.filter( + Program.degree_type.in_(['MA', 'MS', 'PhD', 'MPH', 'MBA', 'MEng', 'JD', 'MD']) + ).count() + return render_template('admissions.html', + undergrad_programs=undergrad_programs, + grad_programs=grad_programs) + + +@app.route('/about') +def about(): + stats = { + 'nobel_laureates': 12, + 'top_10_programs': 50, + 'varsity_sports': 30, + 'national_titles': 105, + 'faculty_count': 1629, + 'undergrad_count': 31800, + 'grad_count': 12000, + 'degree_programs': 350, + 'founded': 1868, + 'acres': 1232, + 'libraries': 32, + 'alumni': 600000, + } + return render_template('about.html', stats=stats) + + +@app.route('/search') +def search(): + q = request.args.get('q', '').strip() + results = {'programs': [], 'news': [], 'events': [], 'faculty': [], + 'research': []} + total = 0 + if q: + results['programs'] = Program.query.filter( + db.or_( + Program.name.ilike(f'%{q}%'), + Program.description.ilike(f'%{q}%'), + )).limit(10).all() + results['news'] = NewsArticle.query.filter( + db.or_( + NewsArticle.title.ilike(f'%{q}%'), + NewsArticle.summary.ilike(f'%{q}%'), + NewsArticle.tags.ilike(f'%{q}%'), + )).order_by(NewsArticle.published_date.desc()).limit(10).all() + results['events'] = Event.query.filter( + db.or_( + Event.title.ilike(f'%{q}%'), + Event.description.ilike(f'%{q}%'), + )).limit(10).all() + results['faculty'] = Faculty.query.filter( + db.or_( + Faculty.name.ilike(f'%{q}%'), + Faculty.research_interests.ilike(f'%{q}%'), + Faculty.bio.ilike(f'%{q}%'), + )).limit(10).all() + results['research'] = ResearchCenter.query.filter( + db.or_( + ResearchCenter.name.ilike(f'%{q}%'), + ResearchCenter.description.ilike(f'%{q}%'), + ResearchCenter.focus_areas.ilike(f'%{q}%'), + )).limit(10).all() + total = sum(len(v) for v in results.values()) + return render_template('search.html', q=q, results=results, total=total) + + +@app.route('/faculty') +def faculty(): + q = request.args.get('q', '').strip() + dept_slug = request.args.get('dept', '') + page = request.args.get('page', 1, type=int) + + query = Faculty.query + if q: + query = query.filter( + db.or_( + Faculty.name.ilike(f'%{q}%'), + Faculty.research_interests.ilike(f'%{q}%'), + Faculty.title.ilike(f'%{q}%'), + )) + if dept_slug: + dept = Department.query.filter_by(slug=dept_slug).first() + if dept: + query = query.filter(Faculty.department_id == dept.id) + + query = query.order_by(Faculty.name) + total = query.count() + faculty_list = query.offset((page - 1) * PER_PAGE).limit(PER_PAGE).all() + total_pages = ceil(total / PER_PAGE) if total else 1 + + all_depts = Department.query.order_by(Department.name).all() + return render_template('faculty.html', + faculty_list=faculty_list, + total=total, + page=page, + total_pages=total_pages, + all_depts=all_depts, + current_dept=dept_slug, + q=q) + + +@app.route('/faculty/') +def faculty_profile(slug): + member = Faculty.query.filter_by(slug=slug).first_or_404() + colleagues = [] + if member.department_id: + colleagues = Faculty.query.filter( + Faculty.department_id == member.department_id, + Faculty.id != member.id + ).limit(5).all() + return render_template('faculty_profile.html', member=member, colleagues=colleagues) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('index')) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data.lower().strip()).first() + if user and user.check_password(form.password.data): + login_user(user) + next_page = request.args.get('next') + flash('Welcome back!', 'success') + return redirect(next_page or url_for('index')) + flash('Invalid email or password.', 'danger') + return render_template('login.html', form=form) + + +@app.route('/register', methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('index')) + form = RegisterForm() + if form.validate_on_submit(): + if User.query.filter_by(email=form.email.data.lower().strip()).first(): + flash('Email already registered.', 'danger') + elif User.query.filter_by(username=form.username.data.strip()).first(): + flash('Username already taken.', 'danger') + else: + user = User( + email=form.email.data.lower().strip(), + username=form.username.data.strip(), + full_name=form.full_name.data.strip(), + ) + user.set_password(form.password.data) + db.session.add(user) + db.session.commit() + login_user(user) + flash('Account created! Welcome to UC Berkeley.', 'success') + return redirect(url_for('index')) + return render_template('register.html', form=form) + + +@app.route('/logout', methods=['GET', 'POST']) +@login_required +def logout(): + logout_user() + flash('You have been logged out.', 'info') + return redirect(url_for('index')) + + +@app.route('/account') +@login_required +def account(): + bookmarks = Bookmark.query.filter_by(user_id=current_user.id).order_by( + Bookmark.created_at.desc()).all() + bookmark_details = [] + for bm in bookmarks: + detail = {'bookmark': bm, 'item': None, 'title': '', 'url': '#'} + if bm.item_type == 'program': + item = db.session.get(Program, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.name + detail['url'] = url_for('program_detail', slug=item.slug) + elif bm.item_type == 'news': + item = db.session.get(NewsArticle, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.title + detail['url'] = url_for('news_article', slug=item.slug) + elif bm.item_type == 'event': + item = db.session.get(Event, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.title + detail['url'] = url_for('event_detail', event_id=item.id) + elif bm.item_type == 'faculty': + item = db.session.get(Faculty, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.name + detail['url'] = url_for('faculty_profile', slug=item.slug) + elif bm.item_type == 'research': + item = db.session.get(ResearchCenter, bm.item_id) + if item: + detail['item'] = item + detail['title'] = item.name + detail['url'] = url_for('research_center', slug=item.slug) + bookmark_details.append(detail) + return render_template('account.html', bookmark_details=bookmark_details) + + +@app.route('/bookmark/add', methods=['POST']) +@login_required +def bookmark_add(): + item_type = request.form.get('item_type') + item_id = request.form.get('item_id', type=int) + note = request.form.get('note', '') + if item_type and item_id: + existing = Bookmark.query.filter_by( + user_id=current_user.id, item_type=item_type, item_id=item_id + ).first() + if not existing: + bm = Bookmark(user_id=current_user.id, item_type=item_type, + item_id=item_id, note=note) + db.session.add(bm) + db.session.commit() + flash('Saved to bookmarks.', 'success') + else: + flash('Already bookmarked.', 'info') + next_url = request.form.get('next') or request.referrer or url_for('account') + return redirect(next_url) + + +@app.route('/bookmark/remove', methods=['POST']) +@login_required +def bookmark_remove(): + bookmark_id = request.form.get('bookmark_id', type=int) + if bookmark_id: + bm = db.session.get(Bookmark, bookmark_id) + if bm and bm.user_id == current_user.id: + db.session.delete(bm) + db.session.commit() + flash('Bookmark removed.', 'info') + return redirect(request.referrer or url_for('account')) + + +@app.route('/_health') +def health(): + try: + college_count = College.query.count() + program_count = Program.query.count() + return jsonify({ + 'status': 'ok', + 'site': 'berkeley', + 'colleges': college_count, + 'programs': program_count, + }) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + + +@app.errorhandler(404) +def not_found(e): + return render_template('404.html'), 404 + + +@app.errorhandler(500) +def server_error(e): + return render_template('500.html'), 500 + + +# ─── Startup ────────────────────────────────────────────────────────────────── + +with app.app_context(): + db.create_all() + from seed_data import seed + seed() + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=40015, debug=False) diff --git a/sites/berkeley/seed_data.py b/sites/berkeley/seed_data.py new file mode 100644 index 000000000..3865acdc5 --- /dev/null +++ b/sites/berkeley/seed_data.py @@ -0,0 +1,2248 @@ +#!/usr/bin/env python3 +"""Seed data for UC Berkeley mirror site. Idempotent — safe to call multiple times.""" +import re +from datetime import datetime, timedelta + + +def slugify(text): + if not text: + return '' + s = re.sub(r'[^a-zA-Z0-9\s-]', '', text) + s = re.sub(r'[\s]+', '-', s.strip().lower()) + return s + + +def seed(): + from app import db, College, Department, Program, NewsArticle, Event, \ + ResearchCenter, Faculty, User, Bookmark + from flask_bcrypt import Bcrypt + import app as app_module + + bcrypt = Bcrypt(app_module.app) + + # ── Idempotency gate ────────────────────────────────────────────────────── + if College.query.first(): + return + + # ── Colleges ────────────────────────────────────────────────────────────── + colleges_data = [ + { + 'name': 'College of Letters and Science', + 'slug': 'letters-and-science', + 'description': 'The largest and most diverse college at Berkeley, encompassing the humanities, social sciences, physical sciences, and biological sciences. With over 80 departments and programs, L&S offers students an unparalleled breadth of intellectual exploration.', + 'dean': 'Dean Mary C. Gilly', + 'founded_year': 1868, + 'undergrad_count': 16000, + 'grad_count': 3500, + 'dept_count': 80, + }, + { + 'name': 'College of Engineering', + 'slug': 'engineering', + 'description': 'One of the top engineering schools in the world, Berkeley Engineering offers programs that combine rigorous technical training with broad interdisciplinary perspectives, preparing students to tackle the grand challenges of the 21st century.', + 'dean': 'Dean Tsu-Jae King Liu', + 'founded_year': 1931, + 'undergrad_count': 4500, + 'grad_count': 3200, + 'dept_count': 11, + }, + { + 'name': 'Haas School of Business', + 'slug': 'haas-business', + 'description': 'The Haas School of Business develops leaders who redefine how we do business. Ranked among the top business schools globally, Haas emphasizes innovation, sustainability, and responsible stewardship.', + 'dean': 'Dean Ann Harrison', + 'founded_year': 1898, + 'undergrad_count': 700, + 'grad_count': 1500, + 'dept_count': 6, + }, + { + 'name': 'School of Law', + 'slug': 'law', + 'description': 'Berkeley Law is one of the nation\'s premier law schools. Known for its commitment to public interest law and social justice, Boalt Hall has educated generations of legal scholars, practitioners, and public servants.', + 'dean': 'Dean Erwin Chemerinsky', + 'founded_year': 1894, + 'undergrad_count': 0, + 'grad_count': 900, + 'dept_count': 4, + }, + { + 'name': 'School of Information', + 'slug': 'information', + 'description': 'The School of Information is dedicated to creating and sharing knowledge that serves individuals, organizations, and communities in an information-intensive society. I School researchers and students study the intersection of people, information, and technology.', + 'dean': 'Dean Coye Cheshire', + 'founded_year': 1996, + 'undergrad_count': 200, + 'grad_count': 600, + 'dept_count': 3, + }, + { + 'name': 'College of Chemistry', + 'slug': 'chemistry', + 'description': 'The College of Chemistry is the oldest professional school at Berkeley, and consistently ranks among the top chemistry and chemical engineering programs in the world. The college houses Nobel Prize-winning faculty and cutting-edge research labs.', + 'dean': 'Dean Douglas Clark', + 'founded_year': 1872, + 'undergrad_count': 800, + 'grad_count': 700, + 'dept_count': 2, + }, + { + 'name': 'Graduate School of Education', + 'slug': 'education', + 'description': 'Berkeley\'s Graduate School of Education advances educational equity and excellence through research, practice, and policy. Faculty and students work to transform educational systems and improve outcomes for all learners.', + 'dean': 'Dean Mindy Kornhaber', + 'founded_year': 1892, + 'undergrad_count': 0, + 'grad_count': 600, + 'dept_count': 5, + }, + { + 'name': 'School of Public Health', + 'slug': 'public-health', + 'description': 'The School of Public Health at Berkeley prepares leaders to improve health through research, education, and public service. With expertise spanning epidemiology, biostatistics, health policy, and environmental health.', + 'dean': 'Dean Michael Lu', + 'founded_year': 1943, + 'undergrad_count': 300, + 'grad_count': 800, + 'dept_count': 6, + }, + { + 'name': 'College of Natural Resources', + 'slug': 'natural-resources', + 'description': 'The College of Natural Resources integrates natural, social, and human sciences to develop sustainable approaches to environmental and resource management challenges. Students learn to address complex problems at the intersection of ecology, society, and policy.', + 'dean': 'Dean David Ackerly', + 'founded_year': 1868, + 'undergrad_count': 1800, + 'grad_count': 400, + 'dept_count': 7, + }, + { + 'name': 'College of Environmental Design', + 'slug': 'environmental-design', + 'description': 'The College of Environmental Design prepares students to shape the built and natural environment through architecture, landscape architecture, city planning, and urban design. CED emphasizes sustainable, equitable, and beautiful environments.', + 'dean': 'Dean Jennifer Wolch', + 'founded_year': 1959, + 'undergrad_count': 900, + 'grad_count': 600, + 'dept_count': 3, + }, + { + 'name': 'Goldman School of Public Policy', + 'slug': 'public-policy', + 'description': 'The Goldman School of Public Policy educates students to develop and implement effective, evidence-based policies that address society\'s most pressing challenges. The school bridges rigorous analysis with practical problem-solving.', + 'dean': 'Dean Henry Brady', + 'founded_year': 1969, + 'undergrad_count': 0, + 'grad_count': 350, + 'dept_count': 2, + }, + { + 'name': 'School of Social Welfare', + 'slug': 'social-welfare', + 'description': 'The School of Social Welfare advances the well-being of vulnerable populations and communities through education, research, and service. The school prepares social workers and researchers to address poverty, inequality, and social injustice.', + 'dean': 'Dean Tina Sacks', + 'founded_year': 1944, + 'undergrad_count': 0, + 'grad_count': 350, + 'dept_count': 3, + }, + { + 'name': 'School of Journalism', + 'slug': 'journalism', + 'description': 'The Graduate School of Journalism at Berkeley trains journalists to serve the public interest through rigorous reporting, ethical practice, and innovative storytelling. Graduates work across all media platforms locally and globally.', + 'dean': 'Dean Geeta Anand', + 'founded_year': 1903, + 'undergrad_count': 0, + 'grad_count': 200, + 'dept_count': 2, + }, + { + 'name': 'School of Optometry', + 'slug': 'optometry', + 'description': 'The School of Optometry provides doctoral education in optometry and vision science, combining clinical training with cutting-edge research on visual function, disease, and rehabilitation. The school\'s clinic serves the Bay Area community.', + 'dean': 'Dean John Flanagan', + 'founded_year': 1923, + 'undergrad_count': 0, + 'grad_count': 350, + 'dept_count': 3, + }, + ] + + colleges = {} + for cd in colleges_data: + c = College(**cd) + db.session.add(c) + db.session.flush() + colleges[cd['slug']] = c + + # ── Departments ─────────────────────────────────────────────────────────── + departments_data = [ + # College of Letters and Science + ('Department of Mathematics', 'mathematics', 'letters-and-science', + 'The Department of Mathematics offers comprehensive programs in pure and applied mathematics, statistics, and mathematical physics.', 'Prof. Tatiana Toro', '(510) 642-6550', '970 Evans Hall'), + ('Department of Physics', 'physics', 'letters-and-science', + 'The Physics Department conducts research across particle physics, condensed matter, astrophysics, and quantum information.', 'Prof. Hartmut Haeffner', '(510) 642-2319', '366 LeConte Hall'), + ('Department of History', 'history', 'letters-and-science', + 'The History Department offers rich programs in U.S., European, Asian, African, and Latin American history spanning ancient times to the present.', 'Prof. Scott Saul', '(510) 642-1971', '3229 Dwinelle Hall'), + ('Department of English', 'english', 'letters-and-science', + 'The English Department offers courses in literature, creative writing, rhetoric, and linguistics from medieval to contemporary periods.', 'Prof. Ramona Naddaff', '(510) 642-3467', '322 Wheeler Hall'), + ('Department of Political Science', 'political-science', 'letters-and-science', + 'The Political Science Department explores comparative politics, international relations, American politics, and political theory.', 'Prof. Robert Van Houweling', '(510) 642-6323', '210 Barrows Hall'), + ('Department of Psychology', 'psychology', 'letters-and-science', + 'The Psychology Department bridges cognitive, developmental, social, clinical, and neuroscience approaches to understanding human behavior.', 'Prof. Stephen Hinshaw', '(510) 642-5292', '3210 Tolman Hall'), + ('Department of Economics', 'economics', 'letters-and-science', + 'The Economics Department is home to world-renowned scholars in micro- and macroeconomics, econometrics, development, and behavioral economics.', 'Prof. Ulrike Malmendier', '(510) 642-0822', '508-1 Evans Hall'), + ('Department of Sociology', 'sociology', 'letters-and-science', + 'The Sociology Department investigates social structures, inequality, organizations, culture, and social change with global reach.', 'Prof. Cybelle Fox', '(510) 642-4766', '410 Barrows Hall'), + ('Department of Chemistry', 'chemistry-dept', 'chemistry', + 'The Department of Chemistry is home to Nobel laureates and world-leading researchers in organic, inorganic, physical, and theoretical chemistry.', 'Prof. F. Dean Toste', '(510) 642-5882', '420 Latimer Hall'), + ('Department of Chemical and Biomolecular Engineering', 'chemical-biomolecular-engineering', 'chemistry', + 'CBE combines engineering principles with biological sciences to create innovations in pharmaceuticals, energy, and materials.', 'Prof. Nitash Balsara', '(510) 642-5003', '201 Gilman Hall'), + # College of Engineering + ('Department of Electrical Engineering and Computer Sciences', 'eecs', 'engineering', + 'EECS is consistently ranked as one of the top programs globally, with research spanning AI, systems, hardware, algorithms, and robotics.', 'Prof. James Demmel', '(510) 642-1042', '253 Cory Hall'), + ('Department of Mechanical Engineering', 'mechanical-engineering', 'engineering', + 'The ME department conducts research in design, robotics, biomechanics, manufacturing, and energy systems.', 'Prof. Grace Gu', '(510) 642-5260', '6117 Etcheverry Hall'), + ('Department of Civil and Environmental Engineering', 'civil-environmental-engineering', 'engineering', + 'CEE addresses critical infrastructure, environmental protection, and sustainability challenges through research and education.', 'Prof. Kenichi Soga', '(510) 642-3261', '760 Davis Hall'), + ('Department of Bioengineering', 'bioengineering', 'engineering', + 'The Bioengineering Department develops solutions at the interface of engineering and the life sciences, from medical devices to synthetic biology.', 'Prof. Sanjay Kumar', '(510) 642-5204', '306 Stanley Hall'), + ('Department of Materials Science and Engineering', 'materials-science', 'engineering', + 'MSE research covers nanomaterials, biomaterials, electronic materials, and computational materials design.', 'Prof. Daryl Chrzan', '(510) 642-3801', '210 Hearst Memorial Mining Building'), + ('Department of Nuclear Engineering', 'nuclear-engineering', 'engineering', + 'Nuclear Engineering at Berkeley covers nuclear energy, radiation sciences, plasma physics, and nuclear security.', 'Prof. Massimiliano Fratoni', '(510) 642-5010', '4153 Etcheverry Hall'), + ('Department of Industrial Engineering and Operations Research', 'ieor', 'engineering', + 'IEOR applies mathematical modeling, statistics, and optimization to design and improve complex systems.', 'Prof. Zeynep Erkin Baz', '(510) 642-4616', '4141 Etcheverry Hall'), + # Haas School of Business + ('Finance Group', 'finance', 'haas-business', + 'The Finance Group conducts leading research in corporate finance, investments, financial markets, and behavioral finance.', 'Prof. Terrance Odean', '(510) 642-1752', 'F402 Haas School of Business'), + ('Operations and Information Technology Management', 'operations-it', 'haas-business', + 'OITM researches supply chain management, information systems, data analytics, and technology strategy.', 'Prof. Ying-Ju Chen', '(510) 642-1780', 'F620 Haas School of Business'), + # School of Law + ('Jurisprudence and Social Policy', 'jurisprudence-social-policy', 'law', + 'JSP offers interdisciplinary study of law and social science, preparing scholars and practitioners for complex legal challenges.', 'Prof. Jonathan Simon', '(510) 642-3476', '489 Simon Hall'), + # School of Information + ('Information Studies', 'information-studies', 'information', + 'The I School offers interdisciplinary programs at the intersection of information, technology, and people.', 'Prof. Marti Hearst', '(510) 642-1464', '102 South Hall'), + # School of Public Health + ('Epidemiology and Biostatistics', 'epidemiology-biostatistics', 'public-health', + 'The division trains researchers and practitioners to understand disease distribution and health determinants in populations.', 'Prof. Alan Hubbard', '(510) 642-4712', '50 University Hall'), + ('Environmental Health Sciences', 'environmental-health', 'public-health', + 'EHS investigates how environmental factors affect human health, from air quality to chemical exposures.', 'Prof. John Balmes', '(510) 642-3074', 'Valley Life Sciences Building'), + # College of Natural Resources + ('Department of Environmental Science, Policy, and Management', 'espm', 'natural-resources', + 'ESPM integrates natural and social sciences to address environmental challenges including conservation, agriculture, and climate change.', 'Prof. Katharine Mach', '(510) 642-7171', '130 Mulford Hall'), + ('Department of Nutritional Sciences and Toxicology', 'nutritional-sciences', 'natural-resources', + 'NST investigates the roles of nutrients and dietary factors in human health, disease prevention, and toxicology.', 'Prof. Hei Sook Sul', '(510) 642-0646', '119 Morgan Hall'), + # College of Environmental Design + ('Department of Architecture', 'architecture', 'environmental-design', + 'The Architecture Department offers programs in design, theory, history, and technology of the built environment.', 'Prof. Tom Buresh', '(510) 642-4942', '232 Wurster Hall'), + ('Department of City and Regional Planning', 'city-planning', 'environmental-design', + 'DCRP prepares planners and urban designers to create equitable, sustainable, and vibrant cities and regions.', 'Prof. Malo Hutson', '(510) 642-3256', '228 Wurster Hall'), + ('Department of Landscape Architecture and Environmental Planning', 'landscape-architecture', 'environmental-design', + 'LAEP trains landscape architects and environmental planners to design and manage outdoor spaces and ecosystems.', 'Prof. Joe McBride', '(510) 642-4022', '202 Wurster Hall'), + # Graduate School of Education + ('Cognition and Development', 'cognition-development', 'education', + 'Researchers in Cognition and Development study how learning occurs across the lifespan and in diverse educational contexts.', 'Prof. Alison Gopnik', '(510) 642-2984', '4533 Tolman Hall'), + ('Social and Cultural Studies', 'social-cultural-studies', 'education', + 'SCS examines education through the lenses of race, gender, culture, and social justice to advance equity in schools.', 'Prof. Zeus Leonardo', '(510) 642-0818', '5629 Tolman Hall'), + ] + + departments = {} + for (name, slug, college_slug, desc, chair, phone, location) in departments_data: + college = colleges[college_slug] + d = Department( + name=name, slug=slug, college_id=college.id, + description=desc, chair=chair, phone=phone, location=location + ) + db.session.add(d) + db.session.flush() + departments[slug] = d + + # ── Programs ────────────────────────────────────────────────────────────── + programs_data = [ + # Letters and Science - BA + ('Applied Mathematics', 'BA', 'letters-and-science', 'mathematics', + 'The Applied Mathematics major trains students in mathematical modeling, numerical analysis, and computation for real-world problem solving.', + 'Calculus sequence, Linear Algebra, Differential Equations, Probability, Statistics, Numerical Analysis, plus upper division electives in applied areas.', + 120, 4.0, 'November 30', False, False), + ('Pure Mathematics', 'BA', 'letters-and-science', 'mathematics', + 'Pure Mathematics develops rigorous mathematical reasoning across analysis, algebra, topology, and number theory.', + 'Calculus, Linear Algebra, Abstract Algebra, Real Analysis, Complex Analysis, Topology, plus graduate preparatory courses.', + 120, 4.0, 'November 30', False, False), + ('Physics', 'BA', 'letters-and-science', 'physics', + 'The Physics BA provides a comprehensive foundation in classical and modern physics, preparing graduates for careers in science, technology, and education.', + 'Mechanics, Electricity & Magnetism, Quantum Mechanics, Thermodynamics, Mathematical Methods, plus laboratory courses.', + 120, 4.0, 'November 30', False, False), + ('History', 'BA', 'letters-and-science', 'history', + 'The History major develops critical thinking, research, and writing skills through the study of human societies across time and place.', + 'Lower division breadth requirements, upper division seminars, reading courses, and a senior research thesis.', + 120, 4.0, 'November 30', False, False), + ('English', 'BA', 'letters-and-science', 'english', + 'The English major offers deep engagement with literature, language, and writing across centuries and cultures.', + 'Survey courses in British and American literature, upper division seminars, literary theory, and senior writing project.', + 120, 4.0, 'November 30', False, False), + ('Political Science', 'BA', 'letters-and-science', 'political-science', + 'Political Science prepares students to understand government, politics, and international relations through empirical and theoretical lenses.', + 'Introduction to Political Theory, American Politics, Comparative Politics, International Relations, Methods, senior thesis.', + 120, 4.0, 'November 30', False, False), + ('Psychology', 'BA', 'letters-and-science', 'psychology', + 'The Psychology major explores the science of mind and behavior through biological, cognitive, developmental, and social perspectives.', + 'Introduction to Psychology, Statistics, Research Methods, Biological Bases of Behavior, Cognitive Psychology, upper division electives.', + 120, 4.0, 'November 30', False, False), + ('Economics', 'BA', 'letters-and-science', 'economics', + 'The Economics major provides rigorous training in microeconomics, macroeconomics, and econometrics, with applications across industries.', + 'Principles of Microeconomics, Macroeconomics, Econometrics, Intermediate Micro and Macro, upper division electives.', + 120, 4.0, 'November 30', False, False), + ('Sociology', 'BA', 'letters-and-science', 'sociology', + 'Sociology examines social structures, institutions, inequalities, and social change through empirical and theoretical approaches.', + 'Classical and Contemporary Theory, Research Methods, Statistics, Social Inequality, upper division seminars.', + 120, 4.0, 'November 30', False, False), + # Engineering - BS + ('Computer Science', 'BS', 'engineering', 'eecs', + 'The CS BS at Berkeley is one of the most sought-after programs in the world, covering algorithms, systems, AI, and theory.', + 'Data Structures, Algorithms, Computer Architecture, Operating Systems, AI, Machine Learning, Software Engineering, plus technical electives.', + 128, 4.0, 'November 30', False, False), + ('Electrical Engineering and Computer Sciences', 'BS', 'engineering', 'eecs', + 'The EECS BS integrates hardware and software, preparing graduates for careers at the intersection of computing and electronics.', + 'Circuits, Signals & Systems, VLSI Design, Computer Architecture, Algorithms, Software Engineering, Machine Learning.', + 128, 4.0, 'November 30', False, False), + ('Mechanical Engineering', 'BS', 'engineering', 'mechanical-engineering', + 'The ME BS covers engineering design, thermodynamics, fluid mechanics, solid mechanics, robotics, and manufacturing.', + 'Statics, Dynamics, Thermodynamics, Fluid Mechanics, Materials, Control Systems, Design Projects, senior capstone.', + 128, 4.0, 'November 30', False, False), + ('Civil Engineering', 'BS', 'engineering', 'civil-environmental-engineering', + 'Civil Engineering prepares engineers to design, build, and manage infrastructure including bridges, buildings, and water systems.', + 'Structural Analysis, Geotechnical Engineering, Transportation Engineering, Environmental Engineering, senior design project.', + 128, 4.0, 'November 30', False, False), + ('Bioengineering', 'BS', 'engineering', 'bioengineering', + 'Bioengineering applies engineering principles to medicine and biology, spanning medical devices, biomaterials, and synthetic biology.', + 'Cell and Molecular Biology, Biomechanics, Bioelectricity, Biomaterials, Bioinstrumentation, lab rotations, senior capstone.', + 128, 4.0, 'November 30', False, False), + ('Materials Science and Engineering', 'BS', 'engineering', 'materials-science', + 'MSE trains engineers to design advanced materials from metals and ceramics to polymers and nanomaterials.', + 'Crystal Structure, Thermodynamics, Electronic Properties, Mechanical Behavior, Materials Lab, senior thesis project.', + 128, 4.0, 'November 30', False, False), + ('Industrial Engineering and Operations Research', 'BS', 'engineering', 'ieor', + 'IEOR teaches optimization, statistics, and systems engineering with applications in logistics, finance, and healthcare.', + 'Linear Programming, Probability, Statistics, Simulation, Supply Chain, Human Factors, senior design project.', + 128, 4.0, 'November 30', False, False), + ('Nuclear Engineering', 'BS', 'engineering', 'nuclear-engineering', + 'Nuclear Engineering prepares engineers in reactor design, radiation safety, plasma physics, and nuclear security.', + 'Nuclear Physics, Reactor Theory, Radiation Detection, Thermodynamics, Nuclear Safety, senior project.', + 128, 4.0, 'November 30', False, False), + # Chemistry + ('Chemistry', 'BS', 'chemistry', 'chemistry-dept', + 'The Chemistry BS provides rigorous training in organic, inorganic, physical, and analytical chemistry with extensive laboratory experience.', + 'General Chemistry, Organic Chemistry, Physical Chemistry, Analytical Chemistry, Biochemistry, Spectroscopy, senior research.', + 126, 4.0, 'November 30', False, False), + ('Chemical Engineering', 'BS', 'chemistry', 'chemical-biomolecular-engineering', + 'Chemical Engineering combines chemistry, physics, and mathematics to design processes that convert raw materials into useful products.', + 'Mass and Energy Balances, Fluid Mechanics, Heat Transfer, Thermodynamics, Reactor Design, Process Control, senior design.', + 128, 4.0, 'November 30', False, False), + # Natural Resources + ('Environmental Sciences', 'BS', 'natural-resources', 'espm', + 'Environmental Sciences prepares students to understand and address environmental challenges through an interdisciplinary approach.', + 'Ecology, Chemistry, Statistics, Environmental Policy, Field Methods, Capstone Project.', + 120, 4.0, 'November 30', False, False), + ('Nutritional Sciences', 'BS', 'natural-resources', 'nutritional-sciences', + 'Nutritional Sciences trains students in the biochemistry of nutrients, dietary assessment, and the relationships between diet and health.', + 'Biochemistry, Physiology, Nutritional Biochemistry, Diet and Chronic Disease, Nutritional Epidemiology, senior research.', + 120, 4.0, 'November 30', False, False), + # Environmental Design + ('Architecture', 'BS', 'environmental-design', 'architecture', + 'The Architecture BS develops design skills, technical knowledge, and critical thinking in the built environment.', + 'Design Studios, Structures, Environmental Systems, History and Theory, Materials and Methods, senior thesis studio.', + 120, 4.0, 'November 30', False, False), + # Graduate Programs - MS + ('Computer Science', 'MS', 'engineering', 'eecs', + 'The CS MS at Berkeley provides advanced training in algorithms, systems, artificial intelligence, and computer science theory.', + 'Foundational coursework in theory, systems, and AI; research project or thesis; breadth requirements across CS areas.', + 24, 1.5, 'December 1', False, True), + ('Electrical Engineering', 'MS', 'engineering', 'eecs', + 'The EE MS covers advanced topics in signal processing, communications, power systems, photonics, and nanoelectronics.', + 'Advanced coursework in core EE areas, research project, comprehensive exam.', + 24, 1.5, 'December 1', False, True), + ('Mechanical Engineering', 'MS', 'engineering', 'mechanical-engineering', + 'The ME MS provides specialized training in areas such as robotics, biomechanics, energy systems, and manufacturing.', + 'Advanced coursework, research project or thesis, qualifying exam.', + 24, 1.5, 'December 1', False, True), + ('Data Science', 'MS', 'information', 'information-studies', + 'The MIDS is a professional Master\'s program preparing data scientists for careers in industry, government, and research.', + 'Data Engineering, Machine Learning, Statistics, Research Design, Visualization, Leadership, capstone project.', + 54, 2.0, 'February 1', True, False), + ('Information Management and Systems', 'MS', 'information', 'information-studies', + 'MIMS focuses on information organization, retrieval, interface design, and the social dimensions of technology.', + 'Foundations of Information, Research Methods, Synthesis Projects, User Experience, Technology Policy.', + 48, 2.0, 'January 15', False, False), + ('Public Health', 'MPH', 'public-health', 'epidemiology-biostatistics', + 'The MPH prepares public health professionals to address population health challenges through research, program development, and policy advocacy.', + 'Epidemiology, Biostatistics, Environmental Health, Health Policy, Program Planning, Community Practicum, capstone.', + 42, 2.0, 'December 1', False, False), + ('City and Regional Planning', 'MS', 'environmental-design', 'city-planning', + 'The MCP prepares planners to address urban challenges including housing, transportation, environmental sustainability, and economic development.', + 'Planning Theory, Research Methods, Urban Design Studio, Policy Analysis, Community Development, thesis or professional report.', + 48, 2.0, 'January 5', False, False), + ('Environmental Science, Policy and Management', 'MS', 'natural-resources', 'espm', + 'The ESPM MS trains scholars and practitioners to address environmental challenges from field-level conservation to global climate policy.', + 'Research Methods, Environmental Law, Ecosystem Services, Internship, thesis research.', + 24, 2.0, 'January 6', False, False), + # PhD Programs + ('Computer Science', 'PhD', 'engineering', 'eecs', + 'The CS PhD program produces leading researchers who advance the frontiers of computing in algorithms, AI, systems, and theory.', + 'Qualifying Examination, Dissertation Proposal, original dissertation research, teaching requirement.', + 0, 5.0, 'December 1', False, True), + ('Physics', 'PhD', 'letters-and-science', 'physics', + 'The Physics PhD trains world-class researchers in theoretical and experimental physics across all major subdisciplines.', + 'Preliminary Exam, Qualifying Exam, research rotations, dissertation, teaching assistant requirement.', + 0, 5.5, 'December 15', False, True), + ('Mathematics', 'PhD', 'letters-and-science', 'mathematics', + 'The Mathematics PhD prepares mathematicians for research careers at universities and research institutions worldwide.', + 'Preliminary Exam, Oral Qualifying Exam, dissertation on original mathematical research.', + 0, 5.0, 'December 15', False, False), + ('Economics', 'PhD', 'letters-and-science', 'economics', + 'The Economics PhD program trains economists who advance knowledge in micro- and macroeconomics, econometrics, and applied fields.', + 'First and second year courses, Qualifying Fields, dissertation, job market paper.', + 0, 5.0, 'December 1', False, False), + ('Sociology', 'PhD', 'letters-and-science', 'sociology', + 'The Sociology PhD produces rigorous social scientists who study stratification, organizations, culture, and global inequality.', + 'Coursework, Qualifying Examination, dissertation research, departmental seminar participation.', + 0, 5.5, 'December 15', False, False), + ('Chemistry', 'PhD', 'chemistry', 'chemistry-dept', + 'The Chemistry PhD trains researchers in organic, inorganic, physical, and theoretical chemistry in world-class laboratories.', + 'Preliminary Exam, candidacy requirements, dissertation research, group meetings.', + 0, 5.0, 'December 1', False, True), + ('Chemical Engineering', 'PhD', 'chemistry', 'chemical-biomolecular-engineering', + 'The ChE PhD develops engineers who advance process design, catalysis, biomolecular engineering, and energy systems.', + 'Preliminary Exam, Qualifying Exam, research rotations, dissertation.', + 0, 5.0, 'December 1', False, True), + ('Bioengineering', 'PhD', 'engineering', 'bioengineering', + 'The Bioengineering PhD develops leaders who advance medicine and biology through engineering innovation.', + 'Qualifying Exam, candidacy, dissertation research, lab rotations.', + 0, 5.0, 'December 1', False, True), + ('Environmental Science, Policy and Management', 'PhD', 'natural-resources', 'espm', + 'The ESPM PhD trains environmental scholars to conduct original research on ecology, conservation, and environmental policy.', + 'Qualifying Exam, dissertation proposal, dissertation research, teaching requirement.', + 0, 5.5, 'January 6', False, False), + ('Public Health', 'PhD', 'public-health', 'epidemiology-biostatistics', + 'The PhD in Epidemiology trains researchers to investigate disease patterns, risk factors, and population health interventions.', + 'Preliminary Exam, Qualifying Exam, dissertation proposal, dissertation research.', + 0, 5.0, 'December 1', False, True), + # Professional Programs + ('Business Administration', 'MBA', 'haas-business', 'finance', + 'The Haas MBA is a two-year, full-time program that develops ethical, innovative leaders who redefine how we do business.', + 'Core curriculum in accounting, finance, marketing, strategy, operations; electives; experiential learning; leadership development.', + 0, 2.0, 'January 5', False, False), + ('Juris Doctor', 'JD', 'law', 'jurisprudence-social-policy', + 'The Berkeley Law JD is a three-year professional degree program preparing lawyers for careers in public interest, private practice, and academia.', + 'First year core curriculum; upper division electives; clinical programs; journals; moot court; public interest commitment.', + 0, 3.0, 'February 1', False, False), + ('Master of Engineering', 'MEng', 'engineering', 'eecs', + 'The MEng is a one-year professional program combining advanced technical depth with leadership and problem-solving skills for industry.', + 'Technical coursework, IEOR leadership curriculum, capstone project solving real engineering challenges.', + 24, 1.0, 'March 15', False, False), + ('Optometry', 'MD', 'optometry', None, + 'The OD degree is a four-year professional doctoral program providing comprehensive clinical training in optometric care and vision science.', + 'Vision Science courses, clinical rotations, primary care and specialty clinics, research requirement.', + 0, 4.0, 'February 1', False, False), + ] + + for (name, degree_type, college_slug, dept_slug, desc, reqs, + units, duration, deadline, is_online, gre_req) in programs_data: + college = colleges.get(college_slug) + dept = departments.get(dept_slug) if dept_slug else None + slug_base = slugify(f"{name}-{degree_type}") + slug = slug_base + counter = 1 + while Program.query.filter_by(slug=slug).first(): + slug = f"{slug_base}-{counter}" + counter += 1 + p = Program( + name=name, slug=slug, degree_type=degree_type, + college_id=college.id if college else None, + department_id=dept.id if dept else None, + description=desc, requirements=reqs, + units=units, duration_years=duration, + application_deadline=deadline, + is_online=is_online, gre_required=gre_req + ) + db.session.add(p) + + db.session.flush() + + # ── Research Centers ────────────────────────────────────────────────────── + research_centers_data = [ + ('Berkeley Artificial Intelligence Research Lab', 'bair', 'engineering', + 'BAIR brings together UC Berkeley researchers across machine learning, deep learning, robotics, computer vision, and natural language processing.', + 'Prof. Pieter Abbeel', 'Machine Learning, Deep Learning, Computer Vision, NLP, Robotics', 2013), + ('Berkeley Institute for Data Science', 'bids', 'letters-and-science', + 'BIDS catalyzes data science research and education across all disciplines at Berkeley through collaborative data science projects.', + 'Prof. David Culler', 'Data Science, Statistics, Computational Methods, Open Science', 2013), + ('Simons Institute for the Theory of Computing', 'simons-institute', 'engineering', + 'The Simons Institute advances theoretical computer science through semester-long programs bringing together researchers from around the world.', + 'Prof. Shafi Goldwasser', 'Algorithms, Complexity Theory, Cryptography, Quantum Computing', 2012), + ('Energy Biosciences Institute', 'energy-biosciences', 'chemistry', + 'EBI conducts research on biofuels, bioenergy, and the biological conversion of plant biomass to transportation fuels.', + 'Prof. Harvey Blanch', 'Biofuels, Plant Biology, Enzyme Engineering, Energy', 2007), + ('Center for Information Technology Research in the Interest of Society', 'citris', 'engineering', + 'CITRIS develops information technology solutions for society\'s most pressing challenges in health, energy, and environment.', + 'Prof. Costas Spanos', 'IoT, Health Technology, Energy Systems, Smart Cities', 2001), + ('Berkeley Center for Green Chemistry', 'bcgc', 'chemistry', + 'BCGC advances the science and practice of green chemistry by developing safer, more sustainable chemicals and processes.', + 'Prof. John Hartwig', 'Green Chemistry, Sustainable Processes, Chemical Safety', 2008), + ('Center for the Built Environment', 'cbe', 'environmental-design', + 'CBE conducts research on building energy efficiency, indoor environmental quality, and sustainable design strategies.', + 'Prof. Edward Arens', 'Building Performance, Thermal Comfort, Energy Efficiency', 1997), + ('Berkeley Seismological Laboratory', 'seismo-lab', 'letters-and-science', + 'The Seismological Lab monitors and studies earthquakes in Northern California and conducts fundamental research on seismic hazard.', + 'Prof. Douglas Dreger', 'Earthquake Science, Seismic Hazard, Tectonic Geology', 1887), + ('Mathematical Sciences Research Institute', 'msri', 'letters-and-science', + 'MSRI (now SLMath) is the world\'s leading mathematical research center, hosting programs in all areas of pure and applied mathematics.', + 'Prof. Tatiana Toro', 'Pure Mathematics, Applied Mathematics, Statistics', 1982), + ('Institute of Urban and Regional Development', 'iurd', 'environmental-design', + 'IURD conducts policy-oriented research on cities, regions, and communities to promote equitable and sustainable development.', + 'Prof. Karen Chapple', 'Urban Policy, Housing, Regional Development, Transportation', 1962), + ('Berkeley Population Center', 'bpc', 'letters-and-science', + 'BPC supports interdisciplinary research in demography and population health across the social and biological sciences.', + 'Prof. Ron Lee', 'Demography, Population Health, Aging, Fertility', 2012), + ('Center for Labor Research and Education', 'labor-center', 'letters-and-science', + 'The Labor Center bridges academic research and the working world, providing education programs for workers and policy analysis on labor issues.', + 'Prof. Ken Jacobs', 'Labor Policy, Worker Rights, Inequality, Minimum Wage', 1964), + ('Kavli Energy NanoSciences Institute', 'kavli-ensi', 'chemistry', + 'Kavli ENSI investigates fundamental energy conversion processes at the nanoscale to enable next-generation solar cells, batteries, and catalysts.', + 'Prof. Paul Alivisatos', 'Nanoscience, Energy Conversion, Photovoltaics, Materials', 2012), + ('Center for Effective Global Action', 'cega', 'letters-and-science', + 'CEGA generates evidence on what interventions work to improve lives in the developing world, informing policy and program design.', + 'Prof. Edward Miguel', 'Development Economics, Health, Education, RCTs', 2008), + ('QB3 Institute', 'qb3', 'chemistry', + 'QB3 accelerates the translation of biological discoveries into products and companies that benefit human health and the environment.', + 'Prof. Jamie Cate', 'Biotechnology, Drug Discovery, Biomanufacturing, Genomics', 2000), + ('Jacobs Institute for Design Innovation', 'jacobs-institute', 'engineering', + 'The Jacobs Institute is the hub for design education at Berkeley, providing access to digital fabrication, prototyping, and design expertise.', + 'Prof. Björn Hartmann', 'Design, Prototyping, Human-Computer Interaction, Fabrication', 2012), + ('Berkeley Center for New Media', 'bcnm', 'letters-and-science', + 'BCNM explores the theoretical, empirical, and creative dimensions of digital media and their impact on society and culture.', + 'Prof. Kimiko Ryokai', 'Digital Media, Art and Technology, Cultural Studies', 2004), + ('Center for Healthcare Policy and Research', 'chpr', 'public-health', + 'CHPR conducts research on health system performance, access to care, quality improvement, and health policy effectiveness.', + 'Prof. James Bellows', 'Health Policy, Quality of Care, Health Insurance, Access', 2005), + ('Disaster Resilience Network', 'drn', 'engineering', + 'The Disaster Resilience Network conducts interdisciplinary research on earthquake engineering, infrastructure resilience, and community recovery.', + 'Prof. Kenichi Soga', 'Earthquake Engineering, Infrastructure, Resilience, Emergency Management', 2015), + ('Berkeley Global Campus Initiative', 'bgc', 'letters-and-science', + 'BGC advances Berkeley\'s global research partnerships and international collaborative programs across disciplines.', + 'Prof. Claude Steele', 'International Collaboration, Global Research, Education Policy', 2010), + ('Institute of International Studies', 'iis', 'letters-and-science', + 'IIS promotes Berkeley\'s role as a global leader in international and area studies research, education, and public engagement.', + 'Prof. Steven Weber', 'International Relations, Area Studies, Global Policy, Security', 1955), + ('California Policy Lab', 'cpl', 'letters-and-science', + 'The California Policy Lab partners with state and local governments to use data and rigorous research to improve policy outcomes for Californians.', + 'Prof. Jesse Rothstein', 'California Policy, Poverty, Housing, Criminal Justice', 2017), + ('Center for Responsible, Decentralized Intelligence', 'rdai', 'engineering', + 'RDI advances the responsible development of decentralized technologies including blockchain, cryptocurrencies, and distributed AI.', + 'Prof. Dawn Song', 'Blockchain, Cryptography, Decentralized Finance, AI Safety', 2018), + ('Swartz Center for Computational Neuroscience', 'sccn', 'letters-and-science', + 'SCCN develops advanced computational methods and tools for neuroscience research, including brain-computer interfaces.', + 'Prof. Robert Knight', 'Computational Neuroscience, EEG, Brain-Computer Interface, Cognition', 2005), + ('Center for the Science of Psychedelics', 'bcsp', 'public-health', + 'BCSP studies the mechanisms and therapeutic potential of psychedelic compounds for mental health treatment.', + 'Prof. Michael Pollan', 'Psychedelic Research, Mental Health, Neuroscience, Psychiatry', 2020), + ] + + for (name, slug, college_slug, desc, director, focus, founded) in research_centers_data: + college = colleges.get(college_slug) + rc = ResearchCenter( + name=name, slug=slug, + college_id=college.id if college else None, + description=desc, director=director, + focus_areas=focus, founded_year=founded + ) + db.session.add(rc) + + db.session.flush() + + # ── Faculty ─────────────────────────────────────────────────────────────── + faculty_data = [ + # EECS + ('Jennifer Doudna', 'jennifer-doudna', 'Professor, Nobel Laureate', 'chemistry-dept', + 'jdoudna@berkeley.edu', '674 Li Ka Shing', '(510) 643-0113', + 'CRISPR-Cas9 gene editing, RNA biology, structural biochemistry', + 'Jennifer Doudna is a Nobel Prize-winning biochemist and co-developer of CRISPR-Cas9 genome editing. Her research focuses on RNA biology and structural biology of RNA-protein complexes.', False), + ('Pieter Abbeel', 'pieter-abbeel', 'Professor', 'eecs', + 'pabbeel@cs.berkeley.edu', '425 Soda Hall', '(510) 642-9861', + 'Deep reinforcement learning, robot learning, AI for robotics', + 'Pieter Abbeel is a leading researcher in machine learning and robotics, known for developing algorithms that enable robots to learn from demonstrations.', False), + ('Dan Klein', 'dan-klein', 'Professor', 'eecs', + 'klein@cs.berkeley.edu', '724 Soda Hall', '(510) 642-7732', + 'Natural language processing, machine learning, computational linguistics', + 'Dan Klein\'s research focuses on natural language processing and machine learning, with particular interest in parsing, machine translation, and information extraction.', False), + ('Michael Jordan', 'michael-jordan', 'Professor', 'eecs', + 'jordan@cs.berkeley.edu', '723 Soda Hall', '(510) 642-5022', + 'Machine learning, Bayesian statistics, computational biology, optimization', + 'Michael I. Jordan is a pioneer in machine learning and one of the most cited researchers in computer science. He has made foundational contributions to neural networks, Bayesian inference, and probabilistic graphical models.', False), + ('Stuart Russell', 'stuart-russell', 'Professor', 'eecs', + 'russell@cs.berkeley.edu', '387 Soda Hall', '(510) 642-4964', + 'Artificial intelligence, machine learning, AI safety, probabilistic reasoning', + 'Stuart Russell is a world-renowned AI researcher and co-author of the leading AI textbook. His recent work focuses on AI safety and the development of beneficial AI systems.', False), + ('Shafi Goldwasser', 'shafi-goldwasser', 'Professor, Turing Award Laureate', 'eecs', + 'shafi@cs.berkeley.edu', '627 Soda Hall', '(510) 642-0239', + 'Cryptography, complexity theory, computational number theory', + 'Shafi Goldwasser is a Turing Award-winning cryptographer and theoretician. Her work on interactive proofs, zero-knowledge proofs, and probabilistic encryption has transformed cryptography.', False), + ('Dawn Song', 'dawn-song', 'Professor', 'eecs', + 'dawnsong@cs.berkeley.edu', '639 Soda Hall', '(510) 643-3344', + 'AI security, blockchain, deep learning, privacy, formal verification', + 'Dawn Song is a leading researcher in AI security, blockchain, and machine learning. She founded the Oasis Protocol for secure data sharing and blockchain applications.', False), + ('Ion Stoica', 'ion-stoica', 'Professor', 'eecs', + 'istoica@cs.berkeley.edu', '465 Soda Hall', '(510) 642-0252', + 'Distributed systems, cloud computing, data analytics, networking', + 'Ion Stoica is a distributed systems expert who co-founded Databricks and Anyscale. His research has produced foundational systems including Spark and Ray.', False), + # Mathematics + ('Tatiana Toro', 'tatiana-toro', 'Professor', 'mathematics', + 'toro@math.berkeley.edu', '873 Evans Hall', '(510) 642-5382', + 'Geometric measure theory, elliptic PDE, free boundary problems', + 'Tatiana Toro works in geometric analysis and partial differential equations. She is the Director of the Mathematical Sciences Research Institute (SLMath).', False), + ('Michael Christ', 'michael-christ', 'Professor', 'mathematics', + 'mchrist@math.berkeley.edu', '805 Evans Hall', '(510) 642-2891', + 'Harmonic analysis, partial differential equations, complex analysis', + 'Michael Christ works on harmonic analysis and partial differential equations, making fundamental contributions to multilinear analysis and Fourier restriction theory.', False), + # Physics + ('Saul Perlmutter', 'saul-perlmutter', 'Professor, Nobel Laureate', 'physics', + 'saul@lbl.gov', '50-232 LBL', '(510) 486-5203', + 'Cosmology, dark energy, Type Ia supernovae, observational astronomy', + 'Saul Perlmutter shared the 2011 Nobel Prize in Physics for discovering the accelerating expansion of the universe through observations of distant supernovae.', False), + ('Reinhard Genzel', 'reinhard-genzel', 'Professor, Nobel Laureate', 'physics', + 'genzel@mpe.mpg.de', '501 Campbell Hall', '(510) 642-0234', + 'Galactic center, black holes, infrared astronomy, extragalactic astronomy', + 'Reinhard Genzel shared the 2020 Nobel Prize in Physics for discovering a supermassive black hole at the center of the Milky Way galaxy.', False), + ('Hartmut Haeffner', 'hartmut-haeffner', 'Professor', 'physics', + 'hhaeffner@berkeley.edu', '301 Birge Hall', '(510) 642-0386', + 'Quantum computing, quantum information, trapped ions, atomic physics', + 'Hartmut Haeffner leads experiments on trapped-ion quantum computers, working to build practical quantum computing devices.', False), + # Chemistry + ('F. Dean Toste', 'f-dean-toste', 'Professor', 'chemistry-dept', + 'fdtoste@berkeley.edu', '748 Latimer Hall', '(510) 642-6288', + 'Organometallic chemistry, catalysis, gold catalysis, asymmetric synthesis', + 'Dean Toste is a pioneer in homogeneous catalysis, particularly gold-catalyzed reactions. His group develops new transformations for complex molecule synthesis.', False), + ('John Hartwig', 'john-hartwig', 'Professor', 'chemistry-dept', + 'jhartwig@berkeley.edu', '636 Latimer Hall', '(510) 642-4864', + 'Organometallic chemistry, C-H functionalization, asymmetric catalysis, synthetic methodology', + 'John Hartwig is a leader in organometallic chemistry and develops new catalytic reactions for pharmaceutical and materials synthesis.', False), + # Economics + ('Ulrike Malmendier', 'ulrike-malmendier', 'Professor', 'economics', + 'malmendier@haas.berkeley.edu', '517 Evans Hall', '(510) 643-7552', + 'Behavioral economics, corporate finance, law and economics, economic history', + 'Ulrike Malmendier studies behavioral biases in corporate decision-making, consumer finance, and the long-term effects of historical shocks on economic outcomes.', False), + ('Emmanuel Saez', 'emmanuel-saez', 'Professor', 'economics', + 'saez@econ.berkeley.edu', '549 Evans Hall', '(510) 642-4631', + 'Public economics, inequality, taxation, labor economics', + 'Emmanuel Saez is a leading economist studying income inequality. His work documenting the rise of top income shares has shaped the policy debate on inequality and taxation.', False), + # Political Science + ('Robert Van Houweling', 'robert-van-houweling', 'Professor', 'political-science', + 'rvh@berkeley.edu', '210 Barrows Hall', '(510) 642-4219', + 'American politics, Congress, electoral systems, political institutions', + 'Robert Van Houweling studies American political institutions, particularly Congress, and how institutional design affects representation and policy.', False), + # Bioengineering + ('Sanjay Kumar', 'sanjay-kumar', 'Professor', 'bioengineering', + 'skumar@berkeley.edu', '274B Stanley Hall', '(510) 643-3559', + 'Mechanobiology, biomaterials, cancer biophysics, neural engineering', + 'Sanjay Kumar investigates how mechanical forces affect cell behavior, particularly in brain tumors and neural development.', False), + # Environmental Science + ('Katharine Mach', 'katharine-mach', 'Professor', 'espm', + 'kmach@berkeley.edu', '130 Mulford Hall', '(510) 642-3330', + 'Climate change risk, adaptation policy, food security, IPCC', + 'Katharine Mach studies climate change impacts and adaptation, and has been a lead author of IPCC assessment reports on climate risk.', False), + # Public Health + ('John Balmes', 'john-balmes', 'Professor', 'environmental-health', + 'jbalmes@berkeley.edu', '50 University Hall', '(510) 642-6318', + 'Air pollution, respiratory health, environmental health policy, occupational health', + 'John Balmes is a physician-scientist and environmental health policy expert studying the effects of air pollutants on respiratory and cardiovascular health.', False), + # Architecture + ('Tom Buresh', 'tom-buresh', 'Professor', 'architecture', + 'tburesh@berkeley.edu', '270 Wurster Hall', '(510) 642-9042', + 'Architectural design, housing, urbanism, social and environmental sustainability', + 'Tom Buresh focuses on the intersection of architecture, housing, and urban design, with a commitment to equitable and sustainable built environments.', False), + # Civil Engineering + ('Kenichi Soga', 'kenichi-soga', 'Professor', 'civil-environmental-engineering', + 'soga@berkeley.edu', '440 Davis Hall', '(510) 643-1419', + 'Geotechnical engineering, infrastructure sensing, smart cities, sustainability', + 'Kenichi Soga develops smart infrastructure monitoring systems and has pioneered the use of fiber optic sensing for civil engineering applications.', False), + # Materials Science + ('Daryl Chrzan', 'daryl-chrzan', 'Professor', 'materials-science', + 'dcchrzan@berkeley.edu', '210 Hearst Mining Building', '(510) 643-6543', + 'Computational materials science, nanomaterials, defect physics, semiconductor alloys', + 'Daryl Chrzan uses computational methods to study defects in materials, with applications in semiconductor alloys and nanostructures.', False), + # Sociology + ('Cybelle Fox', 'cybelle-fox', 'Professor', 'sociology', + 'cybellefox@berkeley.edu', '410 Barrows Hall', '(510) 642-0985', + 'Immigration, race and ethnicity, historical sociology, social policy', + 'Cybelle Fox studies the historical origins of racial and ethnic inequalities in social policy, particularly immigration law and welfare state development.', False), + # Psychology + ('Stephen Hinshaw', 'stephen-hinshaw', 'Professor', 'psychology', + 'hinshaw@berkeley.edu', '3220 Tolman Hall', '(510) 642-7153', + 'ADHD, child and adolescent development, mental illness stigma, gender', + 'Stephen Hinshaw is a clinical psychologist and neuroscientist who studies attention-deficit/hyperactivity disorder, mental illness stigma, and developmental psychopathology.', False), + # IEOR + ('Zeynep Erkin Baz', 'zeynep-erkin-baz', 'Professor', 'ieor', + 'zeb@ieor.berkeley.edu', '4145 Etcheverry Hall', '(510) 642-7032', + 'Healthcare operations, humanitarian operations, supply chain management, optimization', + 'Zeynep Erkin Baz applies operations research to healthcare and humanitarian logistics, designing systems to improve access and efficiency.', False), + # Nuclear Engineering + ('Massimiliano Fratoni', 'massimiliano-fratoni', 'Professor', 'nuclear-engineering', + 'fratoni@berkeley.edu', '4153 Etcheverry Hall', '(510) 642-8085', + 'Nuclear reactor physics, fuel cycle analysis, advanced reactor design, thorium', + 'Massimiliano Fratoni conducts research on advanced nuclear reactor designs and nuclear fuel cycle analysis for next-generation energy systems.', False), + # Law + ('Erwin Chemerinsky', 'erwin-chemerinsky', 'Dean and Professor', 'jurisprudence-social-policy', + 'chemerinsky@law.berkeley.edu', '215 Simon Hall', '(510) 642-0865', + 'Constitutional law, criminal procedure, first amendment, civil rights', + 'Erwin Chemerinsky is one of the nation\'s leading constitutional law scholars. As Dean of Berkeley Law, he continues to write and speak widely on civil liberties and social justice.', False), + # Haas Business + ('Terrance Odean', 'terrance-odean', 'Professor', 'finance', + 'odean@haas.berkeley.edu', 'F402 Haas', '(510) 642-6767', + 'Behavioral finance, investor behavior, trading, household finance', + 'Terrance Odean is a behavioral finance pioneer who studies investor psychology, trading behavior, and the financial consequences of cognitive biases.', False), + # Emeriti + ('George Akerlof', 'george-akerlof', 'Professor Emeritus, Nobel Laureate', 'economics', + '', '535 Evans Hall', '', + 'Information asymmetry, labor markets, macroeconomics, behavioral economics', + 'George Akerlof won the 2001 Nobel Prize in Economics for his analysis of markets with asymmetric information, most notably the "market for lemons" paper.', True), + ('Owen Chamberlain', 'owen-chamberlain', 'Professor Emeritus, Nobel Laureate', 'physics', + '', '301 Birge Hall', '', + 'Particle physics, antiproton discovery, nuclear physics', + 'Owen Chamberlain shared the 1959 Nobel Prize in Physics for discovering the antiproton, working at the Berkeley Bevatron accelerator.', True), + ] + + for (name, slug, title, dept_slug, email, office, phone, + research_interests, bio, is_emeritus) in faculty_data: + dept = departments.get(dept_slug) + f = Faculty( + name=name, slug=slug, title=title, + department_id=dept.id if dept else None, + email=email, office=office, phone=phone, + research_interests=research_interests, bio=bio, + is_emeritus=is_emeritus + ) + db.session.add(f) + + db.session.flush() + + # ── News Articles ───────────────────────────────────────────────────────── + base_date = datetime(2025, 4, 1) + + articles_data = [ + # Research + ('Berkeley Researchers Develop AI System That Detects Cancer Early', 'research', + 'Prof. Sarah Chen', -10, True, + 'A team of UC Berkeley bioengineers and computer scientists has developed an artificial intelligence system capable of detecting early-stage pancreatic cancer from standard blood tests with 90% accuracy.', + 'UC Berkeley researchers have developed a breakthrough AI diagnostic tool that analyzes biomarkers in routine blood tests to detect pancreatic cancer years before symptoms appear. The study, published in Nature Medicine, demonstrates how machine learning can transform cancer screening.', + 'AI,cancer,bioengineering,machine learning,health'), + ('New Quantum Computing Lab Opens at Berkeley', 'research', + 'Berkeley News Staff', -15, True, + 'The Bakar Quantum Lab, the largest university-based quantum computing facility on the West Coast, opened this week with six quantum processors available to researchers across disciplines.', + 'UC Berkeley inaugurated the Bakar Quantum Lab, a state-of-the-art facility housing six quantum processors ranging from 20 to 127 qubits. The lab will accelerate research in quantum algorithms, materials simulation, and quantum-safe cryptography.', + 'quantum computing,physics,engineering,technology'), + ('Berkeley Team Wins $10M Grant for Climate Research', 'research', + 'Sarah Martinez', -20, True, + 'An interdisciplinary Berkeley team has received a $10 million National Science Foundation grant to study the feedback loops between Arctic ice loss, ocean circulation, and global weather patterns.', + 'The five-year grant will fund an international team led by Berkeley climate scientists who will deploy autonomous underwater vehicles, satellite sensors, and advanced climate models to map how Arctic changes ripple through the global climate system.', + 'climate change,research,NSF,Arctic,environment'), + ('CRISPR Pioneer Jennifer Doudna Receives National Medal of Science', 'research', + 'Berkeley News Staff', -25, True, + 'Nobel Laureate and Berkeley Professor Jennifer Doudna was awarded the National Medal of Science at a White House ceremony, recognizing her transformative contributions to gene editing.', + 'Professor Jennifer Doudna, who co-developed the CRISPR-Cas9 genome editing technology, received the National Medal of Science from the President. Doudna\'s work has revolutionized biology and opened new possibilities for treating genetic diseases.', + 'CRISPR,genomics,Nobel,award,biochemistry'), + ('Berkeley Scientists Find Microplastics in Human Brain Tissue', 'research', + 'Dr. James Wu', -30, False, + 'Berkeley researchers have detected microplastic particles in human brain tissue samples, raising urgent questions about the health effects of ubiquitous plastic pollution.', + 'A peer-reviewed study from UC Berkeley\'s School of Public Health has found microplastic particles in postmortem human brain tissue at concentrations higher than previously documented in other organs. Researchers are now investigating potential neurological effects.', + 'microplastics,environment,public health,neuroscience'), + ('New Study Links Social Media Use to Teen Mental Health Outcomes', 'research', + 'Prof. Amy Rodriguez', -35, False, + 'A Berkeley longitudinal study tracking 5,000 adolescents found significant associations between heavy social media use and anxiety and depression, with effects varying by platform type.', + 'The three-year study, one of the largest of its kind, tracked social media use, mood, sleep, and mental health in teenagers across California. Results suggest that passive scrolling is more harmful than active social interaction online.', + 'mental health,social media,psychology,adolescents,research'), + ('Berkeley Lab Achieves Record Solar Cell Efficiency', 'research', + 'Dr. Priya Nair', -40, False, + 'Berkeley researchers at the Energy Biosciences Institute have achieved a world-record 35.2% efficiency in a tandem perovskite solar cell, surpassing previous records by more than 2 percentage points.', + 'The breakthrough combines two photovoltaic materials in a tandem structure, capturing a broader spectrum of sunlight. The achievement was independently certified by the National Renewable Energy Laboratory and could dramatically reduce the cost of solar electricity.', + 'solar energy,clean energy,materials science,technology,innovation'), + ('Berkeley Economists Document Growing Wealth Gap in California', 'research', + 'Prof. Emmanuel Saez', -45, False, + 'A new report from Berkeley\'s Center for Equitable Growth shows the top 1% of California households now hold 45% of the state\'s wealth, up from 38% in 2019.', + 'Using tax records and household survey data, Berkeley economists have mapped wealth distribution across California counties. The report calls for targeted policy interventions including wealth taxes, housing investments, and expanded social insurance.', + 'inequality,economics,California,wealth,policy'), + # Campus Life + ('Cal Students Break Guinness World Record for Largest Human Chain', 'campus-life', + 'Daily Cal Staff', -50, False, + 'Over 12,000 UC Berkeley students formed a 3.2-mile human chain around campus in an Earth Day demonstration that shattered the previous record.', + 'In a celebration of Earth Day, UC Berkeley students organized the largest student-led environmental demonstration in university history. The human chain stretched from Sather Gate through Sproul Plaza and around the perimeter of the main campus.', + 'Earth Day,campus,student life,environment,record'), + ('New Student Housing Project Breaks Ground on Telegraph Avenue', 'campus-life', + 'Berkeley News Staff', -55, False, + 'Construction began this week on a 1,200-bed student housing complex on Telegraph Avenue that will prioritize affordability and be completed by Fall 2027.', + 'The $380 million project will create 1,200 units of mixed-income student housing within walking distance of campus. At least 20% of units will be priced at below-market rates for students demonstrating financial need.', + 'housing,students,construction,affordability'), + ('Free Speech Movement Cafe Celebrates 60th Anniversary', 'campus-life', + 'Alumni Affairs', -60, True, + 'Berkeley commemorated the 60th anniversary of the Free Speech Movement with a week of lectures, film screenings, and a memorial gathering at Sproul Plaza.', + 'The 1964 Free Speech Movement, which began at UC Berkeley, transformed American campus culture and student rights. This week\'s celebrations included reflections by participants, lectures by historians, and a new documentary film screening.', + 'history,campus,free speech,anniversary,Sproul'), + ('Student-Led Food Pantry Serves 500 Families Weekly', 'campus-life', + 'Berkeley News Staff', -65, False, + 'The Basic Needs Center\'s student-run food pantry has expanded to serve 500 campus community families each week as food insecurity among college students continues to draw national attention.', + 'The UC Berkeley Basic Needs Center has grown its food assistance program tenfold in five years. The program now includes a student food pantry, CalFresh application assistance, and emergency meal vouchers for students in crisis.', + 'food security,student life,community,equity'), + # Faculty + ('Three Berkeley Professors Elected to National Academy of Sciences', 'faculty', + 'Berkeley News Staff', -70, False, + 'Professors Maria Santos (Chemistry), David Kim (EECS), and Rachel Green (Public Health) were elected to the National Academy of Sciences, among the highest honors in American science.', + 'The National Academy of Sciences elected three Berkeley faculty members for their distinguished achievements in research. This year\'s class of 120 new members includes some of the world\'s most distinguished scientists.', + 'faculty,award,National Academy of Sciences,research,honor'), + ('Berkeley Professor Wins Pulitzer Prize for New Book on Redlining', 'faculty', + 'Berkeley News Staff', -75, True, + 'History Professor Angela Davis has won the Pulitzer Prize in General Nonfiction for her groundbreaking study of redlining\'s legacy in California cities.', + 'Professor Davis\'s book traces how discriminatory housing policies implemented from the 1930s through the 1960s continue to shape racial wealth gaps, school quality, and health outcomes in California communities today.', + 'faculty,award,Pulitzer,history,race,housing'), + ('Berkeley Engineering Dean Named to National AI Advisory Board', 'faculty', + 'Berkeley News Staff', -80, False, + 'Dean Tsu-Jae King Liu has been appointed to the National AI Advisory Committee, advising the federal government on AI policy, workforce development, and safety standards.', + 'The appointment recognizes Dean Liu\'s expertise in semiconductor technology and her leadership of one of the nation\'s premier engineering schools. The committee will provide recommendations to the White House on AI governance frameworks.', + 'faculty,AI,policy,engineering,government'), + # Student + ('Cal Senior Wins Rhodes Scholarship to Study Climate Policy at Oxford', 'student', + 'Undergraduate Affairs', -85, False, + 'Amara Johnson, a senior in Environmental Sciences and Political Science, has been awarded a Rhodes Scholarship, joining a small cohort of American students selected annually for graduate study at Oxford.', + 'Amara Johnson, who grew up in Richmond, California and was the first in her family to attend college, plans to study environmental governance and climate adaptation at Oxford\'s Blavatnik School of Government.', + 'scholarship,Rhodes,student,award,environment'), + ('Berkeley Students Launch Startup That Wins $1M Prize', 'student', + 'Innovation Staff', -90, False, + 'Three Berkeley engineering students have won the first-ever Cal Innovate Grand Prize for their AI-powered prosthetic hand that provides sensory feedback to users.', + 'The team, composed of two bioengineering seniors and a CS junior, developed a prosthetic limb with embedded sensors and machine learning algorithms that allow users to feel pressure and temperature. The prize will fund clinical trials.', + 'startup,innovation,engineering,bioengineering,award'), + # Athletics + ('Cal Bears Football Season Preview: New Coach Aims for Pac-12 Title', 'athletics', + 'Sports Desk', -95, False, + 'First-year head coach Marcus Thompson brings an aggressive offensive scheme to Memorial Stadium as the Cal Bears open fall camp targeting their first Pac-12 championship since 2006.', + 'Coach Thompson, who spent six seasons as offensive coordinator at Oregon, plans to install an uptempo spread offense that emphasizes the passing game and creates mismatches in the secondary. Cal returns 18 starters from last year\'s 7-5 team.', + 'football,Cal Bears,athletics,Pac-12,sports'), + ('Women\'s Gymnastics Wins NCAA Championship', 'athletics', + 'Sports Desk', -100, True, + 'The UC Berkeley women\'s gymnastics team claimed the program\'s 30th NCAA championship title at Fort Worth, defeating Georgia in the final with a score of 198.325.', + 'Led by junior all-around champion Sofia Rodriguez and senior specialist Maya Chen, the Cal gymnastics team dominated the NCAA tournament with three consecutive perfect 10.0 scores on the balance beam in the final.', + 'gymnastics,NCAA,championship,athletics,Cal Bears'), + ('Cal Men\'s Basketball: Golden Bears Make Elite Eight Run', 'athletics', + 'Sports Desk', -105, False, + 'The Cal men\'s basketball team made their deepest NCAA Tournament run in three decades, reaching the Elite Eight before falling to #1 seed Duke in a memorable overtime battle.', + 'Powered by sophomore sensation Marcus Williams, who scored 31 points and grabbed 12 rebounds, the Golden Bears upset three higher seeds before their Cinderella run ended against the Blue Devils. The team finished with a 28-10 record.', + 'basketball,NCAA,tournament,Cal Bears,athletics'), + # Science + ('Berkeley Scientists Discover New Exoplanet in Habitable Zone', 'science', + 'Astronomy Department', -110, False, + 'Berkeley astronomers using the Keck Observatory have confirmed the discovery of an Earth-sized exoplanet orbiting in the habitable zone of a nearby star 42 light years away.', + 'The planet, designated Berkeley-1b, was detected using the transit method combined with radial velocity measurements. Its size, density, and orbital period suggest conditions that could potentially support liquid water on its surface.', + 'astronomy,exoplanet,space,science,discovery'), + ('New Research on Gut Bacteria May Unlock Alzheimer\'s Treatment', 'science', + 'Prof. Helen Park', -115, False, + 'Berkeley neuroscientists have identified specific gut microbiome signatures associated with Alzheimer\'s disease, opening a potential new avenue for early diagnosis and treatment.', + 'The study examined gut bacteria composition in 800 patients across disease stages and found that certain bacterial species correlate strongly with amyloid buildup in the brain. Researchers are now testing whether probiotic interventions can slow disease progression.', + 'Alzheimer\'s,neuroscience,gut microbiome,science,health'), + # Arts + ('Berkeley Art Museum Opens Major Retrospective of Bay Area Abstract Expressionism', 'arts', + 'BAMPFA Staff', -120, False, + 'The Berkeley Art Museum and Pacific Film Archive opens a comprehensive retrospective celebrating 75 years of Bay Area Abstract Expressionism, featuring works by 45 artists.', + 'The exhibition, titled "Color in the Bay: 75 Years of Abstract Expression," showcases paintings, sculptures, and mixed media works by artists who lived and worked in the Bay Area from 1950 to the present, tracing the evolution of a distinctly California voice in abstract art.', + 'art,museum,BAMPFA,exhibition,Bay Area'), + ('Berkeley Symphony Premieres New Orchestral Work Inspired by AI', 'arts', + 'Music Department', -125, False, + 'The Berkeley Symphony premiered "Algorithmic Dreams," a 45-minute orchestral work composed using a generative AI system developed by Berkeley music and computer science faculty.', + 'The composition, created through a collaboration between the Music Department and EECS, uses machine learning trained on 18th-century counterpoint and contemporary American orchestral writing to generate thematic material that was then orchestrated by composers.', + 'music,AI,arts,composition,technology'), + # More Research + ('Berkeley Joins $50 Billion National Semiconductor Research Consortium', 'research', + 'Research Office', -130, False, + 'UC Berkeley has joined a 15-university consortium receiving $50 billion in federal funding to rebuild American semiconductor manufacturing capacity and develop next-generation chip technologies.', + 'The CHIPS for America Act funding will support Berkeley\'s research on 2-nanometer chip fabrication, 3D chip stacking, and quantum dot-based transistors. Berkeley\'s Marvell Nanofabrication Laboratory will receive $500 million in new equipment.', + 'semiconductors,technology,engineering,federal funding,research'), + ('Study Shows Berkeley Graduates Earn Premium Over Peers', 'research', + 'Office of the Chancellor', -135, False, + 'A new longitudinal earnings study shows UC Berkeley graduates earn a median salary 23% higher than graduates of comparable public universities ten years after graduation.', + 'The study tracked earnings for 50,000 graduates from the class of 2010 through 2020 and found that Berkeley\'s wage premium is largest for first-generation college students and students from lower-income families, suggesting the university\'s role in economic mobility.', + 'career,economics,graduates,salary,research'), + # More Campus Life + ('Berkeley Celebrates Installation of Solar Panels on Campanile', 'campus-life', + 'Facilities Staff', -140, False, + 'UC Berkeley completed the installation of 2,000 solar panels on the Campanile Esplanade and surrounding campus buildings, cutting electricity costs by $1.2 million annually.', + 'The latest phase of Berkeley\'s Campus Energy Initiative adds 500 kilowatts of solar generating capacity to the campus microgrid. The university is on track to achieve carbon neutrality by 2026 as part of the UC system\'s climate commitment.', + 'sustainability,solar,environment,campus,energy'), + ('Berkeley Dining Achieves Gold Certification for Sustainability', 'campus-life', + 'Housing and Dining', -145, False, + 'Berkeley Dining has achieved Gold certification from the Real Food Challenge, with 35% of all food purchased now meeting criteria for local, fair trade, ecologically sound, or humane production.', + 'The certification recognizes Berkeley Dining\'s multi-year effort to source more sustainable foods while controlling costs. The program prioritizes relationships with Bay Area farmers, ranchers, and food producers.', + 'sustainability,dining,food,environment,campus'), + ('Cal Day Draws Record 50,000 Visitors to Berkeley Campus', 'campus-life', + 'Admissions Office', -150, False, + 'UC Berkeley\'s annual open house, Cal Day, attracted a record 50,000 prospective students and their families to campus this spring, surpassing the previous record by 8,000.', + 'Visitors participated in department open houses, lab tours, student organization fairs, and info sessions across all 14 colleges. The event featured 400 separate activities and was the first Cal Day since the pandemic-era virtual events.', + 'admissions,Cal Day,campus,visitors,prospective students'), + # More Faculty + ('Berkeley Scientist Named HHMI Investigator', 'faculty', + 'Research Office', -155, False, + 'Assistant Professor Kenji Yamamoto (Bioengineering) has been named a Howard Hughes Medical Institute Investigator, one of the most prestigious honors for early-career biomedical scientists.', + 'The HHMI Investigator program provides long-term, flexible funding for exceptional scientists pursuing unconventional and high-risk research. Yamamoto studies the biomechanical regulation of stem cell differentiation.', + 'faculty,HHMI,award,bioengineering,research'), + ('Berkeley Law Professor Appointed to Federal Circuit Court of Appeals', 'faculty', + 'Law School', -160, False, + 'Berkeley Law Professor Claudia Torres has been confirmed by the Senate and appointed to the U.S. Court of Appeals for the Ninth Circuit, filling a vacancy on the nation\'s largest federal appellate court.', + 'Professor Torres, a scholar of immigration law and administrative law, brings two decades of scholarship and litigation experience to the federal bench. She will continue to supervise two pending law review articles before beginning her judicial duties.', + 'faculty,law,appointment,judge,court'), + ] + + for i, (title, category, author, days_offset, featured, + summary, content, tags) in enumerate(articles_data): + slug_base = slugify(title) + slug = slug_base + counter = 1 + while NewsArticle.query.filter_by(slug=slug).first(): + slug = f"{slug_base}-{counter}" + counter += 1 + pub_date = base_date + timedelta(days=days_offset) + article = NewsArticle( + title=title, slug=slug, category=category.title().replace('-', ' '), + author=author, + published_date=pub_date, + content=content, summary=summary, tags=tags, + view_count=100 + i * 17, + featured=featured + ) + db.session.add(article) + + db.session.flush() + + # ── Events ──────────────────────────────────────────────────────────────── + now = datetime(2026, 5, 12) + + events_data = [ + # Upcoming events + ('Nobel Laureate Lecture: Jennifer Doudna on the Future of Gene Editing', 'Lecture', + now + timedelta(days=3), now + timedelta(days=3, hours=2), + '2050 Valley Life Sciences Building', 'Valley Life Sciences', + 'Division of Biological Sciences', False, 'Free', + 'Professor Jennifer Doudna will discuss recent advances in CRISPR technology, including its applications in treating genetic diseases, improving crop yields, and the ethical frameworks guiding responsible use.'), + ('Spring Career Fair 2026', 'Career', + now + timedelta(days=5), now + timedelta(days=5, hours=5), + 'Recreational Sports Facility', 'RSF', + 'Career Center', True, 'Free', + 'Over 200 employers will be recruiting Berkeley students and alumni across all majors. Bring multiple copies of your resume and dress professionally. Pre-registration recommended but walk-ins welcome.'), + ('Berkeley Film Festival Opening Night', 'Arts', + now + timedelta(days=7), now + timedelta(days=7, hours=3), + '2575 Bancroft Way, Pacific Film Archive', 'BAMPFA', + 'Graduate School of Journalism', False, '$15', + 'The 22nd Annual Berkeley Film Festival opens with the world premiere of "Water Rising," a documentary about climate displacement in Pacific Island nations filmed over three years by Berkeley MFA students.'), + ('AI Ethics Symposium 2026', 'Lecture', + now + timedelta(days=10), now + timedelta(days=10, hours=6), + 'Banatao Auditorium, Sutardja Dai Hall', 'Sutardja Dai Hall', + 'BAIR and Center for Technology, Policy and Society', True, 'Free', + 'A full-day symposium bringing together researchers, policymakers, ethicists, and civil society representatives to examine the governance challenges posed by advanced AI systems.'), + ('Cal vs. Stanford Big Game Pre-Celebration', 'Sports', + now + timedelta(days=12), now + timedelta(days=12, hours=4), + 'Sproul Plaza', 'Sproul Hall', + 'Associated Students of UC Berkeley', False, 'Free', + 'Join thousands of Cal fans for the traditional Big Game Rally featuring the Cal Band, cheers, bonfire, and an appearance by Oski the Bear. Spirit prizes awarded to best-dressed fans.'), + ('Graduate School Information Fair', 'Career', + now + timedelta(days=14), now + timedelta(days=14, hours=4), + 'Pauley Ballroom, MLK Student Union', 'MLK Student Union', + 'Graduate Division', False, 'Free', + 'Representatives from Berkeley\'s 14 graduate schools will be available to discuss admission requirements, financial aid, research opportunities, and career outcomes. All undergraduates welcome.'), + ('Spring Dance Showcase: Movement and Memory', 'Arts', + now + timedelta(days=16), now + timedelta(days=16, hours=2), + 'Zellerbach Hall', 'Zellerbach Hall', + 'Department of Theater, Dance and Performance Studies', False, '$20', + 'The annual Spring Dance Showcase presents original work by MFA candidates in dance, featuring contemporary, modern, and collaborative interdisciplinary performances.'), + ('Hackathon: Code for Climate 2026', 'Career', + now + timedelta(days=18), now + timedelta(days=20, hours=12), + 'Soda Hall', 'Soda Hall', + 'EECS Department and Cal Hacks', False, 'Free', + 'A 48-hour hackathon challenging teams to build software solutions for climate adaptation, carbon accounting, clean energy optimization, and environmental monitoring. $50,000 in prizes.'), + ('Wellness Week Kickoff: Mindfulness and Stress Management', 'Health', + now + timedelta(days=21), now + timedelta(days=21, hours=1, minutes=30), + 'North Bowl, Memorial Glade', 'Memorial Glade', + 'University Health Services', False, 'Free', + 'Begin Wellness Week with an outdoor mindfulness session led by the Tang Center\'s wellness counselors. Learn evidence-based techniques for managing academic stress and maintaining mental health.'), + ('Berkeley Startup Pitch Competition Finals', 'Career', + now + timedelta(days=23), now + timedelta(days=23, hours=3), + '310 Sutardja Dai Hall', 'Sutardja Dai Hall', + 'Haas School of Business and Skydeck', True, 'Free', + 'Watch Berkeley\'s top student-founded startups compete for $250,000 in seed funding. Finalists from EECS, Haas, Bioengineering, and Public Health will pitch to a panel of Silicon Valley investors.'), + ('Commencement 2026: College of Engineering', 'Social', + now + timedelta(days=30), now + timedelta(days=30, hours=3), + 'Haas Pavilion', 'Haas Pavilion', + 'College of Engineering', False, 'Free', + 'Congratulations to the graduating class of 2026! Commencement ceremonies for the College of Engineering will feature student speakers, faculty marshals, and the conferral of degrees.'), + ('Public Lecture: Rethinking Urban Housing in the Bay Area', 'Lecture', + now + timedelta(days=25), now + timedelta(days=25, hours=1, minutes=30), + '112 Wurster Hall', 'Wurster Hall', + 'Department of City and Regional Planning', False, 'Free', + 'A lecture and panel discussion on innovative housing strategies for the Bay Area housing crisis, featuring city planners, developers, tenant advocates, and Berkeley researchers.'), + ('Virtual Admissions Webinar: Applying to Berkeley Graduate Programs', 'Virtual', + now + timedelta(days=8), now + timedelta(days=8, hours=1), + 'Online (Zoom)', 'Virtual', + 'Graduate Division', True, 'Free', + 'Graduate admissions staff will walk prospective applicants through the application process, financial aid options, and connect them with current graduate students. Registration required for Zoom link.'), + ('International Food Festival: A Taste of the World', 'Social', + now + timedelta(days=27), now + timedelta(days=27, hours=4), + 'Sproul Plaza', 'Sproul Plaza', + 'International House and Cross-Cultural Center', False, 'Free', + 'The annual International Food Festival celebrates Berkeley\'s global community with food booths representing 60 countries, cultural performances, traditional dress, and interactive activities.'), + ('Research Mixer: Interdisciplinary AI Projects', 'Social', + now + timedelta(days=15), now + timedelta(days=15, hours=2), + '405 Soda Hall', 'Soda Hall', + 'BAIR and Berkeley Research Office', False, 'Free', + 'Graduate students and postdocs from EECS, Statistics, Cognitive Science, and other departments meet to discuss interdisciplinary AI research and explore collaboration opportunities.'), + # Past events + ('Spring 2026 Orientation: Welcome New Golden Bears', 'Social', + now - timedelta(days=30), now - timedelta(days=30, hours=5), + 'Sproul Plaza', 'Sproul Plaza', + 'Office of Undergraduate Education', False, 'Free', + 'Welcome to Berkeley! New students receive their blue and gold lanyards, meet fellow Bears, and explore the 100+ clubs and organizations at the Student Organizations Fair.'), + ('Nobel Laureate Panel: Science and Society', 'Lecture', + now - timedelta(days=45), now - timedelta(days=45, hours=2), + 'Zellerbach Hall', 'Zellerbach Hall', + 'Division of Research', False, 'Free', + 'Four Berkeley Nobel Laureates discussed how their research has impacted society, the role of public funding in science, and the future of academic research.'), + ('Climate Action Week: Zero Waste Campus Challenge', 'Health', + now - timedelta(days=55), now - timedelta(days=50, hours=8), + 'Various Campus Locations', 'Main Campus', + 'Student Environmental Resource Center', False, 'Free', + 'A week of campus-wide events challenging students, faculty, and staff to reduce their carbon footprint, including waste audits, composting workshops, and zero-emission commute pledges.'), + ('Annual Math Tournament: Bay Area High Schools', 'Lecture', + now - timedelta(days=60), now - timedelta(days=60, hours=5), + '160 Dwinelle Hall', 'Dwinelle Hall', + 'Mathematics Department', False, 'Free', + 'UC Berkeley hosts the annual Bay Area Math Tournament, welcoming 800 high school students to compete in individual and team rounds across algebra, geometry, and number theory.'), + ('Jazz at Noon: Spring Semester Concert Series', 'Arts', + now - timedelta(days=20), now - timedelta(days=20, hours=1), + 'Zellerbach Playhouse', 'Zellerbach Hall', + 'Department of Music', False, 'Free', + 'The Berkeley Jazz Ensemble performs original arrangements in a free lunchtime concert open to the campus community. This week\'s program features student compositions inspired by West African rhythms.'), + ('Women in Engineering Summit 2026', 'Career', + now - timedelta(days=35), now - timedelta(days=35, hours=5), + 'Banatao Auditorium', 'Sutardja Dai Hall', + 'Society of Women Engineers, UC Berkeley Chapter', False, 'Free', + 'An annual conference celebrating and supporting women in engineering, featuring keynote speakers from leading tech companies, networking sessions, and workshops on navigating STEM careers.'), + ('Robotics Demo Day: Berkeley Robotics Showcase', 'Lecture', + now - timedelta(days=40), now - timedelta(days=40, hours=3), + '2111 Etcheverry Hall', 'Etcheverry Hall', + 'EECS and Mechanical Engineering', False, 'Free', + 'Berkeley robotics labs open their doors for a public showcase of research projects, including autonomous vehicles, surgical robots, agricultural drones, and soft robotic systems.'), + # More upcoming + ('Study Abroad Fair: Global Opportunities at Berkeley', 'Career', + now + timedelta(days=35), now + timedelta(days=35, hours=3), + 'Upper Sproul Plaza', 'Sproul Plaza', + 'Berkeley Study Abroad', False, 'Free', + 'Learn about semester and year-long exchange programs in over 40 countries. Berkeley partners with top universities in Europe, Asia, Latin America, and Africa. Scholarships available.'), + ('Berkeley Public Health Forum: Health Equity in California', 'Health', + now + timedelta(days=40), now + timedelta(days=40, hours=3), + '50 Warren Hall', 'Warren Hall', + 'School of Public Health', True, 'Free', + 'A panel of public health researchers, community health workers, and state officials discuss progress and challenges in achieving health equity across California\'s diverse communities.'), + ('Shakespeare Festival: Midsummer Night\'s Dream', 'Arts', + now + timedelta(days=45), now + timedelta(days=45, hours=2, minutes=30), + 'Hearst Greek Theatre', 'Greek Theatre', + 'California Shakespeare Theater', False, '$25', + 'California Shakespeare Theater returns to Berkeley\'s Hearst Greek Theatre for an outdoor production of A Midsummer Night\'s Dream, featuring original music and an acclaimed Bay Area cast.'), + ('Berkeley Data Science Summit', 'Lecture', + now + timedelta(days=50), now + timedelta(days=50, hours=8), + 'Banatao Auditorium', 'Sutardja Dai Hall', + 'Berkeley Institute for Data Science', True, 'Free', + 'A full-day summit bringing together data scientists, researchers, and practitioners to discuss advances in machine learning, statistics, data ethics, and applications across domains.'), + ('Annual Picnic Day: Unversity Community Celebration', 'Social', + now + timedelta(days=60), now + timedelta(days=60, hours=6), + 'Memorial Glade', 'Memorial Glade', + 'Office of the Chancellor', False, 'Free', + 'The annual Picnic Day celebration brings together students, faculty, staff, alumni, and community members for a day of food, music, student performances, and research showcases on Memorial Glade.'), + ('Berkeley Entrepreneurship Week', 'Career', + now + timedelta(days=65), now + timedelta(days=70, hours=17), + 'Haas School of Business', 'Haas School', + 'Haas Entrepreneurship Programs', False, 'Free', + 'A week-long series of events including investor panels, founder talks, startup workshops, and a pitch competition for Berkeley students across all disciplines exploring entrepreneurship.'), + ] + + for (title, category, start_dt, end_dt, location, building, + organizer, reg_req, cost, desc) in events_data: + e = Event( + title=title, category=category, + start_datetime=start_dt, end_datetime=end_dt, + location=location, building=building, + organizer=organizer, registration_required=reg_req, + cost=cost, description=desc + ) + db.session.add(e) + + db.session.flush() + + # ── Additional Faculty ──────────────────────────────────────────────────── + additional_faculty = [ + ('Alison Gopnik', 'alison-gopnik', 'Professor', 'cognition-development', + 'gopnik@berkeley.edu', '3321 Tolman Hall', '(510) 642-7138', + 'Cognitive development, learning in children, causal reasoning, philosophy of mind', + 'Alison Gopnik is a developmental psychologist and philosopher who studies how young children learn and how their minds develop. Her popular science books have reached millions of readers.', False), + ('Venus Ranieri', 'venus-ranieri', 'Assistant Professor', 'sociology', + 'vranieri@berkeley.edu', '417 Barrows Hall', '(510) 642-6821', + 'Gender and work, organizational sociology, stratification, labor markets', + 'Venus Ranieri investigates how organizational practices and workplace norms produce and reproduce gender inequality, particularly in professional and managerial occupations.', False), + ('Russell Poldrack', 'russell-poldrack', 'Visiting Professor', 'psychology', + 'poldrack@berkeley.edu', '3210 Tolman Hall', '', + 'Cognitive neuroscience, fMRI, decision making, impulsivity, reward', + 'Russell Poldrack studies the neural basis of decision-making, learning, and self-control using functional MRI and computational modeling approaches.', False), + ('Philip Stark', 'philip-stark', 'Professor', 'mathematics', + 'stark@stat.berkeley.edu', '367 Evans Hall', '(510) 642-1467', + 'Statistics, election auditing, causal inference, food safety, uncertainty quantification', + 'Philip Stark is a statistician who pioneered risk-limiting audits of elections and has applied statistical methods to problems ranging from food safety to earthquake prediction.', False), + ('David Card', 'david-card', 'Professor, Nobel Laureate', 'economics', + 'card@econ.berkeley.edu', '549 Evans Hall', '(510) 642-0822', + 'Labor economics, immigration, minimum wage, returns to education, health economics', + 'David Card won the 2021 Nobel Prize in Economics for his empirical contributions to labor economics, particularly his influential natural experiments studying minimum wage effects and returns to education.', False), + ('Anne Baranger', 'anne-baranger', 'Professor of Teaching', 'chemistry-dept', + 'abaranger@berkeley.edu', '430 Latimer Hall', '(510) 643-2489', + 'Chemistry education, active learning, diversity in STEM, general chemistry pedagogy', + 'Anne Baranger leads efforts to transform undergraduate chemistry education at Berkeley through evidence-based active learning approaches and inclusive pedagogical practices.', False), + ('Koushik Sen', 'koushik-sen', 'Professor', 'eecs', + 'ksen@cs.berkeley.edu', '665 Soda Hall', '(510) 642-7034', + 'Software testing, program analysis, fuzzing, concurrency, security', + 'Koushik Sen develops automated software testing and program analysis tools. His CUTE and KLEE tools have been widely adopted in industry and academia.', False), + ('Franz Franchetti', 'franz-franchetti', 'Adjunct Professor', 'eecs', + '', '629 Soda Hall', '', + 'High performance computing, compilers, signal processing, FPGA, parallel programming', + 'Franz Franchetti works on automatic performance optimization for scientific computing, developing compiler technology that adapts algorithms to hardware characteristics.', False), + ('Constance Penley', 'constance-penley', 'Visiting Professor', 'english', + '', '322 Wheeler Hall', '', + 'Film studies, media theory, feminist studies, cultural studies, science fiction', + 'Constance Penley is a pioneer in film studies and feminist media theory, known for her work on science fiction, technology, and popular culture.', False), + ('Laura Nader', 'laura-nader', 'Professor Emerita', 'anthropology', + '', '232 Kroeber Hall', '', + 'Comparative law, energy policy, conflict resolution, political ecology', + 'Laura Nader is a pioneer in legal anthropology and has made influential contributions to the study of comparative law, energy policy, and how societies resolve conflict.', True), + ('Mina Aganagic', 'mina-aganagic', 'Professor', 'mathematics', + 'mina@math.berkeley.edu', '801 Evans Hall', '(510) 642-6401', + 'Mathematical physics, string theory, knot theory, mirror symmetry, topological field theory', + 'Mina Aganagic works at the interface of mathematics and string theory, making contributions to knot theory, mirror symmetry, and topological aspects of quantum field theory.', False), + ('Karl Pister', 'karl-pister', 'Professor Emeritus', 'civil-environmental-engineering', + '', '750 Davis Hall', '', + 'Computational mechanics, structural engineering, engineering education', + 'Karl Pister served as Chancellor of UC Santa Cruz and Dean of Engineering at Berkeley. He pioneered the Leadership Excellence through Advanced Degrees (LEAD) scholarship program.', True), + ('Kristin Scott', 'kristin-scott', 'Professor', 'nutritional-sciences', + 'kscott@berkeley.edu', '121 Life Sciences', '(510) 643-1949', + 'Olfactory system, taste, neural circuits, Drosophila, appetite regulation', + 'Kristin Scott studies how the brain processes smell and taste signals to guide feeding behavior, using the fruit fly as a model organism to dissect neural circuits.', False), + ('Adam Arkin', 'adam-arkin', 'Professor', 'bioengineering', + 'aparkin@berkeley.edu', '284 Stanley Hall', '(510) 642-0655', + 'Synthetic biology, systems biology, microbial ecology, bioenergy, DOE genomics', + 'Adam Arkin is a pioneer in synthetic biology and systems biology, leading research on the design of biological circuits and the ecology of microbial communities.', False), + ('Alexei Efros', 'alexei-efros', 'Professor', 'eecs', + 'efros@eecs.berkeley.edu', '723 Soda Hall', '(510) 643-3808', + 'Computer vision, deep learning, image synthesis, generative models, visual perception', + 'Alexei Efros is a computer vision researcher known for seminal work on image quilting, scene completion, and generative image synthesis. His work underpins many modern AI image generation techniques.', False), + ('Pilar Ossorio', 'pilar-ossorio', 'Visiting Scholar', 'jurisprudence-social-policy', + '', '489 Simon Hall', '', + 'Bioethics, genetics, law and science, race and genomics', + 'Pilar Ossorio works at the intersection of law, science, and ethics, studying how genomic technologies interact with concepts of race, identity, and social justice.', False), + ('Sanjit Seshia', 'sanjit-seshia', 'Professor', 'eecs', + 'sseshia@eecs.berkeley.edu', '749 Soda Hall', '(510) 643-7239', + 'Formal methods, automated reasoning, cyber-physical systems, AI safety, verification', + 'Sanjit Seshia develops methods for rigorous design and verification of intelligent systems, including autonomous vehicles, robots, and AI-enabled cyber-physical systems.', False), + ('Camille Crittenden', 'camille-crittenden', 'Lecturer', 'information-studies', + 'crittenden@ischool.berkeley.edu', '102 South Hall', '(510) 642-5614', + 'Technology policy, digital media, journalism, information and society', + 'Camille Crittenden teaches courses on information policy and digital media at the I School, drawing on experience in journalism, policy advocacy, and technology research.', False), + ('Nicholas Jewell', 'nicholas-jewell', 'Professor Emeritus', 'epidemiology-biostatistics', + '', '101 Haviland Hall', '', + 'Biostatistics, HIV/AIDS, causal inference, survival analysis, COVID-19', + 'Nicholas Jewell is a biostatistician who made important contributions to the analysis of HIV/AIDS epidemics and has more recently applied rigorous statistical methods to COVID-19 mortality studies.', True), + ('John DeNero', 'john-denero', 'Associate Teaching Professor', 'eecs', + 'denero@cs.berkeley.edu', '775 Soda Hall', '(510) 664-7073', + 'Natural language processing, machine translation, data science education', + 'John DeNero is a leader in CS education at Berkeley, teaching one of the world\'s largest introductory programming courses. His research focuses on natural language processing and machine translation.', False), + ('Andrew Ng', 'andrew-ng', 'Adjunct Professor', 'eecs', + '', 'Soda Hall', '', + 'Machine learning, deep learning, AI education, robotics', + 'Andrew Ng is a co-founder of Coursera and Google Brain, and a pioneer in deep learning. He taught at Berkeley and Stanford before founding AI Fund and DeepLearning.AI.', False), + ('Elaine Ostrander', 'elaine-ostrander', 'Adjunct Professor', 'nutritional-sciences', + '', '119 Morgan Hall', '', + 'Canine genomics, disease genetics, comparative genomics', + 'Elaine Ostrander is a leading expert in canine genomics at the NIH who collaborates with Berkeley researchers to understand the genetic basis of disease in dogs and humans.', True), + ('Nathan Seiberg', 'nathan-seiberg', 'Visiting Professor', 'physics', + '', '395 LeConte Hall', '', + 'Quantum field theory, string theory, duality, condensed matter physics', + 'Nathan Seiberg is one of the most influential theoretical physicists of his generation, with fundamental contributions to supersymmetric gauge theories and string dualities.', False), + ('Amy Herr', 'amy-herr', 'Professor', 'bioengineering', + 'amyherr@berkeley.edu', '303 Stanley Hall', '(510) 643-8649', + 'Microfluidics, protein analysis, single-cell proteomics, biomedical engineering', + 'Amy Herr engineers microfluidic devices for protein analysis at single-cell resolution, enabling new ways to study cancer heterogeneity and drug responses.', False), + ('Trevor Darrell', 'trevor-darrell', 'Professor', 'eecs', + 'trevor@eecs.berkeley.edu', '729 Soda Hall', '(510) 642-9996', + 'Computer vision, deep learning, domain adaptation, multi-modal learning', + 'Trevor Darrell leads the Berkeley Artificial Intelligence Research group\'s computer vision effort, with wide-ranging contributions to visual recognition, domain adaptation, and visual question answering.', False), + ('Lisa Goldberg', 'lisa-goldberg', 'Adjunct Professor', 'mathematics', + 'lgoldberg@haas.berkeley.edu', '373 Evans Hall', '', + 'Financial mathematics, quantitative finance, risk measurement, portfolio management', + 'Lisa Goldberg conducts research on quantitative investment strategies and risk measurement, with applications in sustainable investing and portfolio construction.', False), + ('Jennifer Granick', 'jennifer-granick', 'Adjunct Professor', 'jurisprudence-social-policy', + '', '489 Simon Hall', '', + 'Surveillance law, cybercrime, First Amendment, internet law, civil liberties', + 'Jennifer Granick is a leading attorney and scholar in cybersecurity and civil liberties law, with expertise in surveillance, government hacking, and internet free speech.', False), + ('Michael Botchan', 'michael-botchan', 'Professor', 'chemistry-dept', + 'botchan@berkeley.edu', '446 Li Ka Shing', '(510) 643-7014', + 'DNA replication, papillomavirus, tumor suppressor proteins, genome stability', + 'Michael Botchan investigates the molecular mechanisms of DNA replication and the role of viral and cellular proteins in maintaining genome integrity.', False), + ('Nils Gehlenborg', 'nils-gehlenborg', 'Adjunct Associate Professor', 'information-studies', + '', '102 South Hall', '', + 'Biomedical data visualization, genomics, visual analytics, precision medicine', + 'Nils Gehlenborg develops visualization tools for biomedical data, creating interactive systems that help researchers understand complex genomic and clinical datasets.', False), + ('Gail Murphy', 'gail-murphy', 'Visiting Professor', 'eecs', + '', 'Soda Hall', '', + 'Software engineering, developer tools, program analysis, productivity', + 'Gail Murphy is a pioneer in software engineering research, developing tools that help software developers be more productive and understand complex codebases.', False), + ('Maja Mataric', 'maja-mataric', 'Visiting Professor', 'eecs', + '', '387 Soda Hall', '', + 'Socially assistive robotics, human-robot interaction, rehabilitation, autism therapy', + 'Maja Mataric is a pioneer in socially assistive robotics, developing robots that help people with autism spectrum disorder, rehabilitation, and cognitive training.', False), + ('Jennifer Chayes', 'jennifer-chayes', 'Professor', 'statistics', + 'jchayes@berkeley.edu', '367 Evans Hall', '(510) 642-2781', + 'Algorithmic game theory, network science, machine learning, economics of algorithms', + 'Jennifer Chayes is Associate Provost for the Division of Computing, Data Science, and Society at Berkeley, and a world leader in network science, algorithmic game theory, and machine learning.', False), + ('Zackary Sholem Berger', 'zackary-berger', 'Visiting Scholar', 'public-health', + '', '50 Warren Hall', '', + 'Health disparities, immigrant health, language and medicine, primary care', + 'Zackary Berger is a primary care physician and health policy researcher who studies health disparities, the role of language in medical care, and immigrant community health needs.', False), + ('Clifford Nass', 'clifford-nass', 'Professor Emeritus', 'cognition-development', + '', '4533 Tolman Hall', '', + 'Human-computer interaction, media psychology, multitasking, voice interfaces', + 'Clifford Nass was a pioneer in human-computer interaction research, demonstrating how people naturally apply social rules to computers and other media.', True), + ('Eliza Strickland', 'eliza-strickland', 'Adjunct Lecturer', 'journalism', + '', '121 North Gate Hall', '', + 'Science journalism, technology journalism, AI reporting, biomedical ethics', + 'Eliza Strickland is a science and technology journalist who has written extensively about artificial intelligence, biomedical research, and the ethical dimensions of emerging technologies.', False), + ('Bernadette Park', 'bernadette-park', 'Visiting Scholar', 'psychology', + '', '3210 Tolman Hall', '', + 'Social cognition, stereotyping, implicit attitudes, person perception, prejudice', + 'Bernadette Park studies how people form and use mental categories of other people, with particular interest in how implicit and explicit stereotypes affect judgment and behavior.', False), + ('Philip Darby', 'philip-darby', 'Adjunct Professor', 'political-science', + '', '210 Barrows Hall', '', + 'International relations, postcolonialism, Asian security, development', + 'Philip Darby examines international relations from postcolonial and critical perspectives, focusing on power, identity, and development in Asia and the Pacific.', False), + ('Charis Thompson', 'charis-thompson', 'Professor', 'social-cultural-studies', + 'charis@berkeley.edu', '5629 Tolman Hall', '(510) 643-7285', + 'Science and technology studies, reproductive technology, race, gender', + 'Charis Thompson is a scholar of science and technology studies who examines how reproductive technologies intersect with race, gender, and bioethics in medical and social contexts.', False), + ('Aditya Garg', 'aditya-garg', 'Assistant Professor', 'landscape-architecture', + 'adgarg@berkeley.edu', '202 Wurster Hall', '(510) 642-4893', + 'Urban ecology, green infrastructure, landscape urbanism, stormwater management', + 'Aditya Garg designs and researches urban landscapes that integrate ecological function with human use, with a focus on water-sensitive design and urban biodiversity.', False), + ('Rosemary Joyce', 'rosemary-joyce', 'Professor', 'cognition-development', + 'rajoyce@berkeley.edu', '232 Kroeber Hall', '(510) 642-3801', + 'Archaeology, Mesoamerica, gender in ancient societies, museum studies', + 'Rosemary Joyce is an archaeologist who studies gender, identity, and material culture in ancient Mesoamerican societies, with fieldwork in Honduras and museum collections research.', False), + ('Pablo Spiller', 'pablo-spiller', 'Professor Emeritus', 'operations-it', + '', 'F620 Haas', '', + 'Regulation, industrial organization, telecommunications policy, energy economics', + 'Pablo Spiller is an expert in regulatory economics who has advised governments and companies worldwide on telecommunications and energy sector reforms.', True), + ('Ricardo Fraiman', 'ricardo-fraiman', 'Visiting Scholar', 'mathematics', + '', '801 Evans Hall', '', + 'Nonparametric statistics, functional data analysis, data depth', + 'Ricardo Fraiman is a statistician specializing in nonparametric methods and functional data analysis, with applications in medicine, engineering, and environmental science.', False), + ('Wendy Brown', 'wendy-brown', 'Professor', 'political-science', + 'wbrown@berkeley.edu', '210 Barrows Hall', '(510) 643-2742', + 'Political theory, neoliberalism, democracy, feminist theory, Foucault', + 'Wendy Brown is a leading political theorist whose work on neoliberalism, democracy, and sovereignty has shaped contemporary political thought across the humanities and social sciences.', False), + ('George Smoot', 'george-smoot', 'Professor Emeritus, Nobel Laureate', 'physics', + '', '359 LeConte Hall', '', + 'Cosmology, cosmic microwave background, dark matter, dark energy, COBE satellite', + 'George Smoot shared the 2006 Nobel Prize in Physics for discovering the anisotropy of the cosmic microwave background radiation, providing strong evidence for the Big Bang model.', True), + ('Mario Molina', 'mario-molina', 'Professor Emeritus, Nobel Laureate', 'chemistry-dept', + '', '420 Latimer Hall', '', + 'Atmospheric chemistry, ozone depletion, CFCs, air quality, climate', + 'Mario Molina shared the 1995 Nobel Prize in Chemistry for his work on the depletion of the ozone layer by chlorofluorocarbons (CFCs), leading to the Montreal Protocol.', True), + ('Melissa Dell', 'melissa-dell', 'Visiting Scholar', 'economics', + '', '549 Evans Hall', '', + 'Economic history, political economy, development economics, Latin America, machine learning', + 'Melissa Dell is a development economist who uses historical analysis and machine learning to study the long-run causes of inequality in Latin America and other developing regions.', False), + ('Shachar Kariv', 'shachar-kariv', 'Professor', 'economics', + 'kariv@econ.berkeley.edu', '533 Evans Hall', '(510) 642-8401', + 'Behavioral economics, experimental economics, social networks, risk and uncertainty', + 'Shachar Kariv uses controlled experiments and revealed preference methods to study how people make economic decisions under uncertainty and in social settings.', False), + ] + + for (name, slug, title, dept_slug, email, office, phone, + research_interests, bio, is_emeritus) in additional_faculty: + dept = departments.get(dept_slug) + existing = Faculty.query.filter_by(slug=slug).first() + if not existing: + f = Faculty( + name=name, slug=slug, title=title, + department_id=dept.id if dept else None, + email=email, office=office, phone=phone, + research_interests=research_interests, bio=bio, + is_emeritus=is_emeritus + ) + db.session.add(f) + + db.session.flush() + + # ── One more faculty to reach 80+ ───────────────────────────────────────── + last_faculty = [ + ('Hany Farid', 'hany-farid', 'Professor', 'eecs', + 'hfarid@berkeley.edu', '731 Soda Hall', '(510) 642-1170', + 'Digital forensics, image analysis, deepfakes, misinformation, disinformation', + 'Hany Farid is a leading expert in digital forensics and image analysis. He develops techniques to detect manipulated images, videos, and audio, and advises on policies to combat misinformation.', False), + ('Anca Dragan', 'anca-dragan', 'Associate Professor', 'eecs', + 'anca@cs.berkeley.edu', '729 Soda Hall', '(510) 643-5812', + 'Human-robot interaction, robot learning, autonomous vehicles, value alignment', + 'Anca Dragan develops algorithms that enable robots to interact with and assist people in natural environments. Her work on value alignment and shared autonomy has influenced autonomous vehicle design.', False), + ('Ren Ng', 'ren-ng', 'Associate Professor', 'eecs', + 'ren@cs.berkeley.edu', '771 Soda Hall', '(510) 642-1862', + 'Computational photography, light field cameras, imaging systems, computer graphics', + 'Ren Ng invented the light field camera and founded Lytro. His research on computational imaging bridges optics, signal processing, and machine learning to create novel imaging capabilities.', False), + ] + for (name, slug, title, dept_slug, email, office, phone, + research_interests, bio, is_emeritus) in last_faculty: + dept = departments.get(dept_slug) + if not Faculty.query.filter_by(slug=slug).first(): + f = Faculty( + name=name, slug=slug, title=title, + department_id=dept.id if dept else None, + email=email, office=office, phone=phone, + research_interests=research_interests, bio=bio, + is_emeritus=is_emeritus + ) + db.session.add(f) + db.session.flush() + + # ── Additional News Articles ────────────────────────────────────────────── + extra_articles = [ + # 2024 articles + ('Berkeley-Led Team Maps First Complete Human Chromosome', 'Research', + 'Dr. Karen Lau', datetime(2024, 5, 15), False, + 'A consortium including Berkeley researchers has completed the first gapless sequence of a human chromosome, a milestone in genomics.', + 'Using long-read sequencing technology developed at Berkeley, researchers have produced the first complete, gap-free sequence of human chromosome 8. The work resolves regions that were previously unsequenceable and reveals new genes relevant to disease.', + 'genomics,chromosome,DNA,research,biology'), + ('Berkeley Joins Global Climate Research Initiative with 30 Universities', 'Research', + 'Climate Research Office', datetime(2024, 6, 1), False, + 'UC Berkeley is a founding member of a new 30-university climate research consortium that will coordinate global weather monitoring, climate modeling, and adaptation strategies.', + 'The Global Climate Research Initiative will share data, computing resources, and researchers across six continents. Berkeley will host the consortium\'s data center and lead the North American monitoring network.', + 'climate,research,international,environment,collaboration'), + ('New Center for Democracy Studies Established at Berkeley', 'Campus Life', + 'Chancellor\'s Office', datetime(2024, 7, 10), False, + 'UC Berkeley has established a new research center dedicated to studying and strengthening democratic institutions, processes, and civic engagement.', + 'The Center for the Study of Democracy, funded by a $25 million gift, will bring together political scientists, legal scholars, historians, and technologists to understand threats to democracy and develop evidence-based interventions.', + 'democracy,politics,research,civic,center'), + ('Berkeley Tops QS World University Rankings for Research Impact', 'Research', + 'Berkeley News Staff', datetime(2024, 8, 20), False, + 'UC Berkeley has ranked first among public universities in the QS World University Rankings on the research impact metric for the third consecutive year.', + 'The QS rankings measure the number of highly cited research publications per faculty member. Berkeley\'s strength spans 14 fields, with top-10 rankings in computer science, engineering, economics, and biological sciences.', + 'ranking,research,university,QS,excellence'), + ('Berkeley Public Health School Launches Free Online COVID Resources', 'Campus Life', + 'School of Public Health', datetime(2024, 9, 5), False, + 'The School of Public Health has launched a freely accessible online portal compiling Berkeley research on COVID-19 long-term effects, vaccine efficacy, and mental health impacts.', + 'The COVID Research Portal aggregates findings from more than 120 Berkeley studies on the pandemic\'s biological, psychological, and social effects. All resources are available in English and Spanish.', + 'COVID-19,public health,online,resources,research'), + ('Berkeley Engineers Build Robot That Can Learn From Watching YouTube', 'Research', + 'Engineering News', datetime(2024, 10, 12), False, + 'Berkeley robotics researchers have developed a system that allows robots to learn manipulation tasks by watching instructional videos, dramatically accelerating robot training.', + 'The system uses large vision-language models to parse video demonstrations and extract task-relevant information, allowing a robot arm to learn complex assembly and cooking tasks with minimal human intervention.', + 'robotics,AI,machine learning,engineering,video'), + ('Nobel Prize Laureate Michael Jordan Lecture Series Kicks Off', 'Faculty', + 'EECS Department', datetime(2024, 11, 1), False, + 'The new Michael I. Jordan Distinguished Lecture Series in Machine Learning and Statistics begins with an inaugural talk on the future of probabilistic AI.', + 'The lecture series, named for Berkeley Professor Michael I. Jordan — often called the "Michael Jordan of machine learning" — will bring world-leading researchers to campus each semester to share advances in statistical machine learning.', + 'machine learning,lecture,faculty,statistics,AI'), + ('Berkeley Researchers Identify Gene Linked to Longevity in Centenarians', 'Science', + 'Prof. Judith Campisi', datetime(2024, 12, 3), False, + 'A Berkeley-led genomic study of 1,200 centenarians has identified a gene variant that appears to protect cells from aging-related damage and inflammation.', + 'The gene, dubbed SAGE-1 (Senescence-Associated Genomic Element 1), was found at significantly higher frequency in individuals who live past 100 without major age-related diseases. The discovery opens new targets for anti-aging interventions.', + 'longevity,aging,genetics,science,centenarians'), + ('Fall Convocation Addresses AI and Academic Integrity', 'Campus Life', + 'Academic Senate', datetime(2024, 12, 15), False, + 'Berkeley\'s Fall Convocation focused this year on how the academic community should adapt policies, pedagogy, and research practices in the age of generative AI.', + 'Chancellor Carol Christ delivered the keynote, urging faculty to develop nuanced AI policies that distinguish between appropriate use of AI tools and academic dishonesty. The convocation also featured a panel of students sharing their perspectives.', + 'AI,academic integrity,campus,policy,education'), + ('Berkeley\'s Free Speech Wall Returns After Renovation', 'Campus Life', + 'Facilities Office', datetime(2025, 1, 8), False, + 'The iconic Free Speech Wall on Bancroft Avenue has reopened after a year-long renovation that preserved its historical character while improving accessibility.', + 'The renovated wall features updated chalk panels, improved lighting for nighttime visibility, and a new digital companion space for virtual expressions. The project included consultation with campus historians and Free Speech Movement veterans.', + 'free speech,campus,history,renovation,Bancroft'), + ('Berkeley Biophysicists Develop Microscope That Can See Individual Proteins', 'Science', + 'Biophysics Department', datetime(2025, 1, 22), False, + 'Berkeley researchers have built a cryo-electron microscope configuration that achieves atomic resolution for membrane proteins, opening new possibilities for drug discovery.', + 'The new cryo-EM setup achieves resolution of 1.2 angstroms for membrane-embedded proteins — a historically difficult target due to their lipid environment. The technique should accelerate structural studies of G-protein coupled receptors, a major drug target class.', + 'biophysics,microscopy,proteins,science,drug discovery'), + ('Berkeley Athletes Win Record 12 Medals at Winter World University Games', 'Athletics', + 'Cal Athletics', datetime(2025, 2, 10), False, + 'Cal Berkeley student-athletes won 12 medals — including five gold — at the Winter World University Games, the most ever for a single US university at the biennial competition.', + 'Athletes competing in skiing, short track speed skating, and biathlon led the way, with the women\'s alpine ski team taking gold in the combined event for the second consecutive time.', + 'athletics,winter sports,World University Games,medals,Cal Bears'), + ('Berkeley Law Clinic Wins Landmark Immigration Case at Ninth Circuit', 'Faculty', + 'Berkeley Law', datetime(2025, 2, 25), False, + 'The Berkeley Law Immigration Clinic secured a landmark Ninth Circuit ruling expanding humanitarian protections for asylum seekers who fled domestic violence.', + 'The 11-0 en banc decision in Rodriguez v. Garland held that domestic violence survivors who demonstrate inability to leave abusive relationships due to government inability to protect them qualify for asylum under existing law.', + 'law,immigration,asylum,clinic,court'), + ('New Biotech Hub Opens Adjacent to Berkeley Campus', 'Campus Life', + 'Economic Development Office', datetime(2025, 3, 5), False, + 'The Berkeley Biotech Innovation District, a new 1.5 million square foot life sciences campus, has opened adjacent to the university, creating space for Berkeley spinouts and biotech companies.', + 'The district hosts 45 startup companies, a wet lab incubator, shared core facilities, and direct collaboration agreements with Berkeley faculty. The project is expected to create 8,000 jobs over a decade.', + 'biotech,innovation,startup,campus,economic development'), + ('Study: Berkeley Grads Lead More Companies Than Any Other Public University', 'Research', + 'Haas School of Business', datetime(2025, 3, 18), False, + 'A new analysis of Fortune 500 leadership finds that UC Berkeley alumni serve as CEOs and CFOs at more companies than graduates of any other public university.', + 'The Haas School study tracked 10,000 company leaders and found that Berkeley engineers, MBAs, and liberal arts graduates appear at the highest rates in technology, finance, and energy sectors. The results hold even when controlling for company size and industry.', + 'alumni,business,leadership,Haas,Fortune 500'), + ('Berkeley Physicists Trap Light in a New State of Matter', 'Science', + 'Physics Department', datetime(2025, 4, 2), False, + 'Physicists at Berkeley have created a new state of matter in which light and matter become so entangled that the hybrid particles behave like neither alone.', + 'Using a silicon nitride microresonator cooled to near absolute zero, the team created "polariton condensates" that exhibit quantum fluid behavior at previously unachievable temperatures. The work could enable room-temperature quantum optical devices.', + 'physics,quantum,photonics,matter,condensate'), + ('Berkeley Celebrates 50 Years of Women in STEM Initiative', 'Campus Life', + 'Diversity, Equity and Inclusion Office', datetime(2025, 1, 30), False, + 'UC Berkeley is celebrating the 50th anniversary of its Women in Science and Engineering program, which has helped thousands of students succeed in male-dominated fields.', + 'The WISE program was founded in 1974 by Professor Birch Bayh in response to low representation of women in engineering and physical sciences. Today, 47% of Berkeley engineering undergraduates identify as women or nonbinary.', + 'diversity,women in STEM,anniversary,engineering,inclusion'), + ('Berkeley Launches Nation\'s First AI Safety Minor', 'Academics', + 'EECS Department', datetime(2024, 8, 5), False, + 'UC Berkeley has launched the first undergraduate minor in AI Safety in the United States, offering interdisciplinary coursework in technical AI safety, AI governance, and philosophy of AI.', + 'The minor draws on faculty from EECS, Philosophy, Economics, and the School of Law to prepare students to work on ensuring that advanced AI systems remain safe, beneficial, and aligned with human values.', + 'AI safety,EECS,minor,academics,curriculum'), + ('Berkeley Model UN Wins Best Delegation at Harvard Conference', 'Student', + 'Student Affairs', datetime(2025, 2, 15), False, + 'The Berkeley Model United Nations team won the Best Large Delegation award at the Harvard National Model United Nations conference, competing against 3,000 students from 120 universities.', + 'The 44-member Berkeley delegation earned 12 individual awards in committees spanning the Security Council, Human Rights Council, and specialized agencies, the highest total in Cal MUN history.', + 'student,Model UN,award,international,leadership'), + ('Berkeley Tops Princeton Review for Social Atmosphere', 'Campus Life', + 'Student Affairs', datetime(2025, 3, 25), False, + 'UC Berkeley has ranked first in The Princeton Review\'s annual survey of colleges with the most politically active student bodies and second in the category of "best college town."', + 'The annual survey of 143,000 students found that Berkeley students report the most community engagement, highest rates of political activity, and strongest sense of campus civic life among all ranked universities.', + 'campus life,Princeton Review,ranking,community,student'), + ('Berkeley Receives $100M Gift to Fund AI Ethics Research', 'Research', + 'Office of the Chancellor', datetime(2024, 11, 20), True, + 'The largest single donation in Berkeley\'s history designated for ethics research will fund a new institute dedicated to studying the societal impacts and ethical dimensions of artificial intelligence.', + 'The gift from a Bay Area technology philanthropist will establish the Berkeley Institute for AI Ethics, with dedicated faculty positions, graduate fellowships, and a public engagement program to translate research into policy.', + 'AI ethics,donation,research,technology,policy'), + ('Berkeley Ranks Second Globally in Startup Founder Alumni', 'Research', + 'Haas School of Business', datetime(2024, 6, 30), False, + 'A new PitchBook ranking places UC Berkeley second globally in the number of alumni who have founded venture-backed startups, trailing only Stanford.', + 'Berkeley alumni have founded more than 14,000 companies in the past decade, with particularly high concentrations in AI, biotechnology, clean energy, and fintech. The campus ecosystem includes 25 incubators and accelerators.', + 'startup,entrepreneurship,alumni,innovation,venture capital'), + ('Faculty Senate Approves Major Curriculum Reform for Undergraduates', 'Campus Life', + 'Academic Senate', datetime(2024, 7, 22), False, + 'The Academic Senate has approved the most significant overhaul of undergraduate general education requirements in 30 years, adding courses in data literacy, climate studies, and global health.', + 'Beginning with the class of 2027, all Berkeley undergraduates will complete a course in computational thinking, a seminar in global environmental challenges, and a community engagement experience. The reforms follow two years of faculty deliberation.', + 'curriculum,academics,reform,undergraduate,requirements'), + ('Berkeley Partnership Brings 100 Inner-City Students to Campus Each Summer', 'Student', + 'Community Engagement Office', datetime(2024, 9, 15), False, + 'A new partnership between Berkeley, Oakland Unified School District, and three nonprofits will bring 100 Oakland high school students to live and study on campus each summer.', + 'The Berkeley Summer Scholars program provides intensive academic coursework, mentorship by current Berkeley students, and lab internships in STEM departments. All costs are covered by a combination of university and private funding.', + 'community,outreach,student,Oakland,STEM'), + ('Berkeley Physicists Achieve New Record in Quantum Entanglement Distance', 'Science', + 'Physics Department', datetime(2024, 10, 28), False, + 'A Berkeley experiment using Berkeley Lab\'s free-electron laser has achieved quantum entanglement over a record 12.5 kilometers of optical fiber.', + 'The achievement represents a key step toward quantum repeaters, which would allow quantum communication networks to span continental distances. The team used error-corrected photon pairs to maintain entanglement fidelity over the full distance.', + 'quantum,entanglement,physics,science,communication'), + ('Berkeley Engineering Students Win Solar Decathlon', 'Student', + 'Engineering News', datetime(2024, 5, 20), False, + 'A team of Berkeley engineering and architecture students won the U.S. Department of Energy\'s Solar Decathlon with a zero-energy affordable housing prototype designed for Bay Area residents.', + 'The team, comprising civil, mechanical, electrical, and architecture students, built a 900-square-foot affordable housing unit that generates more energy than it consumes and can withstand California seismic events. The design is being adapted for mass production.', + 'engineering,solar,student,competition,housing'), + ('Berkeley Law Tops Bar Passage Rates Among Tier-1 Schools', 'Faculty', + 'Berkeley Law', datetime(2024, 11, 15), False, + 'Graduates of Berkeley Law posted the highest first-time bar passage rate (97.2%) among top-14 law schools in California, reflecting the strength of Berkeley\'s rigorous doctrinal training.', + 'The 2024 California bar results show that Berkeley graduates pass at rates exceeding UCLA, USC, and other regional competitors. The school\'s bar prep program, which includes individualized testing practice, contributed to the strong results.', + 'law,bar exam,faculty,legal education,ranking'), + ('Berkeley Names 2025 Distinguished Teaching Award Winners', 'Faculty', + 'Academic Senate', datetime(2025, 4, 10), False, + 'Twelve UC Berkeley faculty members received the Distinguished Teaching Award for 2025, the university\'s highest recognition for excellence in undergraduate instruction.', + 'This year\'s award winners include professors from Biology, Mathematics, Literature, Electrical Engineering, and Nutritional Sciences. Recipients are selected by a student-faculty committee based on teaching evaluations, peer reviews, and student nominations.', + 'faculty,teaching,award,undergraduate,excellence'), + ('Berkeley Hosts First-Ever Interdisciplinary AI and Law Conference', 'Research', + 'Berkeley Law', datetime(2025, 1, 15), False, + 'The Berkeley AI and Law Conference brought together 400 researchers, lawyers, regulators, and technologists to address the legal challenges posed by large language models and autonomous systems.', + 'Sessions covered liability for AI-generated content, algorithmic discrimination, AI and intellectual property, and the regulatory frameworks emerging in California, the EU, and the United States. Keynote speakers included U.S. FTC commissioners and AI lab executives.', + 'AI,law,conference,research,regulation'), + ('Berkeley Dining Launches Plant-Based Monday Initiative', 'Campus Life', + 'Housing and Dining', datetime(2025, 2, 20), False, + 'All Berkeley dining halls will offer expanded plant-based menus every Monday, part of the university\'s commitment to reduce carbon emissions from food by 40% by 2030.', + 'Plant-Based Mondays expand the existing Meatless Monday program and introduce new dishes developed in partnership with Berkeley\'s nutrition faculty. Dining halls will track and publish carbon and water footprint data for each Monday menu.', + 'sustainability,dining,food,environment,plant-based'), + ] + + for (title, category, author_extra, pub_date, featured, summary, content, tags) in extra_articles: + slug_base = slugify(title) + slug = slug_base + counter = 1 + while NewsArticle.query.filter_by(slug=slug).first(): + slug = f"{slug_base}-{counter}" + counter += 1 + article = NewsArticle( + title=title, slug=slug, category=category, + author=author_extra or 'Berkeley News Staff', + published_date=pub_date, + content=content, summary=summary, tags=tags, + view_count=200, + featured=featured + ) + db.session.add(article) + + db.session.flush() + + # ── Additional Programs ─────────────────────────────────────────────────── + extra_programs = [ + # More UG + ('Cognitive Science', 'BA', 'letters-and-science', 'cognition-development', + 'Cognitive Science is an interdisciplinary major that integrates linguistics, psychology, computer science, neuroscience, and philosophy to study the mind.', + 'Intro to Cognitive Science, Linguistics, Probability, Cognitive Development, Human-Computer Interaction, senior seminar.', + 120, 4.0, 'November 30', False, False), + ('Linguistics', 'BA', 'letters-and-science', 'english', + 'The Linguistics major examines the structure, meaning, and use of human language through theoretical and empirical approaches.', + 'Introduction to Linguistics, Phonology, Syntax, Semantics, Historical Linguistics, Field Methods, thesis.', + 120, 4.0, 'November 30', False, False), + ('Music', 'BA', 'letters-and-science', 'cognition-development', + 'The Music BA offers rigorous training in music theory, history, and composition alongside opportunities for performance.', + 'Music Theory, Music History, Aural Skills, Composition, Ethnomusicology electives, senior recital or thesis.', + 120, 4.0, 'November 30', False, False), + ('Art Practice', 'BA', 'letters-and-science', 'social-cultural-studies', + 'The Art Practice major offers studio training in painting, sculpture, photography, digital media, and interdisciplinary art.', + 'Drawing, Intermediate Studio, Art History, Senior Studio, Senior Seminar, senior thesis exhibition.', + 120, 4.0, 'November 30', False, False), + ('Geography', 'BA', 'letters-and-science', 'espm', + 'The Geography major investigates spatial relationships between human societies and the natural environment.', + 'Physical Geography, Human Geography, GIS, Remote Sensing, Regional Studies, senior research project.', + 120, 4.0, 'November 30', False, False), + ('Anthropology', 'BA', 'letters-and-science', 'cognition-development', + 'The Anthropology major integrates archaeology, biological anthropology, cultural anthropology, and linguistic anthropology.', + 'Introduction to Anthropology, four-field breadth courses, Methods, upper-division seminars, senior thesis.', + 120, 4.0, 'November 30', False, False), + ('Philosophy', 'BA', 'letters-and-science', 'jurisprudence-social-policy', + 'The Philosophy major develops rigorous analytical reasoning through the study of logic, ethics, metaphysics, and the history of philosophy.', + 'Logic, History of Philosophy, Ethics, Epistemology, upper division seminars, senior thesis.', + 120, 4.0, 'November 30', False, False), + ('Film and Media', 'BA', 'letters-and-science', 'social-cultural-studies', + 'The Film and Media major offers critical and production-based study of cinema, television, digital media, and audio culture.', + 'Film History, Media Theory, Film Analysis, Production Workshop, upper division seminars, capstone production project.', + 120, 4.0, 'November 30', False, False), + ('Near Eastern Studies', 'BA', 'letters-and-science', 'history', + 'Near Eastern Studies examines the history, languages, literature, and cultures of the ancient and modern Middle East and North Africa.', + 'Arabic or Hebrew or Persian, Area history, Literature in Translation, senior seminar.', + 120, 4.0, 'November 30', False, False), + ('South and Southeast Asian Studies', 'BA', 'letters-and-science', 'history', + 'SSEAS integrates the study of languages, histories, religions, and contemporary issues of South and Southeast Asia.', + 'Language sequence, area history, religious traditions, modern literature, senior research paper.', + 120, 4.0, 'November 30', False, False), + ('Public Health', 'BS', 'public-health', 'epidemiology-biostatistics', + 'The Public Health BS trains undergraduates in the scientific foundations of public health, epidemiology, and health policy.', + 'Biology, Chemistry, Epidemiology, Biostatistics, Environmental Health, Health Policy, Community Practice, senior capstone.', + 120, 4.0, 'November 30', False, False), + ('Conservation and Resource Studies', 'BS', 'natural-resources', 'espm', + 'CRS integrates ecology, economics, social science, and policy to prepare students for careers in conservation and sustainability.', + 'Ecology, Environmental Economics, Conservation Biology, Policy Analysis, Field Practicum, senior thesis.', + 120, 4.0, 'November 30', False, False), + ('Landscape Architecture', 'BS', 'environmental-design', 'landscape-architecture', + 'The Landscape Architecture BS combines studio design training with ecological sciences and site engineering for outdoor environments.', + 'Design Studio, Site Engineering, Plant Materials, Ecological Systems, Landscape History, senior design project.', + 128, 4.0, 'November 30', False, False), + ('Urban Studies', 'BA', 'environmental-design', 'city-planning', + 'Urban Studies provides an interdisciplinary perspective on cities, examining their history, social dynamics, politics, and physical form.', + 'Urban Theory, Urban History, Social Geography, Planning Law, Community Development, senior research project.', + 120, 4.0, 'November 30', False, False), + ('Rhetoric', 'BA', 'letters-and-science', 'english', + 'The Rhetoric major trains students in the theory and practice of persuasive discourse across ancient and contemporary traditions.', + 'Classical Rhetoric, Contemporary Rhetorical Theory, Public Address, Written Argumentation, senior seminar.', + 120, 4.0, 'November 30', False, False), + # More graduate programs + ('Architecture', 'MS', 'environmental-design', 'architecture', + 'The MArch prepares architects for professional practice and research through advanced studio work, seminars, and a thesis project.', + 'Advanced Design Studio, Structures and Technology, Environmental Systems, History and Theory, thesis.', + 60, 2.0, 'January 5', False, False), + ('Landscape Architecture', 'MS', 'environmental-design', 'landscape-architecture', + 'The MLA trains landscape architects to design sustainable outdoor environments, from parks to regional landscapes.', + 'Design Studio, Site and Soils Engineering, Plants and Planting Design, Landscape History, professional report.', + 48, 2.0, 'January 5', False, False), + ('City and Regional Planning', 'MS', 'environmental-design', 'city-planning', + 'The MSCP is a research-focused master\'s degree for those seeking to advance knowledge in urban and regional planning.', + 'Planning Theory, Quantitative Methods, Qualifying Paper, dissertation research.', + 48, 2.0, 'December 15', False, False), + ('Mechanical Engineering', 'PhD', 'engineering', 'mechanical-engineering', + 'The ME PhD produces leading researchers in areas such as robotics, biomechanics, fluid dynamics, and energy systems.', + 'Preliminary Exam, Qualifying Exam, dissertation, teaching requirement.', + 0, 5.0, 'December 1', False, True), + ('Civil and Environmental Engineering', 'PhD', 'engineering', 'civil-environmental-engineering', + 'The CEE PhD trains engineers to conduct original research on infrastructure, geotechnical systems, and environmental quality.', + 'Preliminary Exam, Qualifying Exam, dissertation proposal, dissertation research.', + 0, 5.0, 'December 1', False, True), + ('Materials Science and Engineering', 'PhD', 'engineering', 'materials-science', + 'The MSE PhD trains materials researchers to study structure-property relationships in advanced materials for energy, electronics, and medicine.', + 'Qualifying Exam, dissertation proposal, dissertation research, candidacy.', + 0, 5.0, 'December 1', False, True), + ('Nuclear Engineering', 'PhD', 'engineering', 'nuclear-engineering', + 'The NE PhD prepares researchers to advance nuclear energy, radiation safety, and nuclear security through original research.', + 'Qualifying Exam, candidacy, dissertation, teaching assistant.', + 0, 5.0, 'December 1', False, True), + ('Industrial Engineering and Operations Research', 'PhD', 'engineering', 'ieor', + 'The IEOR PhD trains researchers in optimization, stochastic modeling, and data-driven decision-making for complex systems.', + 'Qualifying Exam, candidacy, dissertation research.', + 0, 5.0, 'December 1', False, True), + ('Political Science', 'PhD', 'letters-and-science', 'political-science', + 'The Political Science PhD trains scholars in comparative politics, international relations, American politics, and political theory.', + 'First-year courses, qualifying examination, dissertation proposal, dissertation, job market paper.', + 0, 5.5, 'December 1', False, False), + ('Psychology', 'PhD', 'letters-and-science', 'psychology', + 'The Psychology PhD prepares researchers across clinical, cognitive, developmental, social, and neuroscience specialties.', + 'Qualifying Exam, research rotations, dissertation proposal, dissertation, teaching requirement.', + 0, 5.0, 'December 1', False, False), + ('History', 'PhD', 'letters-and-science', 'history', + 'The History PhD trains historians who produce original scholarship on societies across time, place, and culture.', + 'Coursework, oral qualifying examination, dissertation proposal, dissertation.', + 0, 6.0, 'December 15', False, False), + ('English', 'PhD', 'letters-and-science', 'english', + 'The English PhD trains literary scholars in critical theory, cultural history, and creative approaches to literature.', + 'Coursework, qualifying examination, dissertation proposal, dissertation.', + 0, 5.5, 'December 15', False, False), + ('Biostatistics', 'PhD', 'public-health', 'epidemiology-biostatistics', + 'The Biostatistics PhD trains researchers to develop and apply statistical methods for biomedical and public health research.', + 'Coursework in statistical theory and methods, qualifying exam, dissertation.', + 0, 5.0, 'December 1', False, True), + ('Environmental Engineering', 'PhD', 'engineering', 'civil-environmental-engineering', + 'The Environmental Engineering PhD trains researchers in water quality, air quality, remediation, and sustainability.', + 'Qualifying Exam, candidacy, dissertation research, teaching.', + 0, 5.0, 'December 1', False, True), + ('Architecture', 'PhD', 'environmental-design', 'architecture', + 'The Architecture PhD trains scholars in the history, theory, and cultural dimensions of the built environment.', + 'Coursework, qualifying examination, dissertation proposal, dissertation.', + 0, 5.0, 'December 15', False, False), + ('Information Science', 'PhD', 'information', 'information-studies', + 'The I School PhD trains researchers to study how people create, share, and use information in technological contexts.', + 'Coursework, qualifying examination, dissertation proposal, dissertation.', + 0, 5.0, 'December 1', False, False), + ('Education', 'PhD', 'education', 'cognition-development', + 'The Education PhD prepares scholars to conduct original research on learning, teaching, and educational systems.', + 'Coursework in learning science or social studies of education, qualifying exam, dissertation.', + 0, 5.5, 'December 15', False, False), + ('Social Welfare', 'PhD', 'social-welfare', None, + 'The Social Welfare PhD trains scholars to conduct rigorous research on poverty, inequality, and social policy.', + 'Coursework, qualifying examination, dissertation research, teaching experience.', + 0, 5.0, 'December 1', False, False), + # More MS programs + ('Electrical Engineering', 'MS', 'engineering', 'eecs', + 'The EE MS covers advanced areas in power, communications, signal processing, photonics, and microelectronics.', + 'Advanced coursework in EE specialization, comprehensive exam or project.', + 24, 1.5, 'December 1', False, True), + ('Environmental Health Sciences', 'MS', 'public-health', 'environmental-health', + 'The MEHS prepares practitioners to assess and manage environmental exposures affecting human health.', + 'Environmental Health principles, Toxicology, Exposure Assessment, Policy, practicum.', + 36, 1.5, 'December 1', False, False), + ('Development Practice', 'MS', 'natural-resources', 'espm', + 'The MDP is a professional master\'s degree for practitioners working on international development and sustainable resource management.', + 'Development Economics, Health and Nutrition, Governance, Field Practicum, thesis or professional project.', + 48, 2.0, 'February 1', False, False), + ('Social Welfare', 'MS', 'social-welfare', None, + 'The MSW prepares professional social workers for practice in clinical, community, and policy settings.', + 'Foundation year generalist practice, concentration year specialization (clinical or macro), field practicum.', + 60, 2.0, 'January 15', False, False), + ('Energy and Resources', 'MS', 'natural-resources', 'espm', + 'The ERG MA trains students to address complex energy and climate challenges from interdisciplinary perspectives.', + 'Energy Systems, Statistics, Environmental Economics, Policy Analysis, master\'s thesis.', + 36, 2.0, 'January 15', False, False), + ('Education', 'MS', 'education', 'social-cultural-studies', + 'The MA in Education prepares educational researchers, policy analysts, and school leaders.', + 'Foundations of Education, Research Methods, specialization courses, thesis or comprehensive exam.', + 30, 1.5, 'January 5', False, False), + ] + + for (name, degree_type, college_slug, dept_slug, desc, reqs, + units, duration, deadline, is_online, gre_req) in extra_programs: + college = colleges.get(college_slug) + dept = departments.get(dept_slug) if dept_slug else None + slug_base = slugify(f"{name}-{degree_type}") + slug = slug_base + counter = 1 + while Program.query.filter_by(slug=slug).first(): + slug = f"{slug_base}-{counter}" + counter += 1 + p = Program( + name=name, slug=slug, degree_type=degree_type, + college_id=college.id if college else None, + department_id=dept.id if dept else None, + description=desc, requirements=reqs, + units=units, duration_years=duration, + application_deadline=deadline, + is_online=is_online, gre_required=gre_req + ) + db.session.add(p) + + db.session.flush() + + # ── Additional Events ───────────────────────────────────────────────────── + extra_events = [ + ('Berkeley AI Lab Open House', 'Lecture', + now + timedelta(days=4), now + timedelta(days=4, hours=3), + '737 Soda Hall', 'Soda Hall', + 'Berkeley Artificial Intelligence Research Lab', False, 'Free', + 'BAIR opens its research labs to students, alumni, and the public. Demonstrations of current projects in robotics, natural language processing, computer vision, and reinforcement learning.'), + ('Women\'s Basketball: Cal vs. Stanford', 'Sports', + now + timedelta(days=6), now + timedelta(days=6, hours=2), + 'Haas Pavilion', 'Haas Pavilion', + 'Cal Athletics', False, '$15', + 'The Cal Golden Bears women\'s basketball team hosts arch-rival Stanford in a Pac-12 matchup. Student tickets available free with valid Cal ID.'), + ('Public Lecture: The Future of Democracy in the Digital Age', 'Lecture', + now + timedelta(days=9), now + timedelta(days=9, hours=1, minutes=30), + '145 Dwinelle Hall', 'Dwinelle Hall', + 'Department of Political Science', False, 'Free', + 'A leading democratic theorist will explore how digital media, social platforms, and AI are reshaping the conditions for democratic deliberation, civic participation, and political accountability.'), + ('Berkeley Chemistry Open House', 'Lecture', + now + timedelta(days=11), now + timedelta(days=11, hours=4), + '420 Latimer Hall', 'Latimer Hall', + 'College of Chemistry', False, 'Free', + 'The College of Chemistry invites prospective students, current undergraduates, and the public to explore research in chemistry, chemical engineering, and biochemistry.'), + ('Film Screening: Documentary on CRISPR Technology', 'Arts', + now + timedelta(days=13), now + timedelta(days=13, hours=1, minutes=45), + '155 Dwinelle Hall', 'Dwinelle Hall', + 'BAMPFA and Innovative Genomics Institute', False, 'Free', + 'A special screening of the documentary "Code of Life," followed by a discussion with Berkeley researchers including members of the Doudna lab about the current state of gene editing research.'), + ('Berkeley Career Alumni Night: Tech Industry', 'Career', + now + timedelta(days=17), now + timedelta(days=17, hours=3), + 'Alumni House', 'Alumni House', + 'Career Center and Alumni Network', True, 'Free', + 'Fifty Berkeley alumni working in technology companies including Google, Apple, Meta, and dozens of startups will share career advice and networking opportunities with current students.'), + ('Graduate Research Symposium 2026', 'Lecture', + now + timedelta(days=19), now + timedelta(days=19, hours=6), + 'Banatao Auditorium', 'Sutardja Dai Hall', + 'Graduate Division', False, 'Free', + 'Graduate students from across Berkeley present their research in a one-day symposium covering every discipline. Flash talks, poster sessions, and discipline-specific panels.'), + ('Berkeley Chinese New Year Festival', 'Social', + now + timedelta(days=22), now + timedelta(days=22, hours=3), + 'Sproul Plaza', 'Sproul Plaza', + 'Asian Student Union', False, 'Free', + 'Celebrate the Lunar New Year with cultural performances, traditional foods, calligraphy, paper cutting, lion dancing, and activities organized by Berkeley\'s Asian and Asian American student organizations.'), + ('Sustainability Summit: How Berkeley Leads on Climate', 'Lecture', + now + timedelta(days=26), now + timedelta(days=26, hours=5), + 'Sibley Auditorium, Bechtel Engineering Center', 'Bechtel Center', + 'Berkeley Sustainability Office', False, 'Free', + 'A full-day summit showcasing Berkeley\'s research and campus initiatives on climate change, from carbon-neutral operations to breakthrough clean energy research.'), + ('Health Professional School Fair', 'Career', + now + timedelta(days=28), now + timedelta(days=28, hours=4), + 'Pauley Ballroom, MLK Student Union', 'MLK Student Union', + 'Health Professions Program', False, 'Free', + 'Representatives from medical, dental, veterinary, pharmacy, public health, and nursing schools provide information about professional health programs. MCAT, GRE, and application advice available.'), + ('Spring Track and Field Invitational', 'Sports', + now + timedelta(days=32), now + timedelta(days=32, hours=6), + 'Edwards Stadium', 'Edwards Stadium', + 'Cal Athletics', False, '$10', + 'The annual Berkeley Spring Invitational track and field meet welcomes competitors from 20 universities. Events include sprints, distance running, field events, and relays.'), + ('Architecture Design Studio Exhibition', 'Arts', + now + timedelta(days=37), now + timedelta(days=37, hours=4), + '112 Wurster Hall', 'Wurster Hall', + 'Department of Architecture', False, 'Free', + 'The Spring semester design studio exhibition features work by undergraduate and graduate architecture students on themes of urban resilience, housing equity, and climate adaptation.'), + ('Berkeley Philosophy Department Colloquium', 'Lecture', + now + timedelta(days=42), now + timedelta(days=42, hours=1, minutes=30), + '234 Moses Hall', 'Moses Hall', + 'Department of Philosophy', False, 'Free', + 'A leading philosopher discusses the implications of large language model capabilities for philosophy of language, mind, and the nature of understanding.'), + ('Cal Day 2026: Open Campus Celebration', 'Social', + now + timedelta(days=55), now + timedelta(days=55, hours=7), + 'Main Campus', 'Main Campus', + 'Office of Undergraduate Admissions', False, 'Free', + 'Cal Day is UC Berkeley\'s annual open campus celebration welcoming prospective students, families, alumni, and community members. Over 400 events across all departments and student organizations.'), + ('Law and Technology Symposium', 'Lecture', + now + timedelta(days=48), now + timedelta(days=48, hours=6), + '295 Simon Hall', 'Simon Hall', + 'Berkeley Law', True, 'Free', + 'Law students, faculty, and tech industry leaders discuss emerging legal questions in AI liability, data privacy, platform regulation, and cybersecurity.'), + # Past events that weren't already covered + ('Berkeley Campus Cleanup Day', 'Social', + now - timedelta(days=10), now - timedelta(days=10, hours=2), + 'Various Campus Locations', 'Main Campus', + 'Student Environmental Resource Center', False, 'Free', + '300 student volunteers participated in a campus-wide cleanup event, collecting over 400 pounds of litter and planting 120 native plants in the Strawberry Creek riparian zone.'), + ('Berkeley Symphony: Winter Concert', 'Arts', + now - timedelta(days=25), now - timedelta(days=25, hours=2), + 'Hertz Hall', 'Hertz Hall', + 'Department of Music', False, '$20', + 'The Berkeley Symphony Orchestra performed a program of Brahms, Bartók, and a world premiere by Berkeley composition faculty.'), + ('International Development Research Workshop', 'Lecture', + now - timedelta(days=50), now - timedelta(days=50, hours=3), + '107 Giannini Hall', 'Giannini Hall', + 'CEGA and ARE Department', False, 'Free', + 'Researchers presented findings from randomized controlled trials on agricultural extension, conditional cash transfers, and educational interventions in Sub-Saharan Africa and South Asia.'), + ('Engineering Innovation Design Challenge Finals', 'Career', + now - timedelta(days=65), now - timedelta(days=65, hours=3), + '310 Sutardja Dai Hall', 'Sutardja Dai Hall', + 'College of Engineering', False, 'Free', + 'Teams of Berkeley engineering students presented design solutions to real-world challenges in healthcare, infrastructure, and clean energy, judged by a panel of industry experts and faculty.'), + ('Multicultural Student Leadership Conference', 'Social', + now - timedelta(days=75), now - timedelta(days=75, hours=5), + 'MLK Student Union', 'MLK Student Union', + 'Cross-Cultural Center', False, 'Free', + 'Annual conference bringing together student leaders from Berkeley\'s diverse cultural and affinity organizations to share best practices, address common challenges, and build cross-community coalitions.'), + ] + + for (title, category, start_dt, end_dt, location, building, + organizer, reg_req, cost, desc) in extra_events: + e = Event( + title=title, category=category, + start_datetime=start_dt, end_datetime=end_dt, + location=location, building=building, + organizer=organizer, registration_required=reg_req, + cost=cost, description=desc + ) + db.session.add(e) + + db.session.flush() + + # ── Additional news bulk fill ───────────────────────────────────────────── + bulk_news = [ + ('Berkeley Researchers Develop Faster COVID Test Using CRISPR', 'Research', + datetime(2023, 3, 10), False, + 'A rapid COVID diagnostic leveraging CRISPR-based detection achieves 98% sensitivity in under 15 minutes without laboratory equipment.', + 'Prof. Jennifer Doudna and collaborators adapted CRISPR-based diagnostics to detect SARS-CoV-2 RNA in saliva samples with high accuracy, enabling rapid decentralized testing.', + 'COVID,CRISPR,diagnostics,research,health'), + ('Berkeley Economist Wins John Bates Clark Medal', 'Faculty', + datetime(2023, 4, 20), False, + 'Assistant Professor Maya Patel has been awarded the John Bates Clark Medal, presented annually to the most promising American economist under 40.', + 'Prof. Patel\'s research on the labor market effects of immigration and the economic mobility of second-generation immigrants has reshaped the field of labor economics.', + 'economics,award,faculty,medal,labor'), + ('Cal Wins Pac-12 Swimming and Diving Championship', 'Athletics', + datetime(2023, 5, 2), False, + 'The UC Berkeley swimming and diving teams swept the Pac-12 Championships, with both the men\'s and women\'s programs claiming conference titles.', + 'Led by national record holders in the 200 butterfly and 400 medley relay, the Bears dominated the three-day meet, scoring 1,400 total points.', + 'swimming,athletics,Pac-12,championship,Cal Bears'), + ('Berkeley\'s Largest Fundraising Campaign Exceeds $6 Billion Goal', 'Campus Life', + datetime(2023, 5, 15), True, + 'The Light the Way campaign — Berkeley\'s largest-ever comprehensive fundraising effort — has surpassed its $6 billion goal, raising $6.4 billion from 260,000 donors.', + 'Funds will support 1,000 new endowed scholarships, 50 additional endowed chairs, four new research institutes, and $600 million in capital improvements. The campaign was launched in 2020.', + 'fundraising,campaign,donors,scholarships,capital'), + ('Berkeley Launches Center for the Future of Work', 'Research', + datetime(2023, 6, 1), False, + 'A new interdisciplinary research center at Berkeley will study how automation, AI, and remote work are transforming labor markets and workforce development.', + 'The Center brings together economists, sociologists, engineers, and policy scholars to study AI\'s effects on employment, wages, and skill requirements across industries.', + 'work,AI,labor,economics,research'), + ('Berkeley Athletics Achieves Highest Academic Performance Rating', 'Athletics', + datetime(2023, 7, 10), False, + 'UC Berkeley student-athletes achieved an Academic Performance Rating of 999 out of 1000, the highest possible score, for the third consecutive year.', + 'All 30 varsity sports teams met or exceeded the NCAA\'s standard for academic progress. The graduation success rate for Berkeley student-athletes is 92%, 17 points above the national average.', + 'athletics,academics,APR,graduation,Cal Bears'), + ('Berkeley Botanist Discovers New Species in Amazon Rainforest', 'Science', + datetime(2023, 8, 5), False, + 'A Berkeley botanist has identified three previously unknown plant species in the Colombian Amazon, underscoring the continued importance of field taxonomy.', + 'The species, discovered during a 2022 field expedition, belong to the genus Mikania and represent distinct evolutionary lineages. They were confirmed using molecular phylogenetics and morphological analysis.', + 'botany,Amazon,rainforest,discovery,science'), + ('Berkeley Computer Scientists Invent New Programming Language for AI Safety', 'Research', + datetime(2023, 9, 18), False, + 'A team from Berkeley\'s EECS department has developed a new formal programming language designed to specify and verify the behavior of machine learning models.', + 'The language, called SafeSpec, allows developers to write formal specifications of desired AI behavior and automatically verify whether a model satisfies those properties. The work was presented at PLDI 2023.', + 'AI safety,programming,EECS,research,verification'), + ('Berkeley Opens New Student Wellness Center', 'Campus Life', + datetime(2023, 10, 2), False, + 'A state-of-the-art student wellness center has opened at Berkeley, offering mental health counseling, nutrition consultation, and fitness programming under one roof.', + 'The $45 million Tang Center expansion includes 30 new counseling offices, a meditation suite, a nutrition clinic, and a recovery support room. The project was funded by student fees approved in a 2019 referendum.', + 'wellness,mental health,student,campus,health'), + ('Berkeley Joins DOE National Laboratory Partnership for Clean Energy', 'Research', + datetime(2023, 10, 20), False, + 'The Department of Energy has awarded UC Berkeley and Lawrence Berkeley National Laboratory a joint center to accelerate next-generation battery technology.', + 'The Joint Center for Energy Storage Research II will develop solid-state batteries and sodium-ion batteries for grid storage and electric vehicles, with $100 million in federal funding over five years.', + 'energy,batteries,DOE,research,clean energy'), + ('Berkeley Hosts First International Symposium on Quantum Biology', 'Science', + datetime(2023, 11, 5), False, + 'Researchers from 20 countries gathered at Berkeley for the first international symposium on quantum biology, exploring whether quantum effects play functional roles in living systems.', + 'Topics ranged from quantum coherence in photosynthesis to possible quantum effects in bird navigation and enzyme catalysis. Berkeley\'s interdisciplinary approach to the field attracted top researchers from physics, chemistry, and biology.', + 'quantum biology,symposium,science,interdisciplinary,research'), + ('Berkeley Students Launch Environmental Monitoring Network Across Bay Area', 'Student', + datetime(2023, 11, 22), False, + 'Engineering and data science students have deployed a network of 200 low-cost air quality sensors across the Bay Area, creating the most granular air pollution map in the region\'s history.', + 'The PurpleAir-compatible sensors measure fine particulate matter, ozone, and nitrogen dioxide at street-level resolution. Data is freely available through an open API and has been used by public health agencies.', + 'environment,sensors,student,Bay Area,air quality'), + ('Berkeley Law Review Publishes Landmark Issue on AI Governance', 'Faculty', + datetime(2023, 12, 5), False, + 'The California Law Review\'s special issue on artificial intelligence governance brings together 12 articles by Berkeley Law faculty on regulatory frameworks for AI systems.', + 'Topics covered include product liability for AI errors, algorithmic discrimination under civil rights law, AI and due process in criminal justice, and comparative international AI governance approaches.', + 'law,AI,governance,faculty,publication'), + ('Berkeley Student Wins International Math Olympiad Gold', 'Student', + datetime(2024, 1, 15), False, + 'A UC Berkeley freshman who competed for the US team in high school has won gold at the International Mathematical Olympiad, the most prestigious competition in mathematics.', + 'Min-Ji Park, an 18-year-old freshman from Houston, solved all six problems correctly — the first perfect score by a US competitor in a decade. Park plans to pursue a PhD in pure mathematics at Berkeley.', + 'student,mathematics,Olympiad,award,international'), + ('Berkeley Researchers Reduce Solar Panel Cost by 40 Percent', 'Research', + datetime(2024, 2, 8), False, + 'A Berkeley-led team has developed a new perovskite solar cell manufacturing process that reduces production costs by 40% while maintaining efficiency above 30%.', + 'The process uses abundant, non-toxic materials and can be manufactured using modified existing semiconductor equipment. The team is partnering with three solar companies to scale the technology.', + 'solar,clean energy,materials science,research,manufacturing'), + ('Berkeley Anthropologist Discovers 4000-Year-Old Urban Planning System', 'Science', + datetime(2024, 3, 1), False, + 'Archaeological excavations led by a Berkeley professor in the Jordan Valley have uncovered evidence of sophisticated urban planning practices dating to 2000 BCE.', + 'The site includes evidence of centralized water management, standardized building codes, and planned street grids, suggesting advanced administrative infrastructure 2,000 years earlier than previously known in the region.', + 'archaeology,history,science,Jordan,urban planning'), + ('Berkeley Public Policy School Releases Annual California Poverty Report', 'Research', + datetime(2024, 3, 20), False, + 'The annual California Poverty Measure report from the Goldman School shows that 15.3% of Californians live in poverty when accounting for cost of living and government programs.', + 'The report finds that the Earned Income Tax Credit and Medi-Cal are the most effective anti-poverty programs in California, each lifting more than 400,000 people above the poverty line.', + 'poverty,policy,California,research,Goldman School'), + ('Berkeley Engineering Named Top Program for Women by US News', 'Faculty', + datetime(2024, 4, 5), False, + 'US News and World Report has ranked Berkeley Engineering the #1 public engineering program for women, citing high enrollment rates, support programs, and faculty representation.', + 'The ranking reflects Berkeley\'s sustained efforts through programs like the Women in Engineering community, the EECS mentorship program, and targeted scholarship funding that have raised women\'s enrollment from 28% to 46% over ten years.', + 'engineering,women,ranking,diversity,faculty'), + ('Cal Band Celebrates 150th Anniversary', 'Campus Life', + datetime(2024, 4, 22), False, + 'The UC Berkeley Marching Band, the oldest intercollegiate band on the West Coast, celebrated its 150th anniversary with a special halftime show at Memorial Stadium.', + 'The Cal Band was founded in 1873 and has performed at every Cal home football game since. The anniversary show featured 350 band members and alumni spanning six decades.', + 'Cal Band,music,anniversary,campus,tradition'), + ('Berkeley Neuroscience Center Awarded $25M to Study Brain Aging', 'Research', + datetime(2024, 5, 10), True, + 'The National Institute on Aging has awarded Berkeley\'s Neuroscience Institute $25 million to establish a Center for Research on the Aging Brain.', + 'The center will use multi-modal neuroimaging, single-cell genomics, and longitudinal behavioral studies to identify the earliest biomarkers of neurodegeneration and test interventions to delay cognitive decline.', + 'neuroscience,aging,NIH,research,brain'), + ('Berkeley Hosts Governor\'s Summit on Technology and Education', 'Campus Life', + datetime(2024, 6, 15), False, + 'UC Berkeley hosted California Governor Newsom\'s Technology and Education Summit, bringing together 300 educational leaders, technology executives, and policymakers.', + 'Summit panels addressed AI in K-12 classrooms, university research-to-industry transfer, workforce training for the AI economy, and digital equity gaps in California schools.', + 'education,technology,policy,California,summit'), + ('Berkeley Sociology Department Receives Diversity in Research Award', 'Faculty', + datetime(2024, 7, 5), False, + 'The American Sociological Association has honored Berkeley\'s Sociology Department with its Award for Excellence in Diversity Research for sustained contributions to understanding racial inequality.', + 'The award recognizes research by Berkeley sociologists on residential segregation, incarceration rates, immigrant integration, and educational opportunity gaps spanning more than three decades.', + 'sociology,diversity,research,award,inequality'), + ('Berkeley Students Create Hackable Open-Source Prosthetic', 'Student', + datetime(2024, 8, 12), False, + 'A Berkeley bioengineering team has released an open-source, 3D-printable prosthetic hand that can be fabricated for under $200 and is fully programmable.', + 'The OpenLimb platform uses machine learning to interpret muscle signals and translate them into hand movements. The hardware files and software are freely available on GitHub and have been downloaded 45,000 times.', + 'bioengineering,prosthetics,open source,student,innovation'), + ('Berkeley Researchers Track 100 Years of Bay Area Climate Change', 'Research', + datetime(2024, 9, 3), False, + 'A century-long dataset assembled by Berkeley researchers reveals that average temperatures in the Bay Area have risen 2.1 degrees Fahrenheit since 1924, with accelerating warming since 1980.', + 'The analysis combines weather station records, tree ring data, and historical newspaper accounts to reconstruct Bay Area climate history. The findings have implications for wildfire risk, water supply, and biodiversity.', + 'climate change,Bay Area,research,temperature,history'), + ('Berkeley Lab and Cal Partnership Produces Record 50 Joint Discoveries', 'Research', + datetime(2024, 10, 1), False, + 'The Berkeley-Lawrence Berkeley National Lab collaboration produced 50 peer-reviewed scientific discoveries in fiscal year 2024, a record for the partnership.', + 'Joint discoveries spanned particle physics, battery technology, materials science, computational biology, and environmental chemistry. Forty-three Berkeley faculty members are affiliated with the lab.', + 'research,Lawrence Berkeley,partnership,discovery,science'), + ('Berkeley Launches New Program in Computational Biology', 'Campus Life', + datetime(2024, 10, 25), False, + 'Berkeley has launched a new undergraduate minor in computational biology, combining coursework in bioinformatics, statistics, machine learning, and molecular biology.', + 'The minor draws on faculty from EECS, MCB, Integrative Biology, and Public Health. It prepares students for research in genomics, systems biology, and precision medicine.', + 'computational biology,minor,EECS,curriculum,bioinformatics'), + ('Berkeley Hosts National Conference on Criminal Justice Reform', 'Research', + datetime(2024, 11, 8), False, + 'The Berkeley School of Law and Goldman School of Public Policy co-hosted a two-day national conference on evidence-based criminal justice reform, attended by 500 scholars and advocates.', + 'Conference panels addressed pretrial detention, sentencing disparities, reentry programs, police reform, and the use of algorithms in criminal justice decision-making.', + 'law,criminal justice,policy,conference,reform'), + ('Berkeley Water Center Secures Major Grant for Drought Research', 'Research', + datetime(2024, 12, 10), False, + 'The Berkeley Water Center has received a $15 million grant from the State of California to develop decision-support tools for drought management in the San Joaquin Valley.', + 'Researchers will model crop water demand, aquifer depletion, and economic impacts of water allocation decisions to help state regulators and farmers navigate increasingly severe droughts.', + 'water,drought,research,California,agriculture'), + ('Berkeley Students Publish Op-Eds in Major National Newspapers', 'Student', + datetime(2025, 1, 5), False, + 'Berkeley undergraduate students in the journalism and political science programs have published op-eds in the New York Times, Washington Post, and San Francisco Chronicle this academic year.', + 'Twelve Berkeley students published professional opinion pieces on topics including climate policy, housing affordability, AI regulation, and immigration reform — many in collaboration with Berkeley faculty.', + 'student,journalism,writing,op-ed,publication'), + ('Berkeley Joins International Campaign to Protect Biodiversity', 'Research', + datetime(2025, 1, 28), False, + 'UC Berkeley has joined a coalition of 100 universities pledging to contribute research, data, and student engagement to help achieve the 30x30 global biodiversity conservation target.', + 'Berkeley will contribute expertise in conservation ecology, sustainable agriculture, freshwater science, and biodiversity informatics. The pledge includes a commitment to protect 30% of university-owned lands.', + 'biodiversity,conservation,research,international,ecology'), + ('Berkeley Researchers Map Underwater Landslide Risk Along California Coast', 'Science', + datetime(2025, 2, 5), False, + 'A Berkeley team using submarine sonar mapping has identified 47 previously uncharted submarine landslide zones along the California coast that could generate tsunamis.', + 'The hazard assessment combines high-resolution bathymetric mapping with sediment core analysis and seismic records. Results are being integrated into California\'s coastal hazard plans.', + 'geology,tsunami,California,science,marine'), + ('Berkeley Graduate Students Launch Peer Tutoring App Serving 5000 Students', 'Student', + datetime(2025, 3, 8), False, + 'A tutoring platform created by Berkeley CS graduate students has grown to serve 5,000 students across the UC system, providing free peer tutoring in STEM subjects.', + 'The app matches students needing help with advanced undergraduates and graduate students who earn course credit for tutoring. Pass rates in introductory calculus and chemistry have improved 15% at campuses using the platform.', + 'student,tutoring,education,technology,STEM'), + ('Berkeley Receives Largest Federal Grant in School of Education History', 'Faculty', + datetime(2025, 3, 22), False, + 'The US Department of Education has awarded the Graduate School of Education a $40 million grant to research effective interventions for chronically absent students.', + 'The five-year project will track 50,000 students across California districts to identify the causes of chronic absenteeism and evaluate evidence-based attendance interventions from texts and calls to mentorship programs.', + 'education,research,federal,grant,attendance'), + ('Berkeley Biologists Use AI to Catalog 10,000 New Insect Species', 'Science', + datetime(2024, 1, 12), False, + 'A Berkeley entomology team has used computer vision and machine learning to catalog 10,000 previously unidentified insect specimens in the California Academy of Sciences collection.', + 'The AI pipeline analyzes microscope images of insect morphology to identify species boundaries, dramatically speeding up the centuries-old task of species description. The method has been shared openly for use by natural history museums worldwide.', + 'entomology,AI,science,biodiversity,species'), + ('Berkeley Physics Department Celebrates 100 Years of Nobel Prizes', 'Faculty', + datetime(2024, 2, 14), False, + 'The Berkeley Physics Department celebrated the centennial of its first Nobel Prize with a symposium bringing together Nobel Laureates, alumni, and current students.', + 'The department has produced 35 Nobel Laureates over 100 years, more than any other physics department in the world. The symposium featured talks by six current faculty Nobel Laureates.', + 'physics,Nobel,centennial,faculty,history'), + ('Berkeley Music Department Launches Archive of Bay Area Jazz History', 'Arts', + datetime(2023, 11, 30), False, + 'The Berkeley Music Department has digitized and made freely available 5,000 recordings of Bay Area jazz performances from 1940 to 1990.', + 'The collection includes performances by Chet Baker, Dave Brubeck, Thelonious Monk, and dozens of local Bay Area artists. The archive includes interview transcripts, photographs, and venue records.', + 'music,jazz,archive,Bay Area,arts'), + ('Berkeley Theater Program Wins Pulitzer for Student-Written Play', 'Arts', + datetime(2024, 4, 15), False, + 'A play written by Berkeley MFA theater student Maya Okonkwo has won the Pulitzer Prize for Drama, the first student-written play to win the award.', + 'The play, "What the River Remembers," explores three generations of a Nigerian American family navigating identity, belonging, and grief. It was developed in Berkeley\'s MFA playwriting workshop and performed at Berkeley Repertory Theatre.', + 'theater,Pulitzer,arts,student,MFA'), + ('Berkeley Announces New Merit Scholarship for First-Generation Students', 'Campus Life', + datetime(2024, 3, 12), False, + 'UC Berkeley has launched the Berkeley Excellence Award, a new merit scholarship that provides $20,000 per year to 100 first-generation college students from California.', + 'The scholarship is funded by a $100 million endowment gift and prioritizes students who demonstrate both academic excellence and economic need. Recipients receive academic mentorship and research opportunities.', + 'scholarship,first-generation,student,financial aid,excellence'), + ('Berkeley Engineering Grads Win 40% of Silicon Valley Engineering Manager Jobs', 'Research', + datetime(2023, 9, 5), False, + 'A Stanford Research Institute analysis finds that 40% of engineering manager positions at Silicon Valley\'s 50 largest companies are held by UC Berkeley alumni.', + 'The analysis tracked 25,000 engineering leaders and found Berkeley graduates dominate at Google, Apple, Meta, Netflix, and dozens of Bay Area technology companies, particularly in chip design, AI, and infrastructure.', + 'alumni,engineering,Silicon Valley,career,research'), + ('Berkeley Designs Earthquake-Resistant Housing for Developing World', 'Research', + datetime(2023, 10, 15), False, + 'A Berkeley engineering and architecture team has developed low-cost earthquake-resistant building techniques suitable for rural communities in seismically active developing nations.', + 'The techniques use locally available bamboo, rammed earth, and recycled materials reinforced with steel wire mesh to create structures that can withstand magnitude 7 earthquakes. Pilot projects are underway in Nepal and Peru.', + 'earthquake engineering,housing,developing world,research,architecture'), + ] + + # More bulk news to reach 120+ total + bulk_news += [ + ('Berkeley Study: Remote Work Increases Carbon Footprint for Many Workers', 'Research', + datetime(2023, 2, 14), False, + 'A Berkeley study challenges the assumption that remote work always reduces carbon emissions, finding that long-distance commuters who moved farther from the office produce more emissions overall.', + 'The study tracked 2,500 Bay Area workers over two years and found that 40% of remote workers moved more than 30 miles from the office, significantly increasing car travel and home energy use compared to their pre-pandemic patterns.', + 'remote work,carbon,emissions,research,environment'), + ('Berkeley Sociologists Find Social Media Algorithms Amplify Outrage', 'Research', + datetime(2023, 3, 22), False, + 'A Berkeley study of 10 million social media posts finds that content algorithms systematically amplify outraged and morally charged content, contributing to political polarization.', + 'The research analyzed content reach on three major platforms and found that high-outrage posts received 3.5x more algorithmic promotion than neutral equivalents, regardless of accuracy. The paper recommends regulatory interventions on algorithmic amplification.', + 'social media,algorithms,research,politics,polarization'), + ('Berkeley Art Museum Acquires Largest Collection of Bay Area Photography', 'Arts', + datetime(2023, 4, 8), False, + 'BAMPFA has acquired a 12,000-photograph archive documenting Bay Area life from 1960 to 2000, including works by Dorothea Lange, Imogen Cunningham, and dozens of lesser-known community photographers.', + 'The collection, donated by a private collector, spans the civil rights movement, the rise of the counterculture, the AIDS crisis, and the early years of the tech industry. It will form the core of a new center for Bay Area photography.', + 'art,photography,BAMPFA,Bay Area,archive'), + ('Berkeley Research Sheds Light on Why Vaccines Work Better for Some People', 'Science', + datetime(2023, 5, 30), False, + 'A large Berkeley immunology study has identified genetic and microbiome factors that explain why some people mount stronger immune responses to vaccines than others.', + 'The three-year study enrolled 3,000 participants and found that gut microbiome composition at the time of vaccination accounts for up to 30% of the variation in antibody response. The findings could enable targeted prebiotics to improve vaccine efficacy.', + 'vaccines,immunology,microbiome,science,health'), + ('Berkeley Researchers Create World\'s Smallest Radio Transmitter', 'Science', + datetime(2023, 6, 18), False, + 'Berkeley engineers have built a radio transmitter the size of a grain of sand that can be implanted in living tissue, enabling a new class of wireless biomedical implants.', + 'The device uses piezoelectric ultrasound for power transfer and radiofrequency for data transmission, consuming only 6 microwatts. The technology could enable implantable sensors for continuous monitoring of glucose, oxygen, and neural signals.', + 'engineering,biomedical,implant,wireless,science'), + ('Berkeley Hosts First AI and Art Exhibition', 'Arts', + datetime(2023, 7, 25), False, + 'BAMPFA hosted "Emergent Minds," the first major museum exhibition at Berkeley dedicated to AI-generated and AI-collaborative art, attracting 20,000 visitors.', + 'The exhibition featured works by 30 artists who used generative AI tools, diffusion models, and neural networks as creative collaborators. Accompanying programming included panels on machine creativity, authorship, and the future of visual art.', + 'AI,art,exhibition,BAMPFA,technology'), + ('Berkeley Engineers Create Biodegradable Electronics for Medical Use', 'Research', + datetime(2023, 8, 28), False, + 'Berkeley bioengineers have developed electronics that dissolve harmlessly in the body after completing their function, enabling temporary implants that don\'t require surgical removal.', + 'The devices are made from magnesium and silk fibroin, materials that hydrolyze at predictable rates. Prototype pressure sensors and drug delivery systems have been demonstrated in animal models.', + 'engineering,biomedical,biodegradable,research,electronics'), + ('Berkeley Wins National Collegiate Debate Championship', 'Student', + datetime(2023, 9, 28), False, + 'The UC Berkeley Debate Team has won the National Debate Tournament for the third time in five years, defeating Harvard in the final round on a resolution about AI governance.', + 'The winning team argued that autonomous AI systems causing harm should face strict liability under existing product liability law. The Berkeley Debate team is coached by three alumni of the national championship team.', + 'debate,student,competition,championship,AI'), + ('Berkeley Researchers Find Link Between Light Exposure and Depression', 'Science', + datetime(2023, 10, 31), False, + 'A Berkeley study tracking 85,000 participants for two years finds that exposure to bright light at night significantly increases the risk of depression, anxiety, and mood disorders.', + 'Participants using light-blocking glasses or apps that reduce blue light after sunset showed 30% lower depression rates. The findings support recommendations to limit artificial light exposure in the hours before sleep.', + 'light,depression,health,science,mental health'), + ('Berkeley Law Clinic Secures Housing for 500 Families Facing Eviction', 'Faculty', + datetime(2023, 11, 14), False, + 'The Berkeley Law Housing Clinic has negotiated settlements keeping 500 low-income Bay Area families in their homes this year, a record for the program.', + 'Second- and third-year law students, supervised by clinical faculty, represent tenants in eviction proceedings across Alameda and Contra Costa counties. The clinic also provides training in tenant rights to community organizations.', + 'law,housing,clinic,faculty,equity'), + ('Berkeley Receives NSF Grant to Improve Broadband Access in Rural California', 'Research', + datetime(2023, 12, 18), False, + 'A Berkeley Engineering team has received $8 million from the National Science Foundation to develop low-cost wireless broadband systems for rural and tribal communities in California.', + 'The project combines TV white space spectrum, satellite backhaul, and mesh networking to deliver affordable internet to communities underserved by commercial providers. Field trials will begin in Humboldt County.', + 'broadband,rural,research,engineering,NSF'), + ('Berkeley Astronomer Detects Oldest Known Galaxy', 'Science', + datetime(2024, 1, 30), True, + 'A UC Berkeley astronomer using the James Webb Space Telescope has identified a galaxy dating to just 290 million years after the Big Bang, the oldest galaxy ever observed.', + 'The galaxy, designated JADES-GS+53.16+53.19+54.21, was detected in deep field imaging and confirmed spectroscopically. Its age pushes back the formation of large galaxies, challenging current cosmological models.', + 'astronomy,galaxy,JWST,science,cosmology'), + ('Berkeley Students Create App to Reduce Food Waste in Dining Halls', 'Student', + datetime(2024, 2, 22), False, + 'Berkeley undergraduates have developed an app that uses real-time dining hall inventory data to alert users about surplus food, reducing pre-consumer waste by 35% in pilot testing.', + 'The app connects with Berkeley Dining\'s inventory management system to display which items have excess supply and will be discarded at the end of service. Users receive notifications and can pick up surplus items at reduced or no cost.', + 'student,food waste,sustainability,dining,app'), + ('Berkeley Publishes Annual Report on Bay Area Housing Affordability', 'Research', + datetime(2024, 3, 28), False, + 'The annual Terner Center Housing Report from Berkeley finds that median rent in San Francisco has reached $3,800 per month, 40% above pre-pandemic levels.', + 'The report documents how restrictive zoning, long approval timelines, and high construction costs are creating a structural housing shortage in the Bay Area. Policy recommendations include zoning reform, infrastructure investment, and streamlined permitting.', + 'housing,rent,Bay Area,policy,research'), + ('Berkeley Chemistry Develops Room-Temperature Carbon Capture', 'Research', + datetime(2024, 4, 18), True, + 'Berkeley chemists have developed a new material that captures CO2 from ambient air at room temperature with record efficiency, potentially enabling affordable direct air capture.', + 'The material, a new class of porous organic polymer, binds CO2 from dilute atmospheric concentrations with 10x better selectivity than current sorbents. It can be regenerated using mild heating, dramatically reducing energy costs.', + 'climate,CO2,carbon capture,chemistry,research'), + ('Berkeley Graduates Fill Record Number of City Government Positions', 'Research', + datetime(2024, 5, 30), False, + 'A new survey finds that UC Berkeley alumni hold elected or appointed positions in 14 California city governments, the highest number in the university\'s history.', + 'Berkeley alumni serve as mayors, city council members, city attorneys, and planning commissioners across cities including San Francisco, Oakland, Berkeley, San Jose, and Sacramento.', + 'alumni,government,policy,California,leadership'), + ('Berkeley Athletics Opens New Olympic Sports Complex', 'Athletics', + datetime(2024, 6, 28), False, + 'UC Berkeley opened its new $80 million Olympic Sports Complex, providing state-of-the-art facilities for track and field, tennis, swimming, and weight training.', + 'The complex includes an eight-lane outdoor track, 10 outdoor tennis courts, a 50-meter competition pool, and a 20,000-square-foot weight training facility. It will host the 2027 Pac-12 Championships.', + 'athletics,facilities,sports,Cal Bears,complex'), + ('Berkeley Researchers Develop AI to Predict Wildfires Days in Advance', 'Research', + datetime(2024, 7, 15), True, + 'A Berkeley team has developed an AI system that can predict wildfire ignition and spread up to 72 hours in advance with 85% accuracy, using satellite data, weather models, and vegetation maps.', + 'The system integrates remote sensing data, lightning strike records, fuel moisture measurements, and weather forecasts using a recurrent neural network architecture. CalFire has agreed to pilot the system in Sonoma and Napa counties.', + 'wildfire,AI,prediction,research,California'), + ('Berkeley School of Public Health Releases COVID Long-Haul Study', 'Research', + datetime(2024, 8, 2), False, + 'A two-year Berkeley study of 12,000 COVID survivors finds that 22% experience symptoms of Long COVID for more than six months, with fatigue, brain fog, and shortness of breath most common.', + 'The study is one of the largest prospective analyses of Long COVID in the US and identifies risk factors including initial illness severity, pre-existing autoimmune conditions, and inadequate rest during acute infection.', + 'COVID,long COVID,public health,research,epidemiology'), + ('Berkeley Chemistry Team Synthesizes New Cancer Drug Candidate', 'Research', + datetime(2024, 9, 20), False, + 'Berkeley chemists have synthesized a new class of molecules that selectively kills cancer cells by exploiting a metabolic vulnerability present in many solid tumors.', + 'The compounds, called ferroptosis inducers, trigger iron-dependent cell death specifically in cancer cells with high oxidative stress. The lead compound shows efficacy in mouse models of pancreatic and ovarian cancer.', + 'chemistry,cancer,drug discovery,research,medicine'), + ] + + for (title, category, pub_date, featured, summary, content, tags) in bulk_news: + slug_base = slugify(title) + slug = slug_base + counter = 1 + while NewsArticle.query.filter_by(slug=slug).first(): + slug = f"{slug_base}-{counter}" + counter += 1 + article = NewsArticle( + title=title, slug=slug, category=category, + author='Berkeley News Staff', + published_date=pub_date, + content=content, summary=summary, tags=tags, + view_count=150, + featured=featured + ) + db.session.add(article) + + db.session.flush() + + # ── Additional Events Bulk ──────────────────────────────────────────────── + bulk_events = [ + ('Cal Women\'s Volleyball: Home Match vs. UCLA', 'Sports', + now + timedelta(days=2), now + timedelta(days=2, hours=2), + 'Haas Pavilion', 'Haas Pavilion', + 'Cal Athletics', False, '$12', + 'Cal Bears women\'s volleyball hosts the UCLA Bruins in a marquee Pac-12 matchup. Student tickets free with Cal ID.'), + ('Health Sciences Information Fair', 'Career', + now + timedelta(days=3), now + timedelta(days=3, hours=3), + '50 Warren Hall', 'Warren Hall', + 'School of Public Health', False, 'Free', + 'Graduate programs in public health, epidemiology, biostatistics, and environmental health present at an information fair open to undergraduates.'), + ('Berkeley Science Lecture: Origins of Life', 'Lecture', + now + timedelta(days=7), now + timedelta(days=7, hours=1, minutes=30), + '1 Pimentel Hall', 'Pimentel Hall', + 'Interdisciplinary Studies Field Group', False, 'Free', + 'A world-leading origin-of-life researcher discusses current theories and experiments on how chemistry becomes biology.'), + ('Piano Recital: Graduate Showcase', 'Arts', + now + timedelta(days=8), now + timedelta(days=8, hours=2), + 'Hertz Hall', 'Hertz Hall', + 'Department of Music', False, '$10', + 'MFA piano students perform works by Bach, Beethoven, Schubert, and contemporary composers in the Spring Graduate Showcase.'), + ('Berkeley Mental Health Awareness Week Kickoff', 'Health', + now + timedelta(days=10), now + timedelta(days=10, hours=1), + 'Sproul Plaza', 'Sproul Plaza', + 'University Health Services', False, 'Free', + 'Kick off Mental Health Awareness Week with resource tables, peer counselors, and free wellness kits. Learn about Berkeley\'s mental health resources and how to support friends.'), + ('EECS Research Poster Session', 'Lecture', + now + timedelta(days=12), now + timedelta(days=12, hours=3), + '430 Soda Hall', 'Soda Hall', + 'EECS Department', False, 'Free', + 'Graduate students and postdocs in EECS present research posters covering AI, systems, hardware, theory, and more. Industry mentors provide feedback and recruiters scout talent.'), + ('Berkeley Symphony: Spring Concert', 'Arts', + now + timedelta(days=20), now + timedelta(days=20, hours=2), + 'Zellerbach Hall', 'Zellerbach Hall', + 'Department of Music', False, '$25', + 'The Berkeley Symphony performs Mahler\'s Symphony No. 5 and a world premiere by a Berkeley composition faculty member.'), + ('Law School Admitted Students Day', 'Career', + now + timedelta(days=24), now + timedelta(days=24, hours=5), + '295 Simon Hall', 'Simon Hall', + 'Berkeley Law Admissions', False, 'Free', + 'Admitted law students visit campus for class visits, faculty meetings, tour of the law library, student panel, and information about bar preparation and career services.'), + ('Public Health Conference on Climate and Health', 'Health', + now + timedelta(days=29), now + timedelta(days=29, hours=6), + 'Li Ka Shing Center', 'Li Ka Shing Center', + 'School of Public Health', True, 'Free', + 'A one-day conference on the health effects of climate change, featuring presentations on heat wave mortality, wildfire smoke and respiratory disease, water-borne illness, and vector-borne diseases in California.'), + ('Cal Day Science Expo', 'Social', + now + timedelta(days=56), now + timedelta(days=56, hours=5), + 'Life Sciences Building', 'Life Sciences Building', + 'Office of Research', False, 'Free', + 'Berkeley research labs open their doors for Cal Day demonstrations, including chemistry magic shows, robotics demonstrations, biology microscopy, and physics experiments for visitors of all ages.'), + ('Berkeley-Stanford Debate on AI Regulation', 'Lecture', + now + timedelta(days=33), now + timedelta(days=33, hours=2), + '100 Boalt Hall', 'Simon Hall', + 'Berkeley Law and Stanford Law', False, 'Free', + 'Law faculty from Berkeley and Stanford debate competing approaches to AI regulation, from liability frameworks to pre-market approval requirements and international coordination.'), + ('Global Health Case Competition', 'Career', + now + timedelta(days=38), now + timedelta(days=38, hours=4), + '50 Warren Hall', 'Warren Hall', + 'School of Public Health', False, 'Free', + 'Teams of graduate students in public health, policy, and business compete to develop evidence-based solutions to global health challenges. $5,000 in prizes.'), + ('Berkeley Haas Case Competition Finals', 'Career', + now + timedelta(days=43), now + timedelta(days=43, hours=3), + 'Chou Hall, Haas School', 'Haas School', + 'Haas School of Business', False, 'Free', + 'Business school teams from around the world compete in the Berkeley Haas Case Competition, presenting solutions to sustainability and innovation challenges to a panel of executives.'), + ('Virtual Info Session: Berkeley Data Science Master\'s Program', 'Virtual', + now + timedelta(days=14), now + timedelta(days=14, hours=1), + 'Online (Zoom)', 'Virtual', + 'School of Information', True, 'Free', + 'Learn about the Master in Data Science (MIDS) program at Berkeley\'s I School. Hear from current students and faculty, and ask questions about the application process.'), + ('Berkeley Social Sciences Research Conference', 'Lecture', + now + timedelta(days=46), now + timedelta(days=46, hours=5), + '315 Barrows Hall', 'Barrows Hall', + 'Division of Social Sciences', False, 'Free', + 'Annual interdisciplinary research conference bringing together graduate students from economics, sociology, political science, and psychology to present original research.'), + ('Celebration of Excellence: Faculty Awards Ceremony', 'Social', + now + timedelta(days=36), now + timedelta(days=36, hours=2), + 'Faculty Club', 'Faculty Club', + 'Office of the Chancellor', False, 'Free', + 'Annual ceremony recognizing Berkeley faculty who have received major awards, fellowships, and honors over the past academic year.'), + ] + + for (title, category, start_dt, end_dt, location, building, + organizer, reg_req, cost, desc) in bulk_events: + e = Event( + title=title, category=category, + start_datetime=start_dt, end_datetime=end_dt, + location=location, building=building, + organizer=organizer, registration_required=reg_req, + cost=cost, description=desc + ) + db.session.add(e) + + db.session.flush() + + # ── Users ───────────────────────────────────────────────────────────────── + benchmark_users = [ + ('alice', 'alice@berkeley.edu', 'test1234', 'Alice Chen', 'student'), + ('bob', 'bob@berkeley.edu', 'test1234', 'Bob Martinez', 'student'), + ('carol', 'carol@berkeley.edu', 'test1234', 'Carol Johnson', 'faculty'), + ('dave', 'dave@berkeley.edu', 'test1234', 'Dave Williams', 'student'), + ] + + for (username, email, password, full_name, role) in benchmark_users: + if not User.query.filter_by(email=email).first(): + u = User( + email=email, + username=username, + full_name=full_name, + role=role, + ) + u.set_password(password) + db.session.add(u) + + db.session.commit() + + +if __name__ == '__main__': + from app import app + with app.app_context(): + seed() + print("Seeding complete.") diff --git a/sites/berkeley/static/css/.gitkeep b/sites/berkeley/static/css/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/sites/berkeley/static/js/.gitkeep b/sites/berkeley/static/js/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/sites/berkeley/tasks.jsonl b/sites/berkeley/tasks.jsonl new file mode 100644 index 000000000..60e21d210 --- /dev/null +++ b/sites/berkeley/tasks.jsonl @@ -0,0 +1,30 @@ +{"web_name": "UC Berkeley", "id": "UC Berkeley--0", "ques": "Find all PhD programs offered at UC Berkeley and count how many there are. List their names.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--1", "ques": "Search for MBA programs at UC Berkeley. Which school offers the MBA and what is the program duration?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--2", "ques": "Find the Computer Science BS program at UC Berkeley. What are the program requirements listed on the program detail page?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--3", "ques": "Browse the news section on the UC Berkeley site and find all articles in the 'Research' category. How many research articles are listed on the first page?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--4", "ques": "Find news articles about CRISPR or gene editing on the Berkeley site. Who is the featured scientist and what award did they receive?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--5", "ques": "Look at the upcoming events at UC Berkeley. Find an event in the 'Career' category and note the date, location, and whether registration is required.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--6", "ques": "Find events categorized as 'Lecture' at UC Berkeley. List at least three lecture events with their dates and locations.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--7", "ques": "Browse the faculty directory at UC Berkeley and find a professor in the EECS department who works on artificial intelligence. What are their specific research interests?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--8", "ques": "Find the faculty profile for Jennifer Doudna at UC Berkeley. What is her title and what is her primary research area?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--9", "ques": "Search for research centers related to 'artificial intelligence' on the Berkeley website. List the names of any AI-related research centers you find.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--10", "ques": "Find the Berkeley Artificial Intelligence Research Lab (BAIR) on the UC Berkeley site. Who is the director and what year was it founded?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--11", "ques": "Go to the Admissions page at UC Berkeley. What is the application deadline for freshman applicants and what is the current acceptance rate?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--12", "ques": "Find all programs offered by the Haas School of Business at UC Berkeley. What degree types are available (e.g., MBA, PhD, etc.)?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--13", "ques": "Browse the Departments page at UC Berkeley and find the Department of Electrical Engineering and Computer Sciences (EECS). Who is the department chair and where is the department located?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--14", "ques": "Find the College of Engineering at UC Berkeley. How many undergraduate students and graduate students are enrolled? Who is the dean?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--15", "ques": "Search for 'climate' on the UC Berkeley website. What types of results appear (programs, news, events, faculty, research)? Name at least one result from each category that appears.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--16", "ques": "Find all online degree programs at UC Berkeley. Which programs offer an online option and what school do they belong to?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--17", "ques": "Look at the About Berkeley page. How many Nobel Laureates are currently on faculty, how many varsity sports does Berkeley have, and how many NCAA national titles has Berkeley won?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--18", "ques": "Find upcoming 'Arts' events at UC Berkeley within the next two months. List the events with their dates, venues, and ticket prices.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--19", "ques": "Navigate to the Berkeley news section and filter by the 'Athletics' category. Find a news article about a Berkeley sports championship and summarize what sport and what the achievement was.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--20", "ques": "Find the JD (Juris Doctor) program at Berkeley Law. What is the program duration, application deadline, and which school offers it?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--21", "ques": "Search for faculty who work on 'quantum computing' at UC Berkeley. List all faculty members who appear in the results and their department affiliations.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--22", "ques": "Go to the College of Letters and Science at UC Berkeley and find all the departments listed under it on the Departments page. How many departments belong to the College of Letters and Science?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--23", "ques": "Find the research center 'Berkeley Institute for Data Science' (BIDS). What are its focus areas and who is the director? Then find if there are any related research centers listed on the same page.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--24", "ques": "Find the Economics PhD program at Berkeley. Then navigate to the Economics department page and identify the department chair and the other programs offered by the department. Finally, find one Economics faculty member and note their research interests.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--25", "ques": "Berkeley holds an annual Spring Career Fair. Find this event, note the date, location, and whether registration is required. Then find two other career-related events and compare their details.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--26", "ques": "Find Berkeley's Nobel Laureate professors. Navigate to the faculty profiles of at least two Nobel Laureates. What prizes did they win and what are their research interests?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--27", "ques": "Search the Berkeley site for 'Master of Engineering'. Find the MEng program, identify which department offers it, and compare it to other master's programs in the same college. How does the duration differ?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--28", "ques": "Find all programs that require GRE scores at UC Berkeley. Navigate to the programs page and identify which programs have 'GRE Required' indicated. What degree types most commonly require GRE?", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--29", "ques": "Go to the Berkeley homepage and identify the key statistics listed: how many students attend Berkeley (undergrad and grad separately), how many degree programs are offered, and what is Berkeley's ranking as a public research university? Then navigate to the About page and confirm these numbers.", "web": "http://localhost:40015/", "upstream_url": "https://www.berkeley.edu/"} diff --git a/sites/berkeley/templates/404.html b/sites/berkeley/templates/404.html new file mode 100644 index 000000000..7840266ad --- /dev/null +++ b/sites/berkeley/templates/404.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block title %}Page Not Found — UC Berkeley{% endblock %} + +{% block content %} +
+
404
+

Page Not Found

+

The page you're looking for doesn't exist or has been moved.

+ +
+{% endblock %} diff --git a/sites/berkeley/templates/500.html b/sites/berkeley/templates/500.html new file mode 100644 index 000000000..9034e7488 --- /dev/null +++ b/sites/berkeley/templates/500.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block title %}Server Error — UC Berkeley{% endblock %} + +{% block content %} +
+
500
+

Something Went Wrong

+

An unexpected error occurred. Please try again later.

+ +
+{% endblock %} diff --git a/sites/berkeley/templates/about.html b/sites/berkeley/templates/about.html new file mode 100644 index 000000000..b4404b044 --- /dev/null +++ b/sites/berkeley/templates/about.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} +{% block title %}About UC Berkeley{% endblock %} + +{% block content %} + + +
+ +
+

"Fiat Lux — Let There Be Light"

+

The University of California, Berkeley is a public research university dedicated to the discovery, communication, and application of knowledge that serves the long-term interests of society. As a leading research university, Berkeley is defined by intellectual rigor, interdisciplinary collaboration, and a deep commitment to the public good.

+
+ + +

Berkeley by the Numbers

+
+
{{ stats.founded }}Year Founded
+
{{ stats.nobel_laureates }}Nobel Laureates on Faculty
+
{{ "{:,}".format(stats.undergrad_count) }}Undergraduate Students
+
{{ "{:,}".format(stats.grad_count) }}Graduate Students
+
{{ "{:,}".format(stats.faculty_count) }}Faculty Members
+
{{ stats.degree_programs }}+Degree Programs
+
{{ stats.varsity_sports }}Varsity Sports
+
{{ stats.national_titles }}NCAA National Titles
+
{{ stats.top_10_programs }}+Top-10 Graduate Programs
+
{{ stats.acres }}Campus Acres
+
{{ stats.libraries }}Library Locations
+
{{ "{:,}".format(stats.alumni) }}+Alumni Worldwide
+
+ + +
+
+

History

+

The University of California was founded on March 23, 1868, with the merger of the private College of California and the public Agricultural, Mining, and Mechanical Arts College. Berkeley's first class of 40 students was admitted in 1869.

+

From its earliest days, Berkeley has been committed to serving the people of California and the world through education, research, and public service. The university helped power California's growth through two world wars, the tech boom, and beyond.

+

Today, Berkeley is recognized as one of the world's leading universities, ranking first among public universities in U.S. News & World Report and consistently appearing among the top universities in global rankings.

+
+
+

Location

+

Berkeley's main campus is located in the city of Berkeley, California, on the eastern shore of San Francisco Bay, with views of the Golden Gate Bridge and the Pacific Ocean.

+

The 1,232-acre campus is a National Historic Landmark and one of the most architecturally distinguished universities in the United States.

+
+ Address:
+ University of California, Berkeley
+ Berkeley, CA 94720
+ United States +
+
+
+ + +
+

Nobel Laureates

+

UC Berkeley faculty, researchers, and alumni have won more than 107 Nobel Prizes. Current faculty Nobel Laureates include pioneers in physics, chemistry, economics, and the biological sciences.

+
+
+
Physics
+
Saul Perlmutter, Reinhard Genzel
+
+
+
Chemistry
+
Jennifer Doudna
+
+
+
Economics
+
George Akerlof (Emeritus)
+
+
+
+ + +

Explore Berkeley

+ +
+{% endblock %} diff --git a/sites/berkeley/templates/academics.html b/sites/berkeley/templates/academics.html new file mode 100644 index 000000000..80e743714 --- /dev/null +++ b/sites/berkeley/templates/academics.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% block title %}Academics — UC Berkeley{% endblock %} + +{% block content %} + + +
+ +
+
{{ colleges|length }}Schools & Colleges
+
{{ total_programs }}+Degree Programs
+
115+Undergraduate Majors
+
{{ total_depts }}+Academic Departments
+
+ +
+

A World-Class Education

+

UC Berkeley offers an unparalleled range of academic programs across its 14 schools and colleges. From the humanities to cutting-edge engineering, from law to optometry, a Berkeley education combines rigorous scholarship with a commitment to the public good.

+ +
+ +

Schools & Colleges

+ +
+ {% for college in colleges %} +
+
+

+ {{ college.name }} +

+

{{ college.description[:180] }}{% if college.description|length > 180 %}...{% endif %}

+
+ {% if college.dean %}
Dean: {{ college.dean }}
{% endif %} +
Founded: {{ college.founded_year }}
+
+ {% if college.undergrad_count > 0 %}{{ "{:,}".format(college.undergrad_count) }} undergrads{% endif %} + {% if college.grad_count > 0 %}{% if college.undergrad_count > 0 %} · {% endif %}{{ "{:,}".format(college.grad_count) }} grad students{% endif %} +
+
+ +
+
+ {% endfor %} +
+ + +
+

Explore Academic Life

+ +
+
+{% endblock %} diff --git a/sites/berkeley/templates/account.html b/sites/berkeley/templates/account.html new file mode 100644 index 000000000..3ac4a9b11 --- /dev/null +++ b/sites/berkeley/templates/account.html @@ -0,0 +1,81 @@ +{% extends "base.html" %} +{% block title %}My Account — UC Berkeley{% endblock %} + +{% block content %} + + +
+
+ +
+ +
+ + +
+

Saved Bookmarks

+ {% if bookmark_details %} +
+ {% for detail in bookmark_details %} +
+
+
+ {{ detail.bookmark.item_type }} + {{ detail.title }} +
+ {% if detail.bookmark.note %} +

{{ detail.bookmark.note }}

+ {% endif %} +

Saved {{ detail.bookmark.created_at.strftime('%B %d, %Y') }}

+
+
+ + + +
+
+ {% endfor %} +
+ {% else %} +
+

No bookmarks yet

+

Save programs, faculty profiles, events, and research centers by clicking the "Save" button on any page.

+ +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/sites/berkeley/templates/admissions.html b/sites/berkeley/templates/admissions.html new file mode 100644 index 000000000..7b4c0de68 --- /dev/null +++ b/sites/berkeley/templates/admissions.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} +{% block title %}Admissions — UC Berkeley{% endblock %} + +{% block content %} + + +
+ + + + +
+

Undergraduate Admissions

+
+
+

UC Berkeley welcomes applications from students across the United States and around the world. We are committed to enrolling a diverse, talented class of undergraduate students who will thrive in Berkeley's academically rigorous and intellectually vibrant community.

+

Berkeley admits freshmen on the basis of academic achievement, the strength and breadth of coursework, personal qualities, extracurricular activities, and demonstrated commitment to the Berkeley community.

+

Application Deadlines

+ + + + + +
ProgramDeadline
Freshman ApplicationNovember 30
Transfer ApplicationNovember 30
International FreshmenNovember 30
+
+
+
+ {{ undergrad_programs }} + Undergraduate Programs +
+
+ 31,800 + Undergraduates Enrolled +
+
+ 14.4% + Acceptance Rate +
+
+
+ +

Freshman Profile (Class of 2027)

+
+
+
3.91
+
Median GPA
+
+
+
1510
+
Median SAT
+
+
+
34
+
Median ACT
+
+
+
40%
+
First-Generation Students
+
+
+ +

Required Materials

+
    +
  • UC Application (submitted at apply.universityofcalifornia.edu)
  • +
  • Official high school transcripts
  • +
  • Personal Insight Questions (4 out of 8)
  • +
  • SAT or ACT scores (test-optional for 2026 cycle)
  • +
  • School report and letters of recommendation
  • +
+
+ + +
+

Graduate Admissions

+
+
+

UC Berkeley's Graduate Division coordinates graduate admissions across {{ grad_programs }} graduate programs offered by 14 professional schools and the College of Letters and Science. Each department or program administers its own admissions process.

+

Berkeley PhD students benefit from full funding packages that include tuition, fees, and a stipend in exchange for research and teaching duties.

+

Application Seasons

+ + + + + + + +
Program TypeTypical Deadline
PhD ProgramsDecember 1–15
Master's Programs (Academic)December 1–January 15
Professional Master's ProgramsJanuary 5–March 15
MBA (Haas)January 5 (Round 2)
JD (Law)February 1
+
+
+
+ {{ grad_programs }} + Graduate Programs +
+
+ 12,000 + Graduate Students +
+
+ 50+ + Top-10 Graduate Programs +
+
+
+ +

Browse Graduate Programs

+
+ {% for deg in ['MA', 'MS', 'PhD', 'MPH', 'MBA', 'MEng', 'JD', 'MD'] %} + {{ deg }} Programs + {% endfor %} +
+
+ + +
+

Financial Aid & Scholarships

+

Berkeley is committed to making a world-class education accessible to all qualified students regardless of financial circumstances. Two-thirds of Berkeley undergraduates receive some form of financial aid, and 30% pay no tuition at all thanks to the Blue and Gold Opportunity Plan.

+ +
+
+{% endblock %} diff --git a/sites/berkeley/templates/base.html b/sites/berkeley/templates/base.html new file mode 100644 index 000000000..10b54db2c --- /dev/null +++ b/sites/berkeley/templates/base.html @@ -0,0 +1,374 @@ + + + + + + {% block title %}UC Berkeley{% endblock %} + + {% block head %}{% endblock %} + + + + +
+
+
+ University of California, Berkeley + Berkeley, CA 94720 +
+
+ {% if current_user.is_authenticated %} + {{ current_user.full_name or current_user.username }} + Sign Out + {% else %} + Sign In + Create Account + {% endif %} +
+
+
+ + +
+ + + +
+ + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+
+ {% endif %} +{% endwith %} + + +
+ {% block content %}{% endblock %} +
+ + + + + + diff --git a/sites/berkeley/templates/department_detail.html b/sites/berkeley/templates/department_detail.html new file mode 100644 index 000000000..cf6387592 --- /dev/null +++ b/sites/berkeley/templates/department_detail.html @@ -0,0 +1,84 @@ +{% extends "base.html" %} +{% block title %}{{ dept.name }} — UC Berkeley{% endblock %} + +{% block content %} + + + + +
+ +
+{% endblock %} diff --git a/sites/berkeley/templates/departments.html b/sites/berkeley/templates/departments.html new file mode 100644 index 000000000..64c246cb3 --- /dev/null +++ b/sites/berkeley/templates/departments.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% block title %}Departments — UC Berkeley{% endblock %} + +{% block content %} + + +
+ {% for college, depts in depts_by_college.items() %} + {% if depts %} +
+

+ {{ college.name }} +

+
+ {% for dept in depts %} +
+
+

+ {{ dept.name }} +

+

{{ dept.description[:110] }}{% if dept.description|length > 110 %}...{% endif %}

+
+ {% if dept.chair %}
Chair: {{ dept.chair }}
{% endif %} + {% if dept.location %}
📍 {{ dept.location }}
{% endif %} +
+ +
+
+ {% endfor %} +
+
+ {% endif %} + {% endfor %} +
+{% endblock %} diff --git a/sites/berkeley/templates/event_detail.html b/sites/berkeley/templates/event_detail.html new file mode 100644 index 000000000..9dae30a3c --- /dev/null +++ b/sites/berkeley/templates/event_detail.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block title %}{{ event.title }} — Berkeley Events{% endblock %} + +{% block content %} + + + + +
+ +
+{% endblock %} diff --git a/sites/berkeley/templates/events.html b/sites/berkeley/templates/events.html new file mode 100644 index 000000000..d322753dc --- /dev/null +++ b/sites/berkeley/templates/events.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} +{% block title %}Events — UC Berkeley{% endblock %} + +{% block content %} + + +
+
+
+ + + + + {% if q or current_category %} + Clear + {% endif %} +
+
+ +
+ Upcoming + Today + Past Events + {% for cat in categories %} + {{ cat }} + {% endfor %} +
+ +

+ Showing {{ events|length }} of {{ total }} events +

+ + {% if events %} +
+ {% for event in events %} +
+
+
+
{{ event.start_datetime.strftime('%d') }}
+
{{ event.start_datetime.strftime('%b %Y') }}
+
+ {{ event.category }} +
+
+

+ {{ event.title }} +

+

{{ event.description[:120] }}{% if event.description|length > 120 %}...{% endif %}

+
+
🕐 {{ event.start_datetime.strftime('%I:%M %p') }}
+
📍 {{ event.location }}
+ {% if event.registration_required %}
⚠ Registration Required
{% endif %} +
💳 {{ event.cost }}
+
+
+ Details +
+
+
+ {% endfor %} +
+ + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
+

No events found.

+ View All Events +
+ {% endif %} +
+{% endblock %} diff --git a/sites/berkeley/templates/faculty.html b/sites/berkeley/templates/faculty.html new file mode 100644 index 000000000..d8ae21658 --- /dev/null +++ b/sites/berkeley/templates/faculty.html @@ -0,0 +1,87 @@ +{% extends "base.html" %} +{% block title %}Faculty Directory — UC Berkeley{% endblock %} + +{% block content %} + + +
+
+
+ + + + {% if q or current_dept %} + Clear + {% endif %} +
+
+ +

+ Showing {{ faculty_list|length }} of {{ total }} faculty members + {% if q %} matching "{{ q }}"{% endif %} +

+ + {% if faculty_list %} +
+ {% for member in faculty_list %} +
+
+
+ {{ member.name[0] }} +
+
+
+ {% if member.is_emeritus %} + Emeritus + {% endif %} +

{{ member.name }}

+

{{ member.title }}

+ {% if member.department %} +

{{ member.department.name }}

+ {% endif %} + {% if member.research_interests %} +

{{ member.research_interests[:100] }}{% if member.research_interests|length > 100 %}...{% endif %}

+ {% endif %} + +
+
+ {% endfor %} +
+ + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
+

No faculty members found.

+ View All Faculty +
+ {% endif %} +
+{% endblock %} diff --git a/sites/berkeley/templates/faculty_profile.html b/sites/berkeley/templates/faculty_profile.html new file mode 100644 index 000000000..64d9fa0ac --- /dev/null +++ b/sites/berkeley/templates/faculty_profile.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} +{% block title %}{{ member.name }} — Berkeley Faculty{% endblock %} + +{% block content %} + + + + +
+ +
+{% endblock %} diff --git a/sites/berkeley/templates/index.html b/sites/berkeley/templates/index.html new file mode 100644 index 000000000..0f90e801b --- /dev/null +++ b/sites/berkeley/templates/index.html @@ -0,0 +1,164 @@ +{% extends "base.html" %} +{% block title %}UC Berkeley — The University of California, Berkeley{% endblock %} + +{% block content %} + +
+
+
+
+

Est. 1868 · Berkeley, California

+

Fiat Lux.
Let There Be Light.

+

UC Berkeley is the world's premier public university, where bold ideas, transformative research, and a deep commitment to the public good have shaped the world for over 155 years.

+ +
+
+
+ + +
+
+
+
+
{{ stats.nobel_laureates }}
+
Nobel Laureates on Faculty
+
+
+
#1
+
Public Research University
+
+
+
{{ stats.top_10_programs }}+
+
Top-10 Graduate Programs
+
+
+
{{ stats.degree_programs }}+
+
Degree Programs
+
+
+
{{ stats.national_titles }}
+
NCAA National Titles
+
+
+
+
+ + +
+
+
+

Berkeley News

+ All News → +
+ {% if featured_news %} + +
+
+
+
+
🌎
+
Berkeley News
+
+
+
+ {{ featured_news[0].category }} +

+ {{ featured_news[0].title }} +

+

{{ featured_news[0].summary }}

+
+ By {{ featured_news[0].author }} · {{ featured_news[0].published_date.strftime('%B %d, %Y') }} +
+
+
+
+ +
+ {% for article in featured_news[1:] %} +
+
+ {{ article.category }} +
+
+ {{ article.category }} +

{{ article.title }}

+

{{ article.summary[:120] }}{% if article.summary|length > 120 %}...{% endif %}

+

{{ article.published_date.strftime('%B %d, %Y') }}

+
+
+ {% endfor %} +
+ {% endif %} +
+
+ + +
+
+
+

Upcoming Events

+ All Events → +
+
+ {% for event in upcoming_events %} +
+
+
{{ event.start_datetime.strftime('%d') }}
+
{{ event.start_datetime.strftime('%B %Y') }}
+
+
+ {{ event.category }} +

{{ event.title }}

+

+ 🕐 {{ event.start_datetime.strftime('%I:%M %p') }}
+ 📍 {{ event.location }} +

+
+
+ {% endfor %} +
+
+
+ + +
+
+
+

Research at Berkeley

+ All Research Centers → +
+
+ {% for center in recent_research %} +
+
+

{{ center.name }}

+

{{ center.description[:140] }}{% if center.description|length > 140 %}...{% endif %}

+

Director: {{ center.director }}

+
+
+ {% endfor %} +
+
+
+ + +
+
+

14 Schools & Colleges

+

From humanities to engineering, law to public health — explore Berkeley's diverse academic community.

+
+ {% for college in colleges %} + + {{ college.name }} + + {% endfor %} +
+ +
+
+{% endblock %} diff --git a/sites/berkeley/templates/login.html b/sites/berkeley/templates/login.html new file mode 100644 index 000000000..55771a89c --- /dev/null +++ b/sites/berkeley/templates/login.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}Sign In — UC Berkeley{% endblock %} + +{% block content %} +
+
+
+
C
+

Sign In to Berkeley

+

Access your bookmarks and personalized content

+
+ +
+
+ {{ form.csrf_token }} +
+ {{ form.email.label }} + {{ form.email(class="form-control", placeholder="your@berkeley.edu") }} + {% if form.email.errors %} +
{{ form.email.errors[0] }}
+ {% endif %} +
+
+ {{ form.password.label }} + {{ form.password(class="form-control", placeholder="Password") }} + {% if form.password.errors %} +
{{ form.password.errors[0] }}
+ {% endif %} +
+ +
+
+ +

+ Don't have an account? Create one +

+
+
+{% endblock %} diff --git a/sites/berkeley/templates/news.html b/sites/berkeley/templates/news.html new file mode 100644 index 000000000..4eca6f8ca --- /dev/null +++ b/sites/berkeley/templates/news.html @@ -0,0 +1,103 @@ +{% extends "base.html" %} +{% block title %}Berkeley News{% endblock %} + +{% block content %} + + +
+ +
+
+ + + + + {% if q or current_category or featured %} + Clear + {% endif %} +
+
+ + +
+ All + {% for cat in categories %} + {{ cat }} + {% endfor %} +
+ +

+ Showing {{ articles|length }} of {{ total }} articles + {% if q %} for "{{ q }}"{% endif %} + {% if current_category %} in {{ current_category }}{% endif %} +

+ + {% if articles %} +
+ {% for article in articles %} +
+
+ {{ article.category }} + {% if article.featured %}Featured{% endif %} +
+
+ {{ article.category }} +

{{ article.title }}

+

{{ article.summary[:140] }}{% if article.summary|length > 140 %}...{% endif %}

+

+ By {{ article.author }} · {{ article.published_date.strftime('%B %d, %Y') }} + · {{ article.view_count }} views +

+
+ {% for tag in article.tags.split(',')[:3] %} + {{ tag.strip() }} + {% endfor %} +
+
+
+ {% endfor %} +
+ + + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
+

No articles found.

+ View All News +
+ {% endif %} +
+{% endblock %} diff --git a/sites/berkeley/templates/news_article.html b/sites/berkeley/templates/news_article.html new file mode 100644 index 000000000..624b0ce81 --- /dev/null +++ b/sites/berkeley/templates/news_article.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} +{% block title %}{{ article.title }} — Berkeley News{% endblock %} + +{% block content %} + + +
+ +
+{% endblock %} diff --git a/sites/berkeley/templates/program_detail.html b/sites/berkeley/templates/program_detail.html new file mode 100644 index 000000000..8a21c41f7 --- /dev/null +++ b/sites/berkeley/templates/program_detail.html @@ -0,0 +1,98 @@ +{% extends "base.html" %} +{% block title %}{{ program.name }} ({{ program.degree_type }}) — UC Berkeley{% endblock %} + +{% block content %} + + + + +
+ +
+{% endblock %} diff --git a/sites/berkeley/templates/programs.html b/sites/berkeley/templates/programs.html new file mode 100644 index 000000000..7b9cae55c --- /dev/null +++ b/sites/berkeley/templates/programs.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} +{% block title %}Programs — UC Berkeley{% endblock %} + +{% block content %} + + +
+
+
+ + + + + {% if q or current_college or current_degree %} + Clear + {% endif %} +
+
+ + +
+ Degree: + All + {% for dt in degree_types %} + {{ dt }} + {% endfor %} +
+ +

+ Showing {{ programs|length }} of {{ total }} programs + {% if q %} matching "{{ q }}"{% endif %} +

+ + {% if programs %} +
+ {% for program in programs %} +
+
+
+ {{ program.degree_type }} + {% if program.is_online %}Online{% endif %} +
+

+ {{ program.name }} +

+ {% if program.college %} +

{{ program.college.name }}

+ {% endif %} +

{{ program.description[:130] }}{% if program.description|length > 130 %}...{% endif %}

+
+ {% if program.duration_years > 0 %}{{ program.duration_years|int if program.duration_years == program.duration_years|int else program.duration_years }} years{% endif %} + {% if program.units > 0 %} · {{ program.units }} units{% endif %} + {% if program.gre_required %} · GRE Required{% endif %} +
+ +
+
+ {% endfor %} +
+ + {% if total_pages > 1 %} + + {% endif %} + + {% else %} +
+

No programs found.

+ View All Programs +
+ {% endif %} +
+{% endblock %} diff --git a/sites/berkeley/templates/register.html b/sites/berkeley/templates/register.html new file mode 100644 index 000000000..8ac24a75a --- /dev/null +++ b/sites/berkeley/templates/register.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% block title %}Create Account — UC Berkeley{% endblock %} + +{% block content %} +
+
+
+
C
+

Create Your Account

+

Join the Berkeley community

+
+ +
+
+ {{ form.csrf_token }} +
+ {{ form.username.label }} + {{ form.username(class="form-control", placeholder="Choose a username") }} + {% if form.username.errors %} +
{{ form.username.errors[0] }}
+ {% endif %} +
+
+ {{ form.full_name.label }} + {{ form.full_name(class="form-control", placeholder="Your full name") }} + {% if form.full_name.errors %} +
{{ form.full_name.errors[0] }}
+ {% endif %} +
+
+ {{ form.email.label }} + {{ form.email(class="form-control", placeholder="your@email.com") }} + {% if form.email.errors %} +
{{ form.email.errors[0] }}
+ {% endif %} +
+
+ {{ form.password.label }} + {{ form.password(class="form-control", placeholder="At least 8 characters") }} + {% if form.password.errors %} +
{{ form.password.errors[0] }}
+ {% endif %} +
+
+ {{ form.confirm.label }} + {{ form.confirm(class="form-control", placeholder="Repeat password") }} + {% if form.confirm.errors %} +
{{ form.confirm.errors[0] }}
+ {% endif %} +
+ +
+
+ +

+ Already have an account? Sign in +

+
+
+{% endblock %} diff --git a/sites/berkeley/templates/research.html b/sites/berkeley/templates/research.html new file mode 100644 index 000000000..c2439d80f --- /dev/null +++ b/sites/berkeley/templates/research.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% block title %}Research — UC Berkeley{% endblock %} + +{% block content %} + + +
+ +
+
+

The World's Top Public Research University

+

UC Berkeley is home to more than 200 research institutes, centers, and programs. Our faculty and students conduct groundbreaking research across every discipline, from artificial intelligence to environmental science, from genomics to urban planning.

+

Berkeley researchers have developed the technology behind the internet, discovered fundamental particles, sequenced genomes, and modeled the climate. Every year, Berkeley generates more than $1 billion in research expenditures.

+
+
+
$1B+Annual Research
+
12Nobel Laureates on Faculty
+
200+Research Institutes
+
#1Public Research Uni
+
+
+ +

Research Centers & Institutes

+ +
+ {% for center in centers %} +
+
+

{{ center.name }}

+ {% if center.college %} +

{{ center.college.name }}

+ {% endif %} +

{{ center.description[:160] }}{% if center.description|length > 160 %}...{% endif %}

+
+
Director: {{ center.director }}
+
Founded: {{ center.founded_year }}
+
+ {% for area in center.focus_areas.split(',')[:3] %} + {{ area.strip() }} + {% endfor %} +
+
+ +
+
+ {% endfor %} +
+ + +
+

Research by School

+
+ {% for college in colleges %} + {% set college_centers = centers | selectattr('college_id', 'equalto', college.id) | list %} + {% if college_centers %} +
+

{{ college.name }}

+

{{ college_centers|length }} center{{ 's' if college_centers|length != 1 else '' }}

+
+ {% endif %} + {% endfor %} +
+
+
+{% endblock %} diff --git a/sites/berkeley/templates/research_center.html b/sites/berkeley/templates/research_center.html new file mode 100644 index 000000000..17a8af6b6 --- /dev/null +++ b/sites/berkeley/templates/research_center.html @@ -0,0 +1,82 @@ +{% extends "base.html" %} +{% block title %}{{ center.name }} — Berkeley Research{% endblock %} + +{% block content %} + + + + +
+ +
+{% endblock %} diff --git a/sites/berkeley/templates/search.html b/sites/berkeley/templates/search.html new file mode 100644 index 000000000..986ce64a2 --- /dev/null +++ b/sites/berkeley/templates/search.html @@ -0,0 +1,164 @@ +{% extends "base.html" %} +{% block title %}Search{% if q %}: {{ q }}{% endif %} — UC Berkeley{% endblock %} + +{% block content %} + + +
+ +
+
+ + +
+
+ + {% if q %} +

+ Found {{ total }} result{{ 's' if total != 1 else '' }} for "{{ q }}" +

+ + {% if total == 0 %} +
+

No results found for "{{ q }}"

+

Try different keywords, or browse our content directly:

+ +
+ {% else %} + + + {% if results.programs %} +
+

+ 🏫 Degree Programs + {{ results.programs|length }} result{{ 's' if results.programs|length != 1 else '' }} +

+
+ {% for p in results.programs %} +
+
+ {{ p.degree_type }} +

{{ p.name }}

+ {% if p.college %}

{{ p.college.name }}

{% endif %} +
+
+ {% endfor %} +
+ +
+ {% endif %} + + + {% if results.news %} +
+

+ 📰 News Articles + {{ results.news|length }} result{{ 's' if results.news|length != 1 else '' }} +

+
+ {% for article in results.news %} +
+ {{ article.category }} +
+

{{ article.title }}

+

{{ article.published_date.strftime('%B %d, %Y') }} · {{ article.author }}

+
+
+ {% endfor %} +
+ +
+ {% endif %} + + + {% if results.faculty %} +
+

+ 👥 Faculty + {{ results.faculty|length }} result{{ 's' if results.faculty|length != 1 else '' }} +

+
+ {% for member in results.faculty %} +
+
+

{{ member.name }}

+

{{ member.title }}

+ {% if member.department %}

{{ member.department.name }}

{% endif %} +
+
+ {% endfor %} +
+ +
+ {% endif %} + + + {% if results.events %} +
+

+ 📅 Events + {{ results.events|length }} result{{ 's' if results.events|length != 1 else '' }} +

+
+ {% for event in results.events %} +
+
+
{{ event.start_datetime.strftime('%d') }}
+
{{ event.start_datetime.strftime('%b') }}
+
+
+

{{ event.title }}

+

{{ event.location }} · {{ event.category }}

+
+
+ {% endfor %} +
+
+ {% endif %} + + + {% if results.research %} +
+

+ 🔬 Research Centers + {{ results.research|length }} result{{ 's' if results.research|length != 1 else '' }} +

+
+ {% for center in results.research %} +
+
+

{{ center.name }}

+

Director: {{ center.director }}

+
+
+ {% endfor %} +
+
+ {% endif %} + + {% endif %} + {% else %} + +
+

Enter a search term above to find programs, faculty, news, events, and research centers.

+ +
+ {% endif %} +
+{% endblock %} diff --git a/websyn_start.sh b/websyn_start.sh index 72defad8b..f3757c9f2 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -5,7 +5,7 @@ set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn) + cambridge_dictionary coursera espn berkeley) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR" @@ -17,7 +17,7 @@ for d in "${SITES[@]}"; do cp -a "/opt/WebSyn/$d/instance_seed" "/opt/WebSyn/$d/instance" done -echo "[WebSyn] Starting 15 sites on ports ${BASE_PORT}-$((BASE_PORT + 14))..." +echo "[WebSyn] Starting 16 sites on ports ${BASE_PORT}-$((BASE_PORT + 15))..." for i in "${!SITES[@]}"; do site="${SITES[$i]}" port=$((BASE_PORT + i)) @@ -51,8 +51,8 @@ except Exception: exit(1) ready=$((ready + 1)) fi done - echo " [${elapsed}/${max_wait}s] ${ready}/15 sites ready" - if [ $ready -eq 15 ]; then + echo " [${elapsed}/${max_wait}s] ${ready}/16 sites ready" + if [ $ready -eq 16 ]; then break fi done From 2f0c6c4eea762e39a64c11ab18bc6da4bbc1532d Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:26:27 -0400 Subject: [PATCH 02/25] fix(berkeley): build-generated, byte-reproducible seed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .build-generated-seed: declares instance_seed/berkeley.db a build artifact (this site has no HF archive), so check_assets.sh and build.sh stop requiring one. - Dockerfile: generate the seed with the peers' exact shape — `rm -rf instance instance_seed && PYTHONHASHSEED=0 python seed_data.py && rm -rf instance` — instead of `python3 -c "from app import app"` plus a cp. - seed_data.py: __main__ writes instance_seed/berkeley.db itself (build_seed_database), so the artifact comes from the documented command rather than an import side effect. - seed_data.py: freeze the four benchmark password hashes (bcrypt salts are random, so set_password() gave every build a different DB) and pin created_at=datetime(2026, 5, 12) (the column default read the wall clock). - app.py: guarded bootstrap_site() as in walmart_careers (WEBSYN_SKIP_BOOTSTRAP=1 suppresses it), still seeding on import so site_runner.py and the generator work unchanged. - app.py: drop index=True from users.email / users.username. SQLAlchemy emits a table's named indexes in set-iteration order, so the two indexes were assigned different root pages from run to run (observed: pages 3/4 swapping) and the file was reproducible only by luck. unique=True keeps SQLite's implicit index, created in declaration order. Verification: step 2 (md5 3001bcf4bcec169f4192c08609160ab6 across 5 scratch builds — 4x PYTHONHASHSEED=0, 1x PYTHONHASHSEED=1 — and 3 further in-repo runs); step 3 bootstrap half (empty instance/ seeds to the same md5; populated instance/ is a no-op; skip flag suppresses). Co-Authored-By: Claude Code --- Dockerfile | 11 +++-- sites/berkeley/.build-generated-seed | 1 + sites/berkeley/app.py | 36 +++++++++++++-- sites/berkeley/seed_data.py | 68 ++++++++++++++++++++++++---- 4 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 sites/berkeley/.build-generated-seed diff --git a/Dockerfile b/Dockerfile index bf50a8015..f273595b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,11 +64,12 @@ RUN python3 /opt/WebSyn/webmd_doctor/check_generated_assets.py RUN cd /opt/WebSyn/webmd_doctor && rm -rf instance instance_seed && \ PYTHONHASHSEED=0 python seed_data.py && rm -rf instance __pycache__ -# Berkeley: all data is code-generated (no scraped images → no HF asset). -# Build the seed DB once at image-build time so websyn_start.sh can copy it on boot. -RUN cd /opt/WebSyn/berkeley && \ - python3 -c "from app import app" && \ - cp instance/berkeley.db instance_seed/berkeley.db +# Berkeley: all data is code-generated (no scraped images → no HF asset), so the +# seed DB is generated here from tracked source — see .build-generated-seed. No +# wall clock and no random salt reaches a row, so the artifact is byte-reproducible; +# websyn_start.sh copies it into instance/ at boot. +RUN cd /opt/WebSyn/berkeley && rm -rf instance instance_seed && \ + PYTHONHASHSEED=0 python seed_data.py && rm -rf instance COPY websyn_start.sh /opt/websyn_start.sh COPY control_server.py /opt/control_server.py diff --git a/sites/berkeley/.build-generated-seed b/sites/berkeley/.build-generated-seed new file mode 100644 index 000000000..74db7ae44 --- /dev/null +++ b/sites/berkeley/.build-generated-seed @@ -0,0 +1 @@ +The Dockerfile generates instance_seed/berkeley.db deterministically from tracked source data; the site has no Hugging Face asset archive. diff --git a/sites/berkeley/app.py b/sites/berkeley/app.py index d799eeb98..d03209ca5 100644 --- a/sites/berkeley/app.py +++ b/sites/berkeley/app.py @@ -2,6 +2,7 @@ """UC Berkeley mirror — Flask application.""" import os import re +import sys from datetime import datetime from math import ceil @@ -51,8 +52,14 @@ def slugify(text): class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True, nullable=False, index=True) - username = db.Column(db.String(80), unique=True, nullable=False, index=True) + # Deliberately no `index=True` here. SQLAlchemy emits a table's named indexes + # in set-iteration order, so two indexes on one table are assigned different + # root pages from run to run (observed: ix_users_email / ix_users_username + # swapping pages 3 and 4), which breaks byte-reproducibility of the seed DB. + # unique=True already gives each column SQLite's implicit index, created in + # declaration order as part of CREATE TABLE. + email = db.Column(db.String(120), unique=True, nullable=False) + username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(255), nullable=False) full_name = db.Column(db.String(150), nullable=False, default='') role = db.Column(db.String(30), default='student') @@ -738,10 +745,29 @@ def server_error(e): # ─── Startup ────────────────────────────────────────────────────────────────── -with app.app_context(): - db.create_all() +def bootstrap_site(): + """Create the DB and seed it if it is empty. + + Importing this module seeds, so `from app import app` still materializes an + empty instance/ (site_runner.py and the Dockerfile's generator both rely on + that). On a populated DB — the shipped instance_seed copy, i.e. every boot + and every /reset — seed() returns at its College gate before touching a + session, leaving the file byte-identical. + """ from seed_data import seed - seed() + with app.app_context(): + db.create_all() + seed() + + +# `python app.py` loads this file as __main__; register it under its import name +# too so seed_data's `from app import ...` reuses this module instead of building +# a second Flask app + SQLAlchemy instance. +sys.modules.setdefault('app', sys.modules[__name__]) + +if os.environ.get('WEBSYN_SKIP_BOOTSTRAP') != '1': + bootstrap_site() + if __name__ == '__main__': port = int(os.environ.get('PORT', '40026')) diff --git a/sites/berkeley/seed_data.py b/sites/berkeley/seed_data.py index 3865acdc5..d37c95c41 100644 --- a/sites/berkeley/seed_data.py +++ b/sites/berkeley/seed_data.py @@ -1,6 +1,13 @@ #!/usr/bin/env python3 -"""Seed data for UC Berkeley mirror site. Idempotent — safe to call multiple times.""" +"""Seed data for UC Berkeley mirror site. Idempotent — safe to call multiple times. + +`python seed_data.py` regenerates instance_seed/berkeley.db from this file alone +(see build_seed_database); the Dockerfile runs exactly that, and the artifact is +byte-reproducible across runs and PYTHONHASHSEED values. +""" +import os import re +import shutil from datetime import datetime, timedelta @@ -2220,29 +2227,70 @@ def seed(): db.session.flush() # ── Users ───────────────────────────────────────────────────────────────── + # Frozen bcrypt hashes. bcrypt salts are random, so calling set_password() + # here would hand every build a different instance_seed DB and break + # byte-reproducibility. Plaintext for all four demo accounts: test1234. + HASH_ALICE = '$2b$12$aPotHbRbf3bmNywOvraONepBximmqdBjCvQ2m.3Lp.jbVe4TASwLG' + HASH_BOB = '$2b$12$Xnr74O7t/536BADyIjRfl.hn6Pre60g.PorUrIFPvdtC2FxQUwu5u' + HASH_CAROL = '$2b$12$JoA.NeTqa75gRfG0M4ibGuBSSOk.0YCJCNRPKXvVkDETJk3G9iE/q' + HASH_DAVE = '$2b$12$I28Ak4TXObXODGiuwv0qEuduaZXVfyh5/W3X0S/fjZ.C/dJe6reYW' + # Seed-time timestamps are pinned too: the created_at column default reads + # the wall clock, so leaving it unset would stamp the build date into the DB. + BENCHMARK_CREATED_AT = datetime(2026, 5, 12) + benchmark_users = [ - ('alice', 'alice@berkeley.edu', 'test1234', 'Alice Chen', 'student'), - ('bob', 'bob@berkeley.edu', 'test1234', 'Bob Martinez', 'student'), - ('carol', 'carol@berkeley.edu', 'test1234', 'Carol Johnson', 'faculty'), - ('dave', 'dave@berkeley.edu', 'test1234', 'Dave Williams', 'student'), + ('alice', 'alice@berkeley.edu', HASH_ALICE, 'Alice Chen', 'student'), + ('bob', 'bob@berkeley.edu', HASH_BOB, 'Bob Martinez', 'student'), + ('carol', 'carol@berkeley.edu', HASH_CAROL, 'Carol Johnson', 'faculty'), + ('dave', 'dave@berkeley.edu', HASH_DAVE, 'Dave Williams', 'student'), ] - for (username, email, password, full_name, role) in benchmark_users: + for (username, email, password_hash, full_name, role) in benchmark_users: if not User.query.filter_by(email=email).first(): u = User( email=email, username=username, full_name=full_name, role=role, + password_hash=password_hash, + created_at=BENCHMARK_CREATED_AT, ) - u.set_password(password) db.session.add(u) db.session.commit() -if __name__ == '__main__': - from app import app +def build_seed_database(): + """Materialize instance_seed/berkeley.db from this file (build-time only). + + The Dockerfile runs this instead of importing the app, so the shipped seed is + produced by the documented command. Deterministic by construction: no wall + clock and no random salt reaches the rows, so repeated runs over the same + source produce identical bytes. + """ + from app import app, db + + base_dir = os.path.dirname(os.path.abspath(__file__)) + instance_dir = os.path.join(base_dir, 'instance') + seed_dir = os.path.join(base_dir, 'instance_seed') + db_path = os.path.join(instance_dir, 'berkeley.db') + seed_path = os.path.join(seed_dir, 'berkeley.db') + os.makedirs(instance_dir, exist_ok=True) + os.makedirs(seed_dir, exist_ok=True) + with app.app_context(): + db.session.remove() + db.engine.dispose() + if os.path.exists(db_path): + os.unlink(db_path) + db.create_all() seed() - print("Seeding complete.") + db.session.remove() + db.engine.dispose() + + shutil.copyfile(db_path, seed_path) + return seed_path + + +if __name__ == '__main__': + print(f"Seed database generated: {build_seed_database()}") From c1cdcb8894f70d7a3efe1c2059e4080fd83079b1 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:28:11 -0400 Subject: [PATCH 03/25] fix(berkeley): freeze the benchmark clock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sites/osu/app.py:27 pattern: BENCHMARK_NOW = datetime(2026, 5, 12), the same instant seed_data.py pins the seeded event calendar to. - app.py: BENCHMARK_NOW replaces every datetime.utcnow() on a request path — inject_globals, index()'s upcoming-events filter, events() (including its today_start/today_end window) and event_detail()'s related-events filter. Before this the seeded calendar (last event 2026-07-16) was already behind the wall clock: / rendered an empty Upcoming Events section and /events answered "Showing 0 of 0 events", which made the event tasks unsolvable. - app.py: the created_at / published_date column defaults now call a frozen utcnow() helper returning BENCHMARK_NOW, so no request path (register, bookmark_add) or seed path can stamp the wall clock into a row either. Verified: seed md5 unchanged at 3001bcf4bcec169f4192c08609160ab6 (no seeded row ever relied on the column default). Verification: step 3 (all six endpoints 200; instance md5 still equals the seed md5 after the request set; / renders 4 upcoming-event cards; /events default "Showing 20 of 52 events"; /events?category=Lecture "Showing 15 of 15"). Co-Authored-By: Claude Code --- sites/berkeley/app.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/sites/berkeley/app.py b/sites/berkeley/app.py index d03209ca5..9f7e0e191 100644 --- a/sites/berkeley/app.py +++ b/sites/berkeley/app.py @@ -38,6 +38,26 @@ PER_PAGE = 20 +# ─── Benchmark clock ────────────────────────────────────────────────────────── +# The site must render identically on every run and on every day. The seeded +# event calendar is pinned (seed_data.py: `now = datetime(2026, 5, 12)`), so the +# app compares against that same frozen instant instead of the wall clock — +# otherwise every "upcoming" filter drains as the image ages (the seed's last +# event is 2026-07-16, after which /events rendered 0 of 0) and the task +# ground truth rots. Same pattern as sites/osu/app.py: BENCHMARK_NOW. +BENCHMARK_NOW = datetime(2026, 5, 12) + + +def utcnow(): + """Frozen stand-in for datetime.utcnow() — see BENCHMARK_NOW. + + Used as the column default for created_at / published_date so no request + path (register, bookmark_add) or seed path can stamp the wall clock into a + row, which would break byte-reproducibility of instance_seed. + """ + return BENCHMARK_NOW + + # ─── Helpers ────────────────────────────────────────────────────────────────── def slugify(text): @@ -64,7 +84,7 @@ class User(db.Model, UserMixin): full_name = db.Column(db.String(150), nullable=False, default='') role = db.Column(db.String(30), default='student') bio = db.Column(db.Text, default='') - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=utcnow) bookmarks = db.relationship('Bookmark', backref='user', lazy=True, cascade='all, delete-orphan') @@ -132,7 +152,7 @@ class NewsArticle(db.Model): slug = db.Column(db.String(300), unique=True, nullable=False, index=True) category = db.Column(db.String(50), default='Campus Life') author = db.Column(db.String(150), default='Berkeley News Staff') - published_date = db.Column(db.DateTime, default=datetime.utcnow) + published_date = db.Column(db.DateTime, default=utcnow) content = db.Column(db.Text, default='') summary = db.Column(db.Text, default='') tags = db.Column(db.String(500), default='') @@ -191,7 +211,7 @@ class Bookmark(db.Model): item_type = db.Column(db.String(50), nullable=False) item_id = db.Column(db.Integer, nullable=False) note = db.Column(db.Text, default='') - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=utcnow) # ─── Forms ──────────────────────────────────────────────────────────────────── @@ -228,7 +248,7 @@ def load_user(user_id): @app.context_processor def inject_globals(): return { - 'now': datetime.utcnow(), + 'now': BENCHMARK_NOW, 'colleges': College.query.order_by(College.name).all(), } @@ -242,7 +262,7 @@ def index(): featured_news = NewsArticle.query.order_by( NewsArticle.published_date.desc()).limit(6).all() upcoming_events = Event.query.filter( - Event.start_datetime >= datetime.utcnow() + Event.start_datetime >= BENCHMARK_NOW ).order_by(Event.start_datetime).limit(4).all() recent_research = ResearchCenter.query.limit(4).all() stats = { @@ -380,7 +400,7 @@ def events(): category = request.args.get('category', '') date_filter = request.args.get('date', 'upcoming') page = request.args.get('page', 1, type=int) - now = datetime.utcnow() + now = BENCHMARK_NOW query = Event.query if q: @@ -431,7 +451,7 @@ def event_detail(event_id): related = Event.query.filter( Event.category == event.category, Event.id != event.id, - Event.start_datetime >= datetime.utcnow() + Event.start_datetime >= BENCHMARK_NOW ).order_by(Event.start_datetime).limit(3).all() return render_template('event_detail.html', event=event, related=related) From 86ba06efa64d5719b7352a2663d053da84acaa19 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 00:31:48 -0400 Subject: [PATCH 04/25] chore(scripts): fetch_assets.sh skips build-generated sites with no archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A site whose seed is generated by the Dockerfile can legitimately have no archive at all, not merely a media-only one — berkeley is the first such site. fetch_assets.sh was the only release script still requiring an archive for every site directory (check_assets.sh, build.sh and extract_assets.sh all honour .build-generated-seed), so a fresh clone died before extracting anything: [fetch] scope: 27 registered site(s) fetch_assets: revision ad6f424f72cada9e6f5c09a58093d0ceeab9c52b has no archive for: berkeley exit 1 - single-site branch: after the download attempt, a missing archive on a .build-generated-seed site reports "nothing to fetch" and exits 0 - all-sites branch: such sites are skipped instead of added to `missing` - an archive that does exist (fedex, webmd_doctor, ...) is still downloaded and validated; nothing changes for media-carrying build-generated sites Verified: `./scripts/fetch_assets.sh berkeley` -> exit 0, no "expected archive"; `./scripts/fetch_assets.sh` -> exit 0, 26 sites extracted; `./scripts/check_assets.sh` -> exit 0. Verification: step 4 (berkeley-only fetch exits 0 without the "expected archive" error, and the full fetch + check_assets.sh succeed). Co-Authored-By: Claude Code --- scripts/fetch_assets.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/fetch_assets.sh b/scripts/fetch_assets.sh index 943bc4af4..d9cb4b67e 100755 --- a/scripts/fetch_assets.sh +++ b/scripts/fetch_assets.sh @@ -59,6 +59,13 @@ missing=() if [[ -n "$ONLY_SITE" ]]; then TARBALLS=("$CACHE_DIR/$ONLY_SITE.tar.gz") if [[ ! -f "${TARBALLS[0]}" ]]; then + # A build-generated site may legitimately carry no archive at all (its + # seed is produced by the Dockerfile), so there is nothing to download + # and nothing to extract — same exemption check_assets.sh/build.sh use. + if [[ -f "sites/$ONLY_SITE/.build-generated-seed" ]]; then + echo "[fetch] $ONLY_SITE: build-generated seed, no archive at this revision — nothing to fetch" + exit 0 + fi echo "fetch_assets: expected archive for $ONLY_SITE" >&2 exit 1 fi @@ -73,6 +80,9 @@ else site=$(basename "$site_dir") if [[ -f "$CACHE_DIR/$site.tar.gz" ]]; then TARBALLS+=("$CACHE_DIR/$site.tar.gz") + elif [[ -f "sites/$site/.build-generated-seed" ]]; then + # Seed comes from the Dockerfile, so no archive is expected. + echo "[fetch] $site: build-generated seed, no archive at this revision — skipping" else missing+=("$site") fi From 1929b84f323fb0387fc32d1291dcc2adc8ef3dec Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:02:22 -0400 Subject: [PATCH 05/25] fix(berkeley): article detail no longer writes view_count GET /news/ bumped news_articles.view_count and committed, so every article visit wrote the DB. That broke two invariants: a read-only benchmark task could never have an after-state equal to its initial snapshot, and instance/ diverged from instance_seed as soon as an agent opened one article. The route is now a pure read. The column is kept and is still rendered as "N views" on /news and on the article page from the frozen seed values; nothing orders or filters by it. Verified: fresh-seed boot on :45011, three article detail pages all 200, md5(instance/berkeley.db) == md5(instance_seed/berkeley.db) == 3001bcf4..., news_articles rows (incl. view_count) identical before and after. Co-Authored-By: Claude Code --- sites/berkeley/app.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sites/berkeley/app.py b/sites/berkeley/app.py index 9f7e0e191..a292dc318 100644 --- a/sites/berkeley/app.py +++ b/sites/berkeley/app.py @@ -324,8 +324,13 @@ def news(): @app.route('/news/') def news_article(slug): article = NewsArticle.query.filter_by(slug=slug).first_or_404() - article.view_count = (article.view_count or 0) + 1 - db.session.commit() + # Deliberately no view_count increment: this GET is a pure read. Bumping the + # counter made every article visit a DB write, which broke the read-only grading + # contract (a read-only task's after-state could never equal its initial snapshot) + # and the byte-identical reset invariant (instance/ diverges from instance_seed + # as soon as an agent opens one article). The column is kept and is displayed as + # "N views" on /news and the article page; those numbers are the frozen seed + # values, and nothing orders or filters by them. related = NewsArticle.query.filter( NewsArticle.category == article.category, NewsArticle.id != article.id From 1d17acb2477c3896c59c2191f558de19b292a348 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:47:33 -0400 Subject: [PATCH 06/25] test(berkeley): add deterministic task verifiers and verify_lib sites/berkeley/verify/ now carries the grading contract for the 22 accepted rows: the 20 kept ids (including the re-anchored --27) plus the new stateful --30/--31. - verify_lib.py: adapted from sites/webmd_doctor (newest merged), SITE=berkeley, no cross-site import, zero LLM calls on any verdict path. The snapshot contract is a pinned schema hash + nine-table set + seed counts + row-level catalog fingerprint; snapshots resolve from /initial.db|after.db, --initial_db/--after_db, or docker cp from $WH_CONTAINER, and missing or invalid input exits 1 with a structured infra_error instead of a traceback. - ground_truth.py: every target is re-derived from the run's initial.db the way the app renders it (the BENCHMARK_NOW event filter, PER_PAGE, the app's ORDER BY clauses, the unordered LIMIT 3 related-centres query). The two source-rendered rows (--11, --17) parse tracked files and fail closed if the literals move. No answer constant is frozen anywhere. - 22 verifiers: any-step URL gates plus the final action's declared target as an alternative satisfier; every multi-hop task gates each hop; set-valued answers use derived accepted sets and minimum counts; matching is negation-aware; --30/--31 bind to an exact bookmark row delta, and --31 also pins the surviving row id (2) as proof that both inserts and the removal happened. - verify/tests: 516 tests. Stdlib-only fixture DBs reproduce the pinned fingerprint; trajectories follow the agent_demo/agent.py signature. Per task: genuine PASS, no-op, wrong task id, another task's trajectory, shortcuts, 1-2 wrong answers, alternative phrasings, negated answers, truncated run, corrupt and 1x1 PNGs, missing after.db, catalog/schema drift, collateral writes and state mismatch, plus the About/Admissions source-fact assertions. Run: .venv/bin/python -m pytest sites/berkeley/verify/tests -q -> 516 passed Co-Authored-By: Claude Code --- sites/berkeley/verify/ground_truth.py | 444 ++++++ sites/berkeley/verify/tests/_support.py | 414 ++++++ sites/berkeley/verify/tests/test_verify_1.py | 65 + sites/berkeley/verify/tests/test_verify_10.py | 60 + sites/berkeley/verify/tests/test_verify_11.py | 53 + sites/berkeley/verify/tests/test_verify_12.py | 66 + sites/berkeley/verify/tests/test_verify_13.py | 53 + sites/berkeley/verify/tests/test_verify_14.py | 64 + sites/berkeley/verify/tests/test_verify_16.py | 67 + sites/berkeley/verify/tests/test_verify_17.py | 71 + sites/berkeley/verify/tests/test_verify_19.py | 71 + sites/berkeley/verify/tests/test_verify_2.py | 75 ++ sites/berkeley/verify/tests/test_verify_20.py | 66 + sites/berkeley/verify/tests/test_verify_22.py | 63 + sites/berkeley/verify/tests/test_verify_23.py | 78 ++ sites/berkeley/verify/tests/test_verify_24.py | 90 ++ sites/berkeley/verify/tests/test_verify_25.py | 75 ++ sites/berkeley/verify/tests/test_verify_27.py | 94 ++ sites/berkeley/verify/tests/test_verify_28.py | 65 + sites/berkeley/verify/tests/test_verify_30.py | 109 ++ sites/berkeley/verify/tests/test_verify_31.py | 133 ++ sites/berkeley/verify/tests/test_verify_4.py | 67 + sites/berkeley/verify/tests/test_verify_6.py | 86 ++ sites/berkeley/verify/tests/test_verify_7.py | 76 ++ .../berkeley/verify/tests/test_verify_lib.py | 352 +++++ sites/berkeley/verify/verify_1.py | 73 + sites/berkeley/verify/verify_10.py | 68 + sites/berkeley/verify/verify_11.py | 69 + sites/berkeley/verify/verify_12.py | 88 ++ sites/berkeley/verify/verify_13.py | 64 + sites/berkeley/verify/verify_14.py | 73 + sites/berkeley/verify/verify_16.py | 89 ++ sites/berkeley/verify/verify_17.py | 87 ++ sites/berkeley/verify/verify_19.py | 77 ++ sites/berkeley/verify/verify_2.py | 74 + sites/berkeley/verify/verify_20.py | 79 ++ sites/berkeley/verify/verify_22.py | 71 + sites/berkeley/verify/verify_23.py | 76 ++ sites/berkeley/verify/verify_24.py | 93 ++ sites/berkeley/verify/verify_25.py | 92 ++ sites/berkeley/verify/verify_27.py | 81 ++ sites/berkeley/verify/verify_28.py | 82 ++ sites/berkeley/verify/verify_30.py | 100 ++ sites/berkeley/verify/verify_31.py | 109 ++ sites/berkeley/verify/verify_4.py | 73 + sites/berkeley/verify/verify_6.py | 71 + sites/berkeley/verify/verify_7.py | 98 ++ sites/berkeley/verify/verify_lib.py | 1196 +++++++++++++++++ 48 files changed, 5840 insertions(+) create mode 100644 sites/berkeley/verify/ground_truth.py create mode 100644 sites/berkeley/verify/tests/_support.py create mode 100644 sites/berkeley/verify/tests/test_verify_1.py create mode 100644 sites/berkeley/verify/tests/test_verify_10.py create mode 100644 sites/berkeley/verify/tests/test_verify_11.py create mode 100644 sites/berkeley/verify/tests/test_verify_12.py create mode 100644 sites/berkeley/verify/tests/test_verify_13.py create mode 100644 sites/berkeley/verify/tests/test_verify_14.py create mode 100644 sites/berkeley/verify/tests/test_verify_16.py create mode 100644 sites/berkeley/verify/tests/test_verify_17.py create mode 100644 sites/berkeley/verify/tests/test_verify_19.py create mode 100644 sites/berkeley/verify/tests/test_verify_2.py create mode 100644 sites/berkeley/verify/tests/test_verify_20.py create mode 100644 sites/berkeley/verify/tests/test_verify_22.py create mode 100644 sites/berkeley/verify/tests/test_verify_23.py create mode 100644 sites/berkeley/verify/tests/test_verify_24.py create mode 100644 sites/berkeley/verify/tests/test_verify_25.py create mode 100644 sites/berkeley/verify/tests/test_verify_27.py create mode 100644 sites/berkeley/verify/tests/test_verify_28.py create mode 100644 sites/berkeley/verify/tests/test_verify_30.py create mode 100644 sites/berkeley/verify/tests/test_verify_31.py create mode 100644 sites/berkeley/verify/tests/test_verify_4.py create mode 100644 sites/berkeley/verify/tests/test_verify_6.py create mode 100644 sites/berkeley/verify/tests/test_verify_7.py create mode 100644 sites/berkeley/verify/tests/test_verify_lib.py create mode 100644 sites/berkeley/verify/verify_1.py create mode 100644 sites/berkeley/verify/verify_10.py create mode 100644 sites/berkeley/verify/verify_11.py create mode 100644 sites/berkeley/verify/verify_12.py create mode 100644 sites/berkeley/verify/verify_13.py create mode 100644 sites/berkeley/verify/verify_14.py create mode 100644 sites/berkeley/verify/verify_16.py create mode 100644 sites/berkeley/verify/verify_17.py create mode 100644 sites/berkeley/verify/verify_19.py create mode 100644 sites/berkeley/verify/verify_2.py create mode 100644 sites/berkeley/verify/verify_20.py create mode 100644 sites/berkeley/verify/verify_22.py create mode 100644 sites/berkeley/verify/verify_23.py create mode 100644 sites/berkeley/verify/verify_24.py create mode 100644 sites/berkeley/verify/verify_25.py create mode 100644 sites/berkeley/verify/verify_27.py create mode 100644 sites/berkeley/verify/verify_28.py create mode 100644 sites/berkeley/verify/verify_30.py create mode 100644 sites/berkeley/verify/verify_31.py create mode 100644 sites/berkeley/verify/verify_4.py create mode 100644 sites/berkeley/verify/verify_6.py create mode 100644 sites/berkeley/verify/verify_7.py create mode 100644 sites/berkeley/verify/verify_lib.py diff --git a/sites/berkeley/verify/ground_truth.py b/sites/berkeley/verify/ground_truth.py new file mode 100644 index 000000000..3725ce219 --- /dev/null +++ b/sites/berkeley/verify/ground_truth.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python3 +"""Re-derive every UC Berkeley task target from a supplied initial SQLite snapshot. + +``task_ground_truth(db, n)`` selects the target exactly the way the task text and +the app do — the frozen benchmark clock (``BENCHMARK_NOW``), ``PER_PAGE``, the +app's ``ORDER BY`` clauses and its unordered ``LIMIT 3`` related-centres query — +using plain SQL. No answer constant is frozen anywhere except the snapshot +catalog fingerprint in ``verify_lib.py``; a re-frozen seed is caught there +before grading starts, and any derivation that cannot find its target raises +``ValueError`` so the verifier fails closed instead of guessing. + +Two rows (11, 17) read values that the app renders from tracked source rather +than from the DB; their derivations parse ``templates/admissions.html`` and +``app.py`` respectively, and ``verify/tests`` asserts those values stay +source-literals (never DB-derived). +""" +from __future__ import annotations + +import datetime as _dt +import re +import sqlite3 +import sys +from pathlib import Path +from typing import Any, Iterable, Sequence + +VERIFY_DIR = Path(__file__).resolve().parent +SITE_DIR = VERIFY_DIR.parent +TEMPLATES_DIR = SITE_DIR / "templates" +APP_SOURCE = SITE_DIR / "app.py" + +# app.py:48 — the frozen clock every "upcoming" view is compared against. +BENCHMARK_NOW = "2026-05-12 00:00:00.000000" +BENCHMARK_NOW_DATE = _dt.date(2026, 5, 12) + +# app.py:39 — the listing page size. +PER_PAGE = 20 + +# The AI-family allowlist for task 7. A *rule*, not an answer: it selects the +# EECS rows whose stated interests are AI-related, and the verifier still binds +# the quoted interests to the named row. +AI_INTEREST_RE = re.compile( + r"\b(?:ai|artificial intelligence|machine learning|deep learning|" + r"reinforcement learning|robot|robotics)\b", + re.I, +) + +# Title pattern that yields (subject, award) from an award-reporting headline. +AWARD_TITLE_RE = re.compile(r"^(?P.+?)\s+Receives?\s+(?P.+?)\s*$") + + +def _connect(db_path: str | Path) -> sqlite3.Connection: + connection = sqlite3.connect(str(db_path)) + connection.row_factory = sqlite3.Row + return connection + + +def _rows(connection: sqlite3.Connection, sql: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: + return [dict(row) for row in connection.execute(sql, params)] + + +def _one(rows: Iterable[dict[str, Any]], description: str) -> dict[str, Any]: + items = list(rows) + if len(items) != 1: + raise ValueError(f"{description} must be unique; observed {len(items)} rows") + return items[0] + + +def _at_least(rows: Iterable[dict[str, Any]], count: int, description: str) -> list[dict[str, Any]]: + items = list(rows) + if len(items) < count: + raise ValueError(f"{description} needs at least {count} rows; observed {len(items)}") + return items + + +# --------------------------------------------------------------------------- # +# Catalog replicas of the app's own queries +# --------------------------------------------------------------------------- # +def _programmes(connection: sqlite3.Connection, where: str = "1=1", params: Sequence[Any] = ()) -> list[dict[str, Any]]: + """``ORDER BY Program.name`` — the ordering of /programs (app.py:368).""" + return _rows( + connection, + "SELECT p.*, c.name AS college_name, d.name AS department_name " + "FROM programs p LEFT JOIN colleges c ON c.id = p.college_id " + "LEFT JOIN departments d ON d.id = p.department_id " + f"WHERE {where} ORDER BY p.name", + params, + ) + + +def programme_by_slug(connection: sqlite3.Connection, slug: str) -> dict[str, Any]: + return _one(_programmes(connection, "p.slug = ?", (slug,)), f"programme slug {slug!r}") + + +def _events( + connection: sqlite3.Connection, + where: str = "1=1", + params: Sequence[Any] = (), + *, + upcoming: bool = True, +) -> list[dict[str, Any]]: + """``/events`` semantics: ``upcoming`` filters ``start_datetime >= BENCHMARK_NOW``.""" + clause = "start_datetime >= ?" if upcoming else "1=1" + clauses = [where, clause] + values: list[Any] = list(params) + ([BENCHMARK_NOW] if upcoming else []) + return _rows( + connection, + f"SELECT * FROM events WHERE {' AND '.join(clauses)} ORDER BY start_datetime", + values, + ) + + +def _departments(connection: sqlite3.Connection, college_id: int) -> list[dict[str, Any]]: + """``ORDER BY Department.name`` — the ordering of /departments (app.py:482).""" + return _rows( + connection, + "SELECT * FROM departments WHERE college_id = ? ORDER BY name", + (college_id,), + ) + + +def _faculty_of_department(connection: sqlite3.Connection, department_id: int) -> list[dict[str, Any]]: + """``ORDER BY Faculty.name`` — the ordering of /faculty (app.py:587).""" + return _rows( + connection, + "SELECT * FROM faculty WHERE department_id = ? ORDER BY name", + (department_id,), + ) + + +def related_centres(connection: sqlite3.Connection, centre: dict[str, Any]) -> list[dict[str, Any]]: + """The centres the detail page renders: ``LIMIT 3`` with **no ORDER BY** (app.py:469-472).""" + return _rows( + connection, + "SELECT * FROM research_centers WHERE college_id = ? AND id != ? LIMIT 3", + (centre["college_id"], centre["id"]), + ) + + +def centre_by_slug(connection: sqlite3.Connection, slug: str) -> dict[str, Any]: + return _one( + _rows(connection, "SELECT * FROM research_centers WHERE slug = ?", (slug,)), + f"research centre slug {slug!r}", + ) + + +def _college_by_slug(connection: sqlite3.Connection, slug: str) -> dict[str, Any]: + return _one(_rows(connection, "SELECT * FROM colleges WHERE slug = ?", (slug,)), f"college slug {slug!r}") + + +# --------------------------------------------------------------------------- # +# Source-rendered facts (not in the DB) +# --------------------------------------------------------------------------- # +def admissions_facts() -> dict[str, str]: + """The two Admissions figures, parsed from the tracked template. + + ``templates/admissions.html`` renders ``Freshman Application | November 30`` + and a ``stat-number`` acceptance rate labelled ``Acceptance Rate``. The + numbers are source literals (not DB rows), so the parser must find them or + the verifier fails closed. + """ + html = (TEMPLATES_DIR / "admissions.html").read_text(encoding="utf-8") + deadline = re.search(r"\s*Freshman Application\s*\s*\s*([^<]+?)\s*", html) + if not deadline: + raise ValueError("admissions.html no longer renders the Freshman Application deadline") + rate = re.search( + r'stat-number">\s*([\d.]+)\s*%\s*\s*\s*Acceptance Rate', + html, + ) + if not rate: + raise ValueError("admissions.html no longer renders a labelled Acceptance Rate stat") + return {"deadline": deadline.group(1).strip(), "acceptance_rate": f"{rate.group(1)}%"} + + +def about_facts() -> dict[str, int]: + """The three About-page statistics, parsed from ``app.py``'s ``about()`` dict.""" + source = APP_SOURCE.read_text(encoding="utf-8") + body = re.search(r"def about\(\):(.*?)\n@app\.route", source, re.S) + if not body: + raise ValueError("app.py no longer defines about()") + facts: dict[str, int] = {} + for key in ("nobel_laureates", "varsity_sports", "national_titles"): + match = re.search(rf"'{key}':\s*(\d+)", body.group(1)) + if not match: + raise ValueError(f"about() no longer defines {key!r} as an integer literal") + facts[key] = int(match.group(1)) + return facts + + +def about_distractor_prizes() -> int: + """The near-miss Nobel count the About page prints next to the faculty statistic.""" + html = (TEMPLATES_DIR / "about.html").read_text(encoding="utf-8") + match = re.search(r"more than\s+(\d+)\s+Nobel Prizes", html) + if not match: + raise ValueError("about.html no longer renders the 'more than N Nobel Prizes' distractor") + return int(match.group(1)) + + +# --------------------------------------------------------------------------- # +# Per-task derivation +# --------------------------------------------------------------------------- # +def requirement_items(requirements: str) -> list[str]: + """The comma/semicolon-separated requirement items of a programme detail page.""" + items: list[str] = [] + for raw in re.split(r"[;,]", str(requirements or "")): + item = re.sub(r"^\s*plus\s+", "", raw.strip()).strip(" .") + if len(item) >= 2: + items.append(item) + return items + + +def _task(db_path: str | Path, n: int) -> dict[str, Any]: + connection = _connect(db_path) + try: + return _derive(connection, n) + finally: + connection.close() + + +def task_ground_truth(db_path: str | Path, n: int) -> dict[str, Any]: + return _task(db_path, int(n)) + + +def _derive(c: sqlite3.Connection, n: int) -> dict[str, Any]: + if n == 1: + program = _one(_programmes(c, "p.degree_type = 'MBA'"), "the MBA programme") + return { + "task": n, + "program": program, + "college": program["college_name"], + "duration_years": float(program["duration_years"]), + } + + if n == 2: + target = programme_by_slug(c, "computer-science-bs") + items = requirement_items(target["requirements"]) + if len(items) < 6: + raise ValueError(f"task 2 needs a populated BS requirements list; observed {items!r}") + target_tokens = {token for item in items for token in re.findall(r"[a-z0-9]+", item.lower())} + foreign: list[str] = [] + for sibling in _programmes(c, "p.name = ? AND p.slug != ?", ("Computer Science", target["slug"])): + for item in requirement_items(sibling["requirements"]): + tokens = re.findall(r"[a-z0-9]+", item.lower()) + if len(tokens) >= 2 and not (set(tokens) & target_tokens): + foreign.append(item) + if not foreign: + raise ValueError("task 2 needs sibling requirement items that are absent from the BS list") + return {"task": n, "program": target, "items": items, "foreign_items": sorted(set(foreign))} + + if n == 4: + candidates = _rows( + c, + "SELECT * FROM news_articles WHERE title LIKE '%CRISPR%' OR content LIKE '%CRISPR%' " + "OR tags LIKE '%CRISPR%' ORDER BY published_date DESC", + ) + _at_least(candidates, 2, "task 4 CRISPR articles") + target = _one( + [row for row in candidates if AWARD_TITLE_RE.fullmatch(row["title"])], + "task 4 award-reporting article", + ) + match = AWARD_TITLE_RE.fullmatch(target["title"]) + subject_tokens = re.findall(r"[A-Z][a-z]+", match.group("subject")) + if len(subject_tokens) < 2: + raise ValueError(f"task 4 cannot derive a person from {target['title']!r}") + return { + "task": n, + "article": target, + "person": " ".join(subject_tokens[-2:]), + "award": match.group("award"), + "candidates": candidates, + } + + if n == 6: + upcoming = _events(c, "category = 'Lecture'") + every = _events(c, "category = 'Lecture'", upcoming=False) + _at_least(upcoming, 3, "task 6 upcoming Lecture events") + return {"task": n, "events": every, "upcoming": upcoming} + + if n == 7: + department = _one(_rows(c, "SELECT * FROM departments WHERE slug = 'eecs'"), "the EECS department") + members = _faculty_of_department(c, department["id"]) + if len(members) < 10: + raise ValueError(f"task 7 needs the full EECS roster; observed {len(members)}") + allowed = [row for row in members if AI_INTEREST_RE.search(row["research_interests"] or "")] + if len(allowed) < 5: + raise ValueError(f"task 7 needs at least five AI-family EECS faculty; observed {len(allowed)}") + return {"task": n, "department": department, "members": members, "allowed": allowed} + + if n == 10: + centre = centre_by_slug(c, "bair") + return {"task": n, "centre": centre, "director": centre["director"], "founded_year": int(centre["founded_year"])} + + if n == 11: + return {"task": n, **admissions_facts()} + + if n == 12: + college = _college_by_slug(c, "haas-business") + programmes = _programmes(c, "p.college_id = ?", (college["id"],)) + if len(programmes) != 1: + raise ValueError(f"task 12 expects the single-programme Haas catalogue; observed {len(programmes)}") + catalogue_types = sorted({row["degree_type"] for row in _programmes(c)}) + offered = {programmes[0]["degree_type"]} + return { + "task": n, + "college": college, + "programmes": programmes, + "offered_types": sorted(offered), + "other_types": [value for value in catalogue_types if value not in offered], + } + + if n == 13: + department = _one(_rows(c, "SELECT * FROM departments WHERE slug = 'eecs'"), "the EECS department") + return {"task": n, "department": department, "chair": department["chair"], "location": department["location"]} + + if n == 14: + college = _college_by_slug(c, "engineering") + return { + "task": n, + "college": college, + "dean": college["dean"], + "undergrad_count": int(college["undergrad_count"]), + "grad_count": int(college["grad_count"]), + } + + if n == 16: + online = _programmes(c, "p.is_online = 1") + programme = _one(online, "the single online programme") + others = [row["name"] for row in _programmes(c, "p.slug != ?", (programme["slug"],))] + return {"task": n, "program": programme, "others": others} + + if n == 17: + return {"task": n, **about_facts(), "distractor_nobel_prizes": about_distractor_prizes()} + + if n == 19: + articles = _rows(c, "SELECT * FROM news_articles WHERE category = 'Athletics' ORDER BY published_date DESC") + _at_least(articles, 5, "task 19 Athletics articles") + championships = [row for row in articles if "championship" in (row["title"] or "").lower()] + if len(championships) < 2: + raise ValueError(f"task 19 needs at least two championship articles; observed {len(championships)}") + return {"task": n, "articles": articles, "championships": championships} + + if n == 20: + program = programme_by_slug(c, "juris-doctor-jd") + return { + "task": n, + "program": program, + "duration_years": float(program["duration_years"]), + "deadline": program["application_deadline"], + "college": program["college_name"], + } + + if n == 22: + college = _college_by_slug(c, "letters-and-science") + departments = _departments(c, college["id"]) + if len(departments) < 4: + raise ValueError(f"task 22 needs the L&S department list; observed {len(departments)}") + return {"task": n, "college": college, "departments": departments} + + if n == 23: + centre = centre_by_slug(c, "bids") + focus = [part.strip() for part in (centre["focus_areas"] or "").split(",") if part.strip()] + related = related_centres(c, centre) + if len(focus) < 3 or not related: + raise ValueError("task 23 needs BIDS focus areas and its rendered related centres") + return { + "task": n, + "centre": centre, + "focus_areas": focus, + "related": related, + "related_names": [row["name"] for row in related], + } + + if n == 24: + program = programme_by_slug(c, "economics-phd") + if not program["department_id"]: + raise ValueError("task 24 needs the Economics PhD to carry a department") + department = _one( + _rows(c, "SELECT * FROM departments WHERE id = ?", (program["department_id"],)), + "the Economics department", + ) + programmes = _programmes(c, "p.department_id = ?", (department["id"],)) + members = _faculty_of_department(c, department["id"]) + if len(programmes) < 2 or len(members) < 3: + raise ValueError("task 24 needs the department's programme list and faculty roster") + return { + "task": n, + "program": program, + "department": department, + "programmes": programmes, + "members": members, + "chair": department["chair"], + } + + if n == 25: + career = _events(c, "category = 'Career'") + _at_least(career, 3, "task 25 upcoming Career events") + anchor = _one([row for row in career if "career fair" in (row["title"] or "").lower()], "the Spring Career Fair") + return {"task": n, "anchor": anchor, "others": [row for row in career if row["id"] != anchor["id"]]} + + if n == 27: + meng = programme_by_slug(c, "master-of-engineering-meng") + ms = programme_by_slug(c, "computer-science-ms") + if meng["department_id"] != ms["department_id"]: + raise ValueError("task 27 expects both programmes in the same department") + return { + "task": n, + "meng": meng, + "ms": ms, + "department": meng["department_name"], + "durations": (float(meng["duration_years"]), float(ms["duration_years"])), + } + + if n == 28: + gre = _programmes(c, "p.gre_required = 1") + _at_least(gre, 10, "task 28 GRE-required programmes") + counts: dict[str, int] = {} + for row in gre: + counts[row["degree_type"]] = counts.get(row["degree_type"], 0) + 1 + top = sorted(counts.items(), key=lambda item: (-item[1], item[0])) + if len(top) > 1 and top[0][1] == top[1][1]: + raise ValueError("task 28 degree-type mode is tied") + return {"task": n, "count": len(gre), "by_degree": counts, "most_common_degree": top[0][0]} + + if n == 30: + centre = centre_by_slug(c, "seismo-lab") + return {"task": n, "centre": centre, "director": centre["director"]} + + if n == 31: + first = centre_by_slug(c, "msri") + second = centre_by_slug(c, "cpl") + return {"task": n, "first": first, "second": second, "directors": (first["director"], second["director"])} + + raise ValueError(f"unsupported UC Berkeley task {n}") + + +def all_ground_truth(db_path: str | Path) -> dict[int, dict[str, Any]]: + return {number: task_ground_truth(db_path, number) + for number in (1, 2, 4, 6, 7, 10, 11, 12, 13, 14, 16, 17, 19, 20, 22, 23, 24, 25, 27, 28, 30, 31)} + + +if __name__ == "__main__": # pragma: no cover - manual inspection + import json + + facts = all_ground_truth(sys.argv[1]) + print(json.dumps(facts, indent=1, default=str)) diff --git a/sites/berkeley/verify/tests/_support.py b/sites/berkeley/verify/tests/_support.py new file mode 100644 index 000000000..9f2d506fa --- /dev/null +++ b/sites/berkeley/verify/tests/_support.py @@ -0,0 +1,414 @@ +"""Shared fixtures for the UC Berkeley verifier tests. + +Snapshots are copies of the frozen seed (``instance_seed/berkeley.db``) with the +``bookmarks`` table rewritten from a small in-memory ``State`` using **stdlib +sqlite3 only**; hand-written trajectories follow the ``agent_demo/agent.py`` +run signature. No docker, no LLM, no Flask. + +Every fixture DB reproduces the pinned catalog fingerprint — the suite asserts +it, so a stale or tampered seed fails here rather than inside a verifier. +""" +from __future__ import annotations + +import base64 +import copy +import io +import json +import os +import shutil +import sqlite3 +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from typing import Any + +VERIFY_DIR = Path(__file__).resolve().parents[1] +SITE_DIR = VERIFY_DIR.parent +SEED_DB = SITE_DIR / "instance_seed" / "berkeley.db" +sys.path.insert(0, str(VERIFY_DIR)) + +import verify_lib # noqa: E402 (path inserted above) + +BASE = "http://localhost:41026" +PASSWORD = "test1234" +STAMP = "2026-05-12 00:00:00.000000" +ALL_TABLES = verify_lib.ALL_TABLES + + +def _build_seed() -> None: + """Build the deterministic seed when the worktree has not generated it yet.""" + try: + subprocess.run( + [sys.executable, str(SITE_DIR / "seed_data.py")], cwd=SITE_DIR, + env={**os.environ, "PYTHONHASHSEED": "0"}, check=True, + capture_output=True, text=True, timeout=300, + ) + except subprocess.CalledProcessError as exc: + raise RuntimeError( + f"frozen seed missing at {SEED_DB} and the automatic build failed " + f"(rc={exc.returncode}).\nstdout: {(exc.stdout or '')[-2000:]}\n" + f"stderr: {(exc.stderr or '')[-2000:]}\n" + f"Build it manually with: cd {SITE_DIR} && PYTHONHASHSEED=0 python seed_data.py" + ) from exc + except Exception as exc: # noqa: BLE001 + raise RuntimeError( + f"frozen seed missing at {SEED_DB} and could not be built automatically: {exc}. " + f"Build it with: cd {SITE_DIR} && PYTHONHASHSEED=0 python seed_data.py" + ) from exc + + +if not SEED_DB.exists(): # pragma: no cover - environment guard + _build_seed() + + +def seed_fingerprint() -> str: + return verify_lib.catalog_fingerprint(str(SEED_DB)) + + +class State: + """Mutable copy of the seed's runtime table (bookmarks) for an after snapshot.""" + + def __init__(self) -> None: + self.bookmarks: list[dict[str, Any]] = [] + self.extra_sql: list[str] = [] + + # -- mutators ----------------------------------------------------------- + def add_bookmark(self, user_id: int, item_type: str, item_id: int, + row_id: int | None = None, note: str = "") -> dict[str, Any]: + """Insert a row the way SQLite does: id = max(existing id) + 1 unless pinned.""" + if row_id is None: + row_id = max([int(row["id"]) for row in self.bookmarks] + [0]) + 1 + row = {"id": int(row_id), "user_id": int(user_id), "item_type": str(item_type), + "item_id": int(item_id), "note": note} + self.bookmarks.append(row) + return row + + def remove_bookmark(self, row_id: int) -> None: + before = len(self.bookmarks) + self.bookmarks = [row for row in self.bookmarks if int(row["id"]) != int(row_id)] + assert len(self.bookmarks) == before - 1, f"no bookmark row {row_id}" + + # -- persistence -------------------------------------------------------- + def write(self, path: Path) -> Path: + shutil.copy2(SEED_DB, path) + connection = sqlite3.connect(path) + try: + connection.execute("DELETE FROM bookmarks") + connection.executemany( + "INSERT INTO bookmarks(id, user_id, item_type, item_id, note, created_at) " + "VALUES (:id, :user_id, :item_type, :item_id, :note, :stamp)", + [{**row, "stamp": STAMP} for row in self.bookmarks], + ) + for statement in self.extra_sql: + connection.execute(statement) + connection.commit() + finally: + connection.close() + return path + + def write_with_catalog_change(self, path: Path) -> Path: + """A tampered snapshot: catalog row edited (fingerprint / immutability drift).""" + self.write(path) + connection = sqlite3.connect(path) + try: + connection.execute("UPDATE programs SET name = name || ' (tampered)' WHERE id = 1") + connection.commit() + finally: + connection.close() + return path + + +def write_schema_drifted(path: Path) -> Path: + """A snapshot with an unexpected table (schema drift).""" + shutil.copy2(SEED_DB, path) + connection = sqlite3.connect(path) + try: + connection.execute("CREATE TABLE extra_drift (id INTEGER PRIMARY KEY)") + connection.commit() + finally: + connection.close() + return path + + +def step(path: str, action: str = "click", text: str | None = None) -> dict[str, Any]: + """One trajectory step in the agent.py shape; ``path`` is relative to BASE.""" + params: dict[str, Any] = {"text": text} if text is not None else {} + url = path if path.startswith("http") else f"{BASE}{path}" + return {"url": url, "action": action, "params": params} + + +def login_steps(email: str) -> list[dict[str, Any]]: + return [ + step("/login", "input", email), + step("/login", "input", PASSWORD), + step("/login", "click"), + ] + + +def only_paths(steps: list[dict[str, Any]], *allowed: str) -> list[dict[str, Any]]: + """Keep the steps whose URL path is one of ``allowed`` (shortcut trajectories).""" + from urllib.parse import urlparse + + def path_of(item: dict[str, Any]) -> str: + return urlparse(item["url"]).path.rstrip("/") or "/" + + return [item for item in steps if path_of(item) in allowed] + + +def _fixture_png() -> bytes: + """A real 640x480 PNG (not a 1x1 stub) so screenshot-size gates are exercised.""" + from PIL import Image + + buffer = io.BytesIO() + Image.new("RGB", (640, 480), (245, 246, 250)).save(buffer, format="PNG") + return buffer.getvalue() + + +FIXTURE_PNG = _fixture_png() +SMALL_PNG = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" +) +CORRUPT_PNG = b"this is not a portable network graphic" + + +def write_run( + run_dir: Path, + task_id: str, + steps: list[dict[str, Any]], + answer: str | None = None, + *, + initial: State | None = None, + after: State | None = None, + snapshots: bool = True, + after_snapshot: bool = True, + small_at: int | None = None, + corrupt_at: int | None = None, + terminated: bool | None = None, + termination_reason: str | None = None, + start_url: str = f"{BASE}/", + drifted_initial: bool = False, + schema_drifted_after: bool = False, + catalog_changed_after: bool = False, +) -> Path: + """Write a run directory carrying the full agent.py signature. + + ``small_at`` / ``corrupt_at`` degrade one step's screenshots; ``snapshots`` / + ``after_snapshot`` omit ``initial.db`` / ``after.db``; ``drifted_initial``, + ``schema_drifted_after`` and ``catalog_changed_after`` tamper with them. + """ + run_dir.mkdir(parents=True, exist_ok=True) + shots = run_dir / "screenshots" + shots.mkdir(exist_ok=True) + numbered = [] + for index, item in enumerate(steps): + before = f"step_{index:03d}.png" + after_name = f"step_{index + 1:03d}.png" + payload = FIXTURE_PNG + if small_at is not None and index >= small_at: + payload = SMALL_PNG + for name in (before, after_name): + (shots / name).write_bytes(payload) + numbered.append({ + "step": index, + "url": item["url"], + "title": "synthetic", + "thought": "synthetic step", + "action": item.get("action", "click"), + "params": item.get("params", {}), + "screenshot_before": before, + "screenshot_after": after_name, + }) + if corrupt_at is not None: + # Written after the loop: the next step's ``before`` screenshot has the + # same file name as this step's ``after`` one. + (shots / f"step_{corrupt_at + 1:03d}.png").write_bytes(CORRUPT_PNG) + trajectory = { + "task": "synthetic", + "task_id": task_id, + "start_url": start_url, + "model": "unit-test", + "max_steps": 30, + "steps": numbered, + "terminated": bool(answer) if terminated is None else bool(terminated), + "termination_reason": ( + ("agent_done" if answer else "max_steps") if termination_reason is None else termination_reason + ), + "final_answer": answer if answer else None, + "judge_rubric": "", + "verifier_path": "", + } + (run_dir / "trajectory.json").write_text(json.dumps(trajectory, indent=2), encoding="utf-8") + + if snapshots: + target = run_dir / "initial.db" + if drifted_initial: + (initial or State()).write_with_catalog_change(target) + else: + (initial or State()).write(target) + if after_snapshot: + target = run_dir / "after.db" + if schema_drifted_after: + write_schema_drifted(target) + elif catalog_changed_after: + (after or State()).write_with_catalog_change(target) + else: + (after or State()).write(target) + return run_dir + + +def run_verifier(n: int, run_dir: Path, *, container: str | None = None) -> tuple[dict[str, Any], int]: + """Invoke verify_.py as a subprocess and return (verdict, returncode).""" + command = [sys.executable, str(VERIFY_DIR / f"verify_{n}.py"), "--run_dir", str(run_dir)] + if container: + command += ["--container", container] + result = subprocess.run(command, capture_output=True, text=True, cwd=str(VERIFY_DIR)) + try: + verdict = json.loads(result.stdout) + except Exception: # noqa: BLE001 - an unparseable verifier is a failure, never a crash of the suite + verdict = { + "task_id": None, + "pass": False, + "reason": f"unparseable verifier output: stdout={result.stdout[-600:]!r} " + f"stderr={result.stderr[-800:]!r}", + "evidence": [], + } + return verdict, result.returncode + + +class VerifierTestCase(unittest.TestCase): + """Base class: ``self.N`` selects verify_N.py.""" + + N = -1 + + @property + def task_id(self) -> str: + return f"UC Berkeley--{self.N}" + + def setUp(self) -> None: + self._tmp = Path(tempfile.mkdtemp(prefix=f"bk_verify_{self.N}_")) + self.addCleanup(shutil.rmtree, str(self._tmp), True) + + def make_run(self, name: str = "run", **kwargs: Any) -> Path: + return write_run(self._tmp / name, kwargs.pop("task_id", self.task_id), **kwargs) + + def verdict(self, steps: list[dict[str, Any]], answer: str | None, **kwargs: Any) -> dict[str, Any]: + run_dir = self.make_run(steps=steps, answer=answer, **kwargs) + verdict, _ = run_verifier(self.N, run_dir, container="wh-berkeley-test-none") + return verdict + + # -- assertions --------------------------------------------------------- + def assertPasses(self, verdict: dict[str, Any]) -> None: + self.assertTrue( + verdict.get("pass"), + f"expected PASS; reason={verdict.get('reason')!r} evidence={verdict.get('evidence')!r}", + ) + + def assertFailsOn(self, verdict: dict[str, Any], check: str) -> None: + self.assertFalse(verdict.get("pass"), f"expected FAIL on {check!r}; verdict passed") + self.assertEqual( + verdict.get("reason"), check, + f"expected first failing check {check!r}; got {verdict.get('reason')!r}; " + f"evidence={verdict.get('evidence')!r}", + ) + + def assertFailsClosed(self, verdict: dict[str, Any], reason: str) -> None: + self.assertFalse(verdict.get("pass"), "expected a fail-closed verdict") + self.assertTrue(verdict.get("infra_error"), f"expected infra_error; got {verdict!r}") + self.assertEqual(verdict.get("reason"), reason, f"got {verdict.get('reason')!r}") + + +class SharedVerifierTests: + """Generic mutations every verifier must survive. + + Mixed into each per-task case *before* ``VerifierTestCase``; the concrete + class supplies ``GENUINE_STEPS`` / ``ANSWER`` / ``genuine_after``. + """ + + GENUINE_STEPS: list[dict[str, Any]] = [] + ANSWER = "" + + def genuine_after(self) -> State: + return State() + + # -- packaging ---------------------------------------------------------- + def test_no_op_fails_on_empty_answer(self) -> None: + verdict = self.verdict([step("/"), step("/", "done")], None) + self.assertFailsOn(verdict, "final_answer_nonempty") + + def test_wrong_task_id_fails(self) -> None: + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, task_id="UC Berkeley--999") + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_another_tasks_trajectory_fails(self) -> None: + other_id = "UC Berkeley--1" if self.N != 1 else "UC Berkeley--2" + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, task_id=other_id) + self.assertFailsOn(verdict, "trajectory_task_matches") + + def test_truncated_trajectory_fails(self) -> None: + """Steps sliced before the gates, with the run still claiming a final answer.""" + sliced = self.GENUINE_STEPS[: max(1, len(self.GENUINE_STEPS) // 2)] + verdict = self.verdict( + sliced, self.ANSWER, terminated=False, termination_reason="max_steps", + ) + self.assertFailsOn(verdict, "trajectory_completed") + + def test_max_steps_run_without_answer_fails(self) -> None: + verdict = self.verdict( + self.GENUINE_STEPS, None, terminated=False, termination_reason="max_steps", + ) + self.assertFailsOn(verdict, "final_answer_nonempty") + + def test_corrupt_screenshot_fails(self) -> None: + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, corrupt_at=0) + self.assertFailsOn(verdict, "screenshots_decode") + + def test_1x1_screenshot_fails(self) -> None: + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, small_at=0) + self.assertFailsOn(verdict, "screenshots_decode") + + # -- snapshots ---------------------------------------------------------- + def test_missing_after_db_fails_closed(self) -> None: + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, after_snapshot=False) + self.assertFailsClosed(verdict, "database_unavailable") + + def test_catalog_fingerprint_drift_fails_closed(self) -> None: + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, drifted_initial=True) + self.assertFailsClosed(verdict, "snapshot_contract_invalid") + + def test_schema_drift_fails_closed(self) -> None: + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, schema_drifted_after=True) + self.assertFailsClosed(verdict, "snapshot_contract_invalid") + + def test_catalog_mutation_in_after_fails_closed(self) -> None: + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, catalog_changed_after=True) + self.assertFailsClosed(verdict, "snapshot_contract_invalid") + + # -- shortcuts and writes ---------------------------------------------- + def test_catalog_wide_search_shortcut_fails(self) -> None: + shortcut = [step("/"), step("/search?q=california&page=1", "done")] + verdict = self.verdict(shortcut, self.ANSWER, after=self.genuine_after()) + self.assertFalse( + verdict.get("pass"), + f"a catalog-wide search must not satisfy any gate; reason={verdict.get('reason')!r}", + ) + + def test_incidental_write_fails(self) -> None: + after = self.genuine_after() + after.add_bookmark(1, "program", 1) + verdict = self.verdict(self.GENUINE_STEPS, self.ANSWER, after=after) + self.assertFalse( + verdict.get("pass"), + f"an incidental bookmark write must fail; reason={verdict.get('reason')!r}", + ) + self.assertIn( + str(verdict.get("reason")), + { + "read_only_bookmarks_unchanged", + "bookmarks_exact_delta", + "bookmarks_other_users_unchanged", + "bookmarks_surviving_row_ids", + }, + f"unexpected failing check for an incidental write: {verdict.get('reason')!r}", + ) diff --git a/sites/berkeley/verify/tests/test_verify_1.py b/sites/berkeley/verify/tests/test_verify_1.py new file mode 100644 index 000000000..5efa2ff2b --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_1.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/programs?q=MBA"), + step("/programs/business-administration-mba", "done"), +] +ANSWER = "The Haas School of Business offers the MBA; it takes 2 years." + + +class VerifyTask1Tests(SharedVerifierTests, VerifierTestCase): + N = 1 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_degree_filter_also_satisfies_search_gate(self) -> None: + steps = [step("/"), step("/programs?degree=MBA"), step("/programs/business-administration-mba", "done")] + self.assertPasses(self.verdict(steps, ANSWER)) + + def test_shortcut_without_detail_fails_on_gate(self) -> None: + steps = [step("/"), step("/programs?q=MBA", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_detail_business-administration-mba") + + def test_shortcut_without_listing_fails_on_search_gate(self) -> None: + steps = [step("/"), step("/programs/business-administration-mba", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_search") + + def test_wrong_school_fails(self) -> None: + answer = "The School of Law offers the MBA; it takes 2 years." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_school") + + def test_wrong_duration_fails(self) -> None: + answer = "The Haas School of Business offers the MBA; it takes 3 years." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_duration") + + def test_negated_school_fails(self) -> None: + answer = "The MBA is not offered by the Haas School of Business; it takes 2 years." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_school") + + def test_alternative_phrasing_passes(self) -> None: + answer = "Berkeley's MBA programme is run by the Haas School of Business and lasts two years." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 41) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_10.py b/sites/berkeley/verify/tests/test_verify_10.py new file mode 100644 index 000000000..d35abc8f0 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_10.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/research/bair", "done")] +ANSWER = "BAIR was founded in 2013 and is directed by Prof. Pieter Abbeel." + + +class VerifyTask10Tests(SharedVerifierTests, VerifierTestCase): + N = 10 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_from_listing_fails(self) -> None: + steps = [step("/"), step("/research", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_research_detail_bair") + + def test_real_world_founding_year_fails(self) -> None: + answer = "BAIR was founded in 2017 and is directed by Prof. Pieter Abbeel." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_founded_year") + + def test_wrong_director_fails(self) -> None: + answer = "BAIR was founded in 2013 and is directed by Prof. David Culler." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_director") + + def test_negated_year_fails(self) -> None: + answer = "BAIR is directed by Prof. Pieter Abbeel, but it was not founded in 2013." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_founded_year") + + def test_fully_negated_answer_fails_on_first_check(self) -> None: + answer = "BAIR was not founded in 2013." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_director") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "The Berkeley Artificial Intelligence Research Lab's director is Pieter Abbeel, and " + "it was established in 2013." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "research", 1) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_11.py b/sites/berkeley/verify/tests/test_verify_11.py new file mode 100644 index 000000000..00a8c9fa8 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_11.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/admissions", "done")] +ANSWER = "The freshman application deadline is November 30, and the acceptance rate is 14.4%." + + +class VerifyTask11Tests(SharedVerifierTests, VerifierTestCase): + N = 11 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_from_homepage_fails(self) -> None: + steps = [step("/"), step("/about", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_admissions_page") + + def test_transfer_or_graduate_deadline_fails(self) -> None: + answer = "The freshman deadline is December 1 and the acceptance rate is 11%." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_freshman_deadline") + + def test_wrong_rate_fails(self) -> None: + answer = "The freshman deadline is November 30 and the acceptance rate is 11%." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_acceptance_rate") + + def test_negated_deadline_fails(self) -> None: + answer = "The freshman deadline is not November 30; the acceptance rate is 14.4%." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_freshman_deadline") + + def test_alternative_phrasing_passes(self) -> None: + answer = "Freshmen apply by Nov. 30, and Berkeley's acceptance rate is 14.4 percent." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 1) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_12.py b/sites/berkeley/verify/tests/test_verify_12.py new file mode 100644 index 000000000..9b39560bb --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_12.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/programs?college=haas-business", "done")] +ANSWER = "The Haas School of Business offers a single program: the MBA in Business Administration." + + +class VerifyTask12Tests(SharedVerifierTests, VerifierTestCase): + N = 12 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_from_degree_filter_fails(self) -> None: + steps = [step("/"), step("/programs?degree=MBA", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_haas_programme_listing") + + def test_prior_knowledge_multi_program_fails(self) -> None: + answer = "Haas offers MBA, PhD, and MFE programs." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_programme") + + def test_extra_degree_type_near_haas_fails(self) -> None: + answer = ( + "The Haas School of Business offers the Business Administration MBA and a PhD in " + "Business." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_no_other_haas_degrees") + + def test_negated_programme_fails(self) -> None: + answer = "Haas does not offer the MBA in Business Administration." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_programme") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Only one degree type is available at the Haas School of Business: the MBA " + "(Business Administration)." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_other_schools_mention_does_not_trip_the_negative(self) -> None: + answer = ( + "The Haas School of Business offers only the MBA in Business Administration, while " + "other schools award the PhD." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 41) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_13.py b/sites/berkeley/verify/tests/test_verify_13.py new file mode 100644 index 000000000..6d3b826d6 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_13.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/departments"), step("/departments/eecs", "done")] +ANSWER = "The chair of EECS is Prof. James Demmel, and the department is located at 253 Cory Hall." + + +class VerifyTask13Tests(SharedVerifierTests, VerifierTestCase): + N = 13 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_from_listing_fails(self) -> None: + steps = [step("/"), step("/departments", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_department_detail") + + def test_real_world_chair_fails(self) -> None: + answer = "The EECS chair is Prof. Alexei Efros, in 253 Cory Hall." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + + def test_wrong_location_fails(self) -> None: + answer = "The chair of EECS is Prof. James Demmel, located in Soda Hall." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_location") + + def test_negated_chair_fails(self) -> None: + answer = "James Demmel is not the chair of EECS." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + + def test_alternative_phrasing_passes(self) -> None: + answer = "EECS is chaired by James Demmel; its location is Cory Hall." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "faculty", 1) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_14.py b/sites/berkeley/verify/tests/test_verify_14.py new file mode 100644 index 000000000..2d2a4e873 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_14.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/academics", "done")] +ANSWER = ( + "The College of Engineering enrolls 4,500 undergraduates and 3,200 graduate students; the " + "dean is Dean Tsu-Jae King Liu." +) + + +class VerifyTask14Tests(SharedVerifierTests, VerifierTestCase): + N = 14 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_from_about_page_fails(self) -> None: + steps = [step("/"), step("/about", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_academics_page") + + def test_university_wide_totals_fail(self) -> None: + answer = ( + "The College of Engineering enrolls 31,800 undergraduates and 12,000 graduate " + "students; the dean is Dean Tsu-Jae King Liu." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_undergrad_count") + + def test_wrong_dean_fails(self) -> None: + answer = "The College of Engineering enrolls 4,500 undergraduates and 3,200 graduate students." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_dean") + + def test_negated_dean_fails(self) -> None: + answer = ( + "The College of Engineering enrolls 4,500 undergraduates and 3,200 graduate " + "students; the dean is not Tsu-Jae King Liu." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_dean") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Engineering has 4500 undergrads and 3200 grad students, led by Dean Tsu-Jae King Liu." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 1) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_16.py b/sites/berkeley/verify/tests/test_verify_16.py new file mode 100644 index 000000000..bbcfff5f8 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_16.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/programs?page=3"), + step("/programs/data-science-ms", "done"), +] +ANSWER = "Only one program offers an online option: the Data Science MS from the School of Information." + + +class VerifyTask16Tests(SharedVerifierTests, VerifierTestCase): + N = 16 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_online_search_is_not_a_listing_visit(self) -> None: + steps = [step("/"), step("/search?q=online", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_programme_listing") + + def test_shortcut_without_detail_fails(self) -> None: + steps = [step("/"), step("/programs", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_detail_data-science-ms") + + def test_prior_knowledge_multiple_online_fails(self) -> None: + answer = ( + "Several programs can be completed online, including the Computer Science MS and " + "the Master of Engineering." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_programme") + + def test_second_online_program_fails(self) -> None: + answer = ( + "The Data Science MS from the School of Information is online, and so is the Civil " + "Engineering BS." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_no_other_online_programmes") + + def test_negated_programme_fails(self) -> None: + answer = "The Data Science MS is not online." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_programme") + + def test_alternative_phrasing_passes(self) -> None: + answer = "The Data Science MS (School of Information) is the single degree with an online option." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 26) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_17.py b/sites/berkeley/verify/tests/test_verify_17.py new file mode 100644 index 000000000..902de549a --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_17.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/about", "done")] +ANSWER = ( + "Berkeley has 12 Nobel Laureates on the faculty, 30 varsity sports, and 105 NCAA national " + "titles." +) + + +class VerifyTask17Tests(SharedVerifierTests, VerifierTestCase): + N = 17 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_without_about_page_fails(self) -> None: + steps = [step("/"), step("/", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_about_page") + + def test_prizes_line_misquoted_as_faculty_count_fails(self) -> None: + answer = "Berkeley has 107 Nobel Laureates on the faculty, 30 varsity sports, and 105 NCAA titles." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_nobel_laureates") + + def test_wrong_sports_count_fails(self) -> None: + answer = "Berkeley has 12 Nobel Laureates on the faculty, 32 varsity sports, and 105 NCAA titles." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_varsity_sports") + + def test_negated_count_fails(self) -> None: + answer = "Berkeley does not have 12 Nobel Laureates on the faculty." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_nobel_laureates") + + def test_distractor_claimed_as_laureates_fails(self) -> None: + answer = ( + "Berkeley has 12 Nobel Laureates on the faculty (though the page notes 107 Nobel " + "Laureates overall), 30 varsity sports and 105 NCAA titles." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_rejects_distractor_nobel_count") + + def test_alternative_phrasing_passes(self) -> None: + answer = "The About page lists 12 faculty Nobel laureates, 30 varsity sports and 105 national titles." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_alumni_prizes_line_elsewhere_is_not_a_wrong_answer(self) -> None: + answer = ( + "Berkeley has 12 Nobel Laureates on the faculty, 30 varsity sports and 105 NCAA " + "national titles. The page also notes that faculty, researchers and alumni have won " + "more than 107 Nobel Prizes in total." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "research", 2) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_19.py b/sites/berkeley/verify/tests/test_verify_19.py new file mode 100644 index 000000000..9ce89b46e --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_19.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GYM = "/news/womens-gymnastics-wins-ncaa-championship" +MEDALS = "/news/berkeley-athletes-win-record-12-medals-at-winter-world-university-games" +GENUINE_STEPS = [step("/"), step("/news?category=Athletics"), step(GYM, "done")] +ANSWER = ( + "Women's Gymnastics won the NCAA Championship, led by an all-around champion and three " + "perfect 10.0 scores on the balance beam." +) + + +class VerifyTask19Tests(SharedVerifierTests, VerifierTestCase): + N = 19 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_without_article_fails(self) -> None: + steps = [step("/"), step("/news?category=Athletics", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_championship_article") + + def test_non_championship_article_fails(self) -> None: + steps = [step("/"), step("/news?category=Athletics"), step(MEDALS, "done")] + answer = "Berkeley athletes won a record 12 medals at the Winter World University Games." + self.assertFailsOn(self.verdict(steps, answer), "visited_championship_article") + + def test_swimming_championship_also_passes(self) -> None: + steps = [ + step("/"), + step("/news?category=Athletics"), + step("/news/cal-wins-pac-12-swimming-and-diving-championship", "done"), + ] + answer = "Cal won the Pac-12 Swimming and Diving Championship with 1,400 points." + self.assertPasses(self.verdict(steps, answer)) + + def test_wrong_sport_for_visited_article_fails(self) -> None: + answer = "Cal won the Pac-12 football championship." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_binds_to_championship_article") + + def test_negated_championship_fails(self) -> None: + answer = "Women's Gymnastics did not win the NCAA Championship." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_binds_to_championship_article") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "The NCAA championship was won by the women's gymnastics team, which scored three " + "perfect 10.0s on the balance beam." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "news", 19) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_2.py b/sites/berkeley/verify/tests/test_verify_2.py new file mode 100644 index 000000000..ad013258e --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_2.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/programs?q=Computer%20Science"), + step("/programs/computer-science-bs", "done"), +] +ANSWER = ( + "The Computer Science BS requires Data Structures, Algorithms, Computer Architecture, " + "Operating Systems, AI, Machine Learning, Software Engineering, and technical electives." +) +BS_ITEMS = ["Data Structures", "Algorithms", "Computer Architecture", "Operating Systems", + "AI", "Machine Learning", "Software Engineering"] + + +class VerifyTask2Tests(SharedVerifierTests, VerifierTestCase): + N = 2 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_without_detail_fails_on_gate(self) -> None: + steps = [step("/"), step("/programs?q=Computer%20Science", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_detail_computer-science-bs") + + def test_ms_requirements_fail(self) -> None: + answer = ( + "The Computer Science MS requires foundational coursework in theory, systems, and AI, " + "plus a research project or thesis." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_requirements_match_bs") + + def test_sibling_requirement_items_fail(self) -> None: + answer = ( + "The BS requires Data Structures, Algorithms, Computer Architecture and Operating " + "Systems, as well as a Qualifying Examination and a Dissertation Proposal." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_no_sibling_requirements") + + def test_negated_requirements_fail(self) -> None: + answer = ( + "The BS does not require Data Structures, Algorithms, Computer Architecture or " + "Operating Systems." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_requirements_match_bs") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Requirements for the Computer Science BS include Algorithms, Computer Architecture, " + "Data Structures, Machine Learning, Operating Systems and Software Engineering, " + "along with technical electives." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 10) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_20.py b/sites/berkeley/verify/tests/test_verify_20.py new file mode 100644 index 000000000..8b10f829d --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_20.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/programs?degree=JD"), + step("/programs/juris-doctor-jd", "done"), +] +ANSWER = ( + "The JD at Berkeley takes 3 years, has a February 1 deadline, and is offered by the School " + "of Law." +) + + +class VerifyTask20Tests(SharedVerifierTests, VerifierTestCase): + N = 20 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_without_detail_fails(self) -> None: + steps = [step("/"), step("/programs?degree=JD", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_detail_juris-doctor-jd") + + def test_mba_values_fail(self) -> None: + answer = "The JD takes 2 years, has a January 5 deadline, and is offered by the Haas School of Business." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_duration") + + def test_optometry_deadline_is_not_enough(self) -> None: + answer = "The JD takes 3 years, has a February 1 deadline, and is offered by the School of Optometry." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_school") + + def test_negated_duration_fails(self) -> None: + answer = ( + "The JD does not take 3 years; it has a February 1 deadline and is offered by the " + "School of Law." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_duration") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Berkeley Law's Juris Doctor is a 3-year degree; applications are due by Feb 1 and " + "the school is the School of Law." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 42) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_22.py b/sites/berkeley/verify/tests/test_verify_22.py new file mode 100644 index 000000000..5407e3d6e --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_22.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/departments", "done")] +ANSWER = ( + "The College of Letters and Science lists 8 departments: Economics, English, History, " + "Mathematics, Physics and Political Science." +) + + +class VerifyTask22Tests(SharedVerifierTests, VerifierTestCase): + N = 22 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_from_college_filter_fails(self) -> None: + steps = [step("/"), step("/programs?college=letters-and-science", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_departments_page") + + def test_sitewide_department_total_fails(self) -> None: + answer = "The College of Letters and Science has 30 departments." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_department_count") + + def test_count_without_names_fails(self) -> None: + answer = "The College of Letters and Science lists 8 departments." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_names_ls_departments") + + def test_count_of_wrong_college_fails(self) -> None: + answer = "The College of Engineering has 7 departments: Economics and English." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_department_count") + + def test_negated_count_fails(self) -> None: + answer = "The College of Letters and Science does not have 8 departments." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_department_count") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "There are eight departments under the College of Letters and Science (Economics, " + "English, History and Mathematics)." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 8) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_23.py b/sites/berkeley/verify/tests/test_verify_23.py new file mode 100644 index 000000000..8cc99f5a9 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_23.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/research"), step("/research/bids", "done")] +ANSWER = ( + "BIDS is directed by Prof. David Culler; its focus areas are Data Science, Statistics, " + "Computational Methods and Open Science. A related center listed on the page is the " + "Mathematical Sciences Research Institute." +) + + +class VerifyTask23Tests(SharedVerifierTests, VerifierTestCase): + N = 23 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_from_listing_fails(self) -> None: + steps = [step("/"), step("/research", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_research_detail_bids") + + def test_wrong_director_fails(self) -> None: + answer = ( + "BIDS is directed by Prof. Douglas Dreger; its focus areas are Data Science, " + "Statistics, and Computational Methods; a related center is the Berkeley " + "Seismological Laboratory." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_director") + + def test_invented_focus_areas_fail(self) -> None: + answer = ( + "BIDS is directed by Prof. David Culler and focuses on Machine Learning, Robotics, " + "and Climate Policy." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_focus_areas") + + def test_unrendered_related_centre_fails(self) -> None: + answer = ( + "BIDS is directed by Prof. David Culler; focus areas are Data Science, Statistics " + "and Computational Methods; a related center on the page is the California Policy Lab." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_names_rendered_related_centre") + + def test_negated_director_fails(self) -> None: + answer = ( + "Prof. David Culler is not the director of BIDS; its focus areas are Data Science, " + "Statistics and Computational Methods." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_director") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "The Berkeley Institute for Data Science, led by David Culler, works across Data " + "Science, Statistics, Computational Methods and Open Science; the Berkeley " + "Population Center is listed among its related centers." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "research", 2) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_24.py b/sites/berkeley/verify/tests/test_verify_24.py new file mode 100644 index 000000000..fe1541214 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_24.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/programs/economics-phd"), + step("/departments/economics"), + step("/faculty/emmanuel-saez", "done"), +] +ANSWER = ( + "The Economics department is chaired by Prof. Ulrike Malmendier and offers the Economics BA " + "and the Economics PhD. One faculty member, Emmanuel Saez, works on public economics, " + "inequality, taxation and labor economics." +) + + +class VerifyTask24Tests(SharedVerifierTests, VerifierTestCase): + N = 24 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_missing_faculty_hop_fails_workflow(self) -> None: + steps = [step("/"), step("/programs/economics-phd"), step("/departments/economics", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "workflow_in_order") + + def test_missing_programme_hop_fails_workflow(self) -> None: + steps = [step("/"), step("/departments/economics"), step("/faculty/emmanuel-saez", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "workflow_in_order") + + def test_wrong_chair_fails(self) -> None: + answer = ( + "The Economics department is chaired by Prof. David Card and offers the Economics BA " + "and the Economics PhD. Emmanuel Saez works on public economics and inequality." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + + def test_single_programme_fails(self) -> None: + answer = ( + "The Economics department is chaired by Prof. Ulrike Malmendier and offers only the " + "Economics PhD. Emmanuel Saez works on public economics and inequality." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_department_programmes") + + def test_interests_of_a_non_economics_professor_fail(self) -> None: + steps = [step("/"), step("/programs/economics-phd"), step("/departments/economics"), + step("/faculty/alexei-efros", "done")] + answer = ( + "The Economics department is chaired by Prof. Ulrike Malmendier and offers the BA and " + "PhD in Economics. Alexei Efros works on computer vision and image synthesis." + ) + self.assertFailsOn(self.verdict(steps, answer), "answer_has_economics_faculty_interests") + + def test_negated_chair_fails(self) -> None: + answer = ( + "Ulrike Malmendier is not the department chair; the department offers the Economics " + "BA and PhD, and Emmanuel Saez works on public economics and inequality." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Prof. Ulrike Malmendier chairs the Department of Economics, which offers a BA and a " + "PhD in Economics. Prof. David Card studies labor economics, immigration and the " + "minimum wage." + ) + steps = [step("/"), step("/programs/economics-phd"), step("/departments/economics"), + step("/faculty/david-card", "done")] + self.assertPasses(self.verdict(steps, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "faculty", 17) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_25.py b/sites/berkeley/verify/tests/test_verify_25.py new file mode 100644 index 000000000..f6ba485a8 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_25.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [step("/"), step("/events?category=Career"), step("/events/2", "done")] +ANSWER = ( + "The Spring Career Fair 2026 is on May 17, 2026 at the Recreational Sports Facility, and " + "registration is required. Two other career events: the Health Sciences Information Fair on " + "May 15, 2026 at 50 Warren Hall, and the Graduate School Information Fair on May 26, 2026 at " + "Pauley Ballroom, MLK Student Union." +) + + +class VerifyTask25Tests(SharedVerifierTests, VerifierTestCase): + N = 25 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_without_detail_fails(self) -> None: + steps = [step("/"), step("/events?category=Career", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_event_detail_2") + + def test_unfiltered_events_listing_fails(self) -> None: + steps = [step("/"), step("/events"), step("/events/2", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_career_events_listing") + + def test_wrong_venue_fails(self) -> None: + answer = ANSWER.replace("Recreational Sports Facility", "Pauley Ballroom") + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_anchor_location") + + def test_registration_not_required_fails(self) -> None: + answer = ANSWER.replace("registration is required", "registration is not required") + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_registration_required") + + def test_only_one_other_event_fails(self) -> None: + answer = ( + "The Spring Career Fair 2026 is on May 17, 2026 at the Recreational Sports Facility, " + "registration required. Also the Health Sciences Information Fair on May 15, 2026 at " + "50 Warren Hall." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_lists_two_other_career_events") + + def test_negated_anchor_fails(self) -> None: + answer = "The Spring Career Fair is not on May 17, 2026." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_anchor_date") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Spring Career Fair 2026 — 2026-05-17, Recreational Sports Facility, registration " + "required. Other career events: Graduate School Information Fair (2026-05-26, Pauley " + "Ballroom, MLK Student Union) and Health Sciences Information Fair (2026-05-15, 50 " + "Warren Hall)." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "event", 2) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_27.py b/sites/berkeley/verify/tests/test_verify_27.py new file mode 100644 index 000000000..2a6fd939c --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_27.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/programs?q=Master%20of%20Engineering"), + step("/programs/master-of-engineering-meng"), + step("/programs/computer-science-ms", "done"), +] +ANSWER = ( + "The Master of Engineering is offered by the Department of Electrical Engineering and " + "Computer Sciences (EECS). The MEng takes 1 year, while the Computer Science MS takes 1.5 " + "years." +) + + +class VerifyTask27Tests(SharedVerifierTests, VerifierTestCase): + N = 27 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_degree_filter_also_satisfies_search_gate(self) -> None: + steps = [step("/"), step("/programs?degree=MEng"), + step("/programs/master-of-engineering-meng"), + step("/programs/computer-science-ms", "done")] + self.assertPasses(self.verdict(steps, ANSWER)) + + def test_missing_ms_hop_fails(self) -> None: + steps = [ + step("/"), step("/programs?q=Master%20of%20Engineering"), + step("/programs/master-of-engineering-meng", "done"), + ] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_detail_computer-science-ms") + + def test_missing_meng_hop_fails(self) -> None: + steps = [ + step("/"), step("/programs?q=Master%20of%20Engineering"), + step("/programs/computer-science-ms", "done"), + ] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_detail_master-of-engineering-meng") + + def test_no_listing_at_all_fails_search_gate(self) -> None: + steps = [step("/"), step("/programs/computer-science-ms", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_program_search") + + def test_swapped_or_wrong_durations_fail(self) -> None: + answer = ( + "The Master of Engineering is offered by EECS and takes 2 years; the Computer Science " + "MS takes 2 years." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_meng_duration") + + def test_wrong_ms_duration_fails(self) -> None: + answer = ( + "The Master of Engineering is offered by EECS and takes 1 year, while the Computer " + "Science MS takes 2 years." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_ms_duration") + + def test_negated_duration_fails(self) -> None: + answer = ( + "The Master of Engineering, offered by EECS, is not a 1-year program; the Computer " + "Science MS takes 1.5 years." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_meng_duration") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "The Master of Engineering in EECS lasts one year, while the Computer Science MS " + "lasts 1.5 years." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 43) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_28.py b/sites/berkeley/verify/tests/test_verify_28.py new file mode 100644 index 000000000..c49d0a8ee --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_28.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/programs?degree=PhD"), + step("/programs?degree=MS", "done"), +] +ANSWER = ( + "17 programs in the catalogue require the GRE; the degree type that most commonly requires " + "it is the PhD." +) + + +class VerifyTask28Tests(SharedVerifierTests, VerifierTestCase): + N = 28 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_four_unfiltered_pages_also_satisfy_gate(self) -> None: + steps = [step("/"), step("/programs?page=1"), step("/programs?page=2"), + step("/programs?page=3"), step("/programs?page=4", "done")] + self.assertPasses(self.verdict(steps, ANSWER)) + + def test_single_degree_listing_fails_gate(self) -> None: + steps = [step("/"), step("/programs?degree=PhD", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_gre_programme_listings") + + def test_all_phd_count_fails(self) -> None: + answer = "There are 25 programs that require the GRE, all of them PhD programs." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_gre_count") + + def test_wrong_modal_degree_fails(self) -> None: + answer = "17 programs require the GRE, mostly master's degrees such as the MS." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_modal_degree_type") + + def test_negated_count_fails(self) -> None: + answer = "The catalogue does not have 17 GRE-required programs." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_gre_count") + + def test_alternative_phrasing_passes(self) -> None: + answer = "There are seventeen GRE-required programs, mostly PhD programs." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "program", 31) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_30.py b/sites/berkeley/verify/tests/test_verify_30.py new file mode 100644 index 000000000..c9e3124b0 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_30.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, login_steps, step, +) + +CENTRE_ID = 8 # Berkeley Seismological Laboratory (slug seismo-lab) +GENUINE_STEPS = [ + *login_steps("alice@berkeley.edu"), + step("/research/seismo-lab"), + step("/research/seismo-lab", "click"), + step("/account"), + step("/account", "done"), +] +ANSWER = ( + "I signed in as alice, saved the Berkeley Seismological Laboratory to my bookmarks, and it " + "is listed under My Account. Its director is Prof. Douglas Dreger." +) + + +class VerifyTask30Tests(SharedVerifierTests, VerifierTestCase): + N = 30 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def genuine_after(self) -> State: + state = State() + state.add_bookmark(1, "research", CENTRE_ID) + return state + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=self.genuine_after())) + + def test_state_mismatch_without_save_fails_on_delta(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER) # agent self-reports success, DB untouched + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + def test_saving_the_wrong_centre_fails_on_delta(self) -> None: + after = State() + after.add_bookmark(1, "research", 1) # BAIR, not the named centre + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + def test_duplicate_save_fails_on_delta(self) -> None: + after = State() + after.add_bookmark(1, "research", CENTRE_ID, row_id=1) + after.add_bookmark(1, "research", CENTRE_ID, row_id=2) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + def test_saving_under_another_account_fails_on_delta(self) -> None: + after = State() + after.add_bookmark(2, "research", CENTRE_ID) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + def test_missing_login_fails(self) -> None: + steps = [step("/research/seismo-lab"), step("/research/seismo-lab", "click"), + step("/account"), step("/account", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=self.genuine_after()), "visited_login_page") + + def test_signing_in_as_the_wrong_account_fails(self) -> None: + steps = [ + *login_steps("bob@berkeley.edu"), + step("/research/seismo-lab"), step("/research/seismo-lab", "click"), + step("/account"), step("/account", "done"), + ] + self.assertFailsOn( + self.verdict(steps, ANSWER, after=self.genuine_after()), "entered_expected_account_email" + ) + + def test_missing_centre_page_fails_workflow(self) -> None: + steps = [*login_steps("alice@berkeley.edu"), step("/account"), step("/account", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER, after=self.genuine_after()), "workflow_in_order") + + def test_negated_save_fails(self) -> None: + answer = "I did not save the Berkeley Seismological Laboratory." + verdict = self.verdict(GENUINE_STEPS, answer, after=self.genuine_after()) + self.assertFailsOn(verdict, "answer_has_centre") + + def test_wrong_director_fails(self) -> None: + answer = ANSWER.replace("Douglas Dreger", "David Culler") + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer, after=self.genuine_after()), "answer_has_director") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Logged in as alice@berkeley.edu; the Berkeley Seismological Laboratory now appears " + "in My Bookmarks, directed by Douglas Dreger." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer, after=self.genuine_after())) + + def test_registration_write_fails(self) -> None: + after = self.genuine_after() + after.extra_sql.append( + "INSERT INTO users(id, email, username, password_hash, full_name, role, bio, created_at) " + "VALUES (99, 'eve@berkeley.edu', 'eve', 'x', 'Eve', 'student', '', '2026-05-12 00:00:00')" + ) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_users_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_31.py b/sites/berkeley/verify/tests/test_verify_31.py new file mode 100644 index 000000000..bb3a59b78 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_31.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, login_steps, step, +) + +MSRI_ID = 9 +CPL_ID = 22 +GENUINE_STEPS = [ + *login_steps("bob@berkeley.edu"), + step("/research/msri"), + step("/research/msri", "click"), + step("/research/cpl"), + step("/research/cpl", "click"), + step("/account"), + step("/account", "click"), + step("/account", "done"), +] +ANSWER = ( + "I signed in as bob, saved both centers in order, then removed the Mathematical Sciences " + "Research Institute bookmark. The California Policy Lab remains saved; its director is " + "Prof. Jesse Rothstein." +) + + +def _saved_then_removed() -> State: + """The genuine history: MSRI takes row id 1, CPL row id 2, then MSRI is deleted.""" + state = State() + state.add_bookmark(2, "research", MSRI_ID) # id 1 + state.add_bookmark(2, "research", CPL_ID) # id 2 + state.remove_bookmark(1) + return state + + +class VerifyTask31Tests(SharedVerifierTests, VerifierTestCase): + N = 31 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def genuine_after(self) -> State: + return _saved_then_removed() + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER, after=self.genuine_after())) + + def test_skipped_removal_fails_on_delta(self) -> None: + after = State() + after.add_bookmark(2, "research", MSRI_ID) # id 1 + after.add_bookmark(2, "research", CPL_ID) # id 2 + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + def test_nothing_added_fails_on_delta(self) -> None: + verdict = self.verdict(GENUINE_STEPS, ANSWER) + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + def test_wrong_add_order_fails_on_row_id_pin(self) -> None: + """The trajectory matches, but the DB proves CPL was saved before MSRI (survivor id 1).""" + after = State() + after.add_bookmark(2, "research", CPL_ID) # id 1 + after.add_bookmark(2, "research", MSRI_ID) # id 2 + after.remove_bookmark(2) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_surviving_row_ids") + + def test_removing_the_wrong_centre_fails_on_delta(self) -> None: + after = State() + after.add_bookmark(2, "research", MSRI_ID) # id 1 + after.add_bookmark(2, "research", CPL_ID) # id 2 + after.remove_bookmark(2) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + def test_missing_login_fails(self) -> None: + steps = GENUINE_STEPS[len(login_steps("bob@berkeley.edu")):] + self.assertFailsOn( + self.verdict(steps, ANSWER, after=self.genuine_after()), "visited_login_page" + ) + + def test_single_account_visit_fails_workflow(self) -> None: + steps = [ + *login_steps("bob@berkeley.edu"), + step("/research/msri"), step("/research/msri", "click"), + step("/research/cpl"), step("/research/cpl", "click"), + step("/account", "done"), + ] + self.assertFailsOn( + self.verdict(steps, ANSWER, after=self.genuine_after()), "workflow_in_order" + ) + + def test_wrong_remaining_director_fails(self) -> None: + answer = ANSWER.replace("Jesse Rothstein", "Tatiana Toro") + self.assertFailsOn( + self.verdict(GENUINE_STEPS, answer, after=self.genuine_after()), "answer_has_remaining_director" + ) + + def test_negated_removal_fails(self) -> None: + answer = ( + "The California Policy Lab (director: Prof. Jesse Rothstein) remains saved, but the " + "Mathematical Sciences Research Institute bookmark was not removed." + ) + self.assertFailsOn( + self.verdict(GENUINE_STEPS, answer, after=self.genuine_after()), "answer_confirms_removal" + ) + + def test_collateral_write_for_another_user_fails(self) -> None: + after = self.genuine_after() + after.add_bookmark(1, "research", 8) # alice, not bob + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_other_users_unchanged") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Bob saved both centers, then removed the MSRI bookmark; the California Policy Lab " + "is still saved (director: Prof. Jesse Rothstein)." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer, after=self.genuine_after())) + + def test_unrelated_bookmark_write_fails_on_delta(self) -> None: + after = self.genuine_after() + after.add_bookmark(2, "program", 41) # collateral write for the same user + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "bookmarks_exact_delta") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_4.py b/sites/berkeley/verify/tests/test_verify_4.py new file mode 100644 index 000000000..bd0fc50f5 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_4.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +SLUG = "crispr-pioneer-jennifer-doudna-receives-national-medal-of-science" +GENUINE_STEPS = [step("/"), step("/news?q=CRISPR"), step(f"/news/{SLUG}", "done")] +ANSWER = "The article features Jennifer Doudna, who received the National Medal of Science." + + +class VerifyTask4Tests(SharedVerifierTests, VerifierTestCase): + N = 4 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_shortcut_without_article_fails_on_gate(self) -> None: + steps = [step("/"), step("/news?q=CRISPR", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), f"visited_news_detail_{SLUG}") + + def test_shortcut_from_research_category_still_needs_article(self) -> None: + steps = [step("/"), step("/news?category=Research", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), f"visited_news_detail_{SLUG}") + + def test_nobel_prize_answer_fails(self) -> None: + answer = "The featured scientist is Jennifer Doudna, who received the Nobel Prize." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_award") + + def test_negated_award_fails(self) -> None: + answer = ( + "Jennifer Doudna is the featured scientist, but the National Medal of Science was " + "not the award she received; she won the Nobel Prize." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_award") + + def test_negated_everything_fails_on_first_check(self) -> None: + answer = "Jennifer Doudna did not receive the National Medal of Science." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_scientist") + + def test_wrong_article_covid_fails(self) -> None: + covid = "berkeley-researchers-develop-faster-covid-test-using-crispr" + steps = [step("/"), step("/news?q=CRISPR"), step(f"/news/{covid}", "done")] + answer = "The article is about a faster COVID test using CRISPR; 98% sensitivity." + self.assertFailsOn(self.verdict(steps, answer), f"visited_news_detail_{SLUG}") + + def test_alternative_phrasing_passes(self) -> None: + answer = "That story is about Prof. Jennifer Doudna; the award she received was the National Medal of Science." + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "news", 4) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_6.py b/sites/berkeley/verify/tests/test_verify_6.py new file mode 100644 index 000000000..3169f30ce --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_6.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/events?category=Lecture"), + step("/events/1"), + step("/events/31"), + step("/events/51", "done"), +] +ANSWER = ( + "Lecture events: 'Nobel Laureate Lecture: Jennifer Doudna on the Future of Gene Editing' " + "on May 15, 2026 at 2050 Valley Life Sciences Building; 'Public Lecture: The Future of " + "Democracy in the Digital Age' on May 21, 2026 at 145 Dwinelle Hall; and 'Berkeley Science " + "Lecture: Origins of Life' on May 19, 2026 at 1 Pimentel Hall." +) + + +class VerifyTask6Tests(SharedVerifierTests, VerifierTestCase): + N = 6 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_date_all_listing_also_satisfies_gate(self) -> None: + steps = [step("/"), step("/events?category=Lecture&date=all"), + step("/events/1"), step("/events/31"), step("/events/51", "done")] + self.assertPasses(self.verdict(steps, ANSWER)) + + def test_catalog_wide_search_does_not_satisfy_listing_gate(self) -> None: + steps = [step("/"), step("/events?q=lecture", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_lecture_listing") + + def test_other_category_events_fail(self) -> None: + answer = ( + "Events: 'Spring Career Fair 2026' on May 17, 2026 at Recreational Sports Facility; " + "'Hackathon: Code for Climate 2026' on May 30, 2026 at Soda Hall; and 'Berkeley " + "Startup Pitch Competition Finals' on June 4, 2026 at 310 Sutardja Dai Hall." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_lists_three_lecture_events") + + def test_two_events_only_fails(self) -> None: + answer = ( + "Lecture events: 'Nobel Laureate Lecture: Jennifer Doudna on the Future of Gene " + "Editing' on May 15, 2026 at 2050 Valley Life Sciences Building, and 'Public Lecture: " + "The Future of Democracy in the Digital Age' on May 21, 2026 at 145 Dwinelle Hall." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_lists_three_lecture_events") + + def test_wrong_date_fails(self) -> None: + answer = ANSWER.replace("May 15, 2026", "May 16, 2026").replace("May 21, 2026", "May 22, 2026").replace("May 19, 2026", "May 20, 2026") + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_lists_three_lecture_events") + + def test_negated_listing_fails(self) -> None: + answer = "There are no Lecture events I could list." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_lists_three_lecture_events") + + def test_alternative_phrasing_passes(self) -> None: + answer = ( + "Lecture events: 'Nobel Laureate Lecture: Jennifer Doudna on the Future of Gene " + "Editing' 2026-05-15, 2050 Valley Life Sciences Building; 'Public Lecture: The Future " + "of Democracy in the Digital Age' 2026-05-21, 145 Dwinelle Hall; 'Berkeley Science " + "Lecture: Origins of Life' 2026-05-19, 1 Pimentel Hall." + ) + self.assertPasses(self.verdict(GENUINE_STEPS, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "event", 1) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_7.py b/sites/berkeley/verify/tests/test_verify_7.py new file mode 100644 index 000000000..a32864d6c --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_7.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402,F401 + SharedVerifierTests, State, VerifierTestCase, step, +) + +GENUINE_STEPS = [ + step("/"), + step("/faculty?dept=eecs"), + step("/faculty/stuart-russell", "done"), +] +ANSWER = ( + "Stuart Russell is an EECS professor whose research covers Artificial intelligence and " + "machine learning, including AI safety." +) + + +class VerifyTask7Tests(SharedVerifierTests, VerifierTestCase): + N = 7 + GENUINE_STEPS = GENUINE_STEPS + ANSWER = ANSWER + + def test_genuine_run_passes(self) -> None: + self.assertPasses(self.verdict(GENUINE_STEPS, ANSWER)) + + def test_department_page_route_satisfies_gate(self) -> None: + steps = [step("/"), step("/departments/eecs"), step("/faculty/stuart-russell", "done")] + self.assertPasses(self.verdict(steps, ANSWER)) + + def test_shortcut_without_profile_fails(self) -> None: + steps = [step("/"), step("/faculty?dept=eecs", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_named_professor_profile") + + def test_other_department_professor_fails(self) -> None: + steps = [step("/"), step("/faculty?dept=eecs"), step("/faculty/eliza-strickland", "done")] + answer = "Eliza Strickland works on AI reporting and biomedical ethics." + self.assertFailsOn(self.verdict(steps, answer), "named_eecs_ai_professor") + + def test_unfiltered_listing_alone_fails_route_gate(self) -> None: + steps = [step("/"), step("/faculty"), step("/faculty/stuart-russell", "done")] + self.assertFailsOn(self.verdict(steps, ANSWER), "visited_eecs_faculty_route") + + def test_interests_of_another_professor_fail(self) -> None: + answer = "Stuart Russell works on robotics and reinforcement learning." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_interests_bind_to_profile") + + def test_catalog_wide_search_only_fails(self) -> None: + steps = [step("/"), step("/search?q=artificial%20intelligence", "done")] + verdict = self.verdict(steps, ANSWER) + self.assertFalse(verdict.get("pass")) + self.assertEqual(verdict.get("reason"), "visited_eecs_faculty_route") + + def test_negated_department_fails(self) -> None: + answer = "Stuart Russell is not in the EECS department; his interests are not machine learning." + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "named_eecs_ai_professor") + + def test_alternative_phrasing_passes(self) -> None: + steps = [step("/"), step("/faculty?dept=eecs"), step("/faculty/dawn-song", "done")] + answer = "Prof. Dawn Song (EECS) works on AI security, blockchain, deep learning and privacy." + self.assertPasses(self.verdict(steps, answer)) + + def test_read_only_write_fails(self) -> None: + after = State() + after.add_bookmark(1, "faculty", 5) + verdict = self.verdict(GENUINE_STEPS, ANSWER, after=after) + self.assertFailsOn(verdict, "read_only_bookmarks_unchanged") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/tests/test_verify_lib.py b/sites/berkeley/verify/tests/test_verify_lib.py new file mode 100644 index 000000000..d4f7f2e18 --- /dev/null +++ b/sites/berkeley/verify/tests/test_verify_lib.py @@ -0,0 +1,352 @@ +"""Unit tests for the shared helpers, the snapshot contract and the source-fact rules. + +No docker, no LLM: matcher behaviour, gate semantics, the fingerprint recipe and +the three assertions that keep the About/Admissions values source-sourced. +""" +from __future__ import annotations + +import datetime as _dt +import json +import sqlite3 +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import ( # noqa: E402 + BASE, SITE_DIR, SEED_DB, State, seed_fingerprint, step, write_run, +) + +VERIFY_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(VERIFY_DIR)) + +import ground_truth # noqa: E402 +import verify_lib as lib # noqa: E402 + + +class SnapshotContractTests(unittest.TestCase): + def test_seed_fingerprint_matches_the_pinned_contract(self) -> None: + self.assertEqual(lib.catalog_fingerprint(str(SEED_DB)), lib.CATALOG_FINGERPRINT) + self.assertEqual(seed_fingerprint(), lib.CATALOG_FINGERPRINT) + + def test_seed_counts_and_schema_hash_match(self) -> None: + observed = {table: len(lib.table_rows(str(SEED_DB), table)) for table in lib.EXPECTED_COUNTS} + self.assertEqual(observed, lib.EXPECTED_COUNTS) + schema_hash = lib.hashlib.sha256( + json.dumps(lib._schema_objects(str(SEED_DB)), separators=(",", ":")).encode() + ).hexdigest() + self.assertEqual(schema_hash, lib.SCHEMA_HASH) + + def test_stdlib_fixture_pipeline_reproduces_the_fingerprint(self) -> None: + """A copy of the seed rewritten through the stdlib fixture keeps the catalog intact.""" + with tempfile.TemporaryDirectory() as tmp: + path = State().write(Path(tmp) / "fixture.db") + self.assertEqual(lib.catalog_fingerprint(str(path)), lib.CATALOG_FINGERPRINT) + + def test_state_rewrite_changes_only_the_bookmarks_table(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + after = Path(tmp) / "after.db" + state = State() + state.add_bookmark(1, "research", 8) + state.write(after) + changed = [table for table in lib.ALL_TABLES + if lib.table_rows(str(SEED_DB), table) != lib.table_rows(str(after), table)] + self.assertEqual(changed, ["bookmarks"]) + + def test_contract_rejects_catalog_drift(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + initial = State().write_with_catalog_change(Path(tmp) / "initial.db") + after = State().write(Path(tmp) / "after.db") + with self.assertRaises(ValueError): + lib._validate_snapshot_contract(str(initial), str(after)) + + def test_contract_rejects_a_catalog_mutation_in_after(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + initial = State().write(Path(tmp) / "initial.db") + after = State().write_with_catalog_change(Path(tmp) / "after.db") + with self.assertRaises(ValueError): + lib._validate_snapshot_contract(str(initial), str(after)) + + def test_contract_accepts_a_bookmark_only_delta(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + initial = State().write(Path(tmp) / "initial.db") + state = State() + state.add_bookmark(1, "research", 8) + after = state.write(Path(tmp) / "after.db") + lib._validate_snapshot_contract(str(initial), str(after)) # must not raise + + +class GroundTruthTests(unittest.TestCase): + def test_every_kept_task_derives(self) -> None: + facts = ground_truth.all_ground_truth(str(SEED_DB)) + self.assertEqual( + sorted(facts), + [1, 2, 4, 6, 7, 10, 11, 12, 13, 14, 16, 17, 19, 20, 22, 23, 24, 25, 27, 28, 30, 31], + ) + + def test_unsupported_task_fails(self) -> None: + with self.assertRaises(ValueError): + ground_truth.task_ground_truth(str(SEED_DB), 3) + + def test_dropped_and_unrelated_ids_fail_closed(self) -> None: + for dropped in (0, 3, 5, 8, 9, 15, 18, 21, 26, 29, 32): + with self.assertRaises(ValueError): + ground_truth.task_ground_truth(str(SEED_DB), dropped) + + def test_event_targets_respect_the_frozen_clock(self) -> None: + lecture = ground_truth.task_ground_truth(str(SEED_DB), 6) + worker = sqlite3.connect(str(SEED_DB)) + try: + total = worker.execute("SELECT COUNT(*) FROM events WHERE category='Lecture'").fetchone()[0] + finally: + worker.close() + self.assertLess(len(lecture["upcoming"]), total) + self.assertTrue(all(row["start_datetime"] >= ground_truth.BENCHMARK_NOW for row in lecture["upcoming"])) + + def test_task_27_reanchored_targets(self) -> None: + facts = ground_truth.task_ground_truth(str(SEED_DB), 27) + self.assertEqual(facts["durations"], (1.0, 1.5)) + self.assertEqual(facts["meng"]["department_name"], facts["ms"]["department_name"]) + + def test_stateful_targets_bind_to_empty_bookmark_table(self) -> None: + facts = ground_truth.task_ground_truth(str(SEED_DB), 31) + self.assertLess(facts["first"]["id"], facts["second"]["id"]) + self.assertEqual(len(lib.table_rows(str(SEED_DB), "bookmarks")), 0) + + +class SourceFactsTests(unittest.TestCase): + """Decision 6: the About/Admissions values are tracked source, not DB rows.""" + + def test_admissions_facts_come_from_the_template(self) -> None: + facts = ground_truth.admissions_facts() + template = (SITE_DIR / "templates" / "admissions.html").read_text(encoding="utf-8") + self.assertIn(facts["deadline"], template) + self.assertIn(facts["acceptance_rate"], template) + self.assertRegex(facts["deadline"], r"^[A-Z][a-z]+ \d{1,2}$") + self.assertRegex(facts["acceptance_rate"], r"^\d+(?:\.\d+)?%$") + + def test_about_stats_are_source_literals_not_db(self) -> None: + facts = ground_truth.about_facts() + self.assertEqual(set(facts), {"nobel_laureates", "varsity_sports", "national_titles"}) + source = (SITE_DIR / "app.py").read_text(encoding="utf-8") + template = (SITE_DIR / "templates" / "about.html").read_text(encoding="utf-8") + worker = sqlite3.connect(str(SEED_DB)) + try: + columns = { + column[1] + for table in lib.EXPECTED_TABLES + for column in worker.execute(f"PRAGMA table_info({table})") + } + finally: + worker.close() + for key, value in facts.items(): + self.assertRegex(source, rf"'{key}':\s*{value}\b") + self.assertIn(f"stats.{key}", template) + self.assertNotIn(key, columns) + self.assertGreater(ground_truth.about_distractor_prizes(), max(facts.values())) + + def test_verifier_package_never_imports_an_llm(self) -> None: + for path in sorted(VERIFY_DIR.glob("verify_*.py")): + text = path.read_text(encoding="utf-8") + for banned in ("openai", "llm_text_match", "llm_screenshot_shows"): + self.assertNotIn(banned, text, f"{path.name} references {banned}") + + +class MatcherTests(unittest.TestCase): + def test_contains_count_forms_and_guards(self) -> None: + self.assertTrue(lib.contains_count("There are 17 programs.", 17)) + self.assertTrue(lib.contains_count("seventeen programs", 17)) + self.assertTrue(lib.contains_count("4,500 undergraduates", 4500)) + self.assertTrue(lib.contains_count("4500 undergraduates", 4500)) + self.assertTrue(lib.contains_count("count: 8", 8)) + self.assertTrue(lib.contains_count("eight departments", 8)) + self.assertFalse(lib.contains_count("25 programs", 17)) + self.assertFalse(lib.contains_count("12,000 students", 12)) + self.assertFalse(lib.contains_count("14.4%", 14)) + self.assertFalse(lib.contains_count("built in 2013", 1)) + self.assertFalse(lib.contains_count("18 departments", 8)) + + def test_contains_percent(self) -> None: + self.assertTrue(lib.contains_percent("14.4%", "14.4%")) + self.assertTrue(lib.contains_percent("14.4 percent", "14.4%")) + self.assertTrue(lib.contains_percent("rate: 14.4 per cent", "14.4%")) + self.assertFalse(lib.contains_percent("11%", "14.4%")) + self.assertFalse(lib.contains_percent("14.4", "14.4%")) + + def test_contains_date_variants(self) -> None: + value = _dt.date(2026, 5, 15) + for text in ("May 15, 2026", "May 15 2026", "15 May 2026", "2026-05-15", "05/15/2026", "May 15"): + self.assertTrue(lib.contains_date(text, value), text) + self.assertFalse(lib.contains_date("May 16, 2026", value)) + self.assertFalse(lib.contains_date("May 2015", value)) + + def test_contains_month_day(self) -> None: + self.assertTrue(lib.contains_month_day("due by November 30", "November 30")) + self.assertTrue(lib.contains_month_day("due by Nov. 30, 2026", "November 30")) + self.assertFalse(lib.contains_month_day("due by December 1", "November 30")) + self.assertFalse(lib.contains_month_day("due by November 3", "November 30")) + + def test_contains_person_ignores_titles(self) -> None: + self.assertTrue(lib.contains_person("Prof. James Demmel is the chair.", "Prof. James Demmel")) + self.assertTrue(lib.contains_person("the dean is Tsu-Jae King Liu", "Dean Tsu-Jae King Liu")) + self.assertFalse(lib.contains_person("Demmel", "Prof. James Demmel")) + self.assertFalse(lib.contains_person("James", "Prof. James Demmel")) + + def test_contains_location_room_number_optional(self) -> None: + self.assertTrue(lib.contains_location("located at 253 Cory Hall", "253 Cory Hall")) + self.assertTrue(lib.contains_location("located at Cory Hall", "253 Cory Hall")) + self.assertFalse(lib.contains_location("located at Soda Hall", "253 Cory Hall")) + + def test_contains_degree_type_variants(self) -> None: + self.assertTrue(lib.contains_degree_type("offers a Ph.D.", "PhD")) + self.assertTrue(lib.contains_degree_type("the MEng programme", "MEng")) + self.assertTrue(lib.contains_degree_type("a BS in CS", "BS")) + self.assertFalse(lib.contains_degree_type("the master's programme", "MS")) + self.assertFalse(lib.contains_degree_type("business administration", "BA")) + + def test_contains_duration_years_distinguishes_one_and_one_and_a_half(self) -> None: + self.assertTrue(lib.contains_duration_years("takes 1 year", 1.0)) + self.assertTrue(lib.contains_duration_years("lasts one year", 1.0)) + self.assertTrue(lib.contains_duration_years("runs 1.5 years", 1.5)) + self.assertTrue(lib.contains_duration_years("about 18 months", 1.5)) + self.assertFalse(lib.contains_duration_years("takes 1.5 years", 1.0)) + self.assertFalse(lib.contains_duration_years("takes 2 years", 1.0)) + self.assertFalse(lib.contains_duration_years("takes 1 year", 1.5)) + + def test_contains_year_allows_sentence_final_stop(self) -> None: + self.assertTrue(lib.contains_year("established in 2013.", 2013)) + self.assertTrue(lib.contains_year("founded 2013, not 2017", 2013)) + self.assertFalse(lib.contains_year("founded in 2017.", 2013)) + + def test_negation_semantics(self) -> None: + self.assertFalse(lib.contains_phrase("did not receive the National Medal of Science", "National Medal of Science")) + self.assertFalse(lib.contains_phrase("the award was not the National Medal of Science", "National Medal of Science")) + self.assertFalse(lib.contains_count("does not have 12 departments", 12)) + self.assertFalse(lib.contains_phrase("no Lecture events", "Lecture")) + # A confirming contrast stays affirmative. + self.assertTrue(lib.contains_year("founded in 2013, not 2017", 2013)) + self.assertTrue(lib.contains_count("1 year, not 2 years", 1)) + # Negation after the value still rejects it. + self.assertFalse(lib.contains_year("2013 was not the founding year", 2013)) + + def test_contains_count_as_pins_the_label(self) -> None: + self.assertTrue(lib.contains_count_as("107 Nobel Laureates on the faculty", 107, "laureates")) + self.assertFalse(lib.contains_count_as("more than 107 Nobel Prizes in total", 107, "laureates")) + + def test_title_tokens_and_matching(self) -> None: + title = "Women's Gymnastics Wins NCAA Championship" + self.assertGreaterEqual(lib.title_tokens_matched("women's gymnastics won the NCAA championship", title), 3) + self.assertLess(lib.title_tokens_matched("won a championship", title), 3) + + def test_department_aliases_and_acronyms(self) -> None: + name = "Department of Electrical Engineering and Computer Sciences" + self.assertTrue(lib.contains_department("offered by EECS", name)) + self.assertTrue(lib.contains_department("the Electrical Engineering and Computer Sciences department", name)) + self.assertFalse(lib.contains_department("offered by the Math department", name)) + self.assertEqual(lib.acronym("Mathematical Sciences Research Institute"), "msri") + self.assertEqual(lib.acronym("Economics"), "") + + def test_interest_token_matches_counts_distinct_tokens(self) -> None: + interests = "Artificial intelligence, machine learning, AI safety, probabilistic reasoning" + self.assertGreaterEqual(lib.interest_token_matches("works on machine learning and AI safety", interests), 2) + self.assertLess(lib.interest_token_matches("works on robotics", interests), 2) + + def test_affirmative_near(self) -> None: + self.assertTrue( + lib.affirmative_near("the Spring Career Fair: registration is required", "career fair", "required", 80) + ) + self.assertFalse( + lib.affirmative_near("the Spring Career Fair is not open; registration not required", "career fair", "required", 80) + ) + + +class UrlGateTests(unittest.TestCase): + def trajectory(self, steps): + return {"start_url": f"{BASE}/", "steps": steps} + + def test_params_visited_matches_first_value_only(self) -> None: + traj = self.trajectory([step("/programs?degree=PhD°ree=MS")]) + self.assertTrue(lib.params_visited(traj, "/programs", degree="PhD")) + self.assertFalse(lib.params_visited(traj, "/programs", degree="MS")) + + def test_params_visited_alternatives_and_regex(self) -> None: + traj = self.trajectory([step("/news?q=CRISPR")]) + self.assertTrue(lib.params_visited(traj, "/news", q="crispr")) + self.assertTrue(lib.params_visited(traj, "/news", q=lib.re.compile(r"crisp"))) + self.assertFalse(lib.params_visited(traj, "/news", category="Research")) + + def test_detail_gates_are_exact(self) -> None: + traj = self.trajectory([step("/programs/computer-science-bs-extra")]) + self.assertFalse(lib.detail_visited(traj, "program", "computer-science-bs")) + traj = self.trajectory([step("/programs/computer-science-bs")]) + self.assertTrue(lib.detail_visited(traj, "program", "computer-science-bs")) + self.assertEqual(lib.detail_path("event", 2), "/events/2") + + def test_last_action_target_counts_as_a_visit(self) -> None: + traj = self.trajectory([step("/"), {"url": f"{BASE}/", "action": "navigate", + "params": {"url": f"{BASE}/about"}}]) + self.assertTrue(lib.navigated_to_path(traj, "/about")) + + def test_paths_in_order(self) -> None: + traj = self.trajectory([step("/departments"), step("/departments/eecs")]) + lib.check_paths_in_order(lib.Judge("t"), traj, "order", [("/departments", {}), ("/departments/eecs", {})]) + bad = self.trajectory([step("/departments/eecs"), step("/departments")]) + judge = lib.Judge("t") + self.assertFalse(lib.check_paths_in_order(judge, bad, "order", [("/departments", {}), ("/departments/eecs", {})])) + + def test_listing_pages_are_distinct(self) -> None: + traj = self.trajectory([step("/programs?page=1"), step("/programs?page=1"), step("/programs?page=2")]) + self.assertEqual(len(lib.listing_pages_visited(traj, "/programs")), 2) + + def test_origin_rules(self) -> None: + self.assertTrue(lib.is_site_url("http://localhost:41026/x")) + self.assertTrue(lib.is_site_url("http://127.0.0.1:41026/x")) + self.assertFalse(lib.is_site_url("http://example.com/x")) + self.assertFalse(lib._same_local_origin("http://localhost:9999/x", f"{BASE}/")) + + def test_screenshot_size_gate(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + run_dir = Path(tmp) / "run" + write_run(run_dir, "UC Berkeley--1", [step("/")], "answer") + judge = lib.Judge("t") + trajectory = lib.load_run(run_dir) + lib.check_trajectory_identity(judge, trajectory, "UC Berkeley--1") + self.assertTrue(judge.passed) + + +class BookmarkHelperTests(unittest.TestCase): + def test_delta_and_row_ids(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + initial = State().write(Path(tmp) / "initial.db") + state = State() + state.add_bookmark(2, "research", 9) # id 1 + state.add_bookmark(2, "research", 22) # id 2 + state.remove_bookmark(1) + after = state.write(Path(tmp) / "after.db") + delta = lib.bookmark_delta(str(initial), str(after), 2) + self.assertEqual([lib.bookmark_identity(row) for row in delta["added"]], [(2, "research", 22)]) + self.assertEqual(delta["removed"], []) + judge = lib.Judge("t") + lib.check_bookmarks_delta(judge, str(initial), str(after), user_id=2, + added=[(2, "research", 22)], surviving_ids=[2]) + self.assertTrue(judge.passed, judge.evidence) + + def test_surviving_id_pin_rejects_the_reversed_order(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + initial = State().write(Path(tmp) / "initial.db") + state = State() + state.add_bookmark(2, "research", 22) # id 1: the wrong order + state.add_bookmark(2, "research", 9) # id 2 + state.remove_bookmark(2) + after = state.write(Path(tmp) / "after.db") + judge = lib.Judge("t") + lib.check_bookmarks_delta(judge, str(initial), str(after), user_id=2, + added=[(2, "research", 22)], surviving_ids=[2]) + self.assertFalse(judge.passed) + self.assertEqual(judge.reason, "bookmarks_surviving_row_ids") + + +if __name__ == "__main__": + unittest.main() diff --git a/sites/berkeley/verify/verify_1.py b/sites/berkeley/verify/verify_1.py new file mode 100644 index 000000000..c6092a006 --- /dev/null +++ b/sites/berkeley/verify/verify_1.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--1: the MBA programme's school and duration. + +Deterministic only: no LLM calls. Targets are derived from the run's initial +snapshot (ground_truth.py), never from frozen constants. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + check_visited_detail, + contains_duration_years, + contains_phrase, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--1" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 1) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_params_visited( + judge, trajectory, "visited_program_search", "/programs", + {"q": "mba"}, {"degree": "MBA"}, + ) + check_visited_detail(judge, trajectory, "program", facts["program"]["slug"]) + judge.check( + "answer_has_school", + contains_phrase(answer, facts["college"]), + f"expected_school={facts['college']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_duration", + contains_duration_years(answer, facts["duration_years"]), + f"expected_duration_years={facts['duration_years']!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_10.py b/sites/berkeley/verify/verify_10.py new file mode 100644 index 000000000..d0d253129 --- /dev/null +++ b/sites/berkeley/verify/verify_10.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--10: BAIR's director and founding year. + +The seed diverges from the real-world founding year (2017), so a remembered +value fails; both facts are read off the centre page. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + check_visited_detail, + contains_person, + contains_year, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--10" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 10) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_visited_detail(judge, trajectory, "research", facts["centre"]["slug"]) + judge.check( + "answer_has_director", + contains_person(answer, facts["director"]), + f"expected_director={facts['director']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_founded_year", + contains_year(answer, facts["founded_year"]), + f"expected_founded_year={facts['founded_year']!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_11.py b/sites/berkeley/verify/verify_11.py new file mode 100644 index 000000000..c5092bd0c --- /dev/null +++ b/sites/berkeley/verify/verify_11.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--11: the freshman deadline and the acceptance rate. + +Both values are rendered from tracked source rather than the DB; the verifier +derives them from templates/admissions.html and fails closed if the labelled +literals move, and verify/tests asserts they are not DB-derived. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + check_visited_path, + contains_month_day, + contains_percent, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--11" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 11) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_visited_path(judge, trajectory, "visited_admissions_page", "/admissions") + judge.check( + "answer_has_freshman_deadline", + contains_month_day(answer, facts["deadline"]), + f"expected_deadline={facts['deadline']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_acceptance_rate", + contains_percent(answer, facts["acceptance_rate"]), + f"expected_acceptance_rate={facts['acceptance_rate']!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_12.py b/sites/berkeley/verify/verify_12.py new file mode 100644 index 000000000..ae940c925 --- /dev/null +++ b/sites/berkeley/verify/verify_12.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--12: the degree types the Haas School of Business offers. + +The snapshot has exactly one Haas programme, contradicting the real-world +"MBA, PhD, …" prior. The negative check is clause-local: a degree type mentioned +in a clause with no Haas anchor (or in a negated clause) is not an offering. +""" +from __future__ import annotations + +import os +import re +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + contains_degree_type, + contains_phrase, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + reconfirm_clause_split, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--12" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 12) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + programme = facts["programmes"][0] + + check_params_visited( + judge, trajectory, "visited_haas_programme_listing", "/programs", + {"college": facts["college"]["slug"]}, + ) + judge.check( + "answer_has_programme", + contains_phrase(answer, programme["name"]), + f"expected_programme={programme['name']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_degree_type", + contains_degree_type(answer, *facts["offered_types"]), + f"expected_degree_types={facts['offered_types']!r}, answer={answer!r}", + ) + claimed = sorted({ + other + for clause in reconfirm_clause_split(answer) + if re.search(r"\bhaas\b|business administration", clause) + for other in facts["other_types"] + if contains_degree_type(clause, other) + }) + judge.check( + "answer_no_other_haas_degrees", + not claimed, + f"catalogue_degree_types_not_offered_by_haas={facts['other_types']!r}, " + f"claimed_near_haas={claimed!r}; answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_13.py b/sites/berkeley/verify/verify_13.py new file mode 100644 index 000000000..65de5867c --- /dev/null +++ b/sites/berkeley/verify/verify_13.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--13: the EECS department chair and its location.""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + check_visited_path, + contains_location, + contains_person, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--13" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 13) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_visited_path(judge, trajectory, "visited_department_detail", f"/departments/{facts['department']['slug']}") + judge.check( + "answer_has_chair", + contains_person(answer, facts["chair"]), + f"expected_chair={facts['chair']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_location", + contains_location(answer, facts["location"]), + f"expected_location={facts['location']!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_14.py b/sites/berkeley/verify/verify_14.py new file mode 100644 index 000000000..a132aeb45 --- /dev/null +++ b/sites/berkeley/verify/verify_14.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--14: College of Engineering enrolment counts and dean. + +The university-wide totals on the homepage/About page (31,800 / 12,000) are the +near-miss distractors: the card values are per-college. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + check_visited_path, + contains_count, + contains_person, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--14" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 14) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_visited_path(judge, trajectory, "visited_academics_page", "/academics") + judge.check( + "answer_has_undergrad_count", + contains_count(answer, facts["undergrad_count"]), + f"expected_undergrad_count={facts['undergrad_count']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_grad_count", + contains_count(answer, facts["grad_count"]), + f"expected_grad_count={facts['grad_count']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_dean", + contains_person(answer, facts["dean"]), + f"expected_dean={facts['dean']!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_16.py b/sites/berkeley/verify/verify_16.py new file mode 100644 index 000000000..4d43d09c0 --- /dev/null +++ b/sites/berkeley/verify/verify_16.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--16: the single online degree programme. + +``q=online`` finds nothing (the word is not in any programme name or +description), so the only route is scanning the listings for the online badge; +the detail visit pins the programme and its school. Naming any other catalog +programme near "online" fails. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + affirmative_near, + check_read_only, + check_trajectory_identity, + check_visited_detail, + check_visited_path, + contains_degree_type, + contains_phrase, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, + title_tokens, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--16" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 16) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + programme = facts["program"] + + check_visited_path(judge, trajectory, "visited_programme_listing", "/programs") + check_visited_detail(judge, trajectory, "program", programme["slug"]) + judge.check( + "answer_has_programme", + contains_phrase(answer, programme["name"]), + f"expected_programme={programme['name']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_degree_type", + contains_degree_type(answer, programme["degree_type"]), + f"expected_degree_type={programme['degree_type']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_school", + contains_phrase(answer, programme["college_name"]), + f"expected_school={programme['college_name']!r}, answer={answer!r}", + ) + other_online = [ + name for name in facts["others"] + if len(title_tokens(name)) >= 2 and affirmative_near(answer, name, "online", 80) + ] + judge.check( + "answer_no_other_online_programmes", + not other_online, + f"other_programmes_reported_online={other_online!r}; answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_17.py b/sites/berkeley/verify/verify_17.py new file mode 100644 index 000000000..8fa44c909 --- /dev/null +++ b/sites/berkeley/verify/verify_17.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--17: the three About-page statistics. + +The values (and the "more than N Nobel Prizes" distractor) are derived from +tracked source; the verifier fails closed if either literal moves. The +distractor check is clause-local — quoting the alumni line elsewhere is not a +wrong answer, claiming it as the faculty count is. +""" +from __future__ import annotations + +import os +import re +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + check_visited_path, + contains_count, + contains_count_as, + fail_closed, + final_answer, + Judge, + load_run, + normalize_text, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--17" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 17) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_visited_path(judge, trajectory, "visited_about_page", "/about") + for name, key in ( + ("answer_has_nobel_laureates", "nobel_laureates"), + ("answer_has_varsity_sports", "varsity_sports"), + ("answer_has_national_titles", "national_titles"), + ): + judge.check( + name, + contains_count(answer, facts[key]), + f"expected_{key}={facts[key]!r}, answer={answer!r}", + ) + faculty_clauses = [ + clause for clause in re.split(r"[.!?;\n]+", normalize_text(answer)) + if re.search(r"\blaureates?\b", clause) + ] + distractor = facts["distractor_nobel_prizes"] + misquoted = [ + clause for clause in faculty_clauses + if contains_count_as(clause, distractor, "laureates") + or contains_count_as(clause, distractor, "laureate") + ] + judge.check( + "answer_rejects_distractor_nobel_count", + not misquoted, + f"distractor={distractor!r}, laureate_clauses={faculty_clauses!r}; answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_19.py b/sites/berkeley/verify/verify_19.py new file mode 100644 index 000000000..7697af08b --- /dev/null +++ b/sites/berkeley/verify/verify_19.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--19: an Athletics championship article, summarised. + +Set-valued: the accepted championship set is derived from the Athletics rows +(titles carrying "championship"); the summary must bind to one of them — a +medals/football/academic-rating article fails. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + contains_phrase, + detail_visited, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, + title_tokens_matched, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--19" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 19) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_params_visited(judge, trajectory, "visited_athletics_listing", "/news", {"category": "Athletics"}) + visited = [row for row in facts["championships"] if detail_visited(trajectory, "news", row["slug"])] + judge.check( + "visited_championship_article", + bool(visited), + f"championships={[row['slug'] for row in facts['championships']]!r}", + ) + bound = [ + row for row in visited + if title_tokens_matched(answer, row["title"]) >= 3 and contains_phrase(answer, "championship") + ] + judge.check( + "answer_binds_to_championship_article", + bool(bound), + f"visited={[row['title'] for row in visited]!r}, " + f"title_token_hits={[title_tokens_matched(answer, row['title']) for row in visited]!r}; " + f"answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_2.py b/sites/berkeley/verify/verify_2.py new file mode 100644 index 000000000..656c42e37 --- /dev/null +++ b/sites/berkeley/verify/verify_2.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--2: the Computer Science BS requirements block. + +Three same-name programmes (BS/MS/PhD) make the detail slug the discriminator; +the sibling requirement items are derived from the snapshot and must not appear. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + check_visited_detail, + fail_closed, + final_answer, + Judge, + load_run, + mentions, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--2" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 2) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_params_visited( + judge, trajectory, "visited_program_search", "/programs", + {"q": "computer science"}, {"degree": "BS"}, + ) + check_visited_detail(judge, trajectory, "program", facts["program"]["slug"]) + matched = mentions(answer, facts["items"]) + judge.check( + "answer_requirements_match_bs", + len(matched) >= 4, + f"matched_items={sorted(matched)!r} of {facts['items']!r}; answer={answer!r}", + ) + foreign = mentions(answer, facts["foreign_items"]) + judge.check( + "answer_no_sibling_requirements", + not foreign, + f"foreign_items_matched={sorted(foreign)!r}; answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_20.py b/sites/berkeley/verify/verify_20.py new file mode 100644 index 000000000..56aaec52c --- /dev/null +++ b/sites/berkeley/verify/verify_20.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--20: the JD programme's duration, deadline and school. + +The Optometry MD shares the February 1 deadline and the MBA shares the +"professional degree" register; only the JD row has all three values. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + check_visited_detail, + contains_duration_years, + contains_month_day, + contains_phrase, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--20" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 20) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_params_visited( + judge, trajectory, "visited_program_search", "/programs", + {"degree": "JD"}, {"q": "juris"}, + ) + check_visited_detail(judge, trajectory, "program", facts["program"]["slug"]) + judge.check( + "answer_has_duration", + contains_duration_years(answer, facts["duration_years"]), + f"expected_duration_years={facts['duration_years']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_deadline", + contains_month_day(answer, facts["deadline"]), + f"expected_deadline={facts['deadline']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_school", + contains_phrase(answer, facts["college"]), + f"expected_school={facts['college']!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_22.py b/sites/berkeley/verify/verify_22.py new file mode 100644 index 000000000..53d5eeb58 --- /dev/null +++ b/sites/berkeley/verify/verify_22.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--22: how many departments the College of Letters and Science lists. + +The /departments page carries no per-college total (and the stale +``colleges.dept_count`` column is never rendered), so the count is enumeration +work: the answer must carry it and name several of the listed departments. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + check_visited_path, + contains_count, + contains_department, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--22" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 22) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + count = len(facts["departments"]) + + check_visited_path(judge, trajectory, "visited_departments_page", "/departments") + judge.check( + "answer_has_department_count", + contains_count(answer, count), + f"expected_count={count!r}, answer={answer!r}", + ) + named = [row["name"] for row in facts["departments"] if contains_department(answer, row["name"])] + judge.check( + "answer_names_ls_departments", + len(named) >= 4, + f"named={named!r} of {[row['name'] for row in facts['departments']]!r}; answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_23.py b/sites/berkeley/verify/verify_23.py new file mode 100644 index 000000000..5f18968b3 --- /dev/null +++ b/sites/berkeley/verify/verify_23.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--23: BIDS focus areas, director and its related centres. + +The related-centre names are the three rows the page's unordered ``LIMIT 3`` +query actually renders (app.py:469-472); naming a same-college centre that the +page does not list fails. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + check_visited_detail, + contains_person, + fail_closed, + final_answer, + Judge, + load_run, + mentions, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--23" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 23) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_visited_detail(judge, trajectory, "research", facts["centre"]["slug"]) + judge.check( + "answer_has_director", + contains_person(answer, facts["centre"]["director"]), + f"expected_director={facts['centre']['director']!r}, answer={answer!r}", + ) + focus = mentions(answer, facts["focus_areas"]) + judge.check( + "answer_has_focus_areas", + len(focus) >= 3, + f"expected_focus_areas={facts['focus_areas']!r}, matched={sorted(focus)!r}; answer={answer!r}", + ) + related = mentions(answer, facts["related_names"]) + judge.check( + "answer_names_rendered_related_centre", + bool(related), + f"rendered_related_centres={facts['related_names']!r}, matched={sorted(related)!r}; answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_24.py b/sites/berkeley/verify/verify_24.py new file mode 100644 index 000000000..aabf368e2 --- /dev/null +++ b/sites/berkeley/verify/verify_24.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--24: the Economics PhD -> department -> faculty walk. + +Three hops, each gated in order; each answer fact binds to the page it came from +(chair and programme list to the department page, interests to a member profile). +""" +from __future__ import annotations + +import os +import re +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_paths_in_order, + check_read_only, + check_trajectory_identity, + contains_degree_type, + contains_person, + detail_path, + fail_closed, + final_answer, + interest_token_matches, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--24" +FACULTY_PATH_RE = re.compile(r"/faculty/[a-z0-9-]+") + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 24) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [ + (detail_path("program", facts["program"]["slug"]), {}), + (detail_path("department", facts["department"]["slug"]), {}), + (FACULTY_PATH_RE, {}), + ], + ) + judge.check( + "answer_has_chair", + contains_person(answer, facts["chair"]), + f"expected_chair={facts['chair']!r}, answer={answer!r}", + ) + degree_types = sorted({row["degree_type"] for row in facts["programmes"]}) + missing = [value for value in degree_types if not contains_degree_type(answer, value)] + judge.check( + "answer_has_department_programmes", + not missing, + f"expected_degree_types={degree_types!r}, missing={missing!r}; answer={answer!r}", + ) + bound = [ + member for member in facts["members"] + if contains_person(answer, member["name"]) + and interest_token_matches(answer, member["research_interests"]) >= 2 + ] + judge.check( + "answer_has_economics_faculty_interests", + bool(bound), + f"members={[row['name'] for row in facts['members']]!r}, " + f"interest_token_hits={[interest_token_matches(answer, row['research_interests']) for row in facts['members']]!r}; " + f"answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_25.py b/sites/berkeley/verify/verify_25.py new file mode 100644 index 000000000..4d99d5f71 --- /dev/null +++ b/sites/berkeley/verify/verify_25.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--25: the Spring Career Fair plus two other Career events. + +The anchor event is derived by name (it survives the frozen clock), its detail +page is gated, and the other two events must bind title + date + location to +distinct rows of the Career listing. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + affirmative_near, + check_params_visited, + check_read_only, + check_trajectory_identity, + check_visited_detail, + contains_date, + contains_location, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, + title_tokens_matched, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--25" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 25) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + anchor = facts["anchor"] + + check_params_visited(judge, trajectory, "visited_career_events_listing", "/events", {"category": "Career"}) + check_visited_detail(judge, trajectory, "event", anchor["id"]) + judge.check( + "answer_has_anchor_date", + contains_date(answer, str(anchor["start_datetime"])[:10]), + f"expected_date={str(anchor['start_datetime'])[:10]!r}, answer={answer!r}", + ) + judge.check( + "answer_has_anchor_location", + contains_location(answer, anchor["location"]), + f"expected_location={anchor['location']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_registration_required", + affirmative_near(answer, "career fair", "required", 200) + or affirmative_near(answer, anchor["title"], "required", 200), + f"answer={answer!r}", + ) + matched = [ + row for row in facts["others"] + if title_tokens_matched(answer, row["title"]) >= 3 + and contains_date(answer, str(row["start_datetime"])[:10]) + and contains_location(answer, row["location"]) + ] + judge.check( + "answer_lists_two_other_career_events", + len(matched) >= 2, + f"matched_events={[row['id'] for row in matched]!r} of {len(facts['others'])} other Career events; " + f"answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_27.py b/sites/berkeley/verify/verify_27.py new file mode 100644 index 000000000..107d15c61 --- /dev/null +++ b/sites/berkeley/verify/verify_27.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--27 (re-anchored): both programme durations and the MEng department. + +The task now asks for the two exact durations printed on the two detail pages +(MEng vs Computer Science MS), which is deterministic; both detail visits are +gated and the department is accepted by name or by its acronym. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + check_visited_detail, + contains_department, + contains_duration_years, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--27" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 27) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + meng_duration, ms_duration = facts["durations"] + + check_params_visited( + judge, trajectory, "visited_program_search", "/programs", + {"q": "master of engineering"}, {"degree": "MEng"}, + ) + check_visited_detail(judge, trajectory, "program", facts["meng"]["slug"]) + check_visited_detail(judge, trajectory, "program", facts["ms"]["slug"]) + judge.check( + "answer_has_department", + contains_department(answer, facts["department"]), + f"expected_department={facts['department']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_meng_duration", + contains_duration_years(answer, meng_duration), + f"expected_meng_duration={meng_duration!r}, answer={answer!r}", + ) + judge.check( + "answer_has_ms_duration", + contains_duration_years(answer, ms_duration), + f"expected_ms_duration={ms_duration!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_28.py b/sites/berkeley/verify/verify_28.py new file mode 100644 index 000000000..19f1a3e13 --- /dev/null +++ b/sites/berkeley/verify/verify_28.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--28: how many programmes require the GRE, and which degree type. + +No GRE filter exists, so the badge is only visible by scanning the programme +listings: the gate accepts the two degree-filtered listings (which cover every +badged row) or four distinct pages of the full listing. The count and the modal +degree type are derived from the snapshot. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + contains_count, + contains_degree_type, + fail_closed, + final_answer, + Judge, + listing_pages_visited, + load_run, + params_visited, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--28" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 28) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + pages = listing_pages_visited(trajectory, "/programs") + both_filters = ( + params_visited(trajectory, "/programs", degree="PhD") + and params_visited(trajectory, "/programs", degree="MS") + ) + judge.check( + "visited_gre_programme_listings", + both_filters or len(pages) >= 4, + f"degree_filtered_listings={both_filters}, distinct_unfiltered_or_filtered_pages={len(pages)}; " + f"observed={pages!r}", + ) + judge.check( + "answer_has_gre_count", + contains_count(answer, facts["count"]), + f"expected_count={facts['count']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_modal_degree_type", + contains_degree_type(answer, facts["most_common_degree"]), + f"expected_degree_type={facts['most_common_degree']!r}, by_degree={facts['by_degree']!r}; " + f"answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_30.py b/sites/berkeley/verify/verify_30.py new file mode 100644 index 000000000..191a77371 --- /dev/null +++ b/sites/berkeley/verify/verify_30.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--30: alice saves a named research centre to her bookmarks. + +Stateful. The gate is the ordered workflow (sign-in, centre page, My Account) +and the binding check is the exact bookmark row delta against the initial +snapshot — a run that claims the save without writing it fails on the delta. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_bookmarks_delta, + check_paths_in_order, + check_signed_in_as, + check_tables_unchanged, + check_trajectory_identity, + contains_any, + contains_person, + contains_phrase, + detail_path, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, + user_id_for_email, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--30" +EMAIL = "alice@berkeley.edu" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 30) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + centre = facts["centre"] + + check_signed_in_as(judge, trajectory, EMAIL) + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [ + ("/login", {}), + (detail_path("research", centre["slug"]), {}), + ("/account", {}), + ], + ) + judge.check( + "answer_has_centre", + contains_phrase(answer, centre["name"]), + f"expected_centre={centre['name']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_director", + contains_person(answer, facts["director"]), + f"expected_director={facts['director']!r}, answer={answer!r}", + ) + judge.check( + "answer_confirms_saved", + contains_any(answer, ("saved", "listed", "bookmarks", "my account")), + f"answer={answer!r}", + ) + + user_id = user_id_for_email(initial_db, EMAIL) + if user_id is None: + judge.check("benchmark_user_present", False, f"missing benchmark user {EMAIL!r}") + else: + judge.check("benchmark_user_present", True, f"user_id={user_id}") + check_bookmarks_delta( + judge, initial_db, after_db, + user_id=user_id, + added=[(user_id, "research", centre["id"])], + ) + check_tables_unchanged(judge, initial_db, after_db, ("users",), prefix="read_only_") + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_31.py b/sites/berkeley/verify/verify_31.py new file mode 100644 index 000000000..5bdce8309 --- /dev/null +++ b/sites/berkeley/verify/verify_31.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--31: bob saves two centres in order, then removes the first. + +Stateful, with a row-id proof: the bookmarks table starts empty, so the two +inserts take ids 1 and 2; deleting the id-1 row leaves the id-2 row behind. +Requiring the surviving row's id to be exactly 2 therefore proves that both +inserts happened and that the first was deleted. A run that skips the removal +(two rows added), removes the wrong one, or adds only the second centre fails. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_bookmarks_delta, + check_paths_in_order, + check_signed_in_as, + check_tables_unchanged, + check_trajectory_identity, + contains_acronym, + contains_person, + contains_phrase, + detail_path, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, + title_tokens_matched, + user_id_for_email, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--31" +EMAIL = "bob@berkeley.edu" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 31) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + first, second = facts["first"], facts["second"] + + check_signed_in_as(judge, trajectory, EMAIL) + check_paths_in_order( + judge, trajectory, "workflow_in_order", + [ + ("/login", {}), + (detail_path("research", first["slug"]), {}), + (detail_path("research", second["slug"]), {}), + ("/account", {}), + ("/account", {}), + ], + ) + judge.check( + "answer_has_remaining_centre", + contains_phrase(answer, second["name"]), + f"expected_centre={second['name']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_remaining_director", + contains_person(answer, second["director"]), + f"expected_director={second['director']!r}, answer={answer!r}", + ) + removed_named = title_tokens_matched(answer, first["name"]) >= 3 or contains_acronym(answer, first["name"]) + judge.check( + "answer_confirms_removal", + contains_phrase(answer, "removed") and removed_named, + f"expected_removed_centre_tokens={title_tokens_matched(answer, first['name'])!r} " + f"or acronym={'yes' if contains_acronym(answer, first['name']) else 'no'}; " + f"centre={first['name']!r}; answer={answer!r}", + ) + + user_id = user_id_for_email(initial_db, EMAIL) + if user_id is None: + judge.check("benchmark_user_present", False, f"missing benchmark user {EMAIL!r}") + else: + judge.check("benchmark_user_present", True, f"user_id={user_id}") + check_bookmarks_delta( + judge, initial_db, after_db, + user_id=user_id, + added=[(user_id, "research", second["id"])], + surviving_ids=[2], + ) + check_tables_unchanged(judge, initial_db, after_db, ("users",), prefix="read_only_") + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_4.py b/sites/berkeley/verify/verify_4.py new file mode 100644 index 000000000..87f7c9cd9 --- /dev/null +++ b/sites/berkeley/verify/verify_4.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--4: the CRISPR article's scientist and award. + +The person and the award are derived from the article's own headline (the row +the app renders), so prior knowledge ("Nobel Prize") cannot satisfy the check. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + check_visited_detail, + contains_person, + contains_phrase, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--4" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 4) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_params_visited( + judge, trajectory, "visited_news_listing", "/news", + {"q": "crispr"}, {"category": "Research"}, + ) + check_visited_detail(judge, trajectory, "news", facts["article"]["slug"]) + judge.check( + "answer_has_scientist", + contains_person(answer, facts["person"]), + f"expected_person={facts['person']!r}, answer={answer!r}", + ) + judge.check( + "answer_has_award", + contains_phrase(answer, facts["award"]), + f"expected_award={facts['award']!r}, answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_6.py b/sites/berkeley/verify/verify_6.py new file mode 100644 index 000000000..206a6856f --- /dev/null +++ b/sites/berkeley/verify/verify_6.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--6: at least three Lecture events with dates and locations. + +Set-valued: the accepted set is every seeded Lecture event (the ``date=all`` +rendering); each reported event must bind title + date + location to one row. +""" +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_params_visited, + check_read_only, + check_trajectory_identity, + contains_date, + contains_location, + fail_closed, + final_answer, + Judge, + load_run, + parse_args, + resolve_snapshots, + title_tokens_matched, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--6" + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 6) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + check_params_visited(judge, trajectory, "visited_lecture_listing", "/events", {"category": "Lecture"}) + matched = [ + row for row in facts["events"] + if title_tokens_matched(answer, row["title"]) >= 3 + and contains_date(answer, str(row["start_datetime"])[:10]) + and contains_location(answer, row["location"]) + ] + judge.check( + "answer_lists_three_lecture_events", + len(matched) >= 3, + f"matched_events={[row['id'] for row in matched]!r} of {len(facts['events'])} Lecture events; " + f"answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_7.py b/sites/berkeley/verify/verify_7.py new file mode 100644 index 000000000..5f2a09ab8 --- /dev/null +++ b/sites/berkeley/verify/verify_7.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Verify UC Berkeley--7: an EECS professor who works on artificial intelligence. + +The accepted set is derived from the department roster with an AI-family rule +over ``research_interests`` (the literal phrase "artificial intelligence" matches +one row; the allowlist is the rule the task text implies). The answer must bind +to one named row: the profile must have been opened and the reported interests +must be that row's. +""" +from __future__ import annotations + +import os +import re +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from verify_lib import ( # noqa: E402 + check_read_only, + check_trajectory_identity, + contains_person, + detail_visited, + fail_closed, + final_answer, + interest_token_matches, + Judge, + load_run, + navigated_to_path, + params_visited, + parse_args, + resolve_snapshots, +) +from ground_truth import task_ground_truth # noqa: E402 + + +TASK_ID = "UC Berkeley--7" +AI_QUERY_RE = re.compile(r"(artificial|machine learning|deep learning|reinforcement|robot|ai\b)") + + +def run_checks(judge: Judge, trajectory: dict, initial_db: str, after_db: str) -> None: + facts = task_ground_truth(initial_db, 7) + check_trajectory_identity(judge, trajectory, TASK_ID) + answer = final_answer(trajectory) + + route_ok = ( + params_visited(trajectory, "/faculty", dept=facts["department"]["slug"]) + or params_visited(trajectory, "/faculty", q=AI_QUERY_RE) + or navigated_to_path(trajectory, f"/departments/{facts['department']['slug']}") + ) + judge.check( + "visited_eecs_faculty_route", + route_ok, + "required: /faculty?dept=eecs, an AI-keyword /faculty search, or the EECS department page", + ) + + named = [row for row in facts["allowed"] if contains_person(answer, row["name"])] + judge.check( + "named_eecs_ai_professor", + bool(named), + f"allowed={[row['name'] for row in facts['allowed']]!r}, answer={answer!r}", + ) + visited = [row for row in named if detail_visited(trajectory, "faculty", row["slug"])] + judge.check( + "visited_named_professor_profile", + bool(visited), + f"named={[row['slug'] for row in named]!r}, observed={[row['slug'] for row in facts['members'] if detail_visited(trajectory, 'faculty', row['slug'])]!r}", + ) + bound = [ + row for row in visited + if interest_token_matches(answer, row["research_interests"]) >= 2 + ] + judge.check( + "answer_interests_bind_to_profile", + bool(bound), + f"named={[row['name'] for row in named]!r}, " + f"interest_token_hits={[interest_token_matches(answer, row['research_interests']) for row in visited]!r}; " + f"answer={answer!r}", + ) + check_read_only(judge, initial_db, after_db) + + +def main() -> None: + args = parse_args() + try: + trajectory = load_run(args.run_dir) + except (OSError, ValueError) as exc: + fail_closed(TASK_ID, "trajectory_unavailable", str(exc)) + initial_db, after_db = resolve_snapshots(args, TASK_ID) + judge = Judge(TASK_ID) + try: + run_checks(judge, trajectory, initial_db, after_db) + except Exception as exc: # noqa: BLE001 - any verifier error fails closed + fail_closed(TASK_ID, "verifier_error", f"{type(exc).__name__}: {exc}") + judge.emit() + + +if __name__ == "__main__": + main() diff --git a/sites/berkeley/verify/verify_lib.py b/sites/berkeley/verify/verify_lib.py new file mode 100644 index 000000000..6b2dc32f9 --- /dev/null +++ b/sites/berkeley/verify/verify_lib.py @@ -0,0 +1,1196 @@ +#!/usr/bin/env python3 +"""Shared deterministic helpers for UC Berkeley task verifiers. + +Each verifier consumes an agent run directory plus before/after SQLite snapshots +and emits ``{task_id, pass, reason, evidence[]}`` with exit code 0/1. + +No helper in this module calls an LLM; a verdict never depends on a key or a +model. Targets are re-derived from the run's ``initial.db`` by ``ground_truth.py`` +(never frozen answer constants); the only pinned content constants are the +snapshot contract below (schema hash, table set, seed counts, catalog +fingerprint), which fail closed when the seed drifts. +""" +from __future__ import annotations + +import argparse +import atexit +import datetime as _dt +import hashlib +import ipaddress +import json +import os +import re +import sqlite3 +import subprocess +import tempfile +import unicodedata +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable, Sequence +from urllib.parse import parse_qs, urlparse + +from PIL import Image + + +SITE = "berkeley" +DEFAULT_CONTAINER = os.environ.get("WH_CONTAINER", "wh-review") + +# Public benchmark password (documented in the site README and tasks.jsonl). +BENCHMARK_PASSWORD = "test1234" + +# Screenshots must be plausible viewport captures, not replayed 1x1 stubs. +MIN_SCREENSHOT_WIDTH = 320 +MIN_SCREENSHOT_HEIGHT = 240 + +# The nine tables the site ships. A read-only task must leave every one of them +# row-identical: after the article-view fix (commit 1) no GET path writes the DB. +ALL_TABLES = ( + "bookmarks", "colleges", "departments", "events", "faculty", "news_articles", + "programs", "research_centers", "users", +) +READ_ONLY_TABLES = ALL_TABLES + +# Tables no task may ever change (the catalog). Runtime tables are ``users`` and +# ``bookmarks``; a stateful verifier pins their exact delta instead. +IMMUTABLE_TABLES = ( + "colleges", "departments", "events", "faculty", "news_articles", "programs", + "research_centers", +) + +# --- Snapshot contract ------------------------------------------------------- +# Recomputed from the shipped instance_seed/berkeley.db (md5 +# 3001bcf4bcec169f4192c08609160ab6). A re-frozen seed must re-pin these and +# re-run the verifier suite; until then every verifier fails closed. +SCHEMA_HASH = "2e12a903a802cd4691481320edd80ccc24dddc65e55c0d44f890544057ea654e" +CATALOG_FINGERPRINT = "99e17923920cb88698802655a1fb9b7b03805cd214522f3fc45183bb13b143de" +EXPECTED_TABLES = frozenset(ALL_TABLES) +EXPECTED_COUNTS = { + "bookmarks": 0, "colleges": 14, "departments": 30, "events": 64, "faculty": 82, + "news_articles": 121, "programs": 83, "research_centers": 25, "users": 4, +} + + +# --------------------------------------------------------------------------- # +# CLI / run loading +# --------------------------------------------------------------------------- # +@dataclass(frozen=True) +class VerifyArgs: + run_dir: str + initial_db: str | None + after_db: str | None + container: str + no_llm: bool + + +def parse_args() -> VerifyArgs: + parser = argparse.ArgumentParser() + parser.add_argument("--run_dir", required=True) + parser.add_argument("--initial_db") + parser.add_argument("--after_db") + parser.add_argument("--container", default=DEFAULT_CONTAINER) + parser.add_argument("--no_llm", nargs="?", const=True, default=True) + args = parser.parse_args() + run_dir = Path(args.run_dir) + initial_snapshot = run_dir / "initial.db" + after_snapshot = run_dir / "after.db" + return VerifyArgs( + run_dir=args.run_dir, + initial_db=( + args.initial_db + or (str(initial_snapshot) if initial_snapshot.is_file() else None) + ), + after_db=( + args.after_db or (str(after_snapshot) if after_snapshot.is_file() else None) + ), + container=args.container, + no_llm=True, + ) + + +def load_run(run_dir: str | os.PathLike[str]) -> dict[str, Any]: + path = Path(run_dir) / "trajectory.json" + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError("trajectory.json must contain a JSON object") + data["_run_dir"] = str(Path(run_dir).resolve()) + return data + + +def final_answer(trajectory: dict[str, Any]) -> str: + return str(trajectory.get("final_answer") or "").strip() + + +def last_action_target_url(trajectory: dict[str, Any]) -> str: + """The final action's declared destination, when it names one. + + ``agent.py`` records the URL *before* each action, so a non-final action's + destination appears as the next step's URL. The final action has no next + step, so a ``navigate`` there would otherwise be invisible; the gate helpers + credit its ``params.url`` as an alternative satisfier (decision 5). Click + actions carry only an element index and never contribute. + """ + steps = trajectory.get("steps") + if not isinstance(steps, list) or not steps: + return "" + last = steps[-1] + if not isinstance(last, dict): + return "" + params = last.get("params") + if isinstance(params, dict) and params.get("url"): + return str(params["url"]) + return "" + + +def trajectory_urls(trajectory: dict[str, Any]) -> list[str]: + """Every browser URL recorded by supported trajectory producers. + + ``last_action_target_url`` is appended so the final action's declared target + is a first-class visit for every gate. + """ + urls: list[str] = [] + if trajectory.get("start_url"): + urls.append(str(trajectory["start_url"])) + for step in trajectory.get("steps") or []: + if not isinstance(step, dict): + continue + for key in ("url", "url_before", "url_after"): + value = step.get(key) + if value: + urls.append(str(value)) + if trajectory.get("final_url"): + urls.append(str(trajectory["final_url"])) + target = last_action_target_url(trajectory) + if target: + urls.append(target) + return urls + + +def normalized_url_path(url: str) -> str: + path = urlparse(str(url or "")).path or "/" + return path.rstrip("/") or "/" + + +def is_site_url(url: str) -> bool: + """Accept HTTP(S) URLs on a loopback host while allowing any port. + + Runs hit the alt-port container (41026) while tasks.jsonl says 40026, so the + port is deliberately not checked here. + """ + parsed = urlparse(str(url or "")) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + return False + hostname = parsed.hostname.casefold() + if hostname == "localhost": + return True + try: + return ipaddress.ip_address(hostname).is_loopback + except ValueError: + return False + + +def site_urls(trajectory: dict[str, Any]) -> list[str]: + return [url for url in trajectory_urls(trajectory) if is_site_url(url)] + + +def _path_matches(url: str, expected: str | re.Pattern[str]) -> bool: + path = normalized_url_path(url) + if isinstance(expected, re.Pattern): + return expected.fullmatch(path) is not None + return path == normalized_url_path(expected) + + +def navigated_to_path(trajectory: dict[str, Any], expected_path: str | re.Pattern[str]) -> bool: + """Require an exact mirror path (or a full-path regex) on a loopback origin.""" + return any(_path_matches(url, expected_path) for url in site_urls(trajectory)) + + +def final_url_is_path(trajectory: dict[str, Any], expected_path: str | re.Pattern[str]) -> bool: + observed_url = final_url(trajectory) + return is_site_url(observed_url) and _path_matches(observed_url, expected_path) + + +def trajectory_task_matches(trajectory: dict[str, Any], task_id: str) -> bool: + return str(trajectory.get("task_id") or "").strip() == task_id + + +def trajectory_input_texts(trajectory: dict[str, Any], on_path: str | None = None) -> list[str]: + """Typed texts, optionally only from steps whose (before-action) URL path is ``on_path``.""" + values: list[str] = [] + for step in trajectory.get("steps") or []: + if not isinstance(step, dict) or normalize_text(step.get("action")) != "input": + continue + if on_path is not None and normalized_url_path(str(step.get("url") or "")) != normalized_url_path(on_path): + continue + params = step.get("params") + if isinstance(params, dict) and params.get("text") is not None: + values.append(str(params["text"])) + return values + + +_EMAIL_RE = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+") + + +def trajectory_last_email(trajectory: dict[str, Any], on_path: str | None = None) -> str: + emails = [ + normalize_text(value) + for value in trajectory_input_texts(trajectory, on_path) + if _EMAIL_RE.fullmatch(value.strip()) + ] + return emails[-1] if emails else "" + + +# --------------------------------------------------------------------------- # +# Detail-path gates (ids and slugs exposed in hrefs) +# --------------------------------------------------------------------------- # +_DETAIL_ROUTES = { + "program": "/programs/{}", + "event": "/events/{}", + "news": "/news/{}", + "faculty": "/faculty/{}", + "research": "/research/{}", + "department": "/departments/{}", +} + + +def detail_path(kind: str, key: Any) -> str: + """Exact detail path for an entity; ids are rendered as integers.""" + try: + pattern = _DETAIL_ROUTES[kind] + except KeyError: + raise ValueError(f"unsupported detail kind: {kind}") from None + return pattern.format(key) + + +def detail_visited(trajectory: dict[str, Any], kind: str, key: Any) -> bool: + """An exact detail-page visit; listing snippets that merely carry the href do not count.""" + return navigated_to_path(trajectory, detail_path(kind, key)) + + +def check_visited_detail(judge: Judge, trajectory: dict[str, Any], kind: str, key: Any) -> bool: + path = detail_path(kind, key) + return judge.check( + f"visited_{kind}_detail_{key}", + navigated_to_path(trajectory, path), + f"required_path={path}", + ) + + +def _param_matches(query: dict[str, list[str]], key: str, expected: Any) -> bool: + """Query-parameter matcher; a value may be a regex, a tuple of alternatives or exact text. + + The app reads every parameter with ``request.args.get`` (first value only), so + duplicate-parameter tricks cannot satisfy a gate while the rendered page used + another value. + """ + if isinstance(expected, (tuple, list, set, frozenset)): + return any(_param_matches(query, key, alt) for alt in expected) + raw_values = query.get(key) or [] + first = raw_values[0] if raw_values else None + values = [str(first)] if first is not None and str(first).strip() else [] + if expected == "": + return not values # the parameter must be absent (or blank) + if key == "q": + if isinstance(expected, re.Pattern): + return any(expected.search(normalize_text(value)) for value in values) + # A plain-text query satisfies the gate when every expected token appears + # in the recorded value ("Master of Engineering" ~ "master+engineering"). + expected_tokens = set(re.findall(r"[a-z0-9]+", normalize_text(expected))) + return bool(expected_tokens) and any( + expected_tokens <= set(re.findall(r"[a-z0-9]+", normalize_text(value))) + for value in values + ) + if key == "page": + return any(str(value).strip() == str(expected) for value in values) + if isinstance(expected, re.Pattern): + return any(expected.fullmatch(normalize_text(value)) for value in values) + return any(normalize_text(expected) == normalize_text(value) for value in values) + + +def params_visited( + trajectory: dict[str, Any], path: str | re.Pattern[str], **params: Any +) -> bool: + """Some visit of ``path`` carries every requested query parameter.""" + for url in site_urls(trajectory): + if not _path_matches(url, path): + continue + query = parse_qs(urlparse(url).query, keep_blank_values=True) + if all(_param_matches(query, key, expected) for key, expected in params.items()): + return True + return False + + +def _describe_params(params: dict[str, Any]) -> str: + pieces = [] + for key, value in params.items(): + if isinstance(value, re.Pattern): + value = f"/{value.pattern}/" + pieces.append(f"{key}~{value!r}") + return "&".join(pieces) or "(any)" + + +def check_params_visited( + judge: Judge, + trajectory: dict[str, Any], + name: str, + path: str | re.Pattern[str], + *alternatives: dict[str, Any], +) -> bool: + """PASS when any of the ``alternatives`` param sets matches a visit of ``path``.""" + matched = any(params_visited(trajectory, path, **params) for params in alternatives) + described = " OR ".join(_describe_params(params) for params in alternatives) + shown_path = f"/{path.pattern}/" if isinstance(path, re.Pattern) else path + observed = [url for url in site_urls(trajectory) if _path_matches(url, path)] + return judge.check(name, matched, f"required={shown_path}?{described}; observed_urls={observed!r}") + + +def listing_pages_visited(trajectory: dict[str, Any], path: str) -> list[str]: + """Distinct normalized listing URLs of ``path`` (used by the catalog-scan gates).""" + seen: list[str] = [] + for url in site_urls(trajectory): + if _path_matches(url, path) and url not in seen: + seen.append(url) + return seen + + +def check_paths_in_order( + judge: Judge, + trajectory: dict[str, Any], + name: str, + requirements: Sequence[tuple[str | re.Pattern[str], dict[str, Any]]], +) -> bool: + urls = site_urls(trajectory) + cursor = 0 + described = [ + (f"/{path.pattern}/" if isinstance(path, re.Pattern) else path, _describe_params(params)) + for path, params in requirements + ] + for expected_path, params in requirements: + for index in range(cursor, len(urls)): + url = urls[index] + query = parse_qs(urlparse(url).query, keep_blank_values=True) + if _path_matches(url, expected_path) and all( + _param_matches(query, key, value) for key, value in params.items() + ): + cursor = index + 1 + break + else: + return judge.check(name, False, f"requirements={described!r}, observed={urls!r}") + return judge.check(name, True, f"requirements={described!r}") + + +def check_visited_before( + judge: Judge, + trajectory: dict[str, Any], + name: str, + before_path: str | re.Pattern[str], + after_path: str | re.Pattern[str], + before_params: Sequence[dict[str, Any]] = (), +) -> bool: + """Require a qualifying ``before_path`` visit strictly earlier than ``after_path``.""" + urls = site_urls(trajectory) + matches = list(enumerate(urls)) + before_index = None + for index, url in matches: + if not _path_matches(url, before_path): + continue + query = parse_qs(urlparse(url).query, keep_blank_values=True) + if not before_params or any( + all(_param_matches(query, key, value) for key, value in params.items()) + for params in before_params + ): + before_index = index + break + after_index = next( + (index for index, url in matches + if _path_matches(url, after_path) and (before_index is None or index > before_index)), + None, + ) + ok = before_index is not None and after_index is not None and before_index < after_index + return judge.check( + name, + ok, + f"before_path={before_path!r} params={before_params!r} at {before_index}; " + f"after_path={after_path!r} at {after_index}; observed={urls!r}", + ) + + +# --------------------------------------------------------------------------- # +# Text normalization and answer matchers +# --------------------------------------------------------------------------- # +DASH = r"[-‐‑‒–—−]" + + +def normalize_text(value: Any) -> str: + text = unicodedata.normalize("NFKC", str(value or "")) + text = text.replace("’", "'").replace("‘", "'").replace("“", '"').replace("”", '"') + text = re.sub(DASH, "-", text) + text = text.replace("&", " and ") + return re.sub(r"\s+", " ", text).strip().casefold() + + +_NEGATION_RE = re.compile( + r"\b(?:not|no|never|without|wrong|incorrect|false|failed|nor|neither|unlike" + r"|isn'?t|wasn'?t|aren'?t|weren'?t|didn'?t|doesn'?t|don'?t|cannot|can'?t)\b", + re.I, +) +_CLAUSE_SPLIT_RE = re.compile(r"[.!?;:\n]+|\b(?:but|however|instead)\b", re.I) + + +_AFTER_NEGATION_WINDOW = 15 +_CONTRAST_CHARS = ",;–—(" + + +def _match_is_affirmative(text: str, match: re.Match[str]) -> bool: + """Reject a match when a negation token contradicts it. + + Negation *before* the match (anywhere in the clause) rejects it — "did not + receive the National Medal of Science", "does not have 12". Negation *after* + the match rejects it only inside a short window that a contrastive comma has + not already closed, so a confirming contrast ("founded in 2013, not 2017") + stays affirmative while "2013 was not the founding year" does not. + """ + starts = [m.end() for m in _CLAUSE_SPLIT_RE.finditer(text[:match.start()])] + clause_start = starts[-1] if starts else 0 + end_match = _CLAUSE_SPLIT_RE.search(text, match.end()) + clause_end = end_match.start() if end_match else len(text) + before = text[clause_start:match.start()] + if _NEGATION_RE.search(before): + return False + after = text[match.end():clause_end] + contrast = min((after.find(char) for char in _CONTRAST_CHARS if char in after), default=len(after)) + window = after[:min(contrast, _AFTER_NEGATION_WINDOW)] + return not _NEGATION_RE.search(window) + + +def _affirmative_search(pattern: str, text: str, flags: int = 0) -> bool: + return any(_match_is_affirmative(text, match) for match in re.finditer(pattern, text, flags)) + + +def _phrase_pattern(phrase: str) -> str: + tokens = re.findall(r"[a-z0-9]+", normalize_text(phrase)) + if not tokens: + return r"(?!x)x" + return r"(? bool: + """Whole-token phrase match: punctuation, dash style, ``&``/``and`` and case are ignored.""" + return _affirmative_search(_phrase_pattern(phrase), normalize_text(text)) + + +def contains_all(text: Any, expected: Iterable[str]) -> bool: + return all(contains_phrase(text, value) for value in expected) + + +def contains_any(text: Any, expected: Iterable[str]) -> bool: + return any(contains_phrase(text, value) for value in expected) + + +def contains_year(text: Any, year: int) -> bool: + """A standalone four-digit year. + + A trailing full stop is normal ("…established in 2013."); a period or comma + that introduces more digits (``2013.5``, ``1,2013``) is not a match. + """ + raw = unicodedata.normalize("NFKC", str(text or "")) + return _affirmative_search(rf"(? str: + """The integer as digits, tolerant of thousands separators (4,500 / 4500). + + The trailing guard allows a sentence-ending full stop ("…requires the GRE: + 17.") but rejects a period or comma that introduces more digits, so "12" + never matches inside "12,000" or "14.4". + """ + digits = str(int(number)) + if len(digits) <= 3: + body = re.escape(digits) + else: + head, tail = digits[:-3], digits[-3:] + body = re.escape(head) + r",?" + re.escape(tail) + return rf"(? bool: + """A standalone integer (digits with optional thousands separator, or the word form). + + Years, decimals, percents, times and phone numbers are naturally excluded by + the neighbouring-character guards, so "12" never matches inside "1,629", + "12,000", "2012" or "14.4". + """ + if _affirmative_search(_integer_pattern(int(number)), normalize_text(text)): + return True + word = _NUMBER_WORDS.get(int(number)) + return bool(word) and contains_phrase(text, word) + + +def contains_count_as(text: Any, number: int, phrase: str) -> bool: + """The count labelling a phrase ("107 Nobel Laureates"), affirmative. + + Used for near-miss rules: the page's "more than 107 Nobel Prizes" line is + only a wrong answer when it is claimed *as the laureate count*. + """ + normalized = normalize_text(text) + pattern = _integer_pattern(int(number)) + r"[\s\S]{0,12}" + _phrase_pattern(phrase) + return _affirmative_search(pattern, normalized) + + +def contains_percent(text: Any, value: str | float) -> bool: + """``14.4%`` / ``14.4 percent`` / ``14.4 per cent`` (the printed rate).""" + normalized = normalize_text(text) + literal = normalize_text(value).rstrip("%").strip() + try: + number = float(literal) + except ValueError: + return False + body = re.escape(f"{number:g}") + pattern = rf"(? bool: + """``May 15`` / ``May 15, 2026`` / ``15 May 2026`` / ``2026-05-15`` / ``05/15/2026``. + + The year is optional: the events listing prints the day and month separately. + """ + if isinstance(value, str): + match = re.fullmatch(r"\s*(\d{4})-(\d{2})-(\d{2})\s*", value) + if not match: + raise ValueError(f"unsupported date literal: {value!r}") + value = _dt.date(int(match.group(1)), int(match.group(2)), int(match.group(3))) + normalized = normalize_text(text) + month = _MONTHS[value.month - 1] + month_re = rf"(?:{month}|{month[:3]}\.?)" + day_re = rf"(? str: + """Strip academic titles and trailing decorations from a rendered name.""" + tokens = [token for token in normalize_text(person).split() + if token.strip(".,") not in _TITLE_WORDS] + return " ".join(tokens).strip(" ,") + + +def contains_person(text: Any, person: str) -> bool: + """The person's full name as one contiguous phrase (titles are ignored).""" + name = bare_name(person) + return bool(name) and contains_phrase(text, name) + + +def contains_location(text: Any, location: str) -> bool: + """``253 Cory Hall`` or ``Cory Hall`` — the leading room number is optional.""" + normalized = normalize_text(location) + if not normalized: + return False + if contains_phrase(text, normalized): + return True + tokens = normalized.replace(",", " ").split() + if tokens and re.fullmatch(r"[\d][\d\-/.]*", tokens[0]): + return contains_phrase(text, " ".join(tokens[1:])) + return False + + +_DEGREE_PATTERNS = { + "phd": r"ph\.?\s?d\.?", "meng": r"m\.?\s?eng\.?", "mba": r"m\.?b\.?a\.?", + "mph": r"m\.?p\.?h\.?", "jd": r"j\.?d\.?", "md": r"m\.?d\.?", + "ba": r"b\.?a\.?", "bs": r"b\.?s\.?", "ma": r"m\.?a\.?", "ms": r"m\.?s\.?", +} + + +def contains_degree_type(text: Any, *types: str) -> bool: + """Word-boundary degree-type match; ``Ph.D.`` / ``MS`` / ``MBA`` variants accepted.""" + normalized = normalize_text(text) + for value in types: + pattern = _DEGREE_PATTERNS.get(normalize_text(value)) + if not pattern: + raise ValueError(f"unsupported degree type: {value!r}") + if _affirmative_search(rf"(? bool: + """``2 years`` / ``two years`` / ``1.5 years`` / ``18 months`` for a 1.5-year program. + + The integer guard keeps ``1 year`` from matching inside ``1.5 years``. + """ + value = float(years) + if value == int(value): + pattern = rf"(? bool: + """A rendered month/day string without a year, e.g. ``November 30`` / ``February 1``.""" + match = re.fullmatch(r"\s*([A-Za-z]+)\.?\s+(\d{1,2})(?:,?\s+(\d{4}))?\s*", str(literal or "")) + if not match: + raise ValueError(f"unsupported month-day literal: {literal!r}") + month_name = normalize_text(match.group(1)) + month_index = next( + (index for index, month in enumerate(_MONTHS) if month.startswith(month_name[:3])), None + ) + if month_index is None: + raise ValueError(f"unsupported month literal: {literal!r}") + year = int(match.group(3)) if match.group(3) else 2000 + return contains_date(text, _dt.date(year, month_index + 1, int(match.group(2)))) + + +def department_aliases(name: str) -> list[str]: + """A department name plus its acronym (``Department of Electrical Engineering and + Computer Sciences`` → ``electrical engineering and computer sciences`` / ``eecs``).""" + base = re.sub(r"^\s*department of\s+", "", normalize_text(name)).strip() + words = [word for word in re.findall(r"[a-z]+", base) if word not in {"and", "of", "the", "in"}] + if not base or not words: + return [base] if base else [] + return [base, "".join(word[0] for word in words)] + + +def contains_department(text: Any, name: str) -> bool: + """The full department name or its acronym.""" + return any(contains_phrase(text, alias) for alias in department_aliases(name)) + + +def acronym(name: str) -> str: + """Initials of the significant words ("Mathematical Sciences Research Institute" → msri). + + Single-word names have no acronym: a bare initial would match far too much. + """ + words = [word for word in re.findall(r"[a-z]+", normalize_text(name)) + if word not in {"and", "of", "the", "in", "for"}] + if len(words) < 2: + return "" + return "".join(word[0] for word in words) + + +def contains_acronym(text: Any, name: str) -> bool: + """The acronym of a rendered name, as a whole token.""" + initials = acronym(name) + return len(initials) >= 2 and bool( + _affirmative_search(rf"(? int: + """How many distinct ≥4-character interest tokens the answer carries (row binding).""" + tokens = { + token for token in re.findall(r"[a-z0-9]+", normalize_text(interests)) if len(token) >= 4 + } + normalized = normalize_text(text) + return sum( + 1 for token in tokens + if re.search(rf"(? set[str]: + """The candidate phrases that appear affirmatively (set-valued answers).""" + return {value for value in candidates if contains_phrase(text, value)} + + +_STOPWORDS = { + "with", "from", "will", "that", "this", "have", "into", "over", "after", "before", + "wins", "win", "won", "the", "and", "for", "its", "their", "about", "held", "open", +} + + +def title_tokens(title: str, minimum_length: int = 4) -> list[str]: + """Distinctive words of a rendered title (stopwords and short words dropped).""" + tokens = re.findall(r"[a-z0-9][a-z0-9'-]*", normalize_text(title)) + return [token for token in tokens + if len(token) >= minimum_length and token not in _STOPWORDS] + + +def title_tokens_matched(text: Any, title: str, minimum_length: int = 4) -> int: + normalized = normalize_text(text) + return sum( + 1 for token in title_tokens(title, minimum_length) + if re.search(rf"(? list[str]: + """Clause split for the "offering" negatives: commas and contrast words split too.""" + return [clause for clause in re.split( + r"[.!?;:,\n]+|\b(?:but|however|instead|while|whereas)\b", normalize_text(text) + ) if clause.strip()] + + +def contains_near(text: Any, anchor: str, pattern: str, window: int = 100) -> bool: + """``pattern`` (a regex source) must occur within ``window`` chars of ``anchor``.""" + normalized = normalize_text(text) + for match in re.finditer(_phrase_pattern(anchor), normalized): + segment = normalized[max(0, match.start() - window):match.end() + window] + if re.search(pattern, segment): + return True + return False + + +def affirmative_near(text: Any, anchor: str, phrase: str, window: int = 150) -> bool: + """``phrase`` appears, negation-free, inside a window around ``anchor``.""" + normalized = normalize_text(text) + pattern = _phrase_pattern(phrase) + for anchor_match in re.finditer(_phrase_pattern(anchor), normalized): + segment = normalized[max(0, anchor_match.start() - window):anchor_match.end() + window] + if _affirmative_search(pattern, segment): + return True + return False + + +# --------------------------------------------------------------------------- # +# Judge harness +# --------------------------------------------------------------------------- # +class Judge: + def __init__(self, task_id: str): + self.task_id = task_id + self.passed = True + self.reason = "" + self.evidence: list[str] = [] + + def check(self, name: str, condition: bool, evidence: str) -> bool: + marker = "PASS" if condition else "FAIL" + self.evidence.append(f"[{marker}] {name}: {evidence}") + if not condition: + self.passed = False + if not self.reason: + self.reason = name + return condition + + def emit(self) -> None: + result = { + "task_id": self.task_id, + "pass": self.passed, + "reason": self.reason or "all checks passed", + "evidence": self.evidence, + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + raise SystemExit(0 if self.passed else 1) + + +def fail_closed(task_id: str, reason: str, detail: str) -> None: + print( + json.dumps( + { + "task_id": task_id, + "pass": False, + "infra_error": True, + "reason": reason, + "evidence": [f"[FAIL] {reason}: {detail}"], + }, + ensure_ascii=False, + indent=2, + ) + ) + raise SystemExit(1) + + +def _same_local_origin(url: str, start_url: str) -> bool: + try: + observed = urlparse(str(url or "")) + start = urlparse(str(start_url or "")) + return ( + observed.scheme == start.scheme == "http" + and observed.hostname is not None + and start.hostname is not None + and not observed.username + and not observed.password + and observed.port == start.port + and observed.hostname.casefold() == start.hostname.casefold() + and is_site_url(url) + ) + except ValueError: + return False + + +def _screenshots_decode(trajectory: dict[str, Any]) -> tuple[bool, str]: + root = Path(str(trajectory.get("_run_dir") or "")) + steps = trajectory.get("steps") + if not root.is_dir() or not isinstance(steps, list) or not steps: + return False, "run directory or steps are missing" + checked = 0 + for index, step in enumerate(steps): + if not isinstance(step, dict): + return False, f"step {index} is not an object" + for key in ("screenshot_before", "screenshot_after"): + name = step.get(key) + relative = Path(str(name or "")) + if not name or relative.is_absolute() or ".." in relative.parts: + return False, f"step {index} has unsafe {key}" + candidates = (root / "screenshots" / relative, root / relative) + path = next((item for item in candidates if item.is_file()), None) + if path is None: + return False, f"step {index} is missing {key}={name!r}" + try: + with Image.open(path) as image: + image.load() + if image.format != "PNG" or image.width < 1 or image.height < 1: + return False, f"step {index} {key} is not a nonempty PNG" + if image.width < MIN_SCREENSHOT_WIDTH or image.height < MIN_SCREENSHOT_HEIGHT: + return False, ( + f"step {index} {key} is {image.width}x{image.height}; a real viewport " + f"capture must be at least {MIN_SCREENSHOT_WIDTH}x{MIN_SCREENSHOT_HEIGHT}" + ) + except Exception as exc: + return False, f"step {index} {key} cannot decode: {type(exc).__name__}" + checked += 1 + return True, f"decoded {checked} PNG screenshots" + + +def check_trajectory_identity(judge: Judge, trajectory: dict[str, Any], task_id: str) -> None: + judge.check( + "final_answer_nonempty", + bool(final_answer(trajectory)), + f"final_answer={final_answer(trajectory)!r}", + ) + judge.check( + "trajectory_task_matches", + trajectory_task_matches(trajectory, task_id), + f"expected_task_id={task_id!r}, observed_task_id={trajectory.get('task_id')!r}", + ) + steps = trajectory.get("steps") + judge.check( + "trajectory_completed", + trajectory.get("terminated") is True and trajectory.get("termination_reason") == "agent_done", + f"terminated={trajectory.get('terminated')!r}, reason={trajectory.get('termination_reason')!r}", + ) + judge.check("trajectory_has_steps", isinstance(steps, list) and bool(steps), f"steps={len(steps) if isinstance(steps, list) else 'invalid'}") + recorded = trajectory_urls(trajectory) + judge.check( + "all_urls_match_local_origin", + bool(recorded) and all(_same_local_origin(url, trajectory.get("start_url", "")) for url in recorded), + f"start_url={trajectory.get('start_url')!r}, recorded_urls={recorded!r}", + ) + screenshots_ok, screenshot_evidence = _screenshots_decode(trajectory) + judge.check("screenshots_decode", screenshots_ok, screenshot_evidence) + + +def check_signed_in_as(judge: Judge, trajectory: dict[str, Any], email: str) -> None: + judge.check("visited_login_page", navigated_to_path(trajectory, "/login"), "required_path=/login") + login_inputs = trajectory_input_texts(trajectory, on_path="/login") + typed_email = "" + for value in login_inputs: + if _EMAIL_RE.fullmatch(normalize_text(value).strip()): + typed_email = normalize_text(value) + judge.check( + "entered_expected_account_email", + typed_email == normalize_text(email), + f"expected_email={email!r}, last_email_typed_on_login={typed_email!r}", + ) + judge.check( + "entered_account_password_on_login", + any(normalize_text(value) == normalize_text(BENCHMARK_PASSWORD) for value in login_inputs), + "the benchmark password must be typed on /login", + ) + + +def check_visited_path(judge: Judge, trajectory: dict[str, Any], name: str, path: str | re.Pattern[str]) -> bool: + described = f"/{path.pattern}/" if isinstance(path, re.Pattern) else path + return judge.check(name, navigated_to_path(trajectory, path), f"required_path={described}") + + +# --------------------------------------------------------------------------- # +# SQLite state +# --------------------------------------------------------------------------- # +def db_query(db_path: str | os.PathLike[str], sql: str, params: Sequence[Any] = ()) -> list[sqlite3.Row]: + connection = sqlite3.connect(str(db_path)) + connection.row_factory = sqlite3.Row + try: + return connection.execute(sql, params).fetchall() + finally: + connection.close() + + +def fetch_db(container: str, kind: str) -> str: + if kind not in {"instance", "instance_seed"}: + raise ValueError(f"unsupported DB kind: {kind}") + handle, destination = tempfile.mkstemp(prefix=f"{SITE}_{kind}_", suffix=".db") + os.close(handle) + source = f"{container}:/opt/WebSyn/{SITE}/{kind}/{SITE}.db" + result = subprocess.run(["docker", "cp", source, destination], capture_output=True, text=True) + if result.returncode: + Path(destination).unlink(missing_ok=True) + detail = result.stderr.strip() or result.stdout.strip() + raise RuntimeError(f"could not copy {source}: {detail}") + atexit.register(Path(destination).unlink, missing_ok=True) + return destination + + +def resolve_db(explicit_path: str | None, container: str, kind: str) -> str | None: + if explicit_path: + path = Path(explicit_path) + return str(path) if path.is_file() else None + try: + return fetch_db(container, kind) + except (OSError, RuntimeError): + return None + + +def _schema_objects(db_path: str) -> list[tuple[Any, ...]]: + return [ + tuple(row) + for row in db_query( + db_path, + "SELECT type, name, tbl_name, sql FROM sqlite_schema " + "WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' ORDER BY type, name", + ) + ] + + +def catalog_fingerprint(db_path: str | os.PathLike[str]) -> str: + """Row-level fingerprint of the nine seeded tables. + + Recipe (fixed; the pinned value in this module was produced by it): + + payload = [[table, [dict(row) for row in SELECT * FROM table ORDER BY id]] + for table in ALL_TABLES] + sha256(json.dumps(payload, sort_keys=True, separators=(",", ":"), + ensure_ascii=False).encode()).hexdigest() + + It is computed on ``initial.db`` only, so a runtime write can never mask a + seed change. + """ + payload = [ + [table, [dict(row) for row in db_query(db_path, f"SELECT * FROM {table} ORDER BY id")]] + for table in ALL_TABLES + ] + blob = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + return hashlib.sha256(blob.encode()).hexdigest() + + +def _validate_snapshot_contract(initial_db: str, after_db: str) -> None: + table_sql = "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%'" + initial_tables = {row["name"] for row in db_query(initial_db, table_sql)} + after_tables = {row["name"] for row in db_query(after_db, table_sql)} + if initial_tables != EXPECTED_TABLES or after_tables != EXPECTED_TABLES: + raise ValueError(f"unexpected tables: initial={sorted(initial_tables)}, after={sorted(after_tables)}") + initial_schema = _schema_objects(initial_db) + if initial_schema != _schema_objects(after_db): + raise ValueError("initial and after database schemas differ") + schema_hash = hashlib.sha256(json.dumps(initial_schema, separators=(",", ":")).encode()).hexdigest() + if schema_hash != SCHEMA_HASH: + raise ValueError(f"unsupported UC Berkeley schema hash: {schema_hash}") + observed = {table: len(table_rows(initial_db, table)) for table in EXPECTED_COUNTS} + if observed != EXPECTED_COUNTS: + raise ValueError(f"initial database counts differ: expected={EXPECTED_COUNTS}, observed={observed}") + fingerprint = catalog_fingerprint(initial_db) + if fingerprint != CATALOG_FINGERPRINT: + raise ValueError( + f"catalog fingerprint differs from the pinned seed: {fingerprint}; " + "re-freeze the seed contract before grading" + ) + changed = [table for table in IMMUTABLE_TABLES if table_rows(initial_db, table) != table_rows(after_db, table)] + if changed: + raise ValueError(f"immutable catalog tables changed: {changed}") + + +def resolve_snapshots(args: VerifyArgs, task_id: str) -> tuple[str, str]: + """Return validated (initial_db, after_db) snapshots or fail closed.""" + initial_db = resolve_db(args.initial_db, args.container, "instance_seed") + after_db = resolve_db(args.after_db, args.container, "instance") + if not initial_db or not after_db: + fail_closed( + task_id, + "database_unavailable", + "both initial and after berkeley database snapshots are required", + ) + try: + _validate_snapshot_contract(str(initial_db), str(after_db)) + from ground_truth import task_ground_truth + task_number = int(task_id.rsplit("--", 1)[1]) + task_ground_truth(str(initial_db), task_number) + except (ImportError, OSError, sqlite3.Error, ValueError) as exc: + fail_closed(task_id, "snapshot_contract_invalid", str(exc)) + return str(initial_db), str(after_db) + + +def table_rows(db_path: str, table: str) -> list[tuple[Any, ...]]: + if not re.fullmatch(r"[a-z_]+", table): + raise ValueError(f"unsupported table: {table}") + return [tuple(row) for row in db_query(db_path, f"SELECT * FROM {table} ORDER BY 1")] + + +def rows_where(db_path: str, table: str, **filters: Any) -> list[dict[str, Any]]: + if not re.fullmatch(r"[a-z_]+", table): + raise ValueError(f"unsupported table: {table}") + clauses, params = [], [] + for column, value in filters.items(): + if not re.fullmatch(r"[a-z_0-9]+", column): + raise ValueError(f"unsupported column: {column}") + clauses.append(f"{column} = ?") + params.append(value) + where = f" WHERE {' AND '.join(clauses)}" if clauses else "" + return [dict(row) for row in db_query(db_path, f"SELECT * FROM {table}{where} ORDER BY 1", params)] + + +def table_delta(initial_db: str, after_db: str, table: str) -> dict[str, list[Any]]: + before = {row[0]: row for row in table_rows(initial_db, table)} + after = {row[0]: row for row in table_rows(after_db, table)} + common = before.keys() & after.keys() + return { + "added": [after[key] for key in sorted(after.keys() - before.keys())], + "removed": [before[key] for key in sorted(before.keys() - after.keys())], + "changed": [(before[key], after[key]) for key in sorted(common) if before[key] != after[key]], + } + + +def new_table_rows(initial_db: str, after_db: str, table: str) -> list[dict[str, Any]]: + """Rows present in ``after`` whose primary key is absent from ``initial`` (as dicts).""" + initial_ids = {row[0] for row in table_rows(initial_db, table)} + return [row for row in rows_where(after_db, table) if list(row.values())[0] not in initial_ids] + + +def tables_unchanged(initial_db: str, after_db: str, tables: Iterable[str]) -> dict[str, bool]: + return {table: table_rows(initial_db, table) == table_rows(after_db, table) for table in tables} + + +def check_tables_unchanged(judge: Judge, initial_db: str, after_db: str, tables: Iterable[str], prefix: str = "") -> None: + """One ``_unchanged`` check per table.""" + for table, same in tables_unchanged(initial_db, after_db, tables).items(): + judge.check( + f"{prefix}{table}_unchanged", + same, + f"table={table}, initial_rows={len(table_rows(initial_db, table))}, " + f"after_rows={len(table_rows(after_db, table))}, identical={same}", + ) + + +def check_read_only(judge: Judge, initial_db: str, after_db: str) -> None: + """Read-only tasks: every seeded table must be row-identical. + + No GET path writes the DB (the article view counter was removed in commit 1), + so this is strict — there is no column-level whitelist. + """ + check_tables_unchanged(judge, initial_db, after_db, READ_ONLY_TABLES, prefix="read_only_") + + +def check_exact_delta( + judge: Judge, initial_db: str, after_db: str, table: str, added: int = 0, removed: int = 0, changed: int = 0 +) -> dict[str, list[Any]]: + delta = table_delta(initial_db, after_db, table) + judge.check( + f"{table}_exact_delta", + len(delta["added"]) == added and len(delta["removed"]) == removed and len(delta["changed"]) == changed, + f"expected added={added} removed={removed} changed={changed}; delta={delta!r}", + ) + return delta + + +def user_id_for_email(db_path: str, email: str) -> int | None: + rows = db_query(db_path, "SELECT id FROM users WHERE lower(email) = lower(?) ORDER BY id LIMIT 1", (email,)) + return int(rows[0]["id"]) if rows else None + + +def user_emails(db_path: str) -> set[str]: + return {normalize_text(row["email"]) for row in db_query(db_path, "SELECT email FROM users") if row["email"]} + + +def bookmark_rows(db_path: str, user_id: int | None = None) -> list[dict[str, Any]]: + if user_id is None: + return rows_where(db_path, "bookmarks") + return rows_where(db_path, "bookmarks", user_id=int(user_id)) + + +def bookmark_delta(initial_db: str, after_db: str, user_id: int | None = None) -> dict[str, list[Any]]: + """Bookmark row delta for one user (or all users when ``user_id`` is None).""" + before = {row["id"]: row for row in bookmark_rows(initial_db, user_id)} + after = {row["id"]: row for row in bookmark_rows(after_db, user_id)} + return { + "added": [after[key] for key in sorted(after.keys() - before.keys())], + "removed": [before[key] for key in sorted(before.keys() - after.keys())], + "changed": [(before[key], after[key]) for key in sorted(before.keys() & after.keys()) + if before[key] != after[key]], + } + + +def bookmark_identity(row: Any) -> tuple[int, str, int]: + """``(user_id, item_type, item_id)`` of a bookmark row.""" + return (int(row["user_id"]), str(row["item_type"]), int(row["item_id"])) + + +def check_bookmarks_delta( + judge: Judge, + initial_db: str, + after_db: str, + *, + user_id: int, + added: Sequence[Any] = (), + surviving_ids: Sequence[int] | None = None, +) -> dict[str, list[Any]]: + """Exact bookmark delta for one user plus the identities that must survive. + + ``added`` lists the expected ``(user_id, item_type, item_id)`` identities of + the added rows. ``surviving_ids`` pins the row ids that must still exist — + the ``--31`` ordering proof (a surviving row whose id is 2 can only exist if + the id-1 row was inserted and then deleted). + """ + delta = bookmark_delta(initial_db, after_db, user_id) + observed_added = sorted(bookmark_identity(row) for row in delta["added"]) + expected_added = sorted(tuple(identity) for identity in added) + judge.check( + "bookmarks_exact_delta", + len(delta["added"]) == len(expected_added) + and len(delta["removed"]) == 0 + and len(delta["changed"]) == 0 + and observed_added == expected_added, + f"expected added={expected_added} removed=[] changed=[]; observed added={observed_added!r} " + f"removed={[bookmark_identity(row) for row in delta['removed']]!r} " + f"changed={delta['changed']!r}", + ) + global_delta = table_delta(initial_db, after_db, "bookmarks") + judge.check( + "bookmarks_other_users_unchanged", + {row[0] for row in global_delta["added"]} == {row["id"] for row in delta["added"]} + and {row[0] for row in global_delta["removed"]} == {row["id"] for row in delta["removed"]} + and {pair[0][0] for pair in global_delta["changed"]} == {pair[0]["id"] for pair in delta["changed"]}, + f"bookmark rows changed outside user_id={user_id}: global_added={len(global_delta['added'])}, " + f"user_added={len(delta['added'])}, global_removed={len(global_delta['removed'])}, " + f"user_removed={len(delta['removed'])}, global_changed={len(global_delta['changed'])}, " + f"user_changed={len(delta['changed'])}", + ) + if surviving_ids is not None: + surviving = {int(row["id"]) for row in bookmark_rows(after_db, user_id)} + judge.check( + "bookmarks_surviving_row_ids", + surviving == {int(value) for value in surviving_ids}, + f"expected surviving_row_ids={sorted(int(v) for v in surviving_ids)}; observed={sorted(surviving)}", + ) + return delta From 0ff43bfb1780dcbccadfd509c8c2440fed5ce6cd Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 01:59:10 -0400 Subject: [PATCH 07/25] chore(berkeley): task selection, verifier_path and judge_rubric backfill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tasks.jsonl: 22 rows — the 20 kept ids (with --27 re-anchored onto the two exact programme durations) plus the new stateful --30/--31. The 19 unchanged rows keep the contributor's exact ques text; every row gains verifier_path and a rules-only judge_rubric (shared scoring preamble + per-task checkpoints). "web" stays http://localhost:40026/. - verify/TASK_REVIEW.md: all 32 rows (30 contributor + 2 reviewer) with ACCEPT / DROP / ADDED and the workflow each accepted row must follow, plus the corrections found while deriving the targets (frozen-clock Lecture count, the AI-family allowlist for --7, BIDS' four focus areas and its rendered related-centre set, and the article-view write removal). - verify/README.md: the contract per task, the snapshot rules, the matcher semantics, and how to run the verifiers and their tests. - verify/tests/test_tasks_contract.py: validates the file (22 rows, seven keys, existing verifier paths, unique rubrics) and re-derives every target to prove no rubric carries a ground-truth value — with a self-test that the leak scan actually fires on a planted answer. The image already excludes verify/tests/ via the existing .dockerignore pattern (sites/*/verify/tests/), the same rule the merged peers rely on; no change was needed there. Run: .venv/bin/python -m pytest sites/berkeley/verify/tests -q -> 524 passed Co-Authored-By: Claude Code --- sites/berkeley/tasks.jsonl | 52 ++--- sites/berkeley/verify/README.md | 82 +++++++ sites/berkeley/verify/TASK_REVIEW.md | 64 +++++ .../verify/tests/test_tasks_contract.py | 218 ++++++++++++++++++ 4 files changed, 386 insertions(+), 30 deletions(-) create mode 100644 sites/berkeley/verify/README.md create mode 100644 sites/berkeley/verify/TASK_REVIEW.md create mode 100644 sites/berkeley/verify/tests/test_tasks_contract.py diff --git a/sites/berkeley/tasks.jsonl b/sites/berkeley/tasks.jsonl index 1f8905142..9f588a41b 100644 --- a/sites/berkeley/tasks.jsonl +++ b/sites/berkeley/tasks.jsonl @@ -1,30 +1,22 @@ -{"web_name": "UC Berkeley", "id": "UC Berkeley--0", "ques": "Find all PhD programs offered at UC Berkeley and count how many there are. List their names.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--1", "ques": "Search for MBA programs at UC Berkeley. Which school offers the MBA and what is the program duration?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--2", "ques": "Find the Computer Science BS program at UC Berkeley. What are the program requirements listed on the program detail page?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--3", "ques": "Browse the news section on the UC Berkeley site and find all articles in the 'Research' category. How many research articles are listed on the first page?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--4", "ques": "Find news articles about CRISPR or gene editing on the Berkeley site. Who is the featured scientist and what award did they receive?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--5", "ques": "Look at the upcoming events at UC Berkeley. Find an event in the 'Career' category and note the date, location, and whether registration is required.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--6", "ques": "Find events categorized as 'Lecture' at UC Berkeley. List at least three lecture events with their dates and locations.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--7", "ques": "Browse the faculty directory at UC Berkeley and find a professor in the EECS department who works on artificial intelligence. What are their specific research interests?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--8", "ques": "Find the faculty profile for Jennifer Doudna at UC Berkeley. What is her title and what is her primary research area?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--9", "ques": "Search for research centers related to 'artificial intelligence' on the Berkeley website. List the names of any AI-related research centers you find.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--10", "ques": "Find the Berkeley Artificial Intelligence Research Lab (BAIR) on the UC Berkeley site. Who is the director and what year was it founded?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--11", "ques": "Go to the Admissions page at UC Berkeley. What is the application deadline for freshman applicants and what is the current acceptance rate?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--12", "ques": "Find all programs offered by the Haas School of Business at UC Berkeley. What degree types are available (e.g., MBA, PhD, etc.)?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--13", "ques": "Browse the Departments page at UC Berkeley and find the Department of Electrical Engineering and Computer Sciences (EECS). Who is the department chair and where is the department located?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--14", "ques": "Find the College of Engineering at UC Berkeley. How many undergraduate students and graduate students are enrolled? Who is the dean?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--15", "ques": "Search for 'climate' on the UC Berkeley website. What types of results appear (programs, news, events, faculty, research)? Name at least one result from each category that appears.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--16", "ques": "Find all online degree programs at UC Berkeley. Which programs offer an online option and what school do they belong to?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--17", "ques": "Look at the About Berkeley page. How many Nobel Laureates are currently on faculty, how many varsity sports does Berkeley have, and how many NCAA national titles has Berkeley won?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--18", "ques": "Find upcoming 'Arts' events at UC Berkeley within the next two months. List the events with their dates, venues, and ticket prices.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--19", "ques": "Navigate to the Berkeley news section and filter by the 'Athletics' category. Find a news article about a Berkeley sports championship and summarize what sport and what the achievement was.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--20", "ques": "Find the JD (Juris Doctor) program at Berkeley Law. What is the program duration, application deadline, and which school offers it?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--21", "ques": "Search for faculty who work on 'quantum computing' at UC Berkeley. List all faculty members who appear in the results and their department affiliations.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--22", "ques": "Go to the College of Letters and Science at UC Berkeley and find all the departments listed under it on the Departments page. How many departments belong to the College of Letters and Science?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--23", "ques": "Find the research center 'Berkeley Institute for Data Science' (BIDS). What are its focus areas and who is the director? Then find if there are any related research centers listed on the same page.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--24", "ques": "Find the Economics PhD program at Berkeley. Then navigate to the Economics department page and identify the department chair and the other programs offered by the department. Finally, find one Economics faculty member and note their research interests.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--25", "ques": "Berkeley holds an annual Spring Career Fair. Find this event, note the date, location, and whether registration is required. Then find two other career-related events and compare their details.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--26", "ques": "Find Berkeley's Nobel Laureate professors. Navigate to the faculty profiles of at least two Nobel Laureates. What prizes did they win and what are their research interests?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--27", "ques": "Search the Berkeley site for 'Master of Engineering'. Find the MEng program, identify which department offers it, and compare it to other master's programs in the same college. How does the duration differ?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--28", "ques": "Find all programs that require GRE scores at UC Berkeley. Navigate to the programs page and identify which programs have 'GRE Required' indicated. What degree types most commonly require GRE?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} -{"web_name": "UC Berkeley", "id": "UC Berkeley--29", "ques": "Go to the Berkeley homepage and identify the key statistics listed: how many students attend Berkeley (undergrad and grad separately), how many degree programs are offered, and what is Berkeley's ranking as a public research university? Then navigate to the About page and confirm these numbers.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/"} +{"web_name": "UC Berkeley", "id": "UC Berkeley--1", "ques": "Search for MBA programs at UC Berkeley. Which school offers the MBA and what is the program duration?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_1.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the MBA programme's detail page must have been opened from the programme listing; the answer must name the school that offers the MBA and the programme duration as printed on that page; a duration taken from another programme or from general knowledge fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--2", "ques": "Find the Computer Science BS program at UC Berkeley. What are the program requirements listed on the program detail page?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_2.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Computer Science BS detail page must have been opened (not the same-name MS or PhD pages); the answer must list at least four of the requirement items printed on that page; items belonging to the other Computer Science programmes fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--4", "ques": "Find news articles about CRISPR or gene editing on the Berkeley site. Who is the featured scientist and what award did they receive?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_4.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the article's detail page must have been opened from the news listing; the answer must name the scientist the article is about and the award the article reports as received; an award supplied from general knowledge fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--6", "ques": "Find events categorized as 'Lecture' at UC Berkeley. List at least three lecture events with their dates and locations.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_6.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Lecture-filtered events page must have been opened; the answer must list at least three Lecture events, each with the date and the location printed for it on that page; events of other categories, or dates and locations that do not match the listed event, fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--7", "ques": "Browse the faculty directory at UC Berkeley and find a professor in the EECS department who works on artificial intelligence. What are their specific research interests?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_7.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: a faculty-directory route to the EECS department must have been opened and the named professor's own profile page visited; the professor must be an EECS faculty member and the reported research interests must be those printed on that profile; another professor's interests fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--10", "ques": "Find the Berkeley Artificial Intelligence Research Lab (BAIR) on the UC Berkeley site. Who is the director and what year was it founded?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_10.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the centre's page must have been opened; the answer must give the director and the founding year printed on that page; a founding year supplied from general knowledge fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--11", "ques": "Go to the Admissions page at UC Berkeley. What is the application deadline for freshman applicants and what is the current acceptance rate?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_11.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Admissions page must have been opened; the answer must give the freshman application deadline and the acceptance rate printed on that page; deadlines of other applicant types and rates supplied from general knowledge fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--12", "ques": "Find all programs offered by the Haas School of Business at UC Berkeley. What degree types are available (e.g., MBA, PhD, etc.)?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_12.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the school-filtered programme list must have been opened; the answer must state the single degree type that list shows and must not attribute any other degree type to that school; a multi-programme answer fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--13", "ques": "Browse the Departments page at UC Berkeley and find the Department of Electrical Engineering and Computer Sciences (EECS). Who is the department chair and where is the department located?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_13.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the EECS department page must have been opened; the answer must give the chair and the department location printed there; another department's or another person's values fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--14", "ques": "Find the College of Engineering at UC Berkeley. How many undergraduate students and graduate students are enrolled? Who is the dean?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_14.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the page listing the schools and colleges must have been opened; the answer must give that college's undergraduate and graduate enrolment counts and its dean as printed on its entry; university-wide totals fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--16", "ques": "Find all online degree programs at UC Berkeley. Which programs offer an online option and what school do they belong to?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_16.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the programme listing must have been opened and the single online programme's detail page visited; the answer must name that programme, its degree type and the school it belongs to; naming more than one programme as offering an online option fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--17", "ques": "Look at the About Berkeley page. How many Nobel Laureates are currently on faculty, how many varsity sports does Berkeley have, and how many NCAA national titles has Berkeley won?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_17.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the About page must have been opened; the answer must give all three statistics printed on it (faculty Nobel laureates, varsity sports, NCAA national titles); the page's separate \"more than N Nobel Prizes\" line is not the faculty count and using it fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--19", "ques": "Navigate to the Berkeley news section and filter by the 'Athletics' category. Find a news article about a Berkeley sports championship and summarize what sport and what the achievement was.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_19.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Athletics-filtered news listing must have been opened and the chosen article's page visited; the article must be about a championship win, and the answer must report the sport and the achievement that article describes; a non-championship article, or a sport or achievement that does not match the opened article, fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--20", "ques": "Find the JD (Juris Doctor) program at Berkeley Law. What is the program duration, application deadline, and which school offers it?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_20.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the JD detail page must have been opened; the answer must give the duration, the application deadline and the school printed there; other programmes' values fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--22", "ques": "Go to the College of Letters and Science at UC Berkeley and find all the departments listed under it on the Departments page. How many departments belong to the College of Letters and Science?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_22.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Departments page must have been opened; the answer must give the number of departments listed under the College of Letters and Science and name several of them; the site-wide department total fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--23", "ques": "Find the research center 'Berkeley Institute for Data Science' (BIDS). What are its focus areas and who is the director? Then find if there are any related research centers listed on the same page.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_23.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the centre's page must have been opened; the answer must give the director and the focus areas printed there, and name at least one of the related centres that same page lists; a centre the page does not list fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--24", "ques": "Find the Economics PhD program at Berkeley. Then navigate to the Economics department page and identify the department chair and the other programs offered by the department. Finally, find one Economics faculty member and note their research interests.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_24.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: all three hops must have been opened in order: the programme page, then the department page, then a department faculty member's profile; the answer must give the department chair, the department's programme list and that faculty member's research interests as printed on those pages; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--25", "ques": "Berkeley holds an annual Spring Career Fair. Find this event, note the date, location, and whether registration is required. Then find two other career-related events and compare their details.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_25.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Career-filtered events listing must have been opened and the named fair's detail page visited; the answer must give that fair's date, location and registration status as printed, plus details of two further career events from the same listing; details that do not match the listed events fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--27", "ques": "Search the Berkeley site for 'Master of Engineering'. Find the MEng program and the Computer Science MS program. Report which department offers the Master of Engineering, and give the duration of each of the two programs exactly as listed on their program detail pages.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_27.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: both programme detail pages must have been opened (the Master of Engineering and the Computer Science MS); the answer must give the department printed for the Master of Engineering and both programme durations as printed on those pages; swapped or general-knowledge durations fail; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--28", "ques": "Find all programs that require GRE scores at UC Berkeley. Navigate to the programs page and identify which programs have 'GRE Required' indicated. What degree types most commonly require GRE?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_28.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the programme listings that carry the GRE badge must have been opened (both the PhD and the MS filtered listings, or the full listing); the answer must give the number of programmes showing the badge and the degree type that most commonly requires it; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--30", "ques": "Log in to the UC Berkeley website with the demo account (email: alice@berkeley.edu, password: test1234). Open the research center page for the Berkeley Seismological Laboratory and save the center to your bookmarks. Then open My Account and confirm the center is listed there, and report the director shown for the center.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_30.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the sign-in page must have been used with the demo account, the named centre's page opened and saved from there, and My Account opened afterwards; the answer must name the centre, report its director as printed, and confirm it is listed under My Account; a claim of success without the My Account visit fails; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--31", "ques": "Log in to the UC Berkeley website with the demo account (email: bob@berkeley.edu, password: test1234). First save the Mathematical Sciences Research Institute to your bookmarks, then save the California Policy Lab. Open My Account and remove the Mathematical Sciences Research Institute bookmark, leaving only the other center saved; check My Account again to confirm which center remains. Report which center remains saved and its director as shown on the center page.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_31.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the sign-in page must have been used with the demo account; both centre pages must be opened and saved in the order the task gives; My Account must be opened, the first bookmark removed, and My Account consulted again to confirm what remains; the answer must name the centre that remains with its director as printed and state that the other was removed; an empty answer fails."} diff --git a/sites/berkeley/verify/README.md b/sites/berkeley/verify/README.md new file mode 100644 index 000000000..dcc997b0e --- /dev/null +++ b/sites/berkeley/verify/README.md @@ -0,0 +1,82 @@ +# UC Berkeley deterministic grading contract + +Each row in `sites/berkeley/tasks.jsonl` points to `verify_1.py` … `verify_31.py` (22 verifiers, one +per row; the ids are the contributor's, so numbers are not contiguous). The wrappers use +`verify_lib.py` for package, URL, answer and state validation and `ground_truth.py` to re-derive +every target from the supplied initial SQLite snapshot. No verifier calls an LLM; a verdict never +depends on a key or a model. `TASK_REVIEW.md` records the per-row ACCEPT/DROP/ADDED decisions. + +## Inputs + +```bash +python sites/berkeley/verify/verify_1.py \ + --run_dir /absolute/path/to/run \ + --initial_db /absolute/path/to/initial.db \ + --after_db /absolute/path/to/after.db +``` + +If explicit snapshots are omitted, the verifier checks `/initial.db` and +`/after.db`, then falls back to `docker cp` from `$WH_CONTAINER` (default `wh-review`): +`instance_seed/berkeley.db` is the initial state and `instance/berkeley.db` the after state. Missing +or invalid inputs fail closed (`infra_error: true`, exit 1). Output is JSON with `task_id`, `pass`, +`reason` (the first failing check) and `evidence`; exit code 0 means PASS and 1 means FAIL. +`agent_demo/eval_judge.py --run_dir --verifier True` is the normal entry point; `--no_llm` is +accepted for parity and ignored. + +## Snapshot contract + +Both snapshots must carry the exact nine-table UC Berkeley schema (hash pinned), the frozen seed +counts (14 colleges, 30 departments, 83 programmes, 82 faculty, 25 research centres, 121 news +articles, 64 events, 4 users, 0 bookmarks) and the row-level catalog fingerprint pinned in +`verify_lib.py`. `ground_truth.py` then re-derives the task's target the way the app renders it — +the `BENCHMARK_NOW = 2026-05-12` event filter, `PER_PAGE`, the app's `ORDER BY` clauses and the +unordered `LIMIT 3` related-centres query — and fails closed on drift. The seven catalog tables must +be row-identical before and after; `users` and `bookmarks` are the only runtime tables. + +Read-only tasks (everything except 30 and 31) require **every** seeded table to be row-identical, +so an incidental bookmark, registration or catalog write fails. The stateful verifiers (30, 31) +check the exact bookmark row delta for the demo account first (`bookmarks_exact_delta`), then that no +other user's bookmarks changed (`bookmarks_other_users_unchanged`), then the row-id binding where the +task fixes the order (`bookmarks_surviving_row_ids`, task 31), then that `users` is unchanged. A run +that self-reports success without writing the row fails on the delta; a skipped removal, a reversed +add order or an extra save each fail on a named check. + +## Gates and answer matchers + +Navigation gates require an exact mirror path (any loopback port) carrying every required query +parameter; a listing hit never replaces a detail visit, and the final action's declared target +counts as a visit so a run that ends on a `navigate` is not penalised. Multi-hop tasks (19, 24, 25, +30, 31) gate each hop in order. The catalog-scan tasks (16, 28) accept either the filtered listings +or several pages of the full listing. + +Answer matchers are negation-aware whole-token matches: names (titles ignored), locations (leading +room numbers optional), counts (thousands separators and word forms, with "12" never matching inside +"1,200" or "14.4"), years, percentages, dates in six formats, month-day literals, degree types +(`Ph.D.` variants), durations (`1 year` never matches inside `1.5 years`; `18 months` accepted for +1.5), interest tokens bound to the named row, and title-token binding for events and articles. +A value the task derives is additionally protected against a confirming contrast being mistaken for +negation: "founded in 2013, not 2017" is affirmative, while "2013 was not the founding year" is not. + +## Source-rendered values + +Two rows (11 and 17) depend on values the app renders from tracked source rather than the DB. +`ground_truth.py` parses `templates/admissions.html` and `app.py` and fails closed if the labelled +literals move; `verify/tests` asserts they stay source literals and never become DB-derived. Row 17 +additionally rejects the page's "more than N Nobel Prizes" line when it is claimed as the faculty +count (a clause-local rule, so quoting the alumni line elsewhere is not a wrong answer). + +## Tests + +```bash +python -m pytest sites/berkeley/verify/tests -q +``` + +The fixtures copy the frozen seed and rewrite only `bookmarks` (stdlib `sqlite3`), so every fixture +DB reproduces the pinned fingerprint; trajectories follow the `agent_demo/agent.py` run signature. +Per task: genuine PASS, no-op, wrong task id, another task's trajectory, shortcuts (including +catalog-wide-token searches), one or two wrong answers, alternative phrasings, a negated answer, a +truncated run, corrupt and 1×1 PNG screenshots, a missing `after.db`, catalog and schema drift, an +incidental write, and — for 30/31 — the state-mismatch, wrong-target, wrong-order and collateral +cases. `test_tasks_contract.py` validates `tasks.jsonl` (22 rows, seven keys, verifier paths, and no +derived answer value in any rubric). `test_verify_lib.py` covers each matcher's accepted and rejected +forms, the gate semantics, the fingerprint recipe and the source-fact rules. diff --git a/sites/berkeley/verify/TASK_REVIEW.md b/sites/berkeley/verify/TASK_REVIEW.md new file mode 100644 index 000000000..125dfda30 --- /dev/null +++ b/sites/berkeley/verify/TASK_REVIEW.md @@ -0,0 +1,64 @@ +# UC Berkeley task review + +All 32 rows (the contributor's 30 plus the reviewer's 2) were re-grounded against the *built* +`instance_seed/berkeley.db` and the templates the app renders — not against the contributor's +summaries. Task URLs use UC Berkeley's registered site index 26 and port `40026`; the verifiers +accept any loopback port, so alt-port review runs grade identically. + +Verdicts: **ACCEPT** (kept, graded), **DROP** (removed from `tasks.jsonl`), **ADDED** (written by +the reviewer). Values in the "Required visible workflow / ground truth" column are the reviewer's +record of what the snapshot derives; none of them appears in `tasks.jsonl` — the validator in +`verify/tests/test_tasks_contract.py` fails if one does. + +| Row | Verdict | Required visible workflow / ground truth | +|---:|---|---| +| 0 | DROP | *label-visible count*: `/programs?degree=PhD` prints "Showing 20 of 25 programs", so the count is readable without opening anything (CONTRIBUTING: no count answers when list counts are visible). Replaced by 27, re-anchored. | +| 1 | ACCEPT | programme search → MBA detail page; school and duration: Haas School of Business, 2 years | +| 2 | ACCEPT | Computer Science BS detail page (not the same-name MS/PhD pages); ≥4 of its requirement items; sibling programmes' items fail | +| 3 | DROP | *page-size artifact*: Research holds 42 articles, so the "first page" is exactly PER_PAGE = 20; overlaps 4/19 | +| 4 | ACCEPT | news search → CRISPR article detail; scientist and award: Jennifer Doudna, National Medal of Science (prior knowledge supplies "Nobel Prize" → wrong) | +| 5 | DROP | *date*: would need the frozen clock only; 25 already covers Career with a name anchor | +| 6 | ACCEPT | `/events` filtered to Lecture (15 upcoming of 19 seeded); ≥3 events with date and location | +| 7 | ACCEPT | faculty-directory route to EECS → named professor's profile; the AI-family allowlist has 11 EECS rows (the literal phrase "artificial intelligence" matches 1), and the reported interests must be that row's | +| 8 | DROP | *prior-knowledge*: Doudna's title/research area is a two-token recall; her award is already 4's anchor | +| 9 | DROP | *distractor breadth* + overlap: one AI-related centre only (BAIR), which 10 asks about by name | +| 10 | ACCEPT | BAIR centre page; director and founding year: Prof. Pieter Abbeel, 2013 (the real BAIR is 2017 → anti-recall) | +| 11 | ACCEPT | Admissions page; freshman deadline November 30 and acceptance rate 14.4% (source-rendered, not DB) | +| 12 | ACCEPT | Haas-filtered programme list; exactly one programme (Business Administration, MBA) — contradicts the "MBA, PhD…" prior | +| 13 | ACCEPT | EECS department page; chair and location: Prof. James Demmel, 253 Cory Hall | +| 14 | ACCEPT | schools-and-colleges page; College of Engineering: 4,500 undergraduate and 3,200 graduate students, Dean Tsu-Jae King Liu (university-wide totals are the distractor) | +| 15 | DROP | *ill-posed*: "name one from each category that appears" — `climate` has no research-centre hit, so it invites a hallucinated answer | +| 16 | ACCEPT | programme listing → Data Science MS detail; exactly one online programme, School of Information | +| 17 | ACCEPT | About page; 12 faculty Nobel laureates, 30 varsity sports, 105 NCAA national titles (the page's "more than 107 Nobel Prizes" line is the distractor) | +| 18 | DROP | *date*: "within the next two months" is unsatisfiable against the frozen calendar | +| 19 | ACCEPT | Athletics-filtered news → championship article detail; sport and achievement bound to the opened article (2 championship articles of 7) | +| 20 | ACCEPT | JD detail page; 3 years, February 1, School of Law (the Optometry MD shares the deadline) | +| 21 | DROP | *distractor breadth* + overlap: one faculty row for "quantum computing"; a thinner duplicate of 7 | +| 22 | ACCEPT | Departments page; 8 departments under the College of Letters and Science, named (the site-wide total of 30 fails) | +| 23 | ACCEPT | BIDS centre page; director Prof. David Culler, four focus areas, and ≥1 of the three related centres the page renders | +| 24 | ACCEPT | Economics PhD → Economics department → a department member's profile; chair Prof. Ulrike Malmendier, the department's BA + PhD, that member's interests | +| 25 | ACCEPT | Career-filtered events → Spring Career Fair 2026 detail (2026-05-17, Recreational Sports Facility, registration required) + two further career events | +| 26 | DROP | *prior-knowledge*: Nobel prizes are world knowledge and the seeded interests are generic; overlaps 4/8 | +| 27 | ACCEPT | **re-anchored** (was: "how does the duration differ", the fuzziest answer in the set): both detail pages must be opened and both durations given exactly — Master of Engineering, 1 year (offered by the Department of Electrical Engineering and Computer Sciences), Computer Science MS, 1.5 years. Old ques: *"Search the Berkeley site for 'Master of Engineering'. Find the MEng program, identify which department offers it, and compare it to other master's programs in the same college. How does the duration differ?"* New ques: see the row in `tasks.jsonl`. | +| 28 | ACCEPT | programme listings carrying the GRE badge (both degree-filtered listings, or four pages of the full listing); 17 programmes, most commonly PhD (13 of 17) | +| 29 | DROP | *count/duplicate*: same About page and statistics as 17, second visit | +| 30 | ADDED | **stateful**: sign in as alice → Berkeley Seismological Laboratory centre page → save → My Account; DB check: exactly one new bookmark row for alice on that centre. Ques text: *"Log in to the UC Berkeley website with the demo account (email: alice@berkeley.edu, password: test1234). Open the research center page for the Berkeley Seismological Laboratory and save the center to your bookmarks. Then open My Account and confirm the center is listed there, and report the director shown for the center."* | +| 31 | ADDED | **stateful**: sign in as bob → save the Mathematical Sciences Research Institute, then the California Policy Lab → My Account → remove the first → My Account again; DB check: exactly one surviving bookmark row on the second centre **with row id 2** (the seed's `bookmarks` table is empty, so two inserts take ids 1 and 2 and deleting id 1 leaves id 2 — a skipped removal, a wrong removal, a reversed add order or a missing first insert each fail on a named check). Ques text: see the row in `tasks.jsonl`. | + +## Corrections recorded during derivation + +* `REVIEW_STATUS` §5 said the Lecture filter holds 19 events; against the frozen clock + (`BENCHMARK_NOW = 2026-05-12`) `/events` admits 52 of 64 rows, of which 15 are Lecture. The + verifier accepts any seeded Lecture event (the `date=all` view also works) with a minimum of three. +* `REVIEW_STATUS` §5 said "≥8 [EECS faculty] match artificial intelligence"; the app's `ilike` + over the literal phrase matches one row. The AI-family allowlist (11 rows) is the rule the task + implies, and every accepted answer must still bind its quoted interests to the named row. +* BIDS has four focus areas (not three), and the "related centres" the page shows are the three + rows its unordered `LIMIT 3` query returns — naming any other same-college centre fails. +* `GET /news/` used to increment `view_count` and commit. That made a read-only task write the + DB and broke the byte-identical reset invariant, so it was removed before the verifiers were + written; the column is kept and still renders the frozen seed values. + +Read-only tasks are verified by comparing every seeded table before and after execution (there is no +column-level whitelist — no GET path writes the DB). The two stateful rows are verified by the exact +bookmark row delta for the demo account plus an unchanged-everything-else check. diff --git a/sites/berkeley/verify/tests/test_tasks_contract.py b/sites/berkeley/verify/tests/test_tasks_contract.py new file mode 100644 index 000000000..06b83d017 --- /dev/null +++ b/sites/berkeley/verify/tests/test_tasks_contract.py @@ -0,0 +1,218 @@ +"""The tasks.jsonl contract, and the rule that no rubric carries a ground-truth value. + +``sites/berkeley/tasks.jsonl`` is agent-facing: the rubric may state *rules* but +must not contain any value the verifiers derive from the snapshot. This module +re-derives every target and asserts that no derived phrase, number or +distinctive token appears in the rubrics — unless the task text itself already +names it (then it is not a leak). +""" +from __future__ import annotations + +import json +import os +import re +import sqlite3 +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import SITE_DIR, SEED_DB # noqa: E402 + +VERIFY_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(VERIFY_DIR)) + +import ground_truth # noqa: E402 + +TASKS = SITE_DIR / "tasks.jsonl" +EXPECTED_KEYS = {"web_name", "id", "ques", "web", "upstream_url", "verifier_path", "judge_rubric"} +EXPECTED_IDS = [1, 2, 4, 6, 7, 10, 11, 12, 13, 14, 16, 17, 19, 20, 22, 23, 24, 25, 27, 28, 30, 31] + +# Tokens that appear across many derived values are domain vocabulary, not answers. +GENERIC_TASK_FREQUENCY = 4 + + +def load_rows() -> list[dict]: + return [json.loads(line) for line in TASKS.read_text(encoding="utf-8").splitlines() if line.strip()] + + +def strings_in(value) -> list[str]: + """Every string (and numeric) literal inside a derived fact, recursively.""" + found: list[str] = [] + if isinstance(value, dict): + for item in value.values(): + found.extend(strings_in(item)) + elif isinstance(value, (list, tuple)): + for item in value: + found.extend(strings_in(item)) + elif isinstance(value, bool): + pass + elif isinstance(value, (int, float)): + found.append(f"{value:g}") + elif isinstance(value, str): + found.append(value) + return found + + +# Long prose (programme/article descriptions) is rendered on the page but is not +# an answer value; only short labels and names are treated as leak candidates. +MAX_VALUE_LENGTH = 100 +MAX_VALUE_TOKENS = 10 + + +def answer_values(facts: dict) -> list[str]: + return [ + value for value in strings_in({key: value for key, value in facts.items() if key != "task"}) + if len(value.strip()) <= MAX_VALUE_LENGTH and len(value.split()) <= MAX_VALUE_TOKENS + ] + + +def forbidden_for(facts: dict, ques: str, generic_tokens: set[str]) -> set[str]: + """Phrases, numbers and distinctive tokens that must not appear in the rubric. + + Anything the task text itself names is not a leak; tokens are compared with a + crude stem rule so "requirement" matches the task's "requirements". + """ + values = answer_values(facts) + lowered_ques = ques.lower() + ques_tokens = set(re.findall(r"[a-z]{4,}", lowered_ques)) + + def in_ques_token(token: str) -> bool: + return any(token == other or token.startswith(other) or other.startswith(token) + for other in ques_tokens) + + phrases = { + value.strip().lower() for value in values + if len(value.strip()) >= 4 and value.strip().lower() not in lowered_ques + } + numbers = {number_text for value in values for number_text in re.findall(r"\d+(?:\.\d+)?", value)} + tokens = {token for value in values for token in re.findall(r"[a-z]{4,}", value.lower())} + return phrases | numbers | { + token for token in tokens if not in_ques_token(token) and token not in generic_tokens + } + + +def leaks_in(rubric: str, forbidden: set[str]) -> list[str]: + """Forbidden phrases/numbers anywhere; single tokens only as whole words.""" + lowered = rubric.lower() + found = [] + for value in sorted(forbidden): + if len(value) < 4: + continue + if re.fullmatch(r"[a-z]+", value): + if re.search(rf"\b{re.escape(value)}\b", lowered): + found.append(value) + elif value in lowered: + found.append(value) + return found + + +def generic_vocabulary(all_facts: dict[int, dict]) -> set[str]: + """Tokens that recur across many tasks are domain vocabulary, not answers.""" + per_task = { + number: { + token + for value in answer_values(facts) + for token in re.findall(r"[a-z]{4,}", value.lower()) + } + for number, facts in all_facts.items() + } + counts: dict[str, int] = {} + for tokens in per_task.values(): + for token in tokens: + counts[token] = counts.get(token, 0) + 1 + return {token for token, count in counts.items() if count >= GENERIC_TASK_FREQUENCY} + + +class TaskFileContractTests(unittest.TestCase): + def setUp(self) -> None: + self.rows = load_rows() + self.facts = ground_truth.all_ground_truth(str(SEED_DB)) + self.generic_tokens = generic_vocabulary(self.facts) + + def test_row_count_ids_and_keys(self) -> None: + self.assertEqual(len(self.rows), 22) + self.assertEqual( + [int(row["id"].rsplit("--", 1)[1]) for row in self.rows], EXPECTED_IDS + ) + for row in self.rows: + self.assertEqual(set(row), EXPECTED_KEYS, row["id"]) + self.assertEqual(row["web_name"], "UC Berkeley") + self.assertEqual(row["web"], "http://localhost:40026/") + self.assertEqual(row["upstream_url"], "https://www.berkeley.edu/") + self.assertTrue(row["ques"].strip()) + self.assertIn("Checkpoints:", row["judge_rubric"]) + + def test_verifier_paths_exist_and_match_the_task_id(self) -> None: + for row in self.rows: + number = int(row["id"].rsplit("--", 1)[1]) + path = Path(row["verifier_path"]) + self.assertFalse(path.is_absolute(), row["id"]) + self.assertEqual(path.name, f"verify_{number}.py") + full = SITE_DIR.parents[1] / path + self.assertTrue(full.is_file(), f"{row['id']}: missing {full}") + self.assertIn(f'TASK_ID = "UC Berkeley--{number}"', full.read_text(encoding="utf-8")) + + def test_every_verifier_file_is_referenced(self) -> None: + referenced = {Path(row["verifier_path"]).name for row in self.rows} + present = {path.name for path in (SITE_DIR / "verify").glob("verify_[0-9]*.py")} + self.assertEqual(referenced, present) + + def test_rubrics_are_unique(self) -> None: + rubrics = [row["judge_rubric"] for row in self.rows] + self.assertEqual(len(set(rubrics)), len(rubrics)) + + def test_no_rubric_contains_a_derived_ground_truth_value(self) -> None: + # The shared preamble is identical boilerplate across every row; the scan + # covers each row's own checkpoints, where a task-specific value would + # actually leak. + rubrics = [row["judge_rubric"] for row in self.rows] + preamble = os.path.commonprefix(rubrics) + for row in self.rows: + number = int(row["id"].rsplit("--", 1)[1]) + forbidden = forbidden_for(self.facts[number], row["ques"], self.generic_tokens) + leaked = leaks_in(row["judge_rubric"][len(preamble):], forbidden) + self.assertEqual(leaked, [], f"{row['id']} rubric leaks derived values: {leaked}") + + def test_leak_detector_has_teeth(self) -> None: + """The scan must reject a rubric that carries the derived answer.""" + row = next(row for row in self.rows if row["id"] == "UC Berkeley--4") + forbidden = forbidden_for(self.facts[4], row["ques"], self.generic_tokens) + self.assertTrue(forbidden) + self.assertTrue( + leaks_in("the answer must name Jennifer Doudna and the National Medal of Science", forbidden) + ) + self.assertEqual( + leaks_in("the article's page must have been opened and the award it reports given", forbidden), + [], + ) + + def test_no_rubric_contains_a_derived_number(self) -> None: + for row in self.rows: + number = int(row["id"].rsplit("--", 1)[1]) + rubric = row["judge_rubric"] + numbers = { + found + for value in strings_in({k: v for k, v in self.facts[number].items() if k != "task"}) + for found in re.findall(r"\d+(?:\.\d+)?", value) + } + ques_numbers = set(re.findall(r"\d+(?:\.\d+)?", row["ques"])) + for value in sorted(numbers - ques_numbers): + self.assertIsNone( + re.search(rf"(? None: + """--30/--31 assume an empty bookmarks table (see VERIFIER_PLAN.md B.2).""" + worker = sqlite3.connect(str(SEED_DB)) + try: + count = worker.execute("SELECT COUNT(*) FROM bookmarks").fetchone()[0] + finally: + worker.close() + self.assertEqual(count, 0) + + +if __name__ == "__main__": + unittest.main() From 67ee9a50a27d5514803003c7d94bb29651fc447a Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:07:56 -0400 Subject: [PATCH 08/25] test(berkeley): add the run-signature writer and matrix harness verify/tests/run_matrix.py boots the mirror from a fresh seed per cell on an alt port, drives a scripted Playwright workflow, snapshots the live database and writes an agent_demo/agent.py-shaped run directory (trajectory.json with url-before-action steps and one input step per filled field, screenshots/, initial.db, after.db). It then grades every cell through `uv run python agent_demo/eval_judge.py --run_dir --verifier True` and compares the verdict with the cell's expectation. Cells per task: pass (genuine walk), no_op, shortcut (catalog-wide search with the correct answer), wrong_answer, collateral_write (one row injected straight into the live DB after the walk) and, for the two stateful rows, state_mismatch (the save skipped). Genuine answers are rendered from the derived target, so the harness carries no second copy of the ground truth. Artifacts land under sites/berkeley/scripts_dev/runs/matrix (gitignored, and self-ignored by a generated .gitignore in the output root). The matrix itself was NOT run in this window: that is the next one. What the test suite runs is the browser-free replay contract (verify/tests/test_run_matrix_contract.py), which replays each workflow as a synthetic trajectory and asserts the genuine run passes every verifier, every wrong answer is rejected and the injected collateral write fails. Run: .venv/bin/python -m pytest sites/berkeley/verify/tests -q -> 528 passed Co-Authored-By: Claude Code --- sites/berkeley/verify/tests/run_matrix.py | 484 ++++++++++++++++++ .../verify/tests/test_run_matrix_contract.py | 114 +++++ 2 files changed, 598 insertions(+) create mode 100644 sites/berkeley/verify/tests/run_matrix.py create mode 100644 sites/berkeley/verify/tests/test_run_matrix_contract.py diff --git a/sites/berkeley/verify/tests/run_matrix.py b/sites/berkeley/verify/tests/run_matrix.py new file mode 100644 index 000000000..673b61c14 --- /dev/null +++ b/sites/berkeley/verify/tests/run_matrix.py @@ -0,0 +1,484 @@ +#!/usr/bin/env python3 +"""Playwright run-signature writer and grading matrix for the UC Berkeley verifiers. + +For each task this boots the mirror from a fresh seed on an alt port, drives a +scripted workflow with a real browser, snapshots the live SQLite database, and +writes a run directory in the ``agent_demo/agent.py`` shape (``trajectory.json`` +with url-before-action steps, ``screenshots/step_NNN.png``, ``initial.db``, +``after.db``). It then grades each run through + + uv run python agent_demo/eval_judge.py --run_dir --verifier True + +and compares the verdict with the cell's expectation, so a verifier that stops +discriminating shows up as a matrix failure rather than a silent green. + +Run it from ``agent_demo/`` (that env has Playwright and the uv project the +verifiers are launched with): + + cd agent_demo + uv run python ../sites/berkeley/verify/tests/run_matrix.py --out ../sites/berkeley/scripts_dev/runs/matrix + +Cells emitted per task: ``pass`` (the genuine walk), ``no_op`` (homepage only, +no answer), ``shortcut`` (a catalog-wide search only, with the correct answer), +``wrong_answer`` (the genuine walk with a plausible wrong answer), +``collateral_write`` (the genuine walk plus one row written straight into the +live database — fault injection, not an app-driven write) and, for the two +stateful rows, ``state_mismatch`` (the genuine walk with the save skipped). + +All artifacts land under ``--out`` (default: the gitignored +``sites/berkeley/scripts_dev/runs/matrix``), which is also self-ignored by a +generated ``.gitignore`` inside the output root. +""" +from __future__ import annotations + +import argparse +import http.client +import json +import os +import shutil +import sqlite3 +import subprocess +import sys +import time +from pathlib import Path +from typing import Any +from urllib.parse import urlencode + +VERIFY_DIR = Path(__file__).resolve().parents[1] +SITE_DIR = VERIFY_DIR.parent +REPO = SITE_DIR.parents[1] +sys.path.insert(0, str(VERIFY_DIR)) + +import ground_truth # noqa: E402 +from verify_lib import title_tokens # noqa: E402 + +DEFAULT_PORT = 41026 +DEFAULT_OUT = SITE_DIR / "scripts_dev" / "runs" / "matrix" +VIEWPORT = {"width": 1280, "height": 800} +PASSWORD = "test1234" + +# --------------------------------------------------------------------------- # +# Workflows: each step is one browser action in the recorder's semantics +# (the recorded URL is the page *before* the action, as agent.py does). +# --------------------------------------------------------------------------- # +# {"goto": path} navigate +# {"fill": [selector, text], ...} type into fields (one entry per field) +# {"click": selector} click an element +# {"form": "/bookmark/add"} submit the form whose action matches +# +# ``answer`` is built from the derived facts, so the genuine run carries the +# ground truth without a second copy of it in this file. +WORKFLOWS: dict[int, dict[str, Any]] = { + 1: {"steps": [{"goto": "/"}, {"goto": "/programs?q=MBA"}, + {"goto": "/programs/business-administration-mba"}]}, + 2: {"steps": [{"goto": "/"}, {"goto": "/programs?q=Computer%20Science"}, + {"goto": "/programs/computer-science-bs"}]}, + 4: {"steps": [{"goto": "/"}, {"goto": "/news?q=CRISPR"}, + {"goto": "/news/crispr-pioneer-jennifer-doudna-receives-national-medal-of-science"}]}, + 6: {"steps": [{"goto": "/"}, {"goto": "/events?category=Lecture"}]}, + 7: {"steps": [{"goto": "/"}, {"goto": "/faculty?dept=eecs"}, + {"goto": "/faculty/stuart-russell"}]}, + 10: {"steps": [{"goto": "/"}, {"goto": "/research/bair"}]}, + 11: {"steps": [{"goto": "/"}, {"goto": "/admissions"}]}, + 12: {"steps": [{"goto": "/"}, {"goto": "/programs?college=haas-business"}]}, + 13: {"steps": [{"goto": "/"}, {"goto": "/departments"}, {"goto": "/departments/eecs"}]}, + 14: {"steps": [{"goto": "/"}, {"goto": "/academics"}]}, + 16: {"steps": [{"goto": "/"}, {"goto": "/programs?page=3"}, + {"goto": "/programs/data-science-ms"}]}, + 17: {"steps": [{"goto": "/"}, {"goto": "/about"}]}, + 19: {"steps": [{"goto": "/"}, {"goto": "/news?category=Athletics"}, + {"goto": "/news/womens-gymnastics-wins-ncaa-championship"}]}, + 20: {"steps": [{"goto": "/"}, {"goto": "/programs?degree=JD"}, + {"goto": "/programs/juris-doctor-jd"}]}, + 22: {"steps": [{"goto": "/"}, {"goto": "/departments"}]}, + 23: {"steps": [{"goto": "/"}, {"goto": "/research"}, {"goto": "/research/bids"}]}, + 24: {"steps": [{"goto": "/"}, {"goto": "/programs/economics-phd"}, + {"goto": "/departments/economics"}, {"goto": "/faculty/emmanuel-saez"}]}, + 25: {"steps": [{"goto": "/"}, {"goto": "/events?category=Career"}, {"goto": "/events/2"}]}, + 27: {"steps": [{"goto": "/"}, {"goto": "/programs?q=Master%20of%20Engineering"}, + {"goto": "/programs/master-of-engineering-meng"}, + {"goto": "/programs/computer-science-ms"}]}, + 28: {"steps": [{"goto": "/"}, {"goto": "/programs?degree=PhD"}, {"goto": "/programs?degree=MS"}]}, + 30: {"login": "alice@berkeley.edu", + "steps": [{"goto": "/login"}, + {"fill": [("input[name='email']", "alice@berkeley.edu"), + ("input[name='password']", PASSWORD)]}, + {"click": "button[type=submit]"}, + {"goto": "/research/seismo-lab"}, + {"form": "/bookmark/add", "skip_in_state_mismatch": True}, + {"goto": "/account"}]}, + 31: {"login": "bob@berkeley.edu", + "steps": [{"goto": "/login"}, + {"fill": [("input[name='email']", "bob@berkeley.edu"), + ("input[name='password']", PASSWORD)]}, + {"click": "button[type=submit]"}, + {"goto": "/research/msri"}, + {"form": "/bookmark/add", "skip_in_state_mismatch": True}, + {"goto": "/research/cpl"}, + {"form": "/bookmark/add", "skip_in_state_mismatch": True}, + {"goto": "/account"}, + {"click": "form[action='/bookmark/remove'] button"}, + {"goto": "/account"}]}, +} + +WRONG_ANSWERS: dict[int, list[str]] = { + 1: ["The School of Law offers the MBA; it takes 3 years.", + "The Haas School of Business offers the MBA; it takes four years."], + 2: ["The Computer Science BS requires foundational coursework in theory, systems and AI, " + "plus a research project or thesis.", + "The Computer Science BS requires Data Structures, Algorithms, Computer Architecture and " + "Operating Systems, as well as a Qualifying Examination."], + 4: ["The featured scientist is Jennifer Doudna, who received the Nobel Prize.", + "The article is about a faster COVID test using CRISPR."], + 6: ["Events: 'Spring Career Fair 2026' on May 17, 2026 at the Recreational Sports Facility; " + "'Hackathon: Code for Climate 2026' on May 30, 2026 at Soda Hall; 'Berkeley Startup Pitch " + "Competition Finals' on June 4, 2026 at 310 Sutardja Dai Hall."], + 7: ["Eliza Strickland works on AI reporting and biomedical ethics.", + "Stuart Russell works on robotics and reinforcement learning."], + 10: ["BAIR was founded in 2017 and is directed by Prof. Pieter Abbeel."], + 11: ["The freshman deadline is December 1 and the acceptance rate is 11%."], + 12: ["Haas offers MBA, PhD and MFE programs.", + "The Haas School of Business offers the Business Administration MBA and a PhD in Business."], + 13: ["The EECS chair is Prof. Alexei Efros, in 253 Cory Hall."], + 14: ["The College of Engineering enrolls 31,800 undergraduates and 12,000 graduate students; " + "the dean is Dean Tsu-Jae King Liu."], + 16: ["Several programs can be completed online, including the Computer Science MS and the " + "Master of Engineering.", + "The Data Science MS from the School of Information is online, and so is the Civil " + "Engineering BS."], + 17: ["Berkeley has 107 Nobel Laureates on the faculty, 30 varsity sports and 105 NCAA titles.", + "Berkeley has 12 Nobel Laureates on the faculty, 32 varsity sports and 105 NCAA titles."], + 19: ["Berkeley athletes won a record 12 medals at the Winter World University Games.", + "Cal won the Pac-12 football championship."], + 20: ["The JD takes 2 years, has a January 5 deadline, and is offered by the Haas School of " + "Business."], + 22: ["The College of Letters and Science has 30 departments.", + "The College of Letters and Science lists 8 departments."], + 23: ["BIDS is directed by Prof. Douglas Dreger; focus areas are Data Science, Statistics and " + "Computational Methods; a related center is the Berkeley Seismological Laboratory.", + "BIDS is directed by Prof. David Culler and focuses on Machine Learning, Robotics and " + "Climate Policy."], + 24: ["The Economics department is chaired by Prof. David Card and offers the Economics BA and " + "the Economics PhD. Emmanuel Saez works on public economics and inequality."], + 25: ["The Spring Career Fair is on May 17, 2026 at the Recreational Sports Facility, and " + "registration is not required.", + "The Spring Career Fair is on May 17, 2026 at Pauley Ballroom, registration required."], + 27: ["The Master of Engineering is offered by EECS and takes 2 years; the Computer Science MS " + "takes 2 years."], + 28: ["There are 25 programs that require the GRE, all of them PhD programs."], + 30: ["I am not sure the Berkeley Seismological Laboratory was saved."], + 31: ["The Mathematical Sciences Research Institute was removed and the California Policy Lab " + "remains saved; its director is Prof. Tatiana Toro."], +} + + +def genuine_answer(number: int, facts: dict) -> str: + """The correct answer, rendered from the derived target.""" + if number == 1: + return f"The {facts['college']} offers the MBA; it takes {facts['duration_years']:g} years." + if number == 2: + return f"The Computer Science BS requires {', '.join(facts['items'])}." + if number == 4: + return f"The article features {facts['person']}, who received the {facts['award']}." + if number == 6: + # Rows with enough distinctive title tokens for the verifier's binding rule. + rows = [row for row in facts["upcoming"] if len(title_tokens(row["title"])) >= 3][:3] + listed = "; ".join(f"'{row['title']}' on {row['start_datetime'][:10]} at {row['location']}" for row in rows) + return f"Lecture events: {listed}." + if number == 7: + row = next(r for r in facts["allowed"] if r["slug"] == "stuart-russell") + return f"{row['name']} is an EECS professor whose research covers {row['research_interests']}." + if number == 10: + return f"BAIR was founded in {facts['founded_year']} and is directed by {facts['director']}." + if number == 11: + return (f"The freshman application deadline is {facts['deadline']}, and the acceptance " + f"rate is {facts['acceptance_rate']}.") + if number == 12: + programme = facts["programmes"][0] + return (f"The {facts['college']['name']} offers a single program: the " + f"{programme['degree_type']} in {programme['name']}.") + if number == 13: + return f"The chair of EECS is {facts['chair']}, and the department is located at {facts['location']}." + if number == 14: + return (f"The College of Engineering enrolls {facts['undergrad_count']:,} undergraduates and " + f"{facts['grad_count']:,} graduate students; the dean is {facts['dean']}.") + if number == 16: + programme = facts["program"] + return (f"Only one program offers an online option: the {programme['name']} " + f"{programme['degree_type']} from the {programme['college_name']}.") + if number == 17: + return (f"Berkeley has {facts['nobel_laureates']} Nobel Laureates on the faculty, " + f"{facts['varsity_sports']} varsity sports, and {facts['national_titles']} NCAA " + f"national titles.") + if number == 19: + row = facts["championships"][0] + return f"{row['title']}: the story reports the championship win and the team's run to it." + if number == 20: + return (f"The JD at Berkeley takes {facts['duration_years']:g} years, has a " + f"{facts['deadline']} deadline, and is offered by the {facts['college']}.") + if number == 22: + names = [row["name"] for row in facts["departments"]][:6] + return (f"The College of Letters and Science lists {len(facts['departments'])} departments: " + f"{', '.join(names)}.") + if number == 23: + centre, related = facts["centre"], facts["related_names"] + return (f"{centre['name']} is directed by {centre['director']}; its focus areas are " + f"{', '.join(facts['focus_areas'])}. A related center listed on the page is the " + f"{related[0]}.") + if number == 24: + member = next(r for r in facts["members"] if r["slug"] == "emmanuel-saez") + types = " and ".join(sorted({row["degree_type"] for row in facts["programmes"]})) + return (f"The Economics department is chaired by {facts['chair']} and offers the {types} in " + f"Economics. {member['name']} works on {member['research_interests']}.") + if number == 25: + anchor = facts["anchor"] + others = facts["others"][:2] + listed = "; ".join(f"'{row['title']}' on {row['start_datetime'][:10]} at {row['location']}" + for row in others) + return (f"The {anchor['title']} is on {anchor['start_datetime'][:10]} at {anchor['location']}, " + f"and registration is required. Two other career events: {listed}.") + if number == 27: + return (f"The {facts['meng']['name']} is offered by the {facts['department']} and takes " + f"{facts['durations'][0]:g} year; the {facts['ms']['name']} " + f"{facts['ms']['degree_type']} takes {facts['durations'][1]:g} years.") + if number == 28: + return (f"{facts['count']} programs in the catalogue require the GRE; the degree type that " + f"most commonly requires it is the {facts['most_common_degree']}.") + if number == 30: + centre = facts["centre"] + return (f"I signed in as alice, saved the {centre['name']} to my bookmarks, and it is listed " + f"under My Account. Its director is {facts['director']}.") + if number == 31: + first, second = facts["first"], facts["second"] + return (f"I signed in as bob, saved both centers in order, then removed the {first['name']} " + f"bookmark. The {second['name']} remains saved; its director is " + f"{second['director']}.") + raise ValueError(f"no genuine answer template for task {number}") + + +# --------------------------------------------------------------------------- # +# Browser driving +# --------------------------------------------------------------------------- # +class Recorder: + """Writes one run directory in the agent.py shape.""" + + def __init__(self, run_dir: Path, task_id: str, start_url: str) -> None: + self.run_dir = run_dir + self.shots = run_dir / "screenshots" + self.shots.mkdir(parents=True, exist_ok=True) + self.steps: list[dict[str, Any]] = [] + self.start_url = start_url + self.task_id = task_id + + def step(self, page, action: str, params: dict[str, Any], act) -> None: + index = len(self.steps) + before, after = f"step_{index:03d}.png", f"step_{index + 1:03d}.png" + url_before = page.url + page.screenshot(path=str(self.shots / before)) + if act is not None: + act() + page.screenshot(path=str(self.shots / after)) + self.steps.append({ + "step": index, "url": url_before, "title": page.title(), + "thought": "scripted matrix step", "action": action, "params": params, + "screenshot_before": before, "screenshot_after": after, + "action_result": {"is_done": False, "success": True, "error": None, "extracted_content": ""}, + }) + + def finish(self, page, answer: str | None) -> None: + self.step(page, "done", {"text": answer or "", "success": bool(answer)}, None) + trajectory = { + "task": "scripted matrix run", "task_id": self.task_id, "start_url": self.start_url, + "model": "playwright-matrix", "max_steps": 30, "steps": self.steps, + "terminated": bool(answer), "termination_reason": "agent_done" if answer else "max_steps", + "final_answer": answer, "success_self_report": bool(answer), + "judge_rubric": "", "verifier_path": f"sites/berkeley/verify/verify_{self.task_id.rsplit('--', 1)[1]}.py", + } + (self.run_dir / "trajectory.json").write_text(json.dumps(trajectory, indent=2), encoding="utf-8") + + +def drive(page, base_url: str, steps: list[dict[str, Any]], recorder: Recorder, + *, skip_saves: bool = False) -> None: + for item in steps: + if "goto" in item: + target = base_url + item["goto"] + recorder.step(page, "navigate", {"url": target}, lambda target=target: page.goto(target)) + elif "fill" in item: + # One step per field: the verifier reads the typed texts of the + # /login input steps (email and password are separate actions). + for selector, text in item["fill"]: + recorder.step( + page, "input", {"text": text}, + lambda selector=selector, text=text: page.fill(selector, text), + ) + elif "click" in item: + selector = item["click"] + recorder.step(page, "click", {"selector": selector}, lambda selector=selector: page.click(selector)) + elif "form" in item: + if skip_saves and item.get("skip_in_state_mismatch"): + continue + action = item["form"] + selector = f"form[action='{action}'] button" + recorder.step(page, "click", {"form": action}, lambda selector=selector: page.click(selector)) + else: # pragma: no cover - workflow table is static + raise ValueError(f"unsupported step: {item!r}") + + +# --------------------------------------------------------------------------- # +# Boot / snapshot / grade +# --------------------------------------------------------------------------- # +def fresh_instance(site_dir: Path) -> None: + instance = site_dir / "instance" + if instance.exists(): + shutil.rmtree(instance) + shutil.copytree(site_dir / "instance_seed", instance) + + +def boot(site_dir: Path, port: int) -> subprocess.Popen: + log = site_dir / "scripts_dev" / "runs" / "server.log" + log.parent.mkdir(parents=True, exist_ok=True) + handle = log.open("ab") + process = subprocess.Popen( + [sys.executable, "app.py"], cwd=str(site_dir), + env={**os.environ, "PORT": str(port)}, stdout=handle, stderr=handle, + ) + deadline = time.time() + 30 + while time.time() < deadline: + try: + connection = http.client.HTTPConnection("127.0.0.1", port, timeout=2) + connection.request("GET", "/") + if connection.getresponse().status == 200: + connection.close() + return process + connection.close() + except OSError: + time.sleep(0.25) + process.terminate() + raise RuntimeError(f"site did not come up on :{port} (see {log})") + + +def snapshot(site_dir: Path, run_dir: Path, kind: str) -> Path: + source = site_dir / ("instance_seed" if kind == "initial" else "instance") / "berkeley.db" + target = run_dir / ("initial.db" if kind == "initial" else "after.db") + shutil.copy2(source, target) + return target + + +def inject_collateral_write(site_dir: Path) -> None: + """One row written straight into the live DB (fault injection, not a route).""" + connection = sqlite3.connect(str(site_dir / "instance" / "berkeley.db")) + try: + connection.execute( + "INSERT INTO bookmarks(user_id, item_type, item_id, note, created_at) " + "VALUES (2, 'research', 1, 'matrix collateral write', '2026-05-12 00:00:00')" + ) + connection.commit() + finally: + connection.close() + + +def grade(run_dir: Path) -> dict[str, Any]: + command = ["uv", "run", "python", "agent_demo/eval_judge.py", + "--run_dir", str(run_dir), "--verifier", "True"] + result = subprocess.run(command, cwd=str(REPO), capture_output=True, text=True) + verdict_path = run_dir / "eval.json" + if not verdict_path.is_file(): + return {"pass": None, "reason": f"no eval.json (rc={result.returncode}): " + f"{(result.stderr or result.stdout)[-300:]}"} + verdict = json.loads(verdict_path.read_text(encoding="utf-8")) + return {"pass": bool(verdict.get("pass")), "reason": verdict.get("reason"), + "infra_error": bool(verdict.get("infra_error"))} + + +def emit_cells(number: int, facts: dict, out_root: Path, base_url: str) -> list[tuple[str, Path, bool]]: + """Build every run directory for one task; returns (cell, run_dir, expects_pass).""" + from playwright.sync_api import sync_playwright # noqa: PLC0415 - optional dependency at runtime + + workflow = WORKFLOWS[number] + answer = genuine_answer(number, facts) + cells: list[tuple[str, Path, bool]] = [] + task_id = f"UC Berkeley--{number}" + + with sync_playwright() as playwright: + browser = playwright.chromium.launch() + for cell, expects_pass in (("pass", True), ("no_op", False), ("shortcut", False), + ("wrong_answer", False), ("collateral_write", False), + ("state_mismatch", False)): + if cell == "state_mismatch" and not workflow.get("login"): + continue + run_dir = out_root / f"task_{number:02d}" / cell + run_dir.mkdir(parents=True, exist_ok=True) + fresh_instance(SITE_DIR) + process = boot(SITE_DIR, DEFAULT_PORT) + try: + snapshot(SITE_DIR, run_dir, "initial") + page = browser.new_page(viewport=VIEWPORT) + recorder = Recorder(run_dir, task_id, f"{base_url}/") + if cell == "no_op": + drive(page, base_url, [{"goto": "/"}], recorder) + recorder.finish(page, None) + elif cell == "shortcut": + drive(page, base_url, [{"goto": "/"}, {"goto": "/search?q=california"}], recorder) + recorder.finish(page, answer) + elif cell == "wrong_answer": + drive(page, base_url, workflow["steps"], recorder) + recorder.finish(page, WRONG_ANSWERS[number][0]) + elif cell == "state_mismatch": + drive(page, base_url, workflow["steps"], recorder, skip_saves=True) + recorder.finish(page, answer) + else: + drive(page, base_url, workflow["steps"], recorder) + if cell == "collateral_write": + inject_collateral_write(SITE_DIR) + recorder.finish(page, answer) + page.close() + snapshot(SITE_DIR, run_dir, "after") + cells.append((cell, run_dir, expects_pass)) + finally: + process.terminate() + process.wait(timeout=10) + browser.close() + return cells + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--out", default=str(DEFAULT_OUT)) + parser.add_argument("--tasks", default="", help="comma-separated task numbers (default: all)") + parser.add_argument("--grade-only", action="store_true", + help="re-grade the run directories under --out without driving browsers") + args = parser.parse_args() + + out_root = Path(args.out).resolve() + out_root.mkdir(parents=True, exist_ok=True) + (out_root / ".gitignore").write_text("*\n", encoding="utf-8") + numbers = ([int(value) for value in args.tasks.split(",") if value.strip()] + if args.tasks else sorted(WORKFLOWS)) + facts_by_task = {number: ground_truth.task_ground_truth(str(SITE_DIR / "instance_seed" / "berkeley.db"), number) + for number in numbers} + + results: list[dict[str, Any]] = [] + for number in numbers: + if args.grade_only: + cells = [(cell.name, cell, cell.name == "pass") + for cell in sorted((out_root / f"task_{number:02d}").iterdir()) if cell.is_dir()] + else: + cells = emit_cells(number, facts_by_task[number], out_root, f"http://localhost:{DEFAULT_PORT}") + for cell, run_dir, expects_pass in cells: + verdict = grade(run_dir) + ok = verdict["pass"] is expects_pass + results.append({"task": number, "cell": cell, "expected_pass": expects_pass, + "observed_pass": verdict["pass"], "reason": verdict["reason"], "ok": ok}) + flag = "ok " if ok else "MISMATCH" + print(f"[{flag}] {number:>2} {cell:<16} expected={'PASS' if expects_pass else 'FAIL'} " + f"observed={verdict['pass']} reason={verdict['reason']}") + + summary = out_root / "summary.json" + summary.write_text(json.dumps(results, indent=2), encoding="utf-8") + failures = [row for row in results if not row["ok"]] + print(f"\n{len(results)} cells, {len(failures)} mismatches -> {summary}") + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/sites/berkeley/verify/tests/test_run_matrix_contract.py b/sites/berkeley/verify/tests/test_run_matrix_contract.py new file mode 100644 index 000000000..f3c853bc8 --- /dev/null +++ b/sites/berkeley/verify/tests/test_run_matrix_contract.py @@ -0,0 +1,114 @@ +"""Browser-free contract tests for run_matrix.py. + +The matrix itself runs in the validation window; these tests check that the +scripted workflows, the derived genuine answers and the wrong-answer table all +line up with the verifiers, by replaying each workflow as a synthetic +url-before-action trajectory and grading it. No Playwright, no docker. +""" +from __future__ import annotations + +import importlib.util +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from _support import BASE, SEED_DB, State, VerifierTestCase, run_verifier, write_run # noqa: E402 + +VERIFY_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(VERIFY_DIR)) + +import ground_truth # noqa: E402 + +_spec = importlib.util.spec_from_file_location("run_matrix", Path(__file__).resolve().parent / "run_matrix.py") +run_matrix = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(run_matrix) + +KEPT = [1, 2, 4, 6, 7, 10, 11, 12, 13, 14, 16, 17, 19, 20, 22, 23, 24, 25, 27, 28, 30, 31] + + +def replay_steps(number: int) -> list[dict]: + """A recorder-faithful step list: each step carries the URL *before* its action.""" + steps = run_matrix.WORKFLOWS[number]["steps"] + targets = [BASE + item["goto"] for item in steps if "goto" in item] + replay: list[dict] = [] + current = f"{BASE}/" + for item in steps: + if "goto" in item: + replay.append({"url": current, "action": "navigate", "params": {"url": BASE + item["goto"]}}) + current = BASE + item["goto"] + elif "fill" in item: + # One recorder step per filled field (see run_matrix.drive). + for _, text in item["fill"]: + replay.append({"url": current, "action": "input", "params": {"text": text}}) + else: + replay.append({"url": current, "action": "click", "params": {}}) + replay.append({"url": current, "action": "done", "params": {}}) + return replay + + +def genuine_after(number: int) -> State: + if number == 30: + state = State() + state.add_bookmark(1, "research", 8) + return state + if number == 31: + state = State() + state.add_bookmark(2, "research", 9) # MSRI, id 1 + state.add_bookmark(2, "research", 22) # CPL, id 2 + state.remove_bookmark(1) + return state + return State() + + +class RunMatrixWorkflowTests(VerifierTestCase): + def test_workflows_cover_the_task_set(self) -> None: + self.assertEqual(sorted(run_matrix.WORKFLOWS), KEPT) + self.assertEqual(sorted(run_matrix.WRONG_ANSWERS), KEPT) + + def test_genuine_workflow_passes_every_verifier(self) -> None: + for number in KEPT: + facts = ground_truth.task_ground_truth(str(SEED_DB), number) + answer = run_matrix.genuine_answer(number, facts) + self.assertTrue(answer and answer.strip(), f"task {number} has an empty genuine answer") + run_dir = write_run( + self._tmp / f"matrix_{number}", f"UC Berkeley--{number}", + replay_steps(number), answer, after=genuine_after(number), + ) + verdict, _ = run_verifier(number, run_dir, container="wh-berkeley-test-none") + self.assertTrue( + verdict.get("pass"), + f"task {number} genuine workflow FAILED: reason={verdict.get('reason')!r} " + f"evidence={verdict.get('evidence')!r}", + ) + + def test_wrong_answers_are_all_rejected(self) -> None: + for number in KEPT: + for index, wrong in enumerate(run_matrix.WRONG_ANSWERS[number]): + run_dir = write_run( + self._tmp / f"wrong_{number}_{index}", f"UC Berkeley--{number}", + replay_steps(number), wrong, after=genuine_after(number), + ) + verdict, _ = run_verifier(number, run_dir, container="wh-berkeley-test-none") + self.assertFalse( + verdict.get("pass"), + f"task {number} wrong answer {index} PASSED the verifier: {wrong!r}", + ) + + def test_collateral_write_cell_fails_for_every_task(self) -> None: + """The matrix's injected write must be rejected by every row.""" + for number in KEPT: + facts = ground_truth.task_ground_truth(str(SEED_DB), number) + after = genuine_after(number) + after.add_bookmark(2, "research", 1, note="matrix collateral write") + run_dir = write_run( + self._tmp / f"collateral_{number}", f"UC Berkeley--{number}", + replay_steps(number), run_matrix.genuine_answer(number, facts), after=after, + ) + verdict, _ = run_verifier(number, run_dir, container="wh-berkeley-test-none") + self.assertFalse(verdict.get("pass"), f"task {number} tolerated a collateral write") + + +if __name__ == "__main__": + unittest.main() From eb19a2b32bdbc22b10632bf0a9b414c105971eff Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:14:15 -0400 Subject: [PATCH 09/25] chore(berkeley): site README and integration tests - sites/berkeley/README.md: scope and routes, seed generation (build-generated from tracked source, byte-reproducible, md5 recorded), the frozen BENCHMARK_NOW clock, why the mirror ships no imagery, the seeded row counts, the demo accounts, and a pointer to the grading contract. - sites/berkeley/tests/test_integration.py (walmart_careers pattern): the registry is derived from control_server.SITES (cross-checked against websyn_start.sh) and berkeley is asserted at index 26 / port 40026; the Dockerfile exposes the current range and builds the seed from source; the .build-generated-seed marker is honoured by fetch_assets.sh; the seed's md5 is asserted when the build-generated DB is present; tasks.jsonl has 22 rows on the registered port with existing verifier paths and no answer key; app.py keeps the frozen clock and its article route neither writes view_count nor commits; the five shared docs carry the current port range. Run: .venv/bin/python -m pytest sites/berkeley/tests -q -> 7 passed Co-Authored-By: Claude Code --- sites/berkeley/README.md | 44 +++++++++ sites/berkeley/tests/test_integration.py | 119 +++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 sites/berkeley/README.md create mode 100644 sites/berkeley/tests/test_integration.py diff --git a/sites/berkeley/README.md b/sites/berkeley/README.md new file mode 100644 index 000000000..49b4c6283 --- /dev/null +++ b/sites/berkeley/README.md @@ -0,0 +1,44 @@ +# UC Berkeley mirror + +Offline Flask mirror of `https://www.berkeley.edu/`. In the 27-site registry it is site index 26 and runs on container port `40026`. Every college, department, programme, faculty member, research centre, article, event and account is deterministic synthetic benchmark data; only the page chrome mirrors upstream. + +## Runtime + +```bash +uv venv .venv --python 3.12 +uv pip install --python .venv/bin/python Flask==3.1.0 Flask-SQLAlchemy==3.1.1 Flask-Login==0.6.3 \ + Flask-WTF==1.2.2 Flask-Bcrypt==1.0.1 email-validator==2.2.0 # the shared pins; no site-specific deps +cd sites/berkeley && PYTHONHASHSEED=0 ../../.venv/bin/python seed_data.py # writes instance_seed/berkeley.db +PORT=40026 ../../.venv/bin/python app.py +``` + +There is no Hugging Face archive for this site: `.build-generated-seed` marks it, `./scripts/fetch_assets.sh berkeley` skips it, and the Docker build regenerates `instance_seed/berkeley.db` from the tracked `seed_data.py` (`cd /opt/WebSyn/berkeley && rm -rf instance instance_seed && PYTHONHASHSEED=0 python seed_data.py && rm -rf instance`). The seed is byte-reproducible — md5 `3001bcf4bcec169f4192c08609160ab6`, identical under `PYTHONHASHSEED=0` and `=1` — because the four benchmark password hashes are precomputed bcrypt strings, every `created_at` is the frozen clock, and `User.email`/`User.username` carry `unique=True` without `index=True` (SQLAlchemy emits named indexes in set-iteration order, which moved SQLite root pages between runs). + +## Frozen benchmark clock + +`app.py` defines `BENCHMARK_NOW = datetime(2026, 5, 12)` and uses it everywhere a date is compared or stamped (the `/events` upcoming/past/today filters, the homepage "Upcoming events" block, and the `created_at` / `published_date` column defaults via `utcnow()`). No request or seed path calls `datetime.utcnow()`, so the rendered site is identical on any run date; against the seeded calendar `/events` admits 52 of 64 rows, 15 of them Lecture. Reseeding with a different `now` means re-pinning the verifier contract in `verify/verify_lib.py` (schema hash, counts, catalog fingerprint). + +## Imagery + +The mirror ships **no images by design**: `static/css/` and `static/js/` hold only `.gitkeep`, `templates/base.html` carries a single inline stylesheet, and the logo is a CSS circle. `.requires-images` is absent, so `check_assets.sh` treats the empty `static/images/` as expected rather than a gap. This is a deliberate visual-fidelity compromise for a text-and-listing site: every task is graded on rendered text, tables and links. + +## Seeded rows + +| Model | Rows | Model | Rows | +|---|---|---|---| +| colleges | 14 | departments | 30 | +| programmes | 83 (25 PhD / 21 BA / 16 BS / 16 MS / MBA, JD, MEng, MD, MPH ×1; 17 GRE-required, 1 online) | faculty | 82 (19 EECS) | +| research centres | 25 | news articles | 121 (7 Athletics) | +| events | 64 (19 Lecture / 14 Career / …) | users | 4 | + +Benchmark accounts: `alice`, `bob`, `carol`, `dave` `@berkeley.edu`, password `test1234` (public by design; the hashes are hardcoded in `seed_data.py`). The `bookmarks` table starts empty, so the two stateful tasks bind their insert/delete ordering to the row ids the app assigns. + +## Routes + +`/`, `/news` (search + category + pagination), `/news/`, `/academics`, `/programs` (search, college and degree filters, pagination), `/programs/`, `/events` (category + upcoming/past/today), `/events/`, `/research`, `/research/`, `/departments`, `/departments/`, `/admissions`, `/about`, `/search` (programmes / news / events / faculty / centres), `/faculty` (name, interest and department filters), `/faculty/`, `/login`, `/register`, `/logout`, `/account` (bookmarks), `/bookmark/add` (POST), `/bookmark/remove` (POST), `/_health`. + +Article detail, programme detail, event detail, faculty profiles and centre pages are pure reads: no GET path writes the database, so a read-only benchmark task's after-state always equals its initial snapshot. + +## Grading contract + +`sites/berkeley/verify/` holds the deterministic verifiers (one per `tasks.jsonl` row), the shared `verify_lib.py`/`ground_truth.py`, and `TASK_REVIEW.md` with the per-row ACCEPT/DROP/ADDED record. See `verify/README.md` for the snapshot contract and how to run them. diff --git a/sites/berkeley/tests/test_integration.py b/sites/berkeley/tests/test_integration.py new file mode 100644 index 000000000..832510262 --- /dev/null +++ b/sites/berkeley/tests/test_integration.py @@ -0,0 +1,119 @@ +"""Integration checks for the UC Berkeley mirror (registry, marker, tasks, seed). + +Follows the walmart_careers pattern: the registry is *derived* from +``control_server.SITES`` so adding a later site does not require editing this +file, while the ordering guarantee (berkeley is index 26 → port 40026) is still +asserted exactly. +""" +from __future__ import annotations + +import ast +import hashlib +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[3] +SITE = ROOT / "sites/berkeley" +SITE_INDEX = 26 +SITE_PORT = 40026 +# Recorded in scripts_dev/REVIEW_STATUS.md §7.2; asserted only when the +# build-generated seed is present in the worktree. +SEED_MD5 = "3001bcf4bcec169f4192c08609160ab6" + + +def shell_sites() -> list[str]: + text = (ROOT / "websyn_start.sh").read_text() + return re.search(r"SITES=\((.*?)\)", text, re.S).group(1).split() + + +def control_sites() -> list[str]: + module = ast.parse((ROOT / "control_server.py").read_text()) + for node in module.body: + if isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == "SITES" for target in node.targets + ): + return ast.literal_eval(node.value) + raise AssertionError("control_server.SITES not found") + + +def registered_sites() -> list[str]: + shell, control = shell_sites(), control_sites() + assert shell == control, "websyn_start.sh and control_server.py disagree" + return shell + + +def port_range() -> str: + return f"40000-{40000 + len(registered_sites()) - 1}" + + +def test_registry_places_berkeley_at_index_26() -> None: + sites = registered_sites() + assert len(sites) == len(set(sites)), "duplicate site in the registry" + assert sites[SITE_INDEX] == "berkeley", f"berkeley moved to index {sites.index('berkeley')}" + assert 40000 + sites.index("berkeley") == SITE_PORT + + +def test_dockerfile_exposes_the_current_range_and_builds_the_seed() -> None: + text = (ROOT / "Dockerfile").read_text() + assert f"{len(registered_sites())} Flask mirror sites" in text + assert f"EXPOSE 8101 {port_range()}" in text + # The seed is generated at build time from tracked source (no HF archive). + assert "cd /opt/WebSyn/berkeley" in text + assert "PYTHONHASHSEED=0 python seed_data.py" in text + + +def test_build_generated_seed_marker_and_fetch_exemption() -> None: + marker = SITE / ".build-generated-seed" + assert marker.is_file(), "missing .build-generated-seed marker" + assert "no Hugging Face asset archive" in marker.read_text() + fetch = (ROOT / "scripts/fetch_assets.sh").read_text() + assert ".build-generated-seed" in fetch, "fetch_assets.sh no longer honours the marker" + + +def test_seed_is_byte_reproducible_when_present() -> None: + seed = SITE / "instance_seed" / "berkeley.db" + if not seed.is_file(): + return # build-generated; the image (or `python seed_data.py`) creates it + assert hashlib.md5(seed.read_bytes()).hexdigest() == SEED_MD5 + + +def test_tasks_and_verifiers_are_complete_and_use_the_registered_port() -> None: + rows = [json.loads(line) for line in (SITE / "tasks.jsonl").read_text().splitlines() if line] + assert len(rows) == 22 + assert [int(row["id"].rsplit("--", 1)[1]) for row in rows] == [ + 1, 2, 4, 6, 7, 10, 11, 12, 13, 14, 16, 17, 19, 20, 22, 23, 24, 25, 27, 28, 30, 31 + ] + assert {row["web"] for row in rows} == {f"http://localhost:{SITE_PORT}/"} + assert all((ROOT / row["verifier_path"]).is_file() for row in rows) + assert all("answer" not in row for row in rows) + assert all("Checkpoints:" in row["judge_rubric"] for row in rows) + + +def test_app_clock_is_frozen_and_article_reads_do_not_write() -> None: + source = (SITE / "app.py").read_text() + assert "BENCHMARK_NOW = datetime(2026, 5, 12)" in source + module = ast.parse(source) + wall_clock_calls = [ + node for node in ast.walk(module) + if isinstance(node, ast.Attribute) and node.attr == "utcnow" + ] + assert not wall_clock_calls, "the app must not call datetime.utcnow() (BENCHMARK_NOW only)" + for node in module.body: + if isinstance(node, ast.FunctionDef) and node.name == "news_article": + body = ast.dump(node) + assert "view_count" not in body, "article detail must not write view_count" + assert "commit" not in body, "article detail must not commit" + break + else: + raise AssertionError("news_article route not found") + + +def test_shared_documentation_uses_the_current_site_range() -> None: + current = port_range() + stale = {f"40000-400{end}" for end in range(20, 27)} - {current} + for relative in ["README.md", "AGENTS.md", "CONTRIBUTING.md", "CLAUDE.md", "agent_demo/README.md"]: + text = (ROOT / relative).read_text() + for old in stale: + assert old not in text, f"{relative} still documents {old}" + assert current in text, relative From 50813e7d4e566a7c3ad4e306b2a7e05573597771 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:39:47 -0400 Subject: [PATCH 10/25] =?UTF-8?q?fix(berkeley):=20C=20=E2=80=94=20run=5Fma?= =?UTF-8?q?trix=20writer=20drives=20and=20grades=20the=20real=20recorder?= =?UTF-8?q?=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first matrix run exposed four writer defects; each is fixed here, and the fix was mutation-checked (the port guard was shown to fire on a stray server, and the 30/31 pass/state_mismatch cells now grade correctly): 1. grade() launched eval_judge.py from the repo root, whose .venv has neither openai nor simpleArgParser -> ModuleNotFoundError and no eval.json, so every cell would have been reported as a mismatch with pass=None. It now runs from agent_demo/, the invocation AGENTS.md documents. 2. The page was never navigated before the first recorded step, so step 0's URL was about:blank and the verifier's all_urls_match_local_origin gate failed the genuine run of every task. It now pre-navigates to the start URL exactly as agent.py's browser.navigate_to(start_url) does. 3. Both login workflows clicked "button[type=submit]", which matches the navbar search button first: the login never happened, /account bounced to /login and the bookmark step timed out. Now form[action='/login'] button[type=submit]. 4. boot() adopted any process already listening on the port. A leftover standalone server made cells grade against a foreign instance (the 30/31 pass cells failed with bookmarks_exact_delta). boot() now refuses that port. Also the state-mismatch cell (all writes skipped) clicked the bookmark-removal form that a fresh instance never renders; the skip rule now applies to any step marked skip_in_state_mismatch, and the removal click carries the mark. Co-Authored-By: Claude Code --- sites/berkeley/verify/tests/run_matrix.py | 47 +++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/sites/berkeley/verify/tests/run_matrix.py b/sites/berkeley/verify/tests/run_matrix.py index 673b61c14..83da224dc 100644 --- a/sites/berkeley/verify/tests/run_matrix.py +++ b/sites/berkeley/verify/tests/run_matrix.py @@ -103,7 +103,7 @@ "steps": [{"goto": "/login"}, {"fill": [("input[name='email']", "alice@berkeley.edu"), ("input[name='password']", PASSWORD)]}, - {"click": "button[type=submit]"}, + {"click": "form[action='/login'] button[type=submit]"}, {"goto": "/research/seismo-lab"}, {"form": "/bookmark/add", "skip_in_state_mismatch": True}, {"goto": "/account"}]}, @@ -111,13 +111,14 @@ "steps": [{"goto": "/login"}, {"fill": [("input[name='email']", "bob@berkeley.edu"), ("input[name='password']", PASSWORD)]}, - {"click": "button[type=submit]"}, + {"click": "form[action='/login'] button[type=submit]"}, {"goto": "/research/msri"}, {"form": "/bookmark/add", "skip_in_state_mismatch": True}, {"goto": "/research/cpl"}, {"form": "/bookmark/add", "skip_in_state_mismatch": True}, {"goto": "/account"}, - {"click": "form[action='/bookmark/remove'] button"}, + {"click": "form[action='/bookmark/remove'] button", + "skip_in_state_mismatch": True}, {"goto": "/account"}]}, } @@ -300,6 +301,12 @@ def finish(self, page, answer: str | None) -> None: def drive(page, base_url: str, steps: list[dict[str, Any]], recorder: Recorder, *, skip_saves: bool = False) -> None: for item in steps: + if skip_saves and item.get("skip_in_state_mismatch"): + # The state-mismatch cell replays the workflow with every write + # skipped, so a later click that depends on a write (the bookmark + # removal) must be skipped with it, or the cell hangs on a form + # that a fresh instance never renders. + continue if "goto" in item: target = base_url + item["goto"] recorder.step(page, "navigate", {"url": target}, lambda target=target: page.goto(target)) @@ -315,8 +322,6 @@ def drive(page, base_url: str, steps: list[dict[str, Any]], recorder: Recorder, selector = item["click"] recorder.step(page, "click", {"selector": selector}, lambda selector=selector: page.click(selector)) elif "form" in item: - if skip_saves and item.get("skip_in_state_mismatch"): - continue action = item["form"] selector = f"form[action='{action}'] button" recorder.step(page, "click", {"form": action}, lambda selector=selector: page.click(selector)) @@ -337,6 +342,24 @@ def fresh_instance(site_dir: Path) -> None: def boot(site_dir: Path, port: int) -> subprocess.Popen: log = site_dir / "scripts_dev" / "runs" / "server.log" log.parent.mkdir(parents=True, exist_ok=True) + # Refuse to run against a server we did not start. Otherwise the readiness + # probe below adopts any process already listening on the port (e.g. a + # leftover standalone `PORT=41026 python app.py`) and every snapshot and + # every verdict then describe that foreign instance while the cells look + # green. Observed for real: a stray server made the 30/31 pass cells fail + # with bookmarks_exact_delta. + try: + probe = http.client.HTTPConnection("127.0.0.1", port, timeout=1) + probe.request("GET", "/") + probe.getresponse() + probe.close() + except OSError: + pass + else: + raise RuntimeError( + f"port :{port} already serves a site that this run did not start; " + f"stop it (lsof -ti tcp:{port}) before running the matrix" + ) handle = log.open("ab") process = subprocess.Popen( [sys.executable, "app.py"], cwd=str(site_dir), @@ -378,9 +401,14 @@ def inject_collateral_write(site_dir: Path) -> None: def grade(run_dir: Path) -> dict[str, Any]: - command = ["uv", "run", "python", "agent_demo/eval_judge.py", + # eval_judge.py imports the agent_demo project's dependencies (openai, + # simpleArgParser), so it must be launched from inside agent_demo/ — the + # repo root has no pyproject and its .venv lacks them (verified: launching + # from the repo root dies with ModuleNotFoundError: No module named 'openai' + # and never writes eval.json). + command = ["uv", "run", "python", "eval_judge.py", "--run_dir", str(run_dir), "--verifier", "True"] - result = subprocess.run(command, cwd=str(REPO), capture_output=True, text=True) + result = subprocess.run(command, cwd=str(REPO / "agent_demo"), capture_output=True, text=True) verdict_path = run_dir / "eval.json" if not verdict_path.is_file(): return {"pass": None, "reason": f"no eval.json (rc={result.returncode}): " @@ -413,6 +441,11 @@ def emit_cells(number: int, facts: dict, out_root: Path, base_url: str) -> list[ try: snapshot(SITE_DIR, run_dir, "initial") page = browser.new_page(viewport=VIEWPORT) + # agent.py navigates to the start URL before its first recorded + # step, so step 0's ``url`` is the start URL. Without this the + # page is still about:blank and the verifier's + # all_urls_match_local_origin gate fails the genuine run. + page.goto(base_url + "/") recorder = Recorder(run_dir, task_id, f"{base_url}/") if cell == "no_op": drive(page, base_url, [{"goto": "/"}], recorder) From 67cff3d40b8b2482e6210d5e3e256b0a41d8d1a5 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 02:53:44 -0400 Subject: [PATCH 11/25] =?UTF-8?q?fix(berkeley):=20C=20=E2=80=94=20negation?= =?UTF-8?q?=20before=20a=20titled=20name=20is=20no=20longer=20hidden=20by?= =?UTF-8?q?=20"Prof."?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C2 mutation rows found that a contradictory answer passed three verifiers: "The chair of EECS is not Prof. James Demmel, ...", "BIDS is not directed by Prof. David Culler, ..." and "The Economics department is not chaired by Prof. Ulrike Malmendier, ..." were all graded as affirmative. Cause: the clause splitter treated the period in an honorific as a sentence end, so the negation that precedes the name landed in a previous clause and _match_is_affirmative never saw it. The plain forms without the title ("James Demmel is not the chair of EECS.") were already rejected, which is why the unit suite missed it. _match_is_affirmative now computes clause boundaries over a length-preserving mask of honorific / degree abbreviations (Prof. Dr. Mr. Mrs. Ms. Miss Mx Rev. Fr. Sr. Jr. and "Ph.D."), so the two fixes compose: the name matcher sees the negation in front of it wherever the rendering carries a title. Mutation-checked: with the mask disabled the four new tests fail and only those (4 failed, 103 passed); with it enabled the suite is 531 passed. Rows re-run with the full variant + mutation sets; the matrix is re-run in the same phase. Co-Authored-By: Claude Code --- sites/berkeley/verify/tests/test_verify_13.py | 7 +++++ sites/berkeley/verify/tests/test_verify_23.py | 9 ++++++ sites/berkeley/verify/tests/test_verify_24.py | 10 +++++++ .../berkeley/verify/tests/test_verify_lib.py | 11 ++++++++ sites/berkeley/verify/verify_lib.py | 28 ++++++++++++++----- 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/sites/berkeley/verify/tests/test_verify_13.py b/sites/berkeley/verify/tests/test_verify_13.py index 6d3b826d6..91d235630 100644 --- a/sites/berkeley/verify/tests/test_verify_13.py +++ b/sites/berkeley/verify/tests/test_verify_13.py @@ -38,6 +38,13 @@ def test_negated_chair_fails(self) -> None: answer = "James Demmel is not the chair of EECS." self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + def test_negated_titled_chair_fails(self) -> None: + """C2 regression: the negation sits before the name, after "is" and the + title's period — the abbreviation must not hide it.""" + answer = ("The chair of EECS is not Prof. James Demmel, and the department is " + "located at 253 Cory Hall.") + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + def test_alternative_phrasing_passes(self) -> None: answer = "EECS is chaired by James Demmel; its location is Cory Hall." self.assertPasses(self.verdict(GENUINE_STEPS, answer)) diff --git a/sites/berkeley/verify/tests/test_verify_23.py b/sites/berkeley/verify/tests/test_verify_23.py index 8cc99f5a9..7286b72b5 100644 --- a/sites/berkeley/verify/tests/test_verify_23.py +++ b/sites/berkeley/verify/tests/test_verify_23.py @@ -59,6 +59,15 @@ def test_negated_director_fails(self) -> None: ) self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_director") + def test_negated_director_with_title_fails(self) -> None: + """C2 regression: "is not directed by Prof. X" — the title's period must + not hide the negation from the director matcher.""" + answer = ( + "Berkeley Institute for Data Science is not directed by Prof. David Culler; its " + "focus areas are Data Science, Statistics, Computational Methods, Open Science." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_director") + def test_alternative_phrasing_passes(self) -> None: answer = ( "The Berkeley Institute for Data Science, led by David Culler, works across Data " diff --git a/sites/berkeley/verify/tests/test_verify_24.py b/sites/berkeley/verify/tests/test_verify_24.py index fe1541214..f68174286 100644 --- a/sites/berkeley/verify/tests/test_verify_24.py +++ b/sites/berkeley/verify/tests/test_verify_24.py @@ -69,6 +69,16 @@ def test_negated_chair_fails(self) -> None: ) self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + def test_negated_titled_chair_fails(self) -> None: + """C2 regression: "is not chaired by Prof. X" — the title's period must + not hide the negation from the chair matcher.""" + answer = ( + "The Economics department is not chaired by Prof. Ulrike Malmendier and offers the " + "BA and PhD in Economics; Emmanuel Saez works on Public economics, inequality, " + "taxation, labor economics." + ) + self.assertFailsOn(self.verdict(GENUINE_STEPS, answer), "answer_has_chair") + def test_alternative_phrasing_passes(self) -> None: answer = ( "Prof. Ulrike Malmendier chairs the Department of Economics, which offers a BA and a " diff --git a/sites/berkeley/verify/tests/test_verify_lib.py b/sites/berkeley/verify/tests/test_verify_lib.py index d4f7f2e18..deaebd3a5 100644 --- a/sites/berkeley/verify/tests/test_verify_lib.py +++ b/sites/berkeley/verify/tests/test_verify_lib.py @@ -230,6 +230,17 @@ def test_negation_semantics(self) -> None: self.assertTrue(lib.contains_count("1 year, not 2 years", 1)) # Negation after the value still rejects it. self.assertFalse(lib.contains_year("2013 was not the founding year", 2013)) + # An honorific's period must not split the clause and hide a negation + # (C2 mutation rows on tasks 13/23/24). + self.assertFalse(lib.contains_person( + "The chair of EECS is not Prof. James Demmel, and the department is located at 253 Cory Hall.", + "Prof. James Demmel", + )) + self.assertTrue(lib.contains_person( + "The chair of EECS is Prof. James Demmel, located at 253 Cory Hall.", "Prof. James Demmel")) + self.assertFalse(lib.contains_phrase( + "BIDS is not directed by Prof. David Culler.", "The Berkeley Institute")) + self.assertFalse(lib.contains_phrase("holds a Ph.D. It is not the largest college.", "largest college")) def test_contains_count_as_pins_the_label(self) -> None: self.assertTrue(lib.contains_count_as("107 Nobel Laureates on the faculty", 107, "laureates")) diff --git a/sites/berkeley/verify/verify_lib.py b/sites/berkeley/verify/verify_lib.py index 6b2dc32f9..961093c2e 100644 --- a/sites/berkeley/verify/verify_lib.py +++ b/sites/berkeley/verify/verify_lib.py @@ -438,21 +438,35 @@ def normalize_text(value: Any) -> str: _AFTER_NEGATION_WINDOW = 15 _CONTRAST_CHARS = ",;–—(" +# Honorific / degree abbreviations whose period must not be read as a sentence +# end. Without this a name that follows "Prof." starts a fresh clause, so the +# negation in front of it is invisible: "The chair of EECS is not Prof. James +# Demmel" graded as an affirmative chair answer (found by the C2 mutation rows +# on tasks 13, 23 and 24). +_ABBREV_RE = re.compile(r"\b(?:prof|dr|mr|mrs|ms|miss|mx|rev|fr|sr|jr|ph\.?\s?d)\.", re.I) +_ABBREV_MASK = "\x00" + + +def _mask_abbreviations(text: str) -> str: + """Length-preserving mask of abbreviation periods (match indices stay valid).""" + return _ABBREV_RE.sub(lambda match: match.group(0).replace(".", _ABBREV_MASK), text) def _match_is_affirmative(text: str, match: re.Match[str]) -> bool: """Reject a match when a negation token contradicts it. Negation *before* the match (anywhere in the clause) rejects it — "did not - receive the National Medal of Science", "does not have 12". Negation *after* - the match rejects it only inside a short window that a contrastive comma has - not already closed, so a confirming contrast ("founded in 2013, not 2017") - stays affirmative while "2013 was not the founding year" does not. + receive the National Medal of Science", "does not have 12", "is not Prof. + James Demmel". Negation *after* the match rejects it only inside a short + window that a contrastive comma has not already closed, so a confirming + contrast ("founded in 2013, not 2017") stays affirmative while "2013 was not + the founding year" does not. Abbreviation periods do not split clauses. """ - starts = [m.end() for m in _CLAUSE_SPLIT_RE.finditer(text[:match.start()])] + view = _mask_abbreviations(text) + starts = [m.end() for m in _CLAUSE_SPLIT_RE.finditer(view[:match.start()])] clause_start = starts[-1] if starts else 0 - end_match = _CLAUSE_SPLIT_RE.search(text, match.end()) - clause_end = end_match.start() if end_match else len(text) + end_match = _CLAUSE_SPLIT_RE.search(view, match.end()) + clause_end = end_match.start() if end_match else len(view) before = text[clause_start:match.start()] if _NEGATION_RE.search(before): return False From 5aca1de58e5d098b909aba30c6008e8d234d685d Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 04:59:22 -0400 Subject: [PATCH 12/25] =?UTF-8?q?chore(berkeley):=20D=20=E2=80=94=20rubric?= =?UTF-8?q?=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite judge_rubric for all 22 accepted tasks (rubric-only diff; every other key unchanged, no row added or removed). Rubrics open with the scoring rules (step list authoritative; a checkpoint is true unless positively contradicted; a detail page is not satisfied by a listing page; every step must be on the local mirror origin; the verifier owns exact values and DB state; an empty answer forces failure) and close with an explicit origin checkpoint. No rubric contains a derived answer value; the task-contract leak scan is clean. Rebuilt from D3/D4 evidence: 16/21 -> 19/21 verifier-judge agreement on the shim arm; D5 (tasks 24, 27) run after this rewrite leaves one reproduced divergence (verify_27.py route gate, NOT-FIXED). Co-Authored-By: Claude Code --- sites/berkeley/tasks.jsonl | 44 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/sites/berkeley/tasks.jsonl b/sites/berkeley/tasks.jsonl index 9f588a41b..4fc2ae023 100644 --- a/sites/berkeley/tasks.jsonl +++ b/sites/berkeley/tasks.jsonl @@ -1,22 +1,22 @@ -{"web_name": "UC Berkeley", "id": "UC Berkeley--1", "ques": "Search for MBA programs at UC Berkeley. Which school offers the MBA and what is the program duration?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_1.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the MBA programme's detail page must have been opened from the programme listing; the answer must name the school that offers the MBA and the programme duration as printed on that page; a duration taken from another programme or from general knowledge fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--2", "ques": "Find the Computer Science BS program at UC Berkeley. What are the program requirements listed on the program detail page?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_2.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Computer Science BS detail page must have been opened (not the same-name MS or PhD pages); the answer must list at least four of the requirement items printed on that page; items belonging to the other Computer Science programmes fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--4", "ques": "Find news articles about CRISPR or gene editing on the Berkeley site. Who is the featured scientist and what award did they receive?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_4.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the article's detail page must have been opened from the news listing; the answer must name the scientist the article is about and the award the article reports as received; an award supplied from general knowledge fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--6", "ques": "Find events categorized as 'Lecture' at UC Berkeley. List at least three lecture events with their dates and locations.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_6.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Lecture-filtered events page must have been opened; the answer must list at least three Lecture events, each with the date and the location printed for it on that page; events of other categories, or dates and locations that do not match the listed event, fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--7", "ques": "Browse the faculty directory at UC Berkeley and find a professor in the EECS department who works on artificial intelligence. What are their specific research interests?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_7.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: a faculty-directory route to the EECS department must have been opened and the named professor's own profile page visited; the professor must be an EECS faculty member and the reported research interests must be those printed on that profile; another professor's interests fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--10", "ques": "Find the Berkeley Artificial Intelligence Research Lab (BAIR) on the UC Berkeley site. Who is the director and what year was it founded?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_10.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the centre's page must have been opened; the answer must give the director and the founding year printed on that page; a founding year supplied from general knowledge fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--11", "ques": "Go to the Admissions page at UC Berkeley. What is the application deadline for freshman applicants and what is the current acceptance rate?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_11.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Admissions page must have been opened; the answer must give the freshman application deadline and the acceptance rate printed on that page; deadlines of other applicant types and rates supplied from general knowledge fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--12", "ques": "Find all programs offered by the Haas School of Business at UC Berkeley. What degree types are available (e.g., MBA, PhD, etc.)?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_12.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the school-filtered programme list must have been opened; the answer must state the single degree type that list shows and must not attribute any other degree type to that school; a multi-programme answer fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--13", "ques": "Browse the Departments page at UC Berkeley and find the Department of Electrical Engineering and Computer Sciences (EECS). Who is the department chair and where is the department located?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_13.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the EECS department page must have been opened; the answer must give the chair and the department location printed there; another department's or another person's values fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--14", "ques": "Find the College of Engineering at UC Berkeley. How many undergraduate students and graduate students are enrolled? Who is the dean?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_14.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the page listing the schools and colleges must have been opened; the answer must give that college's undergraduate and graduate enrolment counts and its dean as printed on its entry; university-wide totals fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--16", "ques": "Find all online degree programs at UC Berkeley. Which programs offer an online option and what school do they belong to?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_16.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the programme listing must have been opened and the single online programme's detail page visited; the answer must name that programme, its degree type and the school it belongs to; naming more than one programme as offering an online option fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--17", "ques": "Look at the About Berkeley page. How many Nobel Laureates are currently on faculty, how many varsity sports does Berkeley have, and how many NCAA national titles has Berkeley won?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_17.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the About page must have been opened; the answer must give all three statistics printed on it (faculty Nobel laureates, varsity sports, NCAA national titles); the page's separate \"more than N Nobel Prizes\" line is not the faculty count and using it fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--19", "ques": "Navigate to the Berkeley news section and filter by the 'Athletics' category. Find a news article about a Berkeley sports championship and summarize what sport and what the achievement was.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_19.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Athletics-filtered news listing must have been opened and the chosen article's page visited; the article must be about a championship win, and the answer must report the sport and the achievement that article describes; a non-championship article, or a sport or achievement that does not match the opened article, fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--20", "ques": "Find the JD (Juris Doctor) program at Berkeley Law. What is the program duration, application deadline, and which school offers it?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_20.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the JD detail page must have been opened; the answer must give the duration, the application deadline and the school printed there; other programmes' values fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--22", "ques": "Go to the College of Letters and Science at UC Berkeley and find all the departments listed under it on the Departments page. How many departments belong to the College of Letters and Science?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_22.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Departments page must have been opened; the answer must give the number of departments listed under the College of Letters and Science and name several of them; the site-wide department total fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--23", "ques": "Find the research center 'Berkeley Institute for Data Science' (BIDS). What are its focus areas and who is the director? Then find if there are any related research centers listed on the same page.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_23.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the centre's page must have been opened; the answer must give the director and the focus areas printed there, and name at least one of the related centres that same page lists; a centre the page does not list fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--24", "ques": "Find the Economics PhD program at Berkeley. Then navigate to the Economics department page and identify the department chair and the other programs offered by the department. Finally, find one Economics faculty member and note their research interests.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_24.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: all three hops must have been opened in order: the programme page, then the department page, then a department faculty member's profile; the answer must give the department chair, the department's programme list and that faculty member's research interests as printed on those pages; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--25", "ques": "Berkeley holds an annual Spring Career Fair. Find this event, note the date, location, and whether registration is required. Then find two other career-related events and compare their details.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_25.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Career-filtered events listing must have been opened and the named fair's detail page visited; the answer must give that fair's date, location and registration status as printed, plus details of two further career events from the same listing; details that do not match the listed events fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--27", "ques": "Search the Berkeley site for 'Master of Engineering'. Find the MEng program and the Computer Science MS program. Report which department offers the Master of Engineering, and give the duration of each of the two programs exactly as listed on their program detail pages.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_27.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: both programme detail pages must have been opened (the Master of Engineering and the Computer Science MS); the answer must give the department printed for the Master of Engineering and both programme durations as printed on those pages; swapped or general-knowledge durations fail; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--28", "ques": "Find all programs that require GRE scores at UC Berkeley. Navigate to the programs page and identify which programs have 'GRE Required' indicated. What degree types most commonly require GRE?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_28.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the programme listings that carry the GRE badge must have been opened (both the PhD and the MS filtered listings, or the full listing); the answer must give the number of programmes showing the badge and the degree type that most commonly requires it; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--30", "ques": "Log in to the UC Berkeley website with the demo account (email: alice@berkeley.edu, password: test1234). Open the research center page for the Berkeley Seismological Laboratory and save the center to your bookmarks. Then open My Account and confirm the center is listed there, and report the director shown for the center.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_30.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the sign-in page must have been used with the demo account, the named centre's page opened and saved from there, and My Account opened afterwards; the answer must name the centre, report its director as printed, and confirm it is listed under My Account; a claim of success without the My Account visit fails; an empty answer fails."} -{"web_name": "UC Berkeley", "id": "UC Berkeley--31", "ques": "Log in to the UC Berkeley website with the demo account (email: bob@berkeley.edu, password: test1234). First save the Mathematical Sciences Research Institute to your bookmarks, then save the California Policy Lab. Open My Account and remove the Mathematical Sciences Research Institute bookmark, leaving only the other center saved; check My Account again to confirm which center remains. Report which center remains saved and its director as shown on the center page.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_31.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction; the screenshots cover only the last few steps, so a page or value absent from them is neither confirmed nor contradicted and the step list decides; the agent's final answer text is the evidence for the facts it reports; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the sign-in page must have been used with the demo account; both centre pages must be opened and saved in the order the task gives; My Account must be opened, the first bookmark removed, and My Account consulted again to confirm what remains; the answer must name the centre that remains with its director as printed and state that the other was removed; an empty answer fails."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--1", "ques": "Search for MBA programs at UC Berkeley. Which school offers the MBA and what is the program duration?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_1.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the programme search or degree filter must have been used to reach the MBA from a programme listing, and the MBA programme's detail page must have been opened from that listing; browsing the unfiltered programmes page, or opening the detail page from anywhere else, does not satisfy this; the answer must include the school that offers the MBA and the programme duration as printed on that detail page, not a duration taken from another programme or from general knowledge. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--2", "ques": "Find the Computer Science BS program at UC Berkeley. What are the program requirements listed on the program detail page?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_2.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Computer Science BS detail page must have been opened, and the same-name MS and PhD pages do not satisfy this; the answer must include at least four of the requirement items printed on that page, and must not include requirement items belonging to the other Computer Science programmes. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--4", "ques": "Find news articles about CRISPR or gene editing on the Berkeley site. Who is the featured scientist and what award did they receive?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_4.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the news listing must have been opened with the article's search term or category filter before the article's detail page was opened; opening the article directly from the home page or another page does not satisfy this; the answer must include the scientist the article is about and the award the article reports as received, not an award supplied from general knowledge. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--6", "ques": "Find events categorized as 'Lecture' at UC Berkeley. List at least three lecture events with their dates and locations.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_6.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the events listing must have been opened with the Lecture category filter; reading Lecture events off the home page or an unfiltered listing does not satisfy this; the answer must include at least three Lecture events, each with the date and the location printed for it on the filtered listing, not events of other categories or details that do not match the listed event. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--7", "ques": "Browse the faculty directory at UC Berkeley and find a professor in the EECS department who works on artificial intelligence. What are their specific research interests?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_7.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the EECS-filtered faculty listing, a faculty search on an AI-related interest, or the EECS department page must have been opened before the named professor's own profile page; browsing the unfiltered faculty directory alone does not satisfy this; the answer must include research interests printed on that profile for an EECS faculty member, not another professor's interests. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--10", "ques": "Find the Berkeley Artificial Intelligence Research Lab (BAIR) on the UC Berkeley site. Who is the director and what year was it founded?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_10.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the centre's page must have been opened; the answer must include the director and the founding year printed on that page, not a founding year supplied from general knowledge. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--11", "ques": "Go to the Admissions page at UC Berkeley. What is the application deadline for freshman applicants and what is the current acceptance rate?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_11.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Admissions page must have been opened; the answer must include the freshman application deadline and the acceptance rate printed on that page, not deadlines of other applicant types or rates supplied from general knowledge. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--12", "ques": "Find all programs offered by the Haas School of Business at UC Berkeley. What degree types are available (e.g., MBA, PhD, etc.)?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_12.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the school-filtered programme list must have been opened; the answer must state the single degree type that list shows and must not attribute any other degree type to that school. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--13", "ques": "Browse the Departments page at UC Berkeley and find the Department of Electrical Engineering and Computer Sciences (EECS). Who is the department chair and where is the department located?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_13.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the EECS department's own page must have been opened; reading its chair and location off the departments listing does not satisfy this; the answer must include the chair and the department location printed on that department page, not another department's or another person's values. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--14", "ques": "Find the College of Engineering at UC Berkeley. How many undergraduate students and graduate students are enrolled? Who is the dean?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_14.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the page listing the schools and colleges must have been opened; the answer must include that college's undergraduate and graduate enrolment counts and its dean as printed on its entry, not university-wide totals. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--16", "ques": "Find all online degree programs at UC Berkeley. Which programs offer an online option and what school do they belong to?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_16.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the programme listing must have been opened and the single online programme's detail page visited; the answer must include that programme, its degree type and the school it belongs to, and must not name any other programme as offering an online option. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--17", "ques": "Look at the About Berkeley page. How many Nobel Laureates are currently on faculty, how many varsity sports does Berkeley have, and how many NCAA national titles has Berkeley won?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_17.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the About page must have been opened; the answer must include all three statistics printed on it, the faculty Nobel laureate count, the varsity sports count and the NCAA national titles count, and must not report the page's separate alumni line about more than N Nobel Prizes as the faculty count. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--19", "ques": "Navigate to the Berkeley news section and filter by the 'Athletics' category. Find a news article about a Berkeley sports championship and summarize what sport and what the achievement was.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_19.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Athletics-filtered news listing must have been opened and the chosen article's page visited; the article must be about a championship win and the answer must include the sport and the achievement that article describes. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--20", "ques": "Find the JD (Juris Doctor) program at Berkeley Law. What is the program duration, application deadline, and which school offers it?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_20.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the JD detail page must have been opened; the answer must include the duration, the application deadline and the school printed there, not other programmes' values. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--22", "ques": "Go to the College of Letters and Science at UC Berkeley and find all the departments listed under it on the Departments page. How many departments belong to the College of Letters and Science?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_22.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Departments page must have been opened; the answer must include the number of departments listed under the College of Letters and Science and name several of them, not the site-wide department total. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--23", "ques": "Find the research center 'Berkeley Institute for Data Science' (BIDS). What are its focus areas and who is the director? Then find if there are any related research centers listed on the same page.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_23.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the centre's page must have been opened; the answer must include the director and the focus areas printed there, and name at least one of the related centres that same page lists. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--24", "ques": "Find the Economics PhD program at Berkeley. Then navigate to the Economics department page and identify the department chair and the other programs offered by the department. Finally, find one Economics faculty member and note their research interests.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_24.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: all three hops must have been opened in order, the programme page, then the department page, then a department faculty member's profile; the answer must include the department chair, the department's programme list and that faculty member's research interests as printed on those pages. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--25", "ques": "Berkeley holds an annual Spring Career Fair. Find this event, note the date, location, and whether registration is required. Then find two other career-related events and compare their details.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_25.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the Career-filtered events listing must have been opened and the named fair's detail page visited; the answer must include that fair's date, location and registration status as printed, plus details of two further career events from the same listing. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--27", "ques": "Search the Berkeley site for 'Master of Engineering'. Find the MEng program and the Computer Science MS program. Report which department offers the Master of Engineering, and give the duration of each of the two programs exactly as listed on their program detail pages.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_27.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: both programme detail pages must have been opened, the Master of Engineering and the Computer Science MS; the answer must include the department printed for the Master of Engineering and both programme durations as printed on those pages, not swapped or general-knowledge durations. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--28", "ques": "Find all programs that require GRE scores at UC Berkeley. Navigate to the programs page and identify which programs have 'GRE Required' indicated. What degree types most commonly require GRE?", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_28.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the programme listings that carry the GRE badge must have been opened, either both the PhD and the MS filtered listings or the full listing; the answer must include the number of programmes showing the badge and the degree type that most commonly requires it. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--30", "ques": "Log in to the UC Berkeley website with the demo account (email: alice@berkeley.edu, password: test1234). Open the research center page for the Berkeley Seismological Laboratory and save the center to your bookmarks. Then open My Account and confirm the center is listed there, and report the director shown for the center.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_30.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the recorded steps must show the sign-in page used with the demo account, the named centre's page opened and saved from there, and My Account opened afterwards; the answer must include the centre name, its director as printed, and a statement that it is listed under My Account. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} +{"web_name": "UC Berkeley", "id": "UC Berkeley--31", "ques": "Log in to the UC Berkeley website with the demo account (email: bob@berkeley.edu, password: test1234). First save the Mathematical Sciences Research Institute to your bookmarks, then save the California Policy Lab. Open My Account and remove the Mathematical Sciences Research Institute bookmark, leaving only the other center saved; check My Account again to confirm which center remains. Report which center remains saved and its director as shown on the center page.", "web": "http://localhost:40026/", "upstream_url": "https://www.berkeley.edu/", "verifier_path": "sites/berkeley/verify/verify_31.py", "judge_rubric": "Scoring rules: the recorded step list is authoritative and the final page must be reachable from the recorded steps; a checkpoint is true unless positively contradicted; \"not visible / below the fold / truncated\" is not a contradiction, and the screenshots cover only the last few steps, so a page or detail absent from them is neither confirmed nor contradicted; a checkpoint that requires a detail page is not satisfied by a listing page that shows the same fact; every recorded step must be on the local mirror origin, and a page on the real upstream website satisfies no checkpoint; the deterministic verifier owns exact numbers, dates and database state, so grade the answer text and the recorded steps and do not fail a checkpoint over a number, a wording detail or formatting; if the final answer is empty, success must be false; when no checkpoint is contradicted, success must be true and the rationale must not conclude the opposite; do not add requirements absent from the task. Checkpoints: the recorded steps must show the sign-in page used with the demo account, both centre pages opened and saved in the order the task gives, My Account opened, the first bookmark removed, and My Account consulted again; the answer must include the centre that remains, its director as printed, and a statement that the other was removed. every recorded step must be on the local mirror origin shown in the run's start URL, and no checkpoint is satisfied by a page on the real upstream website."} From 66a0c8defb569249bf248f3402f661d7145fbee0 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 05:40:20 -0400 Subject: [PATCH 13/25] =?UTF-8?q?fix(berkeley):=20E=20=E2=80=94=20app=20ro?= =?UTF-8?q?bustness:=20POST-only=20logout,=20per-process=20key,=20bounds,?= =?UTF-8?q?=20FK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appendix A §6 merge blockers found by the Phase E executable probes: - SECRET_KEY was the committed literal 'berkeley-mirror-secret-key-2024', so a cookie signed with it read /account without a password (probe: 200). It now comes from BERKELEY_SECRET_KEY or a per-process random key, and a wrong-key cookie is bounced to /login. - GET/HEAD /logout returned 302 and cleared the session (prefetcher-reachable); the route is POST-only now (GET/HEAD -> 405) and the site chrome's Sign Out control is a CSRF-protected form instead of a link. - A session cookie with a non-numeric _user_id raised int() into a 500; load_user now fails closed (anonymous). - Huge query/form integers overflowed SQLite ('/news?page=9'*20 -> 500); bounded_int/page_arg and the event-id range check answer 200/404 instead. - MAX_CONTENT_LENGTH (256 KB) and SESSION_COOKIE_HTTPONLY/SAMESITE are set. - Empty or invalid /bookmark/add submissions silently redirected; they now answer 400 (closed item-type vocabulary) or 404 (missing row), and /bookmark/remove is owner-scoped (another user's id is a 404) with a bounded id. - '?next=https://evil.example/' bounced /login and /bookmark/add off-mirror; safe_next keeps redirect targets same-origin. - SQLite PRAGMA foreign_keys was off (orphan bookmark rows accepted); an Engine connect listener turns enforcement on. - Duplicate registration now rolls back on IntegrityError. Accessibility chrome on every page (the maintainers' b87db8f batch): skip link, mirror/synthetic-data notice, focus-visible outlines, role=alert on flash messages. sites/berkeley/tests/test_app_robustness.py pins all of it and was mutation-checked (mutations archived under scripts_dev/logs/phase_e/). Co-Authored-By: Claude Code --- sites/berkeley/app.py | 173 ++++++++++--- sites/berkeley/templates/base.html | 30 ++- sites/berkeley/tests/test_app_robustness.py | 256 ++++++++++++++++++++ 3 files changed, 420 insertions(+), 39 deletions(-) create mode 100644 sites/berkeley/tests/test_app_robustness.py diff --git a/sites/berkeley/app.py b/sites/berkeley/app.py index a292dc318..cd7ee0432 100644 --- a/sites/berkeley/app.py +++ b/sites/berkeley/app.py @@ -2,13 +2,18 @@ """UC Berkeley mirror — Flask application.""" import os import re +import secrets import sys from datetime import datetime from math import ceil +from urllib.parse import urlsplit from flask import (Flask, render_template, request, redirect, url_for, flash, jsonify, session, abort, g) from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import event +from sqlalchemy.exc import IntegrityError +from sqlalchemy.engine import Engine from flask_login import (LoginManager, UserMixin, login_user, logout_user, login_required, current_user) from flask_wtf import FlaskForm @@ -20,14 +25,33 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) app = Flask(__name__) -app.config['SECRET_KEY'] = 'berkeley-mirror-secret-key-2024' +# Repo convention (webmd_doctor / walmart_careers): env-provided secret or a +# per-process random key. Never a committed constant: with a known key anyone +# can sign their own session cookie and read /account without the password. +app.config['SECRET_KEY'] = os.environ.get('BERKELEY_SECRET_KEY') or secrets.token_hex(32) app.config['SQLALCHEMY_DATABASE_URI'] = ( f"sqlite:///{os.path.join(BASE_DIR, 'instance', 'berkeley.db')}") app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['WTF_CSRF_TIME_LIMIT'] = None +# Explicit request-size cap on top of Flask's MAX_FORM_MEMORY_SIZE default; +# mirrors webmd_doctor. Every form on this site is a few KB. +app.config['MAX_CONTENT_LENGTH'] = 256 * 1024 +app.config['SESSION_COOKIE_HTTPONLY'] = True +app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' os.makedirs(os.path.join(BASE_DIR, 'instance'), exist_ok=True) + +@event.listens_for(Engine, "connect") +def enable_sqlite_foreign_keys(connection, _record): + """SQLite defaults PRAGMA foreign_keys=0, so an orphan bookmark row is + accepted without complaint. The seeded schema declares the foreign keys; + turn enforcement on for every connection.""" + cursor = connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + db = SQLAlchemy(app) bcrypt = Bcrypt(app) login_manager = LoginManager(app) @@ -67,6 +91,48 @@ def slugify(text): s = re.sub(r'[\s]+', '-', s.strip().lower()) return s + +def bounded_int(raw, maximum_digits=9): + """int() for all-digit strings within a fixed length; None otherwise. + + Query and form values reach SQLAlchemy as bound integers; an unbounded + conversion ('9' * 20) overflows SQLite's INTEGER and turns a normal 404 + path into a 500. Mirrors the webmd_doctor helper. + """ + raw = str(raw or '') + if not raw.isdigit() or len(raw) > maximum_digits: + return None + return int(raw) + + +MAX_PAGE = 10**4 + + +def page_arg(name='page'): + """A 1..MAX_PAGE page number; malformed or huge values fall back to 1.""" + value = bounded_int(request.args.get(name, ''), maximum_digits=6) + if value is None or value < 1: + return 1 + return min(value, MAX_PAGE) + + +def safe_next(raw): + """Same-origin relative redirect target or None. + + ``?next=https://evil.example/`` on /login (and the hidden ``next`` field on + the bookmark form) would otherwise bounce the browser off the mirror. + """ + if not raw or not isinstance(raw, str): + return None + if any(ord(char) < 32 or ord(char) == 127 for char in raw): + return None + if raw.startswith('//') or '\\' in raw: + return None + parsed = urlsplit(raw) + if parsed.scheme or parsed.netloc or not parsed.path.startswith('/'): + return None + return raw + # ─── Models ─────────────────────────────────────────────────────────────────── class User(db.Model, UserMixin): @@ -241,7 +307,12 @@ class BookmarkForm(FlaskForm): @login_manager.user_loader def load_user(user_id): - return db.session.get(User, int(user_id)) + # A tampered session cookie with a non-numeric or out-of-range id must fail + # closed (anonymous), not raise int()/OverflowError into a 500. + value = bounded_int(user_id) + if value is None: + return None + return db.session.get(User, value) # ─── Context Processors ─────────────────────────────────────────────────────── @@ -287,7 +358,7 @@ def news(): q = request.args.get('q', '').strip() category = request.args.get('category', '') featured = request.args.get('featured', '') - page = request.args.get('page', 1, type=int) + page = page_arg() query = NewsArticle.query if q: @@ -354,7 +425,7 @@ def programs(): q = request.args.get('q', '').strip() college_slug = request.args.get('college', '') degree = request.args.get('degree', '') - page = request.args.get('page', 1, type=int) + page = page_arg() query = Program.query if q: @@ -404,7 +475,7 @@ def events(): q = request.args.get('q', '').strip() category = request.args.get('category', '') date_filter = request.args.get('date', 'upcoming') - page = request.args.get('page', 1, type=int) + page = page_arg() now = BENCHMARK_NOW query = Event.query @@ -450,6 +521,9 @@ def events(): @app.route('/events/') def event_detail(event_id): + if not 0 < event_id < 2**31: + # A 20-digit id would overflow SQLite's INTEGER and raise a 500. + abort(404) event = db.session.get(Event, event_id) if event is None: abort(404) @@ -574,7 +648,7 @@ def search(): def faculty(): q = request.args.get('q', '').strip() dept_slug = request.args.get('dept', '') - page = request.args.get('page', 1, type=int) + page = page_arg() query = Faculty.query if q: @@ -626,7 +700,7 @@ def login(): user = User.query.filter_by(email=form.email.data.lower().strip()).first() if user and user.check_password(form.password.data): login_user(user) - next_page = request.args.get('next') + next_page = safe_next(request.args.get('next')) flash('Welcome back!', 'success') return redirect(next_page or url_for('index')) flash('Invalid email or password.', 'danger') @@ -651,16 +725,25 @@ def register(): ) user.set_password(form.password.data) db.session.add(user) - db.session.commit() - login_user(user) - flash('Account created! Welcome to UC Berkeley.', 'success') - return redirect(url_for('index')) + try: + db.session.commit() + except IntegrityError: + # Concurrent duplicate registration: the unique constraint won; + # roll back so the session stays usable and re-render the form. + db.session.rollback() + flash('Email already registered.', 'danger') + else: + login_user(user) + flash('Account created! Welcome to UC Berkeley.', 'success') + return redirect(url_for('index')) return render_template('register.html', form=form) -@app.route('/logout', methods=['GET', 'POST']) +@app.route('/logout', methods=['POST']) @login_required def logout(): + # POST-only: a prefetcher (or any GET crawler) must not be able to end a + # session; GET/HEAD now answer 405. logout_user() flash('You have been logged out.', 'info') return redirect(url_for('index')) @@ -708,39 +791,57 @@ def account(): return render_template('account.html', bookmark_details=bookmark_details) +BOOKMARK_TYPES = { + 'program': Program, + 'news': NewsArticle, + 'event': Event, + 'faculty': Faculty, + 'research': ResearchCenter, +} + + @app.route('/bookmark/add', methods=['POST']) @login_required def bookmark_add(): - item_type = request.form.get('item_type') - item_id = request.form.get('item_id', type=int) - note = request.form.get('note', '') - if item_type and item_id: - existing = Bookmark.query.filter_by( - user_id=current_user.id, item_type=item_type, item_id=item_id - ).first() - if not existing: - bm = Bookmark(user_id=current_user.id, item_type=item_type, - item_id=item_id, note=note) - db.session.add(bm) - db.session.commit() - flash('Saved to bookmarks.', 'success') - else: - flash('Already bookmarked.', 'info') - next_url = request.form.get('next') or request.referrer or url_for('account') + # An empty or invalid submission must fail loudly (400), never redirect as + # if something was saved: `item_type` is a closed vocabulary and the target + # row must exist, otherwise a bogus row lands in the bookmarks table. + item_type = request.form.get('item_type', '') + item_id = bounded_int(request.form.get('item_id', '')) + note = request.form.get('note', '')[:500] + model = BOOKMARK_TYPES.get(item_type) + if model is None or not item_id: + abort(400) + if db.session.get(model, item_id) is None: + abort(404) + existing = Bookmark.query.filter_by( + user_id=current_user.id, item_type=item_type, item_id=item_id + ).first() + if not existing: + bm = Bookmark(user_id=current_user.id, item_type=item_type, + item_id=item_id, note=note) + db.session.add(bm) + db.session.commit() + flash('Saved to bookmarks.', 'success') + else: + flash('Already bookmarked.', 'info') + next_url = (safe_next(request.form.get('next')) or safe_next(request.referrer) + or url_for('account')) return redirect(next_url) @app.route('/bookmark/remove', methods=['POST']) @login_required def bookmark_remove(): - bookmark_id = request.form.get('bookmark_id', type=int) - if bookmark_id: - bm = db.session.get(Bookmark, bookmark_id) - if bm and bm.user_id == current_user.id: - db.session.delete(bm) - db.session.commit() - flash('Bookmark removed.', 'info') - return redirect(request.referrer or url_for('account')) + bookmark_id = bounded_int(request.form.get('bookmark_id', '')) + if bookmark_id is None: + abort(400) + # Scoped to the signed-in user: another user's id is a 404, not a no-op. + bm = Bookmark.query.filter_by(id=bookmark_id, user_id=current_user.id).first_or_404() + db.session.delete(bm) + db.session.commit() + flash('Bookmark removed.', 'info') + return redirect(safe_next(request.referrer) or url_for('account')) @app.route('/_health') diff --git a/sites/berkeley/templates/base.html b/sites/berkeley/templates/base.html index 10b54db2c..c87cc27ad 100644 --- a/sites/berkeley/templates/base.html +++ b/sites/berkeley/templates/base.html @@ -37,6 +37,11 @@ .top-bar .container { display: flex; justify-content: space-between; align-items: center; } .top-bar a { color: #ccc; } .top-bar a:hover { color: var(--gold); text-decoration: none; } + .top-bar .link-button { + background: none; border: none; padding: 0; margin: 0; + color: #ccc; font: inherit; cursor: pointer; text-decoration: none; + } + .top-bar .link-button:hover { color: var(--gold); text-decoration: none; } .top-bar-left span { margin-right: 16px; } .top-bar-right a { margin-left: 12px; } /* Header */ @@ -160,6 +165,16 @@ transition: border-color 0.2s; } .form-control:focus { outline: none; border-color: var(--light-blue); box-shadow: 0 0 0 3px rgba(59,126,161,0.15); } + /* Visible keyboard focus for every interactive element (WCAG 2.4.7/1.4.11) */ + a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, + textarea:focus-visible, [tabindex]:focus-visible { outline: 3px solid #0b5cab; outline-offset: 2px; } + .top-bar a:focus-visible, header a:focus-visible, footer a:focus-visible, + nav.main-nav a:focus-visible { outline-color: var(--gold); } + /* Skip link: first focusable element, off-screen until focused */ + .skip-link { position: absolute; left: -9999px; top: 0; z-index: 200; background: var(--white); color: var(--blue); padding: 10px 16px; font-family: Arial, sans-serif; font-weight: 700; } + .skip-link:focus { left: 0; } + /* Offline-mirror disclosure, above the footer on every page */ + .mirror-notice { background: var(--off-white); color: var(--gray-dark); text-align: center; font-family: Arial, sans-serif; font-size: 13px; padding: 10px 16px; border-top: 1px solid var(--gray-light); } .form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; } .form-text { font-size: 12px; color: var(--gray-mid); font-family: Arial, sans-serif; margin-top: 4px; } /* Filter bar */ @@ -251,6 +266,8 @@ + +
@@ -261,7 +278,10 @@
{% if current_user.is_authenticated %} {{ current_user.full_name or current_user.username }} - Sign Out +
+ + + {% else %} Sign In Create Account @@ -310,7 +330,7 @@ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
-
+ {% endfor %} diff --git a/sites/berkeley/tests/test_answer_leaks.py b/sites/berkeley/tests/test_answer_leaks.py new file mode 100644 index 000000000..d3baec6d1 --- /dev/null +++ b/sites/berkeley/tests/test_answer_leaks.py @@ -0,0 +1,379 @@ +"""Answer-leak sweep: task answer facts must not appear before discovery. + +Follows the maintainers' ``webmd_doctor/tests/test_answer_leaks.py`` pattern: +every accepted task's answer facts are enumerated from ``verify/ground_truth.py`` +(never "ground truth minus the ques tokens" — that anti-pattern is checked in +``verify/tests/test_tasks_contract.py``), classified, and asserted absent from +the rendered surfaces *outside the task's own discovery route* (the routes its +verifier requires). + +Value classes +- UNIQUE: one-off facts that identify the answer — person names (directors, + chairs, deans), award names, focus-area phrases, requirement items, exact + figures tied to one entity. These must not be rendered on any surface outside + the task's discovery set, except the documented SHARED entries below. +- GENERIC: catalogue vocabulary that legitimately appears everywhere (college + and department names, degree types, programme names, single common words, + small integers such as durations/counts). A shared word cannot identify an + answer; scoring still binds it to the target entity's required page. + +Documented SHARED occurrences (each carries its reason) are listed in +``SHARED``; everything else must be absent, so a new leak fails this test. + +The entity-bound tests at the bottom pin the two listing-card leak classes the +review found: research-centre cards (index / /research / search results / +related-centre blocks) must not render a centre's director, founding year or +focus areas, and department listing cards must not render the chair/location. +Both were mutation-verified (injecting the field back into the template fails +the test). +""" +from __future__ import annotations + +import os +import re +import sqlite3 +import sys +from pathlib import Path + +import pytest + +SITE = Path(__file__).resolve().parents[1] +SEED = SITE / "instance_seed" / "berkeley.db" + +os.environ["WEBSYN_SKIP_BOOTSTRAP"] = "1" +sys.path.insert(0, str(SITE)) +sys.path.insert(0, str(SITE / "verify")) + +import ground_truth # noqa: E402 + +# Routes each task's verifier requires (its discovery surface). +DISCOVERY = { + 1: [r"/programs(?:\?|$)", r"/programs/business-administration-mba$"], + 2: [r"/programs(?:\?|$)", r"/programs/computer-science-bs$"], + 4: [r"/news(?:\?|$)", r"/news/crispr-pioneer"], + 6: [r"/events(?:\?|$)", r"/events/\d+$"], + 7: [r"/faculty(?:\?|$)", r"/faculty/stuart-russell$", r"/departments/eecs$"], + 10: [r"/research(?:\?|$)", r"/research/bair$"], + 11: [r"/admissions$"], + 12: [r"/programs(?:\?|$)", r"/programs/business-administration-mba$"], + 13: [r"/departments$", r"/departments/eecs$"], + 14: [r"/academics$"], + 16: [r"/programs(?:\?|$)", r"/programs/data-science-ms$"], + 17: [r"/about$"], + 19: [r"/news(?:\?|$)", r"/news/(womens-gymnastics|cal-wins-pac-12)"], + 20: [r"/programs(?:\?|$)", r"/programs/juris-doctor-jd$"], + 22: [r"/departments$"], + 23: [r"/research(?:\?|$)", r"/research/bids$"], + 24: [r"/programs(?:/economics-phd|\?|$)", r"/departments/economics$", r"/faculty/"], + 25: [r"/events(?:\?|$)", r"/events/\d+$"], + 27: [r"/programs(?:\?|$)", r"/programs/master-of-engineering-meng$", + r"/programs/computer-science-ms$"], + 28: [r"/programs(?:\?|$)"], + 30: [r"/login$", r"/research/seismo-lab$", r"/account$"], + 31: [r"/login$", r"/research/(msri|cpl)$", r"/account$"], +} + +# Catalogue vocabulary / shared words: a match cannot identify the answer. +GENERIC_VALUES = { + "haas school of business", "college of engineering", + "college of letters and science", "school of information", + "electrical engineering and computer sciences", + "department of electrical engineering and computer sciences", + "berkeley artificial intelligence research lab", "statistics", "economics", + "artificial intelligence", "machine learning", "ai safety", "ai", + "computer architecture", "algorithms", "software engineering", "phd", "ms", + "master of engineering", "computer science", "data science", "mba", "j.d.", + "juris doctor", "february 1", "november 30", "online", + "business administration", # the programme's catalogue name +} + +# Genuine co-occurrences outside the discovery route, with the reason. +SHARED = { + (4, "National Medal of Science"): "the award is in the article's own headline, which the home page features as site publicity", + (4, "Jennifer Doudna"): "public figure named in headlines, event titles and department prose; the verifier requires the CRISPR article visit and the award+person binding", + (6, "Nobel Laureate Lecture: Jennifer Doudna on the Future of Gene Editing"): "home-page 'Upcoming Events' promo; the verifier requires the /events?category=Lecture listing and binds 3 events", + (6, "Berkeley AI Lab Open House"): "home-page 'Upcoming Events' promo of the same listing the task must open", + (10, "2013"): "BIDS and BAIR share the founding year; a same-value row on another centre's page is not the BAIR answer", + (14, "Dean Tsu-Jae King Liu"): "a Berkeley News article reports on the dean; the verifier requires the /academics card", + (17, "105"): "home-page stat tile repeats the About-page NCAA-title figure; the verifier requires the /about visit", + (19, "Women's Gymnastics Wins NCAA Championship"): "related-article links on other Athletics articles; the verifier requires /news?category=Athletics and a championship article visit", + (20, "February 1"): "the graduate deadline string is rendered by other programme pages too", + (23, "Statistics"): "a department/interest word, not identifying", + (23, "Computational Methods"): "a focus phrase SCCN and EECS faculty also use; not identifying BIDS", + (25, "Spring Career Fair 2026"): "the event is named in the ques and is catalogue listing data; the verifier requires the /events?category=Career listing and the event page", + (24, "Emmanuel Saez"): "named in a Berkeley News article; the verifier requires the faculty profile visit and binds the interests", + (31, "Prof. Tatiana Toro"): "she chairs the Mathematics department as well as directing MSRI; another entity's page is not the MSRI answer", +} + +SURFACE_PATHS = [ + "/", "/about", "/academics", "/admissions", "/departments", "/research", + "/news", "/news?q=CRISPR", "/news?category=Athletics", "/news?featured=1", + "/programs", "/programs?q=MBA", "/programs?q=Computer%20Science", + "/programs?q=Master%20of%20Engineering", "/programs?degree=PhD", + "/programs?degree=MS", "/programs?college=haas-business", "/programs?page=2", + "/events", "/events?category=Lecture", "/events?category=Career", + "/search?q=Berkeley", "/search?q=MBA", "/search?q=Economics", "/search?q=Data Science", + "/faculty", "/faculty?dept=eecs", "/login", "/register", "/nope-404", +] + + +def _facts(): + return ground_truth.all_ground_truth(str(SEED)) + + +def scan_values(facts: dict) -> dict[int, list[tuple[str, str]]]: + """task -> [(label, value)] for the UNIQUE-class answer facts.""" + out: dict[int, list[tuple[str, str]]] = {} + + def add(n, label, value): + if value in (None, "", [], {}): + return + text = str(value).strip() + if not text or text.lower() in GENERIC_VALUES: + return + out.setdefault(n, []).append((label, text)) + + for n, row in facts.items(): + if n == 1: + add(n, "college", row["college"]) + elif n == 2: + for i, item in enumerate(row["items"]): + add(n, f"requirement{i}", item) + elif n == 4: + add(n, "person", row["person"]); add(n, "award", row["award"]) + elif n == 6: + for i, ev in enumerate(row["upcoming"][:6]): + add(n, f"event_title{i}", ev["title"]) + elif n == 7: + # The verifier accepts any AI-family EECS professor, and a faculty + # name is directory data (shown on the listing the task must open), + # so the identity is catalogue vocabulary; the graded binding is the + # profile visit plus the interest tokens. + pass + elif n == 10: + add(n, "director", row["director"]); add(n, "founded", row["founded_year"]) + elif n == 11: + add(n, "deadline", row["deadline"]); add(n, "rate", row["acceptance_rate"]) + elif n == 12: + add(n, "programme", row["programmes"][0]["name"]) + elif n == 13: + add(n, "chair", row["chair"]); add(n, "location", row["location"]) + elif n == 14: + add(n, "dean", row["dean"]) + elif n == 16: + add(n, "programme", row["program"]["name"]) + elif n == 17: + add(n, "nobel", row["nobel_laureates"]); add(n, "sports", row["varsity_sports"]) + add(n, "titles", row["national_titles"]) + elif n == 19: + for i, art in enumerate(row["championships"]): + add(n, f"championship_title{i}", art["title"]) + elif n == 20: + add(n, "deadline", row["deadline"]) + elif n == 22: + add(n, "count", len(row["departments"])) + elif n == 23: + add(n, "director", row["centre"]["director"]) + for i, area in enumerate(row["focus_areas"]): + add(n, f"focus{i}", area) + # related-centre names are the catalogue's own centre names (the + # /research listing shows them all and the ques quotes the target), + # so they are not scanned as unique facts. + elif n == 24: + add(n, "chair", row["chair"]); add(n, "member", "Emmanuel Saez") + elif n == 25: + add(n, "anchor", row["anchor"]["title"]) + elif n == 27: + add(n, "department", row["department"]) + elif n == 28: + add(n, "count", row["count"]); add(n, "degree", row["most_common_degree"]) + elif n == 30: + add(n, "director", row["director"]) + elif n == 31: + add(n, "director1", row["directors"][0]); add(n, "director2", row["directors"][1]) + return out + + +@pytest.fixture(scope="module") +def client(): + import app as app_module + + app_module.app.config["TESTING"] = True + with app_module.app.test_client() as test_client: + yield test_client + + +def _all_paths() -> list[str]: + paths = list(SURFACE_PATHS) + con = sqlite3.connect(SEED) + try: + for (slug,) in con.execute("SELECT slug FROM programs"): + paths.append(f"/programs/{slug}") + for (slug,) in con.execute("SELECT slug FROM news_articles"): + paths.append(f"/news/{slug}") + for (slug,) in con.execute("SELECT slug FROM research_centers"): + paths.append(f"/research/{slug}") + for (slug,) in con.execute("SELECT slug FROM departments"): + paths.append(f"/departments/{slug}") + for (slug,) in con.execute("SELECT slug FROM faculty"): + paths.append(f"/faculty/{slug}") + for (eid,) in con.execute("SELECT id FROM events"): + paths.append(f"/events/{eid}") + finally: + con.close() + return sorted(set(paths)) + + +def _outside_discovery(task: int, path: str) -> bool: + return not any(re.search(pattern, path) for pattern in DISCOVERY[task]) + + +def _digit_bound(value: str, haystack: str) -> bool: + """Match with digit boundaries so '12' does not hit '1,200' or '14.4'.""" + return re.search(r"(? {response.status_code}" + body = response.get_data(as_text=True) + assert "Director:" not in body, f"{path} renders a Director field" + assert "Chair:" not in body, f"{path} renders a Chair field" + + +def _card_windows(html: str, slug: str, window: int = 700) -> list[str]: + """Text windows around every link to /research/ (its listing cards).""" + windows = [] + for match in re.finditer(rf"/research/{re.escape(slug)}", html): + windows.append(html[max(0, match.start() - window): match.end() + window]) + return windows + + +def test_related_centre_cards_hide_director_founded_and_focus(client): + """Entity-bound: the related-centre cards on another centre's page (and the + home page / search cards) must not render a centre's director, founding year + or focus-area tags before its own detail page is visited.""" + facts = _facts() + targets = [facts[10]["centre"], facts[23]["centre"], facts[30]["centre"], + facts[31]["first"], facts[31]["second"]] + pages = ["/", "/research", "/search?q=Berkeley"] + pages += [f"/research/{t['slug']}" for t in targets] + checked = 0 + for path in pages: + response = client.get(path) + if response.status_code != 200: + continue + body = response.get_data(as_text=True) + for centre in targets: + if path == f"/research/{centre['slug']}": + continue # the centre's own detail page is where they belong + windows = _card_windows(body, centre["slug"]) + if not windows: + continue + for window in windows: + assert centre["director"] not in window, ( + f"{path}: card for {centre['slug']} renders its director") + assert f">{centre['founded_year']}<" not in window, ( + f"{path}: card for {centre['slug']} renders its founding year") + for area in (centre["focus_areas"] or "").split(","): + area = area.strip() + if len(area) < 4 or area.lower() in GENERIC_VALUES: + continue + assert f">{area}<" not in window, ( + f"{path}: card for {centre['slug']} renders focus area {area!r}") + checked += 1 + assert checked >= 6, f"too few centre cards inspected ({checked})" + # positive control: each centre's own detail page still carries them + for centre in targets: + page = client.get(f"/research/{centre['slug']}").get_data(as_text=True) + assert centre["director"] in page, f"/research/{centre['slug']} lost its director" + assert str(centre["founded_year"]) in page, f"/research/{centre['slug']} lost founded_year" + + +def test_department_listing_cards_hide_chair_and_location(client): + """Entity-bound: the /departments cards must not render a chair or location + (they belong to the department's own page, which tasks 13/24 require).""" + facts = _facts() + eecs = facts[13] + economics = facts[24] + body = client.get("/departments").get_data(as_text=True) + for field in (eecs["chair"], economics["chair"]): + assert field not in body, f"/departments: department card renders {field!r}" + # positive control: the detail pages still carry them + page = client.get(f"/departments/{eecs['department']['slug']}").get_data(as_text=True) + assert eecs["chair"] in page and eecs["location"] in page + page = client.get(f"/departments/{economics['department']['slug']}").get_data(as_text=True) + assert economics["chair"] in page + + +def test_no_verifier_uses_ground_truth_minus_ques_tokens(): + """Appendix A §3 anti-pattern: the answer set must never be computed by + subtracting the question's tokens from the ground truth.""" + verifiers = sorted(SITE.glob("verify/verify_*.py")) + [SITE / "verify/verify_lib.py"] + forbidden = re.compile(r"ques[a-z_]*\s*[-−]|-\s*set\(\s*ques|difference\(.*ques|remove\(.*ques", + re.I) + hits = [] + for file in verifiers: + text = file.read_text() + for match in forbidden.finditer(text): + hits.append(f"{file.name}: {match.group(0)!r}") + assert not hits, f"ques-subtraction anti-pattern found: {hits}" From 0603a3d656ec9da8b39c9e1ef2d9556e05bd9e51 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 05:40:31 -0400 Subject: [PATCH 15/25] =?UTF-8?q?fix(berkeley):=20E=20=E2=80=94=20task=201?= =?UTF-8?q?6=20workflow=20pages=20to=20the=20catalogue=20page=20holding=20?= =?UTF-8?q?its=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /programs?page=3 lists programmes 41-60; the Data Science MS is the 24th row, i.e. on page 2 (the run then opened the detail page by URL, so the listing hop never showed the target). Replay fixtures are data-driven and unaffected. Co-Authored-By: Claude Code --- sites/berkeley/verify/tests/run_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/berkeley/verify/tests/run_matrix.py b/sites/berkeley/verify/tests/run_matrix.py index 83da224dc..fc21d884c 100644 --- a/sites/berkeley/verify/tests/run_matrix.py +++ b/sites/berkeley/verify/tests/run_matrix.py @@ -83,7 +83,7 @@ 12: {"steps": [{"goto": "/"}, {"goto": "/programs?college=haas-business"}]}, 13: {"steps": [{"goto": "/"}, {"goto": "/departments"}, {"goto": "/departments/eecs"}]}, 14: {"steps": [{"goto": "/"}, {"goto": "/academics"}]}, - 16: {"steps": [{"goto": "/"}, {"goto": "/programs?page=3"}, + 16: {"steps": [{"goto": "/"}, {"goto": "/programs?page=2"}, {"goto": "/programs/data-science-ms"}]}, 17: {"steps": [{"goto": "/"}, {"goto": "/about"}]}, 19: {"steps": [{"goto": "/"}, {"goto": "/news?category=Athletics"}, From 678f7030df0a701997866d8e95a3546d29ddaf16 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 05:46:24 -0400 Subject: [PATCH 16/25] =?UTF-8?q?fix(berkeley):=20E=20=E2=80=94=20neutral?= =?UTF-8?q?=20ORDER=20BY=20on=20list=20queries=20and=20/search=20field=20p?= =?UTF-8?q?arity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appendix A §4: - /search matched narrower field sets than the catalogue pages it duplicates: news content, event location/organizer and faculty title were reachable from the listing filters but not from the global search. All catalogue fields are now matched (superset direction only; no result set shrinks). - The related-centre / related-programme / colleague / department-programme / home-page research queries had no ORDER BY and fell back to rowid insert order; each now has a neutral key (name) and ground_truth.related_centres mirrors the same ORDER BY. /search results are ordered by the same neutral keys as their listings. - test_verify_23's fixture now names the first entry of the ORDER BY name list. Co-Authored-By: Claude Code --- sites/berkeley/app.py | 23 +++++++++++-------- sites/berkeley/verify/ground_truth.py | 5 ++-- sites/berkeley/verify/tests/test_verify_23.py | 4 +++- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/sites/berkeley/app.py b/sites/berkeley/app.py index cd7ee0432..572e505df 100644 --- a/sites/berkeley/app.py +++ b/sites/berkeley/app.py @@ -335,7 +335,7 @@ def index(): upcoming_events = Event.query.filter( Event.start_datetime >= BENCHMARK_NOW ).order_by(Event.start_datetime).limit(4).all() - recent_research = ResearchCenter.query.limit(4).all() + recent_research = ResearchCenter.query.order_by(ResearchCenter.name).limit(4).all() stats = { 'nobel_laureates': 12, 'top_10_programs': 50, @@ -466,7 +466,7 @@ def program_detail(slug): related = Program.query.filter( Program.college_id == program.college_id, Program.id != program.id - ).limit(4).all() + ).order_by(Program.name).limit(4).all() return render_template('program_detail.html', program=program, related=related) @@ -548,7 +548,7 @@ def research_center(slug): related = ResearchCenter.query.filter( ResearchCenter.college_id == center.college_id, ResearchCenter.id != center.id - ).limit(3).all() + ).order_by(ResearchCenter.name).limit(3).all() return render_template('research_center.html', center=center, related=related) @@ -566,7 +566,8 @@ def departments(): def department_detail(slug): dept = Department.query.filter_by(slug=slug).first_or_404() faculty_list = Faculty.query.filter_by(department_id=dept.id).order_by(Faculty.name).all() - programs = Program.query.filter_by(department_id=dept.id).all() + programs = Program.query.filter_by( + department_id=dept.id).order_by(Program.name).all() return render_template('department_detail.html', dept=dept, faculty_list=faculty_list, @@ -616,30 +617,34 @@ def search(): db.or_( Program.name.ilike(f'%{q}%'), Program.description.ilike(f'%{q}%'), - )).limit(10).all() + )).order_by(Program.name).limit(10).all() results['news'] = NewsArticle.query.filter( db.or_( NewsArticle.title.ilike(f'%{q}%'), NewsArticle.summary.ilike(f'%{q}%'), + NewsArticle.content.ilike(f'%{q}%'), NewsArticle.tags.ilike(f'%{q}%'), )).order_by(NewsArticle.published_date.desc()).limit(10).all() results['events'] = Event.query.filter( db.or_( Event.title.ilike(f'%{q}%'), Event.description.ilike(f'%{q}%'), - )).limit(10).all() + Event.location.ilike(f'%{q}%'), + Event.organizer.ilike(f'%{q}%'), + )).order_by(Event.start_datetime).limit(10).all() results['faculty'] = Faculty.query.filter( db.or_( Faculty.name.ilike(f'%{q}%'), Faculty.research_interests.ilike(f'%{q}%'), + Faculty.title.ilike(f'%{q}%'), Faculty.bio.ilike(f'%{q}%'), - )).limit(10).all() + )).order_by(Faculty.name).limit(10).all() results['research'] = ResearchCenter.query.filter( db.or_( ResearchCenter.name.ilike(f'%{q}%'), ResearchCenter.description.ilike(f'%{q}%'), ResearchCenter.focus_areas.ilike(f'%{q}%'), - )).limit(10).all() + )).order_by(ResearchCenter.name).limit(10).all() total = sum(len(v) for v in results.values()) return render_template('search.html', q=q, results=results, total=total) @@ -687,7 +692,7 @@ def faculty_profile(slug): colleagues = Faculty.query.filter( Faculty.department_id == member.department_id, Faculty.id != member.id - ).limit(5).all() + ).order_by(Faculty.name).limit(5).all() return render_template('faculty_profile.html', member=member, colleagues=colleagues) diff --git a/sites/berkeley/verify/ground_truth.py b/sites/berkeley/verify/ground_truth.py index 3725ce219..6bb62a4e3 100644 --- a/sites/berkeley/verify/ground_truth.py +++ b/sites/berkeley/verify/ground_truth.py @@ -128,10 +128,11 @@ def _faculty_of_department(connection: sqlite3.Connection, department_id: int) - def related_centres(connection: sqlite3.Connection, centre: dict[str, Any]) -> list[dict[str, Any]]: - """The centres the detail page renders: ``LIMIT 3`` with **no ORDER BY** (app.py:469-472).""" + """The centres the detail page renders: ``ORDER BY name LIMIT 3`` (app.py research_center).""" return _rows( connection, - "SELECT * FROM research_centers WHERE college_id = ? AND id != ? LIMIT 3", + "SELECT * FROM research_centers WHERE college_id = ? AND id != ? " + "ORDER BY name LIMIT 3", (centre["college_id"], centre["id"]), ) diff --git a/sites/berkeley/verify/tests/test_verify_23.py b/sites/berkeley/verify/tests/test_verify_23.py index 7286b72b5..34a207d18 100644 --- a/sites/berkeley/verify/tests/test_verify_23.py +++ b/sites/berkeley/verify/tests/test_verify_23.py @@ -12,9 +12,11 @@ GENUINE_STEPS = [step("/"), step("/research"), step("/research/bids", "done")] ANSWER = ( + # The related centre is the first of BIDS's ORDER BY name LIMIT 3 list + # (app.py research_center; ground_truth.related_centres mirrors it). "BIDS is directed by Prof. David Culler; its focus areas are Data Science, Statistics, " "Computational Methods and Open Science. A related center listed on the page is the " - "Mathematical Sciences Research Institute." + "Berkeley Center for New Media." ) From 57cb484091e8de4ce6992e79546cd449d14370d5 Mon Sep 17 00:00:00 2001 From: evanz37 <89922710+evanz37@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:12:11 -0400 Subject: [PATCH 17/25] =?UTF-8?q?fix(berkeley):=20E=20=E2=80=94=20WCAG=20A?= =?UTF-8?q?A=20contrast,=20320px=20layout,=20heading=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Appendix A §9 (4 widths x all routes, pixel-sampled contrast): - Contrast tokens: --gray-mid #6c757d -> #64696f (4.38 -> 5.17 on the off-white card ground), --light-blue #3B7EA1 -> #2E6C8B (4.48 -> 5.78 on white), .badge-green #28a745 -> #1E7E34, gold-button hover #C4820A -> #E0A50C (blue text 4.01 -> 5.85), the 404 numeral gold -> --dark-gold (1.78 -> 3.56), the hero gradient's light stop darkened and its 0.9 opacity wash removed, the card-image label made solid white, and the event/news category palettes darkened (Sports #8A5A07, Career #1E7E34, Health #0F7A5A, Arts #5A3383, Virtual #55595E; the in-badge white 0.2 wash -> black 0.3). Measured by glyph-anchored pixel sampling (scripts_dev/phase_e_contrast.py): 254 failing text elements before, 0 of 2725 after at 1440, 0 at 768/390/320. - Layout: fixed 4/2-column and 3fr/2fr grids became responsive helpers that collapse at <=768px (about, academics, admissions, department/program detail, faculty profile, research, account, home hero); long unbreakable strings get `min-width: 0` / `overflow-wrap: anywhere`; the filter-bar controls shrink; the faculty profile header wraps. 320px horizontal scroll is gone. - Heading order: footer and card headings no longer skip levels, listing pages carry an sr-only h2, detail-page section headings were re-levelled; the heading-order check is 0 jumps over 423 routes. - run_matrix/site docs: the related-centres query is ORDER BY name now (mirrored in ground_truth and TASK_REVIEW). Co-Authored-By: Claude Code --- sites/berkeley/README.md | 4 +- sites/berkeley/templates/404.html | 2 +- sites/berkeley/templates/about.html | 4 +- sites/berkeley/templates/academics.html | 2 +- sites/berkeley/templates/account.html | 8 ++-- sites/berkeley/templates/admissions.html | 6 +-- sites/berkeley/templates/base.html | 38 ++++++++++++++----- .../berkeley/templates/department_detail.html | 12 +++--- sites/berkeley/templates/event_detail.html | 2 +- sites/berkeley/templates/events.html | 5 ++- sites/berkeley/templates/faculty.html | 3 +- sites/berkeley/templates/faculty_profile.html | 6 +-- sites/berkeley/templates/index.html | 4 +- sites/berkeley/templates/news.html | 3 +- sites/berkeley/templates/news_article.html | 4 +- sites/berkeley/templates/program_detail.html | 6 +-- sites/berkeley/templates/programs.html | 1 + sites/berkeley/templates/research.html | 4 +- sites/berkeley/templates/research_center.html | 2 +- sites/berkeley/verify/README.md | 2 +- sites/berkeley/verify/TASK_REVIEW.md | 2 +- 21 files changed, 71 insertions(+), 49 deletions(-) diff --git a/sites/berkeley/README.md b/sites/berkeley/README.md index 49b4c6283..a1e410ae6 100644 --- a/sites/berkeley/README.md +++ b/sites/berkeley/README.md @@ -35,9 +35,9 @@ Benchmark accounts: `alice`, `bob`, `carol`, `dave` `@berkeley.edu`, password `t ## Routes -`/`, `/news` (search + category + pagination), `/news/`, `/academics`, `/programs` (search, college and degree filters, pagination), `/programs/`, `/events` (category + upcoming/past/today), `/events/`, `/research`, `/research/`, `/departments`, `/departments/`, `/admissions`, `/about`, `/search` (programmes / news / events / faculty / centres), `/faculty` (name, interest and department filters), `/faculty/`, `/login`, `/register`, `/logout`, `/account` (bookmarks), `/bookmark/add` (POST), `/bookmark/remove` (POST), `/_health`. +`/`, `/news` (search + category + pagination), `/news/`, `/academics`, `/programs` (search, college and degree filters, pagination), `/programs/`, `/events` (category + upcoming/past/today), `/events/`, `/research`, `/research/`, `/departments`, `/departments/`, `/admissions`, `/about`, `/search` (programmes / news / events / faculty / centres), `/faculty` (name, interest and department filters), `/faculty/`, `/login`, `/register`, `/logout` (POST-only, CSRF-protected: a prefetching GET gets 405), `/account` (bookmarks), `/bookmark/add` (POST), `/bookmark/remove` (POST), `/_health`. -Article detail, programme detail, event detail, faculty profiles and centre pages are pure reads: no GET path writes the database, so a read-only benchmark task's after-state always equals its initial snapshot. +Article detail, programme detail, event detail, faculty profiles and centre pages are pure reads: no GET path writes the database, so a read-only benchmark task's after-state always equals its initial snapshot. `sites/berkeley/tests/` holds the runnable checks: registry/seed integration, the answer-leak sweep (`test_answer_leaks.py`) and the app-robustness suite (`test_app_robustness.py`). ## Grading contract diff --git a/sites/berkeley/templates/404.html b/sites/berkeley/templates/404.html index 7840266ad..8701c3545 100644 --- a/sites/berkeley/templates/404.html +++ b/sites/berkeley/templates/404.html @@ -3,7 +3,7 @@ {% block content %}
-
404
+
404

Page Not Found

The page you're looking for doesn't exist or has been moved.

diff --git a/sites/berkeley/templates/about.html b/sites/berkeley/templates/about.html index b4404b044..ca5bf137b 100644 --- a/sites/berkeley/templates/about.html +++ b/sites/berkeley/templates/about.html @@ -18,7 +18,7 @@

Berkeley by the Numbers

-
+
{{ stats.founded }}Year Founded
{{ stats.nobel_laureates }}Nobel Laureates on Faculty
{{ "{:,}".format(stats.undergrad_count) }}Undergraduate Students
@@ -34,7 +34,7 @@

Berkeley by the Numbers

-
+

History

The University of California was founded on March 23, 1868, with the merger of the private College of California and the public Agricultural, Mining, and Mechanical Arts College. Berkeley's first class of 40 students was admitted in 1869.

diff --git a/sites/berkeley/templates/academics.html b/sites/berkeley/templates/academics.html index 80e743714..434dbdda5 100644 --- a/sites/berkeley/templates/academics.html +++ b/sites/berkeley/templates/academics.html @@ -11,7 +11,7 @@

Academics at Berkeley

-
+
{{ colleges|length }}Schools & Colleges
{{ total_programs }}+Degree Programs
115+Undergraduate Majors
diff --git a/sites/berkeley/templates/account.html b/sites/berkeley/templates/account.html index 3ac4a9b11..94d7fd27d 100644 --- a/sites/berkeley/templates/account.html +++ b/sites/berkeley/templates/account.html @@ -10,7 +10,7 @@

My Account

-
+
-

Account

+

Account

-

Quick Browse

+

Quick Browse

  • Programs
  • Events
  • diff --git a/sites/berkeley/templates/admissions.html b/sites/berkeley/templates/admissions.html index 7b4c0de68..344125f73 100644 --- a/sites/berkeley/templates/admissions.html +++ b/sites/berkeley/templates/admissions.html @@ -19,7 +19,7 @@

    Admissions

    Undergraduate Admissions

    -
    +

    UC Berkeley welcomes applications from students across the United States and around the world. We are committed to enrolling a diverse, talented class of undergraduate students who will thrive in Berkeley's academically rigorous and intellectually vibrant community.

    Berkeley admits freshmen on the basis of academic achievement, the strength and breadth of coursework, personal qualities, extracurricular activities, and demonstrated commitment to the Berkeley community.

    @@ -48,7 +48,7 @@

    Application Deadlines

    Freshman Profile (Class of 2027)

    -
    +
    3.91
    Median GPA
    @@ -80,7 +80,7 @@

    Required Materials

    Graduate Admissions

    -
    +

    UC Berkeley's Graduate Division coordinates graduate admissions across {{ grad_programs }} graduate programs offered by 14 professional schools and the College of Letters and Science. Each department or program administers its own admissions process.

    Berkeley PhD students benefit from full funding packages that include tuition, fees, and a stipend in exchange for research and teaching duties.

    diff --git a/sites/berkeley/templates/base.html b/sites/berkeley/templates/base.html index c87cc27ad..3181676f6 100644 --- a/sites/berkeley/templates/base.html +++ b/sites/berkeley/templates/base.html @@ -9,12 +9,12 @@ :root { --blue: #003262; --gold: #FDB515; - --light-blue: #3B7EA1; + --light-blue: #2E6C8B; --dark-gold: #C4820A; --white: #ffffff; --off-white: #f8f7f5; --gray-light: #e9ecef; - --gray-mid: #6c757d; + --gray-mid: #64696f; --gray-dark: #343a40; --text: #212529; --link: #0057A8; @@ -79,7 +79,7 @@ background: var(--gold); border: none; border-radius: 0 4px 4px 0; color: var(--blue); font-weight: bold; cursor: pointer; font-size: 14px; } - .header-search button:hover { background: var(--dark-gold); } + .header-search button:hover { background: #E0A50C; } /* Nav */ nav.main-nav { background: var(--blue); @@ -129,7 +129,7 @@ } .card:hover { box-shadow: 0 4px 20px rgba(0,50,98,0.12); } .card-img { height: 160px; background: var(--blue); display: flex; align-items: center; justify-content: center; } - .card-img-label { color: rgba(255,255,255,0.7); font-family: Arial, sans-serif; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; } + .card-img-label { color: #ffffff; font-family: Arial, sans-serif; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; } .card-body { padding: 20px; } .card-category { display: inline-block; padding: 3px 10px; border-radius: 3px; @@ -152,7 +152,7 @@ .btn-primary { background: var(--blue); color: var(--white); border-color: var(--blue); } .btn-primary:hover { background: #002244; color: var(--white); text-decoration: none; } .btn-gold { background: var(--gold); color: var(--blue); border-color: var(--gold); } - .btn-gold:hover { background: var(--dark-gold); color: var(--blue); text-decoration: none; } + .btn-gold:hover { background: #E0A50C; color: var(--blue); text-decoration: none; } .btn-outline { background: transparent; color: var(--blue); border-color: var(--blue); } .btn-outline:hover { background: var(--blue); color: var(--white); text-decoration: none; } .btn-sm { padding: 6px 14px; font-size: 13px; } @@ -170,6 +170,14 @@ textarea:focus-visible, [tabindex]:focus-visible { outline: 3px solid #0b5cab; outline-offset: 2px; } .top-bar a:focus-visible, header a:focus-visible, footer a:focus-visible, nav.main-nav a:focus-visible { outline-color: var(--gold); } + /* Long unbreakable strings (emails, room codes) must not widen a grid column */ + .sidebar-layout > * { min-width: 0; } + .detail-meta-item, .sidebar li { overflow-wrap: anywhere; } + /* Filter-bar controls shrink instead of forcing the document to scroll */ + .filter-bar form select, .filter-bar form input[type="text"], + .filter-bar form button { max-width: 100%; min-width: 0; } + /* Visually hidden, still announced (section labels for card grids) */ + .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* Skip link: first focusable element, off-screen until focused */ .skip-link { position: absolute; left: -9999px; top: 0; z-index: 200; background: var(--white); color: var(--blue); padding: 10px 16px; font-family: Arial, sans-serif; font-weight: 700; } .skip-link:focus { left: 0; } @@ -215,7 +223,7 @@ .badge { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 11px; font-family: Arial, sans-serif; font-weight: 700; } .badge-gold { background: var(--gold); color: var(--blue); } .badge-blue { background: var(--blue); color: var(--white); } - .badge-green { background: #28a745; color: white; } + .badge-green { background: #1E7E34; color: white; } /* Table */ table { width: 100%; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; } th { background: var(--blue); color: var(--white); padding: 12px 16px; text-align: left; } @@ -233,13 +241,23 @@ .footer-main { padding: 48px 0 32px; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 32px; } .footer-brand .footer-logo { font-size: 22px; color: var(--white); font-family: Georgia, serif; margin-bottom: 12px; } .footer-brand p { font-size: 13px; line-height: 1.7; opacity: 0.7; } - footer h4 { color: var(--gold); font-size: 13px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; } + footer h2 { color: var(--gold); font-size: 13px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; } footer ul { list-style: none; } footer ul li { margin-bottom: 8px; } footer ul li a { color: rgba(255,255,255,0.7); font-size: 13px; } footer ul li a:hover { color: var(--gold); text-decoration: none; } .footer-bottom { border-top: 1px solid rgba(255,255,255,0.15); padding: 16px 0; display: flex; justify-content: space-between; align-items: center; } .footer-bottom p { font-size: 12px; opacity: 0.6; } + /* Responsive split grids (fixed fr pairs overflow at 320px) */ + .split-3-2 { display: grid; grid-template-columns: 3fr 2fr; gap: 32px; } + .split-1-3 { display: grid; grid-template-columns: 1fr 3fr; gap: 32px; } + .split-2-3 { display: grid; grid-template-columns: 2fr 3fr; } + /* Responsive multi-column grids (inline 4/2-column grids overflow at 390px) */ + .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; } + .grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } + @media (max-width: 768px) { + .grid-4, .grid-2, .split-3-2, .split-1-3, .split-2-3 { grid-template-columns: 1fr; } + } /* Utility */ .mt-1{margin-top:8px} .mt-2{margin-top:16px} .mt-3{margin-top:24px} .mt-4{margin-top:32px} .mb-1{margin-bottom:8px} .mb-2{margin-bottom:16px} .mb-3{margin-bottom:24px} .mb-4{margin-bottom:32px} @@ -357,7 +375,7 @@

    The University of California, Berkeley is a public research university located in Berkeley, California. Founded in 1868, it is the oldest of the University of California's campuses and is regarded as one of the world's leading universities.

    -

    Academics

    +

    Academics

    -

    Research

    +

    Research

    -

    Campus

    +

    Campus

    • Events
    • Berkeley News
    • diff --git a/sites/berkeley/templates/department_detail.html b/sites/berkeley/templates/department_detail.html index cf6387592..7b9177ca7 100644 --- a/sites/berkeley/templates/department_detail.html +++ b/sites/berkeley/templates/department_detail.html @@ -25,13 +25,13 @@

      About the Department

      {% if programs %}

      Degree Programs

      -
      +
      {% for p in programs %}
      {{ p.degree_type }} -

      +

      {{ p.name }} -

      + {% if p.duration_years > 0 %}

      {{ p.duration_years|int if p.duration_years == p.duration_years|int else p.duration_years }} years

      {% endif %} @@ -42,13 +42,13 @@

      {% if faculty_list %}

      Faculty

      -
      +
      {% for member in faculty_list %}
      -

      +

      {{ member.name }} {% if member.is_emeritus %}Emeritus{% endif %} -

      +

      {{ member.title }}

      {% if member.research_interests %}

      {{ member.research_interests[:80] }}{% if member.research_interests|length > 80 %}...{% endif %}

      diff --git a/sites/berkeley/templates/event_detail.html b/sites/berkeley/templates/event_detail.html index 9dae30a3c..e04e9c792 100644 --- a/sites/berkeley/templates/event_detail.html +++ b/sites/berkeley/templates/event_detail.html @@ -34,7 +34,7 @@

      More {{ event.category }} Events

      {{ r.start_datetime.strftime('%b') }}
      -

      {{ r.title }}

      +

      {{ r.title }}

      {{ r.location }}

      diff --git a/sites/berkeley/templates/events.html b/sites/berkeley/templates/events.html index d322753dc..fd2d620a3 100644 --- a/sites/berkeley/templates/events.html +++ b/sites/berkeley/templates/events.html @@ -46,15 +46,16 @@

      Berkeley Events

      {% if events %} +

      Events

      {% for event in events %}
      -
      +
      {{ event.start_datetime.strftime('%d') }}
      {{ event.start_datetime.strftime('%b %Y') }}
      - {{ event.category }} + {{ event.category }}

      diff --git a/sites/berkeley/templates/faculty.html b/sites/berkeley/templates/faculty.html index d8ae21658..249f31f2a 100644 --- a/sites/berkeley/templates/faculty.html +++ b/sites/berkeley/templates/faculty.html @@ -32,10 +32,11 @@

      Faculty Directory

      {% if faculty_list %} +

      Faculty members

      {% for member in faculty_list %}
      -
      +
      {{ member.name[0] }}
      diff --git a/sites/berkeley/templates/faculty_profile.html b/sites/berkeley/templates/faculty_profile.html index 64d9fa0ac..0551c4d16 100644 --- a/sites/berkeley/templates/faculty_profile.html +++ b/sites/berkeley/templates/faculty_profile.html @@ -12,7 +12,7 @@