Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2287374
feat: add Versus mirror
Sun-sunshine06 May 28, 2026
c6b8c8a
fix(versus): make the generated seed byte-reproducible
jackjin1997 Sep 13, 2026
1fdf792
fix(versus): protect state-changing posts with CSRF
jackjin1997 Sep 13, 2026
4c223eb
fix(versus): add a favicon
jackjin1997 Sep 13, 2026
53372b5
review(versus): rewrite the task set and add the grading contract
jackjin1997 Sep 13, 2026
bb1c58f
test(versus): pin the defects found in review as regressions
jackjin1997 Sep 13, 2026
bdd2ae0
fix(versus): drop the future-annotations import from verify_lib
jackjin1997 Sep 13, 2026
0cbdc87
fix(versus): reject trajectories recorded against another site's origin
jackjin1997 Sep 13, 2026
ceaaba1
merge: integrate upstream main and move Versus to the port it now gets
jackjin1997 Sep 13, 2026
8519253
fix(versus): rebuild the interface against the source design, with ga…
jackjin1997 Sep 13, 2026
509a4b2
fix(versus): refuse a run that answered from a browser error page
jackjin1997 Sep 13, 2026
a889fca
merge: integrate upstream main and move Versus to the port it now gets
jackjin1997 Sep 13, 2026
7aba94b
test(versus): derive the origin instead of freezing it
jackjin1997 Sep 13, 2026
9af5490
fix(versus): bind the origin check to the run, not to today's port
jackjin1997 Sep 13, 2026
dd7d3ac
test(versus): follow the origin binding, and pin that evidence surviv…
jackjin1997 Sep 13, 2026
d4a6bf6
feat(versus): add sourced Cities and Universities catalogues, and pag…
jackjin1997 Sep 13, 2026
ac11412
feat(versus): five tasks on the sourced categories, and a visible pager
jackjin1997 Sep 14, 2026
9f0f95e
review(versus): trim to 20 tasks, dropping the two shallowest
jackjin1997 Sep 14, 2026
eadc70a
review(versus): retarget task 18 so it stops overlapping task 17
jackjin1997 Sep 14, 2026
9cee826
merge: integrate upstream main and move Versus to the port it now gets
jackjin1997 Sep 14, 2026
18e880c
fix(versus): pasting the page must not count as answering it
jackjin1997 Sep 14, 2026
99eb96d
fix(versus): replace synthetic art with sourced images
jackjin1997 Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# WebHarbor — slim, self-contained image.
# 29 Flask mirror sites + control plane on :8101.
# 30 Flask mirror sites + control plane on :8101.

FROM python:3.12-slim-bookworm

Expand Down Expand Up @@ -70,6 +70,18 @@ RUN cd /opt/WebSyn/healthline && test -f instance_seed/healthline.db && \
PYTHONHASHSEED=0 python3 migrate_seed.py && \
python3 prune_unreferenced_images.py --apply && rm -rf instance

# Versus ships source-backed entity imagery from the pinned asset bundle.
# The generic gate enforces exact coverage, hashes, source URLs and WebP headers.
RUN python3 /opt/check_asset_inventory.py /opt/WebSyn/versus
# The seed remains code-generated; the benchmark password hash is frozen so the
# SQLite output is byte-identical on every build.
RUN cd /opt/WebSyn/versus && \
rm -rf instance instance_seed && \
mkdir -p instance_seed && \
python3 -c "from app import app" && \
cp instance/versus.db instance_seed/versus.db && \
rm -rf instance __pycache__

COPY websyn_start.sh /opt/websyn_start.sh
COPY control_server.py /opt/control_server.py
COPY site_runner.py /opt/site_runner.py
Expand All @@ -92,6 +104,6 @@ os.makedirs('instance_seed', exist_ok=True); \
shutil.copy2('instance/rotten_tomatoes.db', 'instance_seed/rotten_tomatoes.db'); \
print('Rotten Tomatoes seed DB generated at build time.')" && rm -rf /opt/WebSyn/rotten_tomatoes/instance

EXPOSE 8101 40000-40028
EXPOSE 8101 40000-40029

CMD ["/opt/websyn_start.sh"]
1 change: 1 addition & 0 deletions control_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
'osu', 'rotten_tomatoes', 'compass', 'walmart_careers',
'fedex', 'webmd_doctor', 'healthline', 'kaggle',
'nvidia',
'versus',
]
BASE_PORT = 40000
WEBSYN_DIR = '/opt/WebSyn'
Expand Down
1 change: 1 addition & 0 deletions sites/versus/.requires-images
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This site requires the source-backed static/images bundle pinned in the Hugging Face asset repository.
52 changes: 52 additions & 0 deletions sites/versus/NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Third-party material in the Versus mirror

This file records how third-party media is used so a reviewer can identify every
redistributed asset, its source and how to remove it.

## Non-affiliation and trademarks

WebHarbor is an independent research benchmark for web agents. This mirror is not
affiliated with, authorized by, endorsed by or sponsored by Versus Tech or by any
manufacturer, city or university represented here. Names and marks identify the compared
entities only. No licence or permission is granted or implied by their presence.

The running site makes no request to an external service. All media is stored locally in
the pinned Hugging Face asset bundle.

## Imagery

The mirror contains 107 real, entity-matched images under
`static/images/products/`: 20 consumer-electronics products, 52 cities and 35
universities.

- 85 images come from Wikimedia Commons. `asset_inventory.json` records the exact
Commons file page, direct thumbnail URL, author and licence for each file.
- 22 images come from official manufacturer, university, campus, press, identity or
verified organization video pages. They are copyrighted by the named organizations
and are reproduced at reduced resolution solely to identify the entity in this
non-commercial research benchmark.

The source bytes are resized to a 960 × 720 WebP. Photographs use a centered 4:3 crop;
product renders and logos use a contained layout. No image is presented as a measurement
or task answer. `fetch_images.py` pins the source and output hashes, while
`scripts/check_asset_inventory.py` enforces exact coverage, hashes, HTTPS source URLs and
WebP headers during asset checks and the Docker build.

The inventory is the per-file attribution source of truth. In addition to the fields
required by the repository gate, each row records the represented entity, Wikidata QID
when applicable, source kind, source page, source file, licence/disposition, author,
source dimensions and normalized output dimensions.

## Data

Product names, brands, release years, list prices and published specifications follow
the manufacturers' figures. The **Versus Score is not versus.com's value**. It is
synthetic benchmark data, as are all accounts and saved comparisons. `/about` and the
footer state this distinction on the running site.

## Removal

To remove all third-party media, delete the 107 entries from `asset_inventory.json`,
remove `static/images/products/` from the Versus Hugging Face archive, and remove the
image elements from `_product_card.html`, `product.html` and `compare.html`. The routes,
seeded data and all 20 tasks continue to function; only the visual presentation changes.
66 changes: 66 additions & 0 deletions sites/versus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Versus mirror

Offline Flask mirror of `https://versus.com/` for the WebHarbor benchmark. In the
30-site registry it is site index 29 and runs on container port `40029`.

```bash
docker run -d --rm --name wh-versus -p 8101:8101 -p 40000-40029:40000-40029 webharbor:dev
curl -so /dev/null -w "%{http_code}\n" http://localhost:40029/
curl -X POST http://localhost:40029/reset/versus
```

## Assets and build products

The entity images are source-backed assets distributed through the pinned Hugging Face
bundle. The SQLite seed remains a deterministic build product:

| Artefact | Generator | Gate |
| --- | --- | --- |
| `static/images/products/*.webp` (107 images) | `fetch_images.py` from pinned source URLs | `scripts/check_asset_inventory.py` — exact coverage, size, SHA-256, URL and WebP header |
| `instance_seed/versus.db` | `app.py` import side effect | `md5(instance) == md5(instance_seed)` after `/reset/versus` |

The seed's benchmark password hash is a frozen
constant (`BENCHMARK_PASSWORD_HASH`) because `generate_password_hash()` draws a fresh
scrypt salt per call, which made two builds of the same commit differ.

Re-fetch the exact recorded sources and reproduce the images with Pillow 11:

```bash
uv run --python 3.12 --with pillow==11.0.0 --with requests==2.32.5 \
python fetch_images.py
python ../../scripts/check_asset_inventory.py .
```

`--refresh` rewrites pinned source/output hashes and is only for a reviewed source
change. `asset_inventory.json` records the represented entity, source page, direct asset
URL, source and output hashes, dimensions, attribution and licence/disposition.

## What is real and what is not

Product names, brands, release years, list prices and published specifications follow the
manufacturers' figures. The **Versus Score, all user accounts and all saved comparisons
are synthetic benchmark data**. The 107 entity images are real, locally stored media:
85 are Wikimedia Commons files and 22 come from official product, campus, identity,
press or video pages. `/about` and the footer state the distinction. See `NOTICE.md`.

## Catalogue

107 entities across 7 categories: 20 consumer-electronics products, 52 cities and 35
universities. The seed also carries 4 benchmark accounts sharing the password
`TestPass123!` and 3 saved comparisons for `alice.j@test.com`.

## Tasks

20 tasks in `tasks.jsonl`, each with a deterministic verifier in `verify/` and a
`judge_rubric`. Ground truth is derived from the passed `initial_db` rather than frozen
in the verifier, so the expected answer moves with the seed. Navigation checks accept
only steps on this site's own origin, with the port derived from `control_server.py`'s
registry.

Every spec value renders only on detail and comparison pages; cards and the ranking list
carry Score, Price and Year. Questions are written so the answer requires a page the list
does not carry.

```bash
python3 -m unittest discover -s tests -v
```
3 changes: 3 additions & 0 deletions sites/versus/_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Per-site health probe (optional, called by control_server)."""
def health():
return {"ok": True, "site": "versus"}
Loading