feat(adopt-a-pet): add high-fidelity mirror - #94
Conversation
|
Taking ownership of the review for this PR (Track B). Plan: build and drive the mirror per the |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AkNTAZMziDodR6uqa6QBtP
Review —
|
| Check | Result |
|---|---|
python3 -m py_compile sites/adopt_a_pet/*.py |
ok |
| Registration in 3 files | ok — adopt_a_pet is the last entry of SITES in websyn_start.sh and control_server.py (index 24 → 40024); Dockerfile says 25 Flask mirror sites, EXPOSE 8101 40000-40024 |
tasks.jsonl |
ok — 20 rows, exactly web_name,id,ques,web,upstream_url, ids AdoptAPet--0..19, web = 40024 |
Cross-site imports (grep -rn "from sites\.|import sites") |
none |
git diff --check, bash -n websyn_start.sh |
ok |
| Idempotent seeding | ok — seed_benchmark_users, seed_database, seed_user_state are each gated at function level |
| Byte-identical seed | ok — md5(instance) == md5(instance_seed) == 376b1ca1b6197540c9e01f7893096287 after boot, after driving every read-only route, after a restart without reset, and after a second reset (4/4; matches the hash in the author's audit) |
/_health |
ok — {"ok":true,"site":"adopt_a_pet"} |
| HF asset tarball layout | FAIL (blocker) — members are static/images/… and instance_seed/… with no adopt_a_pet/ root. scripts/validate_asset_archive.py adopt_a_pet.tar.gz adopt_a_pet raises ValueError: unexpected site root in archive: 'static/images', so fetch_assets.sh refuses the archive and a fresh-clone build dies at the Dockerfile's test -n "$(ls -A /opt/WebSyn/adopt_a_pet/static/images)". Every other site's tarball (e.g. walmart_careers.tar.gz) is rooted at <site>/. |
| AppleDouble junk in tarball | 25 ._* members (LIBARCHIVE.xattr.com.apple.provenance); the scripts skip them, but pack with COPYFILE_DISABLE=1 |
.assets-revision |
unchanged at 65c479f8…, which does not contain adopt_a_pet.tar.gz (checked the HF tree: 27 archives, none for this site). HF #67 is still open. Repin after merge. |
requirements.txt |
FAIL — contains only Flask; python -c "import app" → ModuleNotFoundError: No module named 'flask_sqlalchemy'. The Docker image masks this (it installs Flask-SQLAlchemy globally); standalone runs and the websyn_start.sh-style run I did cannot start without it. Fixed in the Review PR (pinned to the image's versions). |
| Docs / tests sweep | FAIL — the PR description says the site is "registered in … project documentation", but README / AGENTS / CONTRIBUTING / CLAUDE / agent_demo/README are untouched, and sites/walmart_careers/tests/test_integration.py ("24 Flask mirror sites", EXPOSE 8101 40000-40023, literal EXPECTED list) and sites/rotten_tomatoes/tests/test_environment_quality.py ('… compass walmart_careers)', 40000-40023) go red on this branch. Fixed in the Review PR (same sweep PR #95 did; the doc-range test now derives the range from control_server.SITES). |
| Port collision | note — PR #95 (webmd_doctor) also claims index 24 / port 40024; whichever merges second must be re-indexed. Ports are provisional. |
Docker (from review/pr-94-adopt_a_pet) |
ok, with a caveat: the bundle had to be extracted by hand into sites/adopt_a_pet/ (tar xzf … --exclude='._*') because validate_asset_archive.py rejects its layout. Then: docker build ok (4.69 GB); alt ports 44000-44024: 25/25 return 200; /health ok, 25/25 alive+ready, adopt_a_pet on internal 40024; the image regenerates the seed at build time, so its seed md5 is 993d6113… (not the bundled 376b1ca1… — the generated file is not byte-identical to the bundled one; worth freezing as rotten_tomatoes does); POST /reset/adopt_a_pet → 993d6113… == the image's seed; GET+POST /login then reset → still 993d6113…; docker restart → still 993d6113…; POST /reset-all ok, 25/25 ready in 1.46 s; verifier no-op matrix in the container (--container, DBs via docker cp): 20/20 FAIL, 0 errors; registry tests 4 passed; sites/adopt_a_pet suite 272 passed |
2. Visual fidelity — PASS on layout, FAIL on photo/listing consistency
- Layout vs.
adoptapet.com(side by side, 1280 px): faithful. Same top nav (Find a pet · Find a shelter · Rehome a pet · Breed 101 · Pet advice · Shop · Help · Login · Sign Up), same turquoise hero with "Ready to adopt a pet?", the pill Location / Search-for controls, "Add Pet Criteria" + "Search", the "Try our new AI SmartSearch BETA" line, the three overlapping blue/aqua/yellow showcase cards with the round-cornered photo and "Male, Adult / Phoenix, AZ" caption, the partner strip, "Hi, we're Adopt a Pet", and the footer. Pet cards and the profile page (dark photo panel + aqua fact card with thedltable) match the brand feel. (upstream_home_viewport.pngvsdesktop_home.png) - Responsive: 390 px is clean on home / search / profile / login (
mobile390_home.png); 768 px/searchoverflows horizontally (scrollWidth 827 > clientWidth 768) while the other pages fit (tablet768_search.png). Nit. - Images: 0 broken images on 16 audited pages; 20 distinct files by md5. But the photos contradict the listings (
desktop_pet_archie.png,desktop_pet_olive.png,task5_pet_daisy.png,task13_pet_ruby.png,task17_pet_teddy.png,task14_pet_milo.png,task0_results.png):- species mismatch — Sirius (Chihuahua/Terrier dog) → a cat on a bed; Arno (German Shepherd Dog) → a black cat; Casper (Colorpoint Shorthair cat) → a black dog; Cinders (Domestic Shorthair cat) → a shepherd-mix dog in a crate; Archie (Australian Shepherd) → a grey cat held by a woman; Pepper (cat) → a girl with a black-and-white dog;
- breed/colour mismatch — Daisy "Golden Retriever, Golden" → tan pit-bull type; Ruby "Boxer, Fawn" → grey Weimaraner-type; Luna "Beagle, Tricolor" → white/grey dog on a sofa; Milo "Maine Coon, Orange" → orange-and-white shorthair; Teddy "Poodle, White, Small" and Olive "Siamese, Seal Point" both show the same photo (
rehome.avif/rehome-mobile.avif, a brown shepherd mix plus an orange tabby); - the six
article-*.aviffiles double as the/blogarticle banners ("Bringing home your newly adopted dog" is Daisy's photo,task18_blog.png), andpet-10/11/12.avif(Winston, Yuki, Zorro) are 208 px thumbnails upscaled to a 480 px hero (visibly soft,task2_pet_winston.png).
This is pitfall Fix user-visible mirror URL leaks #7 (cross-field inconsistency): a vision-driven agent asked to "find the kitten" sees cats on dog cards and vice versa. Grading is unaffected (all verifiers read text facts), but "high-fidelity" needs species/breed-consistent photos — either re-harvest per-pet photos or edit the 20 listings to describe what the photos show.
- Descriptions: all 20 "About" texts are one template (
"<Name> is an affectionate <age group> <species> who enjoys companionship, gentle play, and a comfortable place to relax."). Realism nit; nothing grades on it. - Dead chrome (parity nits): "Add Pet Criteria" (
type=button, no handler), "Try our new AI SmartSearch" (plain text), the hamburger and the card hearts are<span>s, "Sort by: Nearest" is static, Distance has a single option, Shop and Help both link to/blog, Rehome a pet links to/#rehomewhich has no anchor. - Pagination label bug: page 2 of the Arizona results reads "Showing 1 - 12 of 12 available pets" (
search.htmlcomputes the start as1 if total else 0instead of(page-1)*6+1).
3. Functional depth — PASS
Driven with Playwright (audit_visual_functional.py, results in visual_functional.json):
| Flow | Result |
|---|---|
Login alice.j@test.com / TestPass123! |
→ /account, "Hi, Alice Johnson"; wrong password → flash "Email or password is incorrect." |
| Favorite add / remove | Sirius: button "♡ Favorite" → "Remove favorite" after reload; account lists ['Sirius', 'Luna']; remove → ['Luna'] (func_account_after_fav_sirius.png) |
| Adoption inquiry | empty submit blocked client-side (phone, experience :invalid); 5-char experience blocked (minlength=15); valid submit → "Your inquiry was sent!" → account row Waymo · Submitted · 602-555-0100; second submit → "You already applied for this pet." (server-side) |
| New Pet Alert | Cat / Siamese / 33130 / 50 → account row Cat · Siamese · 33130 within 50 miles |
| Logout / auth gate | logout → /, Login link back; anonymous /account → 302 /login?next=/account; /apply/*, /alerts also gated; GET /favorite/<slug> → 405 |
| Register | duplicate e-mail → "An account already exists for that email."; short password blocked client-side (minlength=8, also server-side <8); new user → /account "Hi, Test Person", re-login works, row persisted |
| Search | token-overlap on "<city> <state> <postal>": Phoenix, AZ → 12 (all of AZ), Phoenix → 2, 85004 → 1, Arizona/AZ → 12, New York/NY → 2, Seattle → 2, Los Angeles, CA / Boston Celtic → 0 with the "No exact matches" empty state; sidebar filters preserve the location; 6 cards per page with Next/Previous |
| Find a shelter | Seattle/seattle/WA → Pacific Animal Haven; AZ → both AZ rescues; Boston → "No shelters matched your search." |
| 404s | /pet/nope, /shelter/99, /nope → 404 |
| Header/footer links | 8/8 → 200 |
Server-side validation is thin (only required/minlength HTML attributes on the inquiry form; the register route does check password length and duplicates). Not a blocker.
4. Task quality — PASS after 3 rewordings (all 20 now gradable)
Catalog: 20 pets (12 AZ, 2 NY, 2 WA, 2 TX, 2 FL), 6 shelters, 4 users, 1 seeded favorite (alice → luna). Rendered values were cross-checked against the seed DB for every pet a task touches — no page-vs-DB drift. List pages never show fee / age in months / colour / house-trained / good-with-* (checked programmatically for all read tasks), so every fact task needs the profile page. All answers are synthetic (fees, months, phone numbers, .test e-mails), so none is recallable from an LLM's prior knowledge; the seed is frozen, so every answer is stable.
| id | solvable via UI | leak on list page | distractors (results / full matches) | driven steps | verdict | note |
|---|---|---|---|---|---|---|
| 0 | yes | breed + age group on the card (2 of 4 facts); size and fee hidden | 8 results, 2 pages / 1 target | 6 | accept | |
| 1 | ill-posed as written — "Scottsdale, AZ" returns 4 cats with two kittens (Neo, Scottsdale; Amba, Arizona City) | none | 4 / 2 kittens | 6 | accept, reworded → "the kitten whose listing is in Scottsdale itself" (Neo) | |
| 2 | answer depended on the query string — Phoenix, AZ → 7 male dogs, lowest fee Batman $165; Phoenix → 2 dogs, lowest Horus $210 |
fees hidden | 7 results, 2 pages / 7 candidates | 21 | accept, reworded → quotes the location and says the site matches state-wide, "check every results page" | hardest read task: pagination + 7 profiles |
| 3 | yes | hidden | 2 results / 2 full matches (distractor-thin) | 10 | accept | both profiles needed to compare fees |
| 4 | yes | rescue name on profile, phone/e-mail only on /shelter/1 |
1 with breed filter | 9 | accept | |
| 5 | yes | hidden | 2 / 2 | 7 | accept | comparison |
| 6 | yes (stateful) | — | — | 12 | accept | |
| 7 | yes (stateful) | — | — | 8 | accept | only remove control is on /pet/luna |
| 8 | yes (stateful) | — | — | 16 | accept | |
| 9 | yes (stateful) | — | — | 11 | accept | |
| 10 | yes | shelter card shows "2 adoptable pets" (count, not the answer) | 1 shelter | 6 | accept | |
| 11 | yes | name/sex/city on card; fee hidden | 1 result | 5 | accept | single result |
| 12 | yes | cards show age group only | 5 results / 3 Adults must be opened | 14 | accept | good comparison |
| 13 | yes | hidden | 1 result (Austin has 2 dogs: Archie M/Young, Ruby F/Adult) | 9 | accept | single result |
| 14 | yes | hidden | 2 / 2 | 7 | accept | comparison |
| 15 | yes (stateful) | — | — | 14 | accept | |
| 16 | yes (stateful, register) | — | — | 13 | accept | |
| 17 | yes | hidden | 1 result | 8 | accept | single result |
| 18 | yes | titles are the answer, but page-specific | — | 3 | accept | easy, 1 click |
| 19 | vacuous as written — "young dogs near Arizona" returns exactly one dog (Horus), so "among those good with children, lowest fee" compares nothing | hidden | 1 / 1 | 21 | accept, re-anchored → all Arizona dogs, good with children, lowest fee (Arno, Casa Grande, German Shepherd Dog / Mixed Breed, 43 months, $200): 8 results on 2 pages, 4 qualify, every profile needed | now the second hard task |
Difficulty bar: ≥5 actions on 12 tasks (2, 3, 6, 8, 9, 12, 15, 16, 19 …), multi-profile comparison on 2, 3, 5, 12, 14, 19, and two tasks (2, 19) that need pagination plus 7–8 hidden-fact profiles. Weak spots for the author: the six single-result tasks (4, 11, 13, 17, plus 3's two-of-two) reflect the 20-pet catalog — every metro outside Arizona has exactly two pets, so any species/age filter there yields one card. More pets per metro would give real distractors.
5. Required fixes before approval
- Repack the HF tarball so every member is under
adopt_a_pet/(adopt_a_pet/static/images/…,adopt_a_pet/instance_seed/adopt_a_pet.db) and without AppleDouble members (COPYFILE_DISABLE=1 tar czf adopt_a_pet.tar.gz adopt_a_pet/static/images adopt_a_pet/instance_seedfromsites/); push to HF Add GOV.UK mirror + task verifiers (site by @lamawmouk, verifiers by reviewer) #67, then pin.assets-revisionto the merged SHA.scripts/validate_asset_archive.py adopt_a_pet.tar.gz adopt_a_petmust printvalidated N managed members. (scripts/validate_asset_archive.py:23, blocker) - Make the photos match the listings (
sites/adopt_a_pet/app.py:23-25,PETS[...]['image']): Sirius, Arno, Casper, Cinders, Archie, Pepper show the wrong species; Daisy, Ruby, Luna, Milo, Teddy, Olive the wrong breed/colour; Teddy and Olive share one photo; Winston/Yuki/Zorro use 208 px thumbnails. Re-harvest one real photo per pet or rewrite the affected rows (species/breed/colour) to describe the captured photos, and stop reusing the/blogbanners as pet photos. sites/adopt_a_pet/requirements.txt:Flaskalone cannot importapp.py— fixed in the Review PR (Flask==3.1.0,Flask-SQLAlchemy==3.1.1,Werkzeug==3.1.3,SQLAlchemy==2.0.36).- Docs/tests bump to 25 sites /
40000-40024— fixed in the Review PR (README, AGENTS, CONTRIBUTING, CLAUDE,agent_demo/README,sites/walmart_careers/README.md,sites/walmart_careers/tests/test_integration.py,sites/rotten_tomatoes/tests/test_environment_quality.py). Please keep them in sync if the index changes at merge. - Task wording — applied in the Review PR (
sites/adopt_a_pet/tasks.jsonlrows 1, 2, 19, see §4); shout if you prefer different phrasing, the verifiers gate on the same facts either way. - Nits (not blocking):
templates/search.html"Showing 1 - N" start index on page ≥2; 768 px overflow on/search;/#rehomeanchor; Shop/Help →/blog; single-option Distance select; template descriptions.
6. Grading contract (reviewer deliverable, in the Review: PR)
sites/adopt_a_pet/verify/verify_0.py … verify_19.py + verify_lib.py + ground_truth.py + README.md; every tasks.jsonl row now carries verifier_path and an English judge_rubric (rules and fact-checkpoints, no answers; there is no answer key). Deterministic only — the llm_* helpers exist for API parity with merriam_webster and short-circuit under --no_llm True; no verdict depends on a key.
Per run the verifiers check, in order: package (exact task id, non-empty final_answer, agent_done, ≥1 step, every URL on the same loopback host and port as start_url, every step's two screenshots decode as PNG) → snapshot contract (both initial.db/after.db required — explicit flags, <run_dir>/, or docker cp from $WH_CONTAINER; six tables with the model column order; initial counts 4/6/20/1/0/0; pet/shelter/user rows must equal the frozen catalog in ground_truth.py, so any catalog edit fails closed instead of grading against stale truth; pet/shelter immutable) → navigation gates (results URL with the task's location tokens and exact filter params, page=2 where the candidate set spans two pages, every profile whose hidden value a comparison needs, /shelter/<id>, /breeds, /blog; stateful tasks enforce order /login → action page → /account and the typed login e-mail) → answer (negation-aware matchers for names, both breeds, $fee, N months, phone with any separators, e-mail, yes/no statements for good-with-cats/children/house-trained; comparison tasks must attach the winner to "lowest/cheaper/youngest" — a candidate list without a pick fails) → state (read-only tasks: user/favorite/application/pet_alert row-identical; stateful tasks: exact row deltas — T6 +{alice→sirius} with luna kept, T7 −{alice→luna}, T8 one application row with the exact phone/housing/experience/Submitted, T9 one alert row Cat/Siamese/33130/50, T15 +{david→archie, david→ruby}, T16 one new user whose scrypt hash verifies PetFriend123! plus +{jamie→olive}; collateral writes, duplicates, wrong users and stale-state no-ops all fail).
7. Validation of the grading (CONTRIBUTING §C)
- Real runs: a Playwright driver (
drive_tasks.py) solved each task through the UI as an agent would, recordingtrajectory.jsonin theagent.pyshape (URL before every action,inputparams,go_back, before/after PNGs) plusinitial.db(seed) andafter.db(live instance), with a reset before every task; variants were derived from each genuine run.
| task | genuine drive | no-op (home only, empty answer, clean DB) | shortcut (correct answer, no navigation) | wrong answer | state mismatch (agent claims success, DB = seed) |
|---|---|---|---|---|---|
| 0–5, 10–14, 17–19 (read) | PASS ×14 | FAIL ×14 (final_answer_nonempty) |
FAIL ×14 (navigation gate) | FAIL ×14 (answer_*) |
— |
| 6, 7, 8, 9, 15, 16 (stateful) | PASS ×6 | FAIL ×6 | FAIL ×6 (visited_login_page / registered_through_form) |
FAIL ×6 (answer_confirms_*) |
FAIL ×6 (favorites_added_exactly, favorites_removed_exactly, exactly_one_application_added, exactly_one_alert_added, exactly_one_user_added) |
20/20 genuine PASS, 66/66 negatives FAIL, unexpected: none (verify_matrix_final.log). Through the real entry point, agent_demo/eval_judge.py --run_dir … --verifier True returns pass: true on the genuine T0 run and pass: false, reason: visited_dog_search_near_phoenix on its shortcut variant.
- Unit harness (
sites/adopt_a_pet/verify/tests/, dockerignored,uv run python -m unittest discover -s sites/adopt_a_pet/verify/tests): 272 tests, OK — per task: genuine PASS, run-dir snapshot discovery, no-op, wrong task id, off-origin URL, corrupt PNG, missing snapshots →database_unavailable(fail closed), catalog drift →snapshot_contract_invalid(fail closed), shortcut without each gate page, wrong / misattributed answers, read-only writes; stateful: state mismatch, collateral rows, wrong user, duplicates, reordered workflow, wrong e-mail/name/password; plus matcher tests (money, months, phone formats, 14 yes/no phrasings, 10 comparison phrasings incl. "X is cheaper than Y" and "Y is more expensive than X"). - Verifier bug found by the real runs: none in the verifiers themselves; the first driver pass left six comparison tasks red because my driver navigated back with an unrecorded
page.go_back()(so the intermediate profiles never appeared as step URLs).agent.pyrecords the URL of the page the agent is on before every action includinggo_back, so real agents are covered; the driver was fixed and all six pass.
8. Evidence (commands and outputs, standalone on Linux, alt port 45003)
$ python3 scripts/validate_asset_archive.py ../tarball/adopt_a_pet.tar.gz adopt_a_pet
ValueError: unexpected site root in archive: 'static/images'
$ tar tzf adopt_a_pet.tar.gz | head -3 ; tar tzf adopt_a_pet.tar.gz | grep -c '\._'
static/._images
static/images/
static/images/._pet-10.avif
25
$ tar tzf walmart_careers.tar.gz | head -1 # reference layout
walmart_careers/static/images/area-corporate-2.jpg
$ uv pip install -r sites/adopt_a_pet/requirements.txt && python -c "import app"
ModuleNotFoundError: No module named 'flask_sqlalchemy'
$ md5sum instance/adopt_a_pet.db instance_seed/adopt_a_pet.db # boot 1 / read-only drive / restart w/o reset / boot 3
376b1ca1b6197540c9e01f7893096287 instance/adopt_a_pet.db
376b1ca1b6197540c9e01f7893096287 instance_seed/adopt_a_pet.db (identical all four times)
$ curl -s http://127.0.0.1:45003/_health
{"ok":true,"site":"adopt_a_pet"}
$ for p in / /pet/waymo /shelters /shelter/1 /breeds /blog /login /register /account /apply/waymo /alerts /favorite/waymo /nope; do curl -so /dev/null -w "$p -> %{http_code}\n" http://127.0.0.1:45003$p; done
/ -> 200 /pet/waymo -> 200 /shelters -> 200 /shelter/1 -> 200 /breeds -> 200 /blog -> 200 /login -> 200 /register -> 200
/account -> 302 /apply/waymo -> 302 /alerts -> 302 /favorite/waymo -> 405 /nope -> 404
$ grep -n "adopt_a_pet\|40000-4002" websyn_start.sh control_server.py Dockerfile
websyn_start.sh:7: ... compass walmart_careers adopt_a_pet)
control_server.py:29: ... 'compass', 'walmart_careers', 'adopt_a_pet',
Dockerfile:84:EXPOSE 8101 40000-40024
[responsive] tablet768 search: scrollWidth=827 clientWidth=768 overflow=True
[search] loc='Phoenix, AZ' species='Dog' -> 'Showing 1 - 6 of 8 available pets' cards_p1=6 next=1
[search] loc='Phoenix' species='' -> 'Showing 1 - 2 of 2 available pets'
[search] Arizona page2: 'Showing 1 - 12 of 12 available pets'
[task 1] 'Scottsdale, AZ'/'Cat' -> cards=['Neo', 'Amba', 'Casper', 'Cinders'] (two kittens)
[task 19 as written] 'Arizona'/'Dog'/{'age': 'Young'} -> 'Showing 1 - 1 of 1 available pets' cards=['Horus']
[leak] task 2: fee/months/color absent from both results pages for all 7 candidates (same for every read task)
$ uv run python -m unittest discover -s sites/adopt_a_pet/verify/tests -p 'test_*.py'
Ran 272 tests in 47.517s
OK
$ uv run python drive_tasks.py ... --skip_drive | tail -3
19 | PASS | FAIL | FAIL | FAIL | -
unexpected: none
$ uv run python eval_judge.py --run_dir runs/judge_smoke --verifier True
pass: True success: True reason: all checks passed
$ uv run python eval_judge.py --run_dir runs/judge_smoke_shortcut --verifier True
pass: False success: False reason: visited_dog_search_near_phoenix
Screenshots (Playwright, server)
upstream_home_viewport.png · desktop_home.png · mobile390_home.png · tablet768_search.png · task0_results.png (Phoenix, AZ dogs — Sirius and Arno cards show cats) · task2_results.png · task12_results.png · task10_shelters.png · task11_breeds.png · task18_blog.png · desktop_pet_archie.png (dog listing, cat photo) · desktop_pet_olive.png / task17_pet_teddy.png (same photo) · task5_pet_daisy.png · task13_pet_ruby.png · task14_pet_milo.png · task2_pet_winston.png (208 px thumbnail) · func_account_after_fav_sirius.png · func_account_after_alert.png · func_apply_done.png · func_register_ok.png
All cited screenshots: https://github.com/JeremyJC67/WebHarbor/tree/pr-screenshots/reviews/adopt_a_pet
| mirror homepage 1280 px | real adoptapet.com homepage (captured from the review host) |
|---|---|
![]() |
![]() |
| Phoenix, AZ dog results — Sirius and Arno cards show cats | Archie: Australian Shepherd listing, cat photo |
|---|---|
![]() |
![]() |
Second-pass audit —
|
| # | Sev | Area | Finding | Status |
|---|---|---|---|---|
| B1 | blocker | robustness | SECRET_KEY='adopt-a-pet-benchmark' (app.py:8): a cookie signed with that literal logs in as any user, no password |
fixed |
| B2 | blocker | robustness | No CSRF protection on any of the six POST forms; a cross-origin POST created a favourite | fixed |
| M1 | major | robustness | Open redirect on ?next= and on the favourite route's Referer |
fixed |
| M2 | major | robustness | int(request.form['radius']) (app.py:84): HTTP 500 on non-numeric input and on 2**70 |
fixed |
| M3 | major | robustness | Inquiry, alert and registration forms accepted empty, out-of-vocabulary and unbounded input silently | fixed |
| M4 | major | search | "Previous" pagination link emitted a duplicate page parameter and re-served page 2 |
fixed |
| M5 | major | a11y | Horizontal overflow: /search at 768 and 320 px, /shelters at 390 and 320 px |
fixed |
| M6 | major | a11y | Body-text contrast 2.85:1 and 2.32:1, below the WCAG AA 4.5:1 floor | fixed |
| m1 | minor | robustness | No MAX_CONTENT_LENGTH; 400 KB form bodies were accepted and stored |
fixed |
| m2 | minor | robustness | PRAGMA foreign_keys off |
fixed |
| m3 | minor | robustness | No 404/500 handlers or templates (bare Werkzeug page) | fixed |
| m4 | minor | search | Missing ORDER BY on the homepage and the shelter list |
fixed |
| m5 | minor | tasks | Task 4 described a search path that does not exist (our task text, not your code) | fixed |
| m6 | minor | prefill | Hero placeholder was Phoenix, AZ — the exact location tasks 0 and 2 tell the agent to type |
fixed |
| m7 | minor | a11y | Stylesheet defined no focus styles at all | fixed |
| m8 | minor | robustness | Session user_id trusted: a forged list or bool logged in as Alice, a forged dict returned 500 |
fixed |
| N1 | nit | chrome | Shop and Help both linked to /blog; /#rehome had no anchor; single-option Distance select |
fixed |
| N2 | nit | assets | .dockerignore ships sites/*/verify/ground_truth.py — the full answer key — into the image |
report only, repo-wide |
Already filed in the first pass and not re-reported: the malformed HF tarball, the mismatched pet photographs, and the .assets-revision pin.
2. What we changed in your code
The site owner authorised fixing defects directly rather than only reporting them, so these landed as separate reviewer commits on top of your 8e11e7d, which is untouched. Please review them as you would any patch — everything here is yours to accept, amend or reject.
| Commit | File | Change |
|---|---|---|
d6b4080 |
app.py:8 |
SECRET_KEY from ADOPT_A_PET_SECRET_KEY or secrets.token_hex(32), matching compass / osu / target / ted / rotten_tomatoes / walmart_careers |
d6b4080 |
app.py:8 |
added MAX_CONTENT_LENGTH=256*1024 (same value walmart_careers asserts) and WTF_CSRF_TIME_LIMIT=None |
d6b4080 |
app.py |
CSRFProtect(app); a PRAGMA foreign_keys=ON connect listener |
d6b4080 |
app.py:42 |
user() refuses a non-int session user_id instead of passing it to the ORM |
d6b4080 |
app.py |
new helpers bounded_text, one_of, bounded_int, internal_path, safe_referrer, and HOUSING_OPTIONS / SPECIES_OPTIONS / RADIUS_OPTIONS |
d6b4080 |
app.py:71 |
favourite redirect goes through safe_referrer() |
d6b4080 |
app.py:79 |
inquiry form validated server-side; invalid input re-renders the form with an error and HTTP 400 |
d6b4080 |
app.py:84 |
alert form validated server-side, including a five-digit postal check and a radius restricted to the offered set |
d6b4080 |
app.py:101 |
?next= restricted to same-site relative paths |
d6b4080 |
app.py:110 |
registration validated: bounded name and email, email format by regex, password upper bound |
d6b4080 |
app.py:54, :92 |
explicit ORDER BY: Pet.id on the homepage, Shelter.state, city, name on the shelter list |
d6b4080 |
app.py |
400 / 404 / 413 / 500 error handlers |
d6b4080 |
templates/{pet,account,login,register,apply,alerts}.html |
csrf_token() hidden input; maxlength attributes; error blocks on the three validated forms |
d6b4080 |
templates/{400,404,500}.html |
new, extending base.html |
d6b4080 |
requirements.txt |
+ Flask-WTF==1.2.2 (already in the image at Dockerfile:15, so no new image dependency) |
1a9a6e5 |
templates/search.html |
pagination hrefs built from a base query string with page stripped; start index (page-1)*per_page+1; Distance select disabled with an explanatory title |
1a9a6e5 |
templates/base.html |
Shop and Help are inert labels rather than links to /blog |
1a9a6e5 |
templates/index.html |
placeholder City, State or ZIP; a real #rehome section so the nav anchor resolves |
a84eacd |
static/css/site.css |
label colours to #5f5f5f; min-width:0 / minmax(0,1fr) on filter and shelter-search controls; explicit :focus-visible ring; .nav-inert and .form-error |
faa6dcb |
tasks.jsonl row 4 |
our task text, reworded (see section 6) |
Two judgement calls worth flagging, because you may disagree:
- The Distance filter. Your backend has no geo data and ignores the
distanceparameter, so adding more options would create choices nothing honours. I made the controldisabledwithtitle="This mirror does not filter by distance"rather than deleting it, to keep the upstream layout. Say the word if you would rather it went entirely. - Shop and Help. Both pointed at
/blog. Inventing a shop or a help centre is out of scope, so they are now inert labels titled "Not available in this mirror". Same offer.
I also made one change and then reverted it: I briefly added phone and email to the shelter search field set, to close the field-set mismatch between the two search routes. Your token-overlap scorer made that worse — every seeded phone number shares the token 555, so ?q=602-555-0141 returned all six shelters. Reverted to name city state.
3. The two blockers, with their exploits
$ python probes.py # sign {"user_id": 2} with the literal key from app.py:8, GET /account
[6.1 forged-cookie /account] status=200 | greeting='Bob Smith'
$ python probes2.py # POST with Origin and Referer set to http://evil.example, no token
[forms carry a csrf token?] False
[6.3 cross-origin POST /favorite/waymo] status=302 | location=http://evil.example/attack | favorites=[(1, 1, 10), (2, 1, 2)]
Row (2, 1, 2) is a favourite an attacker's page created in a logged-in user's account. After the fixes, with the same probes:
[forged cookie (old key) -> /account] status=302 | loc=/login?next=/account | greeting=none
[cross-origin POST /favorite/waymo WITHOUT token] status=400 | favorites=[(1, 1, 10)]
[POST WITH token but evil Referer] status=302 | redirect=/pet/waymo | favorites=[(1, 1, 10), (2, 1, 2)]
The third line is the control: a legitimate POST still works, so the check is testing the token rather than a broken route.
4. The pagination bug, because it sits on two task paths
Your hrefs were ?{{filters|urlencode}}&page={{page-1}}. On page 2, filters already contains page=2, so the link emits ?location=Arizona&page=2&page=1 — and Werkzeug's request.args.get returns the first value.
$ curl -s "$B/search?location=Arizona&page=2" | grep -o '<nav class="pages">.*</nav>'
<nav class="pages"><a href="?location=Arizona&page=2&page=1">Previous</a></nav>
$ curl -s "$B/search?location=Arizona&page=2&page=1" | grep -o "<h3>[^<]*</h3>"
<h3>Neo</h3> <h3>Sirius</h3> <h3>Waymo</h3> <h3>Winston</h3> <h3>Yuki</h3> <h3>Zorro</h3> # page 2 again
$ curl -s "$B/search?location=Arizona&page=1" | grep -o "<h3>[^<]*</h3>"
<h3>Amba</h3> <h3>Arno</h3> <h3>Batman</h3> <h3>Casper</h3> <h3>Cinders</h3> <h3>Horus</h3> # the real page 1
Tasks 2 and 19 both require results page 2, so an agent that pages forward and then back was stuck. Fixed, and the same template line carried the "Showing 1 - N" start-index nit from the first pass, so that is fixed too:
[pagination /search?location=Arizona&page=2] meta=Showing 7 - 12 of 12 | links=['location=Arizona&page=1']
[following Previous from page 2] Showing 1 - 6 of 12 | ['Amba','Arno','Batman','Casper','Cinders','Horus']
5. Seed provenance — the build-time hash divergence is benign
The first pass noted that the image regenerates the seed at build time and gets md5 993d6113…, while the bundled file is 376b1ca1…. That looked like it might mean stale or divergent data. It does not. I regenerated the seed from your app.py under two SQLite runtimes and compared bytes against rows against the ground truth the verifiers actually derive:
$ python seed_digest.py instance_seed/adopt_a_pet.db <regen sqlite 3.45.1> <regen sqlite 3.50.4>
md5_bytes DIFFER ['376b1ca1b6197540c9e01f7893096287',
'f4dec7fc30ccd7ecd0af086c1fe86e65',
'702af8b3ecdde78806d920578e67b4ad']
size SAME [57344, 57344, 57344]
row_digest_sha256 SAME ['22d8f8a9be0a3305bee062e5560e5fbbb962928d7d1a736c30ad121e9b94ed14', ...]
schema_digest_sha256 SAME ['4990797c7e8db72d69784c1c31692e4dff4a6debb932f60c81ccd7d2f7a4481b', ...]
sqlite_version_that_wrote DIFFER [3040001, 3045001, 3050004]
page_size / freelist_pages / file_change_counter SAME
ground_truth_drift NONE for all
Three different byte images, one identical dataset — the row digest covers all six tables (with password_hash excluded, since it is salted and random per generation), and the drift check is the same comparison verify_lib.catalog_drift runs. So: seed byte-reproducibility does not survive a change of SQLite writer version, but the rows and every value the verifiers grade against do. The container's 993d6113 is a different writer version, not different data.
The reset invariant is unaffected, because /reset copies instance_seed rather than regenerating it:
$ md5sum instance/adopt_a_pet.db instance_seed/adopt_a_pet.db
376b1ca1b6197540c9e01f7893096287 instance/adopt_a_pet.db
376b1ca1b6197540c9e01f7893096287 instance_seed/adopt_a_pet.db
The one thing worth doing anyway is freezing the seed the way rotten_tomatoes does, so the shipped bundle and the built image agree byte-for-byte. That is a nice-to-have, not a blocker.
6. Task 4 — our text, not your code
Task 4 said "Find Arno through the pet search". Your location box scores tokens against city state postal only, never the pet name or breed, which is the right design for a box labelled Location — but it makes the task text wrong:
[4 search] location=Arno | 0 results
[4 search] location=German+Shepherd | 0 results
[4 search] location=Casa+Grande&species=Dog | 1 result | ['Arno']
Reworded to name the location that reaches him and to say explicitly that the box matches location rather than names. The verifier's gates are unchanged (a /search visit, /pet/arno, /shelter/1), and task 4 still drives PASS with its shortcut variant failing on used_pet_search.
7. .dockerignore ships the answer key — repo-wide, not yours
Flagging this for the maintainer rather than for you, because it is not an adopt_a_pet defect and I did not change it.
$ grep -n "verify" .dockerignore
14:sites/*/verify/tests/
$ grep -n "^COPY" Dockerfile
30:COPY sites/ /opt/WebSyn/
$ ls -d sites/*/verify | wc -l
10
.dockerignore excludes sites/*/verify/tests/ but not sites/*/verify/, so ground_truth.py — every adoption fee, exact age in months, colour and shelter phone number — is copied into the image at /opt/WebSyn/adopt_a_pet/verify/ground_truth.py. It is not reachable over HTTP (nothing under static/, no user-controlled file route), and all ten sites that ship a verify/ directory behave the same way, so this is a repo-wide decision about whether answer keys belong in a published image. Worth a maintainer ruling.
For contrast: review-reports/ (2.3 MB, including its assets/) does not ship, because the Dockerfile copies only sites/ and one script.
8. Regression gate, and what is still outstanding
Re-run on the final tree after all four commits:
| Check | Result |
|---|---|
| Task matrix (Playwright drive, all 20 tasks, four variants each) | ok — 20/20 genuine PASS, 66/66 negatives FAIL, unexpected: none |
| Verifier unit harness | ok — Ran 272 tests ... OK |
| Responsive, 4 widths x 15 routes | ok — 60/60 cells clean (was 50/60) |
| Contrast (1440 px, pixel-sampled) | ok — 0 failures (was 4) |
| Answer-leak scan, 20 surfaces | ok — 0 leaks, 36/36 injected leaks detected by the mutation self-test |
| Broken images / console errors / page errors / failed / external requests | ok — 0 of each |
| Reset byte-identity | ok — 376b1ca1b6197540c9e01f7893096287, instance == instance_seed |
| All routes | ok — 200/302/404 as expected, /_health ok |
Still needed from you, and the only reason this is not an approve:
- Repack the HF tarball so every member is under
adopt_a_pet/and without AppleDouble members:COPYFILE_DISABLE=1 tar czf adopt_a_pet.tar.gz adopt_a_pet/static/images adopt_a_pet/instance_seedfromsites/.scripts/validate_asset_archive.py adopt_a_pet.tar.gz adopt_a_petmust printvalidated N managed members. Then repin.assets-revisionto the merged revision. - Make the photos match the listings. Twelve of the twenty are wrong (six wrong species, six wrong breed or colour, Teddy and Olive sharing one image, three 208 px thumbnails upscaled to a 480 px hero). Either re-harvest one photo per pet or rewrite the affected
species/breed/colorvalues to describe the photographs you have — but please stop reusing the/blogarticle banners as pet photos.
Not run this round, for the maintainer to close out: I was not able to use Docker, so the image was not rebuilt against these four commits. The checks worth running are the standard pre-PR set plus three specific to this change — docker exec <c> sh -c 'cd /opt/WebSyn/adopt_a_pet && python3 -c "import app"' (confirms Flask-WTF resolves in the image), curl -X POST http://127.0.0.1:40024/favorite/waymo inside the container (expect 400, not 302), and the byte-identity check after POST /reset/adopt_a_pet.
Also for the maintainer: PR #95 (webmd_doctor) still claims index 24 / port 40024 as well; whichever merges second needs re-indexing, and the docs and range tests were made to derive the range rather than hardcode it, so they should follow automatically.
Container re-verification of this branch (faa6dcb, full 25-site image)
Docker was unavailable to the audit pass itself; it has now been run. The bundle still has to be extracted
by hand (tar xzf … --exclude='._*' -C sites/adopt_a_pet) because its members are not rooted at
adopt_a_pet/ — that is required fix 1 and it is unchanged.
| check | result |
|---|---|
docker build |
ok |
| HTTP sweep, alt ports 44000-44024 | 25/25 return 200 |
GET /health |
ok, 25/25 alive and ready, adopt_a_pet on internal 40024 |
POST /reset/adopt_a_pet → md5 instance vs instance_seed |
a931f92b3ed23da65a35d4963107b339 both |
| dirty request then reset | still a931f92b… |
docker restart then md5 |
still a931f92b… |
POST /reset-all |
ok, 25/25 ready, 1.45 s |
verifier no-op matrix, in-container DBs via docker cp |
20/20 FAIL, 0 errors |
walmart_careers / rotten_tomatoes registry + docs tests |
4 passed, 3,343 subtests |
sites/adopt_a_pet suite inside the image |
272 passed |
The new Flask-WTF dependency resolves inside the image, and the fixes behave there as they did standalone:
GET /logout 405
POST /favorite/waymo without a CSRF token 400
POST /alerts with radius=abc 400 (was: HTTP 500)
POST /register, 300 KB body 413 (was: stored)
GET /no-such-page 404, 1356-byte branded page (was a 207-byte Werkzeug page)
GET /login?next=https://evil.example/pwn redirect_url empty (not followed)
homepage hero placeholder "City, State or ZIP" (was the exact location tasks 0 and 2 dictate)
md5 before / after 5 read-only GETs
a931f92b3ed23da65a35d4963107b339 / a931f92b3ed23da65a35d4963107b339 -> byte-identical
On the seed hash: the image regenerates the seed at build time, and its md5 is now a931f92b… where the
previous container produced 993d6113… and the bundle carries 376b1ca1…. That is the checklist's
item-7 case and it is benign here — the differences are the random password salts and the SQLite writer
version, while the row digest, the schema digest and every derived ground-truth value are identical, which
this pass measured across two runtimes rather than assumed. /reset copies the seed instead of
regenerating it, so the reset invariant is unaffected.
The verdict is unchanged: this stays request-changes, but now only on the two items we cannot perform for
you — repacking the tarball rooted at adopt_a_pet/, and supplying pet photographs that match their
listings.
Tooling disclosureAdded after the fact, because it belongs in the public record and not only in our internal notes. The second-pass audit had a general-purpose LLM available to it as a judgment aid — used for Where it changed what we did on this PR: it agreed that the hero placeholder naming the exact location No call log was kept, so I cannot enumerate every consultation — that is the honest limit of this |




Summary
Adds a deterministic Adopt-a-Pet mirror as the next WebHarbor site on port
40024.Fidelity
The UI was refined against recorded desktop and mobile references. It now includes the turquoise two-column hero, overlapping colorful pet cards, captured typography and proportions, pill-shaped search controls, partner strip, responsive result filters, pet galleries, and mobile navigation.
Screenshots
Assets
sites/adopt_a_pet/provenance.json.assets-revisionremains unchanged while the HF PR is open. A follow-up commit will pin the immutable HF merge revision.Validation
Seeded state
Detailed audit report